Pages

Friday, December 30, 2016

Documentum D2 listener plugin examples

Overview

D2 is the most popular client web application for Documentum platform. However, its users often need to customize its functionalities, which can be easily achieved using custom D2 listener plugins written in Java. For example, plugins could automatically modify, fill or restore some attributes of the selected objects, link them to some folders based on attribute values, or send emails to reviewers.

Whenever some menu item is clicked, the supporting D2 service is invoked in the backend. Let's consider several distinct menu items: View, Paste and Properties.

In D2 config we can see that clicking View translates into publishing action D2_ACTION_CONTENT_VIEW:

Paste item produces event D2_ACTION_PASTE:

Lastly, Properties option calls PropertiesDialog popup:

When one needs to modify some D2 functionality, one can perform the action in D2 user interface and then look into the D2 log to see what service class has been invoked. Plugins override existing services classes. Often D2 service classes have many methods, so additionally the name of the invoked method has to be recovered from the log. Note, invocation of services are logged at DEBUG logging level.

When View is clicked getDownloadUrls method of D2DownloadService is invoked.

When Properties is click getDialog of D2DialogService is invoked. Note, after you click OK in the Properties page popup, D2PropertyService service will be invoked to handle the introduced modifications.

When Paste menu item is selected, copy methods of D2MoveService is invoked.

As you see, regardless of the immediate settings in D2 config, eventually every activity is mediated by D2 service classes.

Plugin classes are complied into a jar file that is placed into D2/WEB-INF/lib directory or anywhere on the classpath. As it is evident from the log, when a service is invoked, D2 class com.emc.d2fs.dctm.aspects.InjectSessionAspect searches the classpath for the plugins overriding the service. If any overriding plugin is found, it is invoked instead of the native service method. Plugin classes are recognized by two distinctive features. All listener plugin classes implement interface ID2fsPlugin, and their class names are composed of the name of the target service class that is concataneted to the keywork Plugin.

The plugins that I developed or upgraded usually execute some code before calling the overridden service, call the service, and then execute again some custom code that sometimes uses the result of the native service. There is a difference between D2 3.1 and D2 4.2 plugins. In D2 4.2 and 4.5 plugins do not have onBefore and onAfter methods. I replace them with custom methods before and after. I illustrated this in the comments in the simplified code below. The examples contains some explanation in comments.

Below I describe several plugins:

Life Cycle Service Plugin

I start from D2 4.5 plugin that is invoked when a user changes lifecyle state of an object.

public class D2LifeCycleServicePlugin extends D2LifeCycleService implements ID2fsPlugin {

    // just for visualizing input arguments and debugging
    void printAttributes(List<Attribute> parameters) {

        for (Attribute a : parameters) {
            System.out.println("attrName/value: " + a.getName() + " " + a.getValue());
        }
    }

    // just for visualizing input arguments and debugging
    void printParameters(D2fsContext d2fsContext) throws DfException, D2fsException {
        ParameterParser d2parameterparser = d2fsContext.getParameterParser();

        for (Attribute a : d2parameterparser.getParameters()) {
            System.out.println("paramName/value: " + a.getName() + " " + a.getValue());
        }
    }

    @Override
    public LifeCycleResult changeState(Context context, String docId, String targetState, String event, String operation, List<Attribute> parameters) throws Exception {
        System.out.println(">D2LifeCycleServicePlugin:changeState: docId=" + docId + "; targetState=" + targetState + "; event=" + event + "; operation=" + operation);
        // just for visualizing input arguments and debugging
        printAttributes(parameters);

        //D2fsContext contains current user and admin user sessions and lots of other values
        D2fsContext d2fsContext = (D2fsContext) context;
        // just for visualizing input arguments and debugging
        printParameters(d2fsContext);
        
        // execute custom code before executing the native code
        before(d2fsContext, docId, event);
        
        // call the native method
        LifeCycleResult result = super.changeState(context, docId, targetState, event, operation, parameters);

        // execute custom code before executing the native code, you can modify the returned value here
        after(d2fsContext, docId, event, targetState);

        return result;
    }

    // execute custom code before executing the native code
    public void before(D2fsContext d2context, String objectId, String event) throws DfException, D2fsException {
        IDfSession session = d2context.getSession();
        IDfSession adminSession = d2context.getAdminSession();
        IDfSysObject object = (IDfSysObject) session.getObject(new DfId(objectId));
        IDfSysObject adminObj = (IDfSysObject) adminSession.getObject(object.getObjectId());
    }

    // execute custom code before executing the native code, you can modify the returned value here
    public void after(D2fsContext d2context, String objectId, String event, String targetState) {
    }

    // two methods producing the plugin info that is shown in D2 About menu when plugin is installed
    @Override
    public String getFullName() {
        return new PluginVersion().getFullName();
    }

