org.kitodo.serviceloader.KitodoServiceLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.kitodo.serviceloader.KitodoServiceLoader.java

Source

/*
 * (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
 *
 * This file is part of the Kitodo project.
 *
 * It is licensed under GNU General Public License version 3 or later.
 *
 * For the full copyright and license information, please read the
 * GPL3-License.txt file that was distributed with this source code.
 */

package org.kitodo.serviceloader;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.faces.context.FacesContext;
import javax.servlet.http.HttpSession;

import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.kitodo.config.KitodoConfig;

public class KitodoServiceLoader<T> {
    private Class clazz;
    private String modulePath = "";

    private static final String POM_PROPERTIES_FILE = "pom.properties";
    private static final String ARTIFACT_ID_PROPERTY = "artifactId";
    private static final String TEMP_DIR_PREFIX = "kitodo_";
    private static final String META_INF_FOLDER = "META-INF";
    private static final String RESOURCES_FOLDER = "resources";
    private static final String PAGES_FOLDER = "pages";
    private static final String JAR = "*.jar";
    private static final String ERROR = "Classpath could not be accessed";

    private static final Path SYSTEM_TEMP_FOLDER = FileSystems.getDefault()
            .getPath(System.getProperty("java.io.tmpdir"));

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

    /**
     * Constructor for KitodoServiceLoader.
     *
     * @param clazz
     *            interface class of module to load
     */
    public KitodoServiceLoader(Class clazz) {
        String modulesDirectory = KitodoConfig.getKitodoModulesDirectory();
        this.clazz = clazz;
        if (!new File(modulesDirectory).exists()) {
            logger.error("Specified module folder does not exist: {}", modulesDirectory);
        } else {
            this.modulePath = modulesDirectory;
        }
    }

    /**
     * Loads a module from the classpath which implements the constructed clazz.
     * Frontend files of all modules will be loaded into the core module.
     *
     * @return A module with type T.
     */
    @SuppressWarnings("unchecked")
    public T loadModule() {

        loadModulesIntoClasspath();
        loadBeans();
        loadFrontendFilesIntoCore();

        ServiceLoader<T> loader = ServiceLoader.load(clazz);

        return loader.iterator().next();
    }

