Bootstrap.java

/*
 * Bootstrap
 */
package gov.usgs.earthquake.distribution;

import gov.usgs.util.Config;
import gov.usgs.util.Configurable;
import gov.usgs.util.StreamUtils;
import gov.usgs.util.logging.LoggingOutputStream;
import gov.usgs.util.logging.SimpleLogFileHandler;
import gov.usgs.util.logging.SimpleLogFormatter;
import gov.usgs.util.logging.StdOutErrLevel;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import java.util.logging.XMLFormatter;

/**
 * Bootstrap is a class used to start an application.
 *
 * It loads a configuration file, sets up initial logging, and starts a
 * configurable main method.
 *
 * @author jmfee
 *
 */
public class Bootstrap {

	// public static
	static {
		gov.usgs.util.protocolhandlers.data.Handler.register();
	}

	/** Default JAR config path. */
	public static final String JAR_CONFIGFILE = "etc/config/config.ini";

	/** Argument for config file. */
	public static final String CONFIGFILE_ARGUMENT = "--configFile=";
	/** Default config file. */
	public static final String DEFAULT_CONFIGFILE = "config.ini";

	/** Whether to test config only. */
	public static final String CONFIG_TEST_ARGUMENT = "--configTest";

	/** Property for log format. */
	public static final String LOGFORMAT_PROPERTY_NAME = "logformat";
	/** log format value for "pdl" format */
	public static final String LOGFORMAT_PDL = "pdl";
	/** log format value for java "simple" format */
	public static final String LOGFORMAT_SIMPLE = "simple";
	/** log format value for java "xml" format */
	public static final String LOGFORMAT_XML = "xml";
	/** Default log format is "simple". */
	public static final String DEFAULT_LOGFORMAT = LOGFORMAT_PDL;

	/** Property for log level. */
	public static final String LOGLEVEL_PROPERTY_NAME = "loglevel";
	/** Default log level is "INFO". */
	public static final String DEFAULT_LOGLEVEL = "INFO";

	/** Property for log directory. */
	public static final String LOGDIRECTORY_PROPERTY_NAME = "logdirectory";
	/** Default log directory is "log". */
	public static final String DEFAULT_LOGDIRECTORY = "log";

	/** Property for log file pattern. */
	public static final String LOGFILE_PROPERTY_NAME = "logfile";
	/** Default log file pattern is "yyyyMMdd'.log'". */
	public static final String DEFAULT_LOGFILE = "yyyyMMdd'.log'";

	/** Property for console redirect. */
	public static final String CONSOLEREDIRECT_PROPERTY_NAME = "redirectconsole";
	/** Default console redirect value is "false" (don't redirect). */
	public static final String DEFAULT_CONSOLEREDIRECT = "false";

	/** Property used to disable tracker updates. */
	public static final String ENABLE_TRACKER_PROPERTY_NAME = "enableTracker";

	/** Argument for mainclass. */
	public static final String MAINCLASS_ARGUMENT = "--mainclass=";
	/** Property for mainclass. */
	public static final String MAINCLASS_PROPERTY_NAME = "mainclass";
	/** Default mainclass is "gov.usgs.earthquake.distribution.ProductClient. */
	public static final String DEFAULT_MAINCLASS = "gov.usgs.earthquake.distribution.ProductClient";
	/** Argument for version */
	public static final String VERSION_ARGUMENT = "--version";

	// private static

	/** Private logging object. */
	private static final Logger LOGGER = Logger.getLogger(Bootstrap.class
			.getName());


	/** List of logger objects that have level overrides configured. */
	private final ArrayList<Logger> loggers = new ArrayList<Logger>();

	/** Constructor */
	public Bootstrap() {
	}

	// members

