Search

Entry Inspector – A Java FilterAPI Plugin Sample

Java Plugins are a great way to add new capabilities to BMC Remedy AR System without having to use the unpopular term ‘customization’. BMC have been adding more and more complex and sophisticated capabilities over last few releases by way of Java plugins so why can’t we?

In order to deploy Java Plugins on your system, you need to make changes to several areas of the installs so as a pre-requisit, you must have access to the server upon which AR System is installed. At a high level, the following is basic set of activities required to deploy a new Java Plugin.

  1. Develop your Plugin and package into a proper Jar file
  2. Deploy your Jar file in the pluginsvr directory
  3. Add an entry for your plugin in the pluginsvr_config.xml file
  4. Add an entry for your plugin in the ar.conf file

Now let’s look at the sample I have put together that satisfies an itch I’ve had since the move away from the User Tool in recent years…  As I’m sure most have realized already, pulling a report on a form with many fields (like HPD:Help Desk as an example) can be frustrating via the Mid-Tier.  If you are making an actual report, going through the effort is worth it but sometimes you just need to dump the data (including fields not on view) in order to see everything to support some customization that are in progress or not behaving correctly.  This scenario in Mid-Tier requires many clicks with new windows popping up and lots of frustrations (especially if it’s the first time you target a form as you need to build the report).  Back in the UserTool, it was much faster to just get a snapshot of the current data via reporting and this is something I miss.

Introducing the Entry Inspector plugin.  This plugin allows for quickly viewing the full contents of any entry from any form and then also give the ability to display only delta’s of that entry after a change has been made.  What this is useful for is you launch a custom Remedy form which I have included in the package that allows you to specify the Form and Entry to view and then the plugin will return all the data within the entry.  While you have the entry being displayed, if you go and make a change to the original entry, you can then come back and hit the Fetch Delta Updates button and it will show you all the fields that have now been updated which gives you a nice visibility into what specifically has changed to both visible and non-visible fields.

Here is a screenshot of this plugin in action via the provided Remedy form.

Entry Inspector

The sample above shows an Incident record loaded and then updates were made to the Impact, Urgency, Notes, Opt Cat 1-2-3 and Status.  As you can see, simply changing those values also changes other values on other fields so those are displayed as well as we are compared all fields to the previous Fetch of the entry record.  The right hand field will only ever contain elements that have changed from the left hand field.

The default field ID to search on for all forms is the Request ID Field (Field ID 1) however for some ITSM forms, this field is hidden and not easy to get to so to simplify things, I’ve added the ability to both define default Field ID to search on based on specific forms (in the sample above, HPD:Help Desk has a default ID to search on of the Incident Number field – 1000000161 rather then the Entry ID field – 1  ) as well as the ability to manually override the default field ID to search on.  This configuration is data driven based on another form called SA:EntryInspector_FormSearchConfig included in the definition file, simply create entries in there and they will be leveraged in the main Entry Inspector form.  Be aware though that whatever Field ID goes in there needs to be something that is guaranteed to be unique, like the request ID or a GUID.  There is a small amount of workflow required to make this form work but it is all self-contained and has no dependencies on any OOTB workflow or forms.

Now let’s look at the Java Plugin that makes the above possible and learn a little something about Java Plugins along the way.

In order to interact with Java Plugins,  specifically in this case Java FilterAPI Plugins which run server side, we need to leverage them as an property in a Set Fields action of a Filter.  This will require us to provide both input and output values.  The input values get sent to the plugin, the plugin does it’s work and then simply returns a List<Value> object that we can map into the output paramter.  This is the basic principal behind the plugins, you send it some data, it does it’s work and sends you the results.  Because we use Java for this, we can do pretty much anything we want as we are not limited by the boundaries of AR System workflow.

Full java source and AR definition file  will be provided at the bottom of this blog for the full example but here we will break things piece by piece.  Let’s start by understanding the basic structure of the Java FilterAPI plugin required:

The Java Plugin API is provided by way of the arpluginsvr81_build001.jar file (for v8.1 of the Plugin Server API, change to appropriate version for your environment).  Once you have loaded this library into your IDE, you will need to import a couple of Classes at a minimum:

import com.bmc.arsys.pluginsvr.plugins.ARFilterAPIPlugin;
import com.bmc.arsys.pluginsvr.plugins.ARPluginContext;

Now that we have those Classes loaded, we can create a stub to build out our functionality:

package net.soleauthority.arsys.plugin;

import com.bmc.arsys.api.ARException;
import com.bmc.arsys.api.ARServerUser;
import com.bmc.arsys.api.Value;
import com.bmc.arsys.pluginsvr.plugins.ARFilterAPIPlugin;
import com.bmc.arsys.pluginsvr.plugins.ARPluginContext;

import java.util.ArrayList;
import java.util.List;

