org.wildfly.extras.db_bootstrap.DbBootstrapScanDetectorProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.wildfly.extras.db_bootstrap.DbBootstrapScanDetectorProcessor.java

Source

/**
 * Copyright (C) 2014 Umbrew (Flemming.Harms@gmail.com)
 *
 * 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 org.wildfly.extras.db_bootstrap;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
import org.jboss.as.server.deployment.Attachments;
import org.jboss.as.server.deployment.DeploymentPhaseContext;
import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
import org.jboss.as.server.deployment.DeploymentUnitProcessor;
import org.jboss.as.server.deployment.module.ResourceRoot;
import org.jboss.dmr.ModelNode;
import org.jboss.modules.ModuleLoadException;
import org.jboss.vfs.VFSUtils;
import org.jboss.vfs.VirtualFile;
import org.jboss.vfs.VirtualFileFilter;
import org.jboss.vfs.VirtualFileFilterWithAttributes;
import org.jboss.vfs.VisitorAttributes;
import org.scannotation.AnnotationDB;
import org.wildfly.extras.db_bootstrap.annotations.BootstrapDatabase;
import org.wildfly.extras.db_bootstrap.annotations.BootstrapSchema;
import org.wildfly.extras.db_bootstrap.annotations.UpdateSchema;
import org.wildfly.extras.db_bootstrap.matchfilter.FilenameContainFilter;

/**
 * Reacts on the deployment process on the specified archives in the configuration. It scan all JAR archives for
 * {@link BootstrapDatabase} annotation to locate database bootstrapping classes. <br>
 * <br>
 *
 * By default it all children in the archive is added to a new class loader and passed to the Hibernate
 * {@link org.hibernate.boot.registry.internal.BootstrapServiceRegistryImpl} for creating a new {@link SessionFactory}
 *
 * @author Frank Vissing (frank.vissing@schneider-electric.com)
 * @author Flemming Harms (flemming.harms@gmail.com)
 * @author Nicky Moelholm (moelholm@gmail.com)
 * @author Rasmus Lund
 */
class DbBootstrapScanDetectorProcessor implements DeploymentUnitProcessor {

    /**
     * Defines the prefix of the system property names that can be used to set and/or override the hibernate properties defined
     * in user-space hibernate configuration files (referenced by the {@link BootstrapDatabase} annotation). <br>
     * <br>
     * For example: Setting the system property <code>dbbootstrap.foohibcfg.connection.url</code> will override any existing
     * hibernate configuration property <code>connection.url</code> in the hibernate configuration xml file referenced by the
     * {@link BootstrapDatabase} annotation with the name <code>foohibcfg</code>.
     */
    public static final String DBBOOTSTRAP_SYSTEM_PROPERTY_PREFIX = "dbbootstrap";
    private final String filename;
    private final FilenameContainFilter filterOnJarFilename;

    public DbBootstrapScanDetectorProcessor(final String filename, final List<ModelNode> filterOnName) {
        this.filename = filename;
        List<String> filter = new ArrayList<>(filterOnName.size());

        for (ModelNode modelNode : filterOnName) {
            filter.add("**/" + modelNode.asString());
        }

        this.filterOnJarFilename = new FilenameContainFilter(filter, VisitorAttributes.RECURSE);

        if (!filterOnName.isEmpty()) {
            DbBootstrapLogger.ROOT_LOGGER.infof("Archive : %s jar-filter %s", this.filename,
                    filterOnJarFilename.toString());
        }
    }

    @Override
    public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
        DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();
        String deploymentName = deploymentUnit.getName();

        if (isSubdeployment(deploymentUnit)) {
            return;
        }

