org.apache.tomee.embedded.TomEEEmbeddedApplicationRunner.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.tomee.embedded.TomEEEmbeddedApplicationRunner.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.tomee.embedded;

import org.apache.commons.lang3.text.StrSubstitutor;
import org.apache.openejb.config.DeploymentsResolver;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.testing.Application;
import org.apache.openejb.testing.ApplicationComposers;
import org.apache.openejb.testing.Classes;
import org.apache.openejb.testing.ContainerProperties;
import org.apache.openejb.testing.Jars;
import org.apache.openejb.testing.RandomPort;
import org.apache.openejb.testing.WebResource;
import org.apache.tomee.embedded.component.TomEEEmbeddedArgs;
import org.apache.tomee.embedded.event.TomEEEmbeddedApplicationRunnerInjection;
import org.apache.webbeans.config.WebBeansContext;
import org.apache.webbeans.inject.OWBInjector;
import org.apache.xbean.finder.AnnotationFinder;
import org.apache.xbean.finder.archive.Archive;
import org.apache.xbean.finder.archive.ClassesArchive;
import org.apache.xbean.finder.archive.FileArchive;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.inject.Vetoed;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Logger;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.logging.Level.SEVERE;
import static org.apache.openejb.loader.JarLocation.jarLocation;
import static org.apache.openejb.util.Classes.ancestors;

@Vetoed
public class TomEEEmbeddedApplicationRunner implements AutoCloseable {
    private static final ConcurrentMap<Runnable, Runnable> SHUTDOWN_TASKS = new ConcurrentHashMap<>();

    static { // to ensure we have an ordering for shutdown tasks, we typically want to avoid Files.delete() before stop()
        Runtime.getRuntime().addShutdownHook(new Thread("TomEEEmbeddedApplicationRunner-shutdown") {
            @Override
            public void run() {
                for (final Runnable task : SHUTDOWN_TASKS.keySet()) {
                    try {
                        task.run();
                    } catch (final Exception e) {
                        Logger.getLogger(TomEEEmbeddedApplicationRunner.class.getName()).log(SEVERE, e.getMessage(),
                                e);
                    }
                }
                SHUTDOWN_TASKS.clear();
            }
        });
    }

    private volatile boolean started = false;
    private volatile Object app;
    private volatile Thread hook;

    public static void run(final Object app, final String... args) {
        final TomEEEmbeddedApplicationRunner runner = new TomEEEmbeddedApplicationRunner();
        runner.start(app, args);
        try {
            new CountDownLatch(1).await();
        } catch (final InterruptedException e) {
            Thread.interrupted();
            runner.close();
        }
    }

    public AutoCloseable start(final Object app, final String... args) {
        setApp(app);
        final Properties overrides = args == null || args.length == 0 ? null : new Properties();
        if (overrides != null) {
            for (final String prop : args) {
                final String[] seg = prop.split("=");
                if (seg[0].startsWith("--")) {
                    seg[0] = seg[0].substring("--".length());
                }
                overrides.put(seg[0], seg[1]);
            }
        }
        try {
            start(app.getClass(), overrides, args);
            return this;
        } catch (final Exception e) {
            throw new IllegalStateException(e);
        }
    }

    public void setApp(final Object app) {
        this.app = app;
    }

    public Object getApp() {
        return app;
    }