public class SampleJavaFilterAPIPlugin extends ARFilterAPIPlugin {
    public List filterAPICall(ARPluginContext context, List paramList)
            throws ARException {

        ARServerUser ars = new ARServerUser(context, "", context.getARConfigEntry("Server-Name"));
        List results = new ArrayList();

        return results;
    }
}

The above is the bare minimum for being able to load as a valid Java plugin. It basically does nothing but will load and execute fine if called. You should note a couple of things, first we are not creating a net new context back to the AR System, we are using the existing context from the Java Plugin Server itself which is much quicker / recommended then creating a new context. You will also notice that we are creating a List<Value> object called results that must be returned from the plugin, this is what eventually will get returned into the Output mapping of the filter Set Fields action.  Specific to my sample form that goes along with this, you’ll notice that because I want this to be an interactive form, I am using a combination of active links that trigger service filters based upon the action I want to perform (FETCH or DELTA).

Now let’s add some code that actually does stuff, first let’s use the List<Value> paramList that we get when our plugin gets instanciated which is basically all the input values from our Filter Set Fields action.  First let’s look at what that Filter Set Fields looks like:

EntryInspector_SetFields

Above we can see that we need to pass in 5 different parameters and then expect a single value returned.  Based on this, let’s add to our plugin.


package net.soleauthority.arsys.plugin;

import com.bmc.arsys.api.ARException;
import com.bmc.arsys.api.ARServerUser;
import com.bmc.arsys.api.Value;
import com.bmc.arsys.pluginsvr.plugins.ARFilterAPIPlugin;
import com.bmc.arsys.pluginsvr.plugins.ARPluginContext;

import java.util.ArrayList;
import java.util.List;

public class SampleJavaFilterAPIPlugin extends ARFilterAPIPlugin {
    public List filterAPICall(ARPluginContext context, List paramList)
            throws ARException {

        ARServerUser ars = new ARServerUser(context, "", context.getARConfigEntry("Server-Name"));
        List results = new ArrayList();

        String targetSchema = paramList.get(0).getValue().toString(); // Pass it in the FormName
        String fieldID = paramList.get(1).getValue().toString(); // Pass in the FieldId to Search
        String searchValue = paramList.get(2).getValue().toString();  // Pass it in the Search Value
        String entryData = null;
        if (paramList.get(3).getValue() != null) {
            entryData = paramList.get(3).getValue().toString(); // Pass it in the Entry - empty on initial fetch
        }
        String action = paramList.get(4).getValue().toString(); // FETCH or DELTA

        return results;
    }
}

Here we can see that we take each of those parameters and map it into some usable variables for us. We are still not really doing anything with this data so now let’s take this the rest of the way and add our logic. We will see below that for the FETCH action that we will create a List object that has for each line a fieldName: FieldValue structure which will eventually be returned as a single Value added to our List object that must be returned from the plugin. For the DELTA action, we pass in both the previous entry we retrieved and then remove from our eventual results all the matching lines so that we are left with a List of only fields that have changed and we return that as our results to be populated into the right hand field of our Entry Inspector form.

