4.3. Customizing Publishing Behavior with copyProperty

The default publishing rules for publishing associations was described in Figure 4-1. By default, scalar attributes are simply copied by value. For any object property, whether scalar attribute or association, custom behavior is sometimes desired. Some properties may not need to be copied at all. Others may need special treatment. This is handled by implementing CustomCopy.copyProperty(CustomCopy, Property, ItemCopier)[1].

When VersionCopier copies any property, whether it's an association or a scalar attribute, it first calls copyProperty(DomainObject, DomainObject, Property). If the object being copied implements CustomCopy, then the VersionCopier first calls copyProperty(CustomCopy, Property, ItemCopier) on the target object. This target object is the new copy that this property is being transferred to.

If copyProperty returns a value of false, then the copy process continues according to the default rules. If copyProperty returns a value of true, indicating that it has already handled the property in question, then the copier takes no further action on this property.

The CustomCopy interface defines a single method of copyProperty(CustomCopy, Property, ItemCopier). ContentItem already implements CustomCopy, making it unnecessary to explicitly implement CustomCopy when adding custom behavior to attributes of a subclass of ContentItem. To implement the custom behavior, add a copyProperty, and in the case of any attributes which are not handled explicitly by your overridden method, simply return super.copyProperty.

4.3.1. copyProperty Use Cases

When implementing copyProperty there are several cases to consider:

  1. Use case: The property should be copied as a normal component, not needing any special treatment, or the particular implementation class knows nothing about the property, as in the case where the property is defined in the superclass.

    In this case, if this class does not extend a class which implements copyProperty, simply return false. If the parent class does implement copyProperty, return super.copyProperty.

  2. Use case: The property is a read-only property, a redundant access point to a field which is controlled by another named property (possibly left for backwards compatibility, a property which is automatically set elsewhere in the domain logic, or a property which for some other reason should not be copied to the published item.)

    In this case, return true.

  3. Use case: The property requires custom code to handle the copy.

    In this case, perform the custom copy operation and return true. Within copyProperty, this is the new (pending) object, so the property needs to be copied to this from the passed-in srcItem. For a scalar attribute which requires some modification before setting it in the new copy, you need to grab the value from srcItem, modify it as required, and set the attribute on this.

    If the property is an association where an otherwise-normal copy needs minor changes before setting this association in the copy, you will need to retrieve the copy (targetComponent) from the passed-in copier by calling copy(srcItem, this, srcComponent, property) on it, make any required modifications to targetComponent, and then set this property on the target copy (this).

    CautionCaution
     

    If the property in question is one which would by default result in deferred associations being created, then the return value of the copy(srcItem, this, srcComponent, property) call will be null. In addition, if such an association is handled in copyProperty without calling copy on the ItemCopier, then the anticipated PublishedLink will never be created, effectively preventing automatic update of this association when the target item is republished.

    This means that you should not try to handle top-level item associations entirely within copyProperty. Partial handling of these cases via copyProperty to deal with, for example, auto-publishing of related items, is supported, and described in the next use case.

  4. Use case: The property requires custom code to handle some portion of the copy process, but after taking certain actions, the VersionCopier should take over, performing the usual publishing actions on the property.

    In this case, perform the required custom steps and then return false. One major use case for this is to take care of situations where the target of a top-level association needs to be auto-published if it is not yet live, but once that action has occurred, the association needs to be treated like a normal item association with automatic updating via the PublishedLink infrastructure.

4.3.2. copyProperty Examples

Here are a couple of examples which illustrate each of the four use cases defined in Section 4.3.1 copyProperty Use Cases. Example 4-1 shows the copyProperty method in the current version of ContentItem:

public boolean copyProperty(final CustomCopy source,
                            final Property property,
                            final ItemCopier copier) {
    String attribute = property.getName();
    if (CHILDREN.equals(attribute)) {
        return true;
    }

    // Ignore live and pending versions.
    if (VERSIONS.equals(attribute)) {
        return true;
    }

    // Don't copy path denormalization.
    if (ANCESTORS.equals(attribute)) {
        return true;
    }

    if ("categories".equals(attribute)) {
        return true;
    }

    // If live Bundle already exists, recategorize.
    if (PARENT.equals(attribute)) {
        ACSObject parent = ((ContentItem)source).getParent();
        if (parent != null && copier.getCopyType() == ItemCopier.\
VERSION_COPY) {
            if (parent instanceof ContentBundle) {
            

                ContentBundle bundle = (ContentBundle) parent;
                ContentBundle liveBundle = (ContentBundle) bundle.\
getPublicVersion();
                if (liveBundle == null) {
                    liveBundle = (ContentBundle) bundle.\
createPendingVersion(null);
                } else {
                    Iterator liveCategories = liveBundle.getCategories();
                    Iterator draftCategories = bundle.getCategories();
                    while (liveCategories.hasNext()) {
                        Category cat = (Category) liveCategories.next();
                        liveBundle.removeCategory(cat);
                    }
                    while (draftCategories.hasNext()) {
                        Category cat = (Category) draftCategories.next();
                        liveBundle.addCategory(cat);
                    }
                }
                setBundle(liveBundle);
                return true;
            } else if (parent instanceof Folder) {
                Folder folder = (Folder) parent;
                Folder liveFolder = (Folder) folder.getLiveVersion();
                if (liveFolder == null) {
                    liveFolder = (Folder) folder.createLiveVersion();
                }
                setParent(liveFolder);
                return true;            
            }
        } 
    }

    return false;
}

