// 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();
}
});
}
}