/* * $Id$ * * Copyright (C) 2003-2015 JNode.org * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; If not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.jnode.configure; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.Enumeration; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import net.n3.nanoxml.IXMLParser; import net.n3.nanoxml.StdXMLReader; import net.n3.nanoxml.XMLElement; import net.n3.nanoxml.XMLException; import net.n3.nanoxml.XMLParserFactory; import org.jnode.configure.PropertySet.Property; import org.jnode.configure.PropertySet.Value; import org.jnode.configure.Screen.Item; import org.jnode.configure.adapter.FileAdapter; /** * This class loads an XML configuration script and creates the in-memory * representation. * * @author crawley@jnode.org */ public class ScriptParser { public static final String SCRIPT = "configureScript"; public static final String BASE_DIR = "baseDir"; public static final String INCLUDE = "include"; public static final String TYPE = "type"; public static final String CONTROL_PROPS = "controlProps"; public static final String PROP_FILE = "propFile"; public static final String FILE_NAME = "fileName"; public static final String SCRIPT_FILE = "scriptFile"; public static final String DEFAULT_FILE = "defaultFile"; public static final String TEMPLATE_FILE = "templateFile"; public static final String FILE_FORMAT = "fileFormat"; public static final String MARKER = "marker"; public static final String DEFAULT_MARKER = "@"; public static final String VALIDATION_CLASS = "validationClass"; public static final String SCREEN = "screen"; public static final String CHANGED = "changed"; public static final String NAME = "name"; public static final String PATTERN = "pattern"; public static final String ALT = "alt"; public static final String VALUE = "value"; public static final String TOKEN = "token"; public static final String PROPERTY = "property"; public static final String PROMPT = "prompt"; public static final String DESCRIPTION = "description"; public static final String DEFAULT = "default"; public static final String TITLE = "title"; public static final String ITEM = "item"; public static final String GUARD_PROP = "guardProp"; public static final String VALUE_IS = "valueIs"; public static final String VALUE_IS_NOT = "valueIsNot"; public static final String EMPTY_TOKEN = "emptyToken"; public static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9.\\-_]+"); private static final Pattern LINE_SPLITTER_PATTERN = Pattern.compile("\r\n|\r(?!\n)|\n"); public static class ParseContext { private final File file; private File baseDir; private XMLElement element; public ParseContext(File file) { super(); this.file = file; this.baseDir = file.getAbsoluteFile().getParentFile(); } public XMLElement getImportElement() { return element; } void setElement(XMLElement element) { this.element = element; } public File getFile() { return file; } public File getBaseDir() { return baseDir; } public void setBaseDir(File baseDir) { this.baseDir = baseDir; } } private final LinkedList<ParseContext> stack = new LinkedList<ParseContext>(); private final Configure configure; public ScriptParser(Configure configure) { this.configure = configure; } public ConfigureScript loadScript(String fileName) throws ConfigureException { configure.verbose("Loading configure script from " + fileName); final File file = new File(fileName); stack.add(new ParseContext(file)); try { final XMLElement root = loadXML(file); configure.debug("Parsing script"); return parseScript(root, file); } finally { stack.removeLast(); } } private XMLElement loadXML(final File file) throws ConfigureException { try { final FileReader r = new FileReader(file); try { StdXMLReader xr = new StdXMLReader(r); IXMLParser parser = XMLParserFactory.createDefaultXMLParser(); parser.setReader(xr); return (XMLElement) parser.parse(); } finally { r.close(); } } catch (FileNotFoundException ex) { throw new ConfigureException("Cannot open " + file, ex); } catch (IOException ex) { throw new ConfigureException("IO error reading " + file, ex); } catch (XMLException ex) { throw new ConfigureException("XML error reading " + file, ex); } catch (Exception ex) { throw new ConfigureException("Unexpected error reading " + file, ex); } } private ConfigureScript parseScript(XMLElement root, File scriptFile) throws ConfigureException { ConfigureScript script = new ConfigureScript(scriptFile); parseScript(root, script); return script; } private void parseScript(XMLElement root, ConfigureScript script) throws ConfigureException { if (!root.getName().equals(SCRIPT)) { error("Root element of a script file should be '" + SCRIPT + "'", root); } String baseDirName = root.getAttribute(BASE_DIR, ""); if (baseDirName.length() > 0) { File baseDir = new File(stack.getLast().getBaseDir(), baseDirName); stack.getLast().setBaseDir(baseDir); } for (Enumeration<?> en = root.enumerateChildren(); en.hasMoreElements(); /**/) { XMLElement element = (XMLElement) en.nextElement(); String elementName = element.getName(); if (elementName.equals(TYPE)) { parseType(element, script); } else if (elementName.equals(CONTROL_PROPS)) { parseControlProps(element, script); } else if (elementName.equals(PROP_FILE)) { parsePropsFile(element, script); } else if (elementName.equals(SCREEN)) { parseScreen(element, script); } else if (elementName.equals(INCLUDE)) { parseInclude(element, script); } else { error("Unrecognized element '" + elementName + "'", element); } } } public File resolvePath(String fileName) { if (fileName == null) { return null; } File res = new File(fileName); if (!res.isAbsolute()) { res = new File(stack.getLast().getBaseDir(), fileName); } return res; } private void parseInclude(XMLElement element, ConfigureScript script) throws ConfigureException { String includeFileName = element.getAttribute(SCRIPT_FILE, null); if (includeFileName == null) { error("A '" + SCRIPT_FILE + "' attribute is required for an '" + INCLUDE + "' element", element); } File includeFile = resolvePath(includeFileName); XMLElement includeRoot = loadXML(includeFile); stack.getLast().setElement(element); stack.add(new ParseContext(includeFile)); try { parseScript(includeRoot, script); } finally { stack.removeLast(); } } private void parseType(XMLElement element, ConfigureScript script) throws ConfigureException { String name = element.getAttribute(NAME, null); checkName(name, NAME, TYPE, element); String patternString = element.getAttribute(PATTERN, null); List<EnumeratedType.Alternate> alternates = new LinkedList<EnumeratedType.Alternate>(); for (Enumeration<?> en = element.enumerateChildren(); en.hasMoreElements(); /**/) { XMLElement child = (XMLElement) en.nextElement(); if (!child.getName().equals(ALT)) { error("A '" + TYPE + "' element can only contain '" + ALT + "' elements", child); } String value = child.getAttribute(VALUE, null); String token = child.getAttribute(TOKEN, value); if (value == null) { error("A '" + VALUE + "' attribute is required for an '" + ALT + "' element", child); } if (token.length() == 0) { // An empty token is problematic because and empty input line is // used to say "use the default value". error("The (specified or implied) value of an '" + ALT + "' element's '" + TOKEN + "' attribute cannot be empty", child); } alternates.add(new EnumeratedType.Alternate(token, value)); } PropertyType type = null; if (patternString == null) { if (alternates.isEmpty()) { error("A '" + TYPE + "' element must have a '" + PATTERN + "' attribute or '" + ALT + "' elements", element); } else { type = new EnumeratedType(name, alternates); } } else { if (!alternates.isEmpty()) { error("A '" + TYPE + "' element cannot have both a '" + PATTERN + "' attribute and '" + ALT + "' elements", element); } else { try { Pattern pattern = Pattern.compile(patternString); String empty = element.getAttribute(EMPTY_TOKEN, null); if (empty == null) { if (pattern.matcher("").matches()) { error("An '" + EMPTY_TOKEN + "' attribute is required because the '" + PATTERN + "' attribute matches the empty string", element); } } else if (empty.length() == 0) { error("The '" + EMPTY_TOKEN + "' attribute must not be an empty string", element); } type = new PatternType(name, pattern, empty); } catch (PatternSyntaxException ex) { error("Invalid '" + PATTERN + "' attribute: " + ex.getDescription(), element); } } } script.addType(type); } private void checkName(String name, String attrName, String elementName, XMLElement element) throws ConfigureException { if (name == null) { error("A '" + attrName + "' attribute is required for a '" + elementName + "' element", element); } if (!NAME_PATTERN.matcher(name).matches()) { error("This value (" + name + ") is not a valid value for a '" + attrName + "' attribute", element); } } private void parseControlProps(XMLElement element, ConfigureScript script) throws ConfigureException { PropertySet propSet = new PropertySet(script); parseProperties(element, propSet, script); script.setControlProps(propSet); } private void parsePropsFile(XMLElement element, ConfigureScript script) throws ConfigureException { String propFileName = element.getAttribute(FILE_NAME, null); if (propFileName == null) { error("A '" + PROP_FILE + "' element requires a '" + FILE_NAME + "' attribute", element); } File propFile = resolvePath(propFileName); String defaultPropFileName = element.getAttribute(DEFAULT_FILE, null); File defaultPropFile = resolvePath(defaultPropFileName); String fileFormat = element.getAttribute(FILE_FORMAT, FileAdapter.JAVA_PROPERTIES_FORMAT); String templateFileName = element.getAttribute(TEMPLATE_FILE, null); File templateFile = resolvePath(templateFileName); String markerStr = element.getAttribute(MARKER, DEFAULT_MARKER); if (markerStr.length() != 1) { error("A '" + MARKER + "' attribute must be one character in length", element); } char marker = markerStr.charAt(0); if (marker == '\n' || marker == '\r') { error("This marker character won't work", element); } PropertySet propSet; try { propSet = new PropertySet(script, propFile, defaultPropFile, templateFile, fileFormat, marker); } catch (ConfigureException ex) { addStack(ex, element); throw ex; } parseProperties(element, propSet, script); script.addPropsFile(propSet); } private PropertySet parseProperties(XMLElement element, PropertySet propSet, ConfigureScript script) throws ConfigureException { for (Enumeration<?> en = element.enumerateChildren(); en.hasMoreElements(); /**/) { XMLElement child = (XMLElement) en.nextElement(); if (child.getName().equals(PROPERTY)) { String name = child.getAttribute(NAME, null); checkName(name, NAME, PROPERTY, child); String typeName = child.getAttribute(TYPE, null); if (name == null) { error("A '" + PROPERTY + "' element requires a '" + TYPE + "' attribute", child); } String description = child.getAttribute(DESCRIPTION, null); if (name == null) { error("A '" + PROPERTY + "' element requires a '" + DESCRIPTION + "' attribute", child); } String defaultText = child.getAttribute(DEFAULT, ""); PropertyType type = script.getTypes().get(typeName); if (type == null) { error("Use of undeclared type '" + typeName + "'", child); } Value defaultValue = type.fromValue(defaultText); configure.debug("Default value for " + name + " is " + (defaultValue == null ? "null" : defaultValue.toString())); try { propSet.addProperty(name, type, description, defaultValue, child, stack.getLast() .getFile()); } catch (ConfigureException ex) { addStack(ex, child); throw ex; } } else { error("Expected only '" + PROPERTY + "' elements in this context", element); } } return propSet; } private void parseScreen(XMLElement element, ConfigureScript script) throws ConfigureException { String title = element.getAttribute(TITLE, null); if (title == null) { error("A '" + SCREEN + "' element requires a '" + TITLE + "' attribute", element); } String guardPropName = element.getAttribute(GUARD_PROP, null); String valueIsStr = element.getAttribute(VALUE_IS, null); String valueIsNotStr = element.getAttribute(VALUE_IS_NOT, null); Value valueIs = null; Value valueIsNot = null; if (guardPropName != null) { Property guardProp = script.getProperty(guardPropName); if (guardProp == null) { error("A guard property '" + guardPropName + "' not declared", element); } if (valueIsStr != null && valueIsNotStr != null) { error("The '" + VALUE_IS + "' and '" + VALUE_IS_NOT + "' attributes cannot be used together", element); } PropertyType type = guardProp.getType(); if (valueIsStr != null) { valueIs = type.fromValue(valueIsStr); if (valueIs == null) { error("The string '" + valueIsStr + "' is not a valid " + type.getTypeName() + " instance", element); } } if (valueIsNotStr != null) { valueIsNot = type.fromValue(valueIsNotStr); if (valueIsNot == null) { error("The string '" + valueIsNotStr + "' is not a valid " + type.getTypeName() + " instance", element); } } } Screen screen = new Screen(title, guardPropName, valueIs, valueIsNot); script.addScreen(screen); for (Enumeration<?> en = element.enumerateChildren(); en.hasMoreElements(); /**/) { XMLElement child = (XMLElement) en.nextElement(); if (!child.getName().equals(ITEM)) { error("Expected an '" + ITEM + "' element", child); } String propName = child.getAttribute(PROPERTY, null); if (propName == null) { error("The '" + PROPERTY + "' attribute is required for an '" + ITEM + "' element", child); } String changed = child.getAttribute(CHANGED, null); if (script.getProperty(propName) == null) { error("Use of undeclared property '" + propName + "'", child); } screen.addItem(new Item(script, propName, unindent(child.getContent()), changed)); } } /** * Take string consisting of one or more lines of text, and "unindent" all * lines by an equal amount such that at least one line has a * non-whitespace, character as the first character. * * @param content the text to be unindented * @return the unindented text. */ private String unindent(String content) { if (content == null || content.length() == 0) { return content; } String[] lines = LINE_SPLITTER_PATTERN.split(content, -1); int minLeadingSpaces = Integer.MAX_VALUE; for (String line : lines) { int count, i; boolean seenNonWhitespace = false; int len = Math.min(minLeadingSpaces, line.length()); for (i = 0, count = 0; i < len && !seenNonWhitespace; i++) { switch (line.charAt(i)) { case ' ': count++; break; case '\t': count = ((count / Configure.TAB_WIDTH) + 1) * Configure.TAB_WIDTH; break; default: seenNonWhitespace = true; } } if (seenNonWhitespace && count < minLeadingSpaces) { minLeadingSpaces = count; } } if (minLeadingSpaces == 0 || minLeadingSpaces == Integer.MAX_VALUE) { return content; } StringBuffer sb = new StringBuffer(content.length()); for (String line : lines) { if (sb.length() > 0) { sb.append(Configure.NEW_LINE); } int i, count; int len = line.length(); for (i = 0, count = 0; i < len && count < minLeadingSpaces; i++) { switch (line.charAt(i)) { case ' ': count++; break; case '\t': count = ((count / Configure.TAB_WIDTH) + 1) * Configure.TAB_WIDTH; break; } } if (i < len) { if (count > minLeadingSpaces) { for (int j = count - minLeadingSpaces; j > 0; j--) { sb.append(' '); } } sb.append(line.substring(i)); } } return sb.toString(); } private void addStack(ConfigureException ex, XMLElement element) { stack.getLast().setElement(element); ParseContext[] stackCopy = new ParseContext[stack.size()]; stack.toArray(stackCopy); ex.setStack(stackCopy); } private void error(String message, XMLElement element) throws ConfigureException { ConfigureException ex = new ConfigureException(message); addStack(ex, element); throw ex; } }