ProductClient.java

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

import gov.usgs.earthquake.indexer.SearchCLI;

import gov.usgs.util.Config;
import gov.usgs.util.DefaultConfigurable;
import gov.usgs.util.StringUtils;

import java.lang.management.ManagementFactory;

import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.management.MBeanServer;
import javax.management.ObjectName;

/**
 * The entry point to product distribution.
 *
 * The ProductClient implements the Configurable interface and can use the
 * following configuration parameters:
 *
 * <dl>
 *
 * <dt>receivers</dt>
 * <dd>(Required) A comma separated list of section names that should be loaded
 * as NotificationReceiver objects. Each receiver is a source of notifications
 * for listeners.</dd>
 *
 * <dt>listeners</dt>
 * <dd>(Required) A comma separated list of section names that should be loaded
 * as NotificationListener objects. Each listener receives notifications from
 * receivers.</dd>
 *
 * <dt>logdirectory</dt>
 * <dd>(Optional) Log directory. Default is "log", relative to the current
 * working directory. Log files using a naming convention
 * <code>ProductClient_YYYYMMDD.log</code>.</dd>
 *
 * <dt>loglevel</dt> <dd>(Optional) Default is INFO. One of SEVERE, WARNING,
 * INFO, CONFIG, FINE, FINER, FINEST</dd>
 *
 * <dt>enableTracker</dt> <dd>(Optional) Default is false. Whether or not to
 * send tracker updates to a product tracker. This is generally desirable, but
 * is disabled by default for the paranoid.</dd>
 *
 * <dt>redirectconsole</dt> <dd>(Optional) Default is false. Whether or not to
 * redirect console output to the log file.</dd>
 *
 * </dl>
 *
 * <p>
 * All listeners listen to all receivers for notifications.
 * </p>
 *
 */
public class ProductClient extends DefaultConfigurable implements
		ProductClientMBean, Bootstrappable {

	/** The "release" version number. */
	public static final String RELEASE_VERSION = "Version 2.8.0 2022-10-11";

	/** Property name used on products for current RELEASE_VERSION. */
	public static final String PDL_CLIENT_VERSION_PROPERTY = "pdl-client-version";

	/** SVN Id property. */
	public static final String SVN_VERSION = "$Id$";

	/** SVN Revision property. */
	public static final String SVN_REVISION = "$Revision$";

	/** SVN LastChangedDate property. */
	public static final String SVN_LAST_CHANGED_DATE = "$LastChangedDate$";

	/** Logging object. */
	private static final Logger LOGGER = Logger.getLogger(ProductClient.class
			.getName());

	/** Defaults are loaded from inside the jar file. */
	public static final String JAR_CONFIG_FILE = "etc/config/config.ini";

	/** Default location of config file. */
	public static final String DEFAULT_CONFIG_FILE = "config.ini";

	/** Custom config file locations. */
	public static final String CONFIG_FILE_ARGUMENT = "--configFile=";

	/** Run the builder. */
	public static final String BUILD_ARGUMENT = "--build";

	/** Run the tracker. */
	public static final String TRACK_ARGUMENT = "--track";

	/** Run the builder. */
	public static final String SEND_ARGUMENT = "--send";

	/** Run the client. */
	public static final String RECEIVE_ARGUMENT = "--receive";

	/** Run the search. */
	public static final String SEARCH_ARGUMENT = "--search";

	/** Show usage. */
	public static final String USAGE_ARGUMENT = "--help";

	/** Property containing list of receivers. */
	public static final String RECEIVERS_PROPERTY_NAME = "receivers";

	/** Property containing list of senders. */
	public static final String LISTENERS_PROPERTY_NAME = "listeners";

	/** Log level property name. */
	public static final String LOGLEVEL_PROPERTY_NAME = "loglevel";

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

	/** Property with location for log directory. */
	public static final String LOGDIRECTORY_PROPERTY_NAME = "logdirectory";

	/** Default directory for logging. */
	public static final String DEFAULT_LOGDIRECTORY = "log";

	/** Default location for log file. */
	public static final String DEFAULT_LOGFILE = "'ProductClient'_yyyyMMdd'.log'";

	/** Whether or not to redirect stdout and stderr to log file. */
	public static final String CONSOLE_REDIRECT_PROPERTY_NAME = "redirectconsole";

	/** Default console redirect value (don't redirect). */
	public static final String DEFAULT_CONSOLE_REDIRECT = "false";

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

	/** Property used to enable Admin Socket */
	public static final String ENABLE_ADMIN_SOCKET = "enableAdminSocket";
	/** Default bool for admin socket property */
	public static final String DEFAULT_ENABLE_ADMIN_SOCKET = "false";

	/** List of receivers that generate notifications. */
	private List<NotificationReceiver> receivers = new LinkedList<NotificationReceiver>();

	/** List of listeners that receive notifications. */
	private List<NotificationListener> listeners = new LinkedList<NotificationListener>();

	/** Whether to start a zabbix agent. */
	private boolean enableAdminSocket = false;
	private boolean enableJMX = true;
	private AdminSocketServer adminSocketServer = null;

	public void configure(Config config) throws Exception {
		loadListeners(config);
		loadReceivers(config);

		// connect all listeners to all receivers
		Iterator<NotificationReceiver> iter = receivers.iterator();
		while (iter.hasNext()) {
			NotificationReceiver receiver = iter.next();
			Iterator<NotificationListener> iter2 = listeners.iterator();
			while (iter2.hasNext()) {
				NotificationListener listener = iter2.next();
				receiver.addNotificationListener(listener);
			}
		}

		enableAdminSocket = Boolean.valueOf(config.getProperty(
				ENABLE_ADMIN_SOCKET, DEFAULT_ENABLE_ADMIN_SOCKET));
	}

	/**
	 * Load listeners from a Config object.
	 *
	 * @param config
	 *            the configuration.
	 * @throws Exception if error occurs
	 */
	public void loadListeners(final Config config) throws Exception {
		Iterator<String> iter = StringUtils.split(
				config.getProperty(LISTENERS_PROPERTY_NAME, ""), ",")
				.iterator();
		while (iter.hasNext()) {
			String listenerName = iter.next();
			LOGGER.config("Loading listener '" + listenerName + "'");

			NotificationListener listener = (NotificationListener) Config
					.getConfig().getObject(listenerName);
			if (listener == null) {
				throw new ConfigurationException("Unable to load listener '"
						+ listenerName
						+ "', make sure it is properly configured.");
			}

			// listenerName references an object in the global configuration
			listeners.add(listener);
		}
	}

	/**
	 * Load NotificationReceivers from a Config object.
	 *
	 * @param config
	 *            the configuration
	 * @throws Exception if error occurs
	 */
	public void loadReceivers(final Config config) throws Exception {
		Iterator<String> iter = StringUtils.split(
				config.getProperty(RECEIVERS_PROPERTY_NAME), ",").iterator();
		while (iter.hasNext()) {
			String receiverName = iter.next();
			LOGGER.config("Loading receiver '" + receiverName + "'");

			NotificationReceiver receiver = (NotificationReceiver) Config
					.getConfig().getObject(receiverName);
			if (receiver == null) {
				throw new ConfigurationException("Unable to load receiver '"
						+ receiverName
						+ "', make sure it is properly configured.");
			}
			// receiverName references an object in the global configuration
			receivers.add(receiver);
		}
	}

	/**
	 * Start up all listeners and receivers.
	 */
	public void startup() throws Exception {
		Iterator<NotificationListener> iter = listeners.iterator();
		while (iter.hasNext()) {
			iter.next().startup();
		}
		Iterator<NotificationReceiver> iter2 = receivers.iterator();
		while (iter2.hasNext()) {
			iter2.next().startup();
		}

		if (enableAdminSocket) {
			LOGGER.info("Starting AdminSocketServer on port 11111");
			adminSocketServer = new AdminSocketServer();
			adminSocketServer.setClient(this);
			adminSocketServer.startup();
		}

		if (enableJMX) {
			MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
			ObjectName clientName = new ObjectName("ProductClient:name=jmx");
			mbs.registerMBean(this, clientName);
		}
	}

	/**
	 * Shut down all receivers and listeners.
	 */
	public void shutdown() throws Exception {
		if (receivers.size() > 0) {
			Iterator<NotificationReceiver> iter2 = receivers.iterator();
			while (iter2.hasNext()) {
				try {
					iter2.next().shutdown();
				} catch (Exception e) {
					// ignore
				}
			}
		}

		if (listeners.size() > 0) {
			Iterator<NotificationListener> iter = listeners.iterator();
			while (iter.hasNext()) {
				try {
					iter.next().shutdown();
				} catch (Exception e) {
					// ignore
				}
			}
		}

		if (adminSocketServer != null) {
			try {
				adminSocketServer.shutdown();
			} catch (Exception e) {
			}
			adminSocketServer = null;
		}
	}

	/**
	 * Entry point into Product Distribution.
	 *
	 * @param args argument
	 */
	public void run(final String[] args) throws Exception {
		try {
			// default is show usage
			boolean receiveProducts = false;
			boolean buildProduct = false;
			boolean trackProduct = false;
			boolean searchProduct = false;
			boolean showUsage = false;

			// parse arguments
			for (String arg : args) {
				if (arg.equals(SEND_ARGUMENT) || arg.equals(BUILD_ARGUMENT)) {
					buildProduct = true;
				} else if (arg.equals(RECEIVE_ARGUMENT)) {
					receiveProducts = true;
				} else if (arg.equals(TRACK_ARGUMENT)) {
					trackProduct = true;
				} else if (arg.equals(SEARCH_ARGUMENT)) {
					searchProduct = true;
				} else if (arg.equals(USAGE_ARGUMENT)) {
					showUsage = true;
				}
			}

			// output current version
			System.err.println("Product Distribution Client");
			System.err.println(RELEASE_VERSION);
			System.err.println();

			if (buildProduct) {
				if (showUsage) {
					System.err.println("Usage: ");
					System.err
							.println("    java -jar ProductClient.jar --build [BUILD ARGUMENTS]");
					System.err.println();
					System.err.println(CLIProductBuilder.getUsage());
					System.exit(0);
				}
				LOGGER.info("Running Product Builder");
				// run builder main
				CLIProductBuilder.main(args);
				System.exit(0);
			} else if (trackProduct) {
				if (showUsage) {
					System.err.println("Usage: ");
					System.err
							.println("    java -jar ProductClient.jar --track [TRACK ARGUMENTS]");
					System.err.println();
					System.err.println(ProductTracker.getUsage());
					System.exit(0);
				}
				LOGGER.info("Running Product Tracker");
				ProductTracker.main(args);
				System.exit(0);
			} else if (searchProduct) {
				// search needs to happen after track, since track also uses a
				// --search argument
				if (showUsage) {
					System.err.println("Usage: ");
					System.err
							.println("    java -jar ProductClient.jar --search [SEARCH ARGUMENTS]");
					System.err.println();
					System.err.println(SearchCLI.getUsage());
					System.exit(0);
				}
				LOGGER.info("Running Product Search");
				SearchCLI.main(args);
				System.exit(0);
			} else if (receiveProducts && !showUsage) {
				// start processing
				LOGGER.info("Starting");
				try {
					startup();
				} catch (Exception e) {
					LOGGER.log(Level.SEVERE,
							"Exceptions while starting, shutting down", e);
					try {
						// this has been throwing exceptions, move into try
						shutdown();
					} finally {
						// exit no matter what
						System.exit(1);
					}
				}
				LOGGER.info("Started");

				// shutdown threads when control-c is pressed
				// otherwise, would continue running
				Runtime.getRuntime().addShutdownHook(new Thread() {
					public void run() {
						try {
							LOGGER.info("Shutting down");
							shutdown();
							LOGGER.info("Shutdown complete");
						} catch (Exception e) {
							LOGGER.log(Level.WARNING,
									"Exception while shutting down", e);
						}
					}
				});
			} else {
				System.err.println("Usage: ");
				System.err
						.println("    java -jar ProductClient.jar [ARGUMENTS]");
				System.err.println();
				System.err.println(getUsage());
				System.exit(1);
			}

		} catch (Exception e) {
			LOGGER.log(Level.SEVERE, "Exception in main", e);
		}
	}

	/**
	 * @return The list of receivers
	 */
	public List<NotificationReceiver> getReceivers() {
		return receivers;
	}

	/**
	 *
	 * @return The list of listeners
	 */
	public List<NotificationListener> getListeners() {
		return listeners;
	}

	/** @return Product usage */
	public static String getUsage() {
		StringBuffer buf = new StringBuffer();

		buf.append("[--configFile=FILE]      override the default config file location\n");
		buf.append("                         default is config.ini in CWD\n");
		buf.append("[--help]                 show this message and exit\n");
		buf.append("[--version]              show the version and exit\n");
		buf.append("[--configTest]           load configuration and exit\n");
		buf.append("\n");
		buf.append("[--send]                 create and send a product\n");
		buf.append("                         try --send --help for more information\n");
		buf.append("[--receive]              receive products\n");
		buf.append("[--track]                check or update product status\n");
		buf.append("                         try --track --help for more information\n");
		buf.append("\n");
		buf.append("You must use one of \"--send\", \"--receive\", or \"--track\"\n");
		buf.append("\n");

		return buf.toString();
	}

	@Override
	public String getListenerQueueStatus() {
		StringBuffer buf = new StringBuffer();
		Iterator<NotificationReceiver> iter = receivers.iterator();
		while (iter.hasNext()) {
			NotificationReceiver receiver = iter.next();
			if (receiver instanceof DefaultNotificationReceiver) {
				buf.append(((DefaultNotificationReceiver) receiver)
						.getListenerQueueStatus());
			}
		}
		return buf.toString();
	}

	@Override
	public String getVersion() {
		return RELEASE_VERSION;
	}

	@Override
	public long getMaxMemory() {
		return Runtime.getRuntime().maxMemory();
	}

	@Override
	public long getFreeMemory() {
		return Runtime.getRuntime().freeMemory();
	}

}