/* * * 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.flex.compiler.internal.config; import java.io.Reader; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.StringTokenizer; import java.util.TreeMap; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.apache.commons.io.IOUtils; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import org.apache.flex.compiler.config.ConfigurationBuffer; import org.apache.flex.compiler.config.ConfigurationInfo; import org.apache.flex.compiler.config.ConfigurationValue; import org.apache.flex.compiler.exceptions.ConfigurationException; import org.apache.flex.compiler.filespecs.IFileSpecification; import org.apache.flex.compiler.internal.config.localization.LocalizationManager; import com.google.common.collect.ImmutableSet; /** * A utility class, which is used to parse an XML file of configuration options * and populate a ConfigurationBuffer. A counterpart of CommandLineConfigurator * and SystemPropertyConfigurator. * * @see <a href="http://help.adobe.com/en_US/flex/using/WS2db454920e96a9e51e63e3d11c0bf67670-7ff2.html">Configuration file syntax</a> */ public class FileConfigurator { public static class SAXConfigurationException extends SAXParseException { private static final long serialVersionUID = -3388781933743434302L; SAXConfigurationException(ConfigurationException e, Locator locator) { super(null, locator); // ? this.innerException = e; } public ConfigurationException innerException; } /** * Load configuration XML file into a {@link ConfigurationBuffer} object. * * @param buffer result {@link ConfigurationBuffer} object. * @param fileSpec configuration XML file. * @param context path context used for resolving relative paths in the * configuration options. * @param rootElement expected root element of the XML DOM tree. * @param ignoreUnknownItems if false, unknown option will cause exception. * @throws ConfigurationException error. */ public static void load( final ConfigurationBuffer buffer, final IFileSpecification fileSpec, final String context, final String rootElement, boolean ignoreUnknownItems) throws ConfigurationException { final String path = fileSpec.getPath(); final Handler h = new Handler(buffer, path, context, rootElement, ignoreUnknownItems); final SAXParserFactory factory = SAXParserFactory.newInstance(); Reader reader = null; try { reader = fileSpec.createReader(); final SAXParser parser = factory.newSAXParser(); final InputSource source = new InputSource(reader); parser.parse(source, h); } catch (SAXConfigurationException e) { throw e.innerException; } catch (SAXParseException e) { throw new ConfigurationException.OtherThrowable(e, null, path, e.getLineNumber()); } catch (Exception e) { throw new ConfigurationException.OtherThrowable(e, null, path, -1); } finally { IOUtils.closeQuietly(reader); } } /** * SAX handler for configuration XML. */ private static class Handler extends DefaultHandler { private static final String ATTRIBUTE_APPEND = "append"; public Handler(ConfigurationBuffer buffer, String source, String contextPath, String rootElement, boolean ignoreUnknownItems) { this.cfgbuf = buffer; this.source = source; this.contextPath = contextPath; this.rootElement = rootElement; this.ignoreUnknownItems = ignoreUnknownItems; } private final Stack<ParseContext> contextStack = new Stack<ParseContext>(); private final ConfigurationBuffer cfgbuf; private final String source; private final String contextPath; private final String rootElement; private final boolean ignoreUnknownItems; private final StringBuilder text = new StringBuilder(); private Locator locator; @Override public void startElement(final String uri, final String localName, final String qname, final Attributes attributes) throws SAXException { // Verify and initialize the context stack at root element. if (contextStack.size() == 0) { if (!qname.equals(rootElement)) { throw new SAXConfigurationException( new ConfigurationException.IncorrectElement(rootElement, qname, this.source, locator.getLineNumber()), locator); } final ParseContext ctx = new ParseContext(); contextStack.push(ctx); return; } final ParseContext ctx = contextStack.peek(); if (ctx.ignore) { // ignore starting new elements return; } if (text.length() > 0) { // Only leave nodes can have CDATA as option values. throw new SAXConfigurationException( new ConfigurationException.UnexpectedCDATA(this.source, locator.getLineNumber()), locator); } final String fullname = ConfigurationBuffer.varname(qname, ctx.base); if (ctx.item != null) { throw new SAXConfigurationException( new ConfigurationException.UnexpectedElement(qname, contextPath, locator.getLineNumber()), locator); } else if (ctx.var != null) { // we're setting values for a variable if (ctx.varArgCount == 1) { // oops, we weren't expecting more than one value! throw new SAXConfigurationException( new ConfigurationException.UnexpectedElement(qname, source, locator.getLineNumber()), locator); } ctx.item = qname; } else if (cfgbuf.isValidVar(fullname)) { ctx.var = fullname; ctx.varArgCount = cfgbuf.getVarArgCount(ctx.var); ctx.append = false; final String append = attributes.getValue(ATTRIBUTE_APPEND); if (append != null) { if (append.equalsIgnoreCase("true") || append.equalsIgnoreCase("false")) ctx.append = Boolean.valueOf(append).booleanValue(); else throw new SAXConfigurationException( new ConfigurationException.BadAppendValue( ctx.var, source, locator.getLineNumber()), locator); } } else if (isSubTree(fullname)) { final ParseContext newctx = new ParseContext(); newctx.base = fullname; contextStack.push(newctx); } else { if (ignoreUnknownItems) { // push a new context and ignore everything until we get the end // of this element. ParseContext newctx = new ParseContext(); newctx.item = qname; newctx.ignore = true; contextStack.push(newctx); return; } System.err.println("Unknown tag:" + fullname); throw new SAXConfigurationException( new ConfigurationException.UnknownVariable( fullname, source, locator.getLineNumber()), locator); } } @Override public void endElement(String uri, String localName, String qname) throws SAXException { final ParseContext ctx = contextStack.peek(); if (ctx.ignore) { // if found the matching end element, then pop the context and stop ignoring input if (ctx.item.equals(qname)) { contextStack.pop(); text.setLength(0); // ignore any text read } return; } // There are four possible states here; // 1. localname==rootElement -> end of file, pop, we're done // 2. localname==itemElement -> finished gathering text, push onto arglist // 2. var is set -> set the var to the argList, pop // 3. var is null -> we're finishing a child config, pop if (qname.equals(rootElement)) { // Finished with the file! } else if (ctx.item != null) { // Finished with the current item. final ParseValue v = new ParseValue(); v.name = qname; v.value = text.toString(); v.line = locator.getLineNumber(); ctx.argList.add(v); text.setLength(0); ctx.item = null; } else if (ctx.var != null) { if ((ctx.varArgCount > 1) && (ctx.argList.size() == 0)) { throw new SAXConfigurationException( new ConfigurationException.IncorrectArgumentCount(ctx.varArgCount, 0, ctx.var, source, locator.getLineNumber()), locator); } if (ctx.varArgCount == 1) { ParseValue v = new ParseValue(); v.name = null; v.value = text.toString(); v.line = locator.getLineNumber(); ctx.argList.add(v); text.setLength(0); } else { if (text.length() > 0) { // "unexpected CDATA encountered, " + ctx.var + " requires named arguments.", locator ); throw new SAXConfigurationException( new ConfigurationException.UnexpectedCDATA(source, locator.getLineNumber()), locator); } } // Finished with the current var, save the current list try { setVar(ctx.var, ctx.argList, locator.getLineNumber(), ctx.append); ctx.var = null; ctx.argList.clear(); ctx.item = null; ctx.append = false; } catch (ConfigurationException e) { throw new SAXConfigurationException(e, locator); } } else { // done with a child config contextStack.pop(); } } public void setVar(String var, List<ParseValue> argList, int line, boolean append) throws ConfigurationException { int varArgCount = cfgbuf.getVarArgCount(var); Map<String, String> items = new HashMap<String, String>(); boolean byName = (varArgCount > 1); if (byName) { for (Iterator<ParseValue> it = argList.iterator(); it.hasNext();) { ParseValue v = it.next(); if (items.containsKey(v.name)) { byName = false; // can't support byName, duplicate item name! break; } else { items.put(v.name, v.value); } } } List<String> args = new LinkedList<String>(); if (byName) { int argc = 0; while (args.size() < items.size()) { String name = cfgbuf.getVarArgName(var, argc++); String val = items.get(name); if (val == null) { throw new ConfigurationException.MissingArgument(name, var, source, line); } args.add(val); } } else { Iterator<ParseValue> it = argList.iterator(); int argc = 0; while (it.hasNext()) { ParseValue v = it.next(); String name = cfgbuf.getVarArgName(var, argc++); if ((v.name != null) && !name.equals(v.name)) { throw new ConfigurationException.UnexpectedArgument(name, v.name, var, source, v.line); } args.add(v.value); } } cfgbuf.setVar(var, args, source, line, contextPath, append); } @Override public void characters(char ch[], int start, int length) { String chars = new String(ch, start, length).trim(); text.append(chars); } @Override public void setDocumentLocator(Locator locator) { this.locator = locator; } } private static class ParseContext { ParseContext() { this.base = null; this.var = null; this.varArgCount = -2; this.argList = new LinkedList<ParseValue>(); this.append = false; this.ignore = false; } public String var; public String base; public String item; public int varArgCount; public boolean append; public List<ParseValue> argList; public boolean ignore; // ignore this variable, do not put in config buffer } private static class ParseValue { public String name; public String value; public int line; } private static class FormatNode { public String fullname; public String shortname; public ConfigurationInfo info; public List<ConfigurationValue> values; public TreeMap<String, FormatNode> children; // only for configs } static final String pad = " "; /** * These XML nodes can have subtrees of configurations. */ protected static final ImmutableSet<String> VALID_SUBTREE_TAG = ImmutableSet.of( "compiler", "compiler.namespaces", "compiler.fonts", "compiler.fonts.languages", "compiler.mxml", "compiler.mxml.imports", "metadata", "licenses", "frames", "runtime-shared-library-settings"); /** * @param fullname * @return */ private static boolean isSubTree(String fullname) { return VALID_SUBTREE_TAG.contains(fullname); } 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(); } private static String formatBuffer1(ConfigurationBuffer cfgbuf, FormatNode node, String indent, LocalizationManager lmgr, String prefix) { StringBuilder buf = new StringBuilder(1024); buf.append(indent + "<" + node.shortname + ">\n"); if (node.children != null) { for (final String key : node.children.keySet()) { final FormatNode child = node.children.get(key); if (child.children != null) // its a config { buf.append(formatBuffer1(cfgbuf, child, indent + pad, lmgr, prefix)); } else { String description = lmgr.getLocalizedTextString(prefix + "." + child.fullname); if (description != null) buf.append(indent + pad + "<!-- " + child.fullname + ": " + description + "-->\n"); if ((child.values == null) || !child.info.isDisplayed()) { boolean newline = false; buf.append(indent + pad + "<!-- " + child.fullname + " usage:\n"); buf.append(indent + pad + "<" + child.shortname + ">"); int i = 0; while (true) { if (child.info.getArgCount() == 1) { buf.append(child.info.getArgName(i)); break; } else { buf.append("\n" + indent + pad + pad + "<" + child.info.getArgName(i) + ">" + classToArgName(child.info.getArgType(i)) + "</" + child.info.getArgName(i) + ">"); newline = true; } if (child.info.getArgCount() == -1) { if (i > 0) { // stop iterating thru arguments when an arg name // matches a previously used arg name. boolean found = false; // true if found argName in the arg list String argName = child.info.getArgName(i + 1); for (int j = i; j >= 0; j--) { if (child.info.getArgName(j).equals(argName)) { found = true; break; } } if (found) { break; } } } else if (i >= child.info.getArgCount()) { break; } ++i; } if (newline) buf.append("\n" + indent + pad); buf.append("</" + child.shortname + ">\n"); buf.append(indent + pad + "-->\n"); } else { // var may be set multiple times... boolean newline = false; for (final ConfigurationValue cv : child.values) { buf.append(indent + pad + "<" + child.shortname + ">"); int argCount = child.info.getArgCount(); // var may have multiple values... int argc = 0; for (final String arg : cv.getArgs()) { if (argCount == 1) { buf.append(arg); break; } else { String argname = child.info.getArgName(argc++); newline = true; buf.append("\n" + indent + pad + pad + "<" + argname + ">" + arg + "</" + argname + ">"); } } if (newline) buf.append("\n" + indent + pad); buf.append("</" + child.shortname + ">\n"); } } } } } buf.append(indent + "</" + node.shortname + ">\n"); return buf.toString(); } private static void addNode(ConfigurationBuffer cfgbuf, String var, FormatNode root) { String name = null; StringTokenizer t = new StringTokenizer(var, "."); FormatNode current = root; while (t.hasMoreTokens()) { String token = t.nextToken(); if (name == null) name = token; else name += "." + token; if (current.children == null) current.children = new TreeMap<String, FormatNode>(); if (isSubTree(name)) { if (!current.children.containsKey(token)) { FormatNode node = new FormatNode(); node.fullname = name; node.shortname = token; node.children = new TreeMap<String, FormatNode>(); current.children.put(token, node); current = node; } else { current = current.children.get(token); } } else if (cfgbuf.isValidVar(name)) { FormatNode node = new FormatNode(); node.fullname = name; node.shortname = token; node.info = cfgbuf.getInfo(name); node.values = cfgbuf.getVar(name); current.children.put(token, node); } } } public static String formatBuffer(ConfigurationBuffer cfgbuf, String rootElement, LocalizationManager lmgr, String prefix) { FormatNode root = new FormatNode(); root.shortname = rootElement; for (final String var : cfgbuf.getVars()) { // if var is a 'hidden' or a 'removed' parameter, don't dump. ConfigurationInfo info = cfgbuf.getInfo(var); if (info != null && (info.isHidden() || info.isRemoved() || !info.isDisplayed())) { continue; } addNode(cfgbuf, var, root); } return formatBuffer1(cfgbuf, root, "", lmgr, prefix); } public static String formatBuffer(ConfigurationBuffer cfgbuf, String rootElement) { return formatBuffer(cfgbuf, rootElement, null, null); } }