All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tp23.xgen.XGen Maven / Gradle / Ivy

There is a newer version: 1.0.3
Show newest version
/*
 * Copyright teknopaul 2014 LGPL
 */
package org.tp23.xgen;

import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Iterator;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;

import org.tp23.xgen.path.XGenPath;
import org.tp23.xgen.path.XGenPathStep;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Contains the core xGen functionality for generating XML.
 * 
 * Also contains convenience methods for evaluating XPaths and Serializing the XML.
 * 
 * This class is not thread safe there should be one instance per Document to be created.
 * 
 * @author teknopaul
 *
 */
public class XGen {

	private String outputEncoding = "UTF-8";
	private String outputIndent = "yes";
	private String outputMethod = "xml";
	
	private final Document document;
	
	// Constructors
	
	/**
	 * Create an XGen instance and create a Document using default JVM XML factories.
	 */
	public XGen() {
		this.document = FactoryUtils.newDocument();
	}
	
	public XGen(DocumentBuilder db) {
		this.document = db.newDocument();
	}
	
	public XGen(Document document) {
		this.document = document;
	}
	
	public XGen(Element elem) {
		this.document = elem.getOwnerDocument();
	}
	
	// New document using xGen path statement

	/**
	 * Create a new document populated with elements form the xGenpath.
	 * The path must be absolute, i.e. it must start with a / and it must have a single root
	 * element, this is a requirement of XML. The root may have attributes.
	 * This method should be called only once per lifetime of the XGen instance.
	 * 
	 * @param xGenPath 
	 * @return org.w3c.dom.Document
	 * @throws XGenExpressionException
	 */
	public XGenNodeList newDocument(String xGenPath) throws XGenExpressionException {
		if (getRoot() != null) {
			throw new IllegalStateException("Document already exists");
		}
		if ( ! xGenPath.startsWith("/")) {
			throw new XGenExpressionException("New document needs an absolute path to start");
		}
		XGenPath parsedGenPath = new XGenPath(xGenPath.substring(1), dotIsClass());
		if ( parsedGenPath.getStep().getArrayLength() != 1 ) {
			throw new XGenExpressionException("Must be only one root element");
		}
		Element root = document.createElement(parsedGenPath.getStep().getElement());
		Map atts = parsedGenPath.getStep().getAttributes();
		if (atts != null) {
			setAttributes(root, atts);
		}
		document.appendChild(root);
		
		XGenNodeList tailNodes = new XGenNodeList(this);
		XGenNodeList context = XGenNodeList.createSingleNodeList(this, root);
		create(context, parsedGenPath.next(), tailNodes);
		if (tailNodes.getLength() == 0) {
			tailNodes.add(root);
		}
		return tailNodes;
	}
	
	/**
	 * @return The root element of the document being created.
	 */
	public Element getRoot() {
		return this.document.getDocumentElement();
	}
	
	// Core XMLcreation methods
	
	/**
	 * Create XML content, and insert it as a chile of the cotent element.
	 * 
	 * @param elem where to insert the new elements
	 * @param xGenPath a xGen path string, or a whole parseable XML doc as a string.
	 * @return The tail nodes, i.e. a list of all leaf nodes created
	 * @throws XGenExpressionException
	 */
	public XGenNodeList create(Element elem, String xGenPath) throws XGenExpressionException {
		XGenNodeList tailNodes = new XGenNodeList(this);
		XGenNodeList context = XGenNodeList.createSingleNodeList(this, elem);
		create(context, xGenPath, tailNodes);
		return tailNodes;
	}
	
	/**
	 * Create XML content, and append to all nodes in the list.
	 */
	public XGenNodeList create(XGenNodeList context, String xGenPath) throws XGenExpressionException {
		XGenNodeList tailNodes = new XGenNodeList(this);
		create(context, xGenPath, tailNodes);
		return tailNodes;
	}
	
	/**
	 * Create XML content, for all nodes in the list.
	 */
	public XGenNodeList create(NodeList context, String xGenPath) throws XGenExpressionException {
		XGenNodeList tailNodes = new XGenNodeList(this);
		XGenNodeList xContext = new XGenNodeList(this);
		xContext.addElements(context);
		create(xContext, xGenPath, tailNodes);
		return tailNodes;
	}
	
