package nl.knaw.huygens.alexandria.endpoint.command;
/*
* #%L
* alexandria-main
* =======
* Copyright (C) 2015 - 2017 Huygens ING (KNAW)
* =======
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.inject.Inject;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.StreamingOutput;
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.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.io.output.StringBuilderWriter;
import org.apache.commons.io.output.WriterOutputStream;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import nl.knaw.huygens.alexandria.api.model.CommandResponse;
import nl.knaw.huygens.alexandria.api.model.Commands;
import nl.knaw.huygens.alexandria.endpoint.command.XpathCommand.XPathResult.Type;
import nl.knaw.huygens.alexandria.service.AlexandriaService;
import nl.knaw.huygens.alexandria.textgraph.TextGraphUtil;
import nl.knaw.huygens.tei.QueryableDocument;
public class XpathCommand extends ResourcesCommand {
private static final String PARAMETER_XPATH = "xpath";
private static class Parameters {
List<ResourceViewId> resourceViewIds;
String xpath;
}
private CommandResponse commandResponse = new CommandResponse();
private AlexandriaService service;
@Inject
public XpathCommand(AlexandriaService service) {
this.service = service;
}
@Override
public String getName() {
return Commands.XPATH;
}
@Override
public CommandResponse runWith(Map<String, Object> parameterMap) {
Parameters parameters = validateParameters(parameterMap);
Map<String, XPathResult> resultMap = new HashMap<>();
if (commandResponse.parametersAreValid()) {
for (ResourceViewId resourceViewId : parameters.resourceViewIds) {
service.runInTransaction(() -> {
UUID resourceId = resourceViewId.getResourceId();
String viewName = resourceViewId.getTextViewName().orElse("");
StreamingOutput xmlOutputStream = TextGraphUtil.xmlOutputStream(service, resourceId, viewName);
StringBuilderWriter sbWriter = new StringBuilderWriter();
WriterOutputStream writerOutputStream = new WriterOutputStream(sbWriter);
try {
xmlOutputStream.write(writerOutputStream);
writerOutputStream.close();
String xml = sbWriter.toString();
XPathResult result = testXPath(parameters.xpath, xml);
resultMap.put(resourceViewId.toString(), result);
} catch (WebApplicationException | IOException | XPathExpressionException e) {
commandResponse.addErrorLine(resourceId + ": " + e.getMessage());
e.printStackTrace();
}
});
}
}
commandResponse.setResult(resultMap);
return commandResponse;
}
static XPathResult testXPath(String xpathQuery, String xml) throws XPathExpressionException {
QueryableDocument qDoc = QueryableDocument.createFromXml(xml, true);
try {
NodeList nodelist = qDoc.evaluateXPathToW3CNodeList(xpathQuery);
return new XPathResult(Type.NODESET, toStringList(nodelist));
} catch (XPathException e) {
if (e.getMessage().contains("#BOOLEAN")) {
Object b = qDoc.evaluateXPathToBoolean(xpathQuery);
return new XPathResult(Type.BOOLEAN, b);
}
if (e.getMessage().contains("#NUMBER")) {
Object n = qDoc.evaluateXPathToDouble(xpathQuery);
return new XPathResult(Type.NUMBER, n);
}
}
String string = qDoc.evaluateXPathToString(xpathQuery);
return new XPathResult(Type.STRING, string);
}
private static List<String> toStringList(NodeList nodelist) {
List<String> list = new ArrayList<>(nodelist.getLength());
for (int i = 0; i < nodelist.getLength(); i++) {
Node node = nodelist.item(i);
list.add(toString(node));
}
return list;
}
public static String toString(Node node) {
if (node == null) {
throw new IllegalArgumentException("node is null.");
}
try {
// Remove unwanted whitespaces
XPath xpath = XPathFactory.newInstance().newXPath();
XPathExpression expr = xpath.compile("//text()[normalize-space()='']");
NodeList nodeList = (NodeList) expr.evaluate(node, XPathConstants.NODESET);
for (int i = 0; i < nodeList.getLength(); ++i) {
Node nd = nodeList.item(i);
nd.getParentNode().removeChild(nd);
}
// Create and setup transformer
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
// transformer.setOutputProperty(OutputKeys.INDENT, "yes");
// transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
// Turn the node into a string
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(node), new StreamResult(writer));
return writer.toString();
} catch (TransformerException | XPathExpressionException e) {
throw new RuntimeException(e);
}
}
private Parameters validateParameters(Map<String, Object> parameterMap) {
final Parameters parameters = new Parameters();
parameters.resourceViewIds = validateResourceViewIds(parameterMap, commandResponse, service);
boolean valid = (commandResponse.getErrorLines().isEmpty());
if (!parameterMap.containsKey(PARAMETER_XPATH)) {
addXPathError();
valid = false;
} else {
try {
parameters.xpath = (String) parameterMap.get(PARAMETER_XPATH);
} catch (ClassCastException e) {
addXPathError();
valid = false;
}
}
if (valid) {
commandResponse.setParametersAreValid(true);
}
return parameters;
}
private void addXPathError() {
commandResponse.addErrorLine("Parameter '" + PARAMETER_XPATH + "' should be a valid xpath query.");
}
public static class XPathResult {
public enum Type {
BOOLEAN, NUMBER, STRING, NODESET
}
Object result;
Type type;
public XPathResult(Type type, Object result) {
this.type = type;
this.result = result;
}
public Object getResult() {
return result;
}
public Type getType() {
return type;
}
}
}