//=============================================================================
//=== Copyright (C) 2001-2005 Food and Agriculture Organization of the
//=== United Nations (FAO-UN), United Nations World Food Programme (WFP)
//=== and United Nations Environment Programme (UNEP)
//===
//=== This library is free software; you can redistribute it and/or
//=== modify it under the terms of the GNU Lesser General Public
//=== License as published by the Free Software Foundation; either
//=== version 2.1 of the License, or (at your option) any later version.
//===
//=== This library 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
//=== Lesser General Public License for more details.
//===
//=== You should have received a copy of the GNU Lesser General Public
//=== License along with this library; if not, write to the Free Software
//=== Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
//===
//=== Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
//=== Rome - Italy. email: GeoNetwork@fao.org
//==============================================================================
package org.fao.geonet.utils;
import net.sf.json.JSON;
import net.sf.json.xml.XMLSerializer;
import net.sf.saxon.Configuration;
import net.sf.saxon.FeatureKeys;
import org.apache.commons.io.IOUtils;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.MimeConstants;
import org.apache.xml.resolver.tools.CatalogResolver;
import org.eclipse.core.runtime.URIUtil;
import org.fao.geonet.exceptions.XSDValidationErrorEx;
import org.jdom.Attribute;
import org.jdom.Content;
import org.jdom.DocType;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.Text;
import org.jdom.filter.ElementFilter;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.SAXOutputter;
import org.jdom.output.XMLOutputter;
import org.jdom.transform.JDOMResult;
import org.jdom.transform.JDOMSource;
import org.jdom.xpath.XPath;
import org.mozilla.universalchardet.UniversalDetector;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.*;
import javax.xml.XMLConstants;
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.URIResolver;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.ValidatorHandler;
import static org.fao.geonet.Constants.ENCODING;
//=============================================================================
/**
* General class of useful static methods.
*/
public final class Xml
{
public static final Namespace xsiNS = Namespace.getNamespace("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
//--------------------------------------------------------------------------
/**
*
* @param validate
* @return
*/
private static SAXBuilder getSAXBuilder(boolean validate) {
SAXBuilder builder = getSAXBuilderWithoutXMLResolver(validate);
Resolver resolver = ResolverWrapper.getInstance();
builder.setEntityResolver(resolver.getXmlResolver());
return builder;
}
private static SAXBuilder getSAXBuilderWithoutXMLResolver(boolean validate) {
SAXBuilder builder = new SAXBuilder(validate);
builder.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
return builder;
}
//--------------------------------------------------------------------------
/**
*
*/
public static void resetResolver() {
Resolver resolver = ResolverWrapper.getInstance();
resolver.reset();
}
//--------------------------------------------------------------------------
//---
//--- Load API
//---
//--------------------------------------------------------------------------
/**
*
* @param file
* @return
* @throws IOException
* @throws JDOMException
*/
public static Element loadFile(String file) throws IOException, JDOMException
{
return loadFile(new File(file));
}
//--------------------------------------------------------------------------
/**
* Loads an xml file from a URL and returns its root node.
*
* @param url
* @return
* @throws IOException
* @throws JDOMException
*/
public static Element loadFile(URL url) throws IOException, JDOMException
{
SAXBuilder builder = getSAXBuilderWithoutXMLResolver(false);//new SAXBuilder();
Document jdoc = builder.build(url);
return (Element) jdoc.getRootElement().detach();
}
//--------------------------------------------------------------------------
/**
* Loads an xml file from a URL after posting content to the URL.
*
* @param url
* @param xmlQuery
* @return
* @throws IOException
* @throws JDOMException
*/
public static Element loadFile(URL url, Element xmlQuery) throws IOException, JDOMException
{
Element result = null;
try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/xml");
connection.setRequestProperty("Content-Length", "" + Integer.toString(getString(xmlQuery).getBytes(ENCODING).length));
connection.setRequestProperty("Content-Language", "en-US");
connection.setDoOutput(true);
PrintStream out = new PrintStream(connection.getOutputStream(), true, ENCODING);
out.print(getString(xmlQuery));
out.close();
SAXBuilder builder = getSAXBuilderWithoutXMLResolver(false);//new SAXBuilder();
Document jdoc = builder.build(connection.getInputStream());
result = (Element)jdoc.getRootElement().detach();
} catch (Exception e) {
Log.error(Log.ENGINE, "Error loading URL " + url.getPath() + " .Threw exception "+e);
e.printStackTrace();
}
return result;
}
//--------------------------------------------------------------------------
/**
* Loads an xml file and returns its root node.
*
* @param file
* @return
* @throws IOException
* @throws JDOMException
*/
public static Element loadFile(File file) throws IOException, JDOMException
{
SAXBuilder builder = getSAXBuilderWithoutXMLResolver(false); //new SAXBuilder();
String convert = System.getProperty("jeeves.filecharsetdetectandconvert");
// detect charset and convert if required
if (convert != null && convert.equals("enabled")) {
byte[] content = convertFileToUTF8ByteArray(file);
return loadStream(new ByteArrayInputStream(content));
// no charset detection and conversion allowed
} else {
Document jdoc = builder.build(file);
return (Element) jdoc.getRootElement().detach();
}
}
//--------------------------------------------------------------------------
/**
* Reads file into byte array, detects charset and converts from this
* charset to UTF8
*
* @param file file to decode and convert to UTF8
* @return
* @throws IOException
* @throws CharacterCodingException
*/
public synchronized static byte[] convertFileToUTF8ByteArray(File file) throws IOException, CharacterCodingException {
FileInputStream in = null;
DataInputStream inStream = null;
try {
in = new FileInputStream(file);
inStream = new DataInputStream(in);
byte[] buf = new byte[(int)file.length()];
int nrRead = inStream.read(buf);
UniversalDetector detector = new UniversalDetector(null);
detector.handleData(buf, 0, nrRead);
detector.dataEnd();
String encoding = detector.getDetectedCharset();
detector.reset();
if (encoding != null) {
if (!encoding.equals(ENCODING)) {
Log.error(Log.JEEVES,"Detected character set "+encoding+", converting to UTF-8");
return convertByteArrayToUTF8ByteArray(buf, encoding);
}
}
return buf;
} finally {
if(in != null) {
IOUtils.closeQuietly(in);
}
if (inStream != null) {
IOUtils.closeQuietly(inStream);
}
}
}
//--------------------------------------------------------------------------
/**
* Decode byte array as specified charset, then convert to UTF-8
* by encoding as UTF8
*
* @param buf byte array to decode and convert to UTF8
* @param charsetName charset to decode byte array into
* @return
* @throws CharacterCodingException
*/
public synchronized static byte[] convertByteArrayToUTF8ByteArray(byte[] buf, String charsetName) throws CharacterCodingException {
Charset cset;
cset = Charset.forName(charsetName); // detected character set name
CharsetDecoder csetDecoder = cset.newDecoder();
Charset utf8 = Charset.forName(ENCODING);
CharsetEncoder utf8Encoder = utf8.newEncoder();
ByteBuffer inputBuffer = ByteBuffer.wrap(buf);
// decode as detected character set
CharBuffer data = csetDecoder.decode(inputBuffer);
// encode as UTF-8
ByteBuffer outputBuffer = utf8Encoder.encode(data);
// remove any nulls from the end of the encoded data why? - this is a
// bug in the encoder???? could also be that the file has characters
// from more than one charset?
byte[] out = outputBuffer.array();
int length = out.length;
while (out[length-1] == 0) length--;
byte[] result = new byte[length];
System.arraycopy(out,0,result,0,length);
// now return the converted bytes
return result;
}
//--------------------------------------------------------------------------
/**
* Loads xml from a string and returns its root node
* (validates the xml if required).
*
* @param data
* @param validate
* @return
* @throws IOException
* @throws JDOMException
*/
public static Element loadString(String data, boolean validate)
throws IOException, JDOMException
{
//SAXBuilder builder = new SAXBuilder(validate);
SAXBuilder builder = getSAXBuilderWithoutXMLResolver(validate); // oasis catalogs are used
Document jdoc = builder.build(new StringReader(data));
return (Element) jdoc.getRootElement().detach();
}
//--------------------------------------------------------------------------
/**
* Loads xml from an input stream and returns its root node.
*
* @param input
* @return
* @throws IOException
* @throws JDOMException
*/
public static Element loadStream(InputStream input) throws IOException, JDOMException
{
SAXBuilder builder = getSAXBuilderWithoutXMLResolver(false); //new SAXBuilder();
builder.setFeature("http://apache.org/xml/features/validation/schema",false);
builder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",false);
Document jdoc = builder.build(input);
return (Element) jdoc.getRootElement().detach();
}
//--------------------------------------------------------------------------
//---
//--- Transform API
//---
//--------------------------------------------------------------------------
/**
* Transforms an xml tree into another using a stylesheet on disk.
*
* @param xml
* @param styleSheetPath
* @return
* @throws Exception
*/
public static Element transform(Element xml, String styleSheetPath) throws Exception
{
JDOMResult resXml = new JDOMResult();
transform(xml, styleSheetPath, resXml, null);
return (Element)resXml.getDocument().getRootElement().detach();
}
//--------------------------------------------------------------------------
/**
* Transforms an xml tree into another using a stylesheet on disk and pass parameters.
*
*
* @param xml
* @param styleSheetPath
* @param params
* @return
* @throws Exception
*/
public static Element transform(Element xml, String styleSheetPath, Map<String, Object> params) throws Exception
{
JDOMResult resXml = new JDOMResult();
transform(xml, styleSheetPath, resXml, params);
return (Element)resXml.getDocument().getRootElement().detach();
}
//--------------------------------------------------------------------------
/**
* Transforms an xml tree putting the result to a stream (uses a stylesheet on disk).
*
* @param xml
* @param styleSheetPath
* @param out
* @throws Exception
*/
public static void transform(Element xml, String styleSheetPath, OutputStream out) throws Exception
{
StreamResult resStream= new StreamResult(out);
transform(xml, styleSheetPath, resStream, null);
}
//--------------------------------------------------------------------------
/**
* Transforms an xml tree putting the result to a stream - no parameters.
*
* @param xml
* @param styleSheetPath
* @param result
* @throws Exception
*/
public static void transform(Element xml, String styleSheetPath, Result result) throws Exception
{
transform(xml, styleSheetPath, result, null);
}
public static Element transformWithXmlParam(Element xml, String styleSheetPath, String xmlParamName, String xmlParam) throws Exception
{
JDOMResult resXml = new JDOMResult();
File styleSheet = new File(styleSheetPath);
Source srcSheet = new StreamSource(styleSheet);
transformWithXmlParam(xml, srcSheet, resXml, xmlParamName, xmlParam);
return (Element)resXml.getDocument().getRootElement().detach();
}
/**
* Transforms an xml tree putting the result to a stream. Sends xml snippet as parameter.
* @param xml
* @param xslt
* @param result
* @param xmlParamName
* @param xmlParam
* @throws Exception
*/
public static void transformWithXmlParam(Element xml, Source xslt, Result result,
String xmlParamName, String xmlParam) throws Exception {
Source srcXml = new JDOMSource(new Document((Element)xml.detach()));
// Dear old saxon likes to yell loudly about each and every XSLT 1.0
// stylesheet so switch it off but trap any exceptions because this
// code is run on transformers other than saxon
TransformerFactory transFact;
transFact = TransformerFactoryFactory.getTransformerFactory();
try {
transFact.setAttribute(FeatureKeys.VERSION_WARNING,false);
transFact.setAttribute(FeatureKeys.LINE_NUMBERING,true);
transFact.setAttribute(FeatureKeys.PRE_EVALUATE_DOC_FUNCTION,true);
transFact.setAttribute(FeatureKeys.RECOVERY_POLICY,Configuration.RECOVER_SILENTLY);
// Add the following to get timing info on xslt transformations
//transFact.setAttribute(FeatureKeys.TIMING,true);
} catch (IllegalArgumentException e) {
System.out.println("WARNING: transformerfactory doesnt like saxon attributes!");
//e.printStackTrace();
} finally {
Transformer t = transFact.newTransformer(xslt);
if (xmlParam != null) {
t.setParameter(xmlParamName, new StreamSource(new StringReader(xmlParam)));
}
t.transform(srcXml, result);
}
}
//--------------------------------------------------------------------------
private static class JeevesURIResolver implements URIResolver {
/**
*
* @param href
* @param base
* @return
* @throws TransformerException
*/
public Source resolve(String href, String base) throws TransformerException {
Resolver resolver = ResolverWrapper.getInstance();
CatalogResolver catResolver = resolver.getCatalogResolver();
if(Log.isDebugEnabled(Log.XML_RESOLVER)) {
Log.debug(Log.XML_RESOLVER, "Trying to resolve "+href+":"+base);
}
Source s = catResolver.resolve(href, base);
boolean isFile = false;
try {
final File file = new File(s.getSystemId());
isFile = file.isFile();
} catch (Exception e) {
isFile = false;
}
// If resolver has a blank XSL file use it to replace
// resolved file that doesn't exist...
String blankXSLFile = resolver.getBlankXSLFile();
if (blankXSLFile != null && s.getSystemId().endsWith(".xsl") && !isFile) {
try {
if(Log.isDebugEnabled(Log.XML_RESOLVER)) {
Log.debug(Log.XML_RESOLVER, " Check if exist " + s.getSystemId());
}
File f = URIUtil.toFile(new URI(s.getSystemId()));
if(Log.isDebugEnabled(Log.XML_RESOLVER))
Log.debug(Log.XML_RESOLVER, "Check on "+f.getPath()+" exists returned: "+f.exists());
// If the resolved resource does not exist, set it to blank file path to not trigger FileNotFound Exception
if (f == null || !(f.exists())) {
if(Log.isDebugEnabled(Log.XML_RESOLVER)) {
Log.debug(Log.XML_RESOLVER, " Resolved resource " + s.getSystemId() + " does not exist. blankXSLFile returned instead.");
}
s.setSystemId(blankXSLFile);
}
}
catch (URISyntaxException e) {
Log.warning(Log.XML_RESOLVER, "URI syntax problem: "+e.getMessage());
e.printStackTrace();
}
}
if (Log.isDebugEnabled(Log.XML_RESOLVER) && s != null) {
Log.debug(Log.XML_RESOLVER, "Resolved as "+s.getSystemId());
}
return s;
}
}
//--------------------------------------------------------------------------
/**
* Transforms an xml tree putting the result to a stream with optional parameters.
*
*
* @param xml
* @param styleSheetPath
* @param result
* @param params
* @throws Exception
*/
public static void transform(Element xml, String styleSheetPath, Result result, Map<String, Object> params) throws Exception
{
File styleSheet = new File(styleSheetPath);
Source srcXml = new JDOMSource(new Document((Element)xml.detach()));
Source srcSheet = new StreamSource(styleSheet);
// Dear old saxon likes to yell loudly about each and every XSLT 1.0
// stylesheet so switch it off but trap any exceptions because this
// code is run on transformers other than saxon
TransformerFactory transFact = TransformerFactoryFactory.getTransformerFactory();
transFact.setURIResolver(new JeevesURIResolver());
try {
transFact.setAttribute(FeatureKeys.VERSION_WARNING,false);
transFact.setAttribute(FeatureKeys.LINE_NUMBERING,true);
transFact.setAttribute(FeatureKeys.PRE_EVALUATE_DOC_FUNCTION,false);
transFact.setAttribute(FeatureKeys.RECOVERY_POLICY,Configuration.RECOVER_SILENTLY);
// Add the following to get timing info on xslt transformations
//transFact.setAttribute(FeatureKeys.TIMING,true);
} catch (IllegalArgumentException e) {
Log.warning(Log.ENGINE, "WARNING: transformerfactory doesnt like saxon attributes!");
//e.printStackTrace();
} finally {
Transformer t = transFact.newTransformer(srcSheet);
if (params != null) {
for (Map.Entry<String,Object> param : params.entrySet()) {
t.setParameter(param.getKey(),param.getValue());
}
}
t.transform(srcXml, result);
}
}
//--------------------------------------------------------------------------
/**
* Clears the cache used in the stylesheet transformer factory. This will only work for the GeoNetwork Caching
* stylesheet transformer factory. This is a no-op for other transformer factories.
*/
public static void clearTransformerFactoryStylesheetCache() {
TransformerFactory transFact = TransformerFactory.newInstance();
try {
Class<?> class1 = transFact.getClass();
Method cacheMethod = class1.getDeclaredMethod("clearCache");
cacheMethod.invoke(transFact, new Object[0]);
} catch (Exception e) {
Log.error(Log.ENGINE, "Failed to find/invoke clearCache method - continuing ("+e.getMessage()+")");
}
}
// --------------------------------------------------------------------------
/**
* Transform an xml tree to PDF using XSL-FOP
    * putting the result to a stream (uses a stylesheet
* on disk)
*/
public static String transformFOP(String uploadDir, Element xml, String styleSheetPath)
throws Exception {
String file = uploadDir + UUID.randomUUID().toString () + ".pdf";
// Step 1: Construct a FopFactory
// (reuse if you plan to render multiple documents!)
FopFactory fopFactory = FopFactory.newInstance();
// Step 2: Set up output stream.
// Note: Using BufferedOutputStream for performance reasons (helpful
// with FileOutputStreams).
OutputStream out = new BufferedOutputStream(new FileOutputStream(
new File(file)));
try {
// Step 3: Construct fop with desired output format
Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, out);
// Step 4: Setup JAXP using identity transformer
TransformerFactory factory = TransformerFactoryFactory.getTransformerFactory();
factory.setURIResolver(new JeevesURIResolver());
Source xslt = new StreamSource(new File(styleSheetPath));
try {
factory.setAttribute(FeatureKeys.VERSION_WARNING,false);
factory.setAttribute(FeatureKeys.LINE_NUMBERING,true);
factory.setAttribute(FeatureKeys.RECOVERY_POLICY,Configuration.RECOVER_SILENTLY);
} catch (IllegalArgumentException e) {
Log.warning(Log.ENGINE, "WARNING: transformerfactory doesnt like saxon attributes!");
//e.printStackTrace();
} finally {
Transformer transformer = factory.newTransformer(xslt);
// Step 5: Setup input and output for XSLT transformation
// Setup input stream
Source src = new JDOMSource(new Document((Element)xml.detach()));
// Resulting SAX events (the generated FO) must be piped through to
// FOP
Result res = new SAXResult(fop.getDefaultHandler());
// Step 6: Start XSLT transformation and FOP processing
transformer.transform(src, res);
}
}
finally {
// Clean-up
out.close();
}
return file;
}
//--------------------------------------------------------------------------
//---
//--- General stuff
//---
//--------------------------------------------------------------------------
/**
* Writes an xml element to a stream.
*
* @param doc
* @param out
* @throws IOException
*/
public static void writeResponse(Document doc, OutputStream out) throws IOException
{
XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
outputter.output(doc, out);
}
//---------------------------------------------------------------------------
/**
* Converts an xml element to a string.
*
* @param data
* @return
*/
public static String getString(Element data)
{
XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
return outputter.outputString(data);
}
//---------------------------------------------------------------------------
/**
* Converts an xml element to JSON
*
* @param xml the XML element
* @return the JSON response
*
* @throws IOException
*/
public static String getJSON (Element xml) throws IOException {
return Xml.getJSON(Xml.getString(xml));
}
/**
* Converts an xml string to JSON
*
* @param xml the XML element
* @return the JSON response
*
* @throws IOException
*/
public static String getJSON (String xml) throws IOException {
XMLSerializer xmlSerializer = new XMLSerializer();
// Disable type hints. When enable, a type attribute in the root
// element will throw NPE.
// http://sourceforge.net/mailarchive/message.php?msg_id=27646519
xmlSerializer.setTypeHintsEnabled(false);
xmlSerializer.setTypeHintsCompatibility(false);
JSON json = xmlSerializer.read(xml);
return json.toString(2);
}
/**
*
* @param data
* @return
*/
public static String getString(DocType data)
{
XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
return outputter.outputString(data);
}
//---------------------------------------------------------------------------
/**
*
* @param data
* @return
*/
public static String getString(Document data)
{
XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
return outputter.outputString(data);
}
//---------------------------------------------------------------------------
/**
* Creates and prepares an XPath element - simple xpath (like "a/b/c").
*
* @param xml
* @param xpath
* @param theNSs
* @return
* @throws JDOMException
*/
private static XPath prepareXPath(Element xml, String xpath, List<Namespace> theNSs) throws JDOMException
{
XPath xp = XPath.newInstance (xpath);
for (Namespace ns : theNSs ) {
xp.addNamespace(ns);
}
return xp;
}
//---------------------------------------------------------------------------
/**
* Retrieves a single XML element given a simple xpath (like "a/b/c").
*
* @param xml
* @param xpath
* @param theNSs
* @return
* @throws JDOMException
*/
public static Object selectSingle(Element xml, String xpath, List<Namespace> theNSs) throws JDOMException {
XPath xp = prepareXPath(xml, xpath, theNSs);
return xp.selectSingleNode(xml);
}
//---------------------------------------------------------------------------
/**
* Retrieves a single XML element as a JDOM element given a simple xpath.
*
* @param xml
* @param xpath
* @return
* @throws JDOMException
*/
public static Element selectElement(Element xml, String xpath) throws JDOMException {
return selectElement(xml, xpath, new ArrayList<Namespace>());
}
//---------------------------------------------------------------------------
/**
* Retrieves a single XML element as a JDOM element given a simple xpath.
*
* @param xml
* @param xpath
* @param theNSs
* @return
* @throws JDOMException
*/
public static Element selectElement(Element xml, String xpath, List<Namespace> theNSs) throws JDOMException {
Object result = selectSingle(xml, xpath, theNSs);
if (result == null) {
return null;
} else if (result instanceof Element) {
Element elem = (Element)result;
return (Element)(elem);
} else {
//-- Found something but not an element
return null;
}
}
//---------------------------------------------------------------------------
/**
* Evaluates an XPath expression on an element and returns Elements.
*
* @param xml
* @param xpath
* @param theNSs
* @return
* @throws JDOMException
*/
public static List<?> selectNodes(Element xml, String xpath, List<Namespace> theNSs) throws JDOMException {
XPath xp = prepareXPath(xml, xpath, theNSs);
return xp.selectNodes(xml);
}
/**
* Evaluates an XPath expression on an document and returns Elements.
*
* @param xml
* @param xpath
* @param theNSs
* @return
* @throws JDOMException
*/
public static List<?> selectDocumentNodes(Element xml, String xpath, List<Namespace> theNSs) throws JDOMException {
XPath xp = XPath.newInstance (xpath);
for (Namespace ns : theNSs ) {
xp.addNamespace(ns);
}
xml = (Element)xml.clone();
Document document = new Document(xml);
return xp.selectNodes(document);
}
//---------------------------------------------------------------------------
/**
* Evaluates an XPath expression on an element and returns Elements.
* @param xml
* @param xpath
* @return
* @throws JDOMException
*/
public static List<?> selectNodes(Element xml, String xpath) throws JDOMException {
return selectNodes(xml, xpath, new ArrayList<Namespace>());
}
//---------------------------------------------------------------------------
/**
* Evaluates an XPath expression on an element and returns string result.
*
* @param xml
* @param xpath
* @return
* @throws JDOMException
*/
public static String selectString(Element xml, String xpath) throws JDOMException {
return selectString(xml, xpath, new ArrayList<Namespace>());
}
//---------------------------------------------------------------------------
/**
* Evaluates an XPath expression on an element and returns string result.
*
* @param xml
* @param xpath
* @param theNSs
* @return
* @throws JDOMException
*/
public static String selectString(Element xml, String xpath, List<Namespace> theNSs) throws JDOMException {
XPath xp = prepareXPath(xml, xpath, theNSs);
return xp.valueOf(xml);
}
//---------------------------------------------------------------------------
/**
* Evaluates an XPath expression on an element and returns true/false.
*
* @param xml
* @param xpath
* @return
* @throws JDOMException
*/
public static boolean selectBoolean(Element xml, String xpath) throws JDOMException {
String result = selectString(xml, xpath, new ArrayList<Namespace>());
return result.length() > 0;
}
//---------------------------------------------------------------------------
/**
* Evaluates an XPath expression on an element and returns true/false.
*
* @param xml
* @param xpath
* @param theNSs
* @return
* @throws JDOMException
*/
public static boolean selectBoolean(Element xml, String xpath, List<Namespace> theNSs) throws JDOMException {
return selectString(xml, xpath, theNSs).length() > 0;
}
//---------------------------------------------------------------------------
/**
* Evaluates an XPath expression on an element and returns number result.
*
* @param xml
* @param xpath
* @return
* @throws JDOMException
*/
public static Number selectNumber(Element xml, String xpath) throws JDOMException {
return selectNumber(xml, xpath, new ArrayList<Namespace>());
}
//---------------------------------------------------------------------------
/**
* Evaluates an XPath expression on an element and returns number result.
*
* @param xml
* @param xpath
* @param theNSs
* @return
* @throws JDOMException
*/
public static Number selectNumber(Element xml, String xpath, List<Namespace> theNSs) throws JDOMException {
XPath xp = prepareXPath(xml, xpath, theNSs);
return xp.numberValueOf(xml);
}
/**
* Search in metadata all matching element for the filter
* and return a list of uuid separated by or to be used in a
* search on uuid. Extract uuid from matched element if
* elementName is null or from the elementName child.
*
* @param element
* @param elementFilter Filter to get element descendants
* @param elementName Child element to get value from. If null, filtered element value is returned
* @param elementNamespace
* @param attributeName Attribute name to get value from. If null, TODO: improve
* @return
*/
public static Set<String> filterElementValues(Element element,
ElementFilter elementFilter,
String elementName,
Namespace elementNamespace,
String attributeName) {
@SuppressWarnings("unchecked")
Iterator<Element> i = element.getDescendants(elementFilter);
Set<String> values = new HashSet<String>();
boolean first = true;
while (i.hasNext()) {
Element e = i.next();
String uuid = elementName == null && attributeName == null?
e.getText() :
(attributeName == null ?
e.getChildText(elementName, elementNamespace) :
e.getAttributeValue(attributeName)
);
if (!"".equals(uuid)) {
values.add(uuid);
}
}
return values;
}
//---------------------------------------------------------------------------
/**
* Error handler that collects up validation errors.
*
*/
public static class ErrorHandler extends DefaultHandler {
private int errorCount = 0;
private Element xpaths;
private Namespace ns = Namespace.NO_NAMESPACE;
private SAXOutputter so;
public void setSo(SAXOutputter so) {
this.so = so;
}
public boolean errors() {
return errorCount > 0;
}
public Element getXPaths() {
return xpaths;
}
public void addMessage ( SAXParseException exception, String typeOfError ) {
if (errorCount == 0) xpaths = new Element("xsderrors", ns);
errorCount++;
Element elem = (Element) so.getLocator().getNode();
Element x = new Element("xpath", ns);
try {
String xpath = org.fao.geonet.utils.XPath.getXPath(elem);
//-- remove the first element to ensure XPath fits XML passed with
//-- root element
if (xpath.startsWith("/")) {
int ind = xpath.indexOf('/',1);
if (ind != -1) {
xpath = xpath.substring(ind+1);
} else {
xpath = "."; // error to be placed on the root element
}
}
x.setText(xpath);
} catch (JDOMException e) {
e.printStackTrace();
x.setText("nopath");
}
String message = exception.getMessage() + " (Element: " + elem.getQualifiedName();
String parentName;
if (!elem.isRootElement()) {
Element parent = (Element)elem.getParent();
if (parent != null)
parentName = parent.getQualifiedName();
else
parentName = "Unknown";
} else {
parentName = "/";
}
message += " with parent element: " + parentName + ")";
Element m = new Element("message", ns).setText(message);
Element errorType = new Element("typeOfError", ns).setText(typeOfError);
Element errorNumber = new Element("errorNumber", ns).setText(String.valueOf(errorCount));
Element e = new Element("error", ns);
e.addContent(errorType);
e.addContent(errorNumber);
e.addContent(m);
e.addContent(x);
xpaths.addContent(e);
}
public void error( SAXParseException parseException ) throws SAXException {
addMessage( parseException, "ERROR" );
}
public void fatalError( SAXParseException parseException ) throws SAXException {
addMessage( parseException, "FATAL ERROR" );
}
public void warning( SAXParseException parseException ) throws SAXException {
addMessage( parseException, "WARNING" );
}
/**
* Set namespace to use for report elements
* @param ns
*/
public void setNs(Namespace ns) {
this.ns = ns;
}
public Namespace getNs() {
return ns;
}
}
//---------------------------------------------------------------------------
/**
* Validates an XML document using the hints in the DocType (DTD validation) or schemaLocation attribute hint.
*
* @param doc
* @throws Exception
*/
public synchronized static void validate(Document doc) throws Exception {
if (doc.getDocType() != null) { // assume DTD validation
SAXBuilder builder = getSAXBuilder(true);
builder.build(new StringReader(getString(doc)));
}
Element xml = doc.getRootElement();
if (xml != null) { // try XSD validation
String schemaLoc = xml.getAttributeValue("schemaLocation", xsiNS);
if (schemaLoc == null || schemaLoc.equals("")) {
throw new IllegalArgumentException("XML document missing/blank schemaLocation hints or DocType dtd - cannot validate");
}
validate(xml);
} else {
throw new IllegalArgumentException("XML document is missing root element - cannot validate");
}
}
//---------------------------------------------------------------------------
/**
* Validates an XML document using the hints in the schemaLocation attribute.
*
* @param xml
* @throws Exception
*/
public synchronized static void validate(Element xml) throws Exception {
Schema schema = factory().newSchema();
ErrorHandler eh = new ErrorHandler();
validateRealGuts(schema, xml, eh);
if (eh.errors()) {
Element xsdXPaths = eh.getXPaths();
throw new XSDValidationErrorEx("XSD Validation error(s):\n"+getString(xsdXPaths), xsdXPaths);
}
}
//---------------------------------------------------------------------------
/**
* Validates an xml document with respect to an xml schema described by .xsd file path.
*
* @param schemaPath
* @param xml
* @throws Exception
*/
public static void validate(String schemaPath, Element xml) throws Exception
{
Element xsdXPaths = validateInfo(schemaPath,xml);
if (xsdXPaths != null && xsdXPaths.getContent().size() > 0) throw new XSDValidationErrorEx("XSD Validation error(s):\n"+getString(xsdXPaths), xsdXPaths);
}
//---------------------------------------------------------------------------
/**
* Validates an xml document with respect to schemaLocation hints.
*
* @param xml
* @return
* @throws Exception
*/
public static Element validateInfo(Element xml) throws Exception
{
ErrorHandler eh = new ErrorHandler();
Schema schema = factory().newSchema();
validateRealGuts(schema, xml, eh);
if (eh.errors()) {
return eh.getXPaths();
} else {
return null;
}
}
//---------------------------------------------------------------------------
/**
* Validates an xml document with respect to schemaLocation hints using supplied error handler.
*
* @param xml
* @param eh
* @return
* @throws Exception
*/
public static Element validateInfo(Element xml, ErrorHandler eh) throws Exception
{
Schema schema = factory().newSchema();
validateRealGuts(schema, xml, eh);
if (eh.errors()) {
return eh.getXPaths();
} else {
return null;
}
}
//---------------------------------------------------------------------------
/**
* Validates an xml document with respect to an xml schema described by .xsd file path.
*
* @param schemaPath
* @param xml
* @return
* @throws Exception
*/
public static Element validateInfo(String schemaPath, Element xml) throws Exception
{
ErrorHandler eh = new ErrorHandler();
validateGuts(schemaPath, xml, eh);
if (eh.errors()) {
return eh.getXPaths();
} else {
return null;
}
}
//---------------------------------------------------------------------------
/**
* Validates an xml document with respect to an xml schema described by .xsd file path using supplied error handler.
*
* @param schemaPath
* @param xml
* @param eh
* @return
* @throws Exception
*/
public static Element validateInfo(String schemaPath, Element xml, ErrorHandler eh)
throws Exception {
validateGuts(schemaPath, xml, eh);
if (eh.errors()) {
return eh.getXPaths();
} else {
return null;
}
}
//---------------------------------------------------------------------------
/**
* Called by validation methods that supply an xml schema described by .xsd file path.
*
* @param schemaPath
* @param xml
* @param eh
* @throws Exception
*/
private static void validateGuts(String schemaPath, Element xml, ErrorHandler eh) throws Exception {
StreamSource schemaFile = new StreamSource(new File(schemaPath));
Schema schema = factory().newSchema(schemaFile);
validateRealGuts(schema, xml, eh);
}
//---------------------------------------------------------------------------
/**
* Called by all validation methods to do the real guts of the validation job.
* @param schema
* @param xml
* @param eh
* @throws Exception
*/
private static void validateRealGuts(Schema schema, Element xml, ErrorHandler eh) throws Exception {
Resolver resolver = ResolverWrapper.getInstance();
ValidatorHandler vh = schema.newValidatorHandler();
vh.setResourceResolver(resolver.getXmlResolver());
vh.setErrorHandler(eh);
SAXOutputter so = new SAXOutputter(vh);
eh.setSo(so);
so.output(xml);
}
private static SchemaFactory factory() {
return SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
}
/**
* Create and XPath expression for identified Element.
*
* @param element element within the xml to find an XPath for.
*
* return xpath or null if there was an error.
*/
public static String getXPathExpr(Content element) {
StringBuilder builder = new StringBuilder();
if (!doCreateXpathExpr(element, builder)) {
return null;
} else {
return builder.toString();
}
}
/**
* Create and XPath expression for identified Element.
*
* @param attribute element within the xml to find an XPath for.
*
* return xpath or null if there was an error.
*/
public static String getXPathExpr(Attribute attribute) {
StringBuilder builder = new StringBuilder();
if (!doCreateXpathExpr(attribute, builder)) {
return null;
} else {
return builder.toString();
}
}
private static boolean doCreateXpathExpr(Object content, StringBuilder builder) {
if (builder.length() > 0) {
builder.insert(0, "/");
}
Element parentElement;
if (content instanceof Element) {
Element element = (Element) content;
final List<Attribute> attributes = element.getAttributes();
doCreateAttributesXpathExpr(builder, attributes);
final String textTrim = element.getTextTrim();
if (!textTrim.isEmpty()) {
boolean addToCondition = builder.length() > 0 && builder.charAt(0) == '[';
if (!addToCondition) {
builder.insert(0, "']");
} else {
builder.deleteCharAt(0);
builder.insert(0, "' and ");
}
builder.insert(0, textTrim).insert(0, "[normalize-space(text()) = '");
}
builder.insert(0, element.getName());
if (element.getNamespacePrefix() != null && !element.getNamespacePrefix().trim().isEmpty()) {
builder.insert(0, ':').insert(0, element.getNamespacePrefix());
}
parentElement = element.getParentElement();
} else if (content instanceof Text) {
final Text text = (Text) content;
builder.insert(0, "text()");
parentElement = text.getParentElement();
} else if (content instanceof Attribute) {
Attribute attribute = (Attribute) content;
builder.insert(0, attribute.getName());
if (attribute.getNamespacePrefix() != null && !attribute.getNamespacePrefix().trim().isEmpty()) {
builder.insert(0, ':').insert(0, attribute.getNamespacePrefix());
}
builder.insert(0, '@');
parentElement = attribute.getParent();
} else {
parentElement = null;
}
if (parentElement != null && parentElement.getParentElement() != null) {
return doCreateXpathExpr(parentElement, builder);
}
return true;
}
private static void doCreateAttributesXpathExpr(StringBuilder builder, List<Attribute> attributes) {
if (!attributes.isEmpty()) {
StringBuilder attBuilder = new StringBuilder("[");
for (Attribute attribute : attributes) {
if (attBuilder.length() > 1) {
attBuilder.append(" and ");
}
attBuilder.append('@');
if (attribute.getNamespacePrefix() != null && !attribute.getNamespacePrefix().trim().isEmpty()) {
attBuilder.append(attribute.getNamespacePrefix()).append(':');
}
attBuilder.append(attribute.getName()).append(" = '").append(attribute.getValue()).append('\'');
}
attBuilder.append("]");
builder.insert(0, attBuilder);
}
}
}