Got Conflicting IDM Driver Dependencies?

0 Likes
over 6 years ago
Note: This solution has been developed as a proof of concept in a lab, but hasn't been battle-tested yet in a production environment.

 

When writing an IDM Driver, it's easy to have dependencies which conflict with another driver's dependencies.

For example, let's say your driver depends on commons-lang-2.6.jar, but other drivers are present which depend on commons-lang-2.0.jar.  So because all the drivers are running in the same JVM, and share the same classloader, there's a good chance the classes from commons-lang-2.0.jar will get loaded first, since commons-lang-2.0.jar comes alphabetically before commons-lang-2.6.jar.  And since your driver is forced to run against an older set of classes, your driver could end up throwing a ClassNotFoundException, or NoSuchMethodException when trying to load them.

One way to address this problem is to use the remote driver feature of IDM.  However, if you don't want the overhead of running a separate JVM per driver, here's a little trick to ensure a driver's dependencies aren't conflicting with another's...




Java EE addresses this same issue, giving each WAR module of an EAR its own class loader.  This allows separate namespaces for applications in the same container, and ensures each web application can introduce its own set of dependencies, without conflicting with another web application.

So couldn't this same technique be applied to IDM drivers?

While it would be great if Identity Manager were updated to behave like a Java EE container, we can accomplish something similar ourselves by introducing a proxy driver class, which loads the real driver class and dependencies into its own special classloader.

To do this in a reusable way, here is an example of a base class, which our drivers could inherit from in order to get the "proxy-ing" functionality:
import java.io.File;
import java.io.FilenameFilter;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

public abstract class BaseDriverShimProxy implements DriverShim {
    private ChildFirstURLClassLoader dependenciesClassLoader;
    private DriverShim delegate;

    public BaseDriverShimProxy() {
        try {
            URL sourceUrl = new URL(this.getClass().getProtectionDomain().getCodeSource().getLocation(), getDependenciesFolder());
            File file = new File(sourceUrl.toURI());

            File[] jars = file.listFiles(new FilenameFilter() {
                public boolean accept(File dir, String name) {
                    if (name.endsWith(".jar")) {
                        return true;
                    }

                    return false;
                }
            });

            List<URL> jarUrls = new ArrayList<URL>(jars.length);
            for (File jarFile : jars) {
                jarUrls.add(jarFile.toURI().toURL());
            }

            dependenciesClassLoader = new ChildFirstURLClassLoader(jarUrls.toArray(new URL[jarUrls.size()]), this.getClass().getClassLoader());
            Class<?> delegateClass = dependenciesClassLoader.loadClass(getDelegateFullyQualifiedName());
            delegate = (DriverShim)delegateClass.newInstance();
        }
        catch (Exception e) {
            throw new RuntimeException("Unable to load the class", e);
        }
    }

    /**
     * @return The subfolder where the classpath will be built, and the DriverShim delegate will be loaded from.
     */
    protected abstract String getDependenciesFolder();

    /**
     * @return The fully-qualified name of the DriverShim to which this proxy delegates to.
     */
    protected abstract String getDelegateFullyQualifiedName();

    //
    //  Pass all calls to this DriverShim along to the delegate:
    //

    @Override
    public PublicationShim getPublicationShim() {
        return delegate.getPublicationShim();
    }

    @Override
    public XmlDocument getSchema(XmlDocument arg0) {
        return delegate.getSchema(arg0);
    }

    @Override
    public SubscriptionShim getSubscriptionShim() {
        return delegate.getSubscriptionShim();
    }

    @Override
    public XmlDocument init(XmlDocument arg0) {
        return delegate.init(arg0);
    }

    @Override
    public XmlDocument shutdown(XmlDocument arg0) {
        return delegate.shutdown(arg0);
    }
}

With this class in place, all you would need to do is create your own driver shim proxy, which extends BaseDriverShimProxy as follows:
public class MyDriverShimProxy extends BaseDriverShimProxy {
    @Override
    protected String getDependenciesFolder() {
        return "myshim";
    }

    @Override
    protected String getDelegateFullyQualifiedName() {
        return "com.mycompany.myshim.MyDriverShim";
    }
}

