Pages

Showing posts with label D2. Show all posts
Showing posts with label D2. Show all posts

Friday, January 13, 2017

Invoking the dm_method from an action plugin to access the manifest of the jar encoding the method

Suppose the jar implemting some functionality is placed somewhere in WEB-INF/lib of some war deployed in some server. And there is a need for the jar to know the contents of its own manifest.mf. Standard methods such as Class.getResource("/META-INF/MANIFEST.MF") can only be chance might return the right url. Usually it will be the manifest of the file that was loaded first by the classloader.

To illustrate how the right manifest could be recovered, I will consider the setup where a dm_method installed as a jar in Documentum Java Methods Server. This jar is supposed to function as an important method. Additionally, administrators need to be able to easily access its description, e.g. build number or time, contained in manifest. For example, this information can be displayed in a popup invoked by clicking on a custom menu item in D2.

When a method jar depends on other classes in the classpath, the jar should be placed in WEB-INF/lib of DmMethods.war. Some people put method jars into dba/java_methods. They work only if the jars have no dependencies in the classpath.

The following class load the manifest specifically from the hosting jar. Obviously the manifest attributes should be filtered so that only pertinent values, such as commit hash or build time, are returned.

public class ManifestLoader {

    // escaped new line so that new lines can be passed to javascript
    public static String NEW_LINE = "\\n";
    private static ManifestLoader instance = new ManifestLoader();

    public static ManifestLoader getInstance() {
        return instance;
    }

    public String getVersionInfo() {
        Attributes manifestAttrs=loadManifestAttributes();
        StringBuilder sb = new StringBuilder();
        // return all attributes, but normally here should be some filter for the pertinent attributes
        for (Object o : manifestAttrs.keySet()) {
            sb.append(o + ": " + manifestAttrs.get(o) + NEW_LINE);
        }
        return sb.toString();
    }
    
    Attributes manifestAttrs;

    Attributes loadManifestAttributes() {
        // load manifest only once per runtime
        if (manifestAttrs == null) {
            manifestAttrs = getPersonalManifestInJBoss().getMainAttributes();
        }
        return manifestAttrs;
    }

    Manifest getPersonalManifestInJBoss() { // works only in JBOSS
        Manifest manifest = new Manifest();
        try {
            // determine the url of this jar
            URL thisJarUrl = getClass().getResource(getClass().getSimpleName() + ".class");
            // convert the url into the filename
            String jarFileName = thisJarUrl.toString().replaceFirst("vfs:/", "jar:file:/");
            String jarExtention = ".jar/";
            jarFileName = jarFileName.substring(0, jarFileName.indexOf(jarExtention) + jarExtention.length()) + "!/";
            // open the jar to extract its contents
            URL jarUrl = new URL(jarFileName);
            JarURLConnection jarConnection = (JarURLConnection) jarUrl.openConnection();
            // here we need only manifest
            manifest = jarConnection.getManifest();
        } catch (IOException ex) {
        }
        return manifest;
    }
}

Below is the simplified class implementing IDmMethod so that the jar can be registered as Documentum dm_method. Note, unlike retrieving the manifest value, the principal skipped here long running functions should by executed asynchronously in this dm_method. When the method is invoked by D2 custom action plugin, it returns the contents of the manifest as an error message. Note, throwing an exception is the only way to pass a message from the invoked dm_method to the invoking dql statement. In the dql statement result collection the message will be stored as error_message attribute value.

public class InvokeMethodFromPlugin implements IDmMethod {

    public static final String INFO_KEY = "info";

    public void execute(Map params, OutputStream out) throws Exception {
        String[] infos = (String[]) params.get(INFO_KEY);
        if (infos != null) {
            throw new RuntimeException(ManifestLoader.getInstance().getVersionInfo());
        }
    }
}

Last, let's consider a simplified D2 action plugin that invokes the method and relays the message to D2 where it can be displayed in javascript alert.

