Search LDAP from ITIM Workflow

The following java class allows free-form LDAP lookups from withing IBMJS scripts, like workflows or provisioning policies. You need to compile it and expose to your IBMJS by adding to scriptframework.properties something like this:

ITIM.extension.Workflow.search=com.ibm.itim.custom.ibmjsextensions.Search

You can then invoke it inside of your java script like so (use real names/DNs in your environments if copying this code):

var searchObj = new DirectoryObjectSearchTypes(); 
var ad_Role = directoryObjectSearch(searchObj.ROLE,"(errolename=AD Approver)",2);
if (ad_Role[0].dn=="erglobalid=1234,ou=roles,erglobalid=00000000000000000000,ou=test,dc=itim,dc=dom")
    Enrole.log("script","***DirectoryObjectSearchTypes.ROLE=success");
else
    Enrole.log("script","*!*DirectoryObjectSearchTypes.ROLE=failure. Returns " + ad_Role[0].dn );

var ad_ApproverList = directoryObjectSearch(searchObj.PERSON, "Person", "(erroles=" + ad_Role[0].dn + ")" , 2);
if (ad_ApproverList[0].dn=="erglobalid=1234,ou=0,ou=people,erglobalid=00000000000000000000,ou=test,dc=itim,dc=dom")
    Enrole.log("script","***DirectoryObjectSearchTypes.PERSON=success");
else
    Enrole.log("script","*!*DirectoryObjectSearchTypes.PERSON=failure. Returns " + ad_ApproverList[0].dn );

var serviceObject = ServiceSearch.searchByName("Active Directory Group Service",2)[0];
var results = directoryObjectSearch(searchObj.ACCOUNT, serviceObject, "ADGroup", "(eruid=Admin)",2);
if (results[0].dn=="erglobalid=1234,ou=0,ou=accounts,erglobalid=00000000000000000000,ou=test,dc=itim,dc=dom")
    Enrole.log("script","***DirectoryObjectSearchTypes.ACCOUNT.search=success.");
else
    Enrole.log("script","*!*DirectoryObjectSearchTypes.ACCOUNT.search=failure. Returns " + results[0].dn);

var RoleObject = directoryObjectLookup(searchObj.ACCOUNT, "erglobalid=1234,ou=0,ou=accounts,erglobalid=00000000000000000000,ou=test,dc=itim,dc=dom");
if (RoleObject.dn=="erglobalid=1234,ou=0,ou=accounts,erglobalid=00000000000000000000,ou=test,dc=itim,dc=dom")
    Enrole.log("script","***DirectoryObjectSearchTypes.ACCOUNT.lookup=success");
else 
    Enrole.log("script","*!*DirectoryObjectSearchTypes.ACCOUNT.lookup=failure. Returns " + RoleObject.dn );

var itimuser = directoryObjectSearch(searchObj.ITIM_USER, "(eruid=1234)"); 
if (itimuser[0].dn=="eruid=1234,ou=systemUser,ou=itim,ou=test,dc=itim,dc=dom")
    Enrole.log("script","***DirectoryObjectSearchTypes.ITIM_USER.lookup=success.");
else
    Enrole.log("script","***DirectoryObjectSearchTypes.ITIM_USER.lookup=fail. Returns " + itimuser[0].dn );]]>

The default list of java objects exposed to IBMJS via scriptframework.properties can be found here and here. Below is the class that implements the search functionality.

/********************************************************************
 * ITIM IBM JS Extension
 * Allow IBMJS scripts to search for directory objects regardless of type, and with a search base.
 * @author Brian Davis, Alex Ivkin
 ********************************************************************/
package com.ibm.itim.custom.ibmjsextensions;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;

