Sunday 22 December 2013

Tracing who the Final Approver was on a UCM Workflow Document

All workflow history "Approves/Rejects" etc is stored in the OCS database. Heres a quick way to track it with metadata.

Quick Steps:
  • Open Workflow Admin Applet
  • Criteria tab
  • Select Workflow from list
  • Click on the final Step on the right
  • hit Edit
  • Click Events tab
  • Hit the Edit button on the Exit event
  • Click the Custom tab on the pop up window
  • Tick Custom Script Expression
  • Copy paste the following code
<$if wfAction like "APPROVE"$>
<$wfUpdateMetaData("xComments", "Final Approval By: " & dUser)$>
<$endif$>

** You can change the xComments field to any metadata field you have set in Config Man that is applied to a Document.

*** dUser will put the username eg (rudderb) for me but I believe its possible to do the following
<$executeService("GET_USER_INFO")$>
<$name="USER_INFO"$>
<$var = rsFirst(name)$>
<$loopwhile getValue(name, "#isRowPresent")$>
<$wfUpdateMetaData("xComments", "Final Approval By: " &  getValue(name, "dFullName"))$>
<$var = rsNext(name)$>
<$endloop$>
EDIT: The above works! I just tested it.

PDF Watermark Internal error: Unexpected Exception (null) during saveTemplate

For whatever reason, it seems that its impossible to change Watermark Templates in the PDF Watermark Administration Applet. Everytime I try I get the above error.

But a work around for this is as follows:
Note the Content ID, if you do a quicksearch for that Content ID you should be able to find and Check Out the .HDA file which contains all the settings defined in the PDF Watermark Administration Applet.

The HDA should contain the following resultSets:
@ResultSet PdfwTextWatermarks
13
text
location
rotation
alignment
fontName
fontSize
fontWeight
fontColor
layer
pageRange
pageRangeModifier
x_coord
y_coord
@end

@ResultSet PdfwImageWatermarks
8
imageID
location
layer
pageRange
pageRangeModifier
x_coord
y_coord
scaleFactor
@end



Add in your settings into these resultSets in the typical HDA fashion and upload the .HDA file as a new revision.

Once you have uploaded your template as a new revision your changes will be immediately reflected in the PDF Watermark Administration Applet.


