package no.met.metadataeditor.dataTypes;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
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 no.met.metadataeditor.EditorException;
import no.met.metadataeditor.dataTypes.attributes.DataAttribute;
import org.apache.commons.io.IOUtils;
import org.jdom2.Attribute;
import org.jdom2.Content;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Text;
import org.jdom2.input.SAXBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
public class EditorTemplate {
private Map<String, EditorVariable> varMap;
private Map<String, String> prefixeNamspace;
private Map<String, String> namespacePrefixes;
private static Map<String,Class<? extends DataAttribute>> supportedTags;
private String templateXML;
public EditorTemplate(InputSource source) throws SAXException, IOException {
// we read the entire XML contents from the source so that we can re-use it later.
if( source.getCharacterStream() != null ){
templateXML = IOUtils.toString(source.getCharacterStream());
} else {
templateXML = IOUtils.toString(source.getByteStream());
}
TemplateHandler th = new TemplateHandler();
th.setNamespacePrefixes(findNamespaces(templateXML));
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
try {
SAXParser saxParser = spf.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setContentHandler(th);
xmlReader.parse(new InputSource(new StringReader(templateXML)));
} catch (ParserConfigurationException e) {
throw new EditorException("Parsing template failed.", e, EditorException.TEMPLATE_PARSE_ERROR);
}
varMap = th.getResultConfig();
namespacePrefixes = th.getNamespacePrefixes();
prefixeNamspace = new HashMap<>();
for (String key : namespacePrefixes.keySet()) {
prefixeNamspace.put(namespacePrefixes.get(key), key);
}
supportedTags = TemplateHandler.getSupportedTags();
}
NamespaceContext getTemplateContext() {
return new NamespaceContext() {
@Override
public String getNamespaceURI(String prefix) {
return prefixeNamspace.get(prefix);
}
@Override
public String getPrefix(String namespaceURI) {
return namespacePrefixes.get(namespaceURI);
}
@Override
@SuppressWarnings("rawtypes")
public Iterator getPrefixes(String namespaceURI) {
// only one prefix by construction
List<String> prefixes = new ArrayList<>();
String prefix = getPrefix(namespaceURI);
if (prefix != null) {
prefixes.add(prefix);
}
return prefixes.iterator();
}
};
}
/**
* Find all the declared namespaces in a XML document.
* @param templateXML The XML content to search for namespaces in
* @return A mapping between namespace URIs and namespace prefixes.
*/
static Map<String,String> findNamespaces(String templateXML){
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db;
Map<String,String> namespaces = new HashMap<>();
try {
db = dbf.newDocumentBuilder();
Document doc = db.parse(IOUtils.toInputStream(templateXML));
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xPath = xPathFactory.newXPath();
XPathExpression xPathExpression = xPath.compile("//namespace::*");
NodeList namespaceNodes = (NodeList) xPathExpression.evaluate(doc, XPathConstants.NODESET);
for( int i = 0; i < namespaceNodes.getLength(); i++ ){
Node node = namespaceNodes.item(i);
// we skip the 'xml' namespace
if(!"xml".equals(node.getLocalName())){
namespaces.put(node.getNodeValue(), node.getLocalName());
}
}
} catch (ParserConfigurationException e) {
String msg = "Failed to created parser";
Logger.getLogger(EditorTemplate.class.getName()).log(Level.SEVERE, msg);
throw new EditorException("Failed to create parser", e, EditorException.TEMPLATE_PARSE_ERROR);
} catch (SAXException e) {
String msg = "SAX error when finding namespaces";
Logger.getLogger(EditorTemplate.class.getName()).log(Level.SEVERE, msg);
throw new EditorException("Failed to create parser", e, EditorException.TEMPLATE_PARSE_ERROR);
} catch (IOException e) {
String msg = "IOException when fidning namespaces";
Logger.getLogger(EditorTemplate.class.getName()).log(Level.SEVERE, msg);
throw new EditorException("Failed to create parser", e, EditorException.TEMPLATE_PARSE_ERROR);
} catch (XPathExpressionException e) {
String msg = "XPath problem when finding namespaces";
Logger.getLogger(EditorTemplate.class.getName()).log(Level.SEVERE, msg);
throw new EditorException("Failed to create parser", e, EditorException.TEMPLATE_PARSE_ERROR);
}
return namespaces;
}
public Map<String,List<EditorVariableContent>> getContent(InputSource xmlData) throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(xmlData);
XPathFactory xpathFact = XPathFactory.newInstance();
XPath xpath = xpathFact.newXPath();
xpath.setNamespaceContext(getTemplateContext());
// retrieve the information for the EditorVariables
return readEditorContent(xpath, "", doc, getVarMap());
}
/**
* recursively read the information in the xml-file, starting at the top-node
*
* @param xpath xpath with correct namespace-context
* @param nodeXpath the elements will be searched for below the current element
* @param node the node below the Editorvariables should be searched
* @param vars a map of editorVariables
*/
private Map<String, List<EditorVariableContent>> readEditorContent(XPath xpath, String nodePath, Node node, Map<String, EditorVariable> vars) {
Map<String,List<EditorVariableContent>> content = new HashMap<>();
for (String varName : vars.keySet()) {
List<EditorVariableContent> contentList = new ArrayList<>();
content.put(varName, contentList);
EditorVariable ev = vars.get(varName);
String selectionPath = ev.getSelectionXPath();
// The variable does not have hard coded selection path (the most common case)
// then select using document path.
if( selectionPath == null ){
selectionPath = ev.getDocumentXPath();
}
selectionPath = selectionPath.substring(nodePath.length());
if (selectionPath.startsWith("/")) {
selectionPath = selectionPath.substring(1);
}
try {
Logger.getLogger(getClass().getName()).fine(String.format("EditorVariable %s with path %s and local path %s", varName, ev.getDocumentXPath(), selectionPath));
XPathExpression expr = xpath.compile(selectionPath);
NodeList evSubnodes = (NodeList) expr.evaluate(node, XPathConstants.NODESET);
for (int i = 0; i < evSubnodes.getLength(); ++i) {
Node subNode = evSubnodes.item(i);
DataAttribute da = readAttributes(ev, subNode, xpath);
EditorVariableContent evc = new EditorVariableContent();
evc.setAttrs(da);
Map<String, List<EditorVariableContent>> children = readEditorContent(xpath, ev.getDocumentXPath(), subNode, ev.getChildren());
evc.setChildren(children);
contentList.add(evc);
}
} catch (XPathExpressionException e) {
// should never happen
throw new EditorException("Failed to evaluate XPath expression: " + selectionPath, e, EditorException.GENERAL_ERROR_CODE);
}
}
return content;
}
private DataAttribute readAttributes(EditorVariable variable, Node node, XPath xpath) {
DataAttribute da = variable.getDataAttributes().newInstance();
// set the attributes
Map<String, String> attXpath = variable.getAttrsXPath();
for (String att : attXpath.keySet()) {
String relAttPath = attXpath.get(att).substring(variable.getDocumentXPath().length());
Logger.getLogger(getClass().getName()).fine(String.format("searching attr %s in %s", att, relAttPath));
if (relAttPath.startsWith("/")) {
// remove leading / in e.g. /text()
relAttPath = relAttPath.substring(1);
}
XPathExpression attExpr;
try {
attExpr = xpath.compile(relAttPath);
String attVal = attExpr.evaluate(node);
Logger.getLogger(getClass().getName()).fine(String.format("%s + value = %s", relAttPath, attVal));
da.addAttribute(att, attVal);
} catch (XPathExpressionException e) {
// should never happen
throw new EditorException("Failed to evaluate XPath expression when getting the actual attributes values:" + relAttPath, e, EditorException.GENERAL_ERROR_CODE );
}
}
return da;
}
/**
* Write the the content of editor variables to the template to produce a new XML file.
* @param templateXML The template
* @param content A map of content for the variables in the template.
* @return
* @throws JDOMException
* @throws IOException
*/
public org.jdom2.Document writeContent(Map<String, List<EditorVariableContent>> content) throws JDOMException, IOException {
SAXBuilder builder = new SAXBuilder();
org.jdom2.Document templateDoc = builder.build(new InputSource(new StringReader(templateXML)));
TemplateNode rootNode = genTemplateTree(templateDoc, content);
org.jdom2.Document doc = replaceVars(rootNode);
pruneTree(doc);
return doc;
}
/**
* Remove the editor variables nodes from the tree.
* @param doc
*/
private void pruneTree(org.jdom2.Document doc){
pruneTreeRecursive(doc.getRootElement(), null, doc);
}
/**
* Prune the tree recursively by making all children of editor variable nodes point to their grandparent
* or if there is no grandparent attach them to the document directly, even though this leads to an invalid XML document.
* @param element The element to process
* @param parent The parent of the processed element
* @param doc The document
*/
private void pruneTreeRecursive(Element element, Element parent, org.jdom2.Document doc){
List<Content> children = new ArrayList<>();
for( Content child : element.getContent()){
children.add(child);
}
if( supportedTags.containsKey(element.getName())){
// need the index of the element to know where to insert the elements
// children
int elementIndex;
if( parent != null ){
elementIndex = parent.indexOf(element);
} else {
elementIndex = doc.indexOf(element);
}
element.detach();
for( Content child : children ){
if( child instanceof Element ){
Element e = (Element) child;
pruneTreeRecursive(e, parent, doc);
}
child.detach();
if( parent != null ){
parent.addContent(elementIndex++, child);
} else {
doc.addContent(elementIndex++, child);
}
}
} else {
for( Content child : children ){
if( child instanceof Element ){
Element e = (Element) child;
pruneTreeRecursive(e, element, doc);
}
}
}
}
/**
* Generate a tree that combines the information from the template document and the variable content. The generated
* tree will be expanded with nodes depending on the information on the variables.
* @param templateDoc
* @param content
* @return
*/
private TemplateNode genTemplateTree(org.jdom2.Document templateDoc, Map<String, List<EditorVariableContent>> content) {
// skip the top node since it is part of the editor template and not the XML we want
// in the end.
Element templateRoot = templateDoc.getRootElement().getChildren().get(0);
TemplateNode trn = new TemplateNode();
trn.children = genTemplateTreeRecursive(templateRoot, content);
trn.xmlNode = templateRoot;
return trn;
}
private List<TemplateNode> genTemplateTreeRecursive(Element element, Map<String, List<EditorVariableContent>> contentMap){
List<TemplateNode> children = new ArrayList<>();
for( Content c : element.getContent() ){
if( c instanceof Element ){
Element child = (Element) c;
if( supportedTags.containsKey(child.getName()) ){
String varName = child.getAttributeValue("varName");
List<EditorVariableContent> contentList = contentMap.get(varName);
// there is not content to fill in for this variable, so we should skip it.
if( contentList == null ){
continue;
}
for( EditorVariableContent evc : contentList ){
TemplateVarNode tvn = new TemplateVarNode();
tvn.content = evc;
tvn.children = genTemplateTreeRecursive(child, evc.getChildren());
tvn.xmlNode = child;
children.add(tvn);
}
} else {
TemplateNode tn = new TemplateXMLNode();
tn.children = genTemplateTreeRecursive(child, contentMap);
tn.xmlNode = child;
children.add(tn);
}
} else {
TemplateNode tn = new TemplateXMLNode();
tn.children = new ArrayList<>();
tn.xmlNode = c;
children.add(tn);
}
}
return children;
}
private org.jdom2.Document replaceVars(TemplateNode root) {
org.jdom2.Document doc = new org.jdom2.Document();
Content c = replaceVarsRecursive(root);
doc.setContent(c);
return doc;
}
private Content replaceVarsRecursive(TemplateNode node){
Content c = node.xmlNode.clone();
if( c instanceof Element ){
Element e = (Element) c;
e.getContent().clear();
for( TemplateNode n : node.children ) {
Content child = replaceVarsRecursive(n);
e.addContent(child);
}
}
if( node instanceof TemplateVarNode ){
TemplateVarNode tvn = (TemplateVarNode) node;
replace(c, tvn.content);
}
return c;
}
private void replace(Content c, EditorVariableContent evc ){
if( c instanceof Text ){
Text text = (Text) c;
String currValue = text.getText();
text.setText(getReplaceValue(currValue, evc));
} else if ( c instanceof Element ){
Element e = (Element) c;
for( Attribute a : e.getAttributes()){
String currValue = a.getValue();
a.setValue(getReplaceValue(currValue, evc));
}
for( Content childContent : e.getContent()){
replace(childContent, evc);
}
}
}
private String getReplaceValue(String value, EditorVariableContent evc){
DataAttribute da = evc.getAttrs();
for( String attrKey : da.getAttributesSetup().keySet() ){
String newValue = da.getAttribute(attrKey);
value = value.replace("$" + attrKey, newValue);
}
return value;
}
public Map<String, EditorVariable> getVarMap() {
return varMap;
}
}