/** * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2006 Sun Microsystems Inc. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * https://opensso.dev.java.net/public/CDDLv1.0.html or * opensso/legal/CDDLv1.0.txt * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at opensso/legal/CDDLv1.0.txt. * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * $Id: XMLUtils.java,v 1.15 2009/10/19 18:19:20 asyhuang Exp $ * */ /* * Portions Copyrighted 2011-2014 ForgeRock AS. */ package com.sun.identity.shared.xml; import com.sun.identity.shared.Constants; import com.sun.identity.shared.configuration.SystemPropertiesManager; import com.sun.identity.shared.datastruct.OrderedSet; import com.sun.identity.shared.debug.Debug; import com.sun.identity.shared.encode.Base64; import org.forgerock.openam.utils.DocumentBuilderProvider; import org.forgerock.openam.utils.Providers; import org.forgerock.openam.utils.SAXParserProvider; import org.w3c.dom.Attr; 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.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import javax.xml.bind.JAXBException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.forgerock.openam.utils.TransformerFactoryProvider; /** * This class contains utilities to parse XML documents */ public class XMLUtils { private static final Map EMPTY_MAP = Collections .unmodifiableMap(new HashMap()); // property to see if XML document validating is needed. The validating // is turned on only if the value for com.iplanet.am.util.xml.validating // property is set to "on" and value for com.iplanet.services.debug.level // property is set to "warning" or "message". private static boolean validating = false; private static String ATTR_BASE64_ENCODED = "com_sun_identity_opensso_base64_encoded"; private static int ATTR_BASE64_ENCODED_LENGTH = ATTR_BASE64_ENCODED.length(); private static final String INVALID_XML_CHARACTERS = "[\u0000-\u0008\u000b-\u001f\ufffe\uffff]"; private static Pattern invalidXMLChars = Pattern.compile(INVALID_XML_CHARACTERS); static { try { String xmlVal = SystemPropertiesManager.get( Constants.XML_VALIDATING, "off"); String debugLevel = SystemPropertiesManager.get( Constants.SERVICES_DEBUG_LEVEL, "error"); if (xmlVal.trim().equalsIgnoreCase("on") && (debugLevel.trim().equalsIgnoreCase("warning") || debugLevel.trim().equalsIgnoreCase("message")) ) { validating = true; } } catch (Exception e) { // ignore since there is not debug class here } } /** * Size of document builder cache. */ private static final int DOCBUILDER_CACHE_SIZE = SystemPropertiesManager.getAsInt(Constants.XML_DOCUMENT_BUILDER_CACHE_SIZE, 500); /** * Size of the SAXParser cache. Defaults to same size as document builder cache. */ private static final int SAXPARSER_CACHE_SIZE = SystemPropertiesManager.getAsInt(Constants.XML_SAXPARSER_CACHE_SIZE, DOCBUILDER_CACHE_SIZE); private static final int TRANSFORMER_FACTORY_CACHE_SIZE = SystemPropertiesManager.getAsInt(Constants.XML_TRANSFORMER_FACTORY_CACHE_SIZE, 500); /** * Provider for DocumentBuilder instances. Caches in an LRU cache per thread. */ private static final DocumentBuilderProvider DOCUMENT_BUILDER_PROVIDER = Providers.documentBuilderProvider(DOCBUILDER_CACHE_SIZE); /** * Provider for SAXParser instances. Caches in a per-thread LRU cache. */ private static final SAXParserProvider SAX_PARSER_PROVIDER = Providers.saxParserProvider(SAXPARSER_CACHE_SIZE); /** * Provider for TransformerFactory instances. Caches in a per-thread LRU cache. */ private static final TransformerFactoryProvider TRANSFORMER_FACTORY_PROVIDER = Providers.transformerFactoryProvider(TRANSFORMER_FACTORY_CACHE_SIZE); public String getATTR_VALUE_PAIR_NODE() { return ATTR_VALUE_PAIR_NODE; } public static boolean isValidating() { return validating; } /** * Converts the XML document from a String format to DOM Document format. * * @param xmlString * is the XML document in a String format * @param debug * is the debug object used for logging debug info * @return Document is the DOM object obtained by converting the String XML * Returns null if xmlString is null. Returns null if there are any * parser errores. */ public static Document toDOMDocument(String xmlString, Debug debug) { if ((xmlString == null) || (xmlString.length() == 0)) { return null; } try { ByteArrayInputStream is = new ByteArrayInputStream(xmlString .getBytes("UTF-8")); return toDOMDocument(is, debug); } catch (UnsupportedEncodingException uee) { if (debug != null && debug.warningEnabled()) { debug.warning("Can't parse the XML document:\n" + xmlString, uee); } return null; } } /** * Converts the XML document from an input stream to DOM Document format. * * @param is * is the InputStream that contains XML document * @return Document is the DOM object obtained by parsing the input stream. * Returns null if there are any parser errores. */ public static Document toDOMDocument(InputStream is, Debug debug) { DocumentBuilder documentBuilder = null; try { // Assign new debug object documentBuilder = getSafeDocumentBuilder(validating); } catch (ParserConfigurationException pe) { if (debug != null) { debug.error("XMLUtils.DocumentBuilder init failed", pe); } } try { if (documentBuilder == null) { if (debug != null) { debug.error("XMLUtils.toDOM : null builder instance"); } return null; } if (debug != null && debug.warningEnabled()) { documentBuilder.setErrorHandler(new ValidationErrorHandler( debug)); } return documentBuilder.parse(is); } catch (Exception e) { // Since there may potentially be several invalid XML documents // that are mostly client-side errors, only a warning is logged for // efficiency reasons. if (debug != null && debug.warningEnabled()) { debug.warning("Can't parse the XML document", e); } return null; } } /** * This method parse an Attributes tag, DTD for Attribute is as follows. * *
* < !-- This DTD defines the DPro Attribute tag. * Unique Declaration name for DOCTYPE tag: * "Directory Pro 5.0 Attributes DTD" * --> * < !ELEMENT Attributes (Attribute)+> * < !ELEMENT Attribute EMPTY> * < !ATTLIST Attribute * name NMTOKEN #REQUIRED * > ** * @param n * Node * @return Set Set of the attribute names */ public static Set parseAttributesTag(Node n) { // get Attribute node list NodeList attributes = n.getChildNodes(); final int numAttributes = attributes.getLength(); if (numAttributes == 0) { return null; } Set set = new HashSet(); for (int l = 0; l < numAttributes; l++) { // get next attribute Node attr = attributes.item(l); if ((attr.getNodeType() != Node.ELEMENT_NODE) && !attr.getNodeName().equals("Attribute")) { // need error handling continue; } set.add(((Element) attr).getAttribute("name")); } return set; } /** * @param parentNode * is the element tag that contains all the AttirbuteValuePair * tags as children * @return Map Returns the AV pairs in a Map where each entry in the Map is * an AV pair. The key is the attribute name and the value is a Set * of String objects. */ public static Map
&
,
* <
, >
, "
, '
with corresponding entity references.
*
* @param text The input that needs to be escaped. May be null.
* @return String with the special characters replaced with entity references. May be null.
*/
public static String escapeSpecialCharacters(String text) {
text = removeInvalidXMLChars(text);
if (text == null || text.isEmpty()) {
return text;
}
final StringBuilder sb = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
switch (c) {
case '&':
sb.append("&");
break;
case '<':
sb.append("<");
break;
case '>':
sb.append(">");
break;
case '\"':
sb.append(""");
break;
case '\'':
sb.append("'");
break;
case '\n':
sb.append("
");
break;
case '\r':
sb.append("
");
break;
default:
sb.append(c);
}
}
return sb.toString();
}
private static boolean invalidXMLCharExists(String st) {
Matcher matcher = invalidXMLChars.matcher(st);
return matcher.find();
}
/**
* Remove invalid XML characters from a string.
* @param text the text to cleanse.
* @return cleansed text or the original string if it is null or empty
*/
public static String removeInvalidXMLChars(String text) {
if (text == null || text.isEmpty()) {
return text;
}
return text.replaceAll(INVALID_XML_CHARACTERS, "");
}
public static Set encodeAttributeSet(Set set, Debug debug) {
if (set == null) {
return set;
}
Set newSet = new HashSet();
Iterator iter = set.iterator();
while(iter.hasNext()) {
Object obj = iter.next();
String st = (String)obj;
if (invalidXMLCharExists(st)) {
st = Base64.encode(st.getBytes());
if ((debug != null) && (debug.warningEnabled())) {
debug.warning("XMLUtils.encodeAttributeSet invalid XML characters get Base64 encoded to be : " + st);
}
st = ATTR_BASE64_ENCODED + st;
}
newSet.add(st);
}
return newSet;
}
public static Set decodeAttributeSet(Set set) {
if (set == null) {
return set;
}
Set newSet = new HashSet();
Iterator iter = set.iterator();
while(iter.hasNext()) {
Object obj = iter.next();
String st = (String)obj;
if (st.startsWith(ATTR_BASE64_ENCODED)) {
st = new String(Base64.decode(
st.substring(ATTR_BASE64_ENCODED_LENGTH)));
}
newSet.add(st);
}
return newSet;
}
public static String removeNullCharAtEnd(String st) {
int index = st.length() - 1;
char c = st.charAt(index);
if (c == '\u0000') {
return st.substring(0, index);
}
return st;
}
/**
* Provides a secure DocumentBuilder implementation, which is protected against
* different types of entity expansion attacks and makes sure that only locally
* available DTDs can be referenced within the XML document.
* @param validating Whether the returned DocumentBuilder should validate input.
* @return A secure DocumentBuilder instance.
* @throws ParserConfigurationException In case xerces does not support one
* of the required features.
*/
public static DocumentBuilder getSafeDocumentBuilder(boolean validating) throws ParserConfigurationException {
return DOCUMENT_BUILDER_PROVIDER.getDocumentBuilder(validating);
}
/**
* Provides a secure SAXParser instance, which is protected against different
* types of entity expension, DoS attacks and makes sure that only locally
* available DTDs can be referenced within the XML document.
* @param validating Whether the returned DocumentBuilder should validate input.
* @return A secure SAXParser instance.
* @throws ParserConfigurationException In case Xerces does not support one of
* the required features.
* @throws SAXException In case Xerces does not support one of the required
* features.
*/
public static SAXParser getSafeSAXParser(boolean validating) throws ParserConfigurationException, SAXException {
return SAX_PARSER_PROVIDER.getSAXParser(validating);
}
/**
* Provides a cached {@link TransformerFactory} instance for the current thread.
*
* @return A cached {@link TransformerFactory} instance.
*/
public static TransformerFactory getTransformerFactory() {
return TRANSFORMER_FACTORY_PROVIDER.getTransformerFactory();
}
/**
* Creates a SAXSource instance based on the incoming InputSource, which
* later on can be safely used by JAXB unmarshalling. The SAXSource will be
* protected against different types of entity expansion, DoS attacks and
* makes sure that only locally available DTDs can be referenced within the
* XML document.
* @param source The InputSource to be unmarshalled by JAXB
* @return A safe SAXSource instance
* @throws JAXBException In case an error occurs while creating the SAXSource
*/
public static SAXSource createSAXSource(InputSource source) throws JAXBException{
try {
SAXParser saxParser = getSafeSAXParser(false);
return new SAXSource(saxParser.getXMLReader(), source);
} catch (Exception ex) {
//Let's convert the exception to a JAXBException, so the unmarshalling
//codes can handle the failure.
throw new JAXBException("Unable to create SAXSource", ex);
}
}
private static String ATTR_VALUE_PAIR_NODE = "AttributeValuePair";
private static String VALUE_NODE = "Value";
}
class ValidationErrorHandler implements ErrorHandler {
private Debug debug;
ValidationErrorHandler(Debug tmpDebug) {
debug = tmpDebug;
}
public void fatalError(SAXParseException spe) throws SAXParseException {
if (debug != null) {
debug.error("XMLUtils.fatalError", spe);
}
}
public void error(SAXParseException spe) throws SAXParseException {
if (debug != null) {
debug.warning("XMLUtils.error", spe);
}
}
public void warning(SAXParseException spe) throws SAXParseException {
if ((debug != null) && (debug.warningEnabled())) {
debug.warning("XMLUtils.warning", spe);
}
}
}