    @Override
    public String getProductName() {
        return new PluginVersion().getProductName();
    }
}
Dialog service plugin (mass update)

There are many dialogs in D2. They are supported by dialog service. The D2 4.5 listener plugin example below shows how to apply custom modifications to the selected objects specifically after Mass Update dialog has been invoked from the context menu.

public class D2DialogServicePlugin extends D2DialogService implements ID2fsPlugin {

    @Override
    public Dialog validDialog(Context context, String id, String dialogName, List<Attribute> parameters) throws Exception {
        Dialog result;

        if (dialogName.equals("MassUpdateDialog")) {
            // Mass Update dialog
            D2fsContext d2fsContext = (D2fsContext) context;
            before(d2fsContext);
            // call the native method
            result = super.validDialog(context, id, dialogName, parameters);
            after(d2fsContext);
        } else {
            // not Mass Update dialog, do nothing except calling the native method
            result = super.validDialog(context, id, dialogName, parameters);
        }

        return result;
    }

    public void before(D2fsContext d2context) throws D2fsException, DfException {
        ParameterParser d2parameterparser = d2context.getParameterParser();

        // Verify the mass update configuration name
        if (d2parameterparser.hasParameter("config_name")) {
            if (d2parameterparser.getStringParameter("config_name").equals("Distribution list")) {
                // get selected objects
                for (int i = 0; i < d2context.getObjectCount(); i++) {
                    IDfSysObject bisObject = (IDfSysObject) d2context.getObject(i);
                    // do something special to each selected object
                }
            }
        }
    }

    public void after(D2fsContext d2context) throws Exception {
        IDfSession session = d2context.getSession();
        ParameterParser d2parameterparser = d2context.getParameterParser();
        // do something to selected objects as in methods before
    }

    @Override
    public String getFullName() {
        return new PluginVersion().getFullName();
    }

    @Override
    public String getProductName() {
        return new PluginVersion().getProductName();
    }
}
Creation service plugin

The most common type of D2 plugins are plugins modifying the properties of newly created objects. For example, values of some attributes can be filtered or somehow modified, some attributes can be assigned some values and the object can be linked to some particular folders based on some attribute values and emails can be sent to some reviewers.

Below is the example of Creation service listener plugin that will work with D2 4.2 and 4.5.

public class D2CreationServicePlugin extends D2CreationService implements ID2fsPlugin {

    @Override
    public String createProperties(Context context, List<Attribute> parameters) throws Exception {
        D2fsContext d2fsContext = (D2fsContext) context;
        Map<String, String> attributeMap = new HashMap<>();
        for (Attribute a : parameters) {
            String name = a.getName();
            String val = a.getValue();
            attributeMap.put(name, val);
            // one can modify the attributes of the object to be created
            // for example, remove commas in repeating attribute authors
            if (name.equals("authors")) {
                String[] vals = val.split(AttributeUtils.SEPARATOR_VALUE);
                for (int i = 0; i < vals.length; i++) {
                    vals[i] = vals[i].replace(",", "");
                }
                a.setValue(ArrayUtil.join(vals, AttributeUtils.SEPARATOR_VALUE));
            }
        }
        String objectType = attributeMap.get("r_object_type");
        // execute some logic before creating the object of some particular type
        if (objectType.equals("custom_object_type")) {
            before(d2fsContext, parameters);
        }

        String result = super.createProperties(context, parameters);

        // execute some logic after the object of some particular type has been saved
        if (objectType.equals("custom_object_type")) {
            String objId = extractNewIDFromReturnString(result);
            after(d2fsContext, objId);
        }
        return result;
    }

    // exctact the id of the created object from the result string returned by the service method
    // <success d2_naming_config="false" new_id="080f42418001febd" locate="true"/>
    String extractNewIDFromReturnString(String s) throws DfException {
        String[] values = s.split(" ");
        for (String str : values) {
            if (str.startsWith("new_id")) {
                String newId = str.split("\"")[1];
                return newId;
            }
        }
        throw new DfException("Cannot extract object id");
    }

    // custom code to be executed before the native code 
    void before(D2fsContext d2context, List<Attribute> parameters) throws DfException, D2fsException {
        IDfSession session = d2context.getSession();
        // do something
    }

    // custom code to be executed after the native code 
    void after(D2fsContext d2context, String id) throws D2fsException, DfException {
        IDfSession session = d2context.getSession();
        IDfSysObject obj = (IDfSysObject) session.getObject(new DfId(id));
        // do something
    }

    @Override
    public String getFullName() {
        return new PluginVersion().getFullName();
    }

    @Override
    public String getProductName() {
        return new PluginVersion().getProductName();
    }
}

