com.brienwheeler.apps.main.ContextMain.java Source code

Java tutorial

Introduction

Here is the source code for com.brienwheeler.apps.main.ContextMain.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2013 Brien L. Wheeler (brienwheeler@yahoo.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.brienwheeler.apps.main;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;

import com.brienwheeler.lib.jmx.MBeanRegistrationSupport;
import com.brienwheeler.lib.spring.beans.PropertyPlaceholderConfigurer;
import com.brienwheeler.lib.spring.beans.SmartClassPathXmlApplicationContext;

/**
 * A class to process properties files and launch application contexts based upon command line
 * arguments and a resource mapping file (by default located at classpath:resourceMap.xml).  Intended
 * to be used as the MainClass of an executable JAR that includes a resource map, one or more
 * application contexts, and all classes to support launch of those contexts.
 * 
 * a "-m" command line argument can be used to redefine the location of the resource map.
 * a "-n" command line argument can be used to not look for or process a resource map.
 * a "-p" command line argument will result in processing one or more properties files
 *       based on the next command line argument (which can be a file location or an alias
 *       from the resource map)
 * a "-c" command line argument will result in launching one or more application contexts
 *       based on the next command line argument (which can be a beans file location or an alias
 *       from the resource map).
 * 
 * -p and -c options are processed in order.  That is, if the command line is
 * "-c ctx1 -p props -c ctx2" then the props will only be in effect when ctx2 is launched.
 * 
 * The -m and -n arguments are detected and handled before any properties files are processed 
 * or contexts are launched (since those arguments may depend on the resource map).
 * 
 * See the Programmer's Guide for detailed information on the resourceMap.xml format.
 * 
 * @author bwheeler
 */
@ManagedResource
public class ContextMain extends MainBase implements ApplicationListener<ContextClosedEvent> {
    private static final Log log = LogFactory.getLog(ContextMain.class);

    private static final String RESOURCE_MAP = "classpath:resourceMap.xml";
    private static final String BEAN_DEFAULTCONTEXT = "defaultContext";
    private static final String BEAN_CONTEXTMAP = "contextMap";
    private static final String BEAN_PROPERTIESMAP = "propertiesMap";
    private static final String BEAN_SYSPROPS = "systemProperties";

    private static final String PROPSPEC_PREFIX = "properties[";
    private static final String PROPSPEC_SUFFIX = "]";

    private boolean shuttingDown = false;

    private final List<IAction> commandLineActions = new ArrayList<IAction>();

    private boolean useResourceMap = true;
    private Properties systemProperties = null;

    // context data
    private Properties contextMap = null; // loaded resouceMap contents
    private String defaultContext = null; // context to launch if no -c argument
    private String resourceMapLocation = RESOURCE_MAP;
    private boolean launchDefaultContext = true;
    private final Map<String, List<AbstractApplicationContext>> contextAliasMap = new HashMap<String, List<AbstractApplicationContext>>(); // processed definitions results
    private final List<AbstractApplicationContext> contextLaunchOrder = new ArrayList<AbstractApplicationContext>(); // order launched to reverse for shutdown
    private final Set<String> contextsResolving = new HashSet<String>(); // circular dependency prevention

    // properties data
    private Properties propertiesMap = null; // loaded resouceMap contents
    private final Set<String> propertiesProcessed = new HashSet<String>(); // processed properties tracking
    private final Set<String> propertiesResolving = new HashSet<String>(); // circular dependency prevention

    /**
     * Java main method to construct a ContextMain and run it.
     * @param args JVM command line arguments
     */
    public static void main(String[] args) {
        new ContextMain(args).run();
    }

    /**
     * Construct the ContextMain object.
     * @param args command line arguments specifying the properties files to process and the
     * application contexts to launch.
     */
    public ContextMain(String[] args) {
        super(args);
        // we will process our own command line so that property load operations are sequenced
        // with context load operations, not all done before context loads
        super.setUseBaseOpts(false);
    }

    @Override
    protected boolean processCommandLineOption(CommandLine commandLine) {
        // here for good form and just in case useBaseOpts is turned to true in the future
        if (super.processCommandLineOption(commandLine))
            return true;

        String opt = commandLine.peekNextArg();

        if (opt.equals("-p")) {
            commandLineActions
                    .add(new PropertyLoadAction(commandLine.skipAndGetNextArg("-p option must have a value")));
            return true;
        }

        if (opt.equals("-c")) {
            commandLineActions
                    .add(new ContextLoadAction(commandLine.skipAndGetNextArg("-c option must have a value")));
            launchDefaultContext = false;
            return true;
        }

        if (opt.equals("-m")) {
            resourceMapLocation = commandLine.skipAndGetNextArg("-m option must have a value");
            return true;
        }

        if (opt.equals("-n")) {
            commandLine.getNextArg();
            useResourceMap = false;
            return true;
        }

        return false;
    }

    @Override
    protected void onRun() {
        MBeanRegistrationSupport.registerMBean(this);

        if (useResourceMap)
            loadResourceMap();

        // before launching any contexts, process any properties specified in the resourceMap
        if (systemProperties != null)
            PropertyPlaceholderConfigurer.processProperties(systemProperties);

        for (IAction action : commandLineActions)
            action.execute();
        if (launchDefaultContext && defaultContext != null)
            findOrLaunchContext(defaultContext);

        waitForAllContexts();
    }

    private <T> T getResource(ApplicationContext context, String resourceName, Class<T> clazz) {
        try {
            return context.getBean(resourceName, clazz);
        } catch (NoSuchBeanDefinitionException e) {
            // ok
        } catch (Exception e) {
            log.error("error loading resourceMap element " + resourceName, e);
        }
        return null;
    }

    private void loadResourceMap() {
        ApplicationContext context = new ClassPathXmlApplicationContext(resourceMapLocation);
        defaultContext = getResource(context, BEAN_DEFAULTCONTEXT, String.class);
        systemProperties = getResource(context, BEAN_SYSPROPS, Properties.class);
        contextMap = getResource(context, BEAN_CONTEXTMAP, Properties.class);
        propertiesMap = getResource(context, BEAN_PROPERTIESMAP, Properties.class);
    }

    private void loadPropertySpec(String propertySpecList) {
        if (propertiesProcessed.contains(propertySpecList))
            return;

        if (propertiesResolving.contains(propertySpecList))
            throw new ResourceMapError("circular dependency: " + propertiesResolving);
        propertiesResolving.add(propertySpecList);

        try {
            // if the list has more than one spec string, split then iteratively recurse
            if (propertySpecList.contains(",")) {
                String[] propertySpecs = propertySpecList.split(",");
                for (String propertySpec : propertySpecs)
                    loadPropertySpec(propertySpec);
                return;
            }

            // ok, propertySpecList has just one spec string in it.  Possibly alias or location.

            // check context map and recurse if present
            if (propertiesMap != null) {
                String resolvedSpecList = propertiesMap.getProperty(propertySpecList);
                if (resolvedSpecList != null) {
                    loadPropertySpec(resolvedSpecList);
                    return;
                }
            }

            // not an alias, must be a location at this point
            PropertyPlaceholderConfigurer.processLocation(propertySpecList);
            propertiesProcessed.add(propertySpecList);
        } finally {
            propertiesResolving.remove(propertySpecList);
        }
    }

    private List<AbstractApplicationContext> findOrLaunchContext(String contextSpecList) {
        // check to see if we've already figured this out
        List<AbstractApplicationContext> resultingContexts = contextAliasMap.get(contextSpecList);
        if (resultingContexts != null)
            return resultingContexts;

        // circular dependency detection
        if (contextsResolving.contains(contextSpecList))
            throw new ResourceMapError("circular dependency: " + contextSpecList);
        String originalContextSpecList = contextSpecList;
        contextsResolving.add(originalContextSpecList);

        try {
            // create container for eventual results
            resultingContexts = new ArrayList<AbstractApplicationContext>();

            // first check to see if it has a properties prefix and process/strip it from spec string
            // do this before looking for comma separated list below so that a comma-separated list
            // within the properties prefix gets handled correctly
            if (contextSpecList.startsWith(PROPSPEC_PREFIX)) {
                int specEnd = contextSpecList.indexOf(PROPSPEC_SUFFIX);
                if (specEnd == -1)
                    throw new ResourceMapError("unterminated property spec in " + contextSpecList);

                String propertySpecList = contextSpecList.substring(PROPSPEC_PREFIX.length(), specEnd);
                contextSpecList = contextSpecList.substring(specEnd + PROPSPEC_SUFFIX.length()).trim();
                loadPropertySpec(propertySpecList);
            }

            // if the list has more than one spec string, split then recurse for first spec and remaining spec list
            int comma = contextSpecList.indexOf(",");
            if (comma != -1) {
                String currentSpec = contextSpecList.substring(0, comma).trim();
                resultingContexts.addAll(findOrLaunchContext(currentSpec));
                String remainingSpecList = contextSpecList.substring(comma + 1).trim();
                resultingContexts.addAll(findOrLaunchContext(remainingSpecList));
                contextAliasMap.put(contextSpecList, resultingContexts);
                return resultingContexts;
            }

            // be robust against a properties spec with no context -- user might do something like this:
            // properties[props1],context1,properties[props2],context2

            // in any case, an empty contextSpecList implies no further action
            if (contextSpecList.isEmpty()) {
                return resultingContexts;
            }

            // ok, contextSpecList has just one spec string in it.  Possibly alias or location.

            // check context map and recurse if present
            if (contextMap != null) {
                String resolvedSpecList = contextMap.getProperty(contextSpecList);
                if (resolvedSpecList != null) {
                    resultingContexts.addAll(findOrLaunchContext(resolvedSpecList));
                    contextAliasMap.put(contextSpecList, resultingContexts);
                    return resultingContexts;
                }
            }

            // not an alias, must be a location at this point
            AbstractApplicationContext context = new SmartClassPathXmlApplicationContext(contextSpecList);
            synchronized (contextLaunchOrder) {
                // this allows a context to close itself during its startup (schematool does this)
                if (context.isActive())
                    contextLaunchOrder.add(context);
                context.addApplicationListener(this);
            }
            resultingContexts.add(context);
            contextAliasMap.put(contextSpecList, resultingContexts);
            return resultingContexts;
        } finally {
            contextsResolving.remove(originalContextSpecList);
        }
    }

    private void waitForAllContexts() {
        try {
            synchronized (contextLaunchOrder) {
                onWaitingForShutdown();
                while (contextLaunchOrder.size() > 0)
                    contextLaunchOrder.wait();
            }
        } catch (InterruptedException e) {
            log.error("interrupted waiting for context shutdown");
            Thread.currentThread().interrupt();
            return;
        }
    }

    @ManagedOperation
    public String shutdown() {
        AbstractApplicationContext context = null;
        int n = 0;

        do {
            context = null;
            synchronized (contextLaunchOrder) {
                shuttingDown = true;
                if (contextLaunchOrder.size() > 0)
                    context = contextLaunchOrder.remove(contextLaunchOrder.size() - 1);
            }

            if (context != null) {
                context.close();
                n++;
            }
        } while (context != null);

        synchronized (contextLaunchOrder) {
            shuttingDown = false;
            contextLaunchOrder.notifyAll();
        }
        return "shutdown " + n + " contexts";
    }

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        final AbstractApplicationContext context = (AbstractApplicationContext) event.getApplicationContext();
        boolean waitAndSignal = false;

        synchronized (contextLaunchOrder) {
            contextLaunchOrder.remove(context);
            // if that was the last context and it self-terminated
            if (contextLaunchOrder.size() == 0 && !shuttingDown)
                waitAndSignal = true;
        }

        if (waitAndSignal) {
            // since the ContextClosedEvent actually comes at the start of the close process,
            // start a background thread to monitor the state of the context and signal
            // the thread waiting in onRun() when it is finished closing.
            new Thread() {
                @Override
                public void run() {
                    while (context.isActive()) {
                        try {
                            Thread.sleep(100L);
                        } catch (InterruptedException e) {
                            log.error("interrupted while waiting for last context to finish shutdown");
                            Thread.currentThread().interrupt();
                        }
                    }

                    synchronized (contextLaunchOrder) {
                        contextLaunchOrder.notifyAll();
                    }
                }
            }.start();
        }
    }

    protected void onWaitingForShutdown() {
    } // testability

    /**
     * Polymorphic interface to represent command line actions that are executed in order
     * as present on the command line.
     * 
     * @author bwheeler
     */
    private static interface IAction {
        void execute();
    }

    /**
     * Command line action to load properties
     * 
     * @author bwheeler
     */
    private class PropertyLoadAction implements IAction {
        private String propertyLocation;

        public PropertyLoadAction(String propertyLocation) {
            this.propertyLocation = propertyLocation;
        }

        @Override
        public void execute() {
            loadPropertySpec(propertyLocation);
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + "[" + propertyLocation + "]";
        }
    }

    /**
     * Command line action to load contexts
     * 
     * @author bwheeler
     */
    private class ContextLoadAction implements IAction {
        private String contextLocation;

        public ContextLoadAction(String contextLocation) {
            this.contextLocation = contextLocation;
        }

        @Override
        public void execute() {
            findOrLaunchContext(contextLocation);
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + "[" + contextLocation + "]";
        }
    }
}