public class LaunchMethodPlugin implements IPluginAction, ID2fsPlugin {

    public static final String INFO_KEY = "info";

    public List<Attribute> getInfo(D2fsContext context) throws D2fsException, DfException {
        IDfSession curSession = context.getSession();
        String dql = "EXECUTE do_method WITH METHOD='MethodName', ARGUMENTS='-" + INFO_KEY + " true'";
        String msg = executeDql(dql, curSession);
        List<Attribute> result = new ArrayList<Attribute>();
        result.add(AttributeUtils.createAttribute("result", msg));
    }

    String invokeMethod(String dql, IDfSession session) throws DfException {
        String msg = null;
        IDfCollection col = null;
        try {
            col = new DfQuery(dql).execute(session, DfQuery.DF_EXEC_QUERY);
            while (col.next()) {
                msg = col.getString("error_message");
            }
        } finally {
            if (col != null) {
                col.close();
            }
        }
        return msg;
    }
}

Monday, January 9, 2017

Decompiling jars obfuscated with AspectJ (e.g. D2FS4DCTM-WEB-4.5.0.jar or dfc.jar)

It is much easier to develop dfc.jar-based applications if dfc source code is available. Unless a jar is deliberately obfuscated, it can be easily decompiled. Unfortunaly, most of the methods of dfc.jar source code include AspectJ expressions that generate logging. Upon compilation AspectJ introduces lots of artificial try-catch blocks, if conditions, synthetic methods and classes. This leads to both trippling the size of the source code and obfuscation. In the decompiled code all AspectJ constructs can be easily eliminated using a simple java application. Unfortunately with most of decompilers, some methods, particularly synchronized or containing synchronized block, are transformed by AspectJ compiler into so complex byte code that they fail to be decompiled by ordinary decompilers.

Development of D2 listener plugins is also easier if the source code of the D2 services is available. The D2 services are encoded in D2FS4DCTM-WEB-4.5.0.jar. If you try to decompile it, you will notice that in all the service classes the original methods encoding all the service logic are missing from the decompiled code. If fact only artificial AspectJ methods remain visible.

For example, let's look into the source code of a decompiled short service class D2DetailService:

public class D2DetailsService extends D2fsAbstractService implements IDetailsService {

    public static Set<String> s_redirectedRefDetail;

    static {
        s_redirectedRefDetail = new HashSet<String>();
        s_redirectedRefDetail.add("Renditions");
        D2DetailsService.s_redirectedRefDetail.add("Audits");
    }

    @InjectSession(redirectReference = RedirectReferenceType.NONE)
    public DocItems getDetailContent(final Context context, final String id, final String detailName, final List<Attribute> parameters) throws Exception {
        return (DocItems) InjectSessionAspect.aspectOf().process(new D2DetailsService$AjcClosure1(new Object[]{this, context, id, detailName, parameters, Factory.makeJP(D2DetailsService.ajc$tjp_0, (Object) this, (Object) this, new Object[]{context, id, detailName, parameters})}).linkClosureAndJoinPoint(69648));
    }

    public class D2DetailsService$AjcClosure1 extends AroundClosure {

        public D2DetailsService$AjcClosure1(final Object[] array) {
            super(array);
        }

        public Object run(final Object[] array) {
            final Object[] state = super.state;
            return D2DetailsService.getDetailContent_aroundBody0((D2DetailsService) state[0], (Context) state[1], (String) state[2], (String) state[3], (List) state[4], (JoinPoint) state[5]);
        }
    }

    public static ID2Detail getD2DetailInstance(final Context context, String detailName) throws Exception {
        ID2Detail result = null;
        Class detailClass = null;
        try {
            detailName = StringUtil.getJavaName(detailName);
            detailClass = Class.forName(String.valueOf(ID2Detail.class.getPackage().getName()) + '.' + detailName);
            result = detailClass.newInstance();
        } catch (ClassNotFoundException ex) {
        }
        return result;
    }
}

