/* * Copyright (C) 2000-2015 aw2.0 LTD * * This file is part of Open BlueDragon (OpenBD) CFML Server Engine. * * OpenBD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * Free Software Foundation,version 3. * * OpenBD 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 OpenBD. If not, see http://www.gnu.org/licenses/ * * Additional permission under GNU GPL version 3 section 7 * * If you modify this Program, or any covered work, by linking or combining * it with any of the JARS listed in the README.txt (or a modified version of * (that library), containing parts covered by the terms of that JAR, the * licensors of this Program grant you additional permission to convey the * resulting work. * README.txt @ http://www.openbluedragon.org/license/README.txt * * http://www.openbd.org/ * $Id: cfXmlData.java 2506 2015-02-08 22:25:59Z alan $ */ package com.naryx.tagfusion.cfm.xml; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import java.io.StringWriter; import java.io.Writer; import java.net.MalformedURLException; import java.net.URL; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.dom.DOMSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import org.w3c.dom.DOMException; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSOutput; import org.xml.sax.EntityResolver; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import com.naryx.tagfusion.cfm.engine.catchDataFactory; import com.naryx.tagfusion.cfm.engine.cfData; import com.naryx.tagfusion.cfm.engine.cfSession; import com.naryx.tagfusion.cfm.engine.cfStructData; import com.naryx.tagfusion.cfm.engine.cfmRunTimeException; import com.naryx.tagfusion.cfm.tag.cfDUMP; import com.naryx.tagfusion.cfm.xml.parse.NoValidationResolver; import com.naryx.tagfusion.cfm.xml.parse.ValidationErrorHandler; import com.naryx.tagfusion.cfm.xml.parse.ValidationInputSource; import com.naryx.tagfusion.cfm.xml.parse.ValidationResolver; import com.naryx.tagfusion.cfm.xml.parse.ValidatorSource; import com.naryx.tagfusion.cfm.xml.parse.XmlSource; public class cfXmlData extends cfStructData implements Serializable { private static final long serialVersionUID = 1; /* Used as the flag to check if current parser is JAXP 1.3 compliant */ private static boolean complianceChecked = false; /* Used to get the JDK version number for working around Xerces bug in JDK 1.5 */ private static String jdkVer = System.getProperty("java.version"); /** * Default constructor. Performs the JAXP 1.3 compliance check if not already checked. * * @param n * Node to create a new cfXmlData with * @param caseSensitive * true if the cfXmlData should be caseSensitive, false otherwise */ public cfXmlData(Node n, boolean caseSensitive) throws cfmRunTimeException { super(XmlHashtableFactory.newXmlHashtable(n, caseSensitive)); if (!complianceChecked) checkCompliance(); } /** * Internal constructor that bypasses the compliance check. * * @param xmlData * cfXmlData that is calling this method * @param n * Node to create a new cfXmlData with * @param caseSensitive * true if the cfXmlData should be caseSensitive, false otherwise */ protected cfXmlData(cfXmlData xmlData, Node n, boolean caseSensitive) { super(XmlHashtableFactory.newXmlHashtable(n, caseSensitive)); } /** * Internal constructor that bypasses the compliance check. * * @param xmlHashtable * XmlHashtable that is calling this method * @param n * Node to create a new cfXmlData with * @param caseSensitive * true if the cfXmlData should be caseSensitive, false otherwise */ protected cfXmlData(XmlHashtable xmlHashtable, Node n, boolean caseSensitive) { super(XmlHashtableFactory.newXmlHashtable(n, caseSensitive)); } /** * Checks that the underlying XML parser is JAXP 1.3 compliant. Throws a cfmRunTimeException * if it is not compliant. * * @throws cfmRunTimeException */ protected static void checkCompliance() throws cfmRunTimeException { // Verify that the XML parser is JAX 1.3 compliant try { DOMImplementation domImpl = DocumentBuilderFactory.newInstance().newDocumentBuilder().getDOMImplementation(); if (!DOMImplementationLS.class.isAssignableFrom(domImpl.getClass())) { String errMsg = "The configured XML parser does not support JAXP 1.3. Please configure the " + "JVM to use a JAXP 1.3 compliant XML parser. If using Sun Microsystems' JDK 1.5, " + "this can be done by setting the system property javax.xml.parsers.DocumentBuilderFactory " + "to \"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl\"."; throw new cfmRunTimeException(catchDataFactory.generalException("errorCode.runtimeError", errMsg)); } } catch (Exception ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } complianceChecked = true; } public String getDataTypeName() { return "xml"; } public cfData duplicate() { Node cn = getXMLNode().cloneNode(true); return new cfXmlData(this, cn, isCaseSensitive()); } public void importXml(cfXmlData toAdd, boolean overwrite) throws cfmRunTimeException { try { // Get the document object (for importing) Document doc = null; if (getXMLNode().getNodeType() == Node.DOCUMENT_NODE) doc = (Document) getXMLNode(); else doc = getXMLNode().getOwnerDocument(); // Import the node Node child = doc.importNode(toAdd.getXMLNode(), true); if (!overwrite) { NodeList nl = getXMLNode().getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { if (nl.item(i).getNodeName().equals(child.getNodeName())) return; } } getXMLNode().appendChild(child); } catch (DOMException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } } public Node getXMLNode() { return ((XmlHashtable) getHashData()).getXMLNode(); } /* * XmlNode api that needed to be exposed for other function manipulation */ public String getName() { return getXMLNode().getNodeName(); } public cfXmlData getParent() throws cfmRunTimeException { if (getXMLNode().getParentNode() != null) return new cfXmlData(getXMLNode().getParentNode(), isCaseSensitive()); else return null; } public void removeChild(cfXmlData child) throws cfmRunTimeException { try { getXMLNode().removeChild(child.getXMLNode()); } catch (DOMException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } } public void insertBefore(cfXmlData child1, cfXmlData child2) throws cfmRunTimeException { try { getXMLNode().insertBefore(child1.getXMLNode(), child2.getXMLNode()); } catch (DOMException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } } public void appendChild(cfXmlData child) throws cfmRunTimeException { try { getXMLNode().appendChild(child.getXMLNode()); } catch (DOMException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } } public String getString() { return toString(); // this is the original code; it was modified to support the ToString() // function; // it's not clear if this method is ever invoked from anywhere else--if // it is, // we may need to put this code back and find a different solution for // ToString() // String val = getXMLNode().getNodeValue(); // if (val == null) // return ""; // else // return val; } public String toString() { return cfXmlData.toString(getXMLNode()); } public static String toString(Node n) { Writer writer = new StringWriter(); Node tmp = null; Document doc = n.getOwnerDocument(); if (doc == null) doc = (Document) n; DOMImplementation impl = doc.getImplementation(); DOMImplementationLS implls = null; if (DOMImplementationLS.class.isAssignableFrom(impl.getClass())) { implls = (DOMImplementationLS) impl; LSOutput lsout = implls.createLSOutput(); lsout.setCharacterStream(writer); try { // Try to avoid serializing nodes that have no children. // This is a patch to avoid bug XERCESJ-1023. // See http://issues.apache.org/jira/browse/XERCESJ-1023 if (n.getFirstChild() == null && jdkVer.startsWith("1.5")) { // Add an empty node child to n so that the call to prepareForSerialization() // traverses down before it walks back up. This is necessary for the // traversal to terminate back at n. Otherwise the traversal will encounter // a NPE at the document's parent. tmp = doc.createTextNode(""); n.appendChild(tmp); } implls.createLSSerializer().write(n, lsout); } finally { if (tmp != null) { // Remove the temp node. n.removeChild(tmp); } } } else if (!complianceChecked) { // Very few ways to get here without a compliance check // but it is posible, so let's log the problem and move on. try { checkCompliance(); } catch (cfmRunTimeException ex) { com.nary.Debug.printStackTrace(ex); } } /* * Old Transformer way of doing things that doesn't handle namespace fix up. * writer = new StringWriter(); * try * { * TransformerFactory transformerFactory = TransformerFactory.newInstance(); * Transformer transformer = transformerFactory.newTransformer(); * transformer.setOutputProperty(OutputKeys.METHOD, "xml"); * transformer.setOutputProperty(OutputKeys.INDENT, "yes"); * transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); * transformer.transform(new DOMSource(n), new StreamResult(writer)); * } * catch (TransformerException ex) * { * com.nary.Debug.printStackTrace(ex); * } */ return writer.toString().trim(); } public void dump(PrintWriter out) { dump(out, "", cfDUMP.TOP_DEFAULT); } public void dump(PrintWriter out, String label, int _top) { ((XmlHashtable) getHashData()).dump(out, label, _top); } public void dumpLong(PrintWriter out) { dumpLong(out, "", cfDUMP.TOP_DEFAULT); } public void dumpLong(PrintWriter out, String _label, int _top) { ((XmlHashtable) getHashData()).dumpLong(out, _label, _top); } /** * Parses and potentially validates a xml document using the specified * ValidatorSource. The InputSourceGenerator produces InputSource objects that wrap * the xml document. The ValiatorSource wraps the validation object (DTD/Schema). * Returns a parsed and potentially validated cfXmlData instance. * Note, because DTD validation occurs during parsing, and xml schema validation takes * place after parsing, we need to separate the validation types into two steps. * Also note, if a customHandler is specified, it may not throw any parse or * validation errors/warnings. Which could lead to a null Document being returned. * * @param xs * generator that creates new InputSource instances * @param validator * wrapper for the validation object (DTD/Schema) * @param customHandler * custom ErrorHandler, may be null * @return parsed and potentially validated xml object, or null * @throws IOException * @throws SAXException * @throws ParserConfigurationException */ protected static Document parseXml(XmlSource xs, ValidatorSource validator, ErrorHandler customHandler) throws IOException, SAXException, ParserConfigurationException { InputSource is = null; boolean schemaValidationRequired = false; boolean dtdRemovalRequired = false; EntityResolver dtdResolver = null; DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance(); fact.setNamespaceAware(true); fact.setIgnoringElementContentWhitespace(true); if (validator.isNull()) { // Return empty content for all entity references (presumably from a <!DOCTYPE ...> // element) so the parser will parse without any DTD validation and not complain when // resolving <!DOCTYPE ...> entity references (if it exists). is = new ValidationInputSource(xs, ValidationInputSource.NO_CHANGE); dtdResolver = new NoValidationResolver(); } else if (validator.isSchema()) { // Return empty content for all entity references (presumably from a <!DOCTYPE ...> // element) so the parser will parse without any DTD validation and not complain when // resolving <!DOCTYPE ...> entity references (if it exists). is = new ValidationInputSource(xs, ValidationInputSource.NO_CHANGE); dtdResolver = new NoValidationResolver(); // Note that we must do some post parse xml schema validation. schemaValidationRequired = true; } else if (validator.isEmptyString() && !xs.hasDTD()) { // Return empty content for all entity references (presumably from a <!DOCTYPE ...> // element) so the parser will parse without any DTD validation and not complain when // resolving <!DOCTYPE ...> entity references (if it exists). is = new ValidationInputSource(xs, ValidationInputSource.NO_CHANGE); dtdResolver = new NoValidationResolver(); // Note that we must do some post parse xml schema validation. This assumes // that the xml doc has some embedded xml schema reference. schemaValidationRequired = true; } else if (validator.isEmptyString()) { // Best have DTD referenced in the xml source. Set DTD validation to true, // leave the existing <!DOCTYPE ...> element intact. fact.setValidating(true); if (customHandler == null) customHandler = new ValidationErrorHandler(); is = new ValidationInputSource(xs, ValidationInputSource.NO_CHANGE); } else { // Must have specified a DTD validator object so set DTD validation // to true, read the <!DOCTYPE ...> element while parsing and return // the specified DTD validator during entity reference lookup, or if // no <!DOCTYPE ..> element exists, add our own so we can return the // specified DTD validator content during entity reference lookup. fact.setValidating(true); if (customHandler == null) customHandler = new ValidationErrorHandler(); dtdRemovalRequired = !xs.hasDTD(); ValidationResolver vr = new ValidationResolver(validator); dtdResolver = vr; is = new ValidationInputSource(xs, vr, ValidationInputSource.READ_ADD); } DocumentBuilder parser = fact.newDocumentBuilder(); parser.setEntityResolver(dtdResolver); // if these are null, it doesn't matter, parser.setErrorHandler(customHandler); // setting these won't change default behavior Document doc = parser.parse(is); if (doc != null) { doc.normalize(); // Now see if we need to do any schema validation if (schemaValidationRequired) validateXml(doc, validator, customHandler); } // Remove the inserted DTD (if necessary) if (doc != null && dtdRemovalRequired && doc.getDoctype() != null) doc.removeChild(doc.getDoctype()); // Return the parsed (and possibly validated Document) return doc; } /** * Validates the specified Document against a ValidatorSource. The ValidatorSource * could represent a DTD or xml schema document. If a validation exception arises, it * will be thrown. * * @param doc * Document to validate * @param validator * ValidatorSource to validate against * @throws cfmRunTimeException */ public static void validateXml(Node node, ValidatorSource validator) throws cfmRunTimeException { validateXml(node, validator, (cfStructData) null); } /** * Validates the specified Document against a ValidatorSource. The ValidatorSource * could represent a DTD or xml schema document. If a validation exception arises, it * will be thrown. However, if the validationMsgs struct is specified a custom * ErrorHandler will be used during validation that collects the validation exceptions * and places them in the specified validationMsgs struct. No error/warning exceptions * will be thrown. * * @param doc * Document to validate * @param validator * ValidatorSource to validate against * @param validationMsgs * cfStructData to collect all the validation messages * @throws cfmRunTimeException */ public static void validateXml(Node node, ValidatorSource validator, cfStructData validationMsgs) throws cfmRunTimeException { ErrorHandler customHandler = null; if (validationMsgs != null) customHandler = new ValidationErrorHandler(validationMsgs); try { validateXml(node, validator, customHandler); } catch (IOException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } catch (SAXParseException ex) { // Since we will have already received/handled this in // our custom ErrorHandler, we don't need to re-handle it. if (customHandler == null) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } } catch (SAXException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } } /** * Validates the specified Document against a ValidatorSource. The ValidatorSource * could represent a DTD or xml schema document. If a validation exception arises, it * will be thrown. However, if the customHandler is specified and it does not throw * error/warning exceptions, it may be the case that this method does not throw * validation exceptions. * * @param doc * Document to validate * @param validator * ValidatorSource to validate against * @param customHandler * custom ErrorHandler, or null * @throws IOException * @throws SAXException */ protected static void validateXml(Node node, ValidatorSource validator, ErrorHandler customHandler) throws IOException, SAXException { SchemaFactory fact = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); if (customHandler == null) customHandler = new ValidationErrorHandler(); fact.setErrorHandler(customHandler); Schema schema = null; // Either use the specified xml schema or assume the xml document // itself has xml schema location hints. if (validator.isEmptyString()) schema = fact.newSchema(); else schema = fact.newSchema(validator.getAsSource()); // Validate it against xml schema Validator v = schema.newValidator(); v.setErrorHandler(customHandler); v.validate(new DOMSource(node)); } /** * Supports parsing File instances, URL instances (that point to the xml content), and * String instances (that represent the xml content itself). The validator object is * also an instance of File, URL, or String. It represents a DTD or xml schema * definition. Returns the parsed xml as an object. * * @param value * a File, URL, or String * @param caseSensitive * true if the cfXmlData is created with case sensitivity, false * otherwise * @param validator * a File, URL, or String * @return parsed xml as a cfXmlData object * @throws cfmRunTimeException */ public static cfXmlData parseXml(Object value, boolean caseSensitive, Object validator) throws cfmRunTimeException { return parseXml(value, caseSensitive, validator, null); } /** * Supports parsing File instances, URL instances (that point to the xml content), and * String instances (that represent the xml content itself). The validator object is * also an instance of File, URL, or String. It represents a DTD or xml schema * definition. Returns the parsed xml as an object. If parseMsgs is specified, a * custom ErrorHandler will be used during parsing and validation that will collect * all the error/warning messages and add them to the specified parseMsgs struct. * Note, no parsing or validation exceptions will be thrown if this parameter is * non-null. Therefore the value of the return may be null. * * @param value * a File, URL, or String * @param caseSensitive * true if the cfXmlData is created with case sensitivity, false * otherwise * @param validator * a File, URL, or String * @param parseMsgs * cfStructData to collect all the parse/validation messages * @return parsed xml as a cfXmlData object * @throws cfmRunTimeException */ public static cfXmlData parseXml(Object value, boolean caseSensitive, Object validator, cfStructData parseMsgs) throws cfmRunTimeException { XmlSource isg = null; ValidatorSource vs = null; ValidationErrorHandler customHandler = null; String msg = null; if (value instanceof XmlSource) isg = (XmlSource) value; else isg = new XmlSource(value); if (validator instanceof ValidatorSource) vs = (ValidatorSource) validator; else vs = new ValidatorSource(validator); if (parseMsgs != null) customHandler = new ValidationErrorHandler(parseMsgs); Document doc = null; try { doc = parseXml(isg, vs, customHandler); } catch (IOException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } catch (SAXParseException ex) { // Since we will have already received/handled this in // our custom ErrorHandler, we don't need to re-handle it. if (customHandler == null) { msg = ex.getMessage() + " Line: " + ex.getLineNumber() + " Column: " + ex.getColumnNumber(); throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), msg, ex)); } } catch (SAXException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } catch (ParserConfigurationException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } catch (IllegalArgumentException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } // Return if (doc != null) return new cfXmlData(doc, caseSensitive); else return null; } /** * Interprets a String value to determine if it represents a File path, or a URL path, * or xml data (DTD/Schema data) itself. Returns a File object, or URL object, or * String object accordingly. * * @param _session * cfSession to determine current working directory * @param str * value to interpret * @return a File, URL, or String object * @throws cfmRunTimeException */ public static Object interpretString(cfSession _session, String str) throws cfmRunTimeException { if (str == null) { return null; } else { str = str.trim(); if (str.isEmpty() || str.charAt(0) == '<') { return str; } else if (str.toLowerCase().startsWith("http:") || str.toLowerCase().startsWith("https:") || str.toLowerCase().startsWith("ftp:")) { // Open a URL to the data try { URL url = new URL(str); return url; } catch (MalformedURLException ex) { throw new cfmRunTimeException(catchDataFactory.javaMethodException("errorCode.javaException", ex.getClass().getName(), ex.getMessage(), ex)); } } else { // Try it as a file File f = new File(str); if (!f.exists()){ f = new File(_session.getPresentDirectory(), str); if (!f.exists()) throw new cfmRunTimeException(catchDataFactory.generalException("errorCode.runtimeError", "Cannot locate: " + str)); } return f; } } } public static cfXmlData newInstance(Object obj, boolean caseSensitive) { try { return new cfXmlData((Node) obj, caseSensitive); } catch (cfmRunTimeException ex) { // Just log it and return a cfXmlData object anyway. com.nary.Debug.printStackTrace(ex); return new cfXmlData((cfXmlData) null, (Node) obj, caseSensitive); } } public static boolean isXmlObject(Object obj) { if (obj == null) return false; else return (Node.class.isAssignableFrom(obj.getClass())); } }