com.ottogroup.bi.asap.repository.ComponentClassloader.java Source code

Java tutorial

Introduction

Here is the source code for com.ottogroup.bi.asap.repository.ComponentClassloader.java

Source

/**
 * Copyright 2014 Otto (GmbH & Co KG)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ottogroup.bi.asap.repository;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import com.ottogroup.bi.asap.component.Component;
import com.ottogroup.bi.asap.component.annotation.AsapComponent;
import com.ottogroup.bi.asap.exception.ComponentInstantiationFailedException;
import com.ottogroup.bi.asap.exception.RequiredInputMissingException;
import com.ottogroup.bi.asap.exception.UnknownComponentException;

/**
 * Reads the contents of a provided jar (array of bytes), extracts all {@link AsapComponent asap components} and allows
 * to access them by providing the component name as well as its version
 * @author mnxfst
 * @since Oct 29, 2014
 * TODO how to manage multiple resources having the same name but being located in differen JAR files?
 * TODO add performance tracking while loading classes
 * TODO more testing on initialize and newInstance
 */
public class ComponentClassloader extends ClassLoader {

    private static final Logger logger = Logger.getLogger(ComponentClassloader.class);

    /** list of jar files which contain pipeline components and thus must be scanned for deployment descriptors */
    private Set<String> componentJarFiles = new HashSet<String>();
    /** mapping from class file to JAR to load it from */
    private Map<String, String> classesJarMapping = new HashMap<>();
    /** mapping from resources to JAR to load it from */
    private Map<String, String> resourcesJarMapping = new HashMap<>();
    /** already resolved classes */
    private Map<String, Class<?>> cachedClasses = new HashMap<>();
    /** managed pipeline components */
    private Map<String, ComponentDescriptor> managedComponents = new HashMap<>();

    /**
     * Initializes the class using the provided input
     * @param parentClassLoader
     */
    public ComponentClassloader(final ClassLoader parentClassLoader) {
        super(parentClassLoader);
    }

    /**
     * Loads referenced class:
     * <ul>
     *   <li>find in already loaded classes</li>
     *   <li>look it up in previously loaded and thus cached classes</li>
     *   <li>find it in jar files</li>
     *   <li>ask parent class loader</li>
     * </ul>
     * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean)
     */
    public Class<?> loadClass(String name) throws ClassNotFoundException {

        synchronized (getClassLoadingLock(name)) {

            // check if class has already been loaded
            Class<?> clazz = findLoadedClass(name);
            if (clazz != null)
                return clazz;

            // check internal cache for already loaded classes
            clazz = cachedClasses.get(name);
            if (clazz != null) {
                return clazz;
            }

            // check if the managed jars contain the class
            if (classesJarMapping.containsKey(name))
                clazz = findClass(name);
            if (clazz != null) {
                return clazz;
            }

            // otherwise hand over the request to the parent class loader
            clazz = super.loadClass(name);
            if (clazz == null)
                throw new ClassNotFoundException("Class '" + name + "' not found");

            return clazz;
        }
    }

    /**
     * Find class inside managed jars or hand over the parent
     * @see java.lang.ClassLoader#findClass(java.lang.String)
     */
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        // find in already loaded classes to make things shorter
        Class<?> clazz = findLoadedClass(name);
        if (clazz != null)
            return clazz;

        // check if the class searched for is contained inside managed jars,
        // otherwise hand over the request to the parent class loader
        String jarFileName = this.classesJarMapping.get(name);
        if (StringUtils.isBlank(jarFileName)) {
            super.findClass(name);
        }

        // try to find class inside jar the class name is associated with
        JarInputStream jarInput = null;
        try {
            // open a stream on jar which contains the class
            jarInput = new JarInputStream(new FileInputStream(jarFileName));
            // ... and iterate through all entries
            JarEntry jarEntry = null;
            while ((jarEntry = jarInput.getNextJarEntry()) != null) {

                // extract the name of the jar entry and check if it has suffix '.class' and thus contains
                // the search for implementation
                String entryFileName = jarEntry.getName();
                if (entryFileName.endsWith(".class")) {

                    // replace slashes by dots, remove suffix and compare the result with the searched for class name 
                    entryFileName = entryFileName.substring(0, entryFileName.length() - 6).replace('/', '.');
                    if (name.equalsIgnoreCase(entryFileName)) {

                        // load bytes from jar entry and define a class over it
                        byte[] data = loadBytes(jarInput);
                        if (data != null) {
                            return defineClass(name, data, 0, data.length);
                        }

                        // if the jar entry does not contain any data, throw an exception
                        throw new ClassNotFoundException("Class '" + name + "' not found");
                    }
                }
            }
        } catch (IOException e) {
            // if any io error occurs: throw an exception
            throw new ClassNotFoundException("Class '" + name + "' not found");
        } finally {
            try {
                jarInput.close();
            } catch (IOException e) {
                logger.error("Failed to close open JAR file '" + jarFileName + "'. Error: " + e.getMessage());
            }
        }