@InjectSession annotation marks methods as the targets for transformation by AspectJ. The annotated method getDetailContent is indeed totally twisted by AspectJ. Namely, the original method is replaced by a substitute method that invokes a bizarre innner class that in turn calls the method getDetailContent_aroundBody0 containing the slightly mutilated code of the original getDetailContent method. The problem is that synthetic getDetailContent_aroundBody0 is missing in the decompiled code. Try any decompilers if you doubt this phenomenon.

The best decompiler for obfuscated and crippled java classes is cfr. It is an extraordinary tool that decompiles everything. However, some manual editing is often necessary for the methods, particularly including many blocks, that other decompilers fail to decompile. Let's see what we can recover with cfr from D2DetailsService service that was partially decompiled above.

public class D2DetailsService extends D2fsAbstractService implements IDetailsService {

    public static Set<String> s_redirectedRefDetail;

    static {
        s_redirectedRefDetail = new HashSet<String>();
        s_redirectedRefDetail.add("Renditions");
        s_redirectedRefDetail.add("Audits");
    }

    @InjectSession(redirectReference = RedirectReferenceType.NONE)
    public DocItems getDetailContent(Context context, String id, String detailName, List<Attribute> parameters) throws Exception {
        Context context2 = context;
        String string = id;
        String string2 = detailName;
        List<Attribute> list = parameters;
        Object[] arrobject = new Object[]{context2, string, string2, list};
        JoinPoint joinPoint = Factory.makeJP((JoinPoint.StaticPart) ajc$tjp_0, (Object) this, (Object) this, (Object[]) arrobject);
        Object[] arrobject2 = new Object[]{this, context2, string, string2, list, joinPoint};
        return (DocItems) InjectSessionAspect.aspectOf().process(new D2DetailsService$AjcClosure1(arrobject2).linkClosureAndJoinPoint(69648));
    }

    public class D2DetailsService$AjcClosure1 extends AroundClosure {

        public D2DetailsService$AjcClosure1(final Object[] array) {
            super(array);
        }

        public Object run(final Object[] array) {
            final Object[] state = super.state;
            return D2DetailsService.getDetailContent_aroundBody0((D2DetailsService) state[0], (Context) state[1], (String) state[2], (String) state[3], (List) state[4], (JoinPoint) state[5]);
        }
    }

    public static ID2Detail getD2DetailInstance(Context context, String detailName) throws Exception {
        ID2Detail result;
        result = null;
        Class detailClass = null;
        try {
            detailName = StringUtil.getJavaName((String) detailName);
            detailClass = Class.forName(String.valueOf(ID2Detail.class.getPackage().getName()) + '.' + detailName);
            result = (ID2Detail) detailClass.newInstance();
        } catch (ClassNotFoundException classNotFoundException) {
        }
        return result;
    }

    static final /* synthetic */ DocItems getDetailContent_aroundBody0(D2DetailsService ajc$this, Context context, String id, String detailName, List parameters, JoinPoint joinPoint) {
        ID2Detail detailInstance;
        DocItems result;
        D2fsContext d2fsContext;
        result = new DocItems();
        d2fsContext = (D2fsContext) context;
        d2fsContext.setParameterParser(parameters);
        if (id != null) {
            d2fsContext.getParameterParser().setParameter("id", (Object) id);
        }
        if (detailName != null && s_redirectedRefDetail.contains(detailName) && !d2fsContext.getParameterParser().getBooleanParameter("redirectedReference", false)) {
            D2fsContext sourceContext = null;
            try {
                sourceContext = ReferenceUtils.getSourceContext(d2fsContext, true);
                if (sourceContext != null) {
                    IDfId sourceId = sourceContext.getFirstId();
                    DocItems docItems = new D2DetailsService().getDetailContent((Context) sourceContext, sourceId.toString(), detailName, parameters);
                    return docItems;
                }
            } catch (Exception exception) {
                if (result.getUpperItem() == null) {
                    ContentBuilder.addUpperItem(result, d2fsContext, id, detailName, null);
                }
                DocItems docItems = result;
                return docItems;
            } finally {
                if (sourceContext != null) {
                    sourceContext.release(false);
                }
            }
        }
        if ((detailInstance = D2DetailsService.getD2DetailInstance(context, detailName)) != null) {
            result = detailInstance.getDetailContent(d2fsContext, id);
        }
        if (result.getUpperItem() == null) {
            ContentBuilder.addUpperItem(result, d2fsContext, id, detailName, null);
        }
        return result;
    }
}

