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