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