    public synchronized void start(final Class<?> marker, final Properties config, final String... args)
            throws Exception {
        if (started) {
            return;
        }

        ensureAppInit(marker);
        started = true;

        final Class<?> appClass = app.getClass();
        final AnnotationFinder finder = new AnnotationFinder(new ClassesArchive(ancestors(appClass)));

        // setup the container config reading class annotation, using a randome http port and deploying the classpath
        final Configuration configuration = new Configuration();
        final ContainerProperties props = appClass.getAnnotation(ContainerProperties.class);
        if (props != null) {
            final Properties runnerProperties = new Properties();
            for (final ContainerProperties.Property p : props.value()) {
                final String name = p.name();
                if (name.startsWith("tomee.embedded.application.runner.")) { // allow to tune the Configuration
                    // no need to filter there since it is done in loadFromProperties()
                    runnerProperties.setProperty(name.substring("tomee.embedded.application.runner.".length()),
                            p.value());
                } else {
                    configuration.property(name, StrSubstitutor.replaceSystemProperties(p.value()));
                }
            }
            if (!runnerProperties.isEmpty()) {
                configuration.loadFromProperties(runnerProperties);
            }
        }
        configuration.loadFromProperties(System.getProperties()); // overrides, note that some config are additive by design

        final List<Method> annotatedMethods = finder
                .findAnnotatedMethods(org.apache.openejb.testing.Configuration.class);
        if (annotatedMethods.size() > 1) {
            throw new IllegalArgumentException("Only one @Configuration is supported: " + annotatedMethods);
        }
        for (final Method m : annotatedMethods) {
            final Object o = m.invoke(app);
            if (Properties.class.isInstance(o)) {
                final Properties properties = Properties.class.cast(o);
                if (configuration.getProperties() == null) {
                    configuration.setProperties(new Properties());
                }
                configuration.getProperties().putAll(properties);
            } else {
                throw new IllegalArgumentException("Unsupported " + o + " for @Configuration");
            }
        }

        final Collection<org.apache.tomee.embedded.LifecycleTask> lifecycleTasks = new ArrayList<>();
        final Collection<Closeable> postTasks = new ArrayList<>();
        final LifecycleTasks tasks = appClass.getAnnotation(LifecycleTasks.class);
        if (tasks != null) {
            for (final Class<? extends org.apache.tomee.embedded.LifecycleTask> type : tasks.value()) {
                final org.apache.tomee.embedded.LifecycleTask lifecycleTask = type.newInstance();
                lifecycleTasks.add(lifecycleTask);
                postTasks.add(lifecycleTask.beforeContainerStartup());
            }
        }

        final Map<String, Field> ports = new HashMap<>();
        {
            Class<?> type = appClass;
            while (type != null && type != Object.class) {
                for (final Field f : type.getDeclaredFields()) {
                    final RandomPort annotation = f.getAnnotation(RandomPort.class);
                    final String value = annotation == null ? null : annotation.value();
                    if (value != null && value.startsWith("http")) {
                        f.setAccessible(true);
                        ports.put(value, f);
                    }
                }
                type = type.getSuperclass();
            }
        }

        if (ports.containsKey("http")) {
            configuration.randomHttpPort();
        }

        // at least after LifecycleTasks to inherit from potential states (system properties to get a port etc...)
        final Configurers configurers = appClass.getAnnotation(Configurers.class);
        if (configurers != null) {
            for (final Class<? extends Configurer> type : configurers.value()) {
                type.newInstance().configure(configuration);
            }
        }

        final Classes classes = appClass.getAnnotation(Classes.class);
        String context = classes != null ? classes.context() : "";
        context = !context.isEmpty() && context.startsWith("/") ? context.substring(1) : context;

        Archive archive = null;
        if (classes != null && classes.value().length > 0) {
            archive = new ClassesArchive(classes.value());
        }

        final Jars jars = appClass.getAnnotation(Jars.class);
        final List<URL> urls;
        if (jars != null) {
            final Collection<File> files = ApplicationComposers.findFiles(jars);
            urls = new ArrayList<>(files.size());
            for (final File f : files) {
                urls.add(f.toURI().toURL());
            }
        } else {
            urls = null;
        }

        final WebResource resources = appClass.getAnnotation(WebResource.class);
        if (resources != null && resources.value().length > 1) {
            throw new IllegalArgumentException("Only one docBase is supported for now using @WebResource");
        }

        String webResource = null;
        if (resources != null && resources.value().length > 0) {
            webResource = resources.value()[0];
        } else {
            final File webapp = new File("src/main/webapp");
            if (webapp.isDirectory()) {
                webResource = "src/main/webapp";
            }
        }

        if (config != null) { // override other config from annotations
            configuration.loadFromProperties(config);
        }

        final Container container = new Container(configuration);
        SystemInstance.get().setComponent(TomEEEmbeddedArgs.class, new TomEEEmbeddedArgs(args, null));
        SystemInstance.get().setComponent(LifecycleTaskAccessor.class, new LifecycleTaskAccessor(lifecycleTasks));
        container.deploy(new Container.DeploymentRequest(context,
                // call ClasspathSearcher that lazily since container needs to be started to not preload logging
                urls == null
                        ? new DeploymentsResolver.ClasspathSearcher()
                                .loadUrls(Thread.currentThread().getContextClassLoader()).getUrls()
                        : urls,
                webResource != null ? new File(webResource) : null, true, null, archive));

        for (final Map.Entry<String, Field> f : ports.entrySet()) {
            switch (f.getKey()) {
            case "http":
                setPortField(f.getKey(), f.getValue(), configuration, context, app);
                break;
            case "https":
                break;
            default:
                throw new IllegalArgumentException("port " + f.getKey() + " not yet supported");
            }
        }

        SystemInstance.get().addObserver(app);
        composerInject(app);

        final AnnotationFinder appFinder = new AnnotationFinder(new ClassesArchive(appClass));
        for (final Method mtd : appFinder.findAnnotatedMethods(PostConstruct.class)) {
            if (mtd.getParameterTypes().length == 0) {
                if (!mtd.isAccessible()) {
                    mtd.setAccessible(true);
                }
                mtd.invoke(app);
            }
        }

        hook = new Thread() {
            @Override
            public void run() { // ensure to log errors but not fail there
                for (final Method mtd : appFinder.findAnnotatedMethods(PreDestroy.class)) {
                    if (mtd.getParameterTypes().length == 0) {
                        if (!mtd.isAccessible()) {
                            mtd.setAccessible(true);
                        }
                        try {
                            mtd.invoke(app);
                        } catch (final IllegalAccessException e) {
                            throw new IllegalStateException(e);
                        } catch (final InvocationTargetException e) {
                            throw new IllegalStateException(e.getCause());
                        }
                    }
                }

                try {
                    container.close();
                } catch (final Exception e) {
                    e.printStackTrace();
                }
                for (final Closeable c : postTasks) {
                    try {
                        c.close();
                    } catch (final IOException e) {
                        e.printStackTrace();
                    }
                }
                postTasks.clear();
                app = null;
                try {
                    SHUTDOWN_TASKS.remove(this);
                } catch (final Exception e) {
                    // no-op: that's ok at that moment if not called manually
                }
            }
        };
        SHUTDOWN_TASKS.put(hook, hook);
    }