        // if no such class exists, throw an exception
        throw new ClassNotFoundException("Class [" + name + "] not found");

    }

    /**
     * Handle resource lookups by first checking the managed JARs and hand
     * it over to the parent if no entry exits
     * @see java.lang.ClassLoader#getResourceAsStream(java.lang.String)
     */
    public InputStream getResourceAsStream(String name) {

        // lookup the name of the JAR file holding the resource 
        String jarFileName = this.resourcesJarMapping.get(name);

        // if there is no such file, hand over the request to the parent class loader
        if (StringUtils.isBlank(jarFileName))
            return super.getResourceAsStream(name);

        // try to find the resource inside the jar it is associated with
        JarInputStream jarInput = null;
        try {
            // open a stream on jar which contains the class
            jarInput = new JarInputStream(new FileInputStream(jarFileName));
            // ... and iterate through all entries
            JarEntry jarEntry = null;
            while ((jarEntry = jarInput.getNextJarEntry()) != null) {

                // extract the name of the jar entry and check if it is equal to the provided name
                String entryFileName = jarEntry.getName();
                if (StringUtils.equals(name, entryFileName)) {

                    // load bytes from jar entry and return it as stream
                    byte[] data = loadBytes(jarInput);
                    if (data != null)
                        return new ByteArrayInputStream(data);
                }
            }
        } catch (IOException e) {
            logger.error("Failed to read resource '" + name + "' from JAR file '" + jarFileName + "'. Error: "
                    + e.getMessage());
        } finally {
            try {
                jarInput.close();
            } catch (IOException e) {
                logger.error("Failed to close open JAR file '" + jarFileName + "'. Error: " + e.getMessage());
            }
        }

        return null;
    }

    /**
     * Initializes the class loader by pointing it to folder holding managed JAR files
     * @param componentFolder
     * @param componentJarIdentifier file to search for in component JAR which identifies it as component JAR 
     * @throws IOException
     * @throws RequiredInputMissingException
     */
    public void initialize(final String componentFolder, final String componentJarIdentifier)
            throws IOException, RequiredInputMissingException {

        ///////////////////////////////////////////////////////////////////
        // validate input
        if (StringUtils.isBlank(componentFolder))
            throw new RequiredInputMissingException("Missing required value for parameter 'componentFolder'");

        File folder = new File(componentFolder);
        if (!folder.isDirectory())
            throw new IOException("Provided input '" + componentFolder + "' does not reference a valid folder");

        File[] jarFiles = folder.listFiles();
        if (jarFiles == null || jarFiles.length < 1)
            throw new RequiredInputMissingException("No JAR files found in folder '" + componentFolder + "'");
        //
        ///////////////////////////////////////////////////////////////////

        logger.info("Initializing component classloader [componentJarIdentifier=" + componentJarIdentifier
                + ", folder=" + componentFolder + "]");

        // step through jar files, ensure it is a file and iterate through its contents
        for (File jarFile : jarFiles) {
            if (jarFile.isFile()) {

                JarInputStream jarInputStream = null;
                try {
                    jarInputStream = new JarInputStream(new FileInputStream(jarFile));
                    JarEntry jarEntry = null;
                    while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {
                        String jarEntryName = jarEntry.getName();
                        // if the current file references a class implementation, replace slashes by dots, strip 
                        // away the class suffix and add a reference to the classes-2-jar mapping 
                        if (StringUtils.endsWith(jarEntryName, ".class")) {
                            jarEntryName = jarEntryName.substring(0, jarEntryName.length() - 6).replace('/', '.');
                            this.classesJarMapping.put(jarEntryName, jarFile.getAbsolutePath());
                        } else {
                            // if the current file references a resource, check if it is the identifier file which
                            // marks this jar to contain component implementation                     
                            if (StringUtils.equalsIgnoreCase(jarEntryName, componentJarIdentifier))
                                this.componentJarFiles.add(jarFile.getAbsolutePath());
                            // ...and add a mapping for resource to jar file as well                     
                            this.resourcesJarMapping.put(jarEntryName, jarFile.getAbsolutePath());
                        }
                    }
                } catch (Exception e) {
                    logger.error("Failed to read from JAR file '" + jarFile.getAbsolutePath() + "'. Error: "
                            + e.getMessage());
                } finally {
                    try {
                        jarInputStream.close();
                    } catch (Exception e) {
                        logger.error("Failed to close open JAR file '" + jarFile.getAbsolutePath() + "'. Error: "
                                + e.getMessage());
                    }
                }
            }
        }

        // load classes from jars marked component files and extract the deployment descriptors
        for (String cjf : this.componentJarFiles) {
            logger.info("Attempting to load pipeline components located in '" + cjf + "'");

            // open JAR file and iterate through it's contents
            JarInputStream jarInputStream = null;
            try {
                jarInputStream = new JarInputStream(new FileInputStream(cjf));
                JarEntry jarEntry = null;
                while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {

                    // fetch name of current entry and ensure it is a class file
                    String jarEntryName = jarEntry.getName();
                    if (jarEntryName.endsWith(".class")) {
                        // replace slashes by dots and strip away '.class' suffix
                        jarEntryName = jarEntryName.substring(0, jarEntryName.length() - 6).replace('/', '.');
                        Class<?> c = loadClass(jarEntryName);
                        AsapComponent pc = c.getAnnotation(AsapComponent.class);
                        if (pc != null) {
                            this.managedComponents.put(getManagedComponentKey(pc.name(), pc.version()),
                                    new ComponentDescriptor(c.getName(), pc.type(), pc.name(), pc.version(),
                                            pc.description()));
                            logger.info("pipeline component found [type=" + pc.type() + ", name=" + pc.name()
                                    + ", version=" + pc.version() + "]");
                            ;
                        }
                    }
                }
            } catch (Exception e) {
                logger.error("Failed to read from JAR file '" + cjf + "'. Error: " + e.getMessage());
            } finally {
                try {
                    jarInputStream.close();
                } catch (Exception e) {
                    logger.error("Failed to close open JAR file '" + cjf + "'. Error: " + e.getMessage());
                }
            }
        }
    }

    /**
     * Looks up the reference {@link DataComponent}, instantiates it and passes over the provided {@link Properties} 
     * @param name name of component to instantiate (required)
     * @param version version of component to instantiate (required)
     * @param properties properties to use for instantiation
     * @return
     * @throws RequiredInputMissingException thrown in case a required parameter value is missing
     * @throws ComponentInstantiationFailedException thrown in case the instantiation failed for any reason
     * @throws UnknownComponentException thrown in case the name and version combination does not reference a managed component
     */
    public Component newInstance(final String name, final String version, final Properties properties)
            throws RequiredInputMissingException, ComponentInstantiationFailedException, UnknownComponentException {

        //////////////////////////////////////////////////////////////////////////////////////////
        // validate provided input      
        if (StringUtils.isBlank(name)) {
            throw new RequiredInputMissingException("Missing required component name");
        }
        if (StringUtils.isBlank(version)) {
            throw new RequiredInputMissingException("Missing required component version");
        }
        //
        //////////////////////////////////////////////////////////////////////////////////////////

        // generate component key and retrieve the associated descriptor ... if available
        String componentKey = getManagedComponentKey(name, version);
        ComponentDescriptor descriptor = this.managedComponents.get(componentKey);
        if (descriptor == null)
            throw new UnknownComponentException("Unknown component [name=" + name + ", version=" + version + "]");

        // if the descriptor exists, load the referenced component class, create an instance and hand over the properties
        try {
            Class<?> messageHandlerClass = loadClass(descriptor.getComponentClass());
            Component instance = (Component) messageHandlerClass.newInstance();
            instance.init(properties);
            return instance;
        } catch (IllegalAccessException e) {
            throw new ComponentInstantiationFailedException("Failed to instantiate component [name=" + name
                    + ", version=" + version + ", reason=" + e.getMessage());
        } catch (InstantiationException e) {
            throw new ComponentInstantiationFailedException("Failed to instantiate component [name=" + name
                    + ", version=" + version + ", reason=" + e.getMessage());
        } catch (ClassNotFoundException e) {
            throw new ComponentInstantiationFailedException("Failed to instantiate component [name=" + name
                    + ", version=" + version + ", reason=" + e.getMessage());
        }
    }

    /**
     * Returns true in case the referenced component managed by this class loader
     * @param name
     * @param version
     * @return
     */
    public boolean isManagedComponent(final String name, final String version) {
        return this.managedComponents.containsKey(getManagedComponentKey(name, version));
    }

    /**
     * Generates a key to be used for looking up the component+version specific class
     * @param name
     * @param version
     * @return
     */
    public String getManagedComponentKey(final String name, final String version) {
        StringBuffer buf = new StringBuffer(name.trim().toLowerCase()).append("~")
                .append(version.trim().toLowerCase());
        return buf.toString();
    }

    /**
     * Reads jar file contents from stream
     * @param is jar input stream
     * @throws IOException
     */
    protected byte[] loadBytes(InputStream is) throws IOException {
        BufferedInputStream bufInputStream = new BufferedInputStream(is, 8192);
        ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream();
        int c;
        int bufferSize = 8192;
        byte[] buffer = new byte[bufferSize];
        while ((c = bufInputStream.read(buffer)) != -1) {
            byteOutStream.write(buffer, 0, c);
        }
        return byteOutStream.toByteArray();
    }

    /**
     * @return the managedComponents
     */
    public Map<String, ComponentDescriptor> getManagedComponents() {
        return managedComponents;
    }

}