In addition to the code analogous to the code that we saw above, we see the nicely decompiled synthetic getDetailContent_aroundBody0 method that essentially contains the untouched original code of the original getDetailContent method. However, unlike the original method, its derivative contains an irrelevant argument ajc$this added by AspectJ.

To sup up, if you develop Documentum applications base on dfc.jar, or if you develop plugins for D2, cfr decompiler is a must-have tool!

Tuesday, January 3, 2017

Documentum D2 custom action plugins

The administrators of D2 can create custom menu items invoking your custom service classes. Menu items are created and set up in D2 config.

Invoking a native D2 service
But first, let's consider a native D2 service can be invoked. Suppose we want to display the value of some attribute of the selected object. For this getProperties methods of D2 PropertyService can be employed. The target property has to be specified in the message field. The returned value can be passed to some javascript function, for example, to be displayed in javascript alert popup.
Invoking a custom service class

Custom service methods must implement marking interface IPluginAction and have the common signature:

public class ActionServiceTemplate implements IPluginAction {

    public List<Attribute> someMethods(D2fsContext d2context) throws Exception {
        ParameterParser d2parameterparser = d2context.getParameterParser();

        // all the parameters received by method
        for (Attribute a : d2parameterparser.getParameters()) {
            System.out.println("paramName/value: " + a.getName() + " " + a.getValue());
        }

        // shortcut method to access the selected object id
        IDfId objectId = d2context.getFirstId();

        // ParameterParser method to retrieve values of received attributes
        String contentType = d2parameterparser.getStringParameter("aContentType");
        String containingFolderObjectId =d2parameterparser.getStringParameter("parentId");

        // the method returns list containing arbitrary nubmer of key value pairs
        List<Attribute> result = new ArrayList<>();
        result.add(AttributeUtils.createAttribute("result", "test"));
        return result;
    }
}

Depending on the menu item clicked, D2 user interface sends to the method various named values ( e.g. ContentType, parentId). The selected object id is always included. The method has only one argument of type D2fsContext. This type includes ParameterParser that is a container for the list of name value pairs each enclosed in Attribute class. An instance of Attribute holds name and value. The values can be directly accessed using ParameterParser method getStringParameter. Some additional examples of how some received values can be used I included the article of listener plugins.

D2fsContext also contains shortcut methods used to directly access some parameters, for example, getFirstId to get id of the selected objects. The selection might include single or multiple objects.

As it was demonstrated above, the method might optionally return a list of results in the same form i.e as Attributes. In D2 user interface the returned results will be available as javascript variables having the same names, for example above we used alert(object_name), that can be further processed by javascript.

The example below shows how to create a custom "Copy link to clipboard" action. The standard "Copy link to clipboard" menu item publishes D2_ACTION_COPY_LINK_IN_CLIPBOARD event that is processed by some Clipbard service that eventually puts the url to the selected object into clipboard.

If custom urls are needed, for example with different hostname and some object-dependent parameters, the custom action plugin is the solution.

public class CopyLink implements IPluginAction {

    public List<Attribute> copyLink(D2fsContext d2context) throws DfException, D2fsException, IOException {
        IDfId objectId = d2context.getFirstId();
        
        ParameterParser d2parameterparser = d2context.getParameterParser();
        String contentType = d2parameterparser.getStringParameter("aContentType");
        String url = "https://www.instagram.com/get?id=" + objectId.getId() + "&type=" + contentType;

        List<Attribute> result = new ArrayList<>();
        result.add(AttributeUtils.createAttribute("result", url));
        return result;
    }
}