Example 4-1. copyProperty in ContentItem

At the beginning of the method there are four instances of the second use case. In all four cases (CHILDREN, VERSIONS, ANCESTORS, and "categories"), the attributes are handled explicitly by other parts of the publishing infrastructure. Because of this, we don't want the metadata-based copying implementation in VersionCopier to attempt to handle these properties. The method returns true and takes no further action.

The other attribute which ContentItem.copyProperty concerns itself with is PARENT. For top-level items, this will generally be the containing ContentBundle. For a ContentBundle, this will be the containing Folder. Special handling is only needed here if the parent is either a ContentBundle or a Folder. The reason is when an item is published, the ContentBundle or Folder will automatically be published, the new pending item will be placed in a new pending ContentBundle, and the new pending bundle will be placed in a new live Folder.

Another condition for special handling is that the copy type of ItemCopier is VERSION_COPY, meaning that the copier is an instance of VersionCopier. The reason for this is that the same basic copying infrastructure, including copyProperty, is used for both publishing and regular copying, as when a new language instance is created for an existing item, or when an item is copied from one folder to another. The publishing of the parent item is only desired for publish operations, but not for other copy operations.

If the parent is a ContentBundle, there are three actions taken:

  1. If the bundle is not yet published, which is the case if this is the first language instance in the bundle to be published, the bundle is published. The new pending/live item must have a pending or live bundle as a parent. This new bundle will remain pending until its first child item is made live, in which case the bundle will be made live at that time as well.

  2. Because categories are applied to bundles rather than item instances, the categories associated with the live bundle must be copied from the categories on the draft bundle when a language instance is published. This is done even if the bundle is already live, so that categorization changes since the last bundle update will take effect on the live item.

  3. The PARENT attribute of the new pending item is set to the published ContentBundle, which may or may not have just been created in the first step. copyProperty now returns true.

If the parent is a Folder, there are actions taken:

  1. If the Folder is not yet published (which would be the case if this is the first item/bundle in the Folder to be published), the Folder is published. The new pending/live item must have a live Folder as a parent. Unlike other items, a Folder will be made live immediately, skipping the pending stage entirely.

  2. The PARENT attribute of the new pending item is set to the published Folder (which may or may not have just been created in the first step). copyProperty now returns true.

When an item several subfolders down is initially published, this will result in a cascading process, with the item, bundle, and all ancestor Folders being published all at once. Both of these special publishing cases involving the PARENT are examples of the third copyProperty use case. Any remaining properties not handled at this point, i.e., the return value is false, illustrate the first copyProperty use case.

In Example 4-2, you use the copyProperty method in the current version of ArticleImageAssociation:

public boolean copyProperty(final CustomCopy source,
                            final Property property,
                            final ItemCopier copier) {
    String attribute = property.getName();
    // don't copy these attributes. 
    // they're controlled by explicit associations
    if (IMAGE_ID.equals(attribute) || ARTICLE_ID.equals(attribute)) {
        return true;
    }

    if (copier.getCopyType() == ItemCopier.VERSION_COPY
        && IMAGE.equals(attribute)) {
        ImageAsset image = ((ArticleImageAssociation)source).getImage();
        if (image != null) {
            ImageAsset liveImage = (ImageAsset) image.getLiveVersion();
            if (liveImage == null) {
                liveImage = (ImageAsset) image.createLiveVersion();
            }
        }
        // This method only makes sure that the ReusableImageAsset
        // is auto-published. It still returns  false so that the
        // copier can generate PublishedLink objects appropriately.
        return false;
    }
    return super.copyProperty(source, property, copier);
}

Example 4-2. copyProperty in ArticleImageAssociation

As with Example 4-1, there are some instances of the second use case. Here, the IMAGE_ID and ARTICLE_ID attributes are ignored. They are only here for backwards compatibility, as the references to the article and image are handled by explicit associations. The method returns true and takes no further action.

The final use case is shown for the IMAGE property when the copy type is VERSION_COPY. In this case, the following actions are taken:

  1. If the image is not yet published (which would be the case if this is the first article to be published which uses this image), the image is published and made live immediately.

  2. Unlike the PARENT case, here the return value is false, because once the image is published, we want the copier to manage the association via PublishedLink, as would be the default handling. The only reason we need this copyProperty handling is that with this version of the publishing system, we have to explicitly auto-publish top-level associated items which need to go live along with the source of the link.

In all other cases, the return value is super.copyProperty(source, property, copier). Because ArticleImageAssociation extends a class which implements copyProperty, the superclass copyProperty implementation needs a chance to process the property.

Notes

[1]

http://rhea.redhat.com/documentation/api/ccm-cms-6.1.0/com/arsdigita/cms/CustomCopy.html#\ copyProperty(com.arsdigita.cms.CustomCopy,com.arsdigita.persistence.metadata.Property,\ com.arsdigita.cms.ItemCopier