/**
 * Copyright (c) 2014 Curtis Gallant <cgallant@soleauthority.net>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package net.soleauthority.arsys.plugin;

import com.bmc.arsys.api.ARException;
import com.bmc.arsys.api.ARServerUser;
import com.bmc.arsys.api.AttachmentField;
import com.bmc.arsys.api.AttachmentValue;
import com.bmc.arsys.api.CharacterField;
import com.bmc.arsys.api.Constants;
import com.bmc.arsys.api.CurrencyField;
import com.bmc.arsys.api.CurrencyValue;
import com.bmc.arsys.api.DateTimeField;
import com.bmc.arsys.api.DiaryField;
import com.bmc.arsys.api.DiaryItem;
import com.bmc.arsys.api.DiaryListValue;
import com.bmc.arsys.api.Entry;
import com.bmc.arsys.api.EntryListInfo;
import com.bmc.arsys.api.EnumItem;
import com.bmc.arsys.api.Field;
import com.bmc.arsys.api.FuncCurrencyInfo;
import com.bmc.arsys.api.QualifierInfo;
import com.bmc.arsys.api.RealField;
import com.bmc.arsys.api.RealFieldLimit;
import com.bmc.arsys.api.SelectionField;
import com.bmc.arsys.api.SelectionFieldLimit;
import com.bmc.arsys.api.StatusHistoryItem;
import com.bmc.arsys.api.StatusHistoryValue;
import com.bmc.arsys.api.Timestamp;
import com.bmc.arsys.api.Value;
import com.bmc.arsys.pluginsvr.plugins.ARFilterAPIPlugin;
import com.bmc.arsys.pluginsvr.plugins.ARPluginContext;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class EntryInspector extends ARFilterAPIPlugin {
    public List filterAPICall(ARPluginContext context, List paramList)
            throws ARException {

        ARServerUser ars = new ARServerUser(context, "", context.getARConfigEntry("Server-Name"));
        List results = new ArrayList();

        String targetSchema = paramList.get(0).getValue().toString(); // Pass it in the FormName
        String fieldID = paramList.get(1).getValue().toString(); // Pass in the FieldId to Search
        String searchValue = paramList.get(2).getValue().toString();  // Pass it in the Search Value
        String entryData = null;
        if (paramList.get(3).getValue() != null) {
            entryData = paramList.get(3).getValue().toString(); // Pass it in the Entry - empty on initial fetch
        }
        String action = paramList.get(4).getValue().toString(); // FETCH or DELTA

        String qualStr = "'" + fieldID + "' = \"" + searchValue + "\"";
        QualifierInfo qual = ars.parseQualification(targetSchema, qualStr);
        List entries = ars.getListEntry(targetSchema, qual, 0, 1, null, null, false, null);
        List fields = ars.getListFieldObjects(targetSchema, Constants.AR_FIELD_TYPE_DATA);

        for (EntryListInfo entryListInfo : entries) {
            Entry entry = ars.getEntry(targetSchema, entryListInfo.getEntryID(), null);
            if (action.equals("FETCH")) {
                List entryStrings = prettyPrintEntry(entry, fields);
                results = createListOfValues(entryStrings);
            } else if (action.equals("DELTA")) {
                List originalEntry = convertStringToStringList(entryData);
                List updatedEntry = prettyPrintEntry(entry, fields);
                updatedEntry.removeAll(originalEntry);
                results = createListOfValues(updatedEntry);
            }
        }
        if (results.isEmpty()) {
            results.add(new Value("No Match Found"));
        }
        return results;
    }

    private List prettyPrintEntry(Entry entry, List fields) throws ARException {
        List recordOutput = new ArrayList();
        for (Field field : fields) {
            if (field.getFieldOption() != 4) {
                Integer fieldID = field.getFieldID();
                Value val = entry.get(fieldID);
                if (val.toString() != null) {
                    if (field instanceof DateTimeField) {
                        Timestamp callDateTimeTS = (Timestamp) val.getValue();
                        if (callDateTimeTS != null) {
                            recordOutput.add(field.getName() + ": " + callDateTimeTS.toDate().toString());
                        }
                    } else if (field instanceof SelectionField) {
                        SelectionFieldLimit selfieldLimit = (SelectionFieldLimit) field.getFieldLimit();
                        List eItemList = selfieldLimit.getValues();
                        for (EnumItem eItem : eItemList) {
                            if (eItem.getEnumItemNumber() == Integer.parseInt(val.toString()))
                                recordOutput.add(field.getName() + ": " + eItem.getEnumItemName());
                        }
                    } else if (field instanceof DiaryField) {
                        DiaryListValue dia = (DiaryListValue) val.getValue();
                        if (dia != null) {
                            StringBuilder diaBuilder = new StringBuilder();
                            for (DiaryItem dlv : dia) {
                                diaBuilder.append(dlv.getUser()).append(" ").append(dlv.getTimestamp().toDate().toString()).append(" ").append(dlv.getText()).append("  ");
                            }
                            recordOutput.add(field.getName() + ": " + diaBuilder);
                        }
                    } else if (field instanceof CurrencyField) {
                        CurrencyValue callCurrencyValue = (CurrencyValue) val.getValue();
                        if (callCurrencyValue != null) {
                            for (FuncCurrencyInfo currInfo : callCurrencyValue.getFuncCurrencyList()) {
                                recordOutput.add(field.getName() + ": " + currInfo.getValue() + " " + currInfo.getCurrencyCode());
                            }
                        }
                    } else if (field instanceof RealField) {
                        RealFieldLimit realLimit = (RealFieldLimit) field.getFieldLimit();
                        int realPrecisionLimit = realLimit.getPrecision();
                        recordOutput.add(field.getName() + ": " + String.format("%1$." + realPrecisionLimit + "f", val.getDoubleValue()));
                    } else if (field instanceof CharacterField) {
                        if (fieldID != 15) {
                            recordOutput.add(field.getName() + ": " + val.toString());
                        } else {
                            StatusHistoryValue shVal = StatusHistoryValue.decode(val.getValue().toString());
                            StringBuilder shBuilder = new StringBuilder();
                            if (shVal != null) {
                                for (StatusHistoryItem shItem : shVal) {
                                    if (shItem != null) {
                                        shBuilder.append(shItem.getTimestamp().getValue()).append("\u0004").append(shItem.getUser()).append("\u0003");
                                    } else {
                                        shBuilder.append("\u0003");
                                    }
                                }
                                recordOutput.add(field.getName() + ": " + shBuilder.toString());
                            }
                        }
                    } else if (field instanceof AttachmentField) {
                        AttachmentValue callAttachValue = (AttachmentValue) val.getValue();
                        if (callAttachValue != null) {
                            String attName = callAttachValue.getName();
                            // Get rid of the folder path if this was a previously exported and then re-imported attachment
                            int lastPos = attName.lastIndexOf('\\');
                            String attNameShort = (lastPos < 0) ? attName : attName.substring(lastPos + 1);
                            recordOutput.add(field.getName() + ": " + attNameShort);
                        }
                    } else {
                        recordOutput.add(field.getName() + ": " + val.toString());
                    }
                } else {
                    recordOutput.add(field.getName() + ": " + "");
                }
            }
        }
        Collections.sort(recordOutput);
        return recordOutput;
    }

    private List createListOfValues(List recordOutput) {
        List listOfValues = new ArrayList();
        StringBuilder tmp = new StringBuilder();
        for (String row : recordOutput) {
            tmp.append(row).append("\n");
        }
        listOfValues.add(new Value(tmp.toString()));
        return listOfValues;
    }

    private List convertStringToStringList(String items) {
        List list = new ArrayList();
        String[] listItems = items.split("\n");
        Collections.addAll(list, listItems);
        return list;
    }
}

And there we have it, a simple yet slightly useful Java FilterAPI plugin that I hope will be beneficial to people who like me get a bit frustrated with what should be a simple task of viewing all the data contained within a record without having to jump through hoops. I created this as a Java FilterAPI plugin is simply to highlight and teach some basics of the plugin architecture and provide a sample for people to learn from, to take full advantage of it requires a couple of forms and some workflow (also included in the package below).

Please read the INSTRUCTIONS.txt file included in the zip file for detailed steps on implementing this in your environment if you are interested.  If there are any bugs found or enhancements that you would like to see, please leave a comment and I will see about updating the package.

And as always, happy coding 🙂

9 thoughts on “Entry Inspector – A Java FilterAPI Plugin Sample

  1. This is great stuff Curtis. I came across this while searching the BMC Communities. Glad to see (and leverage) some of your expertise. Keep it up!

  2. Nice work! Working thru this right now with ARS 8.0. One thing I can’t seem to find is the ar.conf file? Is that the actual file name or is it named something else under 8.0?

    tia,

    adym

    1. Hi Adym,

      ar.conf is the name of the file if you are running non-windows environment. For windows it’s ar.cfg. You will find it in your Remedy installation folder’s conf directory.

      For example, here it is on my local windows installation:
      C:\Program Files\BMC Software\ARSystem\Conf\ar.cfg

      Hope this helps

  3. VERY SMOOTH.
    This is a fantastic learning tool.
    FYI:
    Your Instructions.txt file that comes with the zip file is missing an instruction to add the plugin to ar.cfg

    Once I did that, it worked just fine…now to build one of my own!

  4. I’m trying to use this functionality, But i’m getting the following error
    ARERR [8755] The specified plug-in does not exist. : SOLEAUTHORITY.PLUGIN.ENTRYINSPECTOR

    I’ve updated pluginserv_config.xml and ar.cfg too as
    1.
    SOLEAUTHORITY.PLUGIN.ENTRYINSPECTOR
    FilterAPI
    JAVA
    D:\Program Files\BMC Software\ARSystem\pluginsvr\SA\EntryInspector.jar
    net.soleauthority.arsys.plugin.EntryInspector
    D:\Program Files\BMC Software\ARSystem\pluginsvr\SA\EntryInspector.jar
    </plugin

    2. Server-Plugin-Alias: SOLEAUTHORITY.PLUGIN.ENTRYINSPECTOR CHIREMAPP102D.CHI.catholichealth.net:9977

    Please let me know what would be the issue? and how can i eliminate the error?

    1. Hi Sreekanth,

      The post formatting probably messed up your paste of the pluginsrv_config.xml so I can’t tell if that is right or not, so long as it follows the example provided in the txt file it should be fine (double check paths and all that stuff)

      For the Server-Plugin-Alias line, try this (you need to have the plugin name listed twice in the name):

      Server-Plugin-Alias: SOLEAUTHORITY.PLUGIN.ENTRYINSPECTOR SOLEAUTHORITY.PLUGIN.ENTRYINSPECTOR CHIREMAPP102D.CHI.catholichealth.net:9977

  5. Hi Curtis,

    The article would immensely helpful for creating a fitler API java program, which could be used as a reference for creating any sort of integration with remedy.

    Much appreciate your effort and explanation in this topic.

    Best Regards,
    Kanhu Mohapatra.

Leave a Reply

Your email address will not be published. Required fields are marked *