| /* |
| * |
| * 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 org.apache.royale.linter.config; |
| |
| import java.lang.reflect.Field; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.royale.compiler.exceptions.ConfigurationException; |
| import org.apache.royale.compiler.internal.config.IConfigurationFilter; |
| import org.apache.royale.compiler.internal.config.annotations.ArgumentNameGenerator; |
| import org.apache.royale.compiler.internal.config.annotations.Arguments; |
| import org.apache.royale.compiler.internal.config.annotations.Config; |
| import org.apache.royale.compiler.internal.config.annotations.RoyaleOnly; |
| import org.apache.royale.compiler.internal.config.annotations.InfiniteArguments; |
| import org.apache.royale.compiler.internal.config.annotations.Mapping; |
| import org.apache.royale.compiler.internal.config.annotations.SoftPrerequisites; |
| import org.apache.royale.compiler.problems.ConfigurationProblem; |
| import org.apache.royale.compiler.problems.ICompilerProblem; |
| import org.apache.royale.utils.Trace; |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ImmutableList; |
| |
| /** |
| * The basic idea here is to let you keep all your configuration knowledge in |
| * your configuration object, and to automate as much as possible. Reflection is |
| * used to convert public fields and setters on your configuration object into |
| * settable vars. There are a few key concepts: |
| * <p> |
| * - You should be able to configure absolutely any object.<br> |
| * - Child configuration variables in your config become a dotted hierarchy of |
| * varnames<br> |
| * - All sources of configuration data are buffered and merged (as string |
| * var/vals) before committing to the final configuration. This class acts as |
| * the buffer.<br> |
| * - Hyphenated variables (i.e. "some-var") are automatically configured by |
| * calling your matching setter (i.e. setSomeVar)<br> |
| * - Implementing an getSomeVarInfo() method on your class lets you set up more |
| * complicated config objects<br> |
| * - You can make variables depend on other variables having been set first. |
| * This lets you set a root directory in one var and then use its value in |
| * another.<br> |
| * - Per-variable validation can be performed in setters. Overall validation |
| * should take place as a post-process step.<br> |
| * - You can keep ConfigurationBuffers around and merge multiple buffers |
| * together before committing. Most recent definitions always win.<br> |
| * <p> |
| * The contract with your configuration class: |
| * <p> |
| * - You must provide a method with the signature |
| * "void setYourVar(ConfigurationValue val)" to set your config var. Your setter |
| * method should accept either a single arg of type List or String[], or else an |
| * arglist of simple types. For example |
| * "void myvar(int a, boolean b, String c")".<br> |
| * - You can implement a function with the signature "int yourvar_argcount()" to |
| * require a different number of arguments. This limit will be enforced by |
| * configurators (command line, file, etc.)<br> |
| * - If you provide a setter and explicit parameters (i.e. not List or String[]) |
| * the number of arguments will be automatically determined.<br> |
| * - Each argument to your configuration variable is assumed to have a |
| * (potentially non-unique) name. The default is the simple type of the argument |
| * (boolean, int, string). If the var takes an undetermined number of args via |
| * List or String[], the argname defaults to string.<br> |
| * - You can implement a function with the signature |
| * "String yourvar_argnames(int)" to provide names for each of the parameters. |
| * The integer passed in is the argument number. Return the same name (i.e. |
| * "item") for infinite lists.<br> |
| * - You can implement a function with the signature "String[] yourvar_deps()" |
| * to provide a list of other prerequisites for this var. You will be guaranteed |
| * that the deps are committed before your var, or else a configurationexception |
| * will be thrown if a prerequsite was unset. (Note that infinite cycles are not |
| * checked, so be careful.)<br> |
| */ |
| public final class ConfigurationBuffer |
| { |
| public ConfigurationBuffer(Class<? extends Configuration> configClass) |
| { |
| this(configClass, new HashMap<String, String>()); |
| } |
| |
| public ConfigurationBuffer(Class<? extends Configuration> configClass, Map<String, String> aliases) |
| { |
| this(configClass, aliases, null); |
| } |
| |
| /** |
| * Create a configuration buffer with an optional filter. The filter can be |
| * used to remove unwanted options from a super class. |
| * |
| * @param filter if null there is no filter, otherwise the set of |
| * configuration options is filtered. |
| */ |
| public ConfigurationBuffer(Class<? extends Configuration> configClass, Map<String, String> aliases, IConfigurationFilter filter) |
| { |
| this.configClass = configClass; |
| this.varMap = new HashMap<String, List<ConfigurationValue>>(); |
| this.committed = new HashSet<String>(); |
| |
| loadCache(configClass, filter); |
| assert (varCache.size() > 0) : "coding error: nothing was configurable in the provided object!"; |
| for (Map.Entry<String, String> e : aliases.entrySet()) |
| { |
| addAlias(e.getKey(), e.getValue()); |
| } |
| } |
| |
| public ConfigurationBuffer(ConfigurationBuffer copyFrom, boolean copyCommitted) |
| { |
| this.configClass = copyFrom.configClass; |
| this.varMap = new HashMap<String, List<ConfigurationValue>>(copyFrom.varMap); |
| this.committed = copyCommitted ? new HashSet<String>(copyFrom.committed) : new HashSet<String>(); |
| this.varCache = copyFrom.varCache; // doesn't change after creation |
| this.varList = copyFrom.varList; // doesn't change after creation |
| this.tokens = new HashMap<String, String>(copyFrom.tokens); |
| } |
| |
| public final List<String> dump() |
| { |
| final List<String> dump = new ArrayList<String>(varCache.size()); |
| for (final Map.Entry<String, ConfigurationInfo> entry : varCache.entrySet()) |
| { |
| dump.add(entry.getKey() + "," + entry.getValue().toString()); |
| } |
| Collections.sort(dump); |
| return dump; |
| } |
| |
| public void setVar(String var, String val, String source, int line) throws ConfigurationException |
| { |
| List<String> list = new LinkedList<String>(); |
| list.add(val); |
| setVar(var, list, source, line, null, false); |
| } |
| |
| public void setVar(String var, List<String> vals, String source, int line) throws ConfigurationException |
| { |
| setVar(var, vals, source, line, null, false); |
| } |
| |
| public void setVar(String avar, List<String> vals, String source, int line, String contextPath, boolean append) throws ConfigurationException |
| { |
| String var = unalias(avar); |
| if (!isValidVar(var)) |
| throw new ConfigurationException.UnknownVariable(var, source, line); |
| |
| int argCount = getVarArgCount(var); |
| |
| // -1 means unspecified length, its up to the receiving setter to validate. |
| if (argCount != -1) |
| { |
| addAnyDefaultArgValues(var, argCount, vals); |
| |
| if (vals.size() != argCount) |
| { |
| throw new ConfigurationException.IncorrectArgumentCount(argCount, // expected |
| vals.size(), //passed |
| var, source, line); |
| } |
| } |
| |
| ConfigurationValue val = new ConfigurationValue(this, var, |
| vals, //processValues( var, vals, source, line ), |
| source, line, contextPath); |
| storeValue(var, val, append); |
| committed.remove(var); |
| } |
| |
| public void clearVar(String avar, String source, int line) throws ConfigurationException |
| { |
| String var = unalias(avar); |
| if (!isValidVar(var)) |
| throw new ConfigurationException.UnknownVariable(var, source, line); |
| varMap.remove(var); |
| committed.remove(var); |
| } |
| |
| /** |
| * Remove the configuration values came from the given source. |
| * |
| * @param source source name |
| * @see CommandLineConfigurator#SOURCE_COMMAND_LINE |
| */ |
| public void clearSourceVars(String source) |
| { |
| List<String> remove = new LinkedList<String>(); |
| for (Map.Entry<String, List<ConfigurationValue>> e : varMap.entrySet()) |
| { |
| String var = e.getKey(); |
| List<ConfigurationValue> vals = e.getValue(); |
| |
| List<ConfigurationValue> newvals = new LinkedList<ConfigurationValue>(); |
| for (ConfigurationValue val : vals) |
| { |
| if (!val.getSource().equals(source)) |
| { |
| newvals.add(val); |
| } |
| } |
| if (newvals.size() > 0) |
| varMap.put(var, newvals); |
| else |
| remove.add(var); |
| } |
| for (Iterator<String> it = remove.iterator(); it.hasNext();) |
| { |
| varMap.remove(it.next()); |
| } |
| } |
| |
| public List<String> processValues(String var, List<String> args, String source, int line) throws ConfigurationException |
| { |
| List<String> newArgs = new LinkedList<String>(); |
| for (Iterator<String> it = args.iterator(); it.hasNext();) |
| { |
| String arg = it.next(); |
| |
| int depth = 100; |
| while (depth-- > 0) |
| { |
| int o = arg.indexOf("${"); |
| if (o == -1) |
| break; |
| |
| int c = arg.indexOf("}", o); |
| |
| if (c == -1) |
| { |
| throw new ConfigurationException.Token(ConfigurationException.Token.MISSING_DELIMITER, |
| null, var, source, line); |
| } |
| String token = arg.substring(o + 2, c); |
| String value = getToken(token); |
| |
| if (value == null) |
| { |
| if (value == null) |
| |
| { |
| throw new ConfigurationException.Token(ConfigurationException.Token.UNKNOWN_TOKEN, |
| token, var, source, line); |
| } |
| |
| } |
| arg = arg.substring(0, o) + value + arg.substring(c + 1); |
| |
| } |
| if (depth == 0) |
| { |
| throw new ConfigurationException.Token(ConfigurationException.Token.RECURSION_LIMIT, |
| null, var, source, line); |
| } |
| |
| newArgs.add(arg); |
| } |
| return newArgs; |
| } |
| |
| public void setToken(String token, String value) |
| { |
| tokens.put(token, value); |
| } |
| |
| public String getToken(String token) |
| { |
| if (tokens.containsKey(token)) |
| return tokens.get(token); |
| else |
| { |
| try |
| { |
| return System.getProperty(token); |
| } |
| catch (SecurityException se) |
| { |
| return null; |
| } |
| } |
| } |
| |
| private void storeValue(String avar, ConfigurationValue val, boolean append) throws ConfigurationException |
| { |
| String var = unalias(avar); |
| ConfigurationInfo info = getInfo(var); |
| |
| List<ConfigurationValue> vals; |
| if (varMap.containsKey(var)) |
| { |
| vals = varMap.get(var); |
| assert (vals.size() > 0); |
| ConfigurationValue first = vals.get(0); |
| if (!append && !first.getSource().equals(val.getSource())) |
| vals.clear(); |
| else if (!info.allowMultiple()) |
| throw new ConfigurationException.IllegalMultipleSet( |
| var, |
| val.getSource(), val.getLine()); |
| } |
| else |
| { |
| vals = new LinkedList<ConfigurationValue>(); |
| varMap.put(var, vals); |
| } |
| vals.add(val); |
| } |
| |
| public List<ConfigurationValue> getVar(String avar) |
| { |
| String var = unalias(avar); |
| return varMap.get(var); |
| } |
| |
| public Set<String> getVars() |
| { |
| return varCache.keySet(); |
| } |
| |
| public void merge(ConfigurationBuffer other) |
| { |
| assert (configClass == other.configClass); |
| varMap.putAll(other.varMap); |
| committed.addAll(other.committed); |
| } |
| |
| private final Map<String, List<ConfigurationValue>> varMap; // list of vars that have been set |
| private final Set<String> committed; // set of vars committed to backing config |
| private final Class<? extends Configuration> configClass; // configuration class |
| private Map<String, ConfigurationInfo> varCache // info cache |
| = new HashMap<String, ConfigurationInfo>(); |
| private List<String> requiredList = new LinkedList<String>(); // required vars |
| private List<String> varList = new LinkedList<String>(); // list of vars in order they should be set |
| private Map<String, String> aliases = new HashMap<String, String>(); // variable name aliases |
| private Map<String, String> tokens = new HashMap<String, String>(); // tokens for replacement |
| private List<Object[]> positions = new ArrayList<Object[]>(); |
| |
| private static final String SET_PREFIX = "cfg"; |
| private static final String GET_PREFIX = "get"; |
| private static final String INFO_SUFFIX = "Info"; |
| |
| //----------------------------------------------- |
| // |
| |
| /** |
| * WORKAROUND FOR BUG CMP-396 |
| * |
| * <p> |
| * {@link #c2h(String)} generates option names based on cfgXXX names in |
| * {@code Configuration}. Since we collapsed all the sub-configurations into |
| * one class, there's no longer a "base name" like "compiler.*" or |
| * "compiler.fonts.*". In order to preserve the dotted naming convention, we |
| * need to know which "-" separated names are actually dotted names. The |
| * {@link #CONVERT_FROM} and {@link #CONVERT_TO} is an <b>ordered</b> lookup |
| * table for option group base names. It's order makes sure that the longest |
| * possible replacement is done. |
| */ |
| private static final ImmutableList<String> CONVERT_FROM = |
| ImmutableList.of( |
| "compiler-fonts-languages-", |
| "compiler-fonts-", |
| "compiler-namespaces-", |
| "compiler-mxml-", |
| "compiler-", |
| "metadata-", |
| "licenses-", |
| "frames-", |
| "runtime-shared-library-settings-"); |
| |
| private static final ImmutableList<String> CONVERT_TO = |
| ImmutableList.of( |
| "compiler.fonts.languages.", |
| "compiler.fonts.", |
| "compiler.namespaces.", |
| "compiler.mxml.", |
| "compiler.", |
| "metadata.", |
| "licenses.", |
| "frames.", |
| "runtime-shared-library-settings."); |
| |
| /** |
| * convert StudlyCaps or camelCase to hyphenated |
| * |
| * @param camel someVar or SomeVar |
| * @return hyphen some-var |
| */ |
| protected static String c2h(String camel) |
| { |
| StringBuilder b = new StringBuilder(camel.length() + 5); |
| for (int i = 0; i < camel.length(); ++i) |
| { |
| char c = camel.charAt(i); |
| if (Character.isUpperCase(c)) |
| { |
| if (i != 0) |
| b.append('-'); |
| b.append(Character.toLowerCase(c)); |
| } |
| else |
| { |
| b.append(camel.charAt(i)); |
| } |
| } |
| final String combined = b.toString(); |
| |
| for (int i = 0; i < CONVERT_FROM.size(); i++) |
| { |
| if (combined.startsWith(CONVERT_FROM.get(i))) |
| { |
| return combined.replaceFirst(CONVERT_FROM.get(i), CONVERT_TO.get(i)); |
| } |
| } |
| return combined; |
| } |
| |
| /** |
| * convert hyphenated to StudlyCaps or camelCase |
| * |
| * @param hyphenated some-var |
| * @return result |
| */ |
| protected static String h2c(String hyphenated, boolean studly) |
| { |
| StringBuilder b = new StringBuilder(hyphenated.length()); |
| boolean capNext = studly; |
| for (int i = 0; i < hyphenated.length(); ++i) |
| { |
| char c = hyphenated.charAt(i); |
| if (c == '-') |
| capNext = true; |
| else |
| { |
| b.append(capNext ? Character.toUpperCase(c) : c); |
| capNext = false; |
| } |
| } |
| return b.toString(); |
| } |
| |
| public static String varname(String membername, String basename) |
| { |
| return ((basename == null) ? membername : (basename + "." + membername)); |
| } |
| |
| private static ConfigurationInfo createInfo(Method setterMethod) |
| { |
| ConfigurationInfo info = null; |
| |
| String infoMethodName = GET_PREFIX + setterMethod.getName().substring(SET_PREFIX.length()) + INFO_SUFFIX; |
| String getterMethodName = GET_PREFIX + setterMethod.getName().substring(SET_PREFIX.length()); |
| @SuppressWarnings("unchecked") |
| Class<? extends Configuration> cfgClass = (Class<? extends Configuration>)setterMethod.getDeclaringClass(); |
| |
| Method infoMethod = null, getterMethod = null; |
| if (!setterMethod.isAnnotationPresent(Config.class)) |
| { |
| try |
| { |
| infoMethod = cfgClass.getMethod(infoMethodName); |
| |
| if (!Modifier.isStatic(infoMethod.getModifiers())) |
| { |
| assert false : ("coding error: " + cfgClass.getName() + "." + infoMethodName + " needs to be static!"); |
| infoMethod = null; |
| } |
| |
| info = (ConfigurationInfo)infoMethod.invoke(null, (Object[])null); |
| |
| } |
| catch (SecurityException e) |
| { |
| e.printStackTrace(); |
| } |
| catch (NoSuchMethodException e) |
| { |
| } |
| catch (IllegalArgumentException e) |
| { |
| e.printStackTrace(); |
| } |
| catch (IllegalAccessException e) |
| { |
| e.printStackTrace(); |
| } |
| catch (InvocationTargetException e) |
| { |
| e.printStackTrace(); |
| } |
| } |
| |
| if (info == null) |
| info = new ConfigurationInfo(); |
| |
| try |
| { |
| getterMethod = cfgClass.getMethod(getterMethodName, (Class[])null); |
| } |
| catch (SecurityException e) |
| { |
| e.printStackTrace(); |
| } |
| catch (NoSuchMethodException e) |
| { |
| } |
| info.setSetterMethod(setterMethod); |
| info.setGetterMethod(getterMethod); |
| |
| return info; |
| } |
| |
| /** |
| * load - prefetch all the interesting names into a dictionary so that we |
| * can find them again more easily. At the end of this call, we will have a |
| * list of every variable and their associated method. |
| * |
| * @param filter if null there is no filter, otherwise the set of |
| * configuration options is filtered. |
| */ |
| private boolean loadCache(Class<? extends Configuration> cfg, IConfigurationFilter filter) |
| { |
| int count = 0; |
| |
| // First, find all vars at this level. |
| for (final Method method : cfg.getMethods()) |
| { |
| if (method.getName().startsWith(SET_PREFIX) || |
| method.isAnnotationPresent(Config.class)) |
| { |
| String configName = null; |
| |
| final Class<?>[] pt = method.getParameterTypes(); |
| assert pt.length > 1 : "Expected at least one parameters on setter."; |
| |
| // Collect configuration info from getXXXInfo() static methods. |
| final ConfigurationInfo info = createInfo(method); |
| |
| // Collect configuration info from annotations. |
| final Config config = method.getAnnotation(Config.class); |
| if (config != null) |
| { |
| info.isAdvanced = config.advanced(); |
| info.isHidden = config.hidden(); |
| info.isRemoved = config.removed(); |
| info.allowMultiple = config.allowMultiple(); |
| info.isPath = config.isPath(); |
| info.isDisplayed = config.displayed(); |
| info.isCompcOnly = config.compcOnly(); |
| info.isRequired = config.isRequired(); |
| |
| // Argument name generator class |
| final ArgumentNameGenerator argumentNameGeneratorClass = |
| method.getAnnotation(ArgumentNameGenerator.class); |
| if (argumentNameGeneratorClass != null) |
| { |
| info.argNameGeneratorClass = argumentNameGeneratorClass.value(); |
| } |
| else |
| { |
| // Argument names |
| final Arguments arguments = method.getAnnotation(Arguments.class); |
| if (arguments != null) |
| info.argnames = arguments.value(); |
| } |
| |
| // Argument count |
| final InfiniteArguments infinite = method.getAnnotation(InfiniteArguments.class); |
| if (infinite != null) |
| info.argcount = ConfigurationInfo.INFINITE_ARGS; |
| |
| // Soft Prerequisites |
| final SoftPrerequisites softPre = method.getAnnotation(SoftPrerequisites.class); |
| if (softPre != null) |
| info.softPrerequisites = softPre.value(); |
| |
| // XML element name for configuration |
| final Mapping mapping = method.getAnnotation(Mapping.class); |
| if (mapping != null) |
| configName = Joiner.on(".").skipNulls().join(mapping.value()); |
| |
| // Is this a Flex only option? |
| final RoyaleOnly royaleOnly = method.getAnnotation(RoyaleOnly.class); |
| if (royaleOnly != null) |
| info.isRoyaleOnly = true; |
| } |
| |
| // Fall back to naming convention for configuration names. |
| if (configName == null) |
| configName = c2h(method.getName().substring(SET_PREFIX.length())); |
| |
| if( filter == null || filter.select(configName) ) |
| { |
| varCache.put(configName, info); |
| varList.add(configName); |
| if (info.isRequired()) |
| { |
| requiredList.add(configName); |
| } |
| ++count; |
| } |
| } |
| } |
| |
| assert (count > 0 || filter != null) : "coding error: config class " + cfg.getName() + " did not define any setters or child configs"; |
| return (count > 0); |
| } |
| |
| String classToArgName(Class<?> c) |
| { |
| // we only support builtin classnames! |
| |
| String className = c.getName(); |
| if (className.startsWith("java.lang.")) |
| className = className.substring("java.lang.".length()); |
| |
| return className.toLowerCase(); |
| } |
| |
| public ConfigurationInfo getInfo(String avar) |
| { |
| String var = unalias(avar); |
| return varCache.get(var); |
| } |
| |
| public String getVarArgName(String avar, int argnum) |
| { |
| String var = unalias(avar); |
| ConfigurationInfo info = getInfo(var); |
| |
| if (info == null) |
| { |
| assert false : ("must call isValid to check vars!"); |
| } |
| |
| return info.getArgName(argnum); |
| } |
| |
| public boolean isValidVar(String avar) |
| { |
| String var = unalias(avar); |
| ConfigurationInfo info = getInfo(var); |
| return (info != null); |
| } |
| |
| public int getVarArgCount(String avar) |
| { |
| ConfigurationInfo info = getInfo(avar); |
| assert (info != null); |
| return info.getArgCount(); |
| } |
| |
| /** |
| * Add any default values to an argument, if the user did not specify them |
| * on the command line. |
| * |
| * @param avar the argument variable |
| * @param argCount the number of argument values specified |
| * @param vals Values to add any default values to |
| */ |
| private void addAnyDefaultArgValues(String avar, int argCount, List<String> vals) |
| { |
| ConfigurationInfo info = getInfo(avar); |
| final int missingArgsCount = argCount - vals.size(); |
| if (missingArgsCount == 0 || info.getDefaultArgValues() == null) |
| return; |
| |
| final String[] defaultArgValues = info.getDefaultArgValues(); |
| final int defaultArgsCount = defaultArgValues.length; |
| final int defaultArgsStart = defaultArgsCount - missingArgsCount; |
| for (int i = defaultArgsStart; i < defaultArgsCount; i++) |
| { |
| vals.add(defaultArgValues[i]); |
| } |
| } |
| |
| /** |
| * commit - bake the resolved map to the configuration |
| * |
| * @param config The configuration to set the buffer variables into. |
| * @param problems A collection where configuration problems are reported. |
| * |
| * @return true if successful, false otherwise. |
| */ |
| public boolean commit(Object config, Collection<ICompilerProblem> problems) |
| { |
| assert (config.getClass() == configClass) : |
| ("coding error: configuration " + config.getClass() + " != template " + configClass); |
| Set<String> done = new HashSet<String>(); |
| boolean success = true; |
| |
| for (Iterator<String> vars = varList.iterator(); vars.hasNext();) |
| { |
| String var = vars.next(); |
| if (varMap.containsKey(var)) |
| { |
| try |
| { |
| commitVariable(config, var, done); |
| } |
| catch (ConfigurationException e) |
| { |
| problems.add(new ConfigurationProblem(e)); |
| success = false; |
| } |
| } |
| } |
| |
| for (Iterator<String> reqs = requiredList.iterator(); reqs.hasNext();) |
| { |
| String req = reqs.next(); |
| |
| if (!committed.contains(req)) |
| { |
| ConfigurationException e = new ConfigurationException.MissingRequirement(req, null, null, -1); |
| problems.add(new ConfigurationProblem( |
| null, |
| -1, |
| -1, |
| -1, |
| -1, |
| e.getMessage())); |
| success = false; |
| } |
| } |
| |
| return success; |
| } |
| |
| /** |
| * commitVariable - copy a variable out of a state into the final config. |
| * This should only be called on variables that are known to exist in the |
| * state! |
| * |
| * @param var variable name to lookup |
| * @param done set of variable names that have been completed so far (for |
| * recursion) |
| */ |
| private void commitVariable(Object config, String var, Set<String> done) throws ConfigurationException |
| { |
| ConfigurationInfo info = getInfo(var); |
| |
| setPrerequisites(info.getPrerequisites(), var, done, config, true); |
| setPrerequisites(info.getSoftPrerequisites(), var, done, config, false); |
| |
| if (committed.contains(var)) |
| return; |
| |
| committed.add(var); |
| done.add(var); |
| |
| assert (varMap.containsKey(var)); |
| List<ConfigurationValue> vals = varMap.get(var); |
| |
| if (vals.size() > 1) |
| { |
| assert (info.allowMultiple()); // assumed to have been previously checked |
| } |
| for (ConfigurationValue val : vals) |
| { |
| try |
| { |
| Object[] args = buildArgList(info, val); |
| |
| info.getSetterMethod().invoke(config, args); |
| } |
| catch (Exception e) |
| { |
| Throwable t = e; |
| |
| if (e instanceof InvocationTargetException) |
| { |
| t = ((InvocationTargetException)e).getTargetException(); |
| } |
| |
| if (Trace.error) |
| t.printStackTrace(); |
| |
| if (t instanceof ConfigurationException) |
| { |
| throw (ConfigurationException)t; |
| } |
| else |
| { |
| throw new ConfigurationException.OtherThrowable(t, var, val.getSource(), val.getLine()); |
| } |
| } |
| } |
| |
| } |
| |
| private void setPrerequisites(String[] prerequisites, String var, Set<String> done, Object config, boolean required) |
| throws ConfigurationException |
| { |
| if (prerequisites != null) |
| { |
| for (int p = 0; p < prerequisites.length; ++p) |
| { |
| String depvar = prerequisites[p]; |
| |
| // Dependencies can only go downward. |
| int dot = var.lastIndexOf('.'); |
| |
| if (dot >= 0) |
| { |
| String car = var.substring(0, dot); |
| //String cdr = var.substring( dot + 1 ); |
| |
| String newDepvar = car + "." + depvar; |
| |
| // Since in royale we have collapsed sub-configurations into one |
| // configuration, some options that were in sub-configurations now |
| // have prerequisites on options in the same configuration. We |
| // need to keep the old configuration mappings so old configurations |
| // options will still work. So a simple thing we can do is if the |
| // dependency variable is invalid (presumably because the |
| // dependency is really on a parent configuration option), |
| // then use the dependency as is depvar) and see if it is |
| // valid. If depvar ends up not being valid then set depvar |
| // to newDepvar so error reporting isn't changed by the new |
| // fall-back behavior. |
| if (isValidVar(newDepvar) || !isValidVar(depvar)) |
| depvar = newDepvar; |
| |
| } |
| |
| if (!done.contains(depvar)) |
| { |
| if (!isValidVar(depvar)) |
| { |
| assert false : ("invalid " + var + " dependency " + depvar); |
| continue; |
| } |
| if (varMap.containsKey(depvar)) |
| { |
| commitVariable(config, depvar, done); |
| } |
| else if (required && !committed.contains(depvar)) |
| { |
| // TODO - can we get source/line for this? |
| throw new ConfigurationException.MissingRequirement(depvar, var, null, -1); |
| } |
| } |
| } |
| } |
| } |
| |
| private String[] constructStringArray(List<String> args) |
| { |
| String[] sa = new String[args.size()]; |
| |
| int i = 0; |
| for (Iterator<String> it = args.iterator(); it.hasNext();) |
| sa[i++] = it.next(); |
| |
| return sa; |
| } |
| |
| private Object constructValueObject(ConfigurationInfo info, ConfigurationValue cv) throws ConfigurationException |
| { |
| try |
| { |
| Class<?>[] pt = info.getSetterMethod().getParameterTypes(); |
| assert (pt.length == 2); // assumed to be checked upstream |
| |
| Object o = pt[1].newInstance(); |
| |
| Field[] fields = pt[1].getFields(); |
| |
| assert (fields.length == cv.getArgs().size()); // assumed to be checked upstream |
| |
| Iterator<String> argsit = cv.getArgs().iterator(); |
| for (int f = 0; f < fields.length; ++f) |
| { |
| String val = (String)argsit.next(); |
| Object valobj = null; |
| Class<?> fc = fields[f].getType(); |
| |
| assert (info.getArgType(f) == fc); |
| assert (info.getArgName(f).equals(ConfigurationBuffer.c2h(fields[f].getName()))); |
| |
| if (fc == String.class) |
| { |
| valobj = val; |
| } |
| else if ((fc == Boolean.class) || (fc == boolean.class)) |
| { |
| // TODO - Boolean.valueOf is pretty lax. Maybe we should restrict to true/false? |
| valobj = Boolean.valueOf(val); |
| } |
| else if ((fc == Integer.class) || (fc == int.class)) |
| { |
| valobj = Integer.decode(val); |
| } |
| else if ((fc == Long.class) || (fc == long.class)) |
| { |
| valobj = Long.decode(val); |
| } |
| else |
| { |
| assert false; // should have checked any other condition upstream! |
| } |
| fields[f].set(o, valobj); |
| } |
| |
| return o; |
| } |
| catch (InstantiationException e) |
| { |
| assert false : ("coding error: unable to instantiate value object when trying to set var " + cv.getVar()); |
| throw new ConfigurationException.OtherThrowable(e, cv.getVar(), cv.getSource(), cv.getLine()); |
| |
| } |
| catch (IllegalAccessException e) |
| { |
| assert false : ("coding error: " + e + " when trying to set var " + cv.getVar()); |
| throw new ConfigurationException.OtherThrowable(e, cv.getVar(), cv.getSource(), cv.getLine()); |
| } |
| } |
| |
| protected static boolean isSupportedSimpleType(Class<?> c) |
| { |
| return ((c == String.class) |
| || (c == Integer.class) || (c == int.class) |
| || (c == Long.class) || (c == long.class) |
| || (c == Boolean.class) || (c == boolean.class)); |
| } |
| |
| protected static boolean isSupportedListType(Class<?> c) |
| { |
| return ((c == List.class) || (c == String[].class)); |
| } |
| |
| protected static boolean isSupportedValueType(Class<?> c) |
| { |
| if (isSupportedSimpleType(c)) |
| return false; |
| |
| Field[] fields = c.getFields(); |
| |
| for (int f = 0; f < fields.length; ++f) |
| { |
| if (!isSupportedSimpleType(fields[f].getType())) |
| return false; |
| } |
| return true; |
| } |
| |
| private Object[] buildArgList(ConfigurationInfo info, ConfigurationValue val) throws ConfigurationException |
| { |
| Method setter = info.getSetterMethod(); |
| |
| Class<?>[] pt = setter.getParameterTypes(); |
| |
| List<String> args = processValues(val.getVar(), val.getArgs(), val.getSource(), val.getLine()); |
| |
| if (info.getArgCount() == -1) |
| { |
| if (pt.length != 2) |
| { |
| assert false : ("coding error: unlimited length setter " + val.getVar() + " must take a single argument of type List or String[]"); |
| return null; |
| } |
| else if (List.class.isAssignableFrom(pt[1])) |
| { |
| return new Object[] {val, args}; |
| } |
| else if (String[].class.isAssignableFrom(pt[1])) |
| { |
| return new Object[] {val, constructStringArray(args)}; |
| } |
| else |
| { |
| assert false : ("coding error: unlimited length setter " + val.getVar() + " must take a single argument of type List or String[]"); |
| return null; |
| } |
| } |
| else |
| { |
| assert (pt.length > 1) : ("coding error: config setter " + val.getVar() + " must accept at least one argument"); |
| // ok, we first check to see if the signature of their setter accepts a list. |
| |
| if (pt.length == 2) |
| { |
| // a variety of specialty setters here... |
| |
| if (List.class.isAssignableFrom(pt[1])) |
| { |
| return new Object[] {val, args}; |
| } |
| else if (String[].class == pt[1]) |
| { |
| return new Object[] {val, constructStringArray(args)}; |
| } |
| else if (isSupportedValueType(pt[1])) |
| { |
| return new Object[] {val, constructValueObject(info, val)}; |
| } |
| } |
| |
| // otherwise, they must have a matching size parm list as the number of args passed in. |
| |
| assert (pt.length == (args.size() + 1)) : ("coding error: config setter " + val.getVar() + " does not have " + args.size() + " parameters!"); |
| |
| Object[] pa = new Object[pt.length]; |
| |
| pa[0] = val; |
| |
| for (int p = 1; p < pt.length; ++p) |
| { |
| String arg = args.get(p - 1); |
| if (pt[p].isAssignableFrom(String.class)) |
| { |
| pa[p] = arg; |
| } |
| else if ((pt[p] == int.class) || (pt[p] == Integer.class)) |
| { |
| try |
| { |
| pa[p] = Integer.decode(arg); |
| |
| } |
| catch (Exception e) |
| { |
| throw new ConfigurationException.TypeMismatch(ConfigurationException.TypeMismatch.INTEGER, |
| arg, val.getVar(), val.getSource(), val.getLine()); |
| } |
| } |
| else if ((pt[p] == long.class) || (pt[p] == Long.class)) |
| { |
| try |
| { |
| pa[p] = Long.decode(arg); |
| |
| } |
| catch (Exception e) |
| { |
| throw new ConfigurationException.TypeMismatch( |
| ConfigurationException.TypeMismatch.LONG, |
| arg, val.getVar(), val.getSource(), val.getLine()); |
| } |
| } |
| else if ((pt[p] == boolean.class) || (pt[p] == Boolean.class)) |
| { |
| try |
| { |
| arg = arg.trim().toLowerCase(); |
| if (arg.equals("true") || arg.equals("false")) |
| { |
| pa[p] = Boolean.valueOf(arg); |
| } |
| else |
| { |
| throw new ConfigurationException.TypeMismatch( |
| ConfigurationException.TypeMismatch.BOOLEAN, arg, val.getVar(), val.getSource(), val.getLine()); |
| } |
| } |
| catch (Exception e) |
| { |
| throw new ConfigurationException.TypeMismatch( |
| ConfigurationException.TypeMismatch.BOOLEAN, arg, val.getVar(), val.getSource(), val.getLine()); |
| } |
| } |
| else |
| { |
| assert false : ("coding error: " + val.getVar() + " setter argument " + p + " is not a supported type"); |
| } |
| } |
| |
| return pa; |
| } |
| } |
| |
| public void addAlias(String alias, String var) |
| { |
| if (!isValidVar(var)) |
| { |
| assert false : ("coding error: can't bind alias " + alias + " to nonexistent var " + var); |
| return; |
| } |
| if (aliases.containsKey(alias)) |
| { |
| assert false : ("coding error: alias " + alias + " already defined as " + aliases.get(alias)); |
| return; |
| } |
| if (varCache.containsKey(alias)) |
| { |
| assert false : ("coding error: can't define alias " + alias + ", it already exists as a var"); |
| return; |
| } |
| |
| aliases.put(alias, var); |
| } |
| |
| public Map<String, String> getAliases() |
| { |
| return aliases; |
| } |
| |
| public String unalias(String var) |
| { |
| String realvar = aliases.get(var); |
| return (realvar == null) ? var : realvar; |
| } |
| |
| public String peekSimpleConfigurationVar(String avar) throws ConfigurationException |
| { |
| String val = null; |
| List<ConfigurationValue> valList = getVar(avar); |
| if (valList != null) |
| { |
| ConfigurationValue cv = (ConfigurationValue)valList.get(0); |
| List<String> args = processValues(avar, cv.getArgs(), cv.getSource(), cv.getLine()); |
| val = args.get(0); |
| } |
| return val; |
| } |
| |
| public List<ConfigurationValue> peekConfigurationVar(String avar) throws ConfigurationException |
| { |
| List<ConfigurationValue> srcList = getVar(avar); |
| if (srcList == null) |
| return null; |
| |
| List<ConfigurationValue> dstList = new LinkedList<ConfigurationValue>(); |
| for (ConfigurationValue srcVal : srcList) |
| { |
| List<String> args = processValues(avar, srcVal.getArgs(), srcVal.getSource(), srcVal.getLine()); |
| |
| ConfigurationValue dstVal = new ConfigurationValue(srcVal.getBuffer(), avar, args, srcVal.getSource(), srcVal.getLine(), srcVal.getContext()); |
| dstList.add(dstVal); |
| } |
| return dstList; |
| } |
| |
| public void addPosition(String var, int iStart, int iEnd) |
| { |
| positions.add(new Object[] {var, new Integer(iStart), new Integer(iEnd)}); |
| } |
| |
| public List<Object[]> getPositions() |
| { |
| return positions; |
| } |
| |
| public static List<String> formatText(String input, int columns) |
| { |
| ArrayList<String> lines = new ArrayList<String>(); |
| |
| if ((input == null) || (input.length() == 0)) |
| return lines; |
| |
| int current = 0; |
| int lineStart = -1; |
| int lineEnd = -1; |
| int wordStart = -1; |
| int wordEnd = -1; |
| boolean start = true; |
| boolean preserve = true; |
| |
| while (true) |
| { |
| if (current < input.length()) |
| { |
| boolean newline = input.charAt(current) == '\n'; |
| boolean printable = (preserve && !newline) || !Character.isWhitespace(input.charAt(current)); |
| |
| if (start) // find a word |
| { |
| if (printable) |
| { |
| if (lineStart == -1) |
| { |
| lineStart = current; |
| } |
| wordStart = current; |
| start = false; |
| } |
| else |
| { |
| if (newline && lineStart != -1) |
| { |
| lines.add(input.substring(lineStart, current)); |
| lineStart = -1; |
| } |
| else if (newline) |
| { |
| lines.add(""); |
| } |
| ++current; |
| } |
| } |
| else |
| // have a word |
| { |
| preserve = false; |
| if (printable) |
| { |
| ++current; |
| } |
| else |
| { |
| wordEnd = current; |
| if (lineEnd == -1) |
| { |
| lineEnd = current; |
| } |
| |
| // two possibilities; if the new word fits in the current line length |
| // without being too many columns, leave on current line. |
| // otherwise, set it as the start of a new line. |
| |
| if (wordEnd - lineStart < columns) |
| { |
| if (newline) |
| { |
| lines.add(input.substring(lineStart, current)); |
| lineStart = -1; |
| lineEnd = -1; |
| wordStart = -1; |
| start = true; |
| preserve = true; |
| ++current; |
| } |
| else |
| { |
| // we have room to add the current word to this line, find new word |
| start = true; |
| lineEnd = current; |
| } |
| } |
| else |
| { |
| // current word pushes things beyond the requested column limit, |
| // dump current text |
| lines.add(input.substring(lineStart, lineEnd)); |
| lineStart = wordStart; |
| lineEnd = -1; |
| wordStart = -1; |
| start = true; |
| if (newline) |
| preserve = true; |
| } |
| } |
| } |
| } |
| else |
| // we're done |
| { |
| // a) no line yet, so don't do anything |
| // b) have line and new word would push over edge, need two lines |
| // c) have line and current word fits, need one line |
| // d) only one word and its too long anyway, need one line |
| |
| if (lineStart != -1) // we have a line in progress |
| { |
| wordEnd = current; |
| if (lineEnd == -1) |
| lineEnd = current; |
| |
| if (((wordEnd - lineStart) < columns) // current word fits |
| || (wordEnd == lineEnd)) // or one long word |
| { |
| lineEnd = wordEnd; |
| lines.add(input.substring(lineStart, wordEnd)); |
| } |
| else |
| // didn't fit, multiple words |
| { |
| lines.add(input.substring(lineStart, lineEnd)); |
| lines.add(input.substring(wordStart, wordEnd)); |
| } |
| } |
| break; |
| } |
| } |
| return lines; |
| } |
| |
| /** |
| * For debugging only. |
| * <p> |
| * Produces an alphabetized list of this buffer's configuration options and their values. |
| * An option such as |
| * <pre> |
| * -foo=aaa,bbb -foo+=ccc |
| * </pre> |
| * will appear as |
| * <pre> |
| * foo=aaa,bbb;ccc |
| * </pre> |
| */ |
| @Override |
| public String toString() |
| { |
| StringBuffer sb = new StringBuffer(); |
| |
| String[] variables = varMap.keySet().toArray(new String[0]); |
| Arrays.sort(variables); |
| |
| for (String var : variables) |
| { |
| sb.append(var); |
| sb.append("="); |
| |
| ArrayList<String> commaSeparatedValues = new ArrayList<String>(); |
| for (ConfigurationValue cv : varMap.get(var)) |
| { |
| List<String> args = cv.getArgs(); |
| String joinedArgs = Joiner.on(',').join(args); |
| commaSeparatedValues.add(joinedArgs); |
| } |
| String rhs = Joiner.on(';').join(commaSeparatedValues); |
| sb.append(rhs); |
| |
| sb.append('\n'); |
| } |
| |
| return sb.toString(); |
| } |
| } |