    /**
     * Loads bean classes and registers them to the FacesContext. Afterwards
     * they can be used in all frontend files
     */
    private void loadBeans() {
        Path moduleFolder = FileSystems.getDefault().getPath(modulePath);
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(moduleFolder, JAR)) {

            for (Path f : stream) {
                try (JarFile jarFile = new JarFile(f.toString())) {

                    if (hasFrontendFiles(jarFile)) {

                        Enumeration<JarEntry> e = jarFile.entries();

                        URL[] urls = { new URL("jar:file:" + f.toString() + "!/") };
                        try (URLClassLoader cl = URLClassLoader.newInstance(urls)) {
                            while (e.hasMoreElements()) {
                                JarEntry je = e.nextElement();

                                /*
                                 * IMPORTANT: Naming convention: the name of the
                                 * java class has to be in upper camel case or
                                 * "pascal case" and must be equal to the file
                                 * name of the corresponding facelet file
                                 * concatenated with the word "Form".
                                 *
                                 * Example: template filename "sample.xhtml" =>
                                 * "SampleForm.java"
                                 *
                                 * That is the reason for the following check
                                 * (e.g. whether the JarEntry name ends with
                                 * "Form.class")
                                 */
                                if (je.isDirectory() || !je.getName().endsWith("Form.class")) {
                                    continue;
                                }

                                String className = je.getName().substring(0, je.getName().length() - 6);
                                className = className.replace('/', '.');
                                Class c = cl.loadClass(className);

                                String beanName = className.substring(className.lastIndexOf('.') + 1).trim();

                                FacesContext facesContext = FacesContext.getCurrentInstance();
                                HttpSession session = (HttpSession) facesContext.getExternalContext()
                                        .getSession(false);

                                session.getServletContext().setAttribute(beanName, c.newInstance());
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error(ERROR, e.getMessage());
        }
    }

    /**
     * If the found jar files have frontend files, they will be extracted and
     * copied into the frontend folder of the core module. Before copying,
     * existing frontend files of the same module will be deleted from the core
     * module. Afterwards the created temporary folder will be deleted as well.
     */
    private void loadFrontendFilesIntoCore() {

        Path moduleFolder = FileSystems.getDefault().getPath(modulePath);

        try (DirectoryStream<Path> stream = Files.newDirectoryStream(moduleFolder, JAR)) {

            for (Path f : stream) {
                File loc = new File(f.toString());
                try (JarFile jarFile = new JarFile(loc)) {

                    if (hasFrontendFiles(jarFile)) {

                        Path temporaryFolder = Files.createTempDirectory(SYSTEM_TEMP_FOLDER, TEMP_DIR_PREFIX);

                        File tempDir = new File(Paths.get(temporaryFolder.toUri()).toAbsolutePath().toString());

                        extractFrontEndFiles(loc.getAbsolutePath(), tempDir);

                        String moduleName = extractModuleName(tempDir);
                        if (!moduleName.isEmpty()) {
                            FacesContext facesContext = FacesContext.getCurrentInstance();
                            HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(false);

                            String filePath = session.getServletContext().getRealPath(File.separator + PAGES_FOLDER)
                                    + File.separator + moduleName;
                            FileUtils.deleteDirectory(new File(filePath));

                            String resourceFolder = String.join(File.separator,
                                    Arrays.asList(tempDir.getAbsolutePath(), META_INF_FOLDER, RESOURCES_FOLDER));
                            copyFrontEndFiles(resourceFolder, filePath);
                        } else {
                            logger.info("No module found in JarFile '" + jarFile.getName() + "'.");
                        }
                        FileUtils.deleteDirectory(tempDir);
                    }
                }
            }
        } catch (Exception e) {
            logger.error(ERROR, e.getMessage());
        }
    }

    /**
     * Extracts the module name of the current module by finding the
     * pom.properties in the given temporary folder
     *
     * @param temporaryFolder
     *            folder in which the pom.properties file will be searched for
     *
     * @return String
     */
    private String extractModuleName(File temporaryFolder) throws IOException {
        String moduleName = "";
        File properties = findFile(POM_PROPERTIES_FILE, temporaryFolder);
        try (InputStream input = new FileInputStream(properties)) {
            Properties prop = new Properties();
            prop.load(input);
            moduleName = prop.getProperty(ARTIFACT_ID_PROPERTY);
        } catch (FileNotFoundException e) {
            logger.error(e.getMessage());
        }
        return moduleName;
    }

    /**
     * Copies extracted frontend files by a given source folder name to the
     * destination Folder given by a destination folder name.
     *
     * @param sourceFolder
     *            copies all extracted frontend files
     * @param destinationFolder
     *            jarFile that will be checked for frontend files
     */
    private void copyFrontEndFiles(String sourceFolder, String destinationFolder) throws IOException {
        FileUtils.copyDirectory(new File(sourceFolder), new File(destinationFolder));
    }

    /**
     * Checks, whether a passed jarFile has frontend files or not. Returns true,
     * when the jar contains a folder with the name "resources".
     *
     * @param jarPath
     *            jarFile that will be checked for frontend files
     * @param destinationFolder
     *            destination path, where the frontend files will be extracted
     *            to
     *
     */
    private void extractFrontEndFiles(String jarPath, File destinationFolder) throws IOException {
        if (!destinationFolder.exists()) {
            destinationFolder.mkdir();
        }

        try (JarFile jar = new JarFile(jarPath)) {
            Enumeration jarEntries = jar.entries();
            while (jarEntries.hasMoreElements()) {
                JarEntry currentJarEntry = (JarEntry) jarEntries.nextElement();

                if (currentJarEntry.getName().contains(RESOURCES_FOLDER)
                        || currentJarEntry.getName().contains(POM_PROPERTIES_FILE)) {
                    File resourceFile = new File(destinationFolder + File.separator + currentJarEntry.getName());
                    if (currentJarEntry.isDirectory()) {
                        resourceFile.mkdirs();
                        continue;
                    }
                    if (currentJarEntry.getName().contains(POM_PROPERTIES_FILE)) {
                        resourceFile.getParentFile().mkdirs();
                    }

                    try (InputStream is = jar.getInputStream(currentJarEntry);
                            FileOutputStream fos = new FileOutputStream(resourceFile)) {
                        while (is.available() > 0) {
                            fos.write(is.read());
                        }
                    }
                }
            }
        }
    }

    /**
     * Checks, whether a passed jarFile has frontend files or not. Returns true,
     * when the jar contains a folder with the name "resources"
     *
     * @param jarFile
     *            jarFile that will be checked for frontend files
     *
     * @return boolean
     */
    private boolean hasFrontendFiles(JarFile jarFile) {
        Enumeration enums = jarFile.entries();
        while (enums.hasMoreElements()) {
            JarEntry jarEntry = (JarEntry) enums.nextElement();
            if (jarEntry.getName().contains(RESOURCES_FOLDER) && jarEntry.isDirectory()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Tries to find a file by a given file name in a folder by folder name.
     *
     * @param name
     *            file name that will be searched for
     * @param folder
     *            folder that will be searched
     *
     * @return File
     *
     * @throws FileNotFoundException
     *             when File with given name could not be found in given folder
     *
     */
    private File findFile(String name, File folder) throws FileNotFoundException {
        Collection<File> s = FileUtils.listFiles(folder, null, true);
        for (File f : s) {
            if (f.getName().equals(name)) {
                return f;
            }
        }
        throw new FileNotFoundException(
                "ERROR: file '" + name + "' not found in folder '" + folder.getAbsolutePath() + "'!");
    }

    /**
     * Loads jars from the pluginsFolder to the classpath, so the ServiceLoader
     * can find them.
     */
    private void loadModulesIntoClasspath() {
        Path moduleFolder = FileSystems.getDefault().getPath(modulePath);

        URLClassLoader sysLoader;
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(moduleFolder, JAR)) {
            for (Path f : stream) {
                File loc = new File(f.toString());
                sysLoader = (URLClassLoader) this.getClass().getClassLoader();
                ArrayList<URL> urls = new ArrayList<>(Arrays.asList(sysLoader.getURLs()));
                URL udir = loc.toURI().toURL();

                if (!urls.contains(udir)) {
                    Class<URLClassLoader> sysClass = URLClassLoader.class;
                    Method method = sysClass.getDeclaredMethod("addURL", URL.class);
                    method.setAccessible(true);
                    method.invoke(sysLoader, udir);
                }
            }
        } catch (IOException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            logger.error(ERROR, e.getMessage());
        }
    }

}