Views: 943
Number of votes: 8
Average rating:

Language Manager: Replace Content with Unpublished Version

When using Language Manager (LM) to duplicate content from other language. It always get content from Published or Common Draft version. There is a customer they want to get content from Ready To Publish version instead. To change the behavior we can intercept the LanguageBranchManager service. In IConfigurableModule you add following line:

public void ConfigureContainer(ServiceConfigurationContext context)
{
    context.Services.Intercept<ILanguageBranchManager>((locator, defaultManager) => new MyLanguageBranchManager(defaultManager));
}

And implment the MyLanguageBranchManager, most of methods we should forward to original service to process. We just modify the [CopyDataFromMasterBranch] method for copying content from Ready To Publish version instead of Published/CommonDraft one. Below is the code for doing this:

public bool CopyDataFromMasterBranch(ContentReference contentReference, string fromLanguageID, string toLanguageID, Func<object, object> transformOnCopyingValue, out ContentReference createdContentLink, bool autoPublish = false)
{
    fromLanguageID = fromLanguageID.Trim();
    toLanguageID = toLanguageID.Trim();

    createdContentLink = null;

    // get ReadyToPublish version here to process copying, fallback to Published or CommonDraft version
    var masterContent = GetReadyForPublishVersion(contentReference, fromLanguageID) ?? GetPublishedOrCommonDraftVersion(contentReference, fromLanguageID);
    var destContent = GetPublishedOrCommonDraftVersion(contentReference, toLanguageID);

    if (masterContent == null)
    {
        throw new ContentNotFoundException(contentReference);
    }

    if (destContent == null)
    {
        CreateLanguageBranch(contentReference, toLanguageID, out createdContentLink);
    }
    var createdDestContent = contentRepository.Service.Get<IContent>(contentReference.ToReferenceWithoutVersion(), new LanguageSelector(toLanguageID));
    createdDestContent = (createdDestContent as IReadOnly).CreateWritableClone() as IContent;

    #region process for Name, PageURLSegment property of both page and block
    if (transformOnCopyingValue == null)
    {
        createdDestContent.Name = masterContent.Name;
    }
    else
    {
        createdDestContent.Name = transformOnCopyingValue(masterContent.Name) as string;
        if (masterContent.Property["PageURLSegment"] != null)
        {
            string url = transformOnCopyingValue(masterContent.Property["PageURLSegment"].ToWebString().Replace('-', ' ')) as string;   /* the result from Bing */
            url = Regex.Replace(url, @"\s+", " ", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant).Trim(); // convert multiple spaces into one space
            url = Regex.Replace(url, @"\s", "-", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); // Replace spaces by dashes
            url = Regex.Replace(url, @"[^a-z0-9~_\-\.]", matchEvaluatorRandomReplace, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); // Remove all non valid chars
            createdDestContent.Property["PageURLSegment"].Value = url;
        }
    }
    #endregion

    #region process all LanguageSpecific, non-meta, editable properties

    foreach (PropertyData destProp in createdDestContent.Property.Where(pd =>
        (pd.IsLanguageSpecific && !pd.IsMetaData && !pd.IsReadOnly) // find all non-metadata properties, which is editable, language specific
        ))
    {
        var nestedContentData = destProp.Value as IContentData;     // block
        if (null != nestedContentData) // if is block, ...
        {
            CopyDataForNestedContentRecursive(masterContent.Property[destProp.Name] as IContentData, nestedContentData, transformOnCopyingValue);
        }
        else
        {
            // some data type and its property type (e.g. CategoryList & PropertyCategory) implements IModifiedTrackable and then a call to IsModified 
            // (leading to IReadOnly.ThrowIfReadOnly) from ContentRepository.Save below, so that must use WritableClone value
            var srcPropValue = masterContent.Property[destProp.Name].Value;
            destProp.Value = srcPropValue is IReadOnly ? ((IReadOnly)srcPropValue).CreateWritableClone() : srcPropValue;
            if (transformOnCopyingValue != null && destProp.Value != null)
            {
                var translatedText = transformOnCopyingValue(destProp);
                TryToAssignStringToProperty(translatedText, destProp);
            }
        }
    }   // end foreach prop in page level

    #endregion

    // skip validation, because translate might be failed sometime.
    var saveFlag = SaveAction.Save | SaveAction.SkipValidation;
    saveFlag = (createdDestContent as IVersionable).Status == VersionStatus.Published ?
        saveFlag | SaveAction.ForceNewVersion : saveFlag | SaveAction.ForceCurrentVersion;
    if (autoPublish)
    {
        saveFlag = saveFlag | SaveAction.Publish;
    }

    createdContentLink = contentRepository.Service.Save(createdDestContent, saveFlag, AccessLevel.NoAccess);
    contentVersionRepository.Service.SetCommonDraft(createdContentLink);

    return true;

    // return _defaultLanguageBranchManager.CopyDataFromMasterBranch(contentReference, fromLanguageID, toLanguageID, transformOnCopyingValue, out createdContentLink, autoPublish);
}