The setting in D2 config should be adjusted as follows:

The plugin returns the custom url as the javascript variable named result. The result is passed to a native D2 javascript method pasteInClipboard that in turn calls the applet to put the value into the clipboard.

Action plugin executing javascript before executing a D2 event

Unfortunately D2 config does not allow executing javascript and then publishing D2 event to ajaxHub. After a service is executed, either javscript is executed (when JS in selected in Type list) or event is published (when EVENT is selected in Type list). When NATIVE is selected, the service results are ignored.

Suppose a confirmation pop up dialog is should appear when a user clicks on some standard action publishing event, for example when "Cancel checkout" is clicked. If the user clicks yes in the popup then the event is published. Alternatively, nothing happens, if no is selected.

I propose a working workaround allowing execute a javascript code before sending D2 event. When a user triggers action normally publishing an event, a custom service is invoked instead. The service does nothing but only returns javascript that executes arbitrary code, such as displaying the stand confirm popup, and then publishes an arbitrary event directly to openAjaxHub.

public class Relay implements IPluginAction {

    static String EVENT_NAME = "myActionName";
    static String CONFIRMATION_MESSAGE = "myActionMessage";

    public List<Attribute> relay(D2fsContext d2context) throws DfException, D2fsException, IOException {
        Map<String, String> parametersMap = new HashMap<>();
        ParameterParser pp = d2context.getParameterParser();

        // forward the original openAjax message together with the event to be published
        for (Attribute a : pp.getParameters()) {
            parametersMap.put(a.getName(), a.getValue());
        }
        Parameters parameters = new Parameters(parametersMap);

        String confimationMessage = pp.getStringParameter(CONFIRMATION_MESSAGE);
        String eventName = pp.getStringParameter(EVENT_NAME);

        // return a string with immediately-invoked javascript function expression
        String js = "(function(){if(confirm('" + confimationMessage + "')){var myPluginContainer=new OpenAjax.hub.InlineContainer(managedHub,'myPluginContainer',{Container:{onSecurityAlert:function(){},onConnect:function(){},onDisconnect:function(){}}}); var myPluginContainerClient=new OpenAjax.hub.InlineHubClient({HubClient:{onSecurityAlert:function(){}},InlineHubClient:{container:myPluginContainer}});myPluginContainerClient.connect(function(hubClient,success){if(success){hubClient.publish('" + eventName + "','" + parameters.toString() + "');console.log('connected and sent');managedHub.removeContainer(myPluginContainer);}else{console.log('failed to connect');}});}})()";

        List<Attribute> result = new ArrayList<>();
        result.add(AttributeUtils.createAttribute("result", js));
        return result;
    }
}

The plugin needs input parameters: the message for the confirm dialog and the event to publish. The script returned by the service is executed by eval function. Now when a user click "Cancel checkout" he will have to additionally press OK in the confirm popup. If the user clicks Cancel nothing will happen.

Action plugin handling multiple selected objects

Action plugins can handle multiple selections. The class template below demonstrate how one could access all the selected objects using a shortcut method getObject. The class additionally demonstrates that plugin does not need to return any results. To set up a plugin that do not return any values, the option NATIVE could be selected in the D2 config field Type.

public class WorkflowActions implements IPluginAction {

    public List<Attribute> startWorkflows(D2fsContext d2context) throws DfException, D2fsException {

        // Loop through all the selected documents
        int i = d2context.getObjectCount();

        for (int j = 0; j < i; j++) {
            IDfSysObject obj = (IDfSysObject) d2context.getObject(j);
            // do something to the object, for example start some workflow
            startWorkflow(obj);
        }

        return new ArrayList<>();
    }
    // very oversimplified method starting workflows on the input object
    void startWorkflow(IDfSysObject obj) throws DfException {
        String workflowName = obj.getTypeName() + " Workflow";
        D2SdkWorkflowLauncher workflowLauncher = new D2SdkWorkflowLauncher(obj.getSession(), workflowName);
        IDfWorkflow workflow = workflowLauncher.startWorkflow(obj, workflowName);
        workflow.setSupervisorName(obj.getSession().getLoginUserName());
    }
}