One important remark, D2CreationService.createProperties method does not link the created object to any folder. The containing folder id is stored in contentId attribute, though. The object is linked much later by D2CreationService.setTemplate method, which first removes all existing links. Before execution of that methods, the object in not linked anywhere except the home folder. So if your plugin uses the parent folder information, you must use the value of contentId parameter.

Property service plugin

Another quite common type of D2 plugins are plugins modifying objects after the object attribute values have been updated in D2 properties widget. For example, values of some attributes can be restored or somehow further modified, some attributes can be assigned some values and the object can be linked to particular folders depending on some attribute values, object life cycle stated can be changed, and emails can be sent to some reviewers.

Below is the example of Property service listener plugin that will work with D2 4.2 and 4.5.

public class D2PropertyServicePlugin extends D2PropertyService implements ID2fsPlugin {

    public XmlNode saveProperties(Context context) throws Exception {
        D2fsContext d2fsContext = (D2fsContext) context;
        ParameterParser d2parameterparser = d2fsContext.getParameterParser();
        String objectId = d2parameterparser.getStringParameter("id");
        IDfSession session = d2fsContext.getSession();
        IDfSysObject obj = (IDfSysObject) session.getObject(new DfId(objectId));
        // apply only to specific target type
        if (obj.getTypeName().equals("target_object_type")) {
            before(d2fsContext, objectId);
        }
        XmlNode r = super.saveProperties(context);
        
          // apply only to specific target type
        if (obj.getTypeName().equals("target_object_type")) {
            after(d2fsContext, objectId);
        }
        return r;
    }

    void before(D2fsContext d2context, String objectId) throws D2fsException, DfException, IOException {
        IDfSession session = d2context.getSession();
        IDfSysObject obj = (IDfSysObject) session.getObject(new DfId(objectId));
        // modify the object before it has been updated, for example backup some attribute values  
        // and then save
        obj.save();
    }

    void after(D2fsContext d2context, String objectId) throws D2WarningException, DfException, D2fsException, IOException, MessagingException {
        IDfSession session = d2context.getSession();
        IDfSysObject obj = (IDfSysObject) session.getObject(new DfId(objectId));
        // modify the object, for example link or fill some attributes
        // and then save
        obj.save();
    }

    @Override
    public String getFullName() {
        return new PluginVersion().getFullName();
    }

    @Override
    public String getProductName() {
        return new PluginVersion().getProductName();
    }
}
Download service plugin (overriding checkin method)

Often when a document has been checked in, some automatic modifications to the object are desired. The plugin could, for example, change life cycle stated of the object and send emails to reviewers.

In D2 4.5 the checkin functionality is mediated by checkin method of Download Service. Note, D2 3.1 and 4.2 have no service method for checkin, this functionality i mediated by com.emc.d2fs.dctm.servlets.upload.Checkin servlet.

The first example that I provide below is of the checkin listener plugin for D2 4.5 and the second for D2 4.2.

The example for D2 4.5:

public class D2DownloadServicePlugin extends D2DownloadService implements ID2fsPlugin {

    @Override
    public String checkin(Context context, String id, File uploadFile, long fileLength, String contentType, String logEntry, String checkinVersionT, boolean makeCurrent, boolean retainLock, boolean keepSymbolicLabel, boolean keepLogEntry, boolean queueRendition, String location, boolean asynchronous, boolean useBocs, Object contentMover) throws Exception {
        D2fsContext d2fsContext = (D2fsContext) context;
        String result = super.checkin(context, id, uploadFile, fileLength, contentType, logEntry, checkinVersionT, makeCurrent, retainLock, keepSymbolicLabel, keepLogEntry, queueRendition, location, asynchronous, useBocs, contentMover);
        after(d2fsContext, result);
        return result;
    }

    private void after(D2fsContext d2context, String objectId) throws DfException, D2fsException {
        IDfSession session = d2context.getSession();

        IDfSysObject obj = (IDfSysObject) session.getObject(new DfId(objectId));

        // apply only objects of specific type
        if (obj.getTypeName().equals("target_object_type")) {
            // modify the object             
        }
    }

    @Override
    public String getFullName() {
        return new PluginVersion().getFullName();
    }

    @Override
    public String getProductName() {
        return new PluginVersion().getProductName();
    }
}

The checkin listener plugin for D2 4.2 is quite different:

public class CheckinListener implements ID2PluginListener, ID2fsPlugin {

    @Override
    public XmlNode onBefore(HttpServletRequest request, HttpServletResponse response, D2HttpContext paramD2HttpContext) throws Exception {
        // do nothing
        return null;
    }