        if (deploymentName.equals(filename)) {
            scanForAnnotationsAndProcessAnnotatedFiles(deploymentUnit, filterOnJarFilename);
        } else {
            DbBootstrapLogger.ROOT_LOGGER.tracef("%s did not match %s", filename, deploymentName);
        }
    }

    private void scanForAnnotationsAndProcessAnnotatedFiles(DeploymentUnit deploymentUnit,
            VirtualFileFilterWithAttributes filterOnJarFilename) {
        ResourceRoot deploymentRoot = deploymentUnit.getAttachment(Attachments.DEPLOYMENT_ROOT);
        VirtualFile root = deploymentRoot.getRoot();

        DbBootstrapLogger.ROOT_LOGGER.tracef("match on %s", root.getPathName());
        try {
            Set<URL> classLoaderurls = getJarList(root, false, filterOnJarFilename);
            if (classLoaderurls.size() > 0) {
                final AnnotationDB db = new AnnotationDB();
                ClassLoader classLoader = addDynamicResources(classLoaderurls, deploymentUnit);
                if (filterOnJarFilename == null) {
                    scanForAnnotation(classLoaderurls, db);
                } else {
                    scanForAnnotation(getJarList(root, true, filterOnJarFilename), db);
                }
                processAnnotatedFiles(db, classLoader);
            }
        } catch (Exception e) {
            DbBootstrapLogger.ROOT_LOGGER.error("Unable to process the internal jar files", e);
        }
    }

    private void processAnnotatedFiles(final AnnotationDB db, final ClassLoader classLoader) throws Exception {
        Map<String, Set<String>> annotationIndex = db.getAnnotationIndex();
        Set<String> databaseBoostrapperClasses = annotationIndex.get(BootstrapDatabase.class.getName());
        if (databaseBoostrapperClasses != null) {
            Map<BootstrapDatabase, Class<?>> bootstrapMap = new HashMap<BootstrapDatabase, Class<?>>(
                    databaseBoostrapperClasses.size());
            for (String clazz : databaseBoostrapperClasses) {
                try {
                    Class<?> annotatedClazz = Class.forName(clazz, true, classLoader);
                    BootstrapDatabase dbBoostrapper = annotatedClazz.getAnnotation(BootstrapDatabase.class);
                    bootstrapMap.put(dbBoostrapper, annotatedClazz);
                } catch (ClassNotFoundException e) {
                    DbBootstrapLogger.ROOT_LOGGER.error("Unable to find class", e);
                }
            }
            processAnnotatedClasses(bootstrapMap, classLoader);
        } else {
            DbBootstrapLogger.ROOT_LOGGER.debug("@BootstrapDatabase annotation was not located in the archive");
        }
    }

    /**
     * Process a sorted list of bootstrap classes, by calling method's annotated with {@link BootstrapSchema} first and second
     * {@link UpdateSchema}.
     *
     * @param bootstrapMap
     * @param classLoader
     * @throws Exception
     */
    private void processAnnotatedClasses(final Map<BootstrapDatabase, Class<?>> bootstrapMap,
            final ClassLoader classLoader) throws Exception {
        List<Entry<BootstrapDatabase, Class<?>>> sortedList = new ArrayList<Entry<BootstrapDatabase, Class<?>>>(
                bootstrapMap.entrySet().size());
        sortedList.addAll(bootstrapMap.entrySet());

        Collections.sort(sortedList, new BootstrapperSorter());
        // Run all BootstrapSchema annotations
        for (Entry<BootstrapDatabase, Class<?>> entry : sortedList) {
            DbBootstrapLogger.ROOT_LOGGER.infof("Executing Bootstrap Schema method for %s %s",
                    entry.getKey().name(), entry.getValue().getName());
            executeMethod(entry.getValue(), entry.getKey(), BootstrapSchema.class, classLoader);
        }

        // Run all UpgradeSchema annotations
        for (Entry<BootstrapDatabase, Class<?>> entry : sortedList) {
            DbBootstrapLogger.ROOT_LOGGER.infof("Executing Update Schema method for %s %s", entry.getKey().name(),
                    entry.getValue().getName());
            executeMethod(entry.getValue(), entry.getKey(), UpdateSchema.class, classLoader);
        }
    }

    private static class BootstrapperSorter implements Comparator<Entry<BootstrapDatabase, Class<?>>> {

        @Override
        public int compare(Entry<BootstrapDatabase, Class<?>> o1, Entry<BootstrapDatabase, Class<?>> o2) {
            int priority1 = o1.getKey().priority();
            int priority2 = o2.getKey().priority();
            if (priority1 == priority2) {
                return 0;
            }
            return (priority1 > priority2 ? -1 : 1);
        }
    }

    /**
     * Execute the method annotated with specified class. If the annotated method has parameter signature {@link Session} it
     * will create a session a pass it as parameter.
     *
     * @param annotatedClazz - The bootstrap class to execute the method on
     * @param bootstrapDatabaseAnnotation - The configuration for creating a session
     * @param annotation - The annotation the method need to be annotated with for calling
     * @param classLoader - The class loader
     * @throws Exception
     */
    private <T extends Annotation> void executeMethod(final Class<?> annotatedClazz,
            final BootstrapDatabase bootstrapDatabaseAnnotation, final Class<T> annotation,
            final ClassLoader classLoader) throws Exception {
        Method[] methods = annotatedClazz.getDeclaredMethods();
        for (Method method : methods) {
            method.setAccessible(true);
            if (method.getAnnotation(annotation) != null) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                boolean sessionParameter = false;
                for (int i = 0; i < parameterTypes.length; i++) {
                    if (parameterTypes[i].equals(Session.class)) {
                        sessionParameter = true;
                        break;
                    }
                }
                Object bootstrapClass = annotatedClazz.newInstance();
                if (sessionParameter) {
                    invokeWithSession(bootstrapDatabaseAnnotation, classLoader, method, bootstrapClass);
                } else {
                    method.invoke(bootstrapClass);
                }
            }
        }
    }

    /**
     * Wrap transaction around the invoke with the {@link Session}, if any exception throw it roll back the tx otherwise commit
     * the tx;
     *
     * @param bootstrapDatabaseAnnotation - the boostrap configuration source
     * @param classLoader - The classloader to load the hibernate resources from
     * @param method - the method to invoke
     * @param bootstrapClass - the class to invoke the method on
     * @throws Exception
     */
    private void invokeWithSession(final BootstrapDatabase bootstrapDatabaseAnnotation,
            final ClassLoader classLoader, Method method, Object bootstrapClass) throws Exception {
        Session session = createSession(bootstrapDatabaseAnnotation, classLoader);
        Transaction tx = session.beginTransaction();
        try {
            method.invoke(bootstrapClass, session);
        } catch (Exception e) {
            DbBootstrapLogger.ROOT_LOGGER.error(String.format("Unable to invoke method %s ", method.getName()), e);
            tx.rollback();
        } finally {
            if (tx.isActive()) {
                tx.commit();
            }
            session.close();
            session.getSessionFactory().close();
        }
    }

    /**
     * Create a {@link Session} based on the provided configuration file.
     *
     * @param bootstrapDatabaseAnnotation - bootstrap configuration source
     * @param classLoader - class loader to use with the session factory
     * @return {@link Session}
     * @throws Exception
     */
    private Session createSession(final BootstrapDatabase bootstrapDatabaseAnnotation,
            final ClassLoader classLoader) throws Exception {
        URL resource = classLoader.getResource(bootstrapDatabaseAnnotation.hibernateCfg());
        DbBootstrapLogger.ROOT_LOGGER.tracef("Using hibernate configuration file %s",
                bootstrapDatabaseAnnotation.hibernateCfg());
        Configuration configuration = new Configuration();
        configuration.configure(resource); // configures settings from hibernate.cfg.xml
        configureSettingsFromSystemProperties(bootstrapDatabaseAnnotation, configuration);
        StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
                .applySettings(configuration.getProperties()).build();
        SessionFactory sessionFactory = configuration.buildSessionFactory(serviceRegistry);
        return sessionFactory.openSession();
    }

    /**
     * Loads all <code>dbbootstrap.[user-space-cfg-name-here].[hibernate-property-name-here]</code> properties from system
     * properties. <br>
     * <br>
     * Any existing hibernate properties with the same name (<code>[hibernate-property-name-here]</code> in above example) will
     * be replaced by the matching system property. <br>
     * <br>
     *
     * @param bootstrapDatabaseAnnotation - bootstrap configuration source
     * @param configuration - the runtime hibernate configuration object
     */
    private void configureSettingsFromSystemProperties(BootstrapDatabase bootstrapDatabaseAnnotation,
            Configuration configuration) {
        String propertyPrefix = String.format("%s.%s", DBBOOTSTRAP_SYSTEM_PROPERTY_PREFIX,
                bootstrapDatabaseAnnotation.name());
        DbBootstrapLogger.ROOT_LOGGER.tracef(
                "Searching for system properties with prefix %s to set and/or override hibernate configuration properties",
                propertyPrefix);
        for (Entry<Object, Object> entrySet : (System.getProperties().entrySet())) {
            if (entrySet.getKey().toString().startsWith(propertyPrefix)) {
                String hibernatePropertyName = entrySet.getKey().toString()
                        .replace(String.format("%s.", propertyPrefix), "");
                String oldHibernatePropertyValue = (configuration.getProperty(hibernatePropertyName) == null)
                        ? " (New property)"
                        : String.format(" (Replacing existing property with old value=%s)",
                                configuration.getProperty(hibernatePropertyName));
                String newHibernatePropertyValue = entrySet.getValue().toString();
                DbBootstrapLogger.ROOT_LOGGER.tracef("Setting hibernate property: %s=%s%s", hibernatePropertyName,
                        newHibernatePropertyValue, oldHibernatePropertyValue);
                configuration.setProperty(hibernatePropertyName, newHibernatePropertyValue);
            }
        }
    }

    /**
     * Scan all jar files for annotations and build a internal map with the result.
     *
     * @param jarList
     * @param db
     * @throws IOException
     * @throws URISyntaxException
     */
    private void scanForAnnotation(Set<URL> jars, AnnotationDB db) throws IOException, URISyntaxException {
        db.setScanClassAnnotations(true);
        db.setScanFieldAnnotations(false);
        db.setScanMethodAnnotations(false);
        db.setScanParameterAnnotations(false);
        db.scanArchives(jars.toArray(new URL[0]));
    }

    /**
     * Return a list of URL's to all the children to the {@link VirtualFile}
     *
     * @param deploymentRoot
     * @param filter - true if the jar filename filter should be applied
     * @param filterOnJarFilename
     * @return A arrays of {@link URL}
     * @throws DeploymentUnitProcessingException
     * @throws IOException
     */
    private Set<URL> getJarList(final VirtualFile deploymentRoot, boolean filter,
            VirtualFileFilter filterOnJarFilename) throws DeploymentUnitProcessingException, IOException {
        TreeSet<URL> uniqueArchiveUrls = new TreeSet<URL>(new UniqueArchiveUrlsComparator());
        List<VirtualFile> entries;

        if (filter) {
            entries = deploymentRoot.getChildrenRecursively(filterOnJarFilename);
        } else {
            entries = deploymentRoot.getChildrenRecursively();
        }

        for (VirtualFile virtualFile : entries) {
            try {
                URL url = VFSUtils.getPhysicalURL(virtualFile);
                uniqueArchiveUrls.add(url);
            } catch (NullPointerException ignore) {
                // Happens if 'filename' refers to a dir or file, which is not an archive or which is not inside an archive.
                // These can safely be ignored.
            }
        }
        return uniqueArchiveUrls;
    }

    private static class UniqueArchiveUrlsComparator implements Comparator<URL> {
        @Override
        public int compare(URL o1, URL o2) {
            return o1.toString().compareTo(o2.toString());
        }
    }

    @Override
    public void undeploy(DeploymentUnit deploymentUnit) {
    }

    /**
     * Add all jar files as dynamic resources to a new class load.
     *
     * @param urls - List of all the jar files to add
     * @param deploymentUnit - The deployment unit the resources should be added too
     * @throws DeploymentUnitProcessingException
     * @throws ModuleLoadException
     */
    private ClassLoader addDynamicResources(final Set<URL> urls, final DeploymentUnit deploymentUnit)
            throws DeploymentUnitProcessingException, ModuleLoadException {
        return URLClassLoader.newInstance(urls.toArray(new URL[0]), getClass().getClassLoader());
    }

    /**
     * As the top level module (including its submodules) gets scanned, we don't want to scan its submodules seperately
     * as well (would lead to double scanning)
     * @param deploymentUnit - the dployment unit to test
     * @return true if this is a sub deployment
     */
    private boolean isSubdeployment(DeploymentUnit deploymentUnit) {
        boolean currentModuleIsSubmoduleInAnotherModule = deploymentUnit.getParent() != null;
        if (currentModuleIsSubmoduleInAnotherModule) {
            DbBootstrapLogger.ROOT_LOGGER.tracef(
                    "Not registering module '%s' for scaning, as it is a submodule of '%s'",
                    deploymentUnit.getName(), deploymentUnit.getParent().getName());
            return true;
        }
        return false;
    }

}