Search

Replicating “Save As” functionality of Developer Studio using Java API

Yesterday there was a BMC Community question around replicating the ‘Save As’ functionality of Developer Studio for Forms using only the Java API.  This ends up being more complicated then it seems so I decided to take a stab yesterday evening at putting together a demonstration program that would accomplish this goal (for Forms only at this point).

Some background; In order to make a true copy of a Form, you need to also make copies of the View and Field objects.  This is complicated by the fact that for more complex forms, there is the issue with field dependencies, e.g. you can’t create a column field until the table holder is created, you can’t create a panel until the panel holder is created, you can’t add a simple character field that lives on a panel until the panel and panel holder is created, etc.  So you need to bring in the fields one at a time by type (unless there is some other method of doing this).

Here is an example of what I’m talking about:

nestedFields
Now let’s look at some code that can solve this mess…

As with all my Remedy Java API code, I have a yaml configuration parser that I use to feed in my connection details as well as any required parameters / inputs.  It is purely optional however if you want to use the subsequent class further down without change you will need this, otherwise you are free to modify the subsequent class to not require this.  (***Note, you can download a copy of all the source files at the bottom of this post)

Here is the code to that class:

/*
 * Copyright (c) 2013 Curtis Gallant <cgallant@gmail.com>
 *
 * 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.utils;

import org.yaml.snakeyaml.Yaml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

class ParseConfig {
    private static String configServer = null;
    private static String configUser = null;
    private static String configPassword = null;
    private static int configPort = 0;
    private static String configPrefix = null;
    private static ArrayList configListOfFormsToCopy;

    public static String getConfigServer() { return configServer; }
    public static String getConfigUser() { return configUser; }
    public static String getConfigPassword() { return configPassword; }
    public static int getConfigPort() { return configPort; }
    public static String getConfigPrefix() { return configPrefix; }
    public static ArrayList getConfigListOfFormsToCopy() { return configListOfFormsToCopy;}

    static void readConfiguration() {
        try {
            InputStream input = new FileInputStream(new File("CloneRemedyObjectConfig.yaml"));
            Yaml yaml = new Yaml();
            Map<String, ArrayList> config = (Map<String, ArrayList>) yaml.load(input);

            // Connection Info
            ArrayList connInfo = config.values().iterator().next();
            for (Object entry : connInfo) {
                // Server Name
                if (((HashMap) entry).get("Server Name") != null) {
                    configServer = ((HashMap) entry).get("Server Name").toString();
                }
                // Username
                if (((HashMap) entry).get("User Name") != null) {
                    configUser = ((HashMap) entry).get("User Name").toString();
                }
                // Password
                if (((HashMap) entry).get("Password") != null) {
                    configPassword = ((HashMap) entry).get("Password").toString();
                }
                // Port
                if (((HashMap) entry).get("Port") != null) {
                    configPort = Integer.parseInt(((HashMap) entry).get("Port").toString());
                }
                // Prefix
                if (((HashMap) entry).get("Prefix") != null) {
                    configPrefix = ((HashMap) entry).get("Prefix").toString();
                }
            }

            // Forms to purge list
            configListOfFormsToCopy = config.get("Forms");

            // Close our config file
            input.close();

        } catch (FileNotFoundException e) {
            System.out.println("Can't find CloneRemedyObjectConfig.yaml file");
            System.exit(1);
        } catch (IOException e) {
            System.out.println(e.getMessage());
            System.exit(1);
        }
    }
}

This will give us a yaml configuration file reader that will provide all our connection info, prefix for newly created forms as well as create an ArrayList of all the forms we want to make copies of. Below is a sample CloneRemedyObjectConfig.yaml file that will be read by the above that I used for testing, it contains one of each of the Form types I chose at random, change these to whatever form you wish to copy:

# Place your connection information in the block below target AR Server
Connection Info:
 - Server Name: <serverName>
   User Name: <adminLoginName>
   Password: <adminLoginPassword>
   Port: <serverPort>

   # Append the following prefix for newly copied forms, make sure to encapsulate with double-quotes
   Prefix: "CG:"

# Place the list of forms you wish to make copies, format is <space>-<space>"<formName>"
# Don't forget the double-quotes
Forms:
- "AST:SoftwareUsageDialog"            #Display form
- "AST:SoftwareUsage"                  #Vendor form
- "AST:Statistics"                     #Regular form
- "AST:SoftwareServer"                 #Join form
- "AR System Metadata: actlink_macro"  #View form

Now that we can parse our configuration file, here is the real Class that does all the work of copying a form.  Here is the high level flow of the program based on my findings (not including boilerplate stuff):

  1. Check if the the new form name will already exist and if it does skip it entirely
  2. If it doesn’t exist, find out what type of form the original one is (Regular, DisplayOnly, Vendor, View or Join) and take appropriate action depending on the form
    • Because when we clone our form, initially the fields/views do not exist so things like results list, indexes, audits and archive settings will throw errors if you just try and createForm the cloned object so we need to dump some properties them prior to our base form creation
    • We then set some of the basics like the new name, owner, etc.
    • If the original form was part of an Application, we need to remove that (same behavior as Developer Studio)
  3. Create the basic form based on our modified clone
  4. Make copies of the View objects from the original form, taking into account that if the form used the Default Administrator View, it will collide with the new view we are trying to create so we need to wrap our attempt at createView to create the view if it truly is new, or if the name/ID is the same as the default on newly created forms, do a setView instead.
  5. Now that we have our views in place we need to copy all the fields, first we get a list of all the fields from the original form and then need to split them into separate lists based on the type of field it is
    • We set the new target form name for the fields as well as set the ReserverID flag to true so that we don’t get errors creating fields in the BMC reserved range
    • For table fields specifically, we need to first strip out the qualification from its TableFieldLimit since it could possibly reference fields that haven’t been created yet (e.g. other table fields or columns)
  6. We then can create all the fields based on our new lists, the order I found that works is the following based on possible hierarchy dependencies for various field properties:
    • PageHolderFields
    • PageFields
    • TrimFields
    • DataFields – these are pretty much every real field on a form other then core form fields
    • CoreFields – these are core fields like 1-8, 15, etc.
      • For some form types we need a bit more logic, e.g. Vendor/Join forms by default create fieldID 1 so for those forms we can’t simply include those fields in the createMultipleFields call, we need to update each of those fields individually with either setField if they already exist so that those already created fields will receive the changes from the origional one (e.g. DisplayProperties) or createField for the remainder of the core fields
    • TableFields
      • Before we create the table fields, now that the fields above have been created, we can re-add the qualification to the object and then create.
    • ColumnFields
    • AttachmentPoolFields
    • AttachmentFields
    • ControlFields

I’ve only added a few scattered comments throughout to help understand some of the code however I have not really documented to much at this point. If there is anything unclear, please let me know in the comments. If you find any bugs also please let me know in the comments. Again this was simply built as a POC and has not been heavily tested outside of my own system and about 30 various forms (of different types). I’ve also left most of the exception handling throw the stack out so that if there are real bugs I can get the details of where it fails, any real program based on this should do exceptions in a ‘nicer’ manner.

/*
 * Copyright (c) 2013 Curtis Gallant <cgallant@gmail.com>
 *
 * 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.utils;

/**
 * CloneRemedyObject - This sample program will create a copy of an AR Form with a defined prefix
 */