	/**
	 * Read configuration from inside jar file, and configFile.
	 *
	 * @param configFile
	 *            config file to load.
	 * @return
	 *            config
	 * @throws IOException
	 *            if IO error occurs
	 */
	public Config loadConfig(final File configFile) throws IOException {
		Config config = new Config();

		// load defaults from jar file
		InputStream in = Bootstrap.class.getClassLoader().getResourceAsStream(
				JAR_CONFIGFILE);
		if (in != null) {
			try {
				config.load(in);
			} finally {
				StreamUtils.closeStream(in);
			}
		} else {
			LOGGER.config("Jar configuration not found");
		}

		// override settings with a config file
		if (configFile.exists()) {
			LOGGER.config("Loading configuration file "
					+ configFile.getCanonicalPath());

			config = new Config(config);
			in = StreamUtils.getInputStream(configFile);
			try {
				config.load(in);
			} finally {
				StreamUtils.closeStream(in);
			}
		}

		return config;
	}

	/**
	 * Sets up LogManager
	 * @param config Config file
	 */
	public void setupLogging(final Config config) {
		final LogManager logManager = LogManager.getLogManager();
		logManager.reset();
		loggers.clear();

		// logging is noisy without this
		for (final String name : new String[]{
			"com.sun.activation",
			"com.sun.xml.bind",
			"javax.xml.bind",
			"org.glassfish.grizzly",
			"org.glassfish.tyrus",
			"sun.awt.X11.timeoutTask.XToolkit"
		}) {
			final Logger logger = Logger.getLogger(name);
			logger.setLevel(Level.INFO);
			// save reference to logger since LogManager uses weakref
			loggers.add(logger);
		};

		final Level level = Level.parse(config.getProperty(LOGLEVEL_PROPERTY_NAME,
				DEFAULT_LOGLEVEL));
		final String logDirectory = config.getProperty(LOGDIRECTORY_PROPERTY_NAME,
				DEFAULT_LOGDIRECTORY);
		LOGGER.config("Logging Level '" + level + "'");
		LOGGER.config("Log directory '" + logDirectory + "'");

		Logger rootLogger = Logger.getLogger("");
		rootLogger.setLevel(level);

		try {
			File logDirectoryFile = new File(logDirectory);
			if (!logDirectoryFile.exists()) {
				LOGGER.fine("Creating log directory");
				if (!logDirectoryFile.mkdirs()) {
					LOGGER.warning("Unable to create log directory");
				}
			}

			// filepattern, maxBytesPerFile, maxFiles, append
			// FileHandler handler = new FileHandler(logFile, 100000, 10, true);
			Handler handler = new SimpleLogFileHandler(logDirectoryFile,
					new SimpleDateFormat(DEFAULT_LOGFILE));
			handler.setLevel(level);
			rootLogger.addHandler(handler);
		} catch (Exception e) {
			LOGGER.log(Level.WARNING, "Unable to create log file handler", e);
		}

		String redirectConsole = config.getProperty(
				CONSOLEREDIRECT_PROPERTY_NAME, DEFAULT_CONSOLEREDIRECT);
		if (!redirectConsole.equals(DEFAULT_CONSOLEREDIRECT)) {
			// default is off, so enable
			System.err.println("Redirecting STDOUT and STDERR to log file");
			System.setOut(new PrintStream(new LoggingOutputStream(Logger
					.getLogger("stdout"), StdOutErrLevel.STDOUT)));
			System.setErr(new PrintStream(new LoggingOutputStream(Logger
					.getLogger("stderr"), StdOutErrLevel.STDERR)));
		} else {
			ConsoleHandler handler = new ConsoleHandler();
			handler.setLevel(level);
			rootLogger.addHandler(handler);
		}

		Formatter formatter;
		String logFormat = config.getProperty(
				LOGFORMAT_PROPERTY_NAME, DEFAULT_LOGFORMAT);
		if (logFormat.equals(LOGFORMAT_SIMPLE)) {
			// built in simple formatter
			formatter = new SimpleFormatter();
		} else if (logFormat.equals(LOGFORMAT_XML)) {
			// built in xml formatter
			formatter = new XMLFormatter();
		} else {
			// pdl style simple formatter
			formatter = new SimpleLogFormatter();
		}
		for (Handler handler : rootLogger.getHandlers()) {
			handler.setFormatter(formatter);
		}
	}

