// Copyright 2010 Google Inc. // // 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 com.google.enterprise.connector.common; import com.google.common.base.Function; import com.google.common.base.Strings; import com.google.enterprise.connector.common.JarUtils; import com.google.enterprise.connector.instantiator.EncryptedPropertyPlaceholderConfigurer; import com.google.enterprise.connector.manager.Context; import com.google.enterprise.connector.servlet.ServletUtil; import com.google.enterprise.connector.util.SAXParseErrorHandler; import com.google.enterprise.connector.util.XmlParseUtil; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.cli.PosixParser; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.HashMap; import java.util.logging.Level; import java.util.logging.Logger; /** * An abstract superclass for building Connector Manager command line apps. */ public abstract class AbstractCommandLineApp { /** * A couple of basic Options that all command line apps should support. * Note that user request for help and version is handled especially by * this framework, so the subclass need not bother with them. * However, the subclass should add these options to the Options object * it constructs. */ protected static final Option HELP_OPTION = new Option("?", "help", false, "Display this help."); protected static final Option VERSION_OPTION = new Option("v", "version", false, "Display version string."); protected static final String NL = System.getProperty("line.separator"); /** Parsed CommandLine. */ protected CommandLine commandLine; /** * Returns the name of the command line application. */ public abstract String getName(); /** * Returns short description of the command line application. */ public abstract String getDescription(); /** * Executes the command line app. * * @param commandLine a parsed {@code org.apache.commons.cli.CommandLine}. */ public abstract void run(CommandLine commandLine) throws Exception; /** * Returns a Command Line Syntax as a String. This is used to generate * the usage output. The base class includes the application name, plus * the base class options (help and version). Subclasses may override this * method adding their additional Options, plus non-option arguments. */ public String getCommandLineSyntax() { return getName() + " [-?] [-v] "; } /** * Returns a base set of Options, including {@code VERSION_OPTION} and * {@code HELP_OPTION}. Subclasses may override this method and add * additional app-specific options to the set. * * @return {@code org.apache.commons.cli.Options} */ protected Options getOptions() { Options options = new Options(); options.addOption(HELP_OPTION); options.addOption(VERSION_OPTION); return options; } /** * Initializes a standalone Connector Manager application context. If the * CommandLineApp starts a standalone context, it must call {@link #shutdown()} * before exiting. * * @param doStart If {@code true}, start the Context via {@link Context#start} * (with traversals disabled), otherwise construct all the initial * beans, but do not actually start the Connector Manager appliction. */ protected void initStandAloneContext(boolean doStart) { // Turn down the logging output to the console. setLoggingLevels(); // Find the Connector Manager WEB-INF directory. File webInfDir = locateWebInf(); if (webInfDir == null) { System.err.println( "Unable to locate the connector-manager webapp directory."); System.err.println("Try changing to that directory, or use"); System.err.println("-Dmanager.dir=/path/to/webapps/connector-manager"); System.exit(-1); } // If a catalina.base property is not specified, make a guess based on the // knowledge that cwd is likely ${catalina.base}/webapps/connector-manager. if (System.getProperty("catalina.base") == null) { try { System.setProperty("catalina.base", webInfDir.getAbsoluteFile() .getParentFile().getParentFile().getParent()); } catch (NullPointerException npe) { // Bad guess. Go on without it. } } // Establish the webapp keystore configuration before initializing // the Context. try { configureCryptor(webInfDir); } catch (IOException e) { System.err.println("Failed to read keystore configuration: " + e); System.exit(-1); } // Setup the standalone application Context. Context context = Context.getInstance(); File contextLocation = new File(webInfDir, "applicationContext.xml"); try { context.setStandaloneContext( contextLocation.getAbsoluteFile().toURI().toURL().toString(), webInfDir.getAbsoluteFile().getParent(), webInfDir.getAbsolutePath()); // At this point the beans have been created, but the Connector Manager // has not started up. if (doStart) { context.setFeeding(false); context.start(); } } catch (Exception e) { System.err.println( "Failed to initialize standalone application context: " + e); System.exit(-1); } } /** * Sets the Logging Levels. This is typically used to turn logging down * to WARNING or SEVERE to avoid excessive logging to the console logger * when running our command line app. Subclasses may override this if * they want different logging behaviour. */ // TODO: Look for logging levels on the command line via -D... protected void setLoggingLevels() { // Turn down the logging output to the console. Logger.getLogger("").setLevel(Level.WARNING); Logger.getLogger("com.google.enterprise.connector").setLevel(Level.WARNING); Logger.getLogger("org.springframework").setLevel(Level.WARNING); } /** * Shuts down the the command line application context. Subclasses may * override this method, but should call super.shutdown() if they do. */ protected void shutdown() { Context.getInstance().shutdown(true); } /** * Returns the Version string for this application. */ protected String getVersion() { return this.getName() + " v" + JarUtils.getJarVersion(this.getClass()); } /** * Displays the product version. */ protected void printVersion() { System.err.println(getVersion()); System.err.println(""); } /** * Displays the product version and exits. This is called automatically * if the user invokes the app with "-v" or "--version". * * @param exitCode code to supply to {@code System.exit()} */ protected void printVersionAndExit(int exitCode) { printVersion(); System.exit(exitCode); } /** * Gets the header that is included in the {@code usage:} message. * Subclasses my override this to add additional information * before the display of options. */ protected String getUsageHeader() { return null; } /** * Gets the footer to be added to the {@code usage:} message. * Subclasses my override this to add additional informative help. */ protected String getUsageFooter() { return null; } /** * Displays the product usage. * invokes the app with "-?", "-h" or "--help" or required {@code Options} * are not supplied. Subclasses may call this if the supplied command line * options are inconsistent with correct operation. **/ protected void printUsage() { PrintWriter out = new PrintWriter(System.err, true); out.println(getVersion()); out.println(getDescription()); out.println(); HelpFormatter helper = new HelpFormatter(); helper.printHelp(out, 79, getCommandLineSyntax(), getUsageHeader(), getOptions(), 7, 4, getUsageFooter()); out.println(); } /** * Displays the product usage, then exits with the supplied code. * This is called automatically if the user invokes the app with "-?", * or "--help" or required {@code Options} are not supplied. * Subclasses may call this if the supplied command line options are * inconsistent with correct operation. * * @param exitCode code to supply to {@code System.exit()} */ protected void printUsageAndExit(int exitCode) { printUsage(); System.exit(exitCode); } /** * Parses the supplied command line arguments according to the configured * {@code Options} generating a {@code CommandLine}. If parsing the options * fails for any reason, or the user specifically requested help, * then {@link #printUsageAndExit(int)} is called. Similarly, if the user * requests the product version, then {@link #printVersionAndExit(int)} * is called. * * @param args String array of supplied command line arguments. */ public CommandLine parseArgs(String[] args) { try { commandLine = new PosixParser().parse(getOptions(), args); if (commandLine.hasOption(HELP_OPTION.getLongOpt())) { printUsageAndExit(0); } else if (commandLine.hasOption(VERSION_OPTION.getLongOpt())) { printVersionAndExit(0); } return commandLine; } catch (ParseException pe) { printUsageAndExit(-1); } return null; } // This is the default keystore config from out-of-box web.xml. private String keystore_type = "JCEKS"; private String keystore_crypto_algo = "AES"; private String keystore_passwd_file = "keystore_passwd"; private String keystore_file = "connector_manager.keystore"; /** * Extracts the keystore configuration from the web.xml. * * @param in an XML InputStream */ private void getKeystoreContextParams(InputStream in) { Document document = XmlParseUtil.parse(in, new SAXParseErrorHandler(), XmlParseUtil.catalogEntityResolver); NodeList params = document.getElementsByTagName("context-param"); if (params == null) { return; } for (int i = 0; i < params.getLength(); i++) { Element param = (Element)params.item(i); String name = XmlParseUtil.getFirstElementByTagName(param, "param-name"); String value = XmlParseUtil.getFirstElementByTagName(param, "param-value"); if (value != null) { if ("keystore_type".equals(name)) { keystore_type = value; } else if ("keystore_crypto_algo".equals(name)) { keystore_crypto_algo = value; } else if ("keystore_passwd_file".equals(name)) { keystore_passwd_file = value; } else if ("keystore_file".equals(name)) { keystore_file = value; } } } } /** * Configure a {@link EncryptedPropertyPlaceholderConfigurer}. * This must be done before starting up a standalone {@link Context}. * Subclasses may override this if they wish to configure the * {@link EncryptedPropertyPlaceholderConfigurer} differently. * * @param webInfDir {@code connector-manager/WEB-INF} directory. */ protected void configureCryptor(File webInfDir) throws IOException { File webXml = new File(webInfDir, "web.xml"); InputStream is = new BufferedInputStream(new FileInputStream(webXml)); getKeystoreContextParams(is); is.close(); // Supply EncryptedPropertyPlaceholder with the keystore config. if (!Strings.isNullOrEmpty(keystore_type)) { EncryptedPropertyPlaceholderConfigurer.setKeyStoreType(keystore_type); } if (!Strings.isNullOrEmpty(keystore_crypto_algo)) { EncryptedPropertyPlaceholderConfigurer .setKeyStoreCryptoAlgo(keystore_crypto_algo); } // Because of differences in ServletContext and StandaloneContext, // there are differences in the expected location of the keystore file. // See keystore configuration in the StartUp servlet for details. if (!Strings.isNullOrEmpty(keystore_file)) { EncryptedPropertyPlaceholderConfigurer .setKeyStorePath(getRealPath(webInfDir, keystore_file)); } if (!Strings.isNullOrEmpty(keystore_passwd_file)) { EncryptedPropertyPlaceholderConfigurer .setKeyStorePasswdPath(getRealPath(webInfDir, keystore_passwd_file)); } } // Relative to a given directory name, where is WEB-INF? private final HashMap<String, String> cmDirsMap = new HashMap<String, String>() {{ put("scripts", "../Tomcat/webapps/connector-manager/WEB-INF"); put("tomcat", "webapps/connector-manager/WEB-INF"); put("webapps", "connector-manager/WEB-INF"); put("connector-manager", "WEB-INF"); put("web-inf", ""); put("local", "google/webapps/connector-manager/WEB-INF"); put("google", "webapps/connector-manager/WEB-INF"); }}; /** * Locate the Connector Manager WEB-INF directory. */ protected File locateWebInf() { String cmdir = System.getProperty("manager.dir", System.getProperty("catalina.base", System.getProperty("user.dir"))); File webinf = locateWebInf(new File(cmdir)); if (webinf == null) { // Maybe we are at the root of the GCI installation. webinf = locateWebInf(new File(cmdir, "Tomcat")); } if (webinf == null) { // Maybe we are at the root of the GSA installation. webinf = locateWebInf(new File(cmdir, "local")); } return webinf; } /** * Locate the Connector Manager WEB-INF directory, relative to dir. */ protected File locateWebInf(File dir) { String path = cmDirsMap.get(dir.getName().toLowerCase()); if (path != null) { File webinf = new File(dir, path); if (webinf.exists() && webinf.isDirectory()) { return webinf.getAbsoluteFile(); } } return null; } /** * Tries to normalize a pathname, as if relative to the context. * Absolute paths are allowed (unlike traditional web-app behaviour). * file: URLs are allowed as well and are treated like absolute paths. * All relative paths are made relative the the web-app WEB-INF directory. * Attempts are made to recognize paths that are already relative to * WEB-INF (they begin with WEB-INF or /WEB-INF). * * @param servletContext the ServletContext * @param name the file name */ private String getRealPath(final File webInfDir, final String name) throws IOException { return ServletUtil.getRealPath(name, new Function<String, String>() { public String apply(String path) { // Force relative paths to be relative to WEB-INF. return new File(webInfDir, name).getAbsolutePath(); } }); } }