    // if app is not set then we'll check if -Dtomee.application-composer.application is set otherwise
    // we'll try to find a single @Application class in the jar containing marker (case for tests).
    private void ensureAppInit(final Class<?> marker) {
        if (app != null) {
            return;
        }

        final Class<?> type;
        final String typeStr = System.getProperty("tomee.application-composer.application");
        if (typeStr != null) {
            try {
                type = Thread.currentThread().getContextClassLoader().loadClass(typeStr);
            } catch (final ClassNotFoundException e) {
                throw new IllegalArgumentException(e);
            }
        } else if (marker == null) {
            throw new IllegalArgumentException(
                    "set tomee.application-composer.application system property or add a marker to the rule or runner");
        } else {
            final Iterator<Class<?>> descriptors = new AnnotationFinder(
                    new FileArchive(Thread.currentThread().getContextClassLoader(), jarLocation(marker)), false)
                            .findAnnotatedClasses(Application.class).iterator();
            if (!descriptors.hasNext()) {
                throw new IllegalArgumentException("No descriptor class using @Application");
            }
            type = descriptors.next();
            if (descriptors.hasNext()) {
                throw new IllegalArgumentException("Ambiguous @Application: " + type + ", " + descriptors.next());
            }
        }
        try {
            app = type.newInstance();
        } catch (final InstantiationException | IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public synchronized void close() {
        if (hook != null) {
            hook.run();
            SHUTDOWN_TASKS.remove(hook);
            hook = null;
            app = null;
        }
    }

    private static void setPortField(final String key, final Field value, final Configuration configuration,
            final String ctx, final Object instance) {
        final int port = "http".equals(key) ? configuration.getHttpPort() : configuration.getHttpsPort();
        if (value.getType() == URL.class) {
            try {
                value.set(instance, new URL(key + "://localhost:" + port + "/" + ctx));
            } catch (final Exception e) {
                throw new IllegalArgumentException(e);
            }
        } else if (value.getType() == int.class) {
            try {
                value.set(instance, port);
            } catch (final Exception e) {
                throw new IllegalArgumentException(e);
            }
        } else {
            throw new IllegalArgumentException("Unsupported " + key);
        }
    }

    public void composerInject(final Object target) throws IllegalAccessException {
        WebBeansContext webBeansContext = null;
        try {
            webBeansContext = WebBeansContext.currentInstance();
        } catch (final IllegalStateException ise) {
            // no-op
        }
        if (webBeansContext != null) {
            OWBInjector.inject(webBeansContext.getBeanManagerImpl(), target, null);
        }

        Class<?> aClass = target.getClass();
        while (aClass != null && aClass != Object.class) {
            for (final Field f : aClass.getDeclaredFields()) {
                final RandomPort randomPort = f.getAnnotation(RandomPort.class);
                if (randomPort != null) {
                    for (final Field field : app.getClass().getDeclaredFields()) {
                        final RandomPort appPort = field.getAnnotation(RandomPort.class);
                        if (field.getType() == f.getType() && appPort != null
                                && appPort.value().equals(randomPort.value())) {
                            if (!field.isAccessible()) {
                                field.setAccessible(true);
                            }
                            if (!f.isAccessible()) {
                                f.setAccessible(true);
                            }

                            final Object value = field.get(app);
                            f.set(target, value);
                            break;
                        }
                    }
                } else if (f.isAnnotationPresent(Application.class)) {
                    if (!f.isAccessible()) {
                        f.setAccessible(true);
                    }
                    f.set(target, app);
                } else if (f.isAnnotationPresent(LifecycleTask.class)) {
                    if (!f.isAccessible()) {
                        f.setAccessible(true);
                    }
                    final LifecycleTaskAccessor accessor = SystemInstance.get()
                            .getComponent(LifecycleTaskAccessor.class);
                    final Class type = f.getType();
                    final Object taskByType = accessor.getTaskByType(type);
                    f.set(target, taskByType);
                } else if (f.isAnnotationPresent(Args.class)) {
                    if (String[].class != f.getType()) {
                        throw new IllegalArgumentException(
                                "@Args can only be used for String[] field, not on " + f.getType());
                    }
                    if (!f.isAccessible()) {
                        f.setAccessible(true);
                    }
                    final TomEEEmbeddedArgs args = SystemInstance.get().getComponent(TomEEEmbeddedArgs.class);
                    f.set(target, args == null ? new String[0] : args.getArgs());
                }
            }
            aClass = aClass.getSuperclass();
        }

        SystemInstance.get().fireEvent(new TomEEEmbeddedApplicationRunnerInjection(target));
    }

    @Retention(RUNTIME)
    @Target(TYPE)
    public @interface LifecycleTasks {
        Class<? extends org.apache.tomee.embedded.LifecycleTask>[] value();
    }

    @Retention(RUNTIME)
    @Target(FIELD)
    public @interface LifecycleTask {
    }

    @Retention(RUNTIME)
    @Target(FIELD)
    public @interface Args {
    }

    @Retention(RUNTIME)
    @Target(TYPE)
    public @interface Configurers {
        Class<? extends Configurer>[] value();
    }

    public interface Configurer {
        void configure(Configuration configuration);
    }

    public static final class LifecycleTaskAccessor {
        private final Collection<org.apache.tomee.embedded.LifecycleTask> tasks;

        private LifecycleTaskAccessor(final Collection<org.apache.tomee.embedded.LifecycleTask> lifecycleTasks) {
            this.tasks = lifecycleTasks;
        }

        public Collection<org.apache.tomee.embedded.LifecycleTask> getTasks() {
            return tasks;
        }

        public <T> T getTaskByType(final Class<T> type) {
            for (final org.apache.tomee.embedded.LifecycleTask task : tasks) {
                if (type == task.getClass()) {
                    return (T) task;
                }
            }
            if (Collection.class.isAssignableFrom(type)) {
                return (T) tasks;
            }
            return null;
        }
    }
}