	/**
	 * Create XML content, and insert it as a child of the root element.
	 * 
	 * @param xGenPath a xGen path string, or a whole parseable XML doc as a string.
	 * @return The tail nodes, i.e. a list of all leaf nodes created
	 * @throws XGenExpressionException
	 */
	public XGenNodeList create(String xGenPath) throws XGenExpressionException {
		XGenNodeList tailNodes = new XGenNodeList(this);
		XGenNodeList context = XGenNodeList.createSingleNodeList(this, getRoot());
		create(context, xGenPath, tailNodes);
		return tailNodes;
	}
	
	// Convenience methods for XPath
	
	/**
	 * Select a single element using an XPath string with the docuument root as the base.
	 * @param xPath
	 * @return org.w3c.Element
	 * @throws XPathExpressionException
	 */
	public Element selectFirst(String xPath) throws XPathExpressionException {
		return (Element)FactoryUtils.newXPath().evaluate(xPath, getRoot(), XPathConstants.NODE);
	}
	
	/**
	 * Select a list of element using an XPath string with the docuument root as the base.
	 * @param xPath
	 * @return XGenNodeList a Mutable NodeList which can be used as a context to create more elements.
	 * @throws XPathExpressionException
	 */
	public XGenNodeList select(String xPath) throws XPathExpressionException {
		NodeList nodeList = (NodeList)FactoryUtils.newXPath().evaluate(xPath, getRoot(), XPathConstants.NODESET);
		return new XGenNodeList(this, nodeList);
	}
	
	/**
	 * Select a single element using an XPath string with the supplied element as the base.
	 * @param context the element from where to start the XPath search
	 * @param xPath
	 * @return org.w3c.Element
	 * @throws XPathExpressionException
	 */
	public Element selectFirst(Element context, String xPath) throws XPathExpressionException {
		return (Element)FactoryUtils.newXPath().evaluate(xPath, context, XPathConstants.NODE);
	}
	
	/**
	 * Select a list of element using an XPath string with the supplied element as the base.
	 * @param context the element from where to start the XPath search
	 * @param xPath
	 * @return XGenNodeList a Mutable NodeList which can be used as a context to create more elements.
	 * @throws XPathExpressionException
	 */
	public XGenNodeList select(Element context, String xPath) throws XPathExpressionException {
		NodeList nodeList = (NodeList)FactoryUtils.newXPath().evaluate(xPath, context, XPathConstants.NODESET);
		return new XGenNodeList(this, nodeList);
	}

	/**
	 * Serializes the while XML document to a string.
	 */
	public String toString() {
		try {
			return serialize();
		}
		catch (TransformerException ex) {
			return "Invalid XML generation";
		} 
	}
	
	
	// Convenience methods for Serialization
	/**
	 * Set encoding used for output, usually UTF-8.
	 * @see javax.xml.transform.OutputKeys#ENCODING
	 */
	public void setOutputEncoding(String outputEncoding) {
		this.outputEncoding = outputEncoding;
	}
	
	/**
	 * Add optional whitespace, newlines and indentation to the XML output.
	 * Defaults to "yes"
	 * @see javax.xml.transform.OutputKeys#INDENT
	 */
	public void setOutputIndent(String outputIndent) {
		this.outputIndent = outputIndent;
	}

	/**
	 * Output xml or html., this also affects how period are treated in element names.
	 * When HTML periods are treated as HTML class attributes.
	 * @see javax.xml.transform.OutputKeys#METHOD
	 */
	public void setOutputMethod(String outputMethod) {
		this.outputMethod = outputMethod;
	}
	
	/**
	 * @return An XML string
	 */
	public String serialize() throws TransformerException {
		StringWriter out = new StringWriter();
		serialize(out);
		return out.getBuffer().toString();
	}
	
	public void serialize(OutputStream out) throws TransformerException {
		StreamResult streamResult = new StreamResult(out);
		serialize(streamResult);
	}
	
