Copying version tree

Some time ago I implemented a simple module that looks for business users as a document with multiple attachments:

To implement such functionality I chose relations:

Why relations? Documentum has only three options to implement such thing:

  • Pages in sysobject – all pages must have the same content type, no option to make a version of separate page
  • Virtual documents – I hate this option because it’s required to checkout objects before modifying structure of virtual document, moreover virtual documents are not trivial for inexperienced user
  • Pure relations

The problems started when the customer asked to implement support versioning of primary document. Why is it a challenge to implement such requirements?

  • Multiple versions of primary document may have different ACLs, so attachments should somehow inherit those ACLs
  • When user “unlinks” attachment from primary document’s version this action must not affect other versions
  • User may potentially delete attachment using some unforeseen actions

To resolve all potential issues I decided to make a copy of whole attachment’s version tree upon checkin of primary document. And what have I found? DFC does not know how to copy version tree!

After some research I have found out that to reproduce document’s version tree it’s enough to do following:

  • copy all documents using ISysObject.saveAsNew() method – this method allows to not copy relations and contents
  • synchronize following attributes with previous version tree: i_chronicle_id, i_antecedent_id, i_has_folder, i_latest_flag, i_branch_cnt, r_version_label and i_direct_dsc – the problem here is all these attributes are “internal”

PoC:

/**
 * @author Andrey B. Panfilov <andrew@panfilov.tel>
 */
public final class VersionTreeCopier {

    public static final String[] VERSION_ATTRS = new String[] {
        DfDocbaseConstants.I_CHRONICLE_ID, "i_antecedent_id", "i_has_folder",
        "i_latest_flag", DfDocbaseConstants.R_VERSION_LABEL, "i_branch_cnt",
        "i_direct_dsc", };

    public static Map<IDfId, IDfId> copy(IDfSession session, IDfId objectId,
            boolean shareContent, boolean copyRelations,
            boolean keepStorageAreasForRenditions) throws DfException {
        IDfSysObject object = (IDfSysObject) session.getObject(objectId);
        Map<IDfId, IDfId> old2new = new HashMap<IDfId, IDfId>();
        List<IDfSysObject> oldDocs = getDocumentsTree(object);
        List<IDfSysObject> newDocs = new ArrayList<IDfSysObject>();
        for (IDfSysObject doc : oldDocs) {
            IDfId copyId = ((ISysObject) doc).saveAsNew(shareContent,
                    copyRelations, keepStorageAreasForRenditions, session);
            old2new.put(doc.getObjectId(), copyId);
            newDocs.add((IDfSysObject) session.getObject(copyId));
        }

        for (int i = 0, n = newDocs.size(); i < n; i++) {
            IDfSysObject copy = newDocs.get(i);
            IDfSysObject old = oldDocs.get(i);
            copyAttributes(old, copy, old2new, VERSION_ATTRS);
            copy.save();
        }

        return old2new;
    }

    public static void copyAttributes(IDfPersistentObject from,
            IDfPersistentObject to, Map<IDfId, IDfId> old2new,
            String... attrNames) throws DfException {
        if (attrNames == null || attrNames.length == 0) {
            return;
        }
        IDfUser user = from.getSession().getUser(null);
        try {
            // dirty hack, must be replaced by persistent object wrapper
            // with calling smth like:
            // Method setTimeInternal = DfTypedObject.class.getDeclaredMethod(
            // "setTimeInternal", String.class, IDfTime.class);
            // setTimeInternal.setAccessible(true);
            // setTimeInternal.invoke(((IPersistentObject)
            // object).getProxyHandler()
            // .____getImp____(), "r_creation_date", new DfTime());
            user.setUserPrivileges(16);
            for (String attrName : attrNames) {
                copyAttributeInternal(from, to, old2new, attrName);
            }
        } finally {
            user.revert();
        }
    }