Sunday, January 1, 2017

Documentum D2 external widget. How to nicely use the current user's session

The code of many sample D2 widgets is available on EMC website. The widgets accessing documentum need a session or credentials to create one. The provided samples use tickets generated by D2 to create a session each time user activates the widget.

I used a simpler way - I reused the current user's session used by D2. To do this, the widget must be included in D2 application, which is not a problem. The same as with plugins, the widget jar should be placed into D2/WEB-INF/lib folder. Then both the static resources and the servlet have the same context root as D2 application and, therefore, can access httpSession of the current user. In D2 all the documentum sessions created for the current user are stored in http session attribute "CONTEXT". The attribute value is a map containing D2 session ids as key and the credentials for corresponding documentum sessions as values.

Project file layout

My sample widget comprises static resources such as html template, javascript and css stylesheet, and a servlet together with auxiliary classes required to generate the output.

myWidget.html

It is a simple file with DIV placeholder for the dynamically generated content. Additionally, the file load two standard scripts enabling the interaction with OpenAjaxHub, which act as a event bus in D2 application. The third script myWidget.js listens to D2 events and updates the html with the content generated by servlet MyWidgetServlet.java.

<html>
  <head>
    <title>My widget</title>
    <script language='javascript' src="container/external-api/OpenAjaxManagedHub-all.js"></script>
    <script language='javascript' src="container/external-api/D2-OAH.js"></script>
    <script src="myWidget.js"></script>
    <link rel="stylesheet" type="text/css" href="myWidgetStyles.css">
  </head>
  <body  onload="myWidget.loadEventHandler()">    
    <div id="myPlaceHolderForHTMLContent"></div>
  </body>
</html>
myWidget.js

The third script myWidget.js connects to ajaxHub, subscribes to D2_EVENT_SELECT_OBJECT event, and calls the servlet whenever the user selects an object. Additionally, when the user select an object visualized in the widget, the script issues event D2_ACTION_LOCATE_OBJECT so that this object is selected in D2 also. Essentially, the script is a relay ensuring the two-way communication between D2 ajaxHub and the widget. When the servlet is accessed it is passed two parameters: the selected object id and D2 session id (mere the D2-specific id of the documentum session used by the current user). The servlet response is inserted into the placeholder div. Then in method attachEventListeners onClick listeners are attached to the displayed objects and the new content is additionally styled and positioned (this code is not shown).