	public void serialize(Writer out) throws TransformerException {
		StreamResult streamResult = new StreamResult(out);
		serialize(streamResult);
	}
	
	public void serialize(StreamResult streamResult) throws TransformerException {
		if (document == null) {
			throw new IllegalStateException("Document not created yet");
		}
		DOMSource domSource = new DOMSource(document);
		TransformerFactory factory = TransformerFactory.newInstance();
		factory.setAttribute("indent-number", "2");
		Transformer serializer = factory.newTransformer();
		if (serializer.getClass().getName().contains("org.apache")) {
			serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
		}
		serializer.setOutputProperty(OutputKeys.ENCODING, this.outputEncoding);
		serializer.setOutputProperty(OutputKeys.INDENT, this.outputIndent);
		serializer.setOutputProperty(OutputKeys.METHOD, this.outputMethod);
		serializer.transform(domSource, streamResult);
	}
	
	// Private creation methods
	
	/**
	 * This method sjhould be the only mehtod that calls Element generation if
	 * parsing XML is also required. 
	 */
	private void create(XGenNodeList context, String xGenPath, XGenNodeList tailNodes) throws XGenExpressionException {
		// hackety ho hum
		if (xGenPath.startsWith("<")) {
			insert(context, xGenPath);
		} else {
			create(context, new XGenPath(xGenPath, dotIsClass()), tailNodes);
		}
	}
	
	/**
	 * Recursively add Elements to the document.
	 * 
	 * @param context  The node to which more Elements are being added
	 * @param xGenPath  The string expressions
	 * @param tailNodes  A list of nodes returned to the client code
	 * @throws XGenExpressionException
	 */
	private void create(XGenNodeList context, XGenPath xGenPath, XGenNodeList tailNodes) throws XGenExpressionException {

		XGenPathStep step = xGenPath.getStep();
		if (step == null) return;
		
		for(Node nextNode : context) {
			if (nextNode instanceof Element) {
				XGenNodeList generatedElements = createPathStep((Element)nextNode, step);
				create(generatedElements, xGenPath.next(), tailNodes);
				if (xGenPath.isTail()) {
					tailNodes.addElements(generatedElements);
				}
			}
		}
		
	}
	
	/**
	 * Recursively add a whole block of XML to the nodes.
	 */
	private void insert(XGenNodeList context, String xml) throws XGenExpressionException {
		try {
			for(Node nextNode : context) {
				Node clone = FactoryUtils.parse(xml).getDocumentElement();
				document.adoptNode(clone);
				nextNode.appendChild(clone);
			}
		} catch (DOMException e) {
			throw new XGenExpressionException("xGen path treated as XML, but was not parseable", e);
		} catch (SAXException e) {
			throw new XGenExpressionException("xGen path treated as XML, but was not parseable", e);
		} catch (IOException e) {
			throw new XGenExpressionException("xGen path treated as XML, but was not parseable", e);

		}
	}
	/**
	 * Create one step in an xGen Path e.g. if path is /html/head/body{id=index}/div[3]
	 * html, head and body are all steps
	 * @param context parent node to the element being created
	 * @param step  step syntax is element or element{att:val} or element[n]
	 */
	private XGenNodeList createPathStep(Element context, XGenPathStep step) {
		XGenNodeList generatedNodes = new XGenNodeList(this);
		for (int i = 0; i < step.getArrayLength(); i++) {
			Element element = document.createElement(step.getElement());
			context.appendChild(element);
			if (step.getAttributes() != null) {
				Iterator iter = step.getAttributes().keySet().iterator();
				while (iter.hasNext()) {
					String key = (String) iter.next();
					element.setAttribute(key, step.getAttributes().get(key));
				}
			}
			generatedNodes.add(element);
		}
		return generatedNodes;
	}
	
	private void setAttributes(Element elem, Map atts) {
		for (String key : atts.keySet()) {
			elem.setAttribute(key, atts.get(key));
		}
	}
	
	private boolean dotIsClass() {
		return "html".equals(outputMethod);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy