package org.zend.php.zendserver.deployment.core.internal.descriptor;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
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;
import org.zend.php.zendserver.deployment.core.DeploymentCore;
import org.zend.php.zendserver.deployment.core.descriptor.ChangeEvent;
import org.zend.php.zendserver.deployment.core.descriptor.DeploymentDescriptorFactory;
import org.zend.php.zendserver.deployment.core.descriptor.DeploymentDescriptorPackage;
import org.zend.php.zendserver.deployment.core.descriptor.IModelContainer;
import org.zend.php.zendserver.deployment.core.descriptor.IModelObject;
public class ModelSerializer {
private static final String AT = "@"; //$NON-NLS-1$
private DocumentBuilderFactory domfactory;
private DocumentBuilder builder;
private XPath xpathObj;
private Document document;
private DocumentStore dest;
private int documentLength;
public ModelSerializer() {
domfactory = DocumentBuilderFactory.newInstance();
try {
builder = domfactory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
DeploymentCore.log(e);
}
XPathFactory factory = XPathFactory.newInstance();
xpathObj = factory.newXPath();
}
public void load(InputStream src, InputStream src2, IModelContainer model) throws XPathExpressionException, SAXException, IOException {
document = builder.parse(src);
if (src.markSupported()) {
src2 = src;
src2.reset();
}
CalculateOffsets c = new CalculateOffsets(src2);
c.traverse(document);
documentLength = c.getDocumentLength();
Node root = getNode(document, DeploymentDescriptorPackage.PACKAGE.xpath);
loadProperties(root, model);
}
public void serialize(IModelContainer model) throws XPathExpressionException, CoreException, TransformerFactoryConfigurationError, TransformerException {
serialize(model, null);
}
public void serialize(IModelContainer model, ChangeEvent event) throws XPathExpressionException, TransformerFactoryConfigurationError, TransformerException {
if (document == null) {
document = DeploymentDescriptorFactory.createEmptyDocument(builder);
}
Node root = getNode(document, DeploymentDescriptorPackage.PACKAGE.xpath);
if (root == null) {
root = addNode(document, DeploymentDescriptorPackage.PACKAGE.xpath, null);
}
writeProperties(root, model, event);
}
public void setOutput(DocumentStore dest) {
this.dest = dest;
}
public void write() throws CoreException {
if (dest == null) {
return;
}
try {
Result result = dest.getOutput();
Source source = new DOMSource(document);
Transformer xformer = TransformerFactory.newInstance().newTransformer();
xformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
xformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); //$NON-NLS-1$ //$NON-NLS-2$
xformer.transform(source, result);
dest.write();
} catch (TransformerFactoryConfigurationError e) {
throw new CoreException(new Status(IStatus.ERROR, DeploymentCore.PLUGIN_ID, e.getMessage(), e));
} catch (TransformerException e) {
throw new CoreException(new Status(IStatus.ERROR, DeploymentCore.PLUGIN_ID, e.getMessage(), e));
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, DeploymentCore.PLUGIN_ID, e.getMessage(), e));
}
}
private void loadProperties(Node doc, IModelObject obj) throws XPathExpressionException {
Feature[] props = obj.getPropertyNames();
for (Feature feature : props) {
String s;
Node propertyNode = null;
if (feature.attrName != null) {
s = getXpathString(doc, getXPath(feature.xpath, feature.attrName));
} else {
propertyNode = getNode(doc, feature.xpath);
s = propertyNode == null ? null : propertyNode.getTextContent();
}
Integer nodeOffset = (Integer) (propertyNode != null ? propertyNode.getUserData(CalculateOffsets.NODE_OFFSET) : doc.getUserData(CalculateOffsets.NODE_OFFSET));
if (nodeOffset != null) {
obj.setOffset(feature, nodeOffset.intValue());
}
String value = stripWhitespaces(s);
obj.set(feature, value);
}
if (obj instanceof IModelContainer) {
loadChildren(doc, (IModelContainer)obj);
}
}
/**
* Loads DOM model to java model adding new elements.
*
* @param doc
* @param model
* @throws XPathExpressionException
*/
private void loadChildren(Node doc, IModelContainer model) throws XPathExpressionException {
Feature[] childNames = model.getChildNames();
for (Feature c : childNames) {
Node[] nodes = getNodes(doc, c.xpath);
List<Object> children = model.getChildren(c);
if (c.type == IModelObject.class) {
for (int i = 0; i < nodes.length; i++) {
Node node = nodes[i];
IModelObject obj;
if (i < children.size()) {
obj = (IModelObject) children.get(i);
} else {
obj = DeploymentDescriptorFactory.createModelElement(c);
children.add(obj);
}
loadProperties(node, obj);
}
for (int i = nodes.length; i < children.size(); i++) {
children.remove(i);
}
} else if (c.type == String.class){
for (int i = 0; i < nodes.length; i++) {
String string = nodes[i].getTextContent();
string = stripWhitespaces(string);
if (i < children.size()) {
children.set(i, string);
} else {
children.add(string);
}
}
for (int i = nodes.length; i < children.size(); i++) {
children.remove(i);
}
} else throw new UnsupportedOperationException("Unsupported collection type "+c.type); //$NON-NLS-1$
}
}
private void writeProperties(Node doc, IModelObject obj, ChangeEvent event)
throws XPathExpressionException {
Node lastAdded = null;
if (obj.isChildrenFirst()) {
lastAdded = writeChildren(doc, (IModelContainer) obj, event, null);
}
if (event == null || event.target == obj) {
Feature[] props = obj.getPropertyNames();
for (Feature feature : props) {
String value = obj.get(feature);
if ((value == null)
|| ("".equals(value) && ((feature.flags & Feature.SET_EMPTY_TO_NULL) == Feature.SET_EMPTY_TO_NULL))) { //$NON-NLS-1$
removeString(doc, feature.xpath, feature.attrName);
} else {
lastAdded = setString(doc, feature.xpath, feature.attrName,
value, lastAdded);
}
}
}
if (!obj.isChildrenFirst() && obj instanceof IModelContainer) {
writeChildren(doc, (IModelContainer) obj, event, lastAdded);
}
}
private Node writeChildren(Node doc, IModelContainer model, ChangeEvent event, Node lastAddedNode) throws XPathExpressionException {
if (event == null || event.target == model) {
Feature[] features = model.getChildNames();
for (Feature f : features) {
Node[] nodes = getNodes(doc, f.xpath);
List<Object> children = model.getChildren(f);
for (int j = 0; j < children.size(); j++) {
Node node = null;
if (j < nodes.length) {
node = nodes[j];
}
if (f.type == IModelObject.class) {
if (node == null) {
node = addNode(doc, f.xpath, lastAddedNode);
}
writeProperties(node, (IModelObject) children.get(j), null); // don't pass event, rewriter all children of modified element
} else if (f.type == String.class) {
if (node == null) {
node = addNode(doc, f.xpath, lastAddedNode);
}
node.setTextContent((String)children.get(j));
} else {
throw new UnsupportedOperationException("Unsupported collection type "+f.type); //$NON-NLS-1$
}
if (node != null) {
lastAddedNode = node;
}
}
for (int j = children.size(); j < nodes.length; j++) {
removeNodes(doc, nodes[j]);
}
}
} else {
Feature[] features = model.getChildNames();
for (Feature f : features) {
List<Object> children = model.getChildren(f);
Node[] nodes = getNodes(doc, f.xpath);
for (int i = 0; i < Math.min(children.size(), nodes.length); i++) {
if (nodes[i] != null) {
Node node = nodes[i];
if (f.type == IModelObject.class) {
writeProperties(node, (IModelObject) children.get(i), event);
}
}
}
}
}
return lastAddedNode;
}
private Node getDirectChild(Node parent, Node child) {
Node p = child.getParentNode();
while (p != parent) {
child = p;
p = child.getParentNode();
if (p == null) {
return null;
}
}
return child;
}
private Node addNode(Node doc, String xpath, Node after) throws XPathExpressionException {
if (xpath == null) {
return doc;
}
// recursively add parent nodes
int idx = xpath.lastIndexOf('/');
String name;
Node parent = null;
if (idx > -1) {
String parentXpath = xpath.substring(0, idx);
name = xpath.substring(idx + 1);
parent = getNode(doc, parentXpath);
if (parent == null) {
parent = addNode(doc, parentXpath, after);
}
} else {
parent = doc;
name = xpath;
}
// handle node order
Node before = null;
if ((after == null) || (after == parent)) { // by default insert at the beginning
before = parent.getFirstChild();
} else { // otherwise add after provided 'after' node
Node sameLevelAfter = getDirectChild(parent, after);
if (sameLevelAfter != null) {
before = sameLevelAfter.getNextSibling();
} else {
before = parent.getFirstChild();
}
}
Element e = document.createElement(name);
parent.insertBefore(e, before);
fixIndent(parent, e);
return e;
}
/**
* Sets indent to same as in previous element
*
* @param parent parent node
* @param e node to indent
*/
private void fixIndent(Node parent, Element e) {
Node prevElem = getPrevSibling(e, Node.ELEMENT_NODE);
if (prevElem != null) { // there's previous element, which indent we should follow
Node prevText = prevElem.getPreviousSibling();
if ((prevText != null) && (prevText.getNodeType() == Node.TEXT_NODE)) { // the previous element has some indent
// figure out current indent of previous sibling
String prevWhitespace = prevText.getTextContent();
int indentIdx = prevWhitespace.lastIndexOf('\n');
if (indentIdx != -1) {
String indent = prevWhitespace.substring(indentIdx);
// try to apply previous sibling indent to current node
Node prev = e.getPreviousSibling();
if (prev.getNodeType() == Node.TEXT_NODE) { // text node already exist, fix it with proper indent
String whitespace = prev.getTextContent();
indentIdx = whitespace.lastIndexOf('\n');
if (indentIdx != -1) {
String currIndent = whitespace.substring(indentIdx);
if (currIndent.trim().length() == 0) { // if can replace
prev.setTextContent(whitespace.substring(0, indentIdx) + indent);
}
} else {
prev.setTextContent(whitespace + indent);
}
} else { // indent node not exists, manually create it with proper indent
Node indentNode = document.createTextNode(indent);
parent.insertBefore(indentNode, e);
}
}
}
}
}
private Node getPrevSibling(Node node, int type) {
Node prevElem = node.getPreviousSibling();
while (prevElem != null && prevElem.getNodeType() != type) {
prevElem = prevElem.getPreviousSibling();
}
return prevElem;
}
/**
* Removes nodes from startnode up the three to parent
* @param parent
* @param node
*/
private void removeNodes(Node border, Node node) {
Node parent = node.getParentNode();
do {
Node sibling = node.getNextSibling();
parent.removeChild(node);
// remove text node after the deleted node
if ((sibling != null) && (sibling.getNodeType() == Node.TEXT_NODE)) {
parent.removeChild(sibling);
}
node = parent;
parent = parent.getParentNode();
} while ((getChildCount(node, Node.ELEMENT_NODE) == 0) && (node != border));
}
/**
* Get the number of children nodes of given type.
*
* @param parent node which children should be counted
* @param type type of children to count
* @return number of children of given type in parent
*/
private int getChildCount(Node parent, short type) {
int length = 0;
NodeList list = parent.getChildNodes();
for (int i = 0; i < list.getLength(); i++) {
length += (list.item(i).getNodeType() ==type) ? 1 : 0;
}
return length;
}
private void removeString(Node node, String xpath, String attrName) throws XPathExpressionException {
Node target = node;
if (xpath != null) {
target = getNode(node, xpath);
}
if (target == null) { // if node not found, then there's nothing to remove
return;
}
if (attrName == null) {
Node sibling = target.getNextSibling();
// remove text node after the deleted node
if ((sibling != null) && (sibling.getNodeType() == Node.TEXT_NODE)) {
target.getParentNode().removeChild(sibling);
}
target.getParentNode().removeChild(target);
} else {
NamedNodeMap attrs = target.getAttributes();
if (attrs.getNamedItem(attrName) != null) {
attrs.removeNamedItem(attrName);
}
}
}
private Node setString(Node node, String xpath, String attrName, String value, Node after) throws XPathExpressionException {
Node target = node;
if (xpath != null) {
target = getNode(node, xpath);
if (target == null) {
target = addNode(node, xpath, after);
}
}
if (attrName != null) {
((Element) target).setAttribute(attrName, value);
return after;
} else {
target.setTextContent(value);
return target;
}
}
private static String getXPath(String nodePath, String attrName) {
if (nodePath != null && attrName == null) {
return nodePath;
} else if (nodePath == null && attrName != null) {
return AT+attrName;
} else {
return nodePath + AT+attrName;
}
}
private String getXpathString(Node node, String xpath) throws XPathExpressionException {
XPathExpression expr = xpathObj.compile(xpath);
String out = (String) expr.evaluate(node, XPathConstants.STRING);
return out;
}
private Node getNode(Node node, String xpath) throws XPathExpressionException {
XPathExpression expr = xpathObj.compile(xpath);
Node newnode = (Node) expr.evaluate(node, XPathConstants.NODE);
return newnode;
}
private Node[] getNodes(Node node, String xpath) throws XPathExpressionException {
XPathExpression expr = xpathObj.compile(xpath);
NodeList list = (NodeList) expr.evaluate(node, XPathConstants.NODESET);
Node[] result = new Node[list.getLength()];
for (int i = 0; i < list.getLength(); i++) {
result[i] = list.item(i);
}
return result;
}
private static String stripWhitespaces(String str) {
if (str == null) {
return null;
}
str = str.trim();
StringBuilder sb = new StringBuilder(str);
boolean isWhiteSpace = false;
int lastWhSpcIdx = -1;
for (int i = str.length() - 1; i >= 0; i--) {
char c = sb.charAt(i);
if (c == ' ' || c=='\t' || c=='\n') { // is white space
if (!isWhiteSpace) { // whitespace after non-whitespaces
lastWhSpcIdx = i;
}
isWhiteSpace = true;
} else if (isWhiteSpace) { // not whitespce, after whitespaces
sb.replace(i+1, lastWhSpcIdx + 1, " "); //$NON-NLS-1$
isWhiteSpace = false;
}
}
return sb.toString();
}
public int getDocumentLength() {
return documentLength;
}
}