import com.ibm.itim.dataservices.model.CompoundDN;
import com.ibm.itim.dataservices.model.DirectoryObjectEntity;
import com.ibm.itim.dataservices.model.DistinguishedName;
import com.ibm.itim.dataservices.model.ModelException;
import com.ibm.itim.dataservices.model.ObjectNotFoundException;
import com.ibm.itim.dataservices.model.SearchParameters;
import com.ibm.itim.dataservices.model.SearchResults;
import com.ibm.itim.dataservices.model.SearchResultsIterator;
import com.ibm.itim.dataservices.model.domain.AccountSearch;
import com.ibm.itim.dataservices.model.domain.DirectorySystemEntity;
import com.ibm.itim.dataservices.model.domain.DirectorySystemSearch;
import com.ibm.itim.dataservices.model.domain.OrganizationEntity;
import com.ibm.itim.dataservices.model.domain.OrganizationSearch;
import com.ibm.itim.dataservices.model.domain.OrganizationalContainer;
import com.ibm.itim.dataservices.model.domain.OrganizationalContainerEntity;
import com.ibm.itim.dataservices.model.domain.OrganizationalContainerSearch;
import com.ibm.itim.dataservices.model.domain.Service;
import com.ibm.itim.dataservices.model.domain.ServiceEntity;
import com.ibm.itim.dataservices.model.domain.ServiceModel;
import com.ibm.itim.dataservices.model.domain.ServiceSearch;
import com.ibm.itim.dataservices.model.system.SystemUserSearch;
import com.ibm.itim.logging.JLogUtil;
import com.ibm.itim.script.ContextItem;
import com.ibm.itim.script.GlobalFunction;
import com.ibm.itim.script.ScriptContextDAO;
import com.ibm.itim.script.ScriptEvaluationException;
import com.ibm.itim.script.ScriptException;
import com.ibm.itim.script.ScriptExtension;
import com.ibm.itim.script.ScriptInterface;
import com.ibm.itim.script.wrappers.ObjectWrapper;
import com.ibm.itim.script.wrappers.ObjectWrapperManager;
import com.ibm.log.Level;
import com.ibm.log.PDLogger;

public class Search implements ScriptExtension {

    // name for extension object name registration
    private static final String JS_CLASS_NAME = "directoryObjectSearch_Lookup";

    // real class name for log category
    private static final String CLASS_NAME = Search.class.getName();
    // trace file logger
    private static final PDLogger traceLogger = JLogUtil.getTraceLogger(Search.class);
    private static final PDLogger msgLogger   = JLogUtil.getMessageLogger(Search.class);
    // prefix text for all log messages
    private static final String LOG_PREFIX = "### JSExtensions:SearchExtension:" + JS_CLASS_NAME + " : ";

    private static final HashMap<Object,String> searchTypes = new HashMap<Object,String>();

    private List<ContextItem> items;

    public List getContextItems() {
        return items;
    }

    public void initialize(ScriptInterface si, ScriptContextDAO dao) throws ScriptException, IllegalArgumentException {
        items = new ArrayList<ContextItem>(3);
        ContextItem ciDOST = ContextItem.createConstructor("DirectoryObjectSearchTypes", DirObjectConstructor.class);
        ContextItem ciDOS = ContextItem.createGlobalFunction("directoryObjectSearch", new DirObjectSearchFunction(dao));
        ContextItem ciDOL = ContextItem.createGlobalFunction("directoryObjectLookup", new DirObjectLookupFunction(dao));
        items.add(ciDOST);
        items.add(ciDOS);
        items.add(ciDOL);
        return;
    }

    /*
     * Build an entity wrapper using an entity's value object.
     */
    private Object wrapEntity(Object entity, ScriptContextDAO dao) throws ScriptEvaluationException {
        //return ObjectWrapperManager.getInstance().wrap(null, (DirectoryObjectEntity) entity, null, null);

        // Add a context item to the scripting environment, so it can be disposed (memory freed) when the ScriptComtextDAO is disposed;
        // and return a wrapped version of the item (ObjectWrapper).
        return dao.addContextItem((DirectoryObjectEntity) entity);
    }