private IContent GetReadyForPublishVersion(ContentReference contentLink, string languageID)
{
    var contentVersionRepository = ServiceLocator.Current.GetInstance<IContentVersionRepository>();
    var versions = contentVersionRepository.List(contentLink, languageID);
    var readyToPublishVersion = versions.FirstOrDefault(v => v.Status == VersionStatus.CheckedIn);

    var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
    if (readyToPublishVersion != null)
    {
        return contentRepository.Get<IContent>(readyToPublishVersion.ContentLink);
    }

    return null;
}

/// <summary>
/// Copies the data for nested content recursively.
/// </summary>
/// <param name="sourceContentData">The source content data.</param>
/// <param name="targetContentData">The target content data.</param>
/// <param name="transformOnCopyingValue">perform a transformation on copying property's value</param>
private void CopyDataForNestedContentRecursive(IContentData sourceContentData, IContentData targetContentData, Func<object, object> transformOnCopyingValue)
{
    using (IEnumerator<PropertyData> propertyDataEnumerator = targetContentData.Property
        .Where(pd => pd.IsLanguageSpecific && !pd.IsMetaData && !pd.IsReadOnly)
        .GetEnumerator())
    {
        while (propertyDataEnumerator.MoveNext())
        {
            var targetProp = propertyDataEnumerator.Current;
            if (targetProp is IContentData)
            {
                CopyDataForNestedContentRecursive(sourceContentData.Property[targetProp.Name] as IContentData, targetProp as IContentData, transformOnCopyingValue);
            }
            else
            {
                // some data type and its property type (e.g. CategoryList & PropertyCategory) implements IModifiedTrackable and then a call to IsModified 
                // (leading to IReadOnly.ThrowIfReadOnly) from ContentRepository.Save below, so that must use WritableClone value
                var srcPropValue = sourceContentData.Property[targetProp.Name].Value;
                targetProp.Value = srcPropValue is IReadOnly ? ((IReadOnly)srcPropValue).CreateWritableClone() : srcPropValue;
                if (transformOnCopyingValue != null)
                {
                    var translatedText = transformOnCopyingValue(targetProp);
                    TryToAssignStringToProperty(translatedText, targetProp);
                }
            }
        }
    }
}

/// <summary>
/// assign <paramref name="obj"/> to <paramref name="prop"/> might lead to exception because <paramref name="prop"/> cannot accept string >255.
/// We try to shorten it before assigning again.
/// </summary>
/// <param name="obj"></param>
/// <param name="prop"></param>
/// <returns></returns>
private void TryToAssignStringToProperty(object obj, PropertyData prop)
{
    try
    {
        prop.Value = obj;
    }
    catch (EPiServerException ex)
    {
        if (ex.Message.Contains("exceeded"))    // exceeded 255 characters
        {
            prop.Value = ((string)obj).Substring(0, 255);
        }
        else
        {
            throw;
        }
    }
}

If you have similar requirement for your site. I hope this will help you a litle bit.

Oct 30, 2018

valdis
( By valdis, 10/30/2018 12:50:11 PM)

just have couple of my 2 cents:

- why ServiceLocator?

- why ".GetEnumerator()" & "while(..MoveNext())"

- if method starts with "Try.." I would not expect to receive Exception back, bot bool instead

- also I would try to split code into smaller chunks to organize this quite complex code

Please login to comment.