Cause:
I'm not 100% on the reason this is all buggered up but I suspect one of the following two reasons:

  1. IdcService=PDFW_SAVE_NEW_TEMPLATE contains bugs
  2. idcToken pdfwUserPassword LocalData parameters are blank in the Templates HDA file (though I think it'd be poor practice to have to include these).

Tuesday 17 December 2013

WSDL to execute SQL against the WebCenter Content/UCM Database

This is handy to get into the guts of the UCM Database and allows you to extract data that isn't available from the Standard IDC Services.

Just remember some data is stored in HDA files in ucm/cs/data and might not be accessible via SQL.

Download the HowToComponents and install DatabaseProvider Component or Download the Component Directly

You can use the following WSDL I created in the WSDL Generator.

** Note you might have to change the following part of the WSDL
<soap:address location="http://localhost:16200/_dav/cs/idcplg" />
to be your UCM Servers Address

Thursday 5 December 2013

Running UCM/WebCenter Content Applets from Linux

On windows you can navigate the UCM installed application and run Content Server Analyzer/Component Wizard etc from Explorer. But on OEL there is no Desktop Manager installed (most cases) so to run these Applets you must use Xterm.

1.       Run MobaXterm
2.       Type “ssh -X oracle@ucmserver” in the main window
3.       cd to the UCM directory /cs/bin/ from memory on server its “/oracle/Middleware/domains/user_projects/ucm/cs/bin” (or close to it)
4.       Dir of the directory should look like
5.       Then type ./<ToolName> 

and voila! The app is now running

javax.security.auth.login.FailedLoginException: [Security:090303]Authentication Failed: User weblogic weblogic.security.providers.authentication.LDAPAtnDelegateException: [Security:090295]

If you are receiving this error then you probably have an External Authentication Provider set to REQUIRED in Weblogic > Server Security Realm

to fix this:
  1. SSH into the Machine
  2. CD to the Admin server directory eg. /u01/app/oracle/admin/<DOMAIN>/aserver/<DOMAIN>/
  3. nano /config/config.xml
  4. search for <sec:control-flag>REQUIRED</sec:control-flag> near the top of the XML
  5. Check that the <sec:name>ActiveDirectoryAuthenticator</sec:name> matches the name of one of your Providers
  6. Change the value to "SUFFICIENT"
Try running ./startWebLogic.sh again

Thursday 21 November 2013

Syncing Active Directory Groups with UCM Aliases

So I was required to find a way to drive Approvers in UCM by Active Directory groups (nightmare!) after some deliberation and a few meetings the simplest solution in my mind was a link between Active Directory Groups and UCM Aliases.

If we could link these lists together then we could have the LDAP server as the source of truth for who belongs in the Alias. And thus a component was born. I extended some CIS code from Jonathan Hult and creating a Filter on the following two services
IdcService=LOGIN
and
IdcService=GET_SEARCH_RESULTS
When a user executes these services custom java code compares the current users Groups agains the Alias list in UCM. If theres a match The user is added to the group!

heres a link to the component!

JAVA: Reading Weblogic Username/Password from the boot.properties file

In order to create a JMX connection to the WLS you must provide a username/password. If you are deploying a webapp to the server and want to read the properties you can use the following methods.

public Map init() {

Map<String, String> userCredentials = new HashMap<String, String>();
       //FIND THE PATH THIS CLASS FILE IS DEPLOYED TO
      Class cls = this.getClass();
      String path = cls.getProtectionDomain().getCodeSource().getLocation().getPath();
      String decodedPath = "FAIL";
       //STRIP THE PATH BACK TO /servers/
try {
              decodedPath = URLDecoder.decode(path, "UTF-8");
             decodedPath = decodedPath.substring(0, decodedPath.indexOf("/servers/"));
     } catch (Exception ex) {
             System.out.println(ex);
     }
//Change domain to aserver from mserver -- DEPENDING ON YOUR DOMAIN SETTINGS
      String domain = decodedPath.replaceAll("mserver", "aserver");

      //GET A STRING OF THE CONTENTS OF THE BOOT.PROPERTIES USING THE readBootProperties method
            String[] wlsbootProperties = readBootProperties(domain);
            String username = "";
            String password = "";
            //Find Username / Password strings
                     for (int i = 0; i < wlsbootProperties.length; i++) {
                if (wlsbootProperties[i].indexOf("username") > -1) {
                    username = wlsbootProperties[i];
                } else if (wlsbootProperties[i].indexOf("password") > -1) {
                    password = wlsbootProperties[i];
                }
            }

            //Strip back to GET AES STRINGS
            username =
                    username.substring(username.indexOf("{AES}"), username.length());
            password =
                    password.substring(password.indexOf("{AES}"), password.length());


            try { //CLEAN OUT BACKSLASHES
                while (username.indexOf("\\") > -1) {
                    username = username.replace("\\", "");
                }
                while (password.indexOf("\\") > -1) {
                    password = password.replace("\\", "");
                }
                           //DECRYPT
                String decryptUser =
                    new weblogic.security.internal.encryption.ClearOrEncryptedService(weblogic.security.internal.SerializedSystemIni.getEncryptionService(domain)).decrypt(username);
                           //DECRYPT
                String decryptPass =
                    new weblogic.security.internal.encryption.ClearOrEncryptedService(weblogic.security.internal.SerializedSystemIni.getEncryptionService(domain)).decrypt(password);
                           //STORE & Return
                userCredentials.put("username", decryptUser);
                userCredentials.put("password", decryptPass);

                return userCredentials;

            } catch (Exception ex) {
                System.out.print(ex);


            }
       
        return userCredentials;

    }

/**
     *  reads the boot properties from the domaindir
     */
    public String[] readBootProperties(String domainDir) {
        String str = "";
        File file =
            new File(domainDir + "/servers/AdminServer/security/boot.properties");
        FileInputStream fis = null;

        try {
            fis = new FileInputStream(file);

            int content;

            while ((content = fis.read()) != -1) {
                // convert to char and display it
                str += (char)content;
                //System.out.print((char) content);
            }


        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null)
                    fis.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return str.split("\n");
    }
Depending on your domain installation you might need to change a few strings to get the
readBootProperties(String domainDir)
Reading the correct files

Wednesday 20 November 2013

Pulling LDAP attributes out of WLS Active Directory Provider

I recently had to connect to the WLS Active Directory Provider in order to execute some custom Java logic. During my research I happened upon the following blog entry Creating Utilities to Manipulate Users in #Weblogic Using #JMX and #MBeans Which worked perfectly for pulling down usernames and user groups but it seemed to be lacking with respect to pulled down LDAP attributes. I mucked around and came up with the following solution.

To pull the LDAP providers hostname out of the WLS Authentication Provider

Add a method at the bottom of the Utilities class
        public static String getUserDetail(String user, String detail) {
         

            try {
                // Set up the environment for creating the initial context
                Hashtable env = new Hashtable();
                env.put(Context.INITIAL_CONTEXT_FACTORY,
                    "com.sun.jndi.ldap.LdapCtxFactory");
                //CREATE LDAP CONNECTION
                env.put(Context.PROVIDER_URL, "ldap://"+connection.getAttribute(defaultAuthenticator, "Host")+":389/");

                // Authenticate
                env.put(Context.SECURITY_AUTHENTICATION, "simple");
                // PULL PRINCIPAL FROM WLS
                env.put(Context.SECURITY_PRINCIPAL, connection.getAttribute(defaultAuthenticator, "Principal"));
                // UNFORTUNATELY HAD TO HARDCODE CREDENTIAL CAN POSSIBLY MOVE TO CONFIG or WEB.XML
                env.put(Context.SECURITY_CREDENTIALS, "Credential");
                DirContext ctx = new InitialDirContext(env);
                String[] attrIDs = { "sAMAccountName", "cn", "title", "mailnickname", "mail", "manager", "department", "telephoneNumber" };
                SearchControls ctls = new SearchControls();
                ctls.setReturningAttributes(attrIDs);
                ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
                NamingEnumeration<SearchResult> answer = ctx.search("LDAP OU", "(&(objectCategory=person)(objectClass=user)(sAMAccountName="+user+"))", ctls);
                while (answer.hasMore()) {
                SearchResult sr = (SearchResult) answer.next();
                    return sr.getAttributes().get(detail).toString();
                }


            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

and voila! We now have a method for pulling out user attributes (mail, manager, title etc) I believe there may be a better way that utilizes JPS which would hopefully circumvent the inclusion of the Credential in my method.

Selecting a Workflow Approver using Tokens & Multiple Aliases

I was given the requirement to create a workflow which could be re-used across several different departments. From the get-go I defined this could be achieved by using:

  • 1 x Token (Open WF Admin -> Options -> Tokens)
  • 1 x Alias for each Department
  • 1 x Alias for each Workflow Step (eg if the WF has 3 steps in document approval than 3 x Aliases)
Now I initially assumed that if I created a Token and added two of the
<$wfAddUser(dWfStepName, "alias")$>
<$wfAddUser(xDepartment, "alias")$>
Statements that it would create an intersection of the Aliases, but instead it just acts as a Union! 

But alas you can write IdocScript within the Token! which allows us to programmatically utilize <$wfAddUser(UCMUSER, "user")$>

So I wrote the following script

//SET ALIASES TO BE COMPARED
<$WFALIAS1NAME = "dWfStepName"$>
<$WFALIAS2NAME = "dDepartment"$>
//STRING TO STORE USERS FOR COMPARISON
<$WFUSERSTRING = ""$>
//EXECUTE SERVICE TO LOAD ALL ALIASES & THEIR USERS
<$executeService("GET_ALIASES")$>
<$name="AliasUserMap"$>
<$var = rsFirst(name)$>
<$loopwhile getValue(name, "#isRowPresent")$>
    <$alias = getValue(name, "dAlias")$>
    <$user = getValue(name, "dUserName")$>
  //IF ALIAS RS Equals the Alias we are searching for
  <$if WFALIAS1NAME LIKE alias$>
        //ADD USER FROM ALIAS MATCH 1 TO USERSTRING
       <$WFUSERSTRING = WFUSERSTRING & "," & user$>
  <$endif$>


    <$var = rsNext(name)$>
<$endloop$>

//START CHECKING THE SECOND ALIAS
<$executeService("GET_ALIASES")$>
<$name="AliasUserMap"$>
<$var = rsFirst(name)$>
<$loopwhile getValue(name, "#isRowPresent")$>
    <$alias = getValue(name, "dAlias")$>
    <$user = getValue(name, "dUserName")$>
  //IF ALIAS RS Equals the Alias we are searching for
  <$if WFALIAS2NAME LIKE alias$>
       //IF THE USER IS IN THIS ALIAS & HAS A MATCH IN USER STRING ADD TO WF
       <$if strIndexOf(WFUSERSTRING,user) !=-1 $>
            <$wfAddUser(user, "user")$>
       <$endif$>
  <$endif$>
    <$var = rsNext(name)$>
<$endloop$>


The script adds the user by looping the first alias creating a string of users in that alias, then looping the second alias and searching for matching users.

jiri.machotka commented on my OTN Post recommended
"rsMergeReplaceOnly() function (see Idoc Script Functions and Variables - 11g Release 1 (11.1.1)). You could create an intersection of the two aliases (by two calls of the function), and then add all remaining users in one loop."

Which sounds like a much cleaner solution that I intend to implement.

Monday 18 November 2013

Bulk / Mass Update Content via RIDC

As Mass Metadata updating cannot be done with Repository Manager and Archiver can be arduous the following Java Class can be used instead. (Change the red text to suit your circumstances)

public class MassUpdater {
    private static IdcClientManager manager = new IdcClientManager();
    private static IdcClient idcClient;
    private static IdcContext userContext;

 public static boolean massUpdate() {

        DataBinder binder = idcClient.createBinder();
        binder.putLocal("IdcService", "GET_SEARCH_RESULTS");
        binder.putLocal("QueryText",
                        "dSecurityGroup <matches> `Public`");
        binder.putLocal("searchFormType", "standard");
        binder.putLocal("SearchQueryFormat", "UNIVERSAL");
        binder.putLocal("ftx", "");
        binder.putLocal("AdvSearch", "True");
        binder.putLocal("folderChildren", "");
        binder.putLocal("ResultCount", "200");
        binder.putLocal("SortField", "dInDate");
        binder.putLocal("SortOrder", "Desc");

        try {
            ServiceResponse response =
                idcClient.sendRequest(userContext, binder);
            DataBinder serverBinder = response.getResponseAsBinder();
              DataResultSet resultSet =
                serverBinder.getResultSet("SearchResults"); // loop over the results
         
            int z = 0;
            for (DataObject dataObject : resultSet.getRows()) {
                singleUpdate(dataObject.get("dDocName"),dataObject.get("dID"),dataObject.get("dRevLabel"),dataObject.get("dSecurityGroup"), "UCMMETADATAFIELD", "UCMMETADATAVALUE");
                z++;
                System.out.println(dataObject.get("dDocTitle")+"----"+z+"/"+resultSet.getRows().size());
                
            }
            return true;

        } catch (Exception ex) {
            System.out.println(ex);
        }

        return false;
    }

 public static boolean singleUpdate(String dDocName, String dID,
                                       String dRevLabel,
                                       String dSecurityGroup, String field, String value) {

        DataBinder binder = idcClient.createBinder();
        binder.putLocal("IdcService", "UPDATE_DOCINFO");
        binder.putLocal("dDocName", dDocName);
        binder.putLocal("dID", dID);
        binder.putLocal("dRevLabel", dRevLabel);
        binder.putLocal("dSecurityGroup", dSecurityGroup);
        binder.putLocal(field, value);
        

        try {
            ServiceResponse response =
                idcClient.sendRequest(userContext, binder);
            DataBinder serverBinder = response.getResponseAsBinder();
            //System.out.println(serverBinder.getLocalData().get("dStatus"));

            return true;

        } catch (Exception ex) {
            System.out.println(ex);
        }

        return false;
    }



     public static void init() {
        try {
            String ucmServer = "ucmServerURL";

            MassUpdater.idcClient =
                    MassUpdater.manager.createClient("idc://" + ucmServer +
                                                     ":4444");
            MassUpdater.idcClient.getConfig().setSocketTimeout(30000); // 30 seconds
            MassUpdater.idcClient.getConfig().setConnectionSize(20);
            userContext = new IdcContext("sysadmin");

        } catch (Exception ex) {
            System.out.println(ex);
        }

    }
}

You may want to extend this to take a Map of Metadata/Value pairs if you want to achieve true re usability.
You should also Paginate your results depending on your MaxQueryRows / ResultCount