    /*
     * Get a reference to the ITIM type-specific search class that matches one
     * of the type specifier constants. These contants are static fields on this
     * class, and are stored as fields on each search object created by
     * DirObjectConstructor.
     */
    private Class getSearchClass(Object classSpecifier) throws ScriptEvaluationException {
        if (!(classSpecifier instanceof Double)) {
            logMsg(Level.DEBUG_MIN, "getSearchClass", "Search class specifier must be a double");
            throw new ScriptEvaluationException("Search class specifier must be a double");
        }
        String className = (String) Search.searchTypes.get(classSpecifier);
        if (className == null) {
            logMsg(Level.DEBUG_MIN, "getSearchClass", "No search class defined for " + classSpecifier);
            throw new ScriptEvaluationException("No search class defined for " + classSpecifier);
        }
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            //SystemLog.getInstance().logError(this, "Failed to load search class " + className, e);
            logException("getSearchClass", "Failed to load search class " + className, e);
            throw new ScriptEvaluationException("Failed to load search class " + className, e);
        }
    }

    public static class DirObjectConstructor {

        /*
         * These fields MUST be Doubles!! If you try to use Integer you will
         * break things.
         * 
         * The reason for this is that when IBMJS passes these values back to us
         * as the first argument of the search or lookup method it will convert
         * the JavaScript numeric value to the smallest subclass of Numeric that
         * will hold the value. So if you make these fields Integer IBMJS will
         * still pass them as Double in the args to the call methods. And when
         * you try to use the Double object as a lookup key in the searchTypes
         * map it will not match the Integer keys that you stored in the map. So
         * the values stored in searchTypes map must by Double.
         */

        public static final Double ACCOUNT = new Double((double) 100);
        public static final Double CONTAINER = new Double((double) 105);
        public static final Double ORG = new Double((double) 106);
        public static final Double PERSON = new Double((double) 107);
        public static final Double ROLE = new Double((double) 108);
        public static final Double SERVICE_GROUP = new Double((double) 109);
        public static final Double SERVICE = new Double((double) 110);
        public static final Double ITIM_GROUP = new Double((double) 111);
        public static final Double ITIM_USER = new Double((double) 112);

        public static final Double ONELEVEL_SCOPE = new Double((double) SearchParameters.ONELEVEL_SCOPE);
        public static final Double SUBTREE_SCOPE = new Double((double) SearchParameters.SUBTREE_SCOPE);

        static {
            searchTypes.put(ACCOUNT, "com.ibm.itim.dataservices.model.domain.AccountSearch");
            searchTypes.put(CONTAINER, "com.ibm.itim.dataservices.model.domain.OrganizationalContainerSearch");
            searchTypes.put(ORG, "com.ibm.itim.dataservices.model.domain.OrganizationSearch");
            searchTypes.put(PERSON, "com.ibm.itim.dataservices.model.domain.PersonSearch");
            searchTypes.put(ROLE, "com.ibm.itim.dataservices.model.domain.RoleSearch");
            searchTypes.put(SERVICE_GROUP, "com.ibm.itim.dataservices.model.domain.ServiceModel");
            searchTypes.put(SERVICE, "com.ibm.itim.dataservices.model.domain.ServiceSearch");
            searchTypes.put(ITIM_GROUP, "com.ibm.itim.dataservices.model.system.SystemRoleSearch");
            searchTypes.put(ITIM_USER, "com.ibm.itim.dataservices.model.system.SystemUserSearch");
        }

        /*
         * This method is called whenever someone does
         * "new DirectoryObjectSearch()" in JavaScript.
         */
        public DirObjectConstructor() {}
    }

    private class DirObjectSearchFunction implements GlobalFunction {

        private final boolean debugging = //SystemLog.getInstance().getPriorityLevel(this) == SystemLog.DEBUG_INFO;
            traceLogger.isLoggable(Level.DEBUG_MAX);

        /*
         * dao will give us access to the scripting environment.
         */
        private ScriptContextDAO dao;

        public DirObjectSearchFunction(ScriptContextDAO context) {
            dao = context;
        }

        /*
         * This method is called whenever someone calls the search
         * method on a search object.
         */

        public Object call(Object[] args) throws ScriptEvaluationException {
            if (args == null || args.length < 2 || args.length > 5) {
                //SystemLog.getInstance().logError(this, "Illegal arguments: search(targetType [,base] [,profile] ,filter [,scope])");
                logMsg(Level.DEBUG_MIN, "DirObjectSearchFunction.call", "Illegal arguments: search(targetType [,base] [,profile] ,filter [,scope])");
                throw new ScriptEvaluationException("Illegal arguments: search(targetType [,base] [,profile] ,filter [,scope])");
            }

            /*
             * The first argument must be the key for a dataservices *Search
             * class or ServiceModel.
             */
            Class searchClass = getSearchClass(args[0]);

            /*
             * The next argument might be a search base. We can tell by whether
             * the argument is an object wrapper.
             * 
             * DirectoryObjectWrapper isn't part of the official ITIM API. But
             * it's what is really created when you use
             * JSDirectoryObjectFactory.createDirectoryObject() to wrap the
             * entity elements we return from our searches. So when a user
             * passes one of these back as a base argument to another search
             * it's what you see as the object's class.
             * 
             * DirectoryObjectWrapper also has a getJavaObject method that
             * returns the original wrapped object. Using that saves us from
             * having to use the dn on the DirectoryObjectWrapper to do a lookup
             * of the object.
             */
            int nextArg = 1;
            Object base = args[1];
            if (this.debugging) {
                // SystemLog.getInstance().logDebug(this, "Checking arg 1 (" + args[1].getClass().getName() + ") for use as base");
                logMsg(Level.DEBUG_MAX, "DirObjectSearchFunction.call", "Checking arg 1 (" + args[1].getClass().getName() + ") for use as base");
            }
            if (base instanceof ObjectWrapper) {
                /*
                 * We have a base argument. Unwrap it. This will give us a value
                 * object for the base. Then we need to check whether its a
                 * valid type to be used as a base.
                 */
                base = ObjectWrapperManager.getInstance().lookupItem(((ObjectWrapper) base).getKey());

                if (searchClass.getName().equals(ServiceModel.class.getName())) {
                    /*
                     * If the search class is ServiceModel then the search base
                     * must be a Service.
                     */
                    if (!(base instanceof Service)) {
                        //SystemLog.getInstance().logError(this, "Service Group base must be a Service");
                        logMsg(Level.DEBUG_MIN, "DirObjectSearchFunction.call", "Service Group base must be a Service");
                        throw new ScriptEvaluationException("Service Group base must be a Service");
                    }
                } else if (searchClass.getName().equals(AccountSearch.class.getName())) {
                    /*
                     * If the search class is AccountSearch then the search base
                     * must be a Service.
                     */
                    if (!(base instanceof Service)) {
                        //SystemLog.getInstance().logError(this, "Account search base must be a Service");
                        logMsg(Level.DEBUG_MIN, "DirObjectSearchFunction.call", "Account search base must be a Service");
                        throw new ScriptEvaluationException("Account search base must be a Service");
                    }

                    /*
                     * Convert the base from a Service to the service's
                     * CompoundDN.
                     */
                    try {
                        DistinguishedName serviceDN = ((Service) base).getDistinguishedName();
                        ServiceEntity serviceEnt = new ServiceSearch().lookup(serviceDN);

                        /*
                         * Containers have a handy method that returns their
                         * CompoundDN. Services don't. So get the CompoundDN of
                         * the service's parent. Then convert that into the
                         * service's.
                         */
                        CompoundDN newBase = ((OrganizationalContainerEntity) serviceEnt.getParent()).getLogicalNameContext();

                        if (newBase.size() == 3)
                            /*
                             * Parent is a container. Replace its dn at the end
                             * of the compound dn.
                             */
                            newBase.replace(2, serviceDN);
                        else
                            /*
                             * Parent is an organization. Append the service dn
                             * to the compound dn.
                             */
                            newBase.append(serviceDN);
                        base = newBase;
                    } catch (Exception e) {
                        //SystemLog.getInstance().logError(this, "Error getting service's DN", e);
                        logException("DirObjectSearchFunction.call", "Error getting service's DN", e);
                        throw new ScriptEvaluationException("Error getting service's DN", e);
                    }
                } else {
                    /*
                     * All other search classes use CompoundDN as their base.
                     * Some can use OrganizationalContainerEntity, but not all.
                     * So we force the base to be a CompoundDN.
                     * 
                     * But the objects that users will be passing to us as bases
                     * will be wrappers for OrganizationalContainer objects. To
                     * turn these into CompoundDN objects we must use the dn
                     * from the wrapper to do a lookup. This will give is an
                     * entity object, and from that we use the
                     * getLogicalNameContext method to get the CompoundDN for
                     * that entity.
                     */
                    if (!(base instanceof OrganizationalContainer)) {
                        //SystemLog.getInstance().logError(this, "Base must be an OrganizationalContainer");
                        logMsg(Level.DEBUG_MIN, "DirObjectSearchFunction.call", "Base must be an OrganizationalContainer");
                        throw new ScriptEvaluationException("Base must be an OrganizationalContainer");
                    }
                    try {
                        base = new OrganizationalContainerSearch().lookup(((OrganizationalContainer) base).getDistinguishedName()).getLogicalNameContext();
                    } catch (Exception e) {
                        //SystemLog.getInstance().logError(this, "Error getting container's DN", e);
                        logException("DirObjectSearchFunction.call", "Error getting container's DN", e);
                        throw new ScriptEvaluationException("Error getting container's DN", e);
                    }
                }
                nextArg++;
            } else {
                /*
                 * No base was specified. Use the organization or tenant
                 * depending on the search type.
                 */
                if (this.debugging) {
                    //SystemLog.getInstance().logDebug(this, "Using default base");
                    logMsg(Level.DEBUG_MAX, "DirObjectSearchFunction.call", "Using default base");
                }
                DirectorySystemEntity tenant = null;
                try {
                    tenant = new DirectorySystemSearch().lookupDefault();
                } catch (ModelException e) {
                    //SystemLog.getInstance().logError(this, "Error getting tenant object", e);
                    logException("DirObjectSearchFunction.call", "Error getting tenant object", e);
                    throw new ScriptEvaluationException("Error getting tenant object", e);
                }
                if (searchClass.getName().equals(OrganizationSearch.class.getName()) || searchClass.getName().equals(SystemUserSearch.class.getName())) {
                    base = tenant.getLogicalNameContext();
                } else {
                    SearchResults results = null;
                    try {
                        OrganizationSearch orgSearch = new OrganizationSearch();

                        /*
                         * Most installations have only one organization. We
                         * will do a search for (o=*) and take the first result,
                         * assuming it's the only result. But for those few
                         * multiorganization installations we will first look
                         * for a "baseOrganization" field on the search object.
                         * Users will set this field to the organization they
                         * wish to use as their default search base for this
                         * search object.
                         */
                        Object baseOrganization = dao.lookupItem("baseOrganization");
                        if (baseOrganization != null) {
                            if (this.debugging) {
                                //SystemLog.getInstance().logDebug(this, "baseOrganization found");
                                logMsg(Level.DEBUG_MAX, "DirObjectSearchFunction.call", "baseOrganization found");
                            }
                            base = orgSearch.lookup(new DistinguishedName(baseOrganization.toString())).getLogicalNameContext();
                        } else {
                            results = orgSearch.searchByFilter(tenant.getDistinguishedName(), "(o=*)", new SearchParameters());
                            base = ((OrganizationEntity) results.iterator().next()).getLogicalNameContext();
                        }
                    } catch (ModelException e) {
                        //SystemLog.getInstance().logError(this, "Error getting organization object", e);
                        logException("DirObjectSearchFunction.call", "Error getting organization object", e);
                        throw new ScriptEvaluationException("Error getting organization object", e);
                    } finally {
                        if (results != null)
                            results.close();
                    }
                }
            }
            if (this.debugging) {
                //SystemLog.getInstance().logDebug(this, "Base is " + base);
                logMsg(Level.DEBUG_MAX, "DirObjectSearchFunction.call", "Base is " + base);
            }

            /*
             * The next argument might be a profile name. There is no way to
             * tell by the arg's class, but the number of arguments remaining
             * will tell. All of the search classes that accept a profile also
             * require a filter and scope. So if there are 3 arguments
             * remaining, they must be profile, filter and scope. Otherwise it's
             * filter and scope, or just filter.
             */
            String profile = null;
            if (args.length - nextArg == 3) {
                profile = args[nextArg++].toString();
                if (this.debugging) {
                    //SystemLog.getInstance().logDebug(this, "Profile is " + profile);
                    logMsg(Level.DEBUG_MAX, "DirObjectSearchFunction.call", "Profile is " + profile);
                }
            }

            /*
             * The next argument is the search filter.
             */
            String filter = args[nextArg++].toString();
            if (this.debugging) {
                //SystemLog.getInstance().logDebug(this, "Filter is " + filter);
                logMsg(Level.DEBUG_MAX, "DirObjectSearchFunction.call", "Filter is " + filter);
            }

            /*
             * The next argument, if it exists, is the scope.
             */
            Object scopeArg = (nextArg == args.length) ? null : args[nextArg];
            if (this.debugging) {
                //SystemLog.getInstance().logDebug(this, "Scope is " + scopeArg);
                logMsg(Level.DEBUG_MAX, "DirObjectSearchFunction.call", "Scope is " + scopeArg);
            }
            if (scopeArg != null && !(scopeArg instanceof Number)) {
                logMsg(Level.DEBUG_MIN, "DirObjectSearchFunction.call", "Illegal search scope: must be numeric, not " + scopeArg.getClass().getName());
                throw new ScriptEvaluationException("Illegal search scope: must be numeric, not " + scopeArg.getClass().getName());
            }

            /*
             * If no scope argument was given use one level as the default.
             */
            int scope = (scopeArg == null) ? SearchParameters.ONELEVEL_SCOPE : ((Number) scopeArg).intValue();
            if (scope != SearchParameters.ONELEVEL_SCOPE && scope != SearchParameters.SUBTREE_SCOPE) {
                logMsg(Level.DEBUG_MIN, "DirObjectSearchFunction.call", "Illegal search scope (" + scope + "): must be "
                        + SearchParameters.ONELEVEL_SCOPE + " or " + SearchParameters.SUBTREE_SCOPE);
                throw new ScriptEvaluationException("Illegal search scope (" + scope + "): must be "
                        + SearchParameters.ONELEVEL_SCOPE + " or " + SearchParameters.SUBTREE_SCOPE);
            }

            /*
             * Create a SearchParameters with no limit on the number of results,
             * all attributes returned, and the scope specified by the caller.
             */
            SearchParameters params = new SearchParameters();
            params.setScope(scope);

            /*
             * Use reflection to get an instance of the search class, and find
             * the method to call on that class given the argument list.
             */
            Object searchInstance;
            Method searchMethod;
            String methodName = null;
            Object[] searchArgs;
            Class[] searchArgClasses = null;
            try {
                if (searchClass.getName().equals(ServiceModel.class.getName())) {
                    /*
                     * The ServiceModel constructor takes a service's
                     * DistinguishedName as an argument.
                     */
                    Constructor cons = searchClass.getConstructor(new Class[] { DistinguishedName.class });
                    searchInstance = cons.newInstance(new Object[] { ((Service) base).getDistinguishedName() });
                    methodName = "getByFilter";
                    searchArgClasses = new Class[] { String.class };
                    searchMethod = searchClass.getMethod(methodName, searchArgClasses);
                    searchArgs = new Object[] { filter };
                } else {
                    /*
                     * All of the other search classes have a 0-argument
                     * constructor.
                     */
                    Constructor cons = searchClass.getConstructor(new Class[0]);
                    searchInstance = cons.newInstance(new Object[0]);
                    searchArgClasses = (profile == null ? new Class[] { base.getClass(), filter.getClass(), params.getClass() } : new Class[] {
                            base.getClass(), profile.getClass(), filter.getClass(), params.getClass() });
                    methodName = "searchByFilter";
                    searchMethod = searchClass.getMethod(methodName, searchArgClasses);
                    searchArgs = (profile == null ? new Object[] { base, filter, params } : new Object[] { base, profile, filter, params });
                }
            } catch (Exception e) {
                StringBuffer classList = new StringBuffer();
                if (searchArgClasses == null)
                    classList.append("???");
                else {
                    for (int i = 0; i < searchArgClasses.length; i++) {
                        classList.append(searchArgClasses[i].getName());
                        classList.append(',');
                    }
                    classList.setLength(classList.length() - 1);
                }
                String msg = "Error getting search method for " + searchClass.getName() + "." + methodName + "(" + classList + ")";
                //SystemLog.getInstance().logError(this, msg, e);
                logException("DirObjectSearchFunction.call", msg, e);
                throw new ScriptEvaluationException(msg, e);
            }

            /*
             * Call the search method and wrap the results.
             */
            SearchResults results = null;
            try {
                results = (SearchResults) searchMethod.invoke(searchInstance, searchArgs);
                SearchResultsIterator srIter = results.iterator();
                Vector<Object> wrappedResults = new Vector<Object>();
                while (srIter.hasNext()) {
                    wrappedResults.add(wrapEntity(srIter.next(), dao));
                }
                //if (this.debugging) {
                    logMsg(Level.DEBUG_MID, "DirObjectSearchFunction.call", "Returning " + wrappedResults.size() + " results for "
                            + "searchClass=" + searchClass.getName()
                            + "; base=" + base
                            + "; profile=" + profile
                            + "; filter=" + filter
                            + "; scope=" + scopeArg);
                //}
                return wrappedResults.toArray();
            } catch (Exception e) {
                //SystemLog.getInstance().logError(this, "Search for directory objects failed", e);
                logException("DirObjectSearchFunction.call", "Search for directory objects failed", e);
                throw new ScriptEvaluationException("Search for directory objects failed", e);
            } finally {
                if (results != null)
                    results.close();
            }
        }

    }

    private class DirObjectLookupFunction implements GlobalFunction {

        /*
         * DAO will give us access to the scripting environment.
         */
        private ScriptContextDAO dao;

        public DirObjectLookupFunction(ScriptContextDAO context) {
            dao = context;
        }

        /*
         * This method is called whenever someone calls the lookup method on a
         * search object.
         */
        public Object call(Object[] args) throws ScriptEvaluationException {
            if (args.length != 2) {
                logMsg(Level.DEBUG_MIN, "DirObjectLookupFunction.call", "Illegal Arguments: DirectoryObject(searchClass, dn)");
                throw new ScriptEvaluationException("Illegal Arguments: DirectoryObject(searchClass, dn)");
            }
            Class searchClass = getSearchClass((Double) args[0]);
            DistinguishedName searchArg = new DistinguishedName(args[1].toString());

            /*
             * Use reflection to create an instance of the search class and call
             * its lookup method.
             */
            Object searchInstance;
            Method searchMethod;
            String methodName = null;
            Class[] searchArgClasses = null;
            try {
                Constructor cons = searchClass.getConstructor(new Class[0]);
                searchInstance = cons.newInstance(new Object[0]);
                searchArgClasses = new Class[] { searchArg.getClass() };
                methodName = "lookup";
                searchMethod = searchClass.getMethod(methodName, searchArgClasses);
            } catch (Exception e) {
                StringBuffer classList = new StringBuffer();
                if (searchArgClasses == null)
                    classList.append("???");
                else {
                    for (int i = 0; i < searchArgClasses.length; i++) {
                        classList.append(searchArgClasses[i].getName());
                        classList.append(',');
                    }
                    classList.setLength(classList.length() - 1);
                }
                String msg = "Error getting search method for " + searchClass.getName() + "." + methodName + "(" + classList + ")";
                logException("DirObjectLookupFunction.call", msg, e);
                throw new ScriptEvaluationException(msg, e);
            }
            try {
                Object wrappedResult = wrapEntity(searchMethod.invoke(searchInstance, new Object[] { searchArg }), dao);
                //if (this.debugging) {
                    logMsg(Level.DEBUG_MID, "DirObjectLookupFunction.call", "Returning result for "
                            + "searchClass=" + searchClass.getName()
                            + "; searchArg=" + searchArg);
                //}
                return wrappedResult;
            } catch (Exception e) {
                if (e instanceof ObjectNotFoundException) {
                    logMsg(Level.DEBUG_MIN, "DirObjectLookupFunction.call", "ObjectNotFoundException, returning null");
                    return null;
                }
                //SystemLog.getInstance().logError(this, "Lookup failed", e);
                logException("DirObjectLookupFunction.call", "Lookup failed", e);
                throw new ScriptEvaluationException("Lookup failed", e);
            }
        }
    }

    private static void logMsg(Level logLevel, String methodName, String text) {
        traceLogger.text(logLevel, CLASS_NAME, methodName, LOG_PREFIX + text);
        Level msgLevel = logLevel;
        // Message logger (msg.log) typically only logs events at INFO or above, so map DEBUG levels to these
        // higher index values are higher priority levels
        if (logLevel.getValue() < Level.INFO_INDEX) {
            if (logLevel.getValue() >= Level.DEBUG_MIN_INDEX)   // DEBUG_MIN => ERROR   
                msgLevel = Level.ERROR;
            else if(logLevel.getValue() >= Level.DEBUG_MID_INDEX)   // DEBUG_MID => WARN
                msgLevel = Level.WARN;
            else {  // logLevel >= Level.DEBUG_MAX_INDEX)       // DEBUG_MAX => INFO
                msgLevel = Level.INFO;
            }
        }
        msgLogger.text(msgLevel, CLASS_NAME, methodName, LOG_PREFIX + text);
    }

    private static void logException(String methodName, String text, Exception e) {
        traceLogger.text(Level.DEBUG_MIN, CLASS_NAME, methodName, LOG_PREFIX + text, e);
        msgLogger.text(Level.ERROR, CLASS_NAME, methodName, LOG_PREFIX + text, e);
    }
}

@Tools @ITIM