var myWidget = {
  clickedObjectId: "", // last selected object id
  widgetIsOn: false, // widget is active

  // Application initializes in response to document load event  
  loadEventHandler: function () {
    console.log("Iframe loaded ");
    myWidget.ajaxHub = new D2OpenAjaxHub();
    myWidget.ajaxHub.connectHub(myWidget.connectCompleted, myWidget.onInitWidget, myWidget.onActiveWidget);
  },
  connectCompleted: function (hubClient, success, error) {
    if (success) {
      console.log("Hub client connected");
      myWidget.subscribeEvents();
    } else
      console.log("Failed to connect");
  },
  // Callback that is invoked upon widget activation 
  onActiveWidget: function (bActiveFlag) {
    console.log("onActiveWidget: " + bActiveFlag);
    // set the internal flag
    myWidget.widgetIsOn = bActiveFlag;
  },
  onInitWidget: function (message) {
    console.log("onWidgetInit");
  },
  // the widget will react to selection of an object in D2, selectObjectCallback will be invoked
  subscribeEvents: function () {
    console.log("subscribeEvents");
    myWidget.ajaxHub.subscribeToChannel("D2_EVENT_SELECT_OBJECT", myWidget.selectObjectCallback, false);
  },
  // invoked when an object is selected in D2
  selectObjectCallback: function (name, msg) {
    var id=msg.get("oam_id");
    console.log("selectObjectCallback id: " + id);
    // check that the widget is active
    if (!myWidget.widgetIsOn) {
      return;
    }
    // react only if the newly selected object is not the same as the currently selected 
    if (myWidget.myClickedObjectId !== id) {
      console.log("selectObjectCallback processing: " + id);
      var xmlhttp = new XMLHttpRequest();
      xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
          console.log("selectObjectCallback received response " + xmlhttp.status);
          // display the generated html
          document.getElementById("myPlaceHolderForHTMLContent").innerHTML = xmlhttp.responseText;
          console.log("selectObjectCallback inserted response ");
           // the html can display some objects that are related to the selected object. For example, 
           // if versions or objects linked by relations are visualized, then one would expect that clicking on an object
           // would trigger something, for example, the clicked object will be selected in D2. 
              myWidget.attachEventListeners();
              console.log("selectObjectCallback attached listeners");
            }
          };
    
          // sent not only object id but also session id so that it can be recovered by the servlet
          xmlhttp.open("GET", "myWidgetServlet?id=" + id +  "&uid=" + msg.get("oam_cuid"), true);
          xmlhttp.send();
          console.log("selectObjectCallback sent ajax request");
          myWidget.myClickedObjectId = "";
        }
      },
      // optionally attach listeners to your generated html or modify html or do anything else
      attachEventListeners: function () {
      },
      // a methods that could be used together with the method above to trigger selection of the object in D2
      // the method sends D2_ACTION_LOCATE_OBJECT event together with the object id to D2 AjaxHub
      displayInD2ObjectSelectedInWidget: function (id) {
        console.log("displayInD2ObjectSelectedInWidget: " + id);
        var messageToSend = new OpenAjaxMessage();
        messageToSend.put("oam_id", id);
        myWidget.ajaxHub.sendMessage("D2_ACTION_LOCATE_OBJECT", messageToSend);
        return messageToSend;
      }
    };
MyWidgetServlet.java

Note, this servlet works only in servers supporting servlet specification 3.0 and above.

The servlet receives the selected object id and the D2-specific documentum session id. Then the documentum session is extracted from http session, which is shared by D2 application and the widget. The session and the selected object id are passed to auxiliary method createPage rendering html. For example, the navigatable version tree of the object could be rendered.

@WebServlet("/myWidgetServlet")
public class MyWidgetServlet extends HttpServlet {

  @Override
  protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    response.setContentType("text/html;charset=UTF-8");
    PrintWriter out = response.getWriter();
    try {
      // the selected object id
      String selectedObjectId = request.getParameter("id");

      Map<String, Object> contextMap = (Map) request.getSession().getAttribute("CONTEXT");

      // get the session of the current user
      Context c = (Context) contextMap.get(request.getParameter("uid"));
      D2fsContext d2fsContext = new D2fsContext(c, false);
      IDfSession session = d2fsContext.getSession();
       
      // use the session and selected object id to create HTML page 
      // for example you can visualize the version tree of the object
      String html = createPage(session, selectedObjectId);
      out.println(html);
    } catch (DfException | D2fsException ex) {
      ex.printStackTrace(out);
    } finally {
      out.close();
    }
  }

  String createPage(IDfSession session, String selectedObjectId) {
    // generate html displayed in the widget
    return html;
  }
}
Installation

After the project is built into jar file and placed into D2/WEB-INF/lib folder, the widget has to be enabled in D2 config:

  • create a new widget entry in D2 config
  • select ExternalWidget option in the Widget type list
  • check Bidirectional communication
  • enter myWidget.html?anything=11 (without an arbitrary parameter the relative url is misunderstood by D2) into the Widget url text input
  • click Save
  • enable the widget in the context matrix of D2 config
  • open D2 and select the widget in the widget gallery

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();
    }
}