    @Override
    public XmlNode onAfter(HttpServletRequest request, HttpServletResponse response, D2HttpContext d2context, XmlNode xmlNode) throws Exception {

        IDfSession session = d2context.getSession();

        String objectId = xmlNode.getFirstXmlNode("new").getAttribute("id").toString();
        IDfSysObject obj = (IDfSysObject) session.getObject(new DfId(objectId));

        //Specific behavior for C-Sox and for Policies and Procedures
        if ("target_object_type".equals(obj.getTypeName())   {
            // do something, for example send emails
        }
        return xmlNode;
    }

    @Override
    public String getFullName() {
        return new SampleVersion().getFullName();
    }

    @Override
    public String getProductName() {
        return new SampleVersion().getProductName();
    }
}
Move service plugin (copy, cut, paste and link)

Copy, cut, paste and link context menu items are mediated by Move service. If you need to modify automatically the selected objects before or after the operation, you can install a listener plugin. The example for D2 4.5:

public class D2MoveServicePlugin extends D2MoveService implements ID2fsPlugin {

    @Override
    public boolean move(Context context, String targetId, String sourceId, String idChild) throws Exception {
        D2fsContext d2fsContext = (D2fsContext) context;
        boolean result = super.move(context, targetId, sourceId, idChild);
        afterMove(d2fsContext, targetId, idChild);
        return result;
    }

    void afterMove(D2fsContext d2context, String dest_id, String objIds) throws DfException, D2fsException {
        IDfSession session = d2context.getSession();
        String[] ids = objIds.split(AttributeUtils.SEPARATOR_VALUE);
        for (String id : ids) {
            IDfSysObject obj = (IDfSysObject) session.getObject(new DfId(id));
            // apply only to a spcific target type
            if (obj.getTypeName().equals("target_object_type")) {
                // modify the object               
            }
        }
    }

    @Override
    public boolean copy(Context context, String targetId, String idChild) throws Exception {
        D2fsContext d2fsContext = (D2fsContext) context;
        boolean result = super.copy(context, targetId, idChild);
        afterCopy(d2fsContext, targetId, idChild);
        return result;
    }

    void afterCopy(D2fsContext d2context, String folderId, String objIds) throws DfException, D2fsException {
        IDfSession session = d2context.getSession();
        String[] ids = objIds.split(AttributeUtils.SEPARATOR_VALUE);
        for (String id : ids) {
            IDfSysObject obj = (IDfSysObject) session.getObject(new DfId(id));
            // modify the object     
        }
    }

    @Override
    public boolean link(Context context, String targetId, String objIds) throws Exception {
        D2fsContext d2fsContext = (D2fsContext) context;
        boolean result = super.link(context, targetId, objIds);
        afterLink(d2fsContext, targetId, objIds);
        return result;
    }

    void afterLink(D2fsContext d2context, String dest_id, String objIds) throws DfException, D2fsException {
        IDfSession session = d2context.getSession();
        String[] ids = objIds.split(AttributeUtils.SEPARATOR_VALUE);
        for (String id : ids) {
            IDfSysObject obj = (IDfSysObject) session.getObject(new DfId(id));
            // apply only to a spcific target type
            if (obj.getTypeName().equals("target_object_type")) {
                // modify the object     
            }
        }
    }

    @Override
    public String getFullName() {
        return new PluginVersion().getFullName();
    }

    @Override
    public String getProductName() {
        return new PluginVersion().getProductName();
    }
}
Destroy service plugin (delete and unlink)

Delete and unlink context menu items are mediated by Destroy service. If you need to modify automatically the selected objects before or after the operation, you can install a listener plugin. The example for D2 4.5:

public class D2DestroyServicePlugin extends D2DestroyService implements ID2fsPlugin {

    @Override
    public Destroyresult destroy(Context context, String id, List<Attribute> attributes) throws D2FailureException, Exception {
        String deleteType = "undefined", parentId = "undefined";
        for (Attribute a : attributes) {
            System.out.println("   " + a.getName() + "=" + a.getValue());
            if (a.getName().equals("version")) {
                deleteType = a.getValue();
            } else if (a.getName().equals("parentId")) {
                parentId = a.getValue();
            }
        }
        D2fsContext d2fsContext = (D2fsContext) context;
        Destroyresult result = super.destroy(context, id, attributes);
        if (deleteType.equals("3")) { // 3 stands for unlink
            after(d2fsContext, id, parentId);
        }
        return result;
    }

    public void after(D2fsContext d2context, String objectId, String parentId) throws DfException, D2fsException {
        IDfSession session = d2context.getSession();
        IDfSysObject obj = (IDfSysObject) session.getObject(new DfId(objectId));
        if (obj.getTypeName().equals("target_object_type")) {
            // do something to the object, save folder name or send emails    
        }
    }

    @Override
    public String getFullName() {
        return new PluginVersion().getFullName();
    }

    @Override
    public String getProductName() {
        return new PluginVersion().getProductName();
    }
}