/* Copyright 2010 Kindleit.net Software Development
*
* 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.
*
* Includes contributions adapted from the Jetty Maven Plugin
* Copyright 2000-2004 Mort Bay Consulting Pty. Ltd.
*/
package net.kindleit.gae;
import static org.codehaus.plexus.util.StringUtils.isEmpty;
import static org.codehaus.plexus.util.StringUtils.isNotEmpty;
import java.io.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.codehaus.plexus.PlexusConstants;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.context.Context;
import org.codehaus.plexus.context.ContextException;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
import com.google.appengine.tools.admin.AppCfg;
/**
* Base MOJO class for working with the Google App Engine SDK.
*
* @author rhansen@kindleit.net
*/
public abstract class EngineGoalBase extends AbstractMojo implements Contextualizable {
private static final String SECURITY_DISPATCHER_CLASS_NAME = "org.sonatype.plexus.components.sec.dispatcher.SecDispatcher";
private static final String GAE_PROPS = "gae.properties";
private static final String INTERRUPTED_EXCEPTION = "Interrupted waiting for process supervisor thread to finish";
protected static final String[] ARG_TYPE = new String[0];
/**
* Plexus container, needed to manually lookup components.
*
* To be able to use Password Encryption http://maven.apache.org/guides/mini/guide-encryption.html
*/
protected PlexusContainer container;
/**
* The Maven settings reference.
*
* @parameter expression="${settings}"
* @required
* @readonly
*/
protected Settings settings;
/**
* The character encoding scheme to be applied interacting with the SDK. Sent as the --compile_encoding flag.
*
* @parameter expression="${encoding}" default-value="${project.build.sourceEncoding}"
* @since 0.8.3
*/
protected String encoding;
/**
* Overrides where the Project War Directory is located.
*
* @parameter expression="${project.build.directory}/${project.build.finalName}"
* @required
*/
protected String appDir;
/**
* Specifies where the Google App Engine SDK is located.
*
* @parameter expression="${gae.home}" default-value=
* "${settings.localRepository}/com/google/appengine/appengine-java-sdk/${gae.version}/appengine-java-sdk-${gae.version}"
* @required
*/
protected String sdkDir;
/**
* Split large jar files (> 10M) into smaller fragments.
*
* @parameter expression="${gae.deps.split}" default-value="false"
*/
protected boolean splitJars;
/**
* The username to use. Will prompt if omitted.
*
* @parameter expression="${gae.email}"
* @deprecated use maven settings.xml/server/username and "serverId" parameter
*/
@Deprecated
protected String emailAccount;
/**
* The server id in maven settings.xml to use for emailAccount(username) and password when connecting to GAE.
*
* If password present in settings "--passin" is set automatically.
*
* @parameter expression="${gae.serverId}"
*/
protected String serverId;
/**
* The server to connect to.
*
* @parameter expression="${gae.server}"
*/
protected String uploadServer;
/**
* The app id. If defined, it overrides the application name defined in the appengine-web.xml.
*
* @parameter expression="${gae.appId}"
* @since 0.9.3
*/
protected String appId;
/**
* The app version. If defined, it overrides the application major version defined in the appengine-web.xml.
*
* @parameter expression="${gae.appVersion}"
* @since 0.9.3
*/
protected String appVersion;
/**
* Overrides the Host header sent with all RPCs.
*
* @parameter expression="${gae.host}"
*/
protected String hostString;
/**
* Do not delete temporary directory used in uploading.
*
* @parameter expression="${gae.keepTemps}" default-value="false"
*/
protected boolean keepTempUploadDir;
/**
* Always read the login password from stdin.
*
* @parameter expression="${gae.passin}" default-value="false"
*/
protected boolean passIn;
/**
* Tell AppCfg to use a proxy.
*
* By default will use first active proxy in maven settings.xml
*
* @parameter expression="${gae.proxy}"
*/
protected String proxy;
/**
* Decides whether to wait after the server is started or to return the execution flow to the user.
*
* @parameter expression="${gae.wait}" default-value="false"
*/
protected boolean wait;
/**
* Port to listen for stop requests on.
*
* @parameter expression="${gae.monitor.port}" default-value="8081"
*/
protected int monitorPort;
/**
* Key to provide when making stop requests.
*
* @parameter expression="${gae.monitor.key}" default-value="monitor.${project.artifactId}"
*/
protected String monitorKey;
/**
* Arbitrary list of Goal Arguments to pass along to the app engine task.
*
* @since 0.9.4
* @parameter
*/
protected List<String> goalArguments;
protected Properties gaeProperties;
public EngineGoalBase() {
gaeProperties = new Properties();
try {
gaeProperties.load(EngineGoalBase.class.getResourceAsStream(GAE_PROPS));
} catch (final IOException e) {
throw new RuntimeException("Unable to load version", e);
}
}
@Override
public void contextualize(final Context context) throws ContextException {
container = (PlexusContainer) context.get(PlexusConstants.PLEXUS_KEY);
}
protected boolean hasServerSettings() {
if (isEmpty(serverId)) {
return false;
} else {
final Server srv = settings.getServer(serverId);
return srv != null;
}
}
/**
* Passes command to the Google App Engine AppCfg runner.
*
* @param command command to run through AppCfg
* @param commandArguments arguments to the AppCfg command.
* @throws MojoExecutionException If {@link #ensureSystemProperties()} fails
*/
protected final void runAppCfg(final String command, final String... commandArguments) throws MojoExecutionException {
final List<String> args = new ArrayList<String>();
args.addAll(getAppCfgArgs());
args.add(command);
args.addAll(Arrays.asList(commandArguments));
if (goalArguments != null) {
args.addAll(goalArguments);
}
ensureSystemProperties();
getLog().debug("execute AppCfg " + args.toString());
if (hasServerSettings()) {
forkPasswordExpectThread(args.toArray(ARG_TYPE), decryptPassword(settings.getServer(serverId).getPassword()));
return;
}
AppCfg.main(args.toArray(ARG_TYPE));
}
/**
* Groups alterations to System properties for the proper execution of the actual GAE code.
*
* @throws MojoExecutionException When the gae.home variable cannot be set.
*/
protected void ensureSystemProperties() throws MojoExecutionException {
// explicitly specify SDK root, as auto-discovery fails when
// appengine-tools-api.jar is loaded from Maven repo, not SDK
String sdk = System.getProperty("appengine.sdk.root");
if (isEmpty(sdk)) {
if (isEmpty(sdkDir)) {
throw new MojoExecutionException(this, "${gae.home} property not set",
gaeProperties.getProperty("home_undefined"));
}
System.setProperty("appengine.sdk.root", sdk = sdkDir);
}
if (!new File(sdk).isDirectory()) {
throw new MojoExecutionException(this, "${gae.home} is not a directory",
gaeProperties.getProperty("home_invalid"));
}
// hack for getting appengine-tools-api.jar on a runtime classpath
// (KickStart checks java.class.path system property for classpath entries)
final String classpath = System.getProperty("java.class.path");
final String toolsJar = sdkDir + "/lib/appengine-tools-api.jar";
if (!classpath.contains(toolsJar)) {
System.setProperty("java.class.path", classpath + File.pathSeparator + toolsJar);
}
}
/**
* Generate all common Google AppEngine Task Parameters for use in all the goals.
*
* @return List of arguments to add.
*/
protected List<String> getAppCfgArgs() {
final List<String> args = getCommonArgs();
addEmailOption(args);
addStringOption(args, "--application=", appId);
addStringOption(args, "--version=", appVersion);
addStringOption(args, "--host=", hostString);
addStringOption(args, "--compile_encoding=", encoding);
addProxyOption(args);
addBooleanOption(args, "--passin", passIn);
if (!passIn) {
addBooleanOption(args, "--disable_prompt", !settings.getInteractiveMode());
}
addBooleanOption(args, "--enable_jar_splitting", splitJars);
addBooleanOption(args, "--retain_upload_dir", keepTempUploadDir);
return args;
}
protected final List<String> getCommonArgs() {
final List<String> args = new ArrayList<String>(9);
args.add("--sdk_root=" + sdkDir);
addStringOption(args, "--server=", uploadServer);
return args;
}
private void forkPasswordExpectThread(final String[] args, final String password) {
getLog().info("Use Settings configuration from server id {" + serverId + "}");
// Parent for all threads created by AppCfg
final ThreadGroup threads = new ThreadGroup("AppCfgThreadGroup");
// Main execution Thread that belong to ThreadGroup threads
final Thread thread = new Thread(threads, "AppCfgMainThread") {
@Override
public void run() {
final PrintStream outOrig = System.out;
final InputStream inOrig = System.in;
final PipedInputStream inReplace = new PipedInputStream();
OutputStream stdin;
try {
stdin = new PipedOutputStream(inReplace);
} catch (final IOException e) {
getLog().error("Unable to redirect input", e);
return;
}
System.setIn(inReplace);
final BufferedWriter stdinWriter = new BufferedWriter(new OutputStreamWriter(stdin));
System.setOut(new PrintStream(new PasswordExpectOutputStream(threads, outOrig, new Runnable() {
@Override
public void run() {
try {
stdinWriter.write(password);
stdinWriter.newLine();
stdinWriter.flush();
} catch (final IOException e) {
getLog().error("Unable to enter password", e);
}
}
}), true));
try {
AppCfg.main(args);
} catch (final Throwable e) {
getLog().error("Unable to execute AppCfg", e);
} finally {
System.setOut(outOrig);
System.setIn(inOrig);
}
}
};
thread.start();
try {
thread.join();
} catch (final InterruptedException e) {
getLog().error(INTERRUPTED_EXCEPTION, e);
}
}
private String decryptPassword(final String password) {
if (isNotEmpty(password)) {
try {
final Class<?> securityDispatcherClass = container.getClass().getClassLoader()
.loadClass(SECURITY_DISPATCHER_CLASS_NAME);
final Object securityDispatcher = container.lookup(SECURITY_DISPATCHER_CLASS_NAME, "maven");
final Method decrypt = securityDispatcherClass.getMethod("decrypt", String.class);
return (String) decrypt.invoke(securityDispatcher, password);
} catch (final Exception e) {
getLog().warn("security features are disabled. Cannot find plexus security dispatcher", e);
}
}
getLog().debug("password could not be decrypted");
return password;
}
private void addEmailOption(final List<String> args) {
if (hasServerSettings() && emailAccount == null) {
addStringOption(args, "--email=", settings.getServer(serverId).getUsername());
if (settings.getServer(serverId).getPassword() != null) {
// Force GAE tools to read from System.in instead of System.console()
passIn = true;
}
} else {
addStringOption(args, "--email=", emailAccount);
}
}
private void addProxyOption(final List<String> args) {
if (isNotEmpty(proxy)) {
addStringOption(args, "--proxy=", proxy);
} else if (hasServerSettings()) {
final Proxy activCfgProxy = settings.getActiveProxy();
if (activCfgProxy != null) {
addStringOption(args, "--proxy=", activCfgProxy.getHost() + ":" + activCfgProxy.getPort());
}
}
}
private final void addBooleanOption(final List<String> args, final String key, final boolean var) {
if (var) {
args.add(key);
}
}
private final void addStringOption(final List<String> args, final String key, final String var) {
if (isNotEmpty(var)) {
args.add(key + var);
}
}
}