blob: a971f524b6f7c90d8d619817ab57f7382c38541e [file] [log] [blame]
/*********************************************************************
* Copyright (c) 2018 The University of York.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipse.epsilon.eol.cli;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.commons.cli.Option;
import org.eclipse.epsilon.common.cli.ConfigParser;
import org.eclipse.epsilon.common.util.FileUtil;
import org.eclipse.epsilon.common.util.StringProperties;
import org.eclipse.epsilon.common.util.StringUtil;
import org.eclipse.epsilon.eol.models.IModel;
import org.eclipse.epsilon.eol.IEolModule;
import org.eclipse.epsilon.eol.launch.EolRunConfiguration;
import org.eclipse.epsilon.eol.launch.IEolRunConfiguration;
/**
* A default config getter which effectively allows main method inheritance.
* Uses reflection to find appropriate constructors and module interface to pass to parseModule method.
* Please note: The constructors of this class must be inherited in R!
* <br/>
* Note that this needn't be subclassed to use it,
* you can just add the required projects to the classpath
* and call it with appropriate arguments, but you must provide
* a module with the -module option.
*
* @author Sina Madani
* @since 1.6
*/
public class EolConfigParser<C extends IEolRunConfiguration, B extends IEolRunConfiguration.Builder<C, B>> extends ConfigParser<C, B> {
/**
* Allows the caller to invoke any subclass of IEolModule.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void main(String... args) throws ClassNotFoundException {
if (args.length > 0) {
if (args[0].toUpperCase().startsWith("CONFIG")) {
Class<? extends IEolRunConfiguration> configClass = (Class<? extends IEolRunConfiguration>)
Class.forName(args[0].substring(7));
String[] adjustedArgs = Arrays.copyOfRange(args, 1, args.length);
new EolConfigParser(IEolRunConfiguration.Builder(configClass)).apply(adjustedArgs).run();
}
else {
new EolConfigParser(IEolRunConfiguration.Builder(getRunConfigurationForScript(args[0])))
.apply(args).run();
}
}
}
private final String
moduleOpt = "module",
modelsOpt = "models",
scriptParamsOpt = "parameters";
public EolConfigParser(B builder) {
super(builder);
requiredUsage += "-models [model class]#[model properties];"+nL;
optionalUsage += " [module] [argtype=argvalue]s..."+nL;
options.addOption(Option.builder(moduleOpt)
.hasArg()
.desc("Specify the module and arguments to the module in key-value pairs. "
+ "Please note: the arguments type must be a fully qualified class and the class must have a String constructor"
+ "which is used to parse the provided argument."
)
.optionalArg(false)
.hasArgs()
.valueSeparator()
.build()
);
options.addOption(Option.builder(modelsOpt)
.hasArgs()
.desc("Specify the models and properties. The format first specifies the concrete Java class to"
+ "be instantiated (fully qualified name after org.eclipse.epsilon.emc.), followed by #,"
+ "followed by comma-separated key=value properties. For example: "
+ "emf.EmfModel#name=modelName,cached=true;plainxml.PlainXmlModel#name=model2. "
+ "This example specifies an EMF and a PlainXML model with their names as properties."
)
.valueSeparator(';')
.build()
);
options.addOption(Option.builder(scriptParamsOpt)
.hasArgs()
.desc("Specify parameters to the script in comma-separated key=value pairs. Note that "
+ "the type of variable passed will always be a String."
)
.optionalArg(true)
.valueSeparator(',')
.build()
);
}
@Override
protected void parseArgs(String[] args) throws Exception {
super.parseArgs(args);
if (cmdLine.hasOption(moduleOpt)) {
builder.module = parseModule(cmdLine.getOptionValues(moduleOpt));
}
if (cmdLine.hasOption(modelsOpt)) {
builder.modelsAndProperties.putAll(parseModelParameters(cmdLine.getOptionValues(modelsOpt)));
}
if (cmdLine.hasOption(scriptParamsOpt)) {
builder.parameters.putAll(parseScriptParameters(cmdLine.getOptionValues(scriptParamsOpt)));
}
}
public static Map<IModel, StringProperties> parseModelParameters(String[] arguments) throws Exception {
Map<IModel, StringProperties> modelMap = new HashMap<>(arguments.length);
for (String arg : arguments) {
String[] modelPropertyEntry = arg.split("#");
if (modelPropertyEntry.length != 2)
continue;
IModel model = (IModel) Class.forName("org.eclipse.epsilon.emc."+modelPropertyEntry[0])
.getDeclaredConstructor()
.newInstance();
StringProperties properties = new StringProperties();
for (String propertyToken : modelPropertyEntry[1].split(",")) {
String[] propEntry = propertyToken.split("=");
assert propEntry.length == 2;
properties.put(propEntry[0], propEntry[1]);
}
modelMap.put(model, properties);
}
return modelMap;
}
public static Map<String, Object> parseScriptParameters(String[] arguments) {
return Arrays.stream(arguments)
.map(param -> {
String[] entry = param.split("=");
String key, value;
if (entry.length != 2) {
key = ""; value = "";
}
else {
key = entry[0]; value = entry[1];
}
return new AbstractMap.SimpleEntry<>(key, value);
})
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
/**
* Attempts to parse a module from command-line arguments, based on assumptions on Epsilon's conventional naming schemes and package structure.
* The names are based on the class name of the return type; so for example for IEolModule, it will use "ERL" as the language and look
* for the appropriate modules and contexts based on this name.
*
* @param args the name of the module (following org.eclipse.epsilon.) followed by an even-arity array with arguments to provide to the context constructor,
* where every even numbered argument (including 0) is the class (type) and every odd numbered argument is the value.
* Note: only types with a String constructor are valid.
*/
@SuppressWarnings("unchecked")
protected static <R extends IEolModule> R parseModule(String[] args) throws IllegalArgumentException {
String basePkg = "org.eclipse.epsilon.";
try {
if (args.length == 0)
throw new IllegalArgumentException("Must provide a module name.");
int additionals = args.length-1;
if (additionals % 2 != 0)
throw new IllegalArgumentException("Must provide the types and arguments for module.");
int arrSize = additionals/2;
Class<?>[] moduleArgTypes = new Class[arrSize];
Object[] parsedArgs = new Object[arrSize];
for (int l = 0, a = 0; l < arrSize*2; l += 2, a++) {
Class<?>[] type = getType(args[l+1]);
Class<?> constructType = type.length == 2 ? type[1] : type[0];
moduleArgTypes[a] = type[0];
parsedArgs[a] = constructType.getConstructor(String.class).newInstance(args[l+2]);
}
Class<?> moduleClass = Class.forName(basePkg+args[0]);
try {
return (R) moduleClass.getDeclaredConstructor(moduleArgTypes).newInstance(parsedArgs);
}
catch (IllegalAccessException ex) {
System.err.println("WARNING: Could not find appropriate constructor for supplied parameters. Proceeding with defaults.");
System.err.println(ex.getMessage());
return (R) moduleClass.getConstructor().newInstance();
}
}
catch (Exception ex) {
throw new IllegalArgumentException("Could not find or instantiate the module: "+ex.getMessage());
}
}
@SuppressWarnings("unchecked")
static Class<? extends IEolRunConfiguration> getRunConfigurationForScript(String scriptPath) {
String ext = FileUtil.getExtension(scriptPath).toLowerCase();
String pkg = ext.equals("egx") ? "egl" : ext;
String className = "org.eclipse.epsilon."+pkg+".launch."+StringUtil.firstToUpper(ext)+"RunConfiguration";
try {
return (Class<? extends IEolRunConfiguration>) Class.forName(className);
}
catch (ClassNotFoundException cnfx) {
return EolRunConfiguration.class;
}
}
private static Class<?>[] getType(String specifiedType) throws ClassNotFoundException {
final String pkg = "java.lang.";
switch (specifiedType) {
case "int": case "Integer": case pkg+"Integer":
return new Class[]{Integer.TYPE, Integer.class};
case "boolean": case "bool": case "Boolean": case pkg+"Boolean":
return new Class[]{Boolean.TYPE, Boolean.class};
case "double": case "Double": case pkg+"Double":
return new Class[]{Double.TYPE, Double.class};
case "float": case "Float": case pkg+"Float":
return new Class[]{Float.TYPE, Float.class};
case "long": case "Long": case pkg+"Long":
return new Class[]{Long.TYPE, Long.class};
case "byte": case "Byte": case pkg+"Byte":
return new Class[]{Byte.TYPE, Byte.class};
case "char": case "Character": case pkg+"Character":
return new Class[]{Character.TYPE, Character.class};
case "String": case "string": case pkg+"String":
return new Class[]{String.class};
default:
return new Class[]{Class.forName(specifiedType)};
}
}
}