blob: 89e015fe7cdfa904a6ce6c66a817391299d04692 [file] [log] [blame]
/*
*
* 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);
}
}