/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and others contributors as indicated
* by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* 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,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* (C) 2005-2006, JBoss Inc.
*/
package org.jboss.tools.smooks.templating.model.xml;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.core.runtime.Assert;
import org.eclipse.emf.common.util.URI;
import org.jboss.tools.smooks.templating.model.ModelBuilder;
import org.jboss.tools.smooks.templating.model.ModelBuilderException;
import org.jboss.tools.smooks.templating.template.xml.XMLFreeMarkerTemplateBuilder;
import org.milyn.xml.DomUtils;
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.xml.sax.SAXException;
/**
* XML Model Builder from an XML Sample.
* <p/>
* The generated model can then be used by the {@link XMLFreeMarkerTemplateBuilder}.
*
* @author <a href="mailto:tom.fennelly@jboss.com">tom.fennelly@jboss.com</a>
*/
public class XMLSampleModelBuilder extends ModelBuilder {
private static DocumentBuilder docBuilder;
private Document model;
static {
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
docBuilderFactory.setNamespaceAware(true);
try {
docBuilder = docBuilderFactory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new RuntimeException(Messages.XMLSampleModelBuilder_UnexpectedXMLException, e);
}
}
public XMLSampleModelBuilder(URI xmlSampleURI) throws IOException, ModelBuilderException {
Assert.isNotNull(xmlSampleURI, "Null 'xmlSampleURI' arg in method call."); //$NON-NLS-1$
File xmlSampleFile = new File(xmlSampleURI.toFileString());
if(!xmlSampleFile.exists()) {
throw new IOException("XML Sample '" + xmlSampleFile.getAbsolutePath() + "' not found."); //$NON-NLS-1$ //$NON-NLS-2$
} else if(!xmlSampleFile.isFile()) {
throw new IOException("XML Sample '" + xmlSampleFile.getAbsolutePath() + "' is not a normal file. Might be a directory etc."); //$NON-NLS-1$ //$NON-NLS-2$
}
try {
model = docBuilder.parse(xmlSampleFile);
} catch (SAXException e) {
throw new ModelBuilderException("Error parsing XML Sample file.", e); //$NON-NLS-1$
}
Element documentElement = model.getDocumentElement();
// The model has no metadata attached since it is based on only a sample,
// so mark it as not being a strict model...
ModelBuilder.setStrictModel(model, false);
trimNonModelNodes(documentElement);
configureModelElementTypes(documentElement);
configureModelElementCardinality(documentElement);
registerNamepsaces(documentElement);
}
public static void trimNonModelNodes(Element element) {
NodeList children = element.getChildNodes();
List<Node> removeableChildren = new ArrayList<Node>();
Set<QName> childElementSet = new HashSet<QName>();
for(int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if(child.getNodeType() == Node.ELEMENT_NODE) {
Element childElement = (Element) child;
QName childElementQName;
if(childElement.getPrefix() != null) {
childElementQName = new QName(childElement.getNamespaceURI(), childElement.getLocalName(), childElement.getPrefix());
} else {
childElementQName = new QName(childElement.getNamespaceURI(), childElement.getTagName());
}
if(!childElementSet.contains(childElementQName)) {
childElementSet.add(childElementQName);
trimNonModelNodes((Element) child);
} else {
removeableChildren.add(child);
}
} else {
removeableChildren.add(child);
}
}
for(Node child : removeableChildren) {
element.removeChild(child);
}
}
private void configureModelElementTypes(Element element) {
NodeList children = element.getChildNodes();
int childCount = children.getLength();
if(childCount > 0) {
// Has child elements, therefore it's a "complex" element type...
ModelBuilder.setElementType(element, ElementType.complex);
for(int i = 0; i < childCount; i++) {
Node child = children.item(i);
if(child.getNodeType() == Node.ELEMENT_NODE) {
configureModelElementTypes((Element) child);
} else {
throw new IllegalStateException("The configureModelElementTypes method can only be called after the model has been trimed of non-model Nodes. Call trimNonModelNodes() before calling configureModelElementTypes()."); //$NON-NLS-1$
}
}
} else {
// Has no child elements, therefore it's a "simple" element type...
ModelBuilder.setElementType(element, ElementType.simple);
}
}
private void configureModelElementCardinality(Element element) {
NodeList children = element.getChildNodes();
List<Node> removeableChildren = new ArrayList<Node>();
int childCount = children.getLength();
Map<String, Element> childElementByNames = new HashMap<String, Element>();
for(int i = 0; i < childCount; i++) {
Node child = children.item(i);
if(child.getNodeType() == Node.ELEMENT_NODE) {
Element childElement = (Element) child;
String elementName = DomUtils.getName(childElement) + ":" + childElement.getNamespaceURI(); // Yes, namespace can be null, but that's OK. //$NON-NLS-1$
Element earlierOccurance = childElementByNames.get(elementName);
// Mark every element as being optional and possibly being multiple...
ModelBuilder.setMinMax(childElement, 0, -1);
if(earlierOccurance != null) {
// According to the sample XML, this element is definitely a
// collection item because it exists more than once, so lets mark it
// such that sub mappings on this element require this collection to be mapped beforehand...
ModelBuilder.setEnforceCollectionSubMappingRules(earlierOccurance, true);
// And remove the duplicates...
removeableChildren.add(childElement);
} else {
// We've no way of knowing whether or not this element is a collection
// item or not, so lets not enforce the collection sub mapping rules...
ModelBuilder.setEnforceCollectionSubMappingRules(childElement, false);
}
configureModelElementCardinality(childElement);
} else {
throw new IllegalStateException("The configureModelElementTypes method can only be called after the model has been trimed of non-model Nodes. Call trimNonModelNodes() before calling configureModelElementTypes()."); //$NON-NLS-1$
}
}
for(Node child : removeableChildren) {
element.removeChild(child);
}
}
private void registerNamepsaces(Element element) {
NamedNodeMap attributes = element.getAttributes();
for(int i = 0; i < attributes.getLength(); i++) {
registerNamespace(attributes.item(i));
}
NodeList children = element.getChildNodes();
for(int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if(child.getNodeType() == Node.ELEMENT_NODE) {
registerNamespace(child);
registerNamepsaces((Element) child);
}
}
}
private void registerNamespace(Node node) {
String nsPrefix = node.getPrefix();
String nsURI = node.getNamespaceURI();
if(nsPrefix != null && nsURI != null) {
getNamespaces().setProperty(nsPrefix, nsURI);
}
}
public Document buildModel() throws ModelBuilderException {
return model;
}
}