/*
* Copyright (c) 2007, Dennis M. Sosnoski. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following
* disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of
* JiBX nor the names of its contributors may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jibx.custom;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.jibx.runtime.IUnmarshallingContext;
import org.jibx.runtime.JiBXException;
/**
* Command line processor for all types of customizable tools. This just provides the basic handling of a customizations
* file, target directory, and overrides of values in the customizations root object.
*
* @author Dennis M. Sosnoski
*/
public abstract class CustomizationCommandLineBase
{
/** Array of method parameter classes for single String parameter. */
public static final Class[] STRING_PARAMETER_ARRAY = new Class[] { String.class };
/** Array of classes for String and unmarshaller parameters. */
public static final Class[] STRING_UNMARSHALLER_PARAMETER_ARRAY =
new Class[] { String.class, IUnmarshallingContext.class };
/** Ordered array of usage lines. */
protected static final String[] COMMON_USAGE_LINES =
new String[] { " -c clean target generation directory (ignored if current directory)",
" -f path input customizations file",
" -t path target directory for generated output (default is current directory)",
" -v verbose output flag" };
/** List of specified classes or files. */
protected List m_extraArgs;
/** Target directory for output. */
protected File m_generateDirectory;
/**
* Process command line arguments array.
*
* @param args
* @return true
if valid, false
if not
* @throws JiBXException
* @throws IOException
*/
public boolean processArgs(String[] args) throws JiBXException, IOException {
boolean clean = false;
boolean verbose = false;
String custom = null;
String genpath = null;
Map overrides = new HashMap();
ArgList alist = new ArgList(args);
m_extraArgs = new ArrayList();
while (alist.hasNext()) {
String arg = alist.next();
if ("-c".equalsIgnoreCase(arg)) {
clean = true;
} else if ("-f".equalsIgnoreCase(arg)) {
custom = alist.next();
} else if ("-t".equalsIgnoreCase(arg)) {
genpath = alist.next();
} else if ("-v".equalsIgnoreCase(arg)) {
verbose = true;
} else if (arg.startsWith("--") && arg.length() > 2 && Character.isLetter(arg.charAt(2))) {
if (!putKeyValue(arg.substring(2), overrides)) {
alist.setValid(false);
}
} else if (!checkParameter(alist)) {
if (arg.startsWith("-")) {
System.err.println("Unknown option flag '" + arg + '\'');
alist.setValid(false);
} else {
m_extraArgs.add(alist.current());
break;
}
}
}
// collect the extra arguments at end
while (alist.hasNext()) {
String arg = alist.next();
if (arg.startsWith("-")) {
System.err.println("Command line options must precede all other arguments: error on '" + arg + '\'');
alist.setValid(false);
break;
} else {
m_extraArgs.add(arg);
}
}
// check for valid command line arguments
if (alist.isValid()) {
// set output directory
if (genpath == null) {
m_generateDirectory = new File(".");
clean = false;
} else {
m_generateDirectory = new File(genpath);
}
if (!m_generateDirectory.exists()) {
m_generateDirectory.mkdirs();
clean = false;
}
if (!m_generateDirectory.canWrite()) {
System.err.println("Target directory " + m_generateDirectory.getPath() + " is not writable");
alist.setValid(false);
} else {
// finish the command line processing
finishParameters(alist);
// report on the configuration
if (verbose) {
verboseDetails();
System.out.println("Output to directory " + m_generateDirectory);
}
// clean generate directory if requested
if (clean) {
CustomUtils.clean(m_generateDirectory);
}
// load customizations and check for errors
if (!loadCustomizations(custom)) {
alist.setValid(false);
} else {
// apply command line overrides to customizations
Map unknowns = applyOverrides(overrides);
if (!unknowns.isEmpty()) {
for (Iterator iter = unknowns.keySet().iterator(); iter.hasNext();) {
String key = (String)iter.next();
System.err.println("Unknown override key '" + key + '\'');
}
alist.setValid(false);
}
}
}
} else {
printUsage();
}
return alist.isValid();
}
/**
* Apply a key/value map to a customization object instance. This uses reflection to match the keys to either set
* methods (with names of the form setZZZText, or setZZZ, taking a single String parameter) or String fields (named
* m_ZZZ). The ZZZ in the names is based on the key name, with hyphenation converted to camel case (leading upper
* camel case, for the method names).
*
* @param map
* @param obj
* @return map for key/values not found in the supplied object
*/
public static Map applyKeyValueMap(Map map, Object obj) {
Map missmap = new HashMap();
for (Iterator iter = map.keySet().iterator(); iter.hasNext();) {
String key = (String)iter.next();
String value = (String)map.get(key);
boolean fail = true;
Throwable t = null;
try {
StringBuffer buff = new StringBuffer(key);
for (int i = 0; i < buff.length(); i++) {
char chr = buff.charAt(i);
if (chr == '-') {
buff.deleteCharAt(i);
buff.setCharAt(i, Character.toUpperCase(buff.charAt(i)));
}
}
String fname = "m_" + buff.toString();
buff.setCharAt(0, Character.toUpperCase(buff.charAt(0)));
String mname = "set" + buff.toString();
Method method = null;
Class clas = obj.getClass();
int argcnt = 1;
while (!clas.getName().equals("java.lang.Object")) {
try {
method = clas.getDeclaredMethod(mname + "Text", STRING_UNMARSHALLER_PARAMETER_ARRAY);
argcnt = 2;
break;
} catch (NoSuchMethodException e) {
try {
method = clas.getDeclaredMethod(mname, STRING_PARAMETER_ARRAY);
break;
} catch (NoSuchMethodException e1) {
clas = clas.getSuperclass();
}
}
}
if (method == null) {
clas = obj.getClass();
while (!clas.getName().equals("java.lang.Object")) {
try {
Field field = clas.getDeclaredField(fname);
try {
field.setAccessible(true);
} catch (SecurityException e) { /* deliberately empty */
}
String type = field.getType().getName();
if ("java.lang.String".equals(type)) {
field.set(obj, value);
fail = false;
} else if ("boolean".equals(type) || "java.lang.Boolean".equals(type)) {
Boolean bval = null;
if ("true".equals(value) || "1".equals(value)) {
bval = Boolean.TRUE;
} else if ("false".equals(value) || "0".equals(value)) {
bval = Boolean.FALSE;
}
if (bval == null) {
throw new IllegalArgumentException("Unknown value '" + value
+ "' for boolean parameter " + key);
}
field.set(obj, bval);
fail = false;
} else if ("[Ljava.lang.String;".equals(type)) {
try {
field.set(obj, org.jibx.runtime.Utility.deserializeTokenList(value));
fail = false;
} catch (JiBXException e) {
throw new IllegalArgumentException("Error processing list value + '" + value +
"': " + e.getMessage());
}
} else {
throw new IllegalArgumentException("Cannot handle field of type " + type);
}
break;
} catch (NoSuchFieldException e) {
clas = clas.getSuperclass();
}
}
} else {
try {
method.setAccessible(true);
} catch (SecurityException e) { /* deliberately empty */
}
Object[] args = new Object[argcnt];
args[0] = value;
method.invoke(obj, args);
fail = false;
}
} catch (IllegalAccessException e) {
t = e;
} catch (SecurityException e) {
t = e;
} catch (IllegalArgumentException e) {
t = e;
} catch (InvocationTargetException e) {
t = e;
} finally {
if (t != null) {
t.printStackTrace();
System.exit(1);
}
}
if (fail) {
missmap.put(key, value);
}
}
return missmap;
}
/**
* Get generate directory.
*
* @return directory
*/
public File getGeneratePath() {
return m_generateDirectory;
}
/**
* Get extra arguments from command line. These extra arguments must follow all parameter flags.
*
* @return args
*/
public List getExtraArgs() {
return m_extraArgs;
}
/**
* Set a key=value definition in a map. This is a command line processing assist method that prints an error message
* directly if the expected format is not found.
*
* @param def
* @param map
* @return true
if successful, false
if error
*/
public static boolean putKeyValue(String def, Map map) {
int split = def.indexOf('=');
if (split >= 0) {
String key = def.substring(0, split);
if (map.containsKey(key)) {
System.err.println("Repeated key item: '" + def + '\'');
return false;
} else {
map.put(key, def.substring(split + 1));
return true;
}
} else {
System.err.println("Missing '=' in expected key=value item: '" + def + '\'');
return false;
}
}
/**
* Merge two arrays of strings, returning an ordered array containing all the strings from both provided arrays.
*
* @param base
* @param adds
* @return ordered merged
*/
protected String[] mergeUsageLines(String[] base, String[] adds) {
if (adds.length == 0) {
return base;
} else {
String fulls[] = new String[base.length + adds.length];
System.arraycopy(base, 0, fulls, 0, base.length);
System.arraycopy(adds, 0, fulls, base.length, adds.length);
Arrays.sort(fulls);
return fulls;
}
}
/**
* Check extension parameter. This method may be overridden by subclasses to process parameters beyond those known
* to this base class.
*
* @param alist argument list
* @return true
if parameter processed, false
if unknown
*/
protected boolean checkParameter(ArgList alist) {
return false;
}
/**
* Finish processing of command line parameters. This method may be overridden by subclasses to implement any added
* processing after all the command line parameters have been handled.
*
* @param alist
*/
protected void finishParameters(ArgList alist) {}
/**
* Print any extension details. This method may be overridden by subclasses to print extension parameter values for
* verbose output.
*/
protected void verboseDetails() {}
/**
* Load the customizations file. This method must load the specified customizations file, or create a default
* customizations instance, of the appropriate type.
*
* @param path customization file path, null
if none
* @return true
if successful, false
if an error
* @throws JiBXException
* @throws IOException
*/
protected abstract boolean loadCustomizations(String path) throws JiBXException, IOException;
/**
* Apply map of override values to customizations read from file or created as default.
*
* @param overmap override key-value map
* @return map for key/values not recognized
*/
protected abstract Map applyOverrides(Map overmap);
/**
* Print usage information.
*/
public abstract void printUsage();
/**
* Wrapper class for command line argument list.
*/
protected static class ArgList
{
private int m_offset;
private final String[] m_args;
private boolean m_valid;
/**
* Constructor.
*
* @param args
*/
protected ArgList(String[] args) {
m_offset = -1;
m_args = args;
m_valid = true;
}
/**
* Check if another argument value is present.
*
* @return true
if argument present, false
if all processed
*/
public boolean hasNext() {
return m_args.length - m_offset > 1;
}
/**
* Get current argument value.
*
* @return argument, or null
if none
*/
public String current() {
return (m_offset >= 0 && m_offset < m_args.length) ? m_args[m_offset] : null;
}
/**
* Get next argument value. If this is called with no argument value available it sets the argument list
* invalid.
*
* @return argument, or null
if none
*/
public String next() {
if (++m_offset < m_args.length) {
return m_args[m_offset];
} else {
m_valid = false;
return null;
}
}
/**
* Set valid state.
*
* @param valid
*/
public void setValid(boolean valid) {
m_valid = valid;
}
/**
* Check if argument list valid.
*
* @return true
if valid, false
if not
*/
public boolean isValid() {
return m_valid;
}
}
}