This is an automated email from the ASF dual-hosted git repository. joshtynjala pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/royale-compiler.git
commit 1a9333bca06dc74bdf79fbb906c47850fc0999f5 Author: Josh Tynjala <[email protected]> AuthorDate: Mon Sep 20 11:25:38 2021 -0700 formatter: command line configuration --- formatter/build.xml | 11 +- .../org/apache/royale/formatter/FORMATTER.java | 90 +- .../formatter/config/CommandLineConfigurator.java | 607 +++++++++ .../royale/formatter/config/Configuration.java | 204 +++ .../formatter/config/ConfigurationBuffer.java | 1346 ++++++++++++++++++++ .../royale/formatter/config/ConfigurationInfo.java | 473 +++++++ .../formatter/config/ConfigurationValue.java | 109 ++ .../royale/formatter/config/Configurator.java | 683 ++++++++++ .../config/IFormatterSettingsConstants.java | 24 + .../config/SystemPropertyConfigurator.java | 82 ++ 10 files changed, 3595 insertions(+), 34 deletions(-) diff --git a/formatter/build.xml b/formatter/build.xml index 1243567..93e6bc8 100644 --- a/formatter/build.xml +++ b/formatter/build.xml @@ -115,9 +115,14 @@ <target name="jar-test" > <echo>using formatter.jar from ${sdk}</echo> - <java jar="${sdk}/formatter.jar" fork="true" - failonerror="true"> - </java> + <java jar="${sdk}/formatter.jar" fork="true" resultproperty="formatter.result"/> + <fail message="Starting Failed"> + <condition> + <not> + <equals arg1="${formatter.result}" arg2="1"/> + </not> + </condition> + </fail> </target> <!-- diff --git a/formatter/src/main/java/org/apache/royale/formatter/FORMATTER.java b/formatter/src/main/java/org/apache/royale/formatter/FORMATTER.java index ae80ce7..cb33693 100644 --- a/formatter/src/main/java/org/apache/royale/formatter/FORMATTER.java +++ b/formatter/src/main/java/org/apache/royale/formatter/FORMATTER.java @@ -29,6 +29,9 @@ import java.util.List; import java.util.Scanner; import org.apache.commons.io.FileUtils; +import org.apache.royale.compiler.clients.problems.ProblemFormatter; +import org.apache.royale.compiler.clients.problems.ProblemPrinter; +import org.apache.royale.compiler.clients.problems.ProblemQuery; import org.apache.royale.compiler.common.VersionInfo; import org.apache.royale.compiler.exceptions.ConfigurationException; import org.apache.royale.compiler.internal.parsing.as.ASParser; @@ -43,8 +46,12 @@ import org.apache.royale.compiler.internal.parsing.as.StreamingASTokenizer; import org.apache.royale.compiler.internal.tree.as.FileNode; import org.apache.royale.compiler.internal.workspaces.Workspace; import org.apache.royale.compiler.parsing.IASToken; +import org.apache.royale.compiler.problems.ConfigurationProblem; import org.apache.royale.compiler.problems.ICompilerProblem; import org.apache.royale.compiler.problems.UnexpectedExceptionProblem; +import org.apache.royale.formatter.config.Configuration; +import org.apache.royale.formatter.config.ConfigurationBuffer; +import org.apache.royale.formatter.config.Configurator; import org.apache.royale.utils.FilenameNormalization; /** @@ -102,12 +109,14 @@ class FORMATTER { public boolean ignoreProblems = false; public boolean collapseEmptyBlocks = false; + private ProblemQuery problems; private List<File> inputFiles = new ArrayList<File>(); private boolean writeBackToInputFiles = false; private boolean listChangedFiles = false; public int execute(String[] args) { ExitCode exitCode = ExitCode.SUCCESS; + problems = new ProblemQuery(); try { boolean continueFormatting = configure(args); @@ -151,13 +160,19 @@ class FORMATTER { } } } + } else if (problems.hasFilteredProblems()) { + exitCode = ExitCode.FAILED_WITH_CONFIG_PROBLEMS; + } else { + exitCode = ExitCode.PRINT_HELP; } - } catch (ConfigurationException e) { - System.err.println(e.getMessage()); - exitCode = ExitCode.FAILED_WITH_CONFIG_PROBLEMS; } catch (Exception e) { System.err.println(e.getMessage()); exitCode = ExitCode.FAILED_WITH_EXCEPTIONS; + } finally { + if (problems.hasFilteredProblems()) { + final ProblemPrinter printer = new ProblemPrinter(ProblemFormatter.DEFAULT_FORMATTER); + printer.printProblems(problems.getFilteredProblems()); + } } return exitCode.code; } @@ -193,28 +208,33 @@ class FORMATTER { return formatText(text, null); } - private boolean configure(String[] args) throws ConfigurationException { + private boolean configure(String[] args) { if (args.length == 0) { printHelp(); return false; } - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - if (arg.charAt(0) == '-') { - if (arg.equals("-write") || arg.equals("-w")) { - writeBackToInputFiles = true; - } else if (arg.equals("-list") || arg.equals("-l")) { - listChangedFiles = true; - } else if (arg.equals("-help") || arg.equals("-h")) { - printHelp(); - return false; - } else { - throw new ConfigurationException("Unknown command-line argument: " + arg, null, -1); - } - } else { - File inputFile = new File(arg); + try { + problems = new ProblemQuery(); + + Configurator configurator = new Configurator(); + configurator.setConfiguration(args, "files"); + Configuration config = configurator.getConfiguration(); + ConfigurationBuffer configBuffer = configurator.getConfigurationBuffer(); + + problems.addAll(configurator.getConfigurationProblems()); + + if (configBuffer.getVar("version") != null) + return false; + + if (problems.hasErrors()) + return false; + + writeBackToInputFiles = config.getWriteFiles(); + listChangedFiles = config.getListFiles(); + for (String filePath : config.getFiles()) { + File inputFile = new File(filePath); if (!inputFile.exists()) { - throw new ConfigurationException("Input file does not exist: " + arg, null, -1); + throw new ConfigurationException("Input file does not exist: " + filePath, null, -1); } if (inputFile.isDirectory()) { addDirectory(inputFile); @@ -222,21 +242,29 @@ class FORMATTER { inputFiles.add(inputFile); } } - } - if (inputFiles.size() == 0 && writeBackToInputFiles) { - throw new ConfigurationException("Cannot use -w with standard input", null, -1); - } - if (writeBackToInputFiles) { - if (inputFiles.size() == 0) { - throw new ConfigurationException("Cannot use -w with standard input", null, -1); + if (inputFiles.size() == 0 && listChangedFiles) { + throw new ConfigurationException("Cannot use -list-files with standard input", null, -1); } - for (File inputFile : inputFiles) { - if (!inputFile.canWrite()) { - throw new ConfigurationException("File is read-only: " + inputFile.getPath(), null, -1); + if (writeBackToInputFiles) { + if (inputFiles.size() == 0) { + throw new ConfigurationException("Cannot use -write-files with standard input", null, -1); + } + for (File inputFile : inputFiles) { + if (!inputFile.canWrite()) { + throw new ConfigurationException("File is read-only: " + inputFile.getPath(), null, -1); + } } } + return true; + } catch (ConfigurationException e) { + final ICompilerProblem problem = new ConfigurationProblem(e); + problems.add(problem); + return false; + } catch (Exception e) { + final ICompilerProblem problem = new ConfigurationProblem(null, -1, -1, -1, -1, e.getMessage()); + problems.add(problem); + return false; } - return true; } private void addDirectory(File inputFile) { diff --git a/formatter/src/main/java/org/apache/royale/formatter/config/CommandLineConfigurator.java b/formatter/src/main/java/org/apache/royale/formatter/config/CommandLineConfigurator.java new file mode 100644 index 0000000..a42960f --- /dev/null +++ b/formatter/src/main/java/org/apache/royale/formatter/config/CommandLineConfigurator.java @@ -0,0 +1,607 @@ +/* + * + * 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.formatter.config; + +import java.util.Arrays; +import java.util.List; +import java.util.LinkedList; +import java.util.TreeSet; +import java.util.Set; +import java.util.Iterator; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; +import java.io.File; + +import com.google.common.base.Joiner; + +import org.apache.royale.compiler.Messages; +import org.apache.royale.compiler.exceptions.ConfigurationException; +import org.apache.royale.compiler.internal.config.localization.LocalizationManager; + +/** + * A utility class, which is used to parse an array of command line args and + * populate a ConfigurationBuffer. It also contains some associated methods like + * brief() and usage(). A counterpart of FileConfigurator and + * SystemPropertyConfigurator. + */ +public class CommandLineConfigurator +{ + public static final String SOURCE_COMMAND_LINE = "command line"; + + /** + * parse - buffer up configuration vals from the command line + * + * @param buffer the configuration buffer to hold the results + * @param defaultvar the variable name where the trailing loose args go + * @param args the command line + */ + public static void parse(final ConfigurationBuffer buffer, + final String defaultvar, + final String[] args) + throws ConfigurationException + { + // "no-default-arg" means the application does not have a default var. + assert defaultvar == null || buffer.isValidVar(defaultvar) || "no-default-arg".equals(defaultvar) : "coding error: config must provide default var " + defaultvar; + + Map<String, String> aliases = getAliases(buffer); + final int START = 1; + final int ARGS = 2; + final int EXEC = 3; + final int DONE = 4; + + int i = 0, iStart = 0, iEnd = 0; + String var = null; + int varArgCount = -2; + List<String> argList = new LinkedList<String>(); + Set<String> vars = new HashSet<String>(); + boolean append = false; + boolean dash = true; + + int mode = START; + + while (mode != DONE) + { + switch (mode) + { + case START: + { + iStart = i; + + if (args.length == i) + { + mode = DONE; + break; + } + // expect -var, --, or the beginning of default args + + mode = ARGS; + varArgCount = -2; + + if (args[i].equals("--")) + { + dash = false; + if (defaultvar != null) + var = defaultvar; + else + mode = START; + ++i; + } + else if (dash && args[i].startsWith("+")) + { + String token = null; + int c = (args[i].length() > 1 && args[i].charAt(1) == '+') ? 2 : 1; // gnu-style? + + int equals = args[i].indexOf('='); + String rest = null; + if (equals != -1) + { + rest = args[i].substring(equals + 1); + token = args[i++].substring(c, equals); + } + else + { + token = args[i++].substring(c); + } + if (equals != -1) + { + iEnd = i; + buffer.setToken(token, rest); + buffer.addPosition(token, iStart, iEnd); + } + else + { + if (i == args.length) + { + throw new ConfigurationException.Token(ConfigurationException.Token.INSUFFICIENT_ARGS, + token, var, source, -1); + } + rest = args[i++]; + iEnd = i; + buffer.setToken(token, rest); + buffer.addPosition(token, iStart, iEnd); + } + mode = START; + break; + } + else if (dash && isAnArgument(args[i])) + { + int c = (args[i].length() > 1 && args[i].charAt(1) == '-') ? 2 : 1; // gnu-style? + + int plusequals = args[i].indexOf("+="); + int equals = args[i].indexOf('='); + String rest = null; + if (plusequals != -1) + { + rest = args[i].substring(plusequals + 2); + var = args[i++].substring(c, plusequals); + append = true; + } + else if (equals != -1) + { + rest = args[i].substring(equals + 1); + var = args[i++].substring(c, equals); + } + else + { + var = args[i++].substring(c); + } + + if (aliases.containsKey(var)) + var = aliases.get(var); + + if (!buffer.isValidVar(var)) + { + throw new ConfigurationException.UnknownVariable(var, source, -1); + } + + if (equals != -1) + { + if ((rest == null) || (rest.length() == 0)) + { + varArgCount = -1; + mode = EXEC; + } + else + { + String seps = null; + if (buffer.getInfo(var).isPath()) + { + seps = "[," + File.pathSeparatorChar + "]"; + } + else + { + seps = ","; + } + + String[] tokens = rest.split(seps); + argList.addAll(Arrays.asList(tokens)); + varArgCount = buffer.getVarArgCount(var); + mode = EXEC; + } + } + + } + else + { + // asdoc sets default var as no-default-arg - it has no default vars + if (defaultvar != null && !defaultvar.equals("no-default-arg")) + { + // don't increment i, let ARGS pick it up. + var = defaultvar; + } + else + { + throw new ConfigurationException.UnexpectedDefaults(null, null, -1); + } + } + break; + } + case ARGS: + { + if (varArgCount == -2) + { + if (isBoolean(buffer, var)) + { + varArgCount = 0; + mode = EXEC; + break; + } + else + { + varArgCount = buffer.getVarArgCount(var); + } + } + assert varArgCount >= -1; // just in case the getVarArgCount author was insane. + + if (args.length == i) + { + mode = EXEC; + break; + } + + boolean greedy = buffer.getInfo(var).isGreedy(); + + // accumulating non-command arguments... + + // check for a terminator on our accumulated parameter list + if (!greedy && dash && isAnArgument(args[i])) + { + if (varArgCount == -1) + { + // we were accumulating an unlimited set of args, a new var terminates that. + mode = EXEC; + break; + } + throw new ConfigurationException.IncorrectArgumentCount(varArgCount, argList.size(), var, source, -1); + } + + argList.add(args[i++]); + if (argList.size() == varArgCount) + { + mode = EXEC; + } + + break; + } + case EXEC: + { + if ((varArgCount != -1) && (argList.size() != varArgCount)) + { + throw new ConfigurationException.IncorrectArgumentCount(varArgCount, argList.size(), var, source, -1); + } + if (varArgCount == 0) // boolean flag fakery... + argList.add("true"); + + if (vars.contains(var)) + { + if ((defaultvar != null) && var.equals(defaultvar)) + { + // we could perhaps accumulate the defaults spread out through + // the rest of the flags, but for now we'll call this illegal. + throw new ConfigurationException.InterspersedDefaults(var, source, -1); + } + } + iEnd = i; + buffer.setVar(var, new LinkedList<String>(argList), source, -1, null, append); + buffer.addPosition(var, iStart, iEnd); + append = false; + vars.add(var); + argList.clear(); + mode = START; + break; + } + case DONE: + { + assert false; + break; + } + } + } + } + + /** + * Given a string like "-foo" or "-5" or "-123.mxml", this determines + * whether the string is an argument or... not an argument (e.g. numeral) + */ + private static boolean isAnArgument(final String arg) + { + return (arg.startsWith("-") && + // if the first character after a dash is numeric, this is not + // an argument, it is a parameter (and therefore non-terminating) + (arg.length() > 1) && !Character.isDigit(arg.charAt(1))); + } + + private static Map<String, String> getAliases(ConfigurationBuffer buffer) + { + Map<String, String> aliases = new HashMap<String, String>(); + aliases.putAll(buffer.getAliases()); + for (final String varname : buffer.getVars()) + { + if (varname.indexOf('.') == -1) + continue; + + String leafname = varname.substring(varname.lastIndexOf('.') + 1); + if (aliases.containsKey(leafname)) + continue; + aliases.put(leafname, varname); + } + + return aliases; + } + + private static boolean isBoolean(ConfigurationBuffer buffer, String var) + { + ConfigurationInfo info = buffer.getInfo(var); + + if (info.getArgCount() > 1) + return false; + + Class<?> c = info.getArgType(0); + + return ((c == boolean.class) || (c == Boolean.class)); + } + + public static String brief(String program, String defaultvar, LocalizationManager l10n, String l10nPrefix) + { + Map<String, Object> params = new HashMap<String, Object>(); + params.put("defaultVar", defaultvar); + params.put("program", program); + return l10n.getLocalizedTextString(l10nPrefix + ".Brief", params); + } + + public static String usage(String program, String defaultVar, ConfigurationBuffer cfgbuf, Set<String> keywords, LocalizationManager lmgr, String l10nPrefix) + { + boolean isCompc = program.contains("compc"); + Map<String, String> aliases = getAliases(cfgbuf); + Map<String, String> sesaila = new HashMap<String, String>(); + for (Iterator<Map.Entry<String, String>> it = aliases.entrySet().iterator(); it.hasNext();) + { + Map.Entry<String, String> e = it.next(); + sesaila.put(e.getValue(), e.getKey()); + } + + TreeSet<String> printSet = new TreeSet<String>(); + + boolean all = false; + boolean advanced = false; + boolean details = false; + boolean syntax = false; + boolean printaliases = false; + + // figure out behavior.. + Set<String> newSet = new HashSet<String>(); + for (Iterator<String> kit = keywords.iterator(); kit.hasNext();) + { + String keyword = kit.next(); + + if (keyword.equals("list")) + { + all = true; + newSet.add("*"); + } + else if (keyword.equals("advanced")) + { + advanced = true; + if (keywords.size() == 1) + { + all = true; + newSet.add("*"); + } + } + else if (keyword.equals("details")) + { + details = true; + } + else if (keyword.equals("syntax")) + { + syntax = true; + } + else if (keyword.equals("aliases")) + { + printaliases = true; + } + else + { + details = true; + newSet.add(keyword); + } + } + if (syntax) + { + final List<String> lines = ConfigurationBuffer.formatText(getSyntaxDescription(program, defaultVar, advanced, lmgr, l10nPrefix), 78); + return Joiner.on("\n").join(lines); + } + keywords = newSet; + + // accumulate set to print + for (Iterator<String> kit = keywords.iterator(); kit.hasNext();) + { + String keyword = kit.next().toLowerCase(); + + for (final String var : cfgbuf.getVars()) + { + ConfigurationInfo info = cfgbuf.getInfo(var); + + // If the client is not "compc", skip "compc-only" options. + if (info.isCompcOnly && !isCompc) + continue; + + String description = getDescription(cfgbuf, var, lmgr, l10nPrefix); + + if ((all + || (var.indexOf(keyword) != -1) + || ((description != null) && (description.toLowerCase().indexOf(keyword) != -1)) + || (keyword.matches(var)) + || ((sesaila.get(var) != null) && (sesaila.get(var)).indexOf(keyword) != -1)) + && (!info.isHidden()) + && (!info.isRemoved()) + && (advanced || !info.isAdvanced())) + { + if (printaliases && sesaila.containsKey(var)) + printSet.add(sesaila.get(var)); + else + printSet.add(var); + } + else + { + /* + * for (int i = 0; i < info.getAliases().length; ++i) { + * String alias = info.getAliases()[i]; if (alias.indexOf( + * keyword ) != -1) { printSet.add( var ); } } + */ + } + } + } + + StringBuilder output = new StringBuilder(1024); + + if (printSet.size() == 0) + { + String nkm = lmgr.getLocalizedTextString(l10nPrefix + ".NoKeywordsMatched"); + output.append(nkm); + output.append("\n"); + } + else + for (Iterator<String> it = printSet.iterator(); it.hasNext();) + { + String avar = it.next(); + String var = avar; + if (aliases.containsKey(avar)) + var = aliases.get(avar); + + ConfigurationInfo info = cfgbuf.getInfo(var); + assert info != null; + + output.append("-"); + output.append(avar); + + int count = cfgbuf.getVarArgCount(var); + if ((count >= 1) && (!isBoolean(cfgbuf, var))) + { + for (int i = 0; i < count; ++i) + { + output.append(" <"); + output.append(cfgbuf.getVarArgName(var, i)); + output.append(">"); + } + } + else if (count == -1) + { + String last = ""; + for (int i = 0; i < 5; ++i) + { + String argname = cfgbuf.getVarArgName(var, i); + if (!argname.equals(last)) + { + output.append(" ["); + output.append(argname); + output.append("]"); + last = argname; + } + else + { + output.append(" [...]"); + break; + } + } + } + + output.append("\n"); + + if (details) + { + StringBuilder description = new StringBuilder(160); + if (printaliases) + { + if (aliases.containsKey(avar)) + { + String fullname = lmgr.getLocalizedTextString(l10nPrefix + ".FullName"); + description.append(fullname); + description.append(" -"); + description.append(aliases.get(avar)); + description.append("\n"); + } + } + else if (sesaila.containsKey(var)) + { + String alias = lmgr.getLocalizedTextString(l10nPrefix + ".Alias"); + description.append(alias); + description.append(" -"); + description.append(sesaila.get(var)); + description.append("\n"); + } + + String d = getDescription(cfgbuf, var, lmgr, l10nPrefix); + if (var.equals("help") && (printSet.size() > 2)) + { + String helpKeywords = lmgr.getLocalizedTextString(l10nPrefix + ".HelpKeywords"); + description.append(helpKeywords); + } + else if (d != null) + description.append(d); + + String flags = ""; + if (info.isAdvanced()) + { + String advancedString = lmgr.getLocalizedTextString(l10nPrefix + ".Advanced"); + flags += (((flags.length() == 0) ? " (" : ", ") + advancedString); + } + if (info.allowMultiple()) + { + String repeatableString = lmgr.getLocalizedTextString(l10nPrefix + ".Repeatable"); + flags += (((flags.length() == 0) ? " (" : ", ") + repeatableString); + } + if ((defaultVar != null) && var.equals(defaultVar)) + { + String defaultString = lmgr.getLocalizedTextString(l10nPrefix + ".Default"); + flags += (((flags.length() == 0) ? " (" : ", ") + defaultString); + } + if (info.isRoyaleOnly()) + { + String royaleOnlylString = Messages.getString("RoyaleOnly"); + flags += (((flags.length() == 0) ? " (" : ", ") + royaleOnlylString); + } + if (flags.length() != 0) + { + flags += ")"; + } + description.append(flags); + + List<String> descriptionLines = ConfigurationBuffer.formatText(description.toString(), 70); + + for (final String next : descriptionLines) + { + output.append(" "); + output.append(next); + output.append("\n"); + } + } + } + return output.toString(); + } + + public static String getDescription(ConfigurationBuffer buffer, String var, LocalizationManager l10n, String l10nPrefix) + { + String key = (l10nPrefix == null) ? var : (l10nPrefix + "." + var); + String description = l10n.getLocalizedTextString(key, null); + + return description; + } + + public static String getSyntaxDescription(String program, String defaultVar, boolean advanced, LocalizationManager l10n, String l10nPrefix) + { + Map<String, Object> params = new HashMap<String, Object>(); + params.put("defaultVar", defaultVar); + params.put("program", program); + + String key = l10nPrefix + "." + (advanced ? "AdvancedSyntax" : "Syntax"); + String text = l10n.getLocalizedTextString(key, params); + + if (text == null) + { + text = "No syntax help available, try '-help list' to list available configuration variables."; + assert false : "Localized text for syntax description not found!"; + } + return text; + } + + public static final String source = SOURCE_COMMAND_LINE; +} diff --git a/formatter/src/main/java/org/apache/royale/formatter/config/Configuration.java b/formatter/src/main/java/org/apache/royale/formatter/config/Configuration.java new file mode 100644 index 0000000..3f33aec --- /dev/null +++ b/formatter/src/main/java/org/apache/royale/formatter/config/Configuration.java @@ -0,0 +1,204 @@ +/* + * + * 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.formatter.config; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.royale.compiler.exceptions.ConfigurationException; +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.InfiniteArguments; +import org.apache.royale.compiler.internal.config.annotations.Mapping; +import org.apache.royale.compiler.problems.DeprecatedConfigurationOptionProblem; +import org.apache.royale.compiler.problems.ICompilerProblem; +import org.apache.royale.compiler.problems.RemovedConfigurationOptionProblem; + +public class Configuration { + + private static Map<String, String> aliases = null; + + public static Map<String, String> getAliases() + { + if (aliases == null) + { + aliases = new HashMap<String, String>(); + + aliases.put("w", "write-files"); + aliases.put("l", "list-files"); + } + return aliases; + } + + /** + * Collection of fatal and non-fatal configuration problems. + */ + private Collection<ICompilerProblem> configurationProblems = new ArrayList<ICompilerProblem>(); + + /** + * Get the configuration problems. This should be called after the configuration has been processed. + * + * @return a collection of fatal and non-fatal configuration problems. + */ + public Collection<ICompilerProblem> getConfigurationProblems() + { + return configurationProblems; + } + + /** + * Validate configuration options values. + * + * @param configurationBuffer Configuration buffer. + * @throws ConfigurationException Error. + */ + public void validate(ConfigurationBuffer configurationBuffer) throws ConfigurationException + { + // process the merged configuration buffer. right, can't just process the args. + processDeprecatedAndRemovedOptions(configurationBuffer); + } + + private void processDeprecatedAndRemovedOptions(ConfigurationBuffer configurationBuffer) + { + for (final String var : configurationBuffer.getVars()) + { + ConfigurationInfo info = configurationBuffer.getInfo(var); + List<ConfigurationValue> values = configurationBuffer.getVar(var); + if (values != null) + { + for (final ConfigurationValue cv : values) + { + if (info.isRemoved()) + { + addRemovedConfigurationOptionProblem(cv); + } + else if (info.isDeprecated() && configurationBuffer.getVar(var) != null) + { + String replacement = info.getDeprecatedReplacement(); + String since = info.getDeprecatedSince(); + DeprecatedConfigurationOptionProblem problem = new DeprecatedConfigurationOptionProblem(var, + replacement, since, cv.getSource(), cv.getLine()); + configurationProblems.add(problem); + } + } + } + } + } + + /** + * Add a RemovedConfigurationOptionProblem to the list of configuration problems. + * + * @param cv + */ + private void addRemovedConfigurationOptionProblem(ConfigurationValue cv) + { + RemovedConfigurationOptionProblem problem = new RemovedConfigurationOptionProblem(cv.getVar(), cv.getSource(), + cv.getLine()); + configurationProblems.add(problem); + } + + // + // 'help' option from CommandLineConfiguration + // + + /** + * dummy, just a trigger for help text + */ + @Config(displayed = false, greedy = true) + @Arguments("keyword") + @InfiniteArguments + public void setHelp(ConfigurationValue cv, String[] keywords) + { + + } + + // + // 'version' option from CommandLineConfiguration + // + + /** + * Dummy option. Just a trigger for version info. + */ + @Config + public void setVersion(ConfigurationValue cv, boolean value) + { + } + + // + // 'files' option + // + + private List<String> files = new ArrayList<String>(); + + /** + * @return A list of filespecs. It's the default variable for command line. + */ + public List<String> getFiles() + { + return files; + } + + @Config(allowMultiple = true, hidden = true) + @Mapping("files") + @Arguments(Arguments.PATH_ELEMENT) + @InfiniteArguments + public void setFiles(ConfigurationValue cv, List<String> args) throws ConfigurationException + { + this.files.addAll(args); + } + + // + // 'write-files' option + // + + private boolean writeFiles = false; + + public boolean getWriteFiles() + { + return writeFiles; + } + + @Config + @Mapping("write-files") + public void setWriteFiles(ConfigurationValue cv, boolean b) + { + this.writeFiles = b; + } + + // + // 'list-files' option + // + + private boolean listFiles = false; + + public boolean getListFiles() + { + return listFiles; + } + + @Config + @Mapping("list-files") + public void setListFiles(ConfigurationValue cv, boolean b) + { + this.listFiles = b; + } +} diff --git a/formatter/src/main/java/org/apache/royale/formatter/config/ConfigurationBuffer.java b/formatter/src/main/java/org/apache/royale/formatter/config/ConfigurationBuffer.java new file mode 100644 index 0000000..35a6e37 --- /dev/null +++ b/formatter/src/main/java/org/apache/royale/formatter/config/ConfigurationBuffer.java @@ -0,0 +1,1346 @@ +/* + * + * 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.formatter.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(); + } +} diff --git a/formatter/src/main/java/org/apache/royale/formatter/config/ConfigurationInfo.java b/formatter/src/main/java/org/apache/royale/formatter/config/ConfigurationInfo.java new file mode 100644 index 0000000..53836a7 --- /dev/null +++ b/formatter/src/main/java/org/apache/royale/formatter/config/ConfigurationInfo.java @@ -0,0 +1,473 @@ +/* + * + * 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.formatter.config; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.apache.royale.compiler.internal.config.annotations.DefaultArgumentValue; +import com.google.common.base.Joiner; +import com.google.common.base.MoreObjects; + +/** + * Meta information for each configuration options. It is created by + * {@link ConfigurationBuffer#loadCache} from either annotations or + * {@code public static ConfigurationInfo getFooInfo();} methods in + * {@link Configuration} class. + */ +public class ConfigurationInfo +{ + public static final int NOT_SET = -2; + public static final int INFINITE_ARGS = -1; + + /** + * This ctor is used when everything can be introspected off the setter + * method, or else when the names/types are provided by method overrides + * rather than ctor arguments + */ + public ConfigurationInfo() + { + this.argcount = NOT_SET; + this.argnames = null; + } + + /** + * Simple ctor for restricting the number of arguments. + * + * @param argcount number of args, -1 for an infinite list + */ + public ConfigurationInfo(int argcount) + { + this.argcount = argcount; + this.argnames = null; + } + + /** + * Simple ctor for naming the arguments. + * + * @param argnames list of argnames, argcount will default to # of elements + */ + public ConfigurationInfo(String argnames[]) + { + this.argcount = argnames.length; + this.argnames = argnames; + } + + /** + * Use this ctor when you want to set a single list of some number of + * identically named args + * + * @param argcount number of arguments (-1 for infinite) + * @param argname name of each argument + */ + public ConfigurationInfo(int argcount, String argname) + { + this.argcount = argcount; + this.argnames = new String[] {argname}; + } + + /** + * More unusual ctor, this would let you have the first few args named one + * thing, the rest named something else. It is far more likely that you want + * a constrained list of names or else an arbitrary list of identical names. + * + * @param argcount number of arguments + * @param argnames array of argument names + */ + public ConfigurationInfo(int argcount, String argnames[]) + { + this.argcount = argcount; + this.argnames = argnames; + } + + public final int getArgCount() + { + return argcount; + } + + protected int argcount = NOT_SET; + + protected String[] defaultArgValues = null; + + /** + * Get any default values for an argument + * + * @return an array of default argument values. May be null + */ + public final String[] getDefaultArgValues() + { + return defaultArgValues; + } + + private static 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(); + } + + /** + * Return the name of each parameter. The default implementation is usually + * sufficient for simple cases, but one could do wacky things here like + * support an infinite list of alternating arg names. + * + * @param argnum The argument number. + * @return name of argument + */ + public String getArgName(int argnum) + { + if (argNameGeneratorClass != null) + { + Method getArgNameMethod; + try + { + getArgNameMethod = argNameGeneratorClass.getMethod("getArgumentName", int.class); + return (String)getArgNameMethod.invoke(null, argnum); + } + catch (Exception e) + { + // TODO: connect these exception to our problem logging subsystem. + e.printStackTrace(); + } + + return ""; + } + + if ((argnames == null) || (argnames.length == 0)) + { + return classToArgName(getArgType(argnum)); + } + else if (argnum >= argnames.length) + { + return argnames[argnames.length - 1]; + } + else + { + return argnames[argnum]; + } + } + + /** + * Return the type of each parameter. This is computed based on your setter, + * and cannot be overridden + * + * @param argnum The argument number. + */ + public final Class<?> getArgType(int argnum) + { + if (argnum >= argtypes.length) + { + return argtypes[argtypes.length - 1]; + } + else + { + return argtypes[argnum]; + } + } + + protected Class<?> argNameGeneratorClass; + protected String[] argnames; + protected Class<?>[] argtypes; + + protected String[] prerequisites = null; + + /** + * Return variable names that should be set before this one. The buffer is + * always set such that it tries to set all variables at a given level + * before setting child values, but you could override by using this. Its + * probably a bad idea to depend on children, though. It is unnecessary to + * set parent vars as prerequisites, since they are implicitly set first + */ + public String[] getPrerequisites() + { + return prerequisites; + } + + protected String[] softPrerequisites = null; + + /** + * Prerequisites which should be set before this one if they exist + */ + public String[] getSoftPrerequisites() + { + return softPrerequisites; + } + + protected boolean allowMultiple = false; + + /** + * Variables are generally only allowed to be set once in a given + * file/cmdline. It is sometimes useful to allow the same set multiple times + * in order to aggregate values. + * + * @return true if the setter can be called multiple times + */ + public boolean allowMultiple() + { + return allowMultiple; + } + + protected String[] aliases = null; + + /** + * Return an array of other names for this variable. + */ + public String[] getAliases() + { + return aliases; + } + + protected boolean isAdvanced = false; + + /** + * Override to make a variable hidden by default (i.e. you need -advanced on + * the cmdline) + */ + public boolean isAdvanced() + { + return isAdvanced; + } + + protected boolean isHidden = false; + + /** + * Override to make a variable completely hidden + */ + public boolean isHidden() + { + return isHidden; + } + + protected boolean isDisplayed = true; + + /** + * Override to prevent printing when dumping configuration + */ + public boolean isDisplayed() + { + return isDisplayed; + } + + /** + * If a variable -must- be set, override this + */ + public boolean isRequired() + { + return isRequired; + } + + protected boolean isRequired = false; + + /** + * Magic used by the command line configurator only at the moment to decide + * whether this variable should eat all subsequent arguments. Useful for + * -help... + */ + public boolean isGreedy() + { + return isGreedy; + } + + protected boolean isGreedy = false; + + public boolean isPath() + { + return isPath; + } + + protected boolean isPath = false; + + public boolean doChecksum() + { + return true; + } + + public String getDeprecatedMessage() + { + return deprecatedMessage; + } + + protected String deprecatedMessage = null; + + public boolean isDeprecated() + { + return isDeprecated; + } + + protected boolean isDeprecated = false; + + public String getDeprecatedReplacement() + { + return deprecatedReplacement; + } + + protected String deprecatedReplacement; + + public String getDeprecatedSince() + { + return deprecatedSince; + } + + protected String deprecatedSince; + + /** + * @return True indicates that the option is no longer + * supported and will not have any affect. + */ + public boolean isRemoved() + { + return isRemoved; + } + + protected boolean isRemoved = false; + + /** + * @return True the option requires Flex in order to be useful. + */ + public boolean isRoyaleOnly() + { + return isRoyaleOnly; + } + + protected boolean isRoyaleOnly = false; + + protected final void setSetterMethod(Method setter) + { + Class<?>[] pt = setter.getParameterTypes(); + + assert (pt.length >= 2) : ("coding error: config setter must take at least 2 args!"); + + this.setter = setter; + + if (pt.length == 2) + { + Class<?> c = pt[1]; + + if (ConfigurationBuffer.isSupportedListType(c)) + { + if (argcount == NOT_SET) + argcount = -1; // infinite list + + argtypes = new Class[] {String.class}; + return; + } + else if (ConfigurationBuffer.isSupportedValueType(c)) + { + assert (argcount == NOT_SET) : ("coding error: value object setter cannot override argcount"); + assert (argnames == null) : ("coding error: value object setter cannot override argnames"); + + Field[] fields = c.getFields(); + + argcount = fields.length; + + assert (argcount > 0) : ("coding error: " + setter + " value object " + c.getName() + " must contain at least one public field"); + + argnames = new String[fields.length]; + argtypes = new Class[fields.length]; + + for (int f = 0; f < fields.length; ++f) + { + argnames[f] = ConfigurationBuffer.c2h(fields[f].getName()); + argtypes[f] = fields[f].getType(); + } + return; + } + } + + assert ((argcount == NOT_SET) || (argcount == pt.length - 1)) : ("coding error: the argument count must match the number of setter arguments"); + // We've taken care of lists and value objects, from here on out, it must match the parameter list. + + argcount = pt.length - 1; + + DefaultArgumentValue defaultArgValuesAnno = setter.getAnnotation(DefaultArgumentValue.class); + if (defaultArgValuesAnno != null) + defaultArgValues = defaultArgValuesAnno.value(); + + argtypes = new Class[pt.length - 1]; + for (int i = 1; i < pt.length; ++i) + { + assert (ConfigurationBuffer.isSupportedSimpleType(pt[i])) : ("coding error: " + setter.getClass().getName() + "." + setter.getName() + " parameter " + i + " is not a supported type!"); + argtypes[i - 1] = pt[i]; + } + } + + protected final Method getSetterMethod() + { + return setter; + } + + private Method setter; + private Method getter; + + protected final void setGetterMethod(Method getter) + { + this.getter = getter; + } + + protected final Method getGetterMethod() + { + return getter; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper("") + .add("alias", arrayAsString(getAliases())) + .add("argcount", getArgCount()) + .add("argnames", arrayAsString(argnames)) + .add("argtypes", arrayAsString(argtypes)) + .add("deprecated", isDeprecated()) + .add("deprecatedMessage", getDeprecatedMessage()) + .add("deprecatedReplacement", getDeprecatedReplacement()) + .add("deprecatedSince", getDeprecatedSince()) + .add("getter", getGetterMethod() == null ? "null" : getGetterMethod().getName()) + .add("setter", getSetterMethod() == null ? "null" : getSetterMethod().getName()) + .add("required", isRequired()) + .add("Prerequisites", arrayAsString(getPrerequisites())) + .add("softPrerequisites", arrayAsString(getSoftPrerequisites())) + .add("advanced", isAdvanced()) + .add("allow multiple", allowMultiple()) + //.add("doChecksum", doChecksum()) + .add("displayed", isDisplayed()) + .add("greedy", isGreedy()) + .add("hidden", isHidden()) + .add("removed", isRemoved()) + .add("path", isPath()) + .toString(); + } + + private String arrayAsString(Object[] array) + { + if (array == null) + return ""; + else + return "[" + Joiner.on(",").join(array) + "]"; + } + + /** + * True if only {@code compc} client can use this option. + */ + public boolean isCompcOnly = false; +} diff --git a/formatter/src/main/java/org/apache/royale/formatter/config/ConfigurationValue.java b/formatter/src/main/java/org/apache/royale/formatter/config/ConfigurationValue.java new file mode 100644 index 0000000..a768560 --- /dev/null +++ b/formatter/src/main/java/org/apache/royale/formatter/config/ConfigurationValue.java @@ -0,0 +1,109 @@ +/* + * + * 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.formatter.config; + +import java.util.List; +import java.util.LinkedList; + + +/** + * This class represents an instance of a configuration option. For + * example, "-debug=true". + */ +public class ConfigurationValue +{ + protected ConfigurationValue( ConfigurationBuffer buffer, String var, List<String> args, String source, int line, String context ) + { + this.buffer = buffer; + this.var = var; + this.args = new LinkedList<String>( args ); + this.source = source; + this.line = line; + this.context = context; + } + + /** + * getArgs + * + * @return list of values provided, in schema order + */ + public final List<String> getArgs() + { + return args; + } + + /** + * getBuffer + * + * @return a handle to the associated buffer holding this value + */ + public final ConfigurationBuffer getBuffer() + { + return buffer; + } + + /** + * getSource + * + * @return a string representing the origin of this value, or null if unknown + */ + public final String getSource() + { + return source; + } + + /** + * getLine + * + * @return the line number of the origin of this value, or -1 if unknown + */ + public final int getLine() + { + return line; + } + + /** + * getVar + * + * @return the full name of this configuration variable in the hierarchy + */ + public final String getVar() + { + return var; + } + + /** + * getContext + * + * @return the path of the enclosing context where the variable was set + * (i.e. the directory where the config file was found) + */ + public final String getContext() + { + return context; + } + + private final ConfigurationBuffer buffer; + private final String var; + private final List<String> args; + private final String source; + private final int line; + private final String context; +} diff --git a/formatter/src/main/java/org/apache/royale/formatter/config/Configurator.java b/formatter/src/main/java/org/apache/royale/formatter/config/Configurator.java new file mode 100644 index 0000000..c2abb48 --- /dev/null +++ b/formatter/src/main/java/org/apache/royale/formatter/config/Configurator.java @@ -0,0 +1,683 @@ +/* + * + * 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.formatter.config; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.royale.compiler.exceptions.ConfigurationException; +import org.apache.royale.compiler.internal.config.localization.LocalizationManager; +import org.apache.royale.compiler.internal.config.localization.ResourceBundleLocalizer; +import org.apache.royale.compiler.problems.ConfigurationProblem; +import org.apache.royale.compiler.problems.ICompilerProblem; +import org.apache.royale.utils.FilenameNormalization; +import org.apache.royale.utils.Trace; + +/** + * A class that allows a client change compiler settings and to + * configure projects and targets from those settings. + */ +public class Configurator implements Cloneable +{ + /** + * Convert file path strings to {@code File} objects. Null values are + * discarded. + * + * @param paths List of file paths + * @return List of File objects. No null values will be returned. + */ + public static List<File> toFiles(final List<String> paths) + { + final List<File> result = new ArrayList<File>(); + for (final String path : paths) + { + if (path != null) + result.add(new File(path)); + } + return result; + } + + /** + * Convert file path strings to {@code File} objects. Null values are + * discarded. + * + * @param paths List of file paths + * @return Array of File objects. No null values will be returned. + */ + public static List<File> toFileList(final List<String> paths) + { + final List<File> result = new ArrayList<File>(); + for (final String path : paths) + { + if (path != null) + result.add(FilenameNormalization.normalize(new File(path))); + } + return result; + } + + /** + * Convert {@code File} objects to {@code String}, where each {@code String} is + * the absolute file path of the file. Null values are discarded. + * + * @param files file specifications + * @return Array of File objects. No null values will be returned. + */ + public static String[] toPaths(File[] files) + { + final List<String> result = new ArrayList<String>(); + for (final File file : files) + { + if (file != null) + result.add(file.getAbsolutePath()); + } + return result.toArray(new String[0]); + } + + // Used to generate the command line + private static final String EQUALS_STRING = "="; + private static final String PLUS_EQUALS_STRING = "+="; + private static final String COMMA_STRING = ","; + private static final String PLUS_STRING = "+"; + /** + * Constructor + */ + public Configurator() + { + this(Configuration.class); + } + + /** + * Constructor + */ + public Configurator(Class<? extends Configuration> configurationClass) + { + this.configurationClass = configurationClass; + args = new LinkedHashMap<String, Object>(); + more = new LinkedHashMap<String, Object>(); + tokens = new TreeMap<String, String>(); + + isConfigurationDirty = true; + configurationDefaultVariable = IFormatterSettingsConstants.FILES; // the default variable of the configuration. + configurationProblems = new ArrayList<ICompilerProblem>(); + + // initialize the localization manager. + LocalizationManager.get().addLocalizer(new ResourceBundleLocalizer()); + } + + private ConfigurationBuffer cfgbuf; + protected Configuration configuration; + private Class<? extends Configuration> configurationClass; + + private Map<String, Object> args, more; + private String[] extras; + private String configurationDefaultVariable; + + private Map<String, String> tokens; + + private boolean isConfigurationDirty; + private boolean configurationSuccess; + protected Collection<ICompilerProblem> configurationProblems; + + // + // IConfigurator related methods + // + + public Collection<ICompilerProblem> validateConfiguration(String[] args) + { + if (args == null) + throw new NullPointerException("args may not be null"); + + List<ICompilerProblem> problems = new ArrayList<ICompilerProblem>(); + ConfigurationBuffer configurationBuffer = createConfigurationBuffer(configurationClass); + + try + { + CommandLineConfigurator.parse(configurationBuffer, null, args); + } + catch (ConfigurationException e) + { + final ICompilerProblem problem = new ConfigurationProblem(e); + problems.add(problem); + } + + return problems; + } + + public Collection<ICompilerProblem> getConfigurationProblems() + { + assert configuration != null : + "Get the configuration problems after calling getConfiguration()"; + + List<ICompilerProblem> problems = new ArrayList<ICompilerProblem>(configurationProblems.size() + + configuration.getConfigurationProblems().size()); + problems.addAll(configurationProblems); + problems.addAll(configuration.getConfigurationProblems()); + return problems; + } + + public Configuration getConfiguration() + { + processConfiguration(); + return configuration; + } + + public ConfigurationBuffer getConfigurationBuffer() + { + return cfgbuf; + } + + /** + * Create a new configuration instance. The Configurator will need to + * create a new configuration for each new configuration. For example, + * creating a new Configurator and getting the target settings will create + * a new configuration. If later on, the configuration is modified by calling + * any of the setter methods on the Configurator, then a new configuration + * will be created the next time applyToProject() or getTargetSettings() is called. + * + * The method may be overriden to allow for greater control when creating a + * custom configuration that extends the built-in configuration. + * + * @return a new configuration instance. If the custom configuration class + * cannot be created, the default configuration class will be created instead. + */ + protected Configuration createConfiguration() + { + try + { + return configurationClass.newInstance(); + } + catch (Exception e) + { + // If there is a problem initializing the configuration, then + // throw a ConfigurationException. + reportConfigurationException(new ConfigurationException.CouldNotInstantiate(configurationClass.getName())); + + // Create the default configuration so we can report configuration + // problems. + try + { + return Configuration.class.newInstance(); + } + catch (Exception e2) + { + // this should never fail + assert(false); + return null; + } + } + } + + /** + * Initialize the configuration and the configuration buffer. + */ + protected void initializeConfiguration() + { + // Create a clean configuration and configuration buffer + configuration = createConfiguration(); + cfgbuf = createConfigurationBuffer(configuration.getClass()); + } + + /** + * Create a configuration buffer. + * @param configurationClass The Configuration object + * @return the configuration buffer to use + */ + protected ConfigurationBuffer createConfigurationBuffer( + Class<? extends Configuration> configurationClass) + { + return new ConfigurationBuffer( + configurationClass, Configuration.getAliases()); + } + + /** + * Wrapper around the real processConfiguration. + * + * @return true if success, false otherwise. + */ + protected boolean processConfiguration() + { + boolean success = true; + + if (isConfigurationDirty) + { + configurationProblems.clear(); + + try + { + success = processConfiguration(getOptions(args, more, processExtras(extras))); + } + catch (ConfigurationException e) + { + reportConfigurationException(e); + success = false; + } + catch (Exception e) + { + e.printStackTrace(); + } + } + else + { + success = configurationSuccess; + } + + isConfigurationDirty = false; + configurationSuccess = success; + return success; + } + + /** + * Does all the work to set the command line arguments info the + * configuration object. + * + * @param argsArray - command line arguments + * + * @return true if successful, false otherwise. + */ + protected boolean processConfiguration(String[] argsArray) + { + initializeConfiguration(); + + boolean success = true; + + try + { + SystemPropertyConfigurator.load(cfgbuf, "royale"); + + // Parse the command line a first time, to peak at stuff like + // "royalelib" and "load-config". The first parse is thrown + // away after that and we intentionally parse a second time + // below. See note below. + CommandLineConfigurator.parse(cfgbuf, configurationDefaultVariable, argsArray); + + overrideDefaults(); + + // Return if "-version" is present so the command line can print the + // version. + if (cfgbuf.getVar("version") != null) + return false; + + // Return so the command line can print help if "-help" is present. + final List<ConfigurationValue> helpVar = cfgbuf.getVar("help"); + if (helpVar != null) + return false; + + // The command line needs to take precedence over all defaults and config files. + // By simply re-merging the command line back on top, + // we will get the behavior we want. + cfgbuf.clearSourceVars(CommandLineConfigurator.source); + CommandLineConfigurator.parse(cfgbuf, configurationDefaultVariable, argsArray); + + // commit() reports problems instead of throwing an exception. This + // allows us to process all the options in a configuration that + // are correct in the hopes that it will be enough to configure a + // project. + if (!cfgbuf.commit(configuration, configurationProblems)) + success = false; + + configuration.validate(cfgbuf); + } + catch (ConfigurationException e) + { + reportConfigurationException(e); + success = false; + } + + return success; + } + + /** + * Override default values. + */ + protected void overrideDefaults() throws ConfigurationException + { + } + + /** + * Convert conifguration exceptions to problems and collect them for + * reporting. + * + * @param e + */ + protected void reportConfigurationException(ConfigurationException e) + { + final ICompilerProblem problem = new ConfigurationProblem(e); + configurationProblems.add(problem); + } + + // + // Configuration related methods + // + protected String[] getOptions(Map<String, Object> args, Map<String, Object> more, + String[] extras) + { + ArrayList<String> buffer = new ArrayList<String>(); + + for (Map.Entry<String, String> tokenEntry : tokens.entrySet()) + { + buffer.add(PLUS_STRING + tokenEntry.getKey() + EQUALS_STRING + tokenEntry.getValue()); + } + + for (Map.Entry<String, Object> arg : args.entrySet()) + { + String key = arg.getKey(); + Object value = arg.getValue(); + + if (value instanceof Boolean) + { + buffer.add(key + EQUALS_STRING + value); + } + else if (value instanceof Number) + { + buffer.add(key); + buffer.add(value.toString()); + } + else if (value instanceof String) + { + if (!"".equals(value)) + { + buffer.add(key); + buffer.add((String)value); + } + else + { + buffer.add(key + EQUALS_STRING); + } + } + else if (value instanceof File) + { + String p = ((File) value).getPath(); + if (!"".equals(p)) + { + buffer.add(key); + buffer.add(p); + } + else + { + buffer.add(key + EQUALS_STRING); + } + } + else if (value instanceof java.util.Date) + { + buffer.add(key); + buffer.add(value.toString()); + } + else if (value instanceof Map) + { + @SuppressWarnings("unchecked") + Map<String, ?> m = (Map<String, ?>) value; + for (Map.Entry<String, ?>entry : m.entrySet()) + { + String k = entry.getKey(); + Object v = entry.getValue(); + + if (v instanceof String) + { + buffer.add(key); + buffer.add(k); + buffer.add((String)v); + } + else if (v instanceof File) + { + buffer.add(key); + buffer.add(k); + buffer.add(((File) v).getPath()); + } + else if (v instanceof Collection) + { + buffer.add(key); + buffer.add(k); + Collection<?> list = (Collection<?>)v; + for (Object next : list) + { + if (next != null) + buffer.add(next.toString()); + } + } + else if (v != null) + { + assert false; + } + } + } + else if (value instanceof int[]) + { + int[] a = (int[]) value; + buffer.add(key); + buffer.add(String.valueOf(a[0])); + buffer.add(String.valueOf(a[1])); + } + else if (value instanceof Collection) + { + Collection<Object> list = new LinkedList<Object>((Collection<?>)args.get(key)); + + int length = list.size(); + if (length > 0) + { + buffer.add(key); + } + for (Object obj : list) + { + if (obj instanceof String) + { + buffer.add((String)obj); + } + else if (obj instanceof File) + { + buffer.add(((File)obj).getPath()); + } + } + } + else if (value != null) + { + assert false; + } + else + { + // System.err.println("unprocessed compiler options: " + key + EQUALS_STRING + value); + } + } + + for (Map.Entry<String, Object> moreEntry : more.entrySet()) + { + String key = moreEntry.getKey(); + Object value = moreEntry.getValue(); + + if (value instanceof Collection) + { + buffer.add(key + PLUS_EQUALS_STRING + toCommaSeparatedString((Collection<?>)value)); + } + else if (value instanceof Map) + { + @SuppressWarnings("unchecked") + Map<String, ?> m = (Map<String, ?>) value; + for (Map.Entry<String, ?>entry : m.entrySet()) + { + String k = entry.getKey(); + Object v = entry.getValue(); + + if (v instanceof Collection) + { + buffer.add(key + PLUS_EQUALS_STRING + k + COMMA_STRING + + toCommaSeparatedString((Collection<?>)v)); + } + else if (v != null) + { + assert false; + } + } + } + else if (value != null) + { + assert false; + } + else + { + // System.err.println("unprocessed compiler options: " + key + EQUALS_STRING + value); + } + } + + // Append extra command line args to the buffer. + if (extras != null && extras.length > 0) + { + for (int i = 0, length = extras == null ? 0 : extras.length; i < length; i++) + { + if (extras[i] != null) + { + buffer.add(extras[i]); + } + } + } + + String[] options = new String[buffer.size()]; + buffer.toArray(options); + + if (Trace.config) + Trace.trace("Configurator: options = " + buffer.toString()); + + return options; + } + + protected String[] processExtras(String[] extraOptions) throws ConfigurationException + { + return extraOptions; + } + + /** + * Sets the configuration parameters. The input should be valid <code>mxmlc/compc</code> command-line arguments.<p> + * + * @param args <code>mxmlc/compc</code> command-line arguments + * @param defaultVariable the default variable of the configuration. + */ + public void setConfiguration(String[] args, String defaultVariable) + { + extras = args; + configurationDefaultVariable = defaultVariable; + isConfigurationDirty = true; + } + + /** + * Defines a token. mxmlc and compc support token substitutions. For example, + * + * <pre> + * mxmlc +royalelib=path1 +foo=bar --var=${foo} + * </pre> + * + * <code>var=bar</code> after the substitution of <code>${foo}</code>. + * + * @param name The name of the token. + * @param value The value of the token. + */ + public void setToken(String name, String value) + { + tokens.put(name, value); + + isConfigurationDirty = true; + } + + /** + * + */ + @Override + public String toString() + { + String[] options; + try + { + options = getOptions(args, more, processExtras(extras)); + } + catch (ConfigurationException e) + { + options = new String[0]; + } + + StringBuilder b = new StringBuilder(); + for (int i = 0; i < options.length; i++) + { + b.append(options[i]); + b.append(' '); + } + return b.toString(); + } + + private String toCommaSeparatedString(Collection<?> values) + { + StringBuilder b = new StringBuilder(); + int length = values.size(); + int i = 0; + for (Object value : values) + { + String valueString = null; + + if (value instanceof String) + { + valueString = (String)value; + } + else if (value instanceof File) + { + valueString = ((File)value).getPath(); + } + + if (valueString != null) + b.append(valueString); + + if (i++ < length - 1) + { + b.append(COMMA_STRING); + } + } + return b.toString(); + } + + @Override + public Configurator clone() + { + Configurator cloneConfig; + + try + { + cloneConfig = (Configurator) super.clone(); + } + catch ( CloneNotSupportedException e ) + { + throw new RuntimeException(e);//wont happen + } + + cloneConfig.args = new LinkedHashMap<String, Object>(args); + cloneConfig.more = new LinkedHashMap<String, Object>(more); + cloneConfig.tokens = new LinkedHashMap<String, String>(tokens); + cloneConfig.configurationClass = configurationClass; + cloneConfig.isConfigurationDirty = true; + + if (extras != null) + { + cloneConfig.extras = new String[extras.length]; + System.arraycopy(extras, 0, cloneConfig.extras, 0, extras.length); + } + + return cloneConfig; + } +} diff --git a/formatter/src/main/java/org/apache/royale/formatter/config/IFormatterSettingsConstants.java b/formatter/src/main/java/org/apache/royale/formatter/config/IFormatterSettingsConstants.java new file mode 100644 index 0000000..6650a90 --- /dev/null +++ b/formatter/src/main/java/org/apache/royale/formatter/config/IFormatterSettingsConstants.java @@ -0,0 +1,24 @@ +/* + * + * 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.formatter.config; + +public interface IFormatterSettingsConstants { + static final String FILES = "files"; +} diff --git a/formatter/src/main/java/org/apache/royale/formatter/config/SystemPropertyConfigurator.java b/formatter/src/main/java/org/apache/royale/formatter/config/SystemPropertyConfigurator.java new file mode 100644 index 0000000..b74fae0 --- /dev/null +++ b/formatter/src/main/java/org/apache/royale/formatter/config/SystemPropertyConfigurator.java @@ -0,0 +1,82 @@ +/* + * + * 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.formatter.config; + +import java.util.Properties; +import java.util.Enumeration; +import java.util.List; +import java.util.LinkedList; +import java.util.StringTokenizer; + +import org.apache.royale.compiler.exceptions.ConfigurationException; + +/** + * A utility class, which is used to load configuration options via + * system properties and populate a ConfigurationBuffer. A + * counterpart of CommandLineConfigurator and FileConfigurator. + */ +public class SystemPropertyConfigurator +{ + /** + * Opportunistically find some configuration settings in system properties. + * @param buffer the intermediate config buffer + * @param prefix an optional prefix to add to the variable, pass null if no prefix + */ + public static void load( final ConfigurationBuffer buffer, String prefix ) throws ConfigurationException + { + try + { + Properties props = System.getProperties(); + + for (Enumeration<?> e = props.propertyNames(); e.hasMoreElements();) + { + String propname = (String) e.nextElement(); + + if (!propname.startsWith( prefix + ".")) + { + String value = System.getProperty( propname ); + buffer.setToken( propname, value ); + continue; + } + + String varname = propname.substring( prefix.length() + 1 ); + + if (!buffer.isValidVar( varname )) + continue; + + String value = System.getProperty( propname ); + + List<String> args = new LinkedList<String>(); + StringTokenizer t = new StringTokenizer( value, "," ); + + while (t.hasMoreTokens()) + { + String token = t.nextToken(); + args.add( token ); + } + buffer.setVar( varname, args, "system properties", -1 ); + } + } + catch (SecurityException se) + { + // just ignore, this is an optional for loading configuration + } + } +}