	public static void main(final String[] args) throws Exception {
		StringBuffer argumentList = new StringBuffer();
		boolean configTest = false;

		String className = null;

		// use default config file
		File configFile = new File(DEFAULT_CONFIGFILE);
		for (String arg : args) {
			argumentList.append(arg).append(" ");
			if (arg.startsWith(CONFIGFILE_ARGUMENT)) {
				// unless config file argument provided
				configFile = new File(arg.replace(CONFIGFILE_ARGUMENT, ""));
			} else if (arg.equals(CONFIG_TEST_ARGUMENT)) {
				configTest = true;
			} else if (arg.startsWith(MAINCLASS_ARGUMENT)) {
				className = arg.replace(MAINCLASS_ARGUMENT, "");
			} else if (arg.equals(VERSION_ARGUMENT)) {
				System.err.println("Product Distribution Client");
				System.err.println(ProductClient.RELEASE_VERSION);
				System.exit(0);
			}
		}

		Bootstrap bootstrap = new Bootstrap();

		// load configuration file
		Config config = bootstrap.loadConfig(configFile);

		// set global config object
		Config.setConfig(config);

		// setup logging based on configuration
		bootstrap.setupLogging(config);

		// java and os information
		LOGGER.config("java.vendor = " + System.getProperty("java.vendor"));
		LOGGER.config("java.version = " + System.getProperty("java.version"));
		LOGGER.config("java.home = " + System.getProperty("java.home"));
		LOGGER.config("os.arch = " + System.getProperty("os.arch"));
		LOGGER.config("os.name = " + System.getProperty("os.name"));
		LOGGER.config("os.version = " + System.getProperty("os.version"));
		LOGGER.config("user.dir = " + System.getProperty("user.dir"));
		LOGGER.config("user.name = " + System.getProperty("user.name"));

		// log command line arguments
		LOGGER.fine("Command line arguments: " + argumentList.toString().trim());

		// configure whether tracker updates are sent.
		String enableTrackerProperty = config
				.getProperty(ENABLE_TRACKER_PROPERTY_NAME);
		if (Boolean.valueOf(enableTrackerProperty)) {
			LOGGER.warning("Enabled tracker updates,"
					+ " this is usually not a good idea.");
			ProductTracker.setTrackerEnabled(true);
		}

		// lookup main class
		if (className == null) {
			// no argument specified, check configuration
			className = config.getProperty(MAINCLASS_PROPERTY_NAME,
					DEFAULT_MAINCLASS);
		}

		// invoke main class main(String[] args) method
		LOGGER.config("Loading main class " + className);
		Bootstrappable main = null;
		try {
			main = (Bootstrappable) Class.forName(className)
					.getConstructor().newInstance();
		} catch (ClassCastException cce) {
			LOGGER.log(Level.SEVERE,
					"Main class must implement the Bootstrappable interface",
					cce);
			System.exit(1);
		}

		// use the configurable interface when available
		if (main instanceof Configurable) {
			Configurable configurable = ((Configurable) main);
			configurable.setName("main");
			try {
				configurable.configure(config);
			} catch (Exception e) {
				LOGGER.log(Level.SEVERE, "Exception loading configuration ", e);
				System.exit(1);
			}
		}

		// configuration loaded okay
		LOGGER.config("Configuration loaded");
		if (configTest) {
			// exit successfully
			System.exit(0);
		}

		// run main instance
		LOGGER.config("Bootstrap complete, running main class\n");
		try {
			main.run(args);
		} catch (Exception e) {
			LOGGER.log(Level.SEVERE, "Main class threw exception, exiting", e);
			System.exit(Bootstrappable.RUN_EXCEPTION_EXIT_CODE);
		}
	}

}