import com.bmc.arsys.api.ARException;
import com.bmc.arsys.api.ARServerUser;
import com.bmc.arsys.api.AttachmentField;
import com.bmc.arsys.api.AttachmentPoolField;
import com.bmc.arsys.api.ColumnField;
import com.bmc.arsys.api.Constants;
import com.bmc.arsys.api.ControlField;
import com.bmc.arsys.api.DisplayOnlyForm;
import com.bmc.arsys.api.Field;
import com.bmc.arsys.api.Form;
import com.bmc.arsys.api.JoinForm;
import com.bmc.arsys.api.ObjectPropertyMap;
import com.bmc.arsys.api.PageField;
import com.bmc.arsys.api.PageHolderField;
import com.bmc.arsys.api.QualifierInfo;
import com.bmc.arsys.api.RegularForm;
import com.bmc.arsys.api.TableField;
import com.bmc.arsys.api.TableFieldLimit;
import com.bmc.arsys.api.TrimField;
import com.bmc.arsys.api.Value;
import com.bmc.arsys.api.VendorForm;
import com.bmc.arsys.api.View;
import com.bmc.arsys.api.ViewCriteria;
import com.bmc.arsys.api.ViewForm;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class CloneRemedyObject {
    private static ARServerUser ctx;

    public CloneRemedyObject() {
        ParseConfig.readConfiguration();
        ctx = new ARServerUser();
        ctx.setServer(ParseConfig.getConfigServer());
        ctx.setUser(ParseConfig.getConfigUser());
        ctx.setPassword(ParseConfig.getConfigPassword());
        ctx.setPort(ParseConfig.getConfigPort());
    }

    public static void main(String[] args) {
        CloneRemedyObject ars = new CloneRemedyObject();
        ars.connectionTest();
        try {
            ctx.useAdminRpcQueue();
            // We will want to create our form as a custom object so set in Best Practice Mode
            ctx.setBaseOverlayFlag(false);
            ctx.setOverlayFlag(true);
            ctx.setOverlayGroup(String.valueOf(Constants.AR_GROUP_ID_ADMINISTRATOR));
            ctx.setDesignOverlayGroup(String.valueOf(Constants.AR_GROUP_ID_ADMINISTRATOR));

            for (Object originalFormName : ParseConfig.getConfigListOfFormsToCopy()) {
                copyForm(originalFormName.toString(), ParseConfig.getConfigPrefix());
            }
        } catch (ARException e) {
            e.printStackTrace();
        }
        ctx.logout();
    }

    private static void copyForm(String originalFormName, String newFormPrefix) {
        try {
            List<String> existingCheck = ctx.getListForm();
            if (existingCheck.contains(newFormPrefix + originalFormName)) {
                System.out.println("Target form already exists, skipping");
                return;
            }
            Form originalForm = ctx.getForm(originalFormName);
            String formType = String.valueOf(originalForm.getClass());
            Form newForm = null;
            if (formType.equals("class com.bmc.arsys.api.RegularForm")) {
                newForm = (RegularForm) originalForm.clone();
                newForm.setSortInfo(null);
                newForm.setAuditInfo(null);
                newForm.setArchiveInfo(null);
                newForm.setEntryListFieldInfo(null);
                newForm.setIndexInfo(null);
            } else if (formType.equals("class com.bmc.arsys.api.DisplayOnlyForm")) {
                newForm = (DisplayOnlyForm) originalForm.clone();
            } else if (formType.equals("class com.bmc.arsys.api.JoinForm")) {
                newForm = (JoinForm) originalForm.clone();
                newForm.setSortInfo(null);
                newForm.setAuditInfo(null);
                newForm.setArchiveInfo(null);
                newForm.setEntryListFieldInfo(null);
            } else if (formType.equals("class com.bmc.arsys.api.ViewForm")) {
                newForm = (ViewForm) originalForm.clone();
                newForm.setSortInfo(null);
                newForm.setAuditInfo(null);
                newForm.setArchiveInfo(null);
                newForm.setEntryListFieldInfo(null);
                newForm.setIndexInfo(null);
            } else if (formType.equals("class com.bmc.arsys.api.VendorForm")) {
                newForm = (VendorForm) originalForm.clone();
                newForm.setSortInfo(null);
                newForm.setAuditInfo(null);
                newForm.setArchiveInfo(null);
                newForm.setEntryListFieldInfo(null);
                newForm.setIndexInfo(null);
            }

            // Set some of the defaults from original form
            assert newForm != null;
            newForm.setName(newFormPrefix + originalFormName);
            newForm.setDefaultVUI("Change Me"); // In-case the original form uses default name
            newForm.setOwner(originalForm.getOwner());
            newForm.setHelpText(originalForm.getHelpText());
            newForm.setDiary(originalForm.getDiary());
            newForm.setLastChangedBy(originalForm.getLastChangedBy());
            newForm.setPermissions(originalForm.getPermissions());

            // Set Object Property Map after removing app owner and deployable application tags
            ObjectPropertyMap propertyMap = (ObjectPropertyMap) originalForm.getProperties().clone();
            for (Iterator<Map.Entry<Integer, Value>> it = propertyMap.entrySet().iterator(); it.hasNext(); ) {
                Map.Entry<Integer, Value> entry = it.next();
                if (entry.getKey().equals(Constants.AR_SMOPROP_APP_OWNER)
                        || entry.getKey().equals(Constants.AR_SMOPROP_APP_LIC_VERSION)
                        || entry.getKey().equals(Constants.AR_SMOPROP_APP_LIC_DESCRIPTOR)
                        || entry.getKey().equals(Constants.AR_SMOPROP_APP_LIC_USER_LICENSABLE)) {
                    it.remove();
                }
            }
            newForm.setProperties(propertyMap);

            // Attempt to create the form
            ctx.createForm(newForm);

            copyFormViews(originalFormName, newFormPrefix);
            copyFormFields(originalFormName, newFormPrefix, formType);

            // Set remaining form settings now that fields are created - Ignoring audit and archive settings
            Form updateForm = ctx.getForm(newFormPrefix + originalFormName);
            if (formType.equals("class com.bmc.arsys.api.RegularForm")) {
                updateForm.setEntryListFieldInfo(originalForm.getEntryListFieldInfo());
                updateForm.setSortInfo(originalForm.getSortInfo());
                updateForm.setIndexInfo(originalForm.getIndexInfo());
                ctx.setForm(updateForm);
            } else if (formType.equals("class com.bmc.arsys.api.JoinForm")) {
                updateForm.setEntryListFieldInfo(originalForm.getEntryListFieldInfo());
                updateForm.setSortInfo(originalForm.getSortInfo());
                ctx.setForm(updateForm);
            } else if (formType.equals("class com.bmc.arsys.api.VendorForm")) {
                updateForm.setEntryListFieldInfo(originalForm.getEntryListFieldInfo());
                updateForm.setSortInfo(originalForm.getSortInfo());
                ctx.setForm(updateForm);
            }
        } catch (ARException e) {
            e.printStackTrace();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

    private static void copyFormViews(String originalFormName, String newFormPrefix) {
        try {
            ViewCriteria viewCriteria = new ViewCriteria();
            viewCriteria.setRetrieveAll(true);

            // Grab the Default Created View ID from Form copy so that we can delete the view later
            int deleteOrUpdateViewID = 0;
            List<View> deleteViews = ctx.getListViewObjects(newFormPrefix + originalFormName, 0,
                    viewCriteria);
            for (View deleteOrUpdateView : deleteViews) {
                deleteOrUpdateViewID = deleteOrUpdateView.getVUIId();
            }

            // Copy or Update the views from the original to the copy
            boolean keepView = false;
            List<View> views = ctx.getListViewObjects(originalFormName, 0, viewCriteria);
            for (View view : views) {
                View copiedView = (View) view.clone();
                copiedView.setFormName(newFormPrefix + originalFormName);
                if (copiedView.getVUIId() == deleteOrUpdateViewID) {
                    ctx.setView(copiedView);
                    keepView = true;
                } else {
                    ctx.createView(copiedView);
                }
            }
            // Delete the default view that got created since it's not on origional
            if (!keepView) {
                ctx.deleteView(newFormPrefix + originalFormName, deleteOrUpdateViewID);
            }
        } catch (ARException e) {
            e.printStackTrace();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

    private static void copyFormFields(String originalFormName, String newFormPrefix, String formType) {
        List<Field> pageHolderFields = new ArrayList<Field>();
        List<Field> pageFields = new ArrayList<Field>();
        Map<Field, QualifierInfo> tableFields = new HashMap<Field, QualifierInfo>();
        List<Field> columnFields = new ArrayList<Field>();
        List<Field> attachPoolFields = new ArrayList<Field>();
        List<Field> attachFields = new ArrayList<Field>();
        List<Field> controlFields = new ArrayList<Field>();
        List<Field> trimFields = new ArrayList<Field>();
        List<Field> dataFields = new ArrayList<Field>();
        List<Field> coreFields = new ArrayList<Field>();

        try {
            List<Field> fields = ctx.getListFieldObjects(originalFormName);
            for (Field field : fields) {
                Field copiedField = (Field) field.clone();
                copiedField.setForm(newFormPrefix + originalFormName);
                copiedField.setReservedIDOK(true);
                if (!copiedField.isCoreField()) {
                    if (copiedField instanceof PageHolderField) {
                        pageHolderFields.add(copiedField);
                    } else if (copiedField instanceof PageField) {
                        pageFields.add(copiedField);
                    } else if (copiedField instanceof TableField) {
                        // For table fields, we must strip the qualification first and re-add later
                        TableFieldLimit tablefieldLimit = (TableFieldLimit) copiedField.getFieldLimit();
                        tablefieldLimit.setQualifier(null);
                        copiedField.setFieldLimit(tablefieldLimit);
                        tableFields.put(copiedField, ((TableFieldLimit) field.getFieldLimit()).getQualifier());
                    } else if (copiedField instanceof ColumnField) {
                        columnFields.add(copiedField);
                    } else if (copiedField instanceof AttachmentPoolField) {
                        attachPoolFields.add(copiedField);
                    } else if (copiedField instanceof AttachmentField) {
                        attachFields.add(copiedField);
                    } else if (copiedField instanceof ControlField) {
                        controlFields.add(copiedField);
                    } else if (copiedField instanceof TrimField) {
                        trimFields.add(copiedField);
                    } else {
                        dataFields.add(copiedField);
                    }
                } else {
                    coreFields.add(copiedField);
                }
            }
            if (pageHolderFields.size() > 0)
                ctx.createMultipleFields(pageHolderFields);
            if (pageFields.size() > 0)
                ctx.createMultipleFields(pageFields);
            if (trimFields.size() > 0)
                ctx.createMultipleFields(trimFields);
            if (dataFields.size() > 0)
                ctx.createMultipleFields(dataFields);
            if (coreFields.size() > 0) {
                if (formType.equals("class com.bmc.arsys.api.VendorForm")) {
                    for (Field field : coreFields) {
                        if (field.getFieldID() == 1) {
                            ctx.setField(field);
                        } else {
                            ctx.createField(field, true);
                        }
                    }
                } else if (formType.equals("class com.bmc.arsys.api.JoinForm")) {
                    for (Field field : coreFields) {
                        if (field.getFieldID() == 1 || field.getFieldID() == 15) {
                            ctx.setField(field);
                        } else {
                            ctx.createField(field, true);
                        }
                    }
                } else {
                    ctx.setMultipleFields(coreFields);
                }
            }
            if (tableFields.size() > 0) {
                for (Map.Entry<Field, QualifierInfo> tableField : tableFields.entrySet()) {
                    ctx.createField(tableField.getKey(), true);
                }
                if (columnFields.size() > 0) {
                    ctx.createMultipleFields(columnFields);
                }
                for (Map.Entry<Field, QualifierInfo> tableField : tableFields.entrySet()) {
                    TableFieldLimit tableFieldLimit = (TableFieldLimit) tableField.getKey().getFieldLimit();
                    tableFieldLimit.setQualifier(tableField.getValue());
                    tableField.getKey().setFieldLimit(tableFieldLimit);
                    ctx.setField(tableField.getKey());
                }
            }
            if (attachPoolFields.size() > 0)
                ctx.createMultipleFields(attachPoolFields);
            if (attachFields.size() > 0)
                ctx.createMultipleFields(attachFields);
            if (controlFields.size() > 0)
                ctx.createMultipleFields(controlFields);
        } catch (ARException e) {
            e.printStackTrace();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

    private void connectionTest() {
        System.out.println();
        try {
            ctx.verifyUser();
        } catch (ARException e) {
            System.out.println("Could not log into server: " + e);
            System.exit(1);
        }
    }
}

Happy Coding 🙂

One thought on “Replicating “Save As” functionality of Developer Studio using Java API

Leave a Reply to Raj Padole Cancel reply

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