/*
* Copyright 2013 the original author or authors.
*
* 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 org.springframework.yarn.launch;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Base implementation used for launching a Spring Application
* Context and executing a bean using a command line. This
* command line runner is meant to be used from a subclass.
* <p>
* The general idea of this launcher concept is to provide
* a way to define context config location, bean name for execution
* handling, options and a arguments. Possible examples are:
* <br>
* <pre>
* contextConfig
* contextConfig,childContextConfig beanIdentifier
* contextConfig beanIdentifier <arguments>
* contextConfig <options> beanIdentifier
* contextConfig <options> beanIdentifier <arguments>
* <options> contextConfig <options> beanIdentifier <arguments>
* </pre>
*
* @author Janne Valkealahti
*
* @param <T> the type of bean to run
*/
public abstract class AbstractCommandLineRunner<T> {
private static final Log log = LogFactory.getLog(AbstractCommandLineRunner.class);
/** Mapper for exit codes */
private ExitCodeMapper exitCodeMapper = new SimpleJvmExitCodeMapper();
/** Static error message holder for testing */
private static String message = "";
/** Exiter helping for testing */
private static SystemExiter systemExiter = new JvmSystemExiter();
/**
* Gets the static error message set for
* this class. This is useful for tests.
*
* @return the static error message
*/
public static String getErrorMessage() {
return message;
}
/**
* Sets the {@link SystemExiter}. Useful
* for testing.
*
* @param systemExiter the system exiter
*/
public static void presetSystemExiter(SystemExiter systemExiter) {
AbstractCommandLineRunner.systemExiter = systemExiter;
}
/**
* Handles the execution of a bean after Application Context(s) has
* been initialized. This is considered to be a main entry point
* what the application will do after initialization.
* <p>
* It is implementors responsibility to decide what to do
* with the given bean since this class only knows the
* typed bean instance.
*
* @param bean the bean instance
* @param parameters the parameters
* @param opts the options
* @return the exit status
*/
protected abstract ExitStatus handleBeanRun(T bean, String[] parameters, Set<String> opts);
/**
* Gets a default bean id which is used to resolve
* the instance from an Application Context.
*
* @return the id of the bean
*/
protected abstract String getDefaultBeanIdentifier();
/**
* Gets the list of valid option arguments.
* Default implementation returns null thus
* not allowing any options exist on a command line.
* <p>
* When overriding valid options make sure that options
* doesn't match anything else planned to be used in
* a command line. i.e. usually it's advised to prefix
* options with '-' character.
*
* @return the list of option arguments
*/
protected List<String> getValidOpts() {
return null;
}
/**
* Allows subclass to modify parsed context configuration path.
* Effectively path returned from this method is used
* internally for the Application Context config location.
* <p>
* Default implementation just returns the given
* without modifying it.
*
* @param path the parsed config path
* @return the config path
*/
protected String getContextConfigPath(String path) {
return path;
}
/**
* Allows subclass to modify parsed context configuration path.
* Effectively path returned from this method is used
* internally for the Application Context config location.
* <p>
* Default implementation just returns the given
* without modifying it.
*
* @param path the parsed config path
* @return the config path
*/
protected String getChildContextConfigPath(String path) {
return path;
}
/**
* Builds the Application Context(s) and handles 'execution'
* of a bean.
*
* @param configLocation the main context config location
* @param masterIdentifier the bean identifier
* @param childConfigLocation the child context config location
* @param parameters the parameters
* @param opts the options
* @return the status of the execution
*/
protected int start(String configLocation, String masterIdentifier,
String childConfigLocation, String[] parameters, Set<String> opts) {
ConfigurableApplicationContext context = null;
ExitStatus exitStatus = ExitStatus.COMPLETED;
try {
context = getApplicationContext(configLocation);
getChildApplicationContext(childConfigLocation, context);
@SuppressWarnings("unchecked")
T bean = (T) context.getBean(masterIdentifier);
if (log.isDebugEnabled()) {
log.debug("Passing bean=" + bean + " from context=" + context + " for beanId=" + masterIdentifier);
}
exitStatus = handleBeanRun(bean, parameters, opts);
} catch (Throwable e) {
e.printStackTrace();
String message = "Terminated in error: " + e.getMessage();
log.error(message, e);
AbstractCommandLineRunner.message = message;
return exitCodeMapper.intValue(ExitStatus.FAILED.getExitCode());
} finally {
if (context != null) {
context.close();
}
}
return exitCodeMapper.intValue(exitStatus.getExitCode());
}
/**
* Gets the Application Context.
*
* @param configLocation the context config location
* @return the configured context
*/
protected ConfigurableApplicationContext getApplicationContext(String configLocation) {
ConfigurableApplicationContext context;
if (ClassUtils.isPresent(configLocation, getClass().getClassLoader())) {
Class<?> clazz = ClassUtils.resolveClassName(configLocation, getClass().getClassLoader());
context = new AnnotationConfigApplicationContext(clazz);
} else {
context = new ClassPathXmlApplicationContext(configLocation);
}
context.getAutowireCapableBeanFactory().autowireBeanProperties(this,
AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false);
return context;
}
/**
* Gets the Application Context.
*
* @param configLocation the context config location
* @param parent the parent context
* @return the configured context
*/
protected ConfigurableApplicationContext getChildApplicationContext(
String configLocation, ConfigurableApplicationContext parent) {
if (configLocation != null) {
ConfigurableApplicationContext context =
new ClassPathXmlApplicationContext(new String[]{configLocation}, parent);
context.getAutowireCapableBeanFactory().autowireBeanProperties(this,
AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false);
return context;
} else {
return null;
}
}
/**
* Exit method wrapping handling through
* {@link SystemExiter}. This method mostly
* exist order to not do a real exit on
* a unit tests.
*
* @param status the exit code
*/
public void exit(int status) {
systemExiter.exit(status);
}
/**
* Main method visible to sub-classes.
*
* @param args the Arguments
*/
protected void doMain(String[] args) {
AbstractCommandLineRunner.message = "";
// stash normal process arguments
List<String> newargs = new ArrayList<String>(Arrays.asList(args));
// read from stdin
try {
if (System.in.available() > 0) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = " ";
while (StringUtils.hasLength(line)) {
if (!line.startsWith("#") && StringUtils.hasText(line)) {
log.debug("Stdin arg: " + line);
newargs.add(line);
}
line = reader.readLine();
}
}
} catch (IOException e) {
log.warn("Could not access stdin (maybe a platform limitation)");
if (log.isDebugEnabled()) {
log.debug("Exception details", e);
}
}
Set<String> opts = new HashSet<String>();
List<String> params = new ArrayList<String>();
int count = 0;
String ctxConfigPath = null;
String childCtxConfigPath = null;
String beanIdentifier = null;
// did subclass provide valid opts
List<String> validOpts = getValidOpts();
for (String arg : newargs) {
if (validOpts != null && validOpts.contains(arg)) {
opts.add(arg);
} else {
switch (count) {
case 0:
if (!arg.contains("=")) {
String[] argSplit = arg.split(",");
ctxConfigPath = argSplit[0];
if (argSplit.length > 1) {
childCtxConfigPath = argSplit[1];
}
}
break;
case 1:
if (!arg.contains("=")) {
beanIdentifier = arg;
} else {
params.add(arg);
}
break;
default:
params.add(arg);
break;
}
count++;
}
}
if(beanIdentifier == null) {
beanIdentifier = getDefaultBeanIdentifier();
}
ctxConfigPath = getContextConfigPath(ctxConfigPath);
childCtxConfigPath = getChildContextConfigPath(childCtxConfigPath);
if (ctxConfigPath == null || beanIdentifier == null) {
String message = "At least 2 arguments are required: Context Config and Bean Identifier.";
log.error(message);
AbstractCommandLineRunner.message = message;
exit(1);
}
String[] parameters = params.toArray(new String[params.size()]);
int result = start(ctxConfigPath, beanIdentifier, childCtxConfigPath, parameters, opts);
exit(result);
}
}