    private static void copyAttributeInternal(IDfPersistentObject from,
            IDfPersistentObject to, Map<IDfId, IDfId> old2new, String attrName)
        throws DfException {
        if (!from.hasAttr(attrName)) {
            return;
        }
        if (!to.hasAttr(attrName)) {
            return;
        }
        if (from.isAttrRepeating(attrName) != to.isAttrRepeating(attrName)) {
            throw new DfException("Unable to copy attribute " + attrName
                    + ", repeating mismatch");
        }
        if (from.getAttrDataType(attrName) != to.getAttrDataType(attrName)) {
            throw new DfException("Unable to copy attribute " + attrName
                    + ", type mismatch");
        }

        if (from.isAttrRepeating(attrName)) {
            to.removeAll(attrName);
            for (int i = 0, n = from.getValueCount(attrName); i < n; i++) {
                to.appendValue(attrName,
                        convert(from.getRepeatingValue(attrName, i), old2new));
            }
        } else {
            to.setValue(attrName, convert(from.getValue(attrName), old2new));
        }
    }

    public static IDfValue convert(IDfValue oldValue, Map<IDfId, IDfId> old2new)
        throws DfException {
        if (old2new == null) {
            return oldValue;
        }
        if (!(oldValue.getDataType() == IDfAttr.DM_ID)) {
            return oldValue;
        }
        IDfId id = oldValue.asId();
        if (old2new.containsKey(id)) {
            id = old2new.get(id);
        } else {
            id = DfId.DF_NULLID;
        }
        return new DfValue(id, IDfAttr.DM_ID);
    }

    public static List<IDfSysObject> getDocumentsTree(IDfSysObject document)
        throws DfException {
        final List<IDfSysObject> documents = new ArrayList<IDfSysObject>();
        final IDfSession session = document.getObjectSession();
        for (DfPair<String, String> versionInfo : getVersionTree(document)) {
            IDfSysObject object = (IDfSysObject) session.getObject(ClientXUtils
                    .getId(versionInfo.getFirst()));
            documents.add(object);
        }
        return documents;
    }

    private static List<DfPair<String, String>> getVersionTree(
            IDfSysObject document) throws DfException {
        final List<DfPair<String, String>> tree = new ArrayList<DfPair<String, String>>();
        IDfQuery query = new DfQuery(getTreeQuery(document));
        IDfCollection coll = null;
        try {
            coll = query.execute(document.getSession(), IDfQuery.DF_EXEC_QUERY);
            while (coll.next()) {
                String version = coll
                        .getString(DfDocbaseConstants.R_VERSION_LABEL);
                String objectId = coll
                        .getString(DfDocbaseConstants.R_OBJECT_ID);
                tree.add(new DfPair<String, String>(objectId, version));
            }
        } finally {
            if (coll != null
                    && coll.getState() != IDfCollection.DF_CLOSED_STATE) {
                coll.close();
            }
        }
        Collections.sort(tree, new VersionComparator());
        return tree;
    }

    private static String getTreeQuery(IDfSysObject document)
        throws DfException {
        StringBuilder queryBuilder = new StringBuilder();
        queryBuilder.append("SELECT r_object_id, r_version_label FROM ")
                .append(document.getTypeName()).append("(ALL) WHERE ");
        queryBuilder.append(" i_chronicle_id='")
                .append(document.getChronicleId().getId()).append("'");
        queryBuilder.append(" AND i_position=-1");
        queryBuilder.append(" ENABLE(ROW_BASED)");
        return queryBuilder.toString();
    }

    static class VersionComparator implements
            Comparator<DfPair<String, String>> {

        @Override
        public int compare(DfPair<String, String> first,
                DfPair<String, String> second) {

            // null checks are skipped

            String[] firstParts = first.getSecond().split("\\.");
            String[] secondParts = second.getSecond().split("\\.");

            int length = Math.max(firstParts.length, secondParts.length);
            for (int i = 0; i < length; i++) {
                int firstPart;
                if (i < firstParts.length) {
                    firstPart = Integer.parseInt(firstParts[i]);
                } else {
                    firstPart = 0;
                }
                int secondPart;
                if (i < secondParts.length) {
                    secondPart = Integer.parseInt(secondParts[i]);
                } else {
                    secondPart = 0;
                }
                if (firstPart < secondPart) {
                    return -1;
                }
                if (firstPart > secondPart) {
                    return 1;
                }
            }
            return 0;
        }

    }

}

2 thoughts on “Copying version tree

  1. Pingback: Copying version tree. Part II | Documentum in a (nuts)HELL

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s