You'll notice MyDriverShimProxy only overrides two methods: getDependenciesFolder and getDelegateFullyQualifiedName.  getDependenciesFolder returns the name of the folder where the dependencies for the delegate (or target) driver shim are located.  These dependencies will only be available to that driver shim.  getDelegateFullyQualifiedName returns the fully qualified name of the driver the proxy class should delegate invocations to.

The final thing needed to make this work, is to introduce a new kind of classloader, which loads classes from the child first before looking to the parent.  The ChildFirstURLClassLoader is listed below:
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandlerFactory;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.util.Enumeration;
import java.util.NoSuchElementException;

/**
 * The ChildFirstURLClassLoader alters regular ClassLoader delegation and will
 * check the URLs used in its initialization for matching classes before
 * delegating to it's parent. Sometimes also referred to as a
 * ParentLastClassLoader
 */
public class ChildFirstURLClassLoader extends URLClassLoader {

    static final PermissionCollection allPermissions = new PermissionCollection() {
        private static final long serialVersionUID = 482874725021998286L;

        // The AllPermission permission
        Permission allPermission = new AllPermission();

        // A simple PermissionCollection that only has AllPermission
        public void add(Permission permission) {
            // do nothing
        }

        public boolean implies(Permission permission) {
            return true;
        }

        @SuppressWarnings("unchecked")
        public Enumeration elements() {
            return new Enumeration() {
                int cur = 0;

                public boolean hasMoreElements() {
                    return cur < 1;
                }

                public Object nextElement() {
                    if (cur == 0) {
                        cur = 1;
                        return allPermission;
                    }
                    throw new NoSuchElementException();
                }
            };
        }
    };

    static {
        // We do this to ensure the anonymous Enumeration class in
        // allPermissions is pre-loaded
        if (allPermissions.elements() == null) {
            throw new IllegalStateException();
        }
    }

    public ChildFirstURLClassLoader(URL[] urls) {
        super(urls);
    }

    public ChildFirstURLClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public ChildFirstURLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }

    public URL getResource(String name) {
        URL resource = findResource(name);
        if (resource == null) {
            ClassLoader parent = getParent();
            if (parent != null) {
                resource = parent.getResource(name);
            }
        }
        return resource;
    }

    @SuppressWarnings("unchecked")
    public synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class clazz = findLoadedClass(name);
        if (clazz == null) {
            try {
                clazz = findClass(name);
            } catch (ClassNotFoundException e) {
                ClassLoader parent = getParent();
                if (parent != null) {
                    clazz = parent.loadClass(name);
                } else {
                    clazz = getSystemClassLoader().loadClass(name);
                }
            }
        }

        if (resolve) {
            resolveClass(clazz);
        }

        return clazz;
    }

    // we want to ensure that the framework has AllPermissions
    public PermissionCollection getPermissions(CodeSource codesource) {
        return allPermissions;
    }
}

When you install your IDM driver, you will want to put BaseDriverShimProxy, MyDriverShimProxy, and ChildFirstURLClassLoader in its own jar file, and place it in the standard location.  On windows that would be:
C:\NetIQ\IdentityManager\NDS\lib

The real driver shim can now be placed in a subfolder identified by the method getDependenciesFolder of the MyDriverShimProxy class.  In our case, that would be:
C:\NetIQ\IdentityManager\NDS\lib\myshim

Now you should be able to configure your driver shim as normal, making sure you specify the driver class as: com.mycompany.myshim.MyDriverShimProxy instead of com.mycompany.myshim.MyDriverShim, and calls will be forwarded from one to the other.

Just remember, that by doing this, you open yourself up to a set of gotchas often found in J2EE containers.  Here's a video explaining if you're not careful, the problems you can run into:

https://www.youtube.com/watch?v=t8sQw3pGJzM

Also make sure you only put the dependencies you need in C:\NetIQ\IdentityManager\NDS\lib\myshim.  You don't want to duplicate the jars already provided by Identity Manager, otherwise you'll get errors mentioned in the video above.  In other words, don't put the following jars in your shim's folder:

  • collections.jar

  • CommonDriverShim.jar

  • dirxml.jar

  • dirxml_misc.jar

  • jsse.jar

  • ldap.jar

  • mapdb-1.0.1.jar

  • nxsl.jar

  • xp.jar


Did this solution work for you?  Please post feedback and/or questions in the comments below.

References:

Labels:

How To-Best Practice
Comment List
Anonymous
Related Discussions
Recommended