package net.jangaroo.exml.parser; import net.jangaroo.exml.api.ExmlcException; import net.jangaroo.exml.compiler.Exmlc; import net.jangaroo.exml.json.Code; import net.jangaroo.exml.json.JsonArray; import net.jangaroo.exml.json.JsonObject; import net.jangaroo.exml.model.AnnotationAt; import net.jangaroo.exml.model.ConfigAttribute; import net.jangaroo.exml.model.ConfigClass; import net.jangaroo.exml.model.ConfigClassRegistry; import net.jangaroo.exml.model.ConfigClassType; import net.jangaroo.exml.model.Declaration; import net.jangaroo.exml.model.ExmlModel; import net.jangaroo.exml.model.PublicApiMode; import net.jangaroo.exml.utils.ExmlUtils; import net.jangaroo.jooc.util.PreserveLineNumberHandler; import net.jangaroo.jooc.Jooc; import net.jangaroo.jooc.ast.ApplyExpr; import net.jangaroo.utils.AS3Type; import net.jangaroo.utils.CompilerUtils; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import static net.jangaroo.jooc.util.PreserveLineNumberHandler.getLineNumber; public final class ExmlToModelParser { private static final String EXT_CONFIG_PACKAGE = "ext.config"; private static final String CONFIG_MODE_AT_SUFFIX = "$at"; private static final String EXT_CONTAINER_CONFIG_QNAME = "ext.config.container"; private static final String EXT_CONTAINER_DEFAULTS_PROPERTY = "defaults"; private static final String EXT_CONTAINER_DEFAULT_TYPE_PROPERTY = "defaultType"; private final ConfigClassRegistry registry; public ExmlToModelParser(ConfigClassRegistry registry) { this.registry = registry; } /** * Parses the exml file into an ExmlModel * @param file the file to parse * @return the parsed model * @throws IOException if the input stream could not be read * @throws SAXException if the XML was not well-formed */ public ExmlModel parse(File file) throws IOException, SAXException { ExmlModel model = new ExmlModel(); String qName = CompilerUtils.qNameFromFile(registry.getConfig().findSourceDir(file), file); String className = CompilerUtils.className(qName); model.setClassName(ExmlUtils.createComponentClassName(className)); ConfigClass configClassByName = registry.getConfigClassByName(registry.getConfig().getConfigClassPackage() + "." + ConfigClass.createConfigClassName(className)); model.setConfigClass(configClassByName); model.setPackageName(CompilerUtils.packageName(qName)); BufferedInputStream inputStream = null; try { inputStream = new BufferedInputStream(new FileInputStream(file)); parse(inputStream, model); } finally { if (inputStream != null) { inputStream.close(); } } return model; } /** * Parse the input stream content into a model. * Close the input stream after reading. * * @param inputStream the input stream * @param model the model * @throws IOException if the input stream could not be read * @throws SAXException if the XML was not well-formed */ private void parse(InputStream inputStream, ExmlModel model) throws IOException, SAXException { Document document = buildDom(inputStream); Node root = document.getFirstChild(); validateRootNode(root); NamedNodeMap attributes = root.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Attr attribute = (Attr) attributes.item(i); //baseClass attribute has been specified, so the super class of the component is actually that if (Exmlc.EXML_BASE_CLASS_ATTRIBUTE.equals(attribute.getLocalName())) { model.setSuperClassName(attribute.getValue()); } else if (Exmlc.EXML_PUBLIC_API_ATTRIBUTE.equals(attribute.getLocalName())) { PublicApiMode publicApiMode = Exmlc.parsePublicApiMode(attribute.getValue()); switch (publicApiMode) { case TRUE: model.addAnnotation(Jooc.PUBLIC_API_INCLUSION_ANNOTATION_NAME); // fall through! case CONFIG: model.getConfigClass().addAnnotation(Jooc.PUBLIC_API_INCLUSION_ANNOTATION_NAME); } } } NodeList childNodes = root.getChildNodes(); Element componentNode = null; for (int i = 0; i < childNodes.getLength(); i++) { Node node = childNodes.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { if (ExmlUtils.isExmlNamespace(node.getNamespaceURI())) { Element element = (Element)node; if (Exmlc.EXML_IMPORT_NODE_NAME.equals(node.getLocalName())) { String importedClassName = element.getAttribute(Exmlc.EXML_IMPORT_CLASS_ATTRIBUTE); if (importedClassName == null || importedClassName.equals("")) { int lineNumber = getLineNumber(componentNode); throw new ExmlcException("<exml:import> element must contain a non-empty class attribute", lineNumber); } model.addImport(importedClassName); } else if (Exmlc.EXML_ANNOTATION_NODE_NAME.equals(node.getLocalName())) { AnnotationAt annotationAt = Exmlc.parseAnnotationAtValue(element.getAttribute(Exmlc.EXML_ANNOTATION_AT_ATTRIBUTE)); if (annotationAt != AnnotationAt.CONFIG) { model.addAnnotation(element.getTextContent()); } } else if (Exmlc.EXML_CONSTANT_NODE_NAME.equals(node.getLocalName())) { String constantTypeName = element.getAttribute(Exmlc.EXML_DECLARATION_TYPE_ATTRIBUTE); model.addImport(constantTypeName); } else if (Exmlc.EXML_DESCRIPTION_NODE_NAME.equals(node.getLocalName())) { model.setDescription(node.getTextContent()); } else if (Exmlc.EXML_VAR_NODE_NAME.equals(node.getLocalName())) { Declaration var = createDeclaration(element, model); if (!model.getVars().contains(var)) { model.addVar(var); } } else if (Exmlc.EXML_CFG_NODE_NAME.equals(node.getLocalName())) { String cfgName = element.getAttribute(Exmlc.EXML_CFG_NAME_ATTRIBUTE); String cfgType = element.getAttribute(Exmlc.EXML_CFG_TYPE_ATTRIBUTE); String cfgDefault = element.getAttribute(Exmlc.EXML_CFG_DEFAULT_ATTRIBUTE); Element defaultValueElement = findChildElement(element, Exmlc.EXML_NAMESPACE_URI, Exmlc.EXML_CFG_DEFAULT_NODE_NAME); if (cfgDefault.length() != 0 && defaultValueElement != null) { throw new ExmlcException("<exml:cfg> default value must be specified as either an attribute or a sub-element, not both for config '" + cfgName + "'.", getLineNumber(element)); } if (cfgDefault.length() > 0) { model.getCfgDefaults().set(cfgName, getAttributeValue(cfgDefault, cfgType)); model.addImport(cfgType); } else if (defaultValueElement != null) { model.getCfgDefaults().set(cfgName, parseValue(model, "Array".equals(cfgType), getChildElements(defaultValueElement))); } } } else { if (componentNode != null) { int lineNumber = getLineNumber(componentNode); throw new ExmlcException("root node of EXML contained more than one component definition", lineNumber); } componentNode = (Element)node; } } } if (componentNode == null) { return; } String superFullClassName = createFullConfigClassNameFromNode(componentNode); if (superFullClassName.equals(model.getConfigClass().getFullName())) { int lineNumber = getLineNumber(componentNode); throw new ExmlcException("Cyclic inheritance error: super class and this component are the same.", lineNumber); } ConfigClass superConfigClass = getConfigClassByName(superFullClassName, componentNode); String superComponentClassName = superConfigClass.getComponentClassName(); if (model.getSuperClassName() == null) { model.setSuperClassName(superComponentClassName); } //but we still need the import model.addImport(superComponentClassName); fillModelAttributes(model, model.getJsonObject(), componentNode, superConfigClass); } private Declaration createDeclaration(Element element, ExmlModel model) { final String name = element.getAttribute(Exmlc.EXML_DECLARATION_NAME_ATTRIBUTE); String type = element.getAttribute(Exmlc.EXML_DECLARATION_TYPE_ATTRIBUTE); if (type == null || type.isEmpty()) { type = AS3Type.ANY.toString(); } final NodeList valueElements = element.getElementsByTagNameNS(Exmlc.EXML_NAMESPACE_URI, Exmlc.EXML_DECLARATION_VALUE_NODE_NAME); final Object valueObject; boolean addTypeCast = false; if (valueElements.getLength() > 0) { if (element.hasAttribute(Exmlc.EXML_DECLARATION_VALUE_ATTRIBUTE)) { throw new ExmlcException("The value of <exml:var> or <exml:const> '\" + name + \"' must be specified as either an attribute or a sub-element, not both.", getLineNumber(element)); } Element valueElement = (Element)valueElements.item(0); final List<Element> valueChildElements = getChildElements(valueElement); if (valueChildElements.isEmpty()) { valueObject = getAttributeValue(valueElement.getTextContent(), type); } else { valueObject = parseValue(model, "Array".equals(type), valueChildElements); addTypeCast = !AS3Type.ANY.toString().equals(type) && !ApplyExpr.isCoerceFunction(type); } } else { valueObject = getAttributeValue(element.getAttribute(Exmlc.EXML_DECLARATION_VALUE_ATTRIBUTE), type); } String value = JsonObject.valueToString(valueObject, 2, 4); if (addTypeCast) { value = type + "(" + value + ")"; } return new Declaration(name, value, type); } private String createFullConfigClassNameFromNode(Node componentNode) { String name = componentNode.getLocalName(); String uri = componentNode.getNamespaceURI(); String packageName = uri == null ? null : ExmlUtils.parsePackageFromNamespace(uri); if (packageName == null) { int lineNumber = getLineNumber(componentNode); throw new ExmlcException("namespace '" + uri + "' of element '" + name + "' in EXML file does not denote a config package", lineNumber); } return packageName + "." + name; } private void fillModelAttributes(ExmlModel model, JsonObject jsonObject, Element componentNode, ConfigClass configClass) { NamedNodeMap attributes = componentNode.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Attr attribute = (Attr) attributes.item(i); String attributeName = attribute.getLocalName(); String attributeValue = attribute.getValue(); ConfigAttribute configAttribute = getCfgByName(configClass, attributeName); jsonObject.set(attributeName, getAttributeValue(attributeValue, configAttribute == null ? null : configAttribute.getType())); } fillModelAttributesFromSubElements(model, jsonObject, componentNode, configClass); } public static Object getAttributeValue(String attributeValue, String type) { if (ExmlUtils.isCodeExpression(attributeValue)) { return JsonObject.code(ExmlUtils.getCodeExpression(CompilerUtils.denormalizeAttributeValue(attributeValue))); } else { AS3Type as3Type = type == null ? AS3Type.ANY : AS3Type.typeByName(type); if (AS3Type.ANY.equals(as3Type)) { as3Type = CompilerUtils.guessType(attributeValue); } if (as3Type != null) { switch (as3Type) { case BOOLEAN: return Boolean.parseBoolean(attributeValue); case NUMBER: return Double.parseDouble(attributeValue); case UINT: case INT: return Long.parseLong(attributeValue); case ARRAY: return new JsonArray(attributeValue); } } } // code expression, Object or specific type. We don't care (for now). return attributeValue; } private static final Map<String, Code> CONFIG_MODE_TO_AT_VALUE = new HashMap<String, Code>(); static { CONFIG_MODE_TO_AT_VALUE.put("append", JsonObject.code("net.jangaroo.ext.Exml.APPEND")); CONFIG_MODE_TO_AT_VALUE.put("prepend", JsonObject.code("net.jangaroo.ext.Exml.PREPEND")); } private void fillModelAttributesFromSubElements(ExmlModel model, JsonObject jsonObject, Element componentNode, ConfigClass configClass) { List<Element> childNodes = getChildElements(componentNode); for (Element element : childNodes) { String elementName = element.getLocalName(); if (ExmlUtils.isExmlNamespace(element.getNamespaceURI()) && "mixins".equals(elementName)) { for (Element mixinElement : getChildElements(element)) { String mixinClassName = createFullConfigClassNameFromNode(mixinElement); ConfigClass mixinConfigClass = getConfigClassByName(mixinClassName, mixinElement); fillModelAttributes(model, jsonObject, mixinElement, mixinConfigClass); } continue; } boolean isConfigTypeArray = isConfigTypeArray(configClass, elementName); String configMode = isConfigTypeArray ? element.getAttribute(Exmlc.EXML_MODE_ATTRIBUTE) : ""; // Special case: if an EXML element representing a config property has attributes, it is treated as // having an untyped object value. Exception: it is an Array-typed property and the sole attribute is "mode". int attributeCount = element.getAttributes().getLength(); if (attributeCount > 1 || attributeCount == 1 && configMode.length() == 0) { // it's an untyped complex object with attributes and sub-properties parseJavaScriptObjectProperty(jsonObject, element); } else { // derive the corresponding "at" value from the specified config mode (if any): Code atValue = CONFIG_MODE_TO_AT_VALUE.get(configMode); if (atValue != null) { isConfigTypeArray = true; if (!configClass.isExmlGenerated()) { throw new ExmlcException("Non-EXML class " + configClass.getComponentClassName() + " does not support config modes.", getLineNumber(element)); } } String directTextContent = getDirectTextContent(element); if (directTextContent != null) { ConfigAttribute configAttribute = getCfgByName(configClass, elementName); final Object attributeValue = getAttributeValue(directTextContent, configAttribute == null ? null : configAttribute.getType()); jsonObject.set(elementName, attributeValue); } else { // it seems to be an array or an object fillJsonObjectProperty(model, jsonObject, elementName, isConfigTypeArray, getChildElements(element)); ifContainerDefaultsThenExtractXtype(jsonObject, configClass, elementName); } // if any "at" value is specified, set the extra mode attribute (...$at): if (atValue != null) { jsonObject.set(elementName + CONFIG_MODE_AT_SUFFIX, atValue); } // empty properties are omitted if the type is not fixed to Array } } } private static String getDirectTextContent(Element element) { final NodeList innerChildNodes = element.getChildNodes(); for (int i = 0; i < innerChildNodes.getLength(); i++) { final Node innerChildNode = innerChildNodes.item(i); if (innerChildNode.getNodeType() == Node.TEXT_NODE) { String directTextContent = ((Text) innerChildNode).getWholeText(); if (directTextContent.trim().length() > 0) { return directTextContent; } } } return null; } private void ifContainerDefaultsThenExtractXtype(JsonObject jsonObject, ConfigClass configClass, String elementName) { // special case: extract xtype from <defaults> as <defaultType>! if (EXT_CONTAINER_DEFAULTS_PROPERTY.equals(elementName) && isContainerConfig(configClass)) { Object value = jsonObject.get(elementName); if (value instanceof JsonObject) { JsonObject jsonObjectValue = (JsonObject) value; // there are two ways an xtype can be specified: // a) as an EXML config class wrapping the JSON object: String xtype = jsonObjectValue.getWrapperClass(); if (xtype != null) { jsonObjectValue.settingWrapperClass(null); } else { // b) as an "xtype" attribute: xtype = (String) jsonObjectValue.remove(ConfigClassType.COMPONENT.getType()); } if (xtype != null) { jsonObject.set(EXT_CONTAINER_DEFAULT_TYPE_PROPERTY, xtype); } } } } private boolean isContainerConfig(ConfigClass configClass) { return configClass != null && (EXT_CONTAINER_CONFIG_QNAME.equals(configClass.getFullName()) || isContainerConfig(configClass.getSuperClass())); } private boolean isConfigTypeArray(ConfigClass configClass, String propertyName) { ConfigAttribute configAttribute = getCfgByName(configClass, propertyName); return configAttribute != null && "Array".equals(configAttribute.getType()); } private void fillJsonObjectProperty(ExmlModel model, JsonObject jsonObject, String propertyName, boolean configTypeArray, List<Element> childElements) { Object value = parseValue(model, configTypeArray, childElements); jsonObject.set(propertyName, value); } private Object parseValue(ExmlModel model, boolean configTypeArray, List<Element> childElements) { Object value; List<Object> childObjects = parseChildObjects(model, childElements); if (childObjects.size() > 1 || configTypeArray) { // TODO: Check for type violation // We must write an array. value = new JsonArray(childObjects.toArray()); } else if (childObjects.size() == 1) { // The property is either unspecified, untyped, or object-typed // and it contains a single child element. Use that element as the // property value. value = childObjects.get(0); } else { value = null; } return value; } private ConfigClass getConfigClassByName(String className, Node errorNode) { ConfigClass configClass = registry.getConfigClassByName(className); if (configClass == null) { int lineNumber = getLineNumber(errorNode); throw new ExmlcException("unknown type '" + className + "'", lineNumber); } return configClass; } private void parseJavaScriptObjectProperty(JsonObject jsonObject, Element propertyElement) { JsonObject propertyObject = new JsonObject(); setUntypedAttributes(propertyElement, propertyObject); for (Element child : getChildElements(propertyElement)) { parseJavaScriptObjectProperty(propertyObject, child); } jsonObject.set(propertyElement.getLocalName(), propertyObject); } private static Element findChildElement(Element element, String namespace, String nodeName) { for (Element child : getChildElements(element)) { if (namespace.equals(child.getNamespaceURI()) && nodeName.equals(child.getLocalName())) { return child; } } return null; } private static List<Element> getChildElements(Element element) { List<Element> result = new ArrayList<Element>(); NodeList propertyChildNotes = element.getChildNodes(); for (int j = 0; j < propertyChildNotes.getLength(); j++) { Node childNode = propertyChildNotes.item(j); if (childNode.getNodeType() == Node.ELEMENT_NODE) { result.add((Element) childNode); } } return result; } private ConfigAttribute getCfgByName(ConfigClass configClass, String attributeName) { ConfigClass current = configClass; while (current != null) { ConfigAttribute configAttribute = current.getCfgByName(attributeName); if (configAttribute != null) { return configAttribute; } String superClassName = current.getSuperClassName(); if (superClassName == null || superClassName.equals("Object")) { break; } current = registry.getConfigClassByName(superClassName); } return null; } private List<Object> parseChildObjects(ExmlModel model, List<Element> elements) { List<Object> childObjects = new ArrayList<Object>(); for (Element arrayItemNode : elements) { Object value; if (ExmlUtils.isExmlNamespace(arrayItemNode.getNamespaceURI()) && Exmlc.EXML_OBJECT_NODE_NAME.equals(arrayItemNode.getLocalName())) { value = parseExmlObjectNode(arrayItemNode); } else { String arrayItemClassName = createFullConfigClassNameFromNode(arrayItemNode); ConfigClass configClass = getConfigClassByName(arrayItemClassName, arrayItemNode); JsonObject arrayItemJsonObject = new JsonObject(); if (configClass.getType().getExtTypeAttribute() != null && EXT_CONFIG_PACKAGE.equals(configClass.getPackageName())) { // Ext classes are always loaded. We can use the type string directly. arrayItemJsonObject.set(configClass.getType().getExtTypeAttribute(), configClass.getTypeValue()); } else if (configClass.getType().isCreatedViaConfigObject()) { arrayItemJsonObject.settingWrapperClass(configClass.getFullName()); model.addImport(configClass.getFullName()); model.addImport(configClass.getComponentClassName()); } else { // Everything not a component, plugin or layout must be created immediately // by using net.jangaroo.ext.create() with its configClass and the config: arrayItemJsonObject.settingConfigClass(configClass.getFullName()); model.addImport(configClass.getFullName()); model.addImport(configClass.getComponentClassName()); model.addImport(JsonObject.NET_JANGAROO_EXT_CREATE); } fillModelAttributes(model, arrayItemJsonObject, arrayItemNode, configClass); value = arrayItemJsonObject; } if (value != null) { childObjects.add(value); } } return childObjects; } private Object parseExmlObjectNode(Node exmlObjectNode) { String textContent = exmlObjectNode.getTextContent(); if (textContent != null && textContent.length() > 0) { return JsonObject.code(textContent.trim()); } else { if (!exmlObjectNode.hasAttributes()) { return null; } JsonObject object = new JsonObject(); setUntypedAttributes(exmlObjectNode, object); return object; } } private void setUntypedAttributes(Node exmlObjectNode, JsonObject object) { NamedNodeMap attributes = exmlObjectNode.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Attr attribute = (Attr) attributes.item(i); String attributeName = attribute.getLocalName(); String attributeValue = attribute.getValue(); object.set(attributeName, getAttributeValue(attributeValue, null)); } } private void validateRootNode(Node root) { int lineNumber = getLineNumber(root); if (!ExmlUtils.isExmlNamespace(root.getNamespaceURI())) { throw new ExmlcException("root node of EXML file must belong to namespace '" + Exmlc.EXML_NAMESPACE_URI + "', but was '" + root.getNamespaceURI() + "'", lineNumber); } if (!Exmlc.EXML_ROOT_NODE_NAMES.contains(root.getLocalName())) { throw new ExmlcException("Root node of EXML file must be one of " + Arrays.toString(Exmlc.EXML_ROOT_NODE_NAMES.toArray()) + ", but was " + root.getLocalName() + ".", lineNumber); } } private Document buildDom(InputStream inputStream) throws SAXException, IOException { SAXParser parser; final Document doc; try { final SAXParserFactory saxFactory = SAXParserFactory.newInstance(); saxFactory.setNamespaceAware(true); parser = saxFactory.newSAXParser(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); doc = factory.newDocumentBuilder().newDocument(); } catch (ParserConfigurationException e) { throw new IllegalStateException("a default dom builder should be provided", e); } PreserveLineNumberHandler handler = new PreserveLineNumberHandler(doc); parser.parse(inputStream, handler); return doc; } }