/* * Copyright 2013 Guidewire Software, Inc. */ package gw.lang.cli; import gw.config.CommonServices; import gw.internal.ext.org.apache.commons.cli.BasicParser; import gw.internal.ext.org.apache.commons.cli.CommandLine; import gw.internal.ext.org.apache.commons.cli.CommandLineParser; import gw.internal.ext.org.apache.commons.cli.Option; import gw.internal.ext.org.apache.commons.cli.Options; import gw.internal.ext.org.apache.commons.cli.ParseException; import gw.lang.reflect.IAnnotationInfo; import gw.lang.reflect.IPropertyInfo; import gw.lang.reflect.IType; import gw.lang.reflect.ITypeInfo; import gw.lang.reflect.TypeSystem; import gw.lang.reflect.gs.IGosuVarPropertyInfo; import gw.lang.reflect.java.JavaTypes; import gw.util.StreamUtil; import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; public class CommandLineAccess { private static List<String> _args = new ArrayList<String>(); private static File _currentProgram = null; private static boolean _exitEnabled = true; private static boolean _useTerminalWidth = true; /** * @return the raw string arguments a Gosu program was started with */ @SuppressWarnings({"UnusedDeclaration"}) public static List<String> getRawArgs() { return _args; } /** * @param args the args to a Gosu program */ public static void setRawArgs(List<String> args) { _args = args; } /** * @return the currently executing program as a file object */ public static File getCurrentProgram() { return _currentProgram; } /** * @param currentProgram - the currently executing program */ public static void setCurrentProgram(File currentProgram) { _currentProgram = currentProgram; } /** * @return true if CommandLineAccess will attempt to use the active terminals width * when printing it's help message */ @SuppressWarnings({"UnusedDeclaration"}) public static boolean isUseTerminalWidth() { return _useTerminalWidth; } /** * Set to true for CommandLineAccess to use the active terminals width when printing it's help message */ public static void setUseTerminalWidth(boolean b) { CommandLineAccess._useTerminalWidth = b; } /** * Initializes the static properties on the given type based on the command * line arguments. If the arguments incorrectly map to the given type, * a help message will be printed and the JVM will exit with a -1 return value. * * @param commandLineShell the class to initialize from the passed in arguments */ public static void initialize(IType commandLineShell) { initialize( commandLineShell, true ); } /** * Initializes the properties on the given object based on the command * line arguments. If the object passed in is a type, static properties * will be initialized. * <p/> * Note that you will get -h, -help and --help for free, there is no need to * explicitly include a help property on your command line class. * * @param obj the class to initialize from the passed in arguments * @param exitOnBadArgs if true is passed in and the arguments incorrectly map to the * given type, a help message will be printed and the JVM will exit with a -1 return value, * otherwise a false value will be returned * @return true if initialization was successful */ public static boolean initialize(Object obj, boolean exitOnBadArgs) { List<IPropertyInfo> propsToSet = new ArrayList<IPropertyInfo>(); IType type = obj instanceof IType ? ((IType) obj) : TypeSystem.getFromObject( obj ); ITypeInfo typeInfo = type.getTypeInfo(); Options options = deriveOptionsFromTypeInfo( typeInfo, propsToSet, obj instanceof IType ); CommandLineParser parser = new BasicParser(); try { CommandLine cl = parser.parse( options, _args.toArray( new String[_args.size()] ) ); for (IPropertyInfo propertyInfo : propsToSet) { propertyInfo = typeInfo.getProperty( propertyInfo.getName() ); if( isBooleanProp( propertyInfo ) ) { propertyInfo.getAccessor().setValue( obj, cl.hasOption( getShortName( propertyInfo ) ) ); } else { String defaultValue = null; if (propertyInfo.hasAnnotation(TypeSystem.get(DefaultValue.class))) { IAnnotationInfo annotationInfo = propertyInfo.getAnnotationsOfType( TypeSystem.get( DefaultValue.class ) ).get( 0 ); defaultValue = ((DefaultValue)annotationInfo.getInstance()).value(); } String shortName = getShortName( propertyInfo ); Object value; if (propertyInfo.getFeatureType().isArray()) { value = cl.getOptionValues( shortName ); if (propertyInfo.hasAnnotation(TypeSystem.get(Args.class))) { value = cl.getArgs(); } else if (value == null) { if (defaultValue != null) { value = defaultValue.split( " +" ); } else { //Set the value to an empty array if the option is present if (cl.hasOption(shortName)) { value = new String[0]; } } } } else { if (!needsArg(propertyInfo) && defaultValue == null) { defaultValue = ""; } value = cl.getOptionValue( shortName, defaultValue ); } try { propertyInfo.getAccessor().setValue( obj, convertValue( propertyInfo.getFeatureType(), value ) ); } catch (Exception e) { throw new ParseException( "The parameter \"" + shortName + "\" requires an argument of type " + propertyInfo.getFeatureType().getRelativeName() + ". The value \"" + value + "\" cannot be converted to this type. Please pass in a valid value." + (e.getMessage() == null ? "" : " Error message was : " + e.getMessage()) ); } } } } catch (ParseException e) { if (exitOnBadArgs) { if( !e.getMessage().endsWith( "-help" ) && !e.getMessage().endsWith( "-h" ) && !e.getMessage().endsWith("--help")) { System.out.println("\nArgument problem: " + e.getMessage() + "\n"); } try { showHelp( getCurrentProgramName(), type ); } catch (StringIndexOutOfBoundsException e1) { System.out.println( "Unable to print help message. Exiting." ); } if (_exitEnabled) { System.exit( -1 ); } throw new SystemExitIgnoredException(); } return false; } return true; } static Object convertValue(IType type, Object value) { if (value instanceof String) { if (type == JavaTypes.SHORT() || type == JavaTypes.pSHORT()) { return Short.parseShort( value.toString() ); } if (type == JavaTypes.INTEGER() || type == JavaTypes.pINT()) { return Integer.parseInt( value.toString() ); } if (type == JavaTypes.LONG() || type == JavaTypes.pLONG()) { return Long.parseLong( value.toString() ); } if (type == JavaTypes.FLOAT() || type == JavaTypes.pFLOAT()) { return Float.parseFloat( value.toString() ); } if (type == JavaTypes.DOUBLE() || type == JavaTypes.pDOUBLE()) { return Double.parseDouble( value.toString() ); } if (type == JavaTypes.DATE()) { try { return CommonServices.getCoercionManager().parseDateTime( value.toString() ); } catch (java.text.ParseException e) { throw new RuntimeException( e ); } } } return CommonServices.getCoercionManager().convertValue( value, type ); } private static String getCurrentProgramName() { File file = getCurrentProgram(); if (file != null) { return file.getName(); } else { return "unknown program"; } } /** * Shows a help message for the program arguments derived from the given type, sent * to stdout * * @param programName the name of the program * @param obj either the type of the command line shell if static properties are used, or the instance if instance properties are used */ public static void showHelp(String programName, Object obj) { PrintWriter pw = new PrintWriter( StreamUtil.getOutputStreamWriter( System.out ) ); printHelpToWriter( programName, obj, pw ); pw.flush(); } /** * Shows a help message for the program arguments derived from the given type, sent * to stdout * * @param obj either the type of the command line shell if static properties are used, or the instance if instance properties are used */ @SuppressWarnings({"UnusedDeclaration"}) public static void showHelp(IType obj) { PrintWriter pw = new PrintWriter( StreamUtil.getOutputStreamWriter( System.out ) ); printHelpToWriter( getCurrentProgramName(), obj, pw ); pw.flush(); } /** * Returns the help message derived from the given type * * @param obj the object or type of the command line shell * @return the help message derived from the given type */ public static String getHelpMessageFor(Object obj) { StringWriter writer = new StringWriter(); PrintWriter pw = new PrintWriter( writer ); printHelpToWriter( getCurrentProgramName(), obj, pw ); pw.flush(); return writer.toString(); } private static void printHelpToWriter(String programName, Object obj, PrintWriter pw) { GosuHelpFormatter formatter = new GosuHelpFormatter(); if( _useTerminalWidth ) // may be invalid if run from a non-terminal environment { formatter.setWidth(80); } ITypeInfo typeInfo = obj instanceof IType ? ((IType)obj).getTypeInfo() : TypeSystem.getFromObject( obj ).getTypeInfo(); Options options = deriveOptionsFromTypeInfo( typeInfo, new ArrayList<IPropertyInfo>(), obj instanceof IType ); System.err.println("Printing help with defaultWidth= " + formatter.getWidth() + " programName= " + programName + " defaultLeftPad= " + formatter.getLeftPadding() + " defaultDescPad= " + formatter.getDescPadding()); formatter.printHelp(pw, formatter.getWidth(), programName, null, options, formatter.getLeftPadding(), formatter.getDescPadding(), null, false); } private static Options deriveOptionsFromTypeInfo( ITypeInfo typeInfo, /*IN-OUT*/ List<IPropertyInfo> propsToSet, boolean useStaticProps) { List<? extends IPropertyInfo> propertyInfos = typeInfo.getProperties(); Options options = new Options(); for (IPropertyInfo propertyInfo : propertyInfos) { if( propertyInfo instanceof IGosuVarPropertyInfo) { IGosuVarPropertyInfo gsVarPropInfo = (IGosuVarPropertyInfo) propertyInfo; if (gsVarPropInfo.hasDeclaredProperty() || gsVarPropInfo.isFinal()) { continue; } } if (useStaticProps == propertyInfo.isStatic() && propertyInfo.isWritable(null)) { String shortName = getShortName( propertyInfo ); boolean needsArg = needsArg( propertyInfo ); GosuOption opt = new GosuOption( shortName, getLongName( propertyInfo ), needsArg, deriveDescription( propertyInfo ) ); opt.setHidden( propertyInfo.hasAnnotation( TypeSystem.get( Hidden.class ) ) ); opt.setRequired( propertyInfo.hasAnnotation( TypeSystem.get( Required.class ) ) ); if (propertyInfo.getFeatureType().isArray()) { opt.setType( propertyInfo.getFeatureType().getComponentType().getName() ); if (propertyInfo.hasAnnotation(TypeSystem.get(ArgNames.class))) { ArgNames argNames = (ArgNames)propertyInfo.getAnnotationsOfType( TypeSystem.get( ArgNames.class ) ).get( 0 ).getInstance(); if (argNames.names() != null && argNames.names().length > 0) { String compoundNames = ""; for (int i = 0; i < argNames.names().length; i++) { if (i != 0) { compoundNames += " "; } compoundNames += argNames.names()[i]; } opt.setArgName( compoundNames ); if (propertyInfo.hasAnnotation(TypeSystem.get(ArgOptional.class))) { opt.setOptionalArg( true ); } opt.setArgs( argNames.names().length ); } } else { opt.setArgs( Option.UNLIMITED_VALUES ); } if (propertyInfo.hasAnnotation(TypeSystem.get(Separator.class))) { Separator argNames = (Separator)propertyInfo.getAnnotationsOfType( TypeSystem.get( Separator.class ) ).get( 0 ).getInstance(); opt.setValueSeparator( argNames.value().charAt( 0 ) ); } } else { opt.setType( propertyInfo.getFeatureType().getName() ); if (propertyInfo.hasAnnotation(TypeSystem.get(ArgNames.class))) { ArgNames argNames = (ArgNames)propertyInfo.getAnnotationsOfType( TypeSystem.get( ArgNames.class ) ).get( 0 ).getInstance(); if (argNames.names() != null && argNames.names().length > 0) { opt.setArgName( argNames.names()[0] ); } } } propsToSet.add( propertyInfo ); options.addOption( opt ); } } return options; } protected static String deriveDescription(IPropertyInfo propertyInfo) { IType intrinsicType = propertyInfo.getOwnersType(); String description; try { ResourceBundle resourceBundle = ResourceBundle.getBundle( intrinsicType.getName(), Locale.getDefault(), TypeSystem.getGosuClassLoader().getActualLoader() ); description = resourceBundle.getString(propertyInfo.getName()); } catch (MissingResourceException e) { description = propertyInfo.getDescription(); } return description == null ? "" : description.replaceAll("\n", " "); } private static String getShortName(IPropertyInfo propertyInfo) { String shortName = makeCmdLineOptionName( propertyInfo ); if (propertyInfo.hasAnnotation(TypeSystem.get(ShortName.class))) { IAnnotationInfo annotation = propertyInfo.getAnnotationsOfType( TypeSystem.get( ShortName.class ) ).get( 0 ); ShortName value = (ShortName)annotation.getInstance(); shortName = value.name(); } return shortName; } private static String getLongName(IPropertyInfo propertyInfo) { String shortName = makeCmdLineOptionName( propertyInfo ); if (propertyInfo.hasAnnotation(TypeSystem.get(LongName.class))) { IAnnotationInfo annotation = propertyInfo.getAnnotationsOfType( TypeSystem.get( LongName.class ) ).get( 0 ); LongName value = (LongName)annotation.getInstance(); shortName = value.name(); } return shortName; } private static String makeCmdLineOptionName(IPropertyInfo propertyInfo) { String name = propertyInfo.getName(); name = name.substring(0, 1).toLowerCase() + name.substring(1); StringBuilder optionName = new StringBuilder(); boolean lastWasLowerCase = false; for (int i = 0; i < name.length(); i++) { if (i == 0) { optionName.append( Character.toLowerCase( name.charAt( i ) ) ); lastWasLowerCase = false; } else if (Character.isUpperCase(name.charAt(i))) { if (lastWasLowerCase) { optionName.append( "_" ); } optionName.append( Character.toLowerCase( name.charAt( i ) ) ); lastWasLowerCase = false; } else { char c = name.charAt( i ); optionName.append( c ); lastWasLowerCase = Character.isLetter( c ) && Character.isLowerCase( c ); } } return optionName.toString(); } private static boolean needsArg(IPropertyInfo propertyInfo) { boolean requiresArgument = true; if (isBooleanProp(propertyInfo)) { requiresArgument = false; } else if (propertyInfo.getFeatureType().isArray()) { requiresArgument = false; } else if (propertyInfo.hasAnnotation(TypeSystem.get(ArgOptional.class))) { requiresArgument = false; } return requiresArgument; } private static boolean isBooleanProp(IPropertyInfo propertyInfo) { return propertyInfo.getFeatureType().equals( JavaTypes.BOOLEAN() ) || propertyInfo.getFeatureType().equals( JavaTypes.pBOOLEAN() ); } public static void setExitEnabled(boolean exitEnabled) { _exitEnabled = exitEnabled; } }