http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/util/CliBuilder.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/CliBuilder.groovy b/src/main/groovy/groovy/util/CliBuilder.groovy new file mode 100644 index 0000000..bc7d44a --- /dev/null +++ b/src/main/groovy/groovy/util/CliBuilder.groovy @@ -0,0 +1,798 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.util + +import groovy.cli.CliBuilderException +import groovy.cli.Option +import groovy.cli.TypedOption +import groovy.cli.Unparsed +import groovy.transform.Undefined +import org.apache.commons.cli.CommandLine +import org.apache.commons.cli.CommandLineParser +import org.apache.commons.cli.DefaultParser +import org.apache.commons.cli.GnuParser +import org.apache.commons.cli.HelpFormatter +import org.apache.commons.cli.Option as CliOption +import org.apache.commons.cli.Options +import org.apache.commons.cli.ParseException +import org.codehaus.groovy.runtime.InvokerHelper +import org.codehaus.groovy.runtime.MetaClassHelper +import org.codehaus.groovy.runtime.StringGroovyMethods + +import java.lang.annotation.Annotation +import java.lang.reflect.Array +import java.lang.reflect.Field +import java.lang.reflect.Method + +/** + * Provides a builder to assist the processing of command line arguments. + * Two styles are supported: dynamic api style (declarative method calls provide a mini DSL for describing options) + * and annotation style (annotations on an interface or class describe options). + * <p> + * <b>Dynamic api style</b> + * <p> + * Typical usage (emulate partial arg processing of unix command: ls -alt *.groovy): + * <pre> + * def cli = new CliBuilder(usage:'ls') + * cli.a('display all files') + * cli.l('use a long listing format') + * cli.t('sort by modification time') + * def options = cli.parse(args) + * assert options // would be null (false) on failure + * assert options.arguments() == ['*.groovy'] + * assert options.a && options.l && options.t + * </pre> + * The usage message for this example (obtained using <code>cli.usage()</code>) is shown below: + * <pre> + * usage: ls + * -a display all files + * -l use a long listing format + * -t sort by modification time + * </pre> + * An underlying parser that supports what is called argument 'bursting' is used + * by default. Bursting would convert '-alt' into '-a -l -t' provided no long + * option exists with value 'alt' and provided that none of 'a', 'l' or 't' + * takes an argument (in fact the last one is allowed to take an argument). + * The bursting behavior can be turned off by using an + * alternate underlying parser. The simplest way to achieve this is by using + * the deprecated GnuParser from Commons CLI with the parser property on the CliBuilder, + * i.e. include <code>parser: new GnuParser()</code> in the constructor call. + * <p> + * Another example (partial emulation of arg processing for 'ant' command line): + * <pre> + * def cli = new CliBuilder(usage:'ant [options] [targets]', + * header:'Options:') + * cli.help('print this message') + * cli.logfile(args:1, argName:'file', 'use given file for log') + * cli.D(args:2, valueSeparator:'=', argName:'property=value', + * 'use value for given property') + * def options = cli.parse(args) + * ... + * </pre> + * Usage message would be: + * <pre> + * usage: ant [options] [targets] + * Options: + * -D <property=value> use value for given property + * -help print this message + * -logfile <file> use given file for log + * </pre> + * And if called with the following arguments '-logfile foo -Dbar=baz target' + * then the following assertions would be true: + * <pre> + * assert options // would be null (false) on failure + * assert options.arguments() == ['target'] + * assert options.Ds == ['bar', 'baz'] + * assert options.logfile == 'foo' + * </pre> + * Note the use of some special notation. By adding 's' onto an option + * that may appear multiple times and has an argument or as in this case + * uses a valueSeparator to separate multiple argument values + * causes the list of associated argument values to be returned. + * <p> + * Another example showing long options (partial emulation of arg processing for 'curl' command line): + * <pre> + * def cli = new CliBuilder(usage:'curl [options] <url>') + * cli._(longOpt:'basic', 'Use HTTP Basic Authentication') + * cli.d(longOpt:'data', args:1, argName:'data', 'HTTP POST data') + * cli.G(longOpt:'get', 'Send the -d data with a HTTP GET') + * cli.q('If used as the first parameter disables .curlrc') + * cli._(longOpt:'url', args:1, argName:'URL', 'Set URL to work with') + * </pre> + * Which has the following usage message: + * <pre> + * usage: curl [options] <url> + * --basic Use HTTP Basic Authentication + * -d,--data <data> HTTP POST data + * -G,--get Send the -d data with a HTTP GET + * -q If used as the first parameter disables .curlrc + * --url <URL> Set URL to work with + * </pre> + * This example shows a common convention. When mixing short and long names, the + * short names are often one character in size. One character options with + * arguments don't require a space between the option and the argument, e.g. + * <code>-Ddebug=true</code>. The example also shows + * the use of '_' when no short option is applicable. + * <p> + * Also note that '_' was used multiple times. This is supported but if + * any other shortOpt or any longOpt is repeated, then the behavior is undefined. + * <p> + * Short option names may not contain a hyphen. If a long option name contains a hyphen, e.g. '--max-wait' then you can either + * use the long hand method call <code>options.hasOption('max-wait')</code> or surround + * the option name in quotes, e.g. <code>options.'max-wait'</code>. + * <p> + * Although CliBuilder on the whole hides away the underlying library used + * for processing the arguments, it does provide some hooks which let you + * make use of the underlying library directly should the need arise. For + * example, the last two lines of the 'curl' example above could be replaced + * with the following: + * <pre> + * import org.apache.commons.cli.* + * ... as before ... + * cli << new Option('q', false, 'If used as the first parameter disables .curlrc') + * cli << Option.builder().longOpt('url').hasArg().argName('URL'). + * desc('Set URL to work with').build() + * ... + * </pre> + * + * CliBuilder also supports Argument File processing. If an argument starts with + * an '@' character followed by a filename, then the contents of the file with name + * filename are placed into the command line. The feature can be turned off by + * setting expandArgumentFiles to false. If turned on, you can still pass a real + * parameter with an initial '@' character by escaping it with an additional '@' + * symbol, e.g. '@@foo' will become '@foo' and not be subject to expansion. As an + * example, if the file temp.args contains the content: + * <pre> + * -arg1 + * paramA + * paramB paramC + * </pre> + * Then calling the command line with: + * <pre> + * someCommand @temp.args -arg2 paramD + * </pre> + * Is the same as calling this: + * <pre> + * someCommand -arg1 paramA paramB paramC -arg2 paramD + * </pre> + * This feature is particularly useful on operating systems which place limitations + * on the size of the command line (e.g. Windows). The feature is similar to + * the 'Command Line Argument File' processing supported by javadoc and javac. + * Consult the corresponding documentation for those tools if you wish to see further examples. + * <p> + * <b>Supported Option Properties</b>: + * <pre> + * argName: String + * longOpt: String + * args: int or String + * optionalArg: boolean + * required: boolean + * type: Class + * valueSeparator: char + * convert: Closure + * defaultValue: String + * </pre> + * See {@link org.apache.commons.cli.Option} for the meaning of most of these properties + * and {@link CliBuilderTest} for further examples. + * <p> + * <b>Annotation style with an interface</b> + * <p> + * With this style an interface is defined containing an annotated method for each option. + * It might look like this (following roughly the earlier 'ls' example): + * <pre> + * import groovy.cli.Option + * import groovy.cli.Unparsed + * + * interface OptionInterface { + * @{@link groovy.cli.Option}(shortName='a', description='display all files') boolean all() + * @{@link groovy.cli.Option}(shortName='l', description='use a long listing format') boolean longFormat() + * @{@link groovy.cli.Option}(shortName='t', description='sort by modification time') boolean time() + * @{@link groovy.cli.Unparsed} List remaining() + * } + * </pre> + * Then this description is supplied to CliBuilder during parsing, e.g.: + * <pre> + * def args = '-alt *.groovy'.split() // normally from commandline itself + * def cli = new CliBuilder(usage:'ls') + * def options = cli.parseFromSpec(OptionInterface, args) + * assert options.remaining() == ['*.groovy'] + * assert options.all() && options.longFormat() && options.time() + * </pre> + * <p> + * <b>Annotation style with a class</b> + * <p> + * With this style a user-supplied instance is used. Annotations on that instance's class + * members (properties and setter methods) indicate how to set options and provide the option details + * using annotation attributes. + * It might look like this (again using the earlier 'ls' example): + * <pre> + * import groovy.cli.Option + * import groovy.cli.Unparsed + * + * class OptionClass { + * @{@link groovy.cli.Option}(shortName='a', description='display all files') boolean all + * @{@link groovy.cli.Option}(shortName='l', description='use a long listing format') boolean longFormat + * @{@link groovy.cli.Option}(shortName='t', description='sort by modification time') boolean time + * @{@link groovy.cli.Unparsed} List remaining + * } + * </pre> + * Then this description is supplied to CliBuilder during parsing, e.g.: + * <pre> + * def args = '-alt *.groovy'.split() // normally from commandline itself + * def cli = new CliBuilder(usage:'ls') + * def options = new OptionClass() + * cli.parseFromInstance(options, args) + * assert options.remaining == ['*.groovy'] + * assert options.all && options.longFormat && options.time + * </pre> + */ +class CliBuilder { + + /** + * Usage summary displayed as the first line when <code>cli.usage()</code> is called. + */ + String usage = 'groovy' + + /** + * Normally set internally but allows you full customisation of the underlying processing engine. + */ + CommandLineParser parser = null + + /** + * To change from the default PosixParser to the GnuParser, set this to false. Ignored if the parser is explicitly set. + * @deprecated use the parser option instead with an instance of your preferred parser + */ + @Deprecated + Boolean posix = null + + /** + * Whether arguments of the form '{@code @}<i>filename</i>' will be expanded into the arguments contained within the file named <i>filename</i> (default true). + */ + boolean expandArgumentFiles = true + + /** + * Normally set internally but can be overridden if you want to customise how the usage message is displayed. + */ + HelpFormatter formatter = new HelpFormatter() + + /** + * Defaults to stdout but you can provide your own PrintWriter if desired. + */ + PrintWriter writer = new PrintWriter(System.out) + + /** + * Optional additional message for usage; displayed after the usage summary but before the options are displayed. + */ + String header = '' + + /** + * Optional additional message for usage; displayed after the options are displayed. + */ + String footer = '' + + /** + * Indicates that option processing should continue for all arguments even + * if arguments not recognized as options are encountered (default true). + */ + boolean stopAtNonOption = true + + /** + * Allows customisation of the usage message width. + */ + int width = HelpFormatter.DEFAULT_WIDTH + + /** + * Not normally accessed directly but full access to underlying options if needed. + */ + Options options = new Options() + + Map<String, TypedOption> savedTypeOptions = new HashMap<String, TypedOption>() + + public <T> TypedOption<T> option(Map args, Class<T> type, String description) { + def name = args.opt ?: '_' + args.type = type + args.remove('opt') + "$name"(args, description) + } + + /** + * Internal method: Detect option specification method calls. + */ + def invokeMethod(String name, Object args) { + if (args instanceof Object[]) { + if (args.size() == 1 && (args[0] instanceof String || args[0] instanceof GString)) { + def option = option(name, [:], args[0]) + options.addOption(option) + + return create(option, null, null, null) + } + if (args.size() == 1 && args[0] instanceof CliOption && name == 'leftShift') { + CliOption option = args[0] + options.addOption(option) + return create(option, null, null, null) + } + if (args.size() == 2 && args[0] instanceof Map) { + def convert = args[0].remove('convert') + def type = args[0].remove('type') + def defaultValue = args[0].remove('defaultValue') + if (type && !(type instanceof Class)) { + throw new CliBuilderException("'type' must be a Class") + } + if ((convert || type) && !args[0].containsKey('args') && + type?.simpleName?.toLowerCase() != 'boolean') { + args[0].args = 1 + } + def option = option(name, args[0], args[1]) + options.addOption(option) + return create(option, type, defaultValue, convert) + } + } + return InvokerHelper.getMetaClass(this).invokeMethod(this, name, args) + } + + /** + * Make options accessible from command line args with parser. + * Returns null on bad command lines after displaying usage message. + */ + OptionAccessor parse(args) { + if (expandArgumentFiles) args = expandArgumentFiles(args) + if (!parser) { + parser = posix != null && posix == false ? new GnuParser() : new DefaultParser() + } + try { + def accessor = new OptionAccessor( + parser.parse(options, args as String[], stopAtNonOption)) + accessor.savedTypeOptions = savedTypeOptions + return accessor + } catch (ParseException pe) { + writer.println("error: " + pe.message) + usage() + return null + } + } + + /** + * Print the usage message with writer (default: System.out) and formatter (default: HelpFormatter) + */ + void usage() { + formatter.printHelp(writer, width, usage, header, options, HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, footer) + writer.flush() + } + + /** + * Given an interface containing members with annotations, derive + * the options specification. + * + * @param optionsClass + * @param args + * @return an instance containing the processed options + */ + public <T> T parseFromSpec(Class<T> optionsClass, String[] args) { + addOptionsFromAnnotations(optionsClass, false) + def cli = parse(args) + def cliOptions = [:] + setOptionsFromAnnotations(cli, optionsClass, cliOptions, false) + cliOptions as T + } + + /** + * Given an instance containing members with annotations, derive + * the options specification. + * + * @param optionInstance + * @param args + * @return the options instance populated with the processed options + */ + public <T> T parseFromInstance(T optionInstance, args) { + addOptionsFromAnnotations(optionInstance.getClass(), true) + def cli = parse(args) + setOptionsFromAnnotations(cli, optionInstance.getClass(), optionInstance, true) + optionInstance + } + + void addOptionsFromAnnotations(Class optionClass, boolean namesAreSetters) { + optionClass.methods.findAll{ it.getAnnotation(Option) }.each { Method m -> + Annotation annotation = m.getAnnotation(Option) + def typedOption = processAddAnnotation(annotation, m, namesAreSetters) + options.addOption(typedOption.cliOption) + } + + def optionFields = optionClass.declaredFields.findAll { it.getAnnotation(Option) } + if (optionClass.isInterface() && !optionFields.isEmpty()) { + throw new CliBuilderException("@Option only allowed on methods in interface " + optionClass.simpleName) + } + optionFields.each { Field f -> + Annotation annotation = f.getAnnotation(Option) + String setterName = "set" + MetaClassHelper.capitalize(f.getName()); + Method m = optionClass.getMethod(setterName, f.getType()) + def typedOption = processAddAnnotation(annotation, m, true) + options.addOption(typedOption.cliOption) + } + } + + private TypedOption processAddAnnotation(Option annotation, Method m, boolean namesAreSetters) { + String shortName = annotation.shortName() + String description = annotation.description() + String defaultValue = annotation.defaultValue() + char valueSeparator = 0 + if (annotation.valueSeparator()) valueSeparator = annotation.valueSeparator() as char + boolean optionalArg = annotation.optionalArg() + Integer numberOfArguments = annotation.numberOfArguments() + String numberOfArgumentsString = annotation.numberOfArgumentsString() + Class convert = annotation.convert() + if (convert == Undefined.CLASS) { + convert = null + } + Map names = calculateNames(annotation.longName(), shortName, m, namesAreSetters) + def builder = names.short ? CliOption.builder(names.short) : CliOption.builder() + if (names.long) { + builder.longOpt(names.long) + } + if (numberOfArguments != 1) { + if (numberOfArgumentsString) { + throw new CliBuilderException("You can't specify both 'numberOfArguments' and 'numberOfArgumentsString'") + } + } + def details = [:] + Class type = namesAreSetters ? (m.parameterTypes.size() > 0 ? m.parameterTypes[0] : null) : m.returnType + if (optionalArg && (!type || !type.isArray())) { + throw new CliBuilderException("Attempted to set optional argument for non array type") + } + def isFlag = type.simpleName.toLowerCase() == 'boolean' + if (numberOfArgumentsString) { + details.args = numberOfArgumentsString + details = adjustDetails(details) + if (details.optionalArg) optionalArg = true + } else { + details.args = isFlag ? 0 : numberOfArguments + } + if (details?.args == 0 && !(isFlag || type.name == 'java.lang.Object')) { + throw new CliBuilderException("Flag '${names.long ?: names.short}' must be Boolean or Object") + } + if (description) builder.desc(description) + if (valueSeparator) builder.valueSeparator(valueSeparator) + if (type) { + if (isFlag && details.args == 1) { + // special flag: treat like normal not boolean expecting explicit 'true' or 'false' param + isFlag = false + } + if (!isFlag) { + builder.hasArg(true) + if (details.containsKey('args')) builder.numberOfArgs(details.args) + } + if (type.isArray()) { + builder.optionalArg(optionalArg) + } + } + def typedOption = create(builder.build(), convert ? null : type, defaultValue, convert) + typedOption + } + + private TypedOption create(CliOption o, Class theType, defaultValue, convert) { + Map<String, Object> result = new TypedOption<Object>() + o.with { + if (opt != null) result.put("opt", opt) + result.put("longOpt", longOpt) + result.put("cliOption", o) + if (defaultValue) { + result.put("defaultValue", defaultValue) + } + if (convert) { + if (theType) { + throw new CliBuilderException("You can't specify 'type' when using 'convert'") + } + result.put("convert", convert) + result.put("type", convert instanceof Class ? convert : convert.getClass()) + } else { + result.put("type", theType) + } + } + savedTypeOptions[o.longOpt ?: o.opt] = result + result + } + + def setOptionsFromAnnotations(def cli, Class optionClass, Object t, boolean namesAreSetters) { + optionClass.methods.findAll{ it.getAnnotation(Option) }.each { Method m -> + Annotation annotation = m.getAnnotation(Option) + Map names = calculateNames(annotation.longName(), annotation.shortName(), m, namesAreSetters) + processSetAnnotation(m, t, names.long ?: names.short, cli, namesAreSetters) + } + optionClass.declaredFields.findAll { it.getAnnotation(Option) }.each { Field f -> + Annotation annotation = f.getAnnotation(Option) + String setterName = "set" + MetaClassHelper.capitalize(f.getName()); + Method m = optionClass.getMethod(setterName, f.getType()) + Map names = calculateNames(annotation.longName(), annotation.shortName(), m, true) + processSetAnnotation(m, t, names.long ?: names.short, cli, true) + } + def remaining = cli.arguments() + optionClass.methods.findAll{ it.getAnnotation(Unparsed) }.each { Method m -> + processSetRemaining(m, remaining, t, cli, namesAreSetters) + } + optionClass.declaredFields.findAll{ it.getAnnotation(Unparsed) }.each { Field f -> + String setterName = "set" + MetaClassHelper.capitalize(f.getName()); + Method m = optionClass.getMethod(setterName, f.getType()) + processSetRemaining(m, remaining, t, cli, namesAreSetters) + } + } + + private void processSetRemaining(Method m, remaining, Object t, cli, boolean namesAreSetters) { + def resultType = namesAreSetters ? m.parameterTypes[0] : m.returnType + def isTyped = resultType?.isArray() + def result + def type = null + if (isTyped) { + type = resultType.componentType + result = remaining.collect{ cli.getValue(type, it, null) } + } else { + result = remaining.toList() + } + if (namesAreSetters) { + m.invoke(t, isTyped ? [result.toArray(Array.newInstance(type, result.size()))] as Object[] : result) + } else { + Map names = calculateNames("", "", m, namesAreSetters) + t.put(names.long, { -> result }) + } + } + + private void processSetAnnotation(Method m, Object t, String name, cli, boolean namesAreSetters) { + def conv = savedTypeOptions[name]?.convert + if (conv && conv instanceof Class) { + savedTypeOptions[name].convert = conv.newInstance(t, t) + } + boolean hasArg = savedTypeOptions[name]?.cliOption?.numberOfArgs == 1 + boolean noArg = savedTypeOptions[name]?.cliOption?.numberOfArgs == 0 + if (namesAreSetters) { + def isBoolArg = m.parameterTypes.size() > 0 && m.parameterTypes[0].simpleName.toLowerCase() == 'boolean' + boolean isFlag = (isBoolArg && !hasArg) || noArg + if (cli.hasOption(name) || isFlag || cli.defaultValue(name)) { + m.invoke(t, [isFlag ? cli.hasOption(name) : + cli.hasOption(name) ? optionValue(cli, name) : cli.defaultValue(name)] as Object[]) + } + } else { + def isBoolRetType = m.returnType.simpleName.toLowerCase() == 'boolean' + boolean isFlag = (isBoolRetType && !hasArg) || noArg + t.put(m.getName(), cli.hasOption(name) ? + { -> isFlag ? true : optionValue(cli, name) } : + { -> isFlag ? false : cli.defaultValue(name) }) + } + } + + private optionValue(cli, String name) { + if (savedTypeOptions.containsKey(name)) { + return cli.getOptionValue(savedTypeOptions[name]) + } + cli[name] + } + + private Map calculateNames(String longName, String shortName, Method m, boolean namesAreSetters) { + boolean useShort = longName == '_' + if (longName == '_') longName = "" + def result = longName + if (!longName) { + result = m.getName() + if (namesAreSetters && result.startsWith("set")) { + result = MetaClassHelper.convertPropertyName(result.substring(3)) + } + } + [long: useShort ? "" : result, short: (useShort && !shortName) ? result : shortName] + } + + // implementation details ------------------------------------- + + /** + * Internal method: How to create an option from the specification. + */ + CliOption option(shortname, Map details, info) { + CliOption option + if (shortname == '_') { + option = CliOption.builder().desc(info).longOpt(details.longOpt).build() + details.remove('longOpt') + } else { + option = new CliOption(shortname, info) + } + adjustDetails(details).each { key, value -> + option[key] = value + } + return option + } + + static Map adjustDetails(Map m) { + m.collectMany { k, v -> + if (k == 'args' && v == '+') { + [[args: org.apache.commons.cli.Option.UNLIMITED_VALUES]] + } else if (k == 'args' && v == '*') { + [[args: org.apache.commons.cli.Option.UNLIMITED_VALUES, + optionalArg: true]] + } else if (k == 'args' && v instanceof String) { + [[args: Integer.parseInt(v)]] + } else { + [[(k): v]] + } + }.sum() + } + + static expandArgumentFiles(args) throws IOException { + def result = [] + for (arg in args) { + if (arg && arg != '@' && arg[0] == '@') { + arg = arg.substring(1) + if (arg[0] != '@') { + expandArgumentFile(arg, result) + continue + } + } + result << arg + } + return result + } + + private static expandArgumentFile(name, args) throws IOException { + def charAsInt = { String s -> s.toCharacter() as int } + new File(name).withReader { r -> + new StreamTokenizer(r).with { + resetSyntax() + wordChars(charAsInt(' '), 255) + whitespaceChars(0, charAsInt(' ')) + commentChar(charAsInt('#')) + quoteChar(charAsInt('"')) + quoteChar(charAsInt('\'')) + while (nextToken() != StreamTokenizer.TT_EOF) { + args << sval + } + } + } + } + +} + +class OptionAccessor { + CommandLine commandLine + Map<String, TypedOption> savedTypeOptions + + OptionAccessor(CommandLine commandLine) { + this.commandLine = commandLine + } + + boolean hasOption(TypedOption typedOption) { + commandLine.hasOption(typedOption.longOpt ?: typedOption.opt) + } + + public <T> T defaultValue(String name) { + Class<T> type = savedTypeOptions[name]?.type + String value = savedTypeOptions[name]?.defaultValue() ? savedTypeOptions[name].defaultValue() : null + return (T) value ? getTypedValue(type, name, value) : null + } + + public <T> T getOptionValue(TypedOption<T> typedOption) { + getOptionValue(typedOption, null) + } + + public <T> T getOptionValue(TypedOption<T> typedOption, T defaultValue) { + String optionName = (String) typedOption.longOpt ?: typedOption.opt + if (commandLine.hasOption(optionName)) { + if (typedOption.containsKey('type') && typedOption.type.isArray()) { + def compType = typedOption.type.componentType + return (T) getTypedValuesFromName(optionName, compType) + } + return getTypedValueFromName(optionName) + } + return defaultValue + } + + private <T> T[] getTypedValuesFromName(String optionName, Class<T> compType) { + CliOption option = commandLine.options.find{ it.longOpt == optionName } + T[] result = null + if (option) { + int count = 0 + def optionValues = commandLine.getOptionValues(optionName) + for (String optionValue : optionValues) { + if (result == null) { + result = (T[]) Array.newInstance(compType, optionValues.length) + } + result[count++] = (T) getTypedValue(compType, optionName, optionValue) + } + } + if (result == null) { + result = (T[]) Array.newInstance(compType, 0) + } + return result + } + + public <T> T getAt(TypedOption<T> typedOption) { + getAt(typedOption, null) + } + + public <T> T getAt(TypedOption<T> typedOption, T defaultValue) { + String optionName = (String) typedOption.longOpt ?: typedOption.opt + if (savedTypeOptions.containsKey(optionName)) { + return getTypedValueFromName(optionName) + } + return defaultValue + } + + private <T> T getTypedValueFromName(String optionName) { + Class type = savedTypeOptions[optionName].type + String optionValue = commandLine.getOptionValue(optionName) + return (T) getTypedValue(type, optionName, optionValue) + } + + private <T> T getTypedValue(Class<T> type, String optionName, String optionValue) { + if (savedTypeOptions[optionName]?.cliOption?.numberOfArgs == 0) { + return (T) commandLine.hasOption(optionName) + } + def convert = savedTypeOptions[optionName]?.convert + return getValue(type, optionValue, convert) + } + + private <T> T getValue(Class<T> type, String optionValue, Closure convert) { + if (!type) { + return (T) optionValue + } + if (Closure.isAssignableFrom(type) && convert) { + return (T) convert(optionValue) + } + if (type?.simpleName?.toLowerCase() == 'boolean') { + return (T) Boolean.parseBoolean(optionValue) + } + StringGroovyMethods.asType(optionValue, (Class<T>) type) + } + + def invokeMethod(String name, Object args) { + return InvokerHelper.getMetaClass(commandLine).invokeMethod(commandLine, name, args) + } + + def getProperty(String name) { + if (!savedTypeOptions.containsKey(name)) { + def alt = savedTypeOptions.find{ it.value.opt == name } + if (alt) name = alt.key + } + def methodname = 'getOptionValue' + Class type = savedTypeOptions[name]?.type + def foundArray = type?.isArray() + if (name.size() > 1 && name.endsWith('s')) { + def singularName = name[0..-2] + if (commandLine.hasOption(singularName) || foundArray) { + name = singularName + methodname += 's' + type = savedTypeOptions[name]?.type + } + } + if (type?.isArray()) { + methodname = 'getOptionValues' + } + if (name.size() == 1) name = name as char + def result = InvokerHelper.getMetaClass(commandLine).invokeMethod(commandLine, methodname, name) + if (result != null) { + if (result instanceof String[]) { + result = result.collect{ type ? getTypedValue(type.isArray() ? type.componentType : type, name, it) : it } + } else { + if (type) result = getTypedValue(type, name, result) + } + } else if (type?.simpleName != 'boolean' && savedTypeOptions[name]?.defaultValue) { + result = getTypedValue(type, name, savedTypeOptions[name].defaultValue) + } else { + result = commandLine.hasOption(name) + } + return result + } + + List<String> arguments() { + commandLine.args.toList() + } +}
http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/util/ClosureComparator.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/ClosureComparator.java b/src/main/groovy/groovy/util/ClosureComparator.java new file mode 100644 index 0000000..dc70ea6 --- /dev/null +++ b/src/main/groovy/groovy/util/ClosureComparator.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.util; + +import groovy.lang.Closure; +import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * A Comparator which uses a closure to compare 2 values being equal + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public class ClosureComparator<T> implements Comparator<T>, Serializable { + + private static final long serialVersionUID = -4593521535656429522L; + Closure closure; + + public ClosureComparator(Closure closure) { + this.closure = closure; + } + + public int compare(T object1, T object2) { + Object value = closure.call(object1, object2); + return DefaultTypeTransformation.intUnbox(value); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/util/ConfigObject.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/ConfigObject.java b/src/main/groovy/groovy/util/ConfigObject.java new file mode 100644 index 0000000..c76bc97 --- /dev/null +++ b/src/main/groovy/groovy/util/ConfigObject.java @@ -0,0 +1,425 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.util; + +import groovy.lang.GroovyObjectSupport; +import groovy.lang.GroovyRuntimeException; +import groovy.lang.Writable; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.runtime.InvokerHelper; +import org.codehaus.groovy.runtime.StringGroovyMethods; +import org.codehaus.groovy.syntax.Types; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * A ConfigObject at a simple level is a Map that creates configuration entries (other ConfigObjects) when referencing them. + * This means that navigating to foo.bar.stuff will not return null but nested ConfigObjects which are of course empty maps + * The Groovy truth can be used to check for the existence of "real" entries. + * + * @author Graeme Rocher + * @author Guillaume Laforge (rewrite in Java related to security constraints on Google App Engine) + * @since 1.5 + */ +public class ConfigObject extends GroovyObjectSupport implements Writable, Map, Cloneable { + + static final Collection<String> KEYWORDS = Types.getKeywords(); + + static final String TAB_CHARACTER = "\t"; + + /** + * The config file that was used when parsing this ConfigObject + */ + private URL configFile; + + private HashMap delegateMap = new LinkedHashMap(); + + public ConfigObject(URL file) { + this.configFile = file; + } + + public ConfigObject() { + this(null); + } + + public URL getConfigFile() { + return configFile; + } + + public void setConfigFile(URL configFile) { + this.configFile = configFile; + } + + /** + * Writes this config object into a String serialized representation which can later be parsed back using the parse() + * method + * + * @see groovy.lang.Writable#writeTo(java.io.Writer) + */ + public Writer writeTo(Writer outArg) throws IOException { + BufferedWriter out = new BufferedWriter(outArg); + try { + writeConfig("", this, out, 0, false); + } finally { + out.flush(); + } + + return outArg; + } + + + /** + * Overrides the default getProperty implementation to create nested ConfigObject instances on demand + * for non-existent keys + */ + public Object getProperty(String name) { + if ("configFile".equals(name)) + return this.configFile; + + if (!containsKey(name)) { + ConfigObject prop = new ConfigObject(this.configFile); + put(name, prop); + + return prop; + } + + return get(name); + } + + /** + * A ConfigObject is a tree structure consisting of nested maps. This flattens the maps into + * a single level structure like a properties file + */ + public Map flatten() { + return flatten(null); + } + + /** + * Flattens this ConfigObject populating the results into the target Map + * + * @see ConfigObject#flatten() + */ + public Map flatten(Map target) { + if (target == null) + target = new ConfigObject(); + populate("", target, this); + + return target; + } + + /** + * Merges the given map with this ConfigObject overriding any matching configuration entries in this ConfigObject + * + * @param other The ConfigObject to merge with + * @return The result of the merge + */ + public Map merge(ConfigObject other) { + return doMerge(this, other); + } + + + /** + * Converts this ConfigObject into a the java.util.Properties format, flattening the tree structure beforehand + * + * @return A java.util.Properties instance + */ + public Properties toProperties() { + Properties props = new Properties(); + flatten(props); + + props = convertValuesToString(props); + + return props; + } + + /** + * Converts this ConfigObject ino the java.util.Properties format, flatten the tree and prefixing all entries with the given prefix + * + * @param prefix The prefix to append before property entries + * @return A java.util.Properties instance + */ + public Properties toProperties(String prefix) { + Properties props = new Properties(); + populate(prefix + ".", props, this); + + props = convertValuesToString(props); + + return props; + } + + private Map doMerge(Map config, Map other) { + for (Object o : other.entrySet()) { + Map.Entry next = (Map.Entry) o; + Object key = next.getKey(); + Object value = next.getValue(); + + Object configEntry = config.get(key); + + if (configEntry == null) { + config.put(key, value); + + continue; + } else { + if (configEntry instanceof Map && !((Map) configEntry).isEmpty() && value instanceof Map) { + // recur + doMerge((Map) configEntry, (Map) value); + } else { + config.put(key, value); + } + } + } + + return config; + } + + private void writeConfig(String prefix, ConfigObject map, BufferedWriter out, int tab, boolean apply) throws IOException { + String space = apply ? StringGroovyMethods.multiply(TAB_CHARACTER, tab) : ""; + + for (Object o1 : map.keySet()) { + String key = (String) o1; + Object v = map.get(key); + + if (v instanceof ConfigObject) { + ConfigObject value = (ConfigObject) v; + + if (!value.isEmpty()) { + + Object dotsInKeys = null; + for (Object o : value.entrySet()) { + Entry e = (Entry) o; + String k = (String) e.getKey(); + if (k.indexOf('.') > -1) { + dotsInKeys = e; + break; + } + } + + int configSize = value.size(); + Object firstKey = value.keySet().iterator().next(); + Object firstValue = value.values().iterator().next(); + + int firstSize; + if (firstValue instanceof ConfigObject) { + firstSize = ((ConfigObject) firstValue).size(); + } else { + firstSize = 1; + } + + if (configSize == 1 || DefaultGroovyMethods.asBoolean(dotsInKeys)) { + if (firstSize == 1 && firstValue instanceof ConfigObject) { + key = KEYWORDS.contains(key) ? InvokerHelper.inspect(key) : key; + String writePrefix = prefix + key + "." + firstKey + "."; + writeConfig(writePrefix, (ConfigObject) firstValue, out, tab, true); + } else if (!DefaultGroovyMethods.asBoolean(dotsInKeys) && firstValue instanceof ConfigObject) { + writeNode(key, space, tab, value, out); + } else { + for (Object j : value.keySet()) { + Object v2 = value.get(j); + Object k2 = ((String) j).indexOf('.') > -1 ? InvokerHelper.inspect(j) : j; + if (v2 instanceof ConfigObject) { + key = KEYWORDS.contains(key) ? InvokerHelper.inspect(key) : key; + writeConfig(prefix + key, (ConfigObject) v2, out, tab, false); + } else { + writeValue(key + "." + k2, space, prefix, v2, out); + } + } + } + } else { + writeNode(key, space, tab, value, out); + } + } + } else { + writeValue(key, space, prefix, v, out); + } + } + } + + private static void writeValue(String key, String space, String prefix, Object value, BufferedWriter out) throws IOException { +// key = key.indexOf('.') > -1 ? InvokerHelper.inspect(key) : key; + boolean isKeyword = KEYWORDS.contains(key); + key = isKeyword ? InvokerHelper.inspect(key) : key; + + if (!StringGroovyMethods.asBoolean(prefix) && isKeyword) prefix = "this."; + out.append(space).append(prefix).append(key).append('=').append(InvokerHelper.inspect(value)); + out.newLine(); + } + + private void writeNode(String key, String space, int tab, ConfigObject value, BufferedWriter out) throws IOException { + key = KEYWORDS.contains(key) ? InvokerHelper.inspect(key) : key; + out.append(space).append(key).append(" {"); + out.newLine(); + writeConfig("", value, out, tab + 1, true); + out.append(space).append('}'); + out.newLine(); + } + + private static Properties convertValuesToString(Map props) { + Properties newProps = new Properties(); + + for (Object o : props.entrySet()) { + Map.Entry next = (Map.Entry) o; + Object key = next.getKey(); + Object value = next.getValue(); + + newProps.put(key, value != null ? value.toString() : null); + } + + return newProps; + } + + private void populate(String suffix, Map config, Map map) { + for (Object o : map.entrySet()) { + Map.Entry next = (Map.Entry) o; + Object key = next.getKey(); + Object value = next.getValue(); + + if (value instanceof Map) { + populate(suffix + key + ".", config, (Map) value); + } else { + try { + config.put(suffix + key, value); + } catch (NullPointerException e) { + // it is idiotic story but if config map doesn't allow null values (like Hashtable) + // we can't do too much + } + } + } + } + + public int size() { + return delegateMap.size(); + } + + public boolean isEmpty() { + return delegateMap.isEmpty(); + } + + public boolean containsKey(Object key) { + return delegateMap.containsKey(key); + } + + public boolean containsValue(Object value) { + return delegateMap.containsValue(value); + } + + public Object get(Object key) { + return delegateMap.get(key); + } + + public Object put(Object key, Object value) { + return delegateMap.put(key, value); + } + + public Object remove(Object key) { + return delegateMap.remove(key); + } + + public void putAll(Map m) { + delegateMap.putAll(m); + } + + public void clear() { + delegateMap.clear(); + } + + public Set keySet() { + return delegateMap.keySet(); + } + + public Collection values() { + return delegateMap.values(); + } + + public Set entrySet() { + return delegateMap.entrySet(); + } + + /** + * Returns a shallow copy of this ConfigObject, keys and configuration entries are not cloned. + * @return a shallow copy of this ConfigObject + */ + public ConfigObject clone() { + try { + ConfigObject clone = (ConfigObject) super.clone(); + clone.configFile = configFile; + clone.delegateMap = (LinkedHashMap) delegateMap.clone(); + return clone; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + + /** + * Checks if a config option is set. Example usage: + * <pre class="groovyTestCase"> + * def config = new ConfigSlurper().parse("foo { password='' }") + * assert config.foo.isSet('password') + * assert config.foo.isSet('username') == false + * </pre> + * + * The check works <b>only</v> for options <b>one</b> block below the current block. + * E.g. <code>config.isSet('foo.password')</code> will always return false. + * + * @param option The name of the option + * @return <code>true</code> if the option is set <code>false</code> otherwise + * @since 2.3.0 + */ + public Boolean isSet(String option) { + if (delegateMap.containsKey(option)) { + Object entry = delegateMap.get(option); + if (!(entry instanceof ConfigObject) || !((ConfigObject) entry).isEmpty()) { + return Boolean.TRUE; + } + } + return Boolean.FALSE; + } + + public String prettyPrint() { + StringWriter sw = new StringWriter(); + try { + writeTo(sw); + } catch (IOException e) { + throw new GroovyRuntimeException(e); + } + + return sw.toString(); + } + + @Override + public String toString() { + StringWriter sw = new StringWriter(); + try { + InvokerHelper.write(sw, this); + } catch (IOException e) { + throw new GroovyRuntimeException(e); + } + + return sw.toString(); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/util/ConfigSlurper.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/ConfigSlurper.groovy b/src/main/groovy/groovy/util/ConfigSlurper.groovy new file mode 100644 index 0000000..20d0723 --- /dev/null +++ b/src/main/groovy/groovy/util/ConfigSlurper.groovy @@ -0,0 +1,309 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.util + +import org.codehaus.groovy.runtime.InvokerHelper + +/** + * ConfigSlurper is a utility class for reading configuration files defined in the form of Groovy + * scripts. Configuration settings can be defined using dot notation or scoped using closures: + * + * <pre><code> + * grails.webflow.stateless = true + * smtp { + * mail.host = 'smtp.myisp.com' + * mail.auth.user = 'server' + * } + * resources.URL = "http://localhost:80/resources" + * </code></pre> + * + * Settings can either be bound into nested maps or onto a specified JavaBean instance. + * In the latter case, an error will be thrown if a property cannot be bound. + * + * @author Graeme Rocher + * @author Andres Almiray + * @since 1.5 + */ +class ConfigSlurper { + private static final ENVIRONMENTS_METHOD = 'environments' + GroovyClassLoader classLoader = new GroovyClassLoader() + private Map bindingVars = [:] + + private final Map<String, String> conditionValues = [:] + private final Stack<Map<String, ConfigObject>> conditionalBlocks = new Stack<Map<String,ConfigObject>>() + + ConfigSlurper() { + this('') + } + + /** + * Constructs a new ConfigSlurper instance using the given environment + * + * @param env The Environment to use + */ + ConfigSlurper(String env) { + conditionValues[ENVIRONMENTS_METHOD] = env + } + + void registerConditionalBlock(String blockName, String blockValue) { + if (blockName) { + if (!blockValue) { + conditionValues.remove(blockName) + } else { + conditionValues[blockName] = blockValue + } + } + } + + Map<String, String> getConditionalBlockValues() { + Collections.unmodifiableMap(conditionValues) + } + + String getEnvironment() { + return conditionValues[ENVIRONMENTS_METHOD] + } + + void setEnvironment(String environment) { + conditionValues[ENVIRONMENTS_METHOD] = environment + } + + /** + * Sets any additional variables that should be placed into the binding when evaluating Config scripts + */ + void setBinding(Map vars) { + this.bindingVars = vars + } + + /** + * Parses a ConfigObject instances from an instance of java.util.Properties + * + * @param The java.util.Properties instance + */ + ConfigObject parse(Properties properties) { + ConfigObject config = new ConfigObject() + for (key in properties.keySet()) { + def tokens = key.split(/\./) + + def current = config + def last + def lastToken + def foundBase = false + for (token in tokens) { + if (foundBase) { + // handle not properly nested tokens by ignoring + // hierarchy below this point + lastToken += "." + token + current = last + } else { + last = current + lastToken = token + current = current."${token}" + if (!(current instanceof ConfigObject)) foundBase = true + } + } + + if (current instanceof ConfigObject) { + if (last[lastToken]) { + def flattened = last.flatten() + last.clear() + flattened.each { k2, v2 -> last[k2] = v2 } + last[lastToken] = properties.get(key) + } + else { + last[lastToken] = properties.get(key) + } + } + current = config + } + return config + } + /** + * Parse the given script as a string and return the configuration object + * + * @see ConfigSlurper#parse(groovy.lang.Script) + */ + ConfigObject parse(String script) { + return parse(classLoader.parseClass(script)) + } + + /** + * Create a new instance of the given script class and parse a configuration object from it + * + * @see ConfigSlurper#parse(groovy.lang.Script) + */ + ConfigObject parse(Class scriptClass) { + return parse(scriptClass.newInstance()) + } + + /** + * Parse the given script into a configuration object (a Map) + * (This method creates a new class to parse the script each time it is called.) + * + * @param script The script to parse + * @return A Map of maps that can be navigating with dot de-referencing syntax to obtain configuration entries + */ + ConfigObject parse(Script script) { + return parse(script, null) + } + + /** + * Parses a Script represented by the given URL into a ConfigObject + * + * @param scriptLocation The location of the script to parse + * @return The ConfigObject instance + */ + ConfigObject parse(URL scriptLocation) { + return parse(classLoader.parseClass(scriptLocation.text).newInstance(), scriptLocation) + } + + /** + * Parses the passed groovy.lang.Script instance using the second argument to allow the ConfigObject + * to retain an reference to the original location other Groovy script + * + * @param script The groovy.lang.Script instance + * @param location The original location of the Script as a URL + * @return The ConfigObject instance + */ + ConfigObject parse(Script script, URL location) { + Stack<String> currentConditionalBlock = new Stack<String>() + def config = location ? new ConfigObject(location) : new ConfigObject() + GroovySystem.metaClassRegistry.removeMetaClass(script.class) + def mc = script.class.metaClass + def prefix = "" + LinkedList stack = new LinkedList() + stack << [config: config, scope: [:]] + def pushStack = { co -> + stack << [config: co, scope: stack.last.scope.clone()] + } + def assignName = { name, co -> + def current = stack.last + current.config[name] = co + current.scope[name] = co + } + mc.getProperty = { String name -> + def current = stack.last + def result + if (current.config.get(name)) { + result = current.config.get(name) + } else if (current.scope[name]) { + result = current.scope[name] + } else { + try { + result = InvokerHelper.getProperty(this, name) + } catch (GroovyRuntimeException e) { + result = new ConfigObject() + assignName.call(name, result) + } + } + result + } + + ConfigObject overrides = new ConfigObject() + mc.invokeMethod = { String name, args -> + def result + if (args.length == 1 && args[0] instanceof Closure) { + if (name in conditionValues.keySet()) { + try { + currentConditionalBlock.push(name) + conditionalBlocks.push([:]) + args[0].call() + } finally { + currentConditionalBlock.pop() + for (entry in conditionalBlocks.pop().entrySet()) { + def c = stack.last.config + (c != config? c : overrides).merge(entry.value) + } + } + } else if (currentConditionalBlock.size() > 0) { + String conditionalBlockKey = currentConditionalBlock.peek() + if (name == conditionValues[conditionalBlockKey]) { + def co = new ConfigObject() + conditionalBlocks.peek()[conditionalBlockKey] = co + + pushStack.call(co) + try { + currentConditionalBlock.pop() + args[0].call() + } finally { + currentConditionalBlock.push(conditionalBlockKey) + } + stack.removeLast() + } + } else { + def co + if (stack.last.config.get(name) instanceof ConfigObject) { + co = stack.last.config.get(name) + } else { + co = new ConfigObject() + } + + assignName.call(name, co) + pushStack.call(co) + args[0].call() + stack.removeLast() + } + } else if (args.length == 2 && args[1] instanceof Closure) { + try { + prefix = name + '.' + assignName.call(name, args[0]) + args[1].call() + } finally { prefix = "" } + } else { + MetaMethod mm = mc.getMetaMethod(name, args) + if (mm) { + result = mm.invoke(delegate, args) + } else { + throw new MissingMethodException(name, getClass(), args) + } + } + result + } + script.metaClass = mc + + def setProperty = { String name, value -> + assignName.call(prefix + name, value) + } + def binding = new ConfigBinding(setProperty) + if (this.bindingVars) { + binding.getVariables().putAll(this.bindingVars) + } + script.binding = binding + + script.run() + + config.merge(overrides) + + return config + } +} + +/** + * Since Groovy Script doesn't support overriding setProperty, we use a trick with the Binding to provide this + * functionality + */ +class ConfigBinding extends Binding { + def callable + ConfigBinding(Closure c) { + this.callable = c + } + + void setVariable(String name, Object value) { + callable(name, value) + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/util/DelegatingScript.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/DelegatingScript.java b/src/main/groovy/groovy/util/DelegatingScript.java new file mode 100644 index 0000000..959c8e8 --- /dev/null +++ b/src/main/groovy/groovy/util/DelegatingScript.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.util; + +import groovy.lang.Binding; +import groovy.lang.GroovyObject; +import groovy.lang.MetaClass; +import groovy.lang.MissingMethodException; +import groovy.lang.MissingPropertyException; +import groovy.lang.Script; +import org.codehaus.groovy.runtime.InvokerHelper; + +/** + * {@link Script} that performs method invocations and property access like {@link groovy.lang.Closure} does. + * + * <p> + * {@link DelegatingScript} is a convenient basis for loading a custom-defined DSL as a {@link Script}, then execute it. + * The following sample code illustrates how to do it: + * + * <pre> + * class MyDSL { + * public void foo(int x, int y, Closure z) { ... } + * public void setBar(String a) { ... } + * } + * + * CompilerConfiguration cc = new CompilerConfiguration(); + * cc.setScriptBaseClass(DelegatingScript.class.getName()); + * GroovyShell sh = new GroovyShell(cl,new Binding(),cc); + * DelegatingScript script = (DelegatingScript)sh.parse(new File("my.dsl")) + * script.setDelegate(new MyDSL()); + * script.run(); + * </pre> + * + * <p> + * <tt>my.dsl</tt> can look like this: + * + * <pre> + * foo(1,2) { + * .... + * } + * bar = ...; + * </pre> + * + * <p> + * {@link DelegatingScript} does this by delegating property access and method invocation to the <tt>delegate</tt> object. + * + * <p> + * More formally speaking, given the following script: + * + * <pre> + * a = 1; + * b(2); + * </pre> + * + * <p> + * Using {@link DelegatingScript} as the base class, the code will run as: + * + * <pre> + * delegate.a = 1; + * delegate.b(2); + * </pre> + * + * ... whereas in plain {@link Script}, this will be run as: + * + * <pre> + * binding.setProperty("a",1); + * ((Closure)binding.getProperty("b")).call(2); + * </pre> + * + * @author Kohsuke Kawaguchi + */ +public abstract class DelegatingScript extends Script { + private Object delegate; + private MetaClass metaClass; + + protected DelegatingScript() { + super(); + } + + protected DelegatingScript(Binding binding) { + super(binding); + } + + /** + * Sets the delegation target. + */ + public void setDelegate(Object delegate) { + this.delegate = delegate; + this.metaClass = InvokerHelper.getMetaClass(delegate.getClass()); + } + + @Override + public Object invokeMethod(String name, Object args) { + try { + if (delegate instanceof GroovyObject) { + return ((GroovyObject) delegate).invokeMethod(name, args); + } + return metaClass.invokeMethod(delegate, name, args); + } catch (MissingMethodException mme) { + return super.invokeMethod(name, args); + } + } + + @Override + public Object getProperty(String property) { + try { + return metaClass.getProperty(delegate,property); + } catch (MissingPropertyException e) { + return super.getProperty(property); + } + } + + @Override + public void setProperty(String property, Object newValue) { + try { + metaClass.setProperty(delegate,property,newValue); + } catch (MissingPropertyException e) { + super.setProperty(property,newValue); + } + } + + public Object getDelegate() { + return delegate; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/util/Eval.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/Eval.java b/src/main/groovy/groovy/util/Eval.java new file mode 100644 index 0000000..87f9295 --- /dev/null +++ b/src/main/groovy/groovy/util/Eval.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.util; + +import groovy.lang.Binding; +import groovy.lang.GroovyShell; +import org.codehaus.groovy.control.CompilationFailedException; + +/** + * Allow easy integration from Groovy into Java through convenience methods. + * <p> + * This class is a simple helper on top of GroovyShell. You can use it to evaluate small + * Groovy scripts that don't need large Binding objects. For example, this script + * executes with no errors: + * <pre class="groovyTestCase"> + * assert Eval.me(' 2 * 4 + 2') == 10 + * assert Eval.x(2, ' x * 4 + 2') == 10 + * </pre> + * + * @see GroovyShell + * @author Dierk Koenig + */ + +public class Eval { + /** + * Evaluates the specified String expression and returns the result. For example: + * <pre class="groovyTestCase"> + * assert Eval.me(' 2 * 4 + 2') == 10 + * </pre> + * @param expression the Groovy expression to evaluate + * @return the result of the expression + * @throws CompilationFailedException if expression is not valid Groovy + */ + public static Object me(final String expression) throws CompilationFailedException { + return me(null, null, expression); + } + + /** + * Evaluates the specified String expression and makes the parameter available inside + * the script, returning the result. For example, this code binds the 'x' variable: + * <pre class="groovyTestCase"> + * assert Eval.me('x', 2, ' x * 4 + 2') == 10 + * </pre> + * @param expression the Groovy expression to evaluate + * @return the result of the expression + * @throws CompilationFailedException if expression is not valid Groovy + */ + public static Object me(final String symbol, final Object object, final String expression) throws CompilationFailedException { + Binding b = new Binding(); + b.setVariable(symbol, object); + GroovyShell sh = new GroovyShell(b); + return sh.evaluate(expression); + } + + /** + * Evaluates the specified String expression and makes the parameter available inside + * the script bound to a variable named 'x', returning the result. For example, this + * code executes without failure: + * <pre class="groovyTestCase"> + * assert Eval.x(2, ' x * 4 + 2') == 10 + * </pre> + * @param expression the Groovy expression to evaluate + * @return the result of the expression + * @throws CompilationFailedException if expression is not valid Groovy + */ + public static Object x(final Object x, final String expression) throws CompilationFailedException { + return me("x", x, expression); + } + + /** + * Evaluates the specified String expression and makes the first two parameters available inside + * the script bound to variables named 'x' and 'y' respectively, returning the result. For example, + * this code executes without failure: + * <pre class="groovyTestCase"> + * assert Eval.xy(2, 4, ' x * y + 2') == 10 + * </pre> + * @param expression the Groovy expression to evaluate + * @return the result of the expression + * @throws CompilationFailedException if expression is not valid Groovy + */ + public static Object xy(final Object x, final Object y, final String expression) throws CompilationFailedException { + Binding b = new Binding(); + b.setVariable("x", x); + b.setVariable("y", y); + GroovyShell sh = new GroovyShell(b); + return sh.evaluate(expression); + } + + /** + * Evaluates the specified String expression and makes the first three parameters available inside + * the script bound to variables named 'x', 'y', and 'z' respectively, returning the result. For + * example, this code executes without failure: + * <pre class="groovyTestCase"> + * assert Eval.xyz(2, 4, 2, ' x * y + z') == 10 + * </pre> + * @param expression the Groovy expression to evaluate + * @return the result of the expression + * @throws CompilationFailedException if expression is not valid Groovy + */ + public static Object xyz(final Object x, final Object y, final Object z, final String expression) throws CompilationFailedException { + Binding b = new Binding(); + b.setVariable("x", x); + b.setVariable("y", y); + b.setVariable("z", z); + GroovyShell sh = new GroovyShell(b); + return sh.evaluate(expression); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/util/Expando.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/Expando.java b/src/main/groovy/groovy/util/Expando.java new file mode 100644 index 0000000..f009551 --- /dev/null +++ b/src/main/groovy/groovy/util/Expando.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.util; + +import groovy.lang.Closure; +import groovy.lang.GroovyObjectSupport; +import groovy.lang.GroovyRuntimeException; +import groovy.lang.MetaExpandoProperty; +import groovy.lang.MissingPropertyException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + + +/** + * Represents a dynamically expandable bean. + * + * @author <a href="mailto:[email protected]">James Strachan</a> + * @author Hein Meling + * @author Pilho Kim + */ +public class Expando extends GroovyObjectSupport { + + private Map expandoProperties; + + public Expando() { + } + + public Expando(Map expandoProperties) { + this.expandoProperties = expandoProperties; + } + + /** + * @return the dynamically expanded properties + */ + public Map getProperties() { + if (expandoProperties == null) { + expandoProperties = createMap(); + } + return expandoProperties; + } + + public List getMetaPropertyValues() { + // run through all our current properties and create MetaProperty objects + List ret = new ArrayList(); + for (Object o : getProperties().entrySet()) { + Entry entry = (Entry) o; + ret.add(new MetaExpandoProperty(entry)); + } + + return ret; + } + + public Object getProperty(String property) { + // always use the expando properties first + Object result = getProperties().get(property); + if (result != null) return result; + try { + return super.getProperty(property); + } + catch (MissingPropertyException e) { + // IGNORE + } + return null; + } + + public void setProperty(String property, Object newValue) { + // always use the expando properties + getProperties().put(property, newValue); + } + + public Object invokeMethod(String name, Object args) { + try { + return super.invokeMethod(name, args); + } + catch (GroovyRuntimeException e) { + // br should get a "native" property match first. getProperty includes such fall-back logic + Object value = this.getProperty(name); + if (value instanceof Closure) { + Closure closure = (Closure) value; + closure = (Closure) closure.clone(); + closure.setDelegate(this); + return closure.call((Object[]) args); + } else { + throw e; + } + } + + } + + /** + * This allows toString to be overridden by a closure <i>field</i> method attached + * to the expando object. + * + * @see java.lang.Object#toString() + */ + public String toString() { + Object method = getProperties().get("toString"); + if (method != null && method instanceof Closure) { + // invoke overridden toString closure method + Closure closure = (Closure) method; + closure.setDelegate(this); + return closure.call().toString(); + } else { + return expandoProperties.toString(); + } + } + + /** + * This allows equals to be overridden by a closure <i>field</i> method attached + * to the expando object. + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + Object method = getProperties().get("equals"); + if (method != null && method instanceof Closure) { + // invoke overridden equals closure method + Closure closure = (Closure) method; + closure.setDelegate(this); + Boolean ret = (Boolean) closure.call(obj); + return ret.booleanValue(); + } else { + return super.equals(obj); + } + } + + /** + * This allows hashCode to be overridden by a closure <i>field</i> method attached + * to the expando object. + * + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + Object method = getProperties().get("hashCode"); + if (method != null && method instanceof Closure) { + // invoke overridden hashCode closure method + Closure closure = (Closure) method; + closure.setDelegate(this); + Integer ret = (Integer) closure.call(); + return ret.intValue(); + } else { + return super.hashCode(); + } + } + + /** + * Factory method to create a new Map used to store the expando properties map + * + * @return a newly created Map implementation + */ + protected Map createMap() { + return new HashMap(); + } + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/util/Factory.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/Factory.java b/src/main/groovy/groovy/util/Factory.java new file mode 100644 index 0000000..c8e8fc7 --- /dev/null +++ b/src/main/groovy/groovy/util/Factory.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.util; + +import groovy.lang.Closure; + +import java.util.Map; + +/** + * @author <a href="mailto:[email protected]">Andres Almiray</a> + * @author Danno Ferrin + */ +public interface Factory { + /** + * + * @return true if no child closures should be processed + */ + boolean isLeaf(); + + /** + * Does this factory "Own" it's child closure. + * + * @return true if the factory should have onContentClosure() called, + * false if the builder should handle it + */ + boolean isHandlesNodeChildren(); + + /** + * Called when a factory is registered to a builder + * @param builder the build the factory has been registered to + * @param registeredName the name the factory has been registered under + */ + void onFactoryRegistration(FactoryBuilderSupport builder, String registeredName, String registeredGroupName); + + /** + * @param builder the FactoryBuilder + * @param name the name of the node being built + * @param value the 'value' argument in the build node + * @param attributes the attributes of the build arg + * @return the object created for the builder + * @throws InstantiationException if attempting to instantiate an interface or abstract class + * @throws IllegalAccessException if the instance can't be created due to a security violation + */ + Object newInstance( FactoryBuilderSupport builder, Object name, Object value, Map attributes ) + throws InstantiationException, IllegalAccessException; + + /** + * @param builder the FactoryBuilder + * @param node the node (returned from newINstance) to consider the attributes for + * @param attributes the attributes, a mutable set + * @return true if the factory builder should use standard bean property matching for the remaining attributes + */ + boolean onHandleNodeAttributes( FactoryBuilderSupport builder, Object node, Map attributes ); + + /** + * Only called if it isLeaf is false and isHandlesNodeChildren is true + * @param builder the FactoryBuilder + * @param node the node (returned from newINstance) to consider the attributes for + * @param childContent the child content closure of the builder + * @return true if the factory builder should apply default node processing to the content child + */ + boolean onNodeChildren( FactoryBuilderSupport builder, Object node, Closure childContent); + + /** + * @param builder the FactoryBuilder + * @param parent the parent node (null if 'root') + * @param node the node just completed + */ + void onNodeCompleted( FactoryBuilderSupport builder, Object parent, Object node ); + + void setParent( FactoryBuilderSupport builder, Object parent, Object child ); + + void setChild( FactoryBuilderSupport builder, Object parent, Object child ); +}
