All Downloads are FREE. Search and download functionalities are using the official Maven repository.

se.idsec.signservice.xml.DOMUtils Maven / Gradle / Ivy

/*
 * Copyright 2019-2024 IDsec Solutions AB
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package se.idsec.signservice.xml;

import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
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.StringWriter;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import java.util.Queue;
import java.util.WeakHashMap;
import java.util.concurrent.ArrayBlockingQueue;

/**
 * Utilities for processing DOM documents.
 *
 * @author Martin Lindström ([email protected])
 * @author Stefan Santesson ([email protected])
 */
public class DOMUtils {

  /** The document builder factory. */
  private static final DocumentBuilderFactory documentBuilderFactory;

  /** DOM transformer for pretty printing of XML nodes. */
  private static final Transformer prettyPrintTransformer;

  /** DOM transformer. */
  private static final Transformer transformer;

  /** We lovingly borrow from Apache's xmlsec ... States the parser pool size. */
  private static final int parserPoolSize = Integer.getInteger("org.apache.xml.security.parser.pool-size", 20);

  /** Map of classloaders and queues of document builders. */
  private static final Map> documentBuilders = Collections.synchronizedMap(
      new WeakHashMap<>());

  static {
    try {
      documentBuilderFactory = DocumentBuilderFactory.newInstance();
      documentBuilderFactory.setNamespaceAware(true);
      documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
      documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
      documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
      documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
      documentBuilderFactory.setXIncludeAware(false);
      documentBuilderFactory.setExpandEntityReferences(false);
    }
    catch (final ParserConfigurationException e) {
      throw new RuntimeException("Failed to setup document builder factory", e);
    }

    try {
      final TransformerFactory transformerFactory = TransformerFactory.newInstance();
      prettyPrintTransformer = transformerFactory.newTransformer();
      prettyPrintTransformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
      prettyPrintTransformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
      prettyPrintTransformer.setOutputProperty(OutputKeys.METHOD, "xml");
      prettyPrintTransformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
      prettyPrintTransformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
      prettyPrintTransformer.setOutputProperty(OutputKeys.INDENT, "yes");

      transformer = transformerFactory.newTransformer();
      transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
    }
    catch (final TransformerConfigurationException e) {
      throw new RuntimeException("Failed to setup transformer", e);
    }
  }

  /**
   * Creates a new {link DocumentBuilder} instance.
   * 

* The document builder factory used is created according to the * OWASP recommendations for XML External Entity Prevention. *

* * @return a "safe" DocumentBuilder instance */ public static DocumentBuilder createDocumentBuilder() { try { return documentBuilderFactory.newDocumentBuilder(); } catch (final ParserConfigurationException e) { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Failed to create document builder"); } } /** * Pretty prints the supplied XML node to a string. * * @param node the XML node to pretty print * @return a formatted string */ public static String prettyPrint(final Node node) { if (node == null) { return ""; } try { final StringWriter writer = new StringWriter(); prettyPrintTransformer.transform(new DOMSource(node), new StreamResult(writer)); return writer.toString(); } catch (final Exception e) { return ""; } } /** * Transforms the supplied XML node into its canonical byte representation. * * @param node the XML node to transform * @return a byte array holding the XML document bytes */ public static byte[] nodeToBytes(final Node node) { try { final ByteArrayOutputStream output = new ByteArrayOutputStream(); transformer.transform(new DOMSource(node), new StreamResult(output)); return output.toByteArray(); } catch (final TransformerException e) { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Failed to transform XML node to bytes"); } } /** * Transforms the supplied XML node into its canonical byte representation and Base64-encoded these bytes. * * @param node the XML node to transform * @return the Base64-encoding of the XML node */ public static String nodeToBase64(final Node node) { return Base64.getEncoder().encodeToString(nodeToBytes(node)); } /** * Parses an input stream into a DOM document. * * @param stream the stream * @return a DOM document */ public static Document inputStreamToDocument(final InputStream stream) { ClassLoader loader = Thread.currentThread().getContextClassLoader(); if (loader == null) { loader = DOMUtils.class.getClassLoader(); } if (loader == null) { try { return createDocumentBuilder().parse(stream); } catch (final SAXException | IOException e) { throw new DOMException(DOMException.SYNTAX_ERR, "Failed to decode bytes into DOM document"); } } final Queue queue = getDocumentBuilderPool(loader); final DocumentBuilder documentBuilder = getDocumentBuilder(queue); try { return documentBuilder.parse(stream); } catch (final SAXException | IOException e) { throw new DOMException(DOMException.SYNTAX_ERR, "Failed to decode bytes into DOM document"); } finally { returnToPool(documentBuilder, queue); } } /** * Parses a byte array into a DOM document. * * @param bytes the bytes to parse * @return a DOM document */ public static Document bytesToDocument(final byte[] bytes) { return inputStreamToDocument(new ByteArrayInputStream(bytes)); } /** * Decodes a Base64 string and parses it into a DOM document. * * @param base64 the Base64-encoded string * @return a DOM document */ public static Document base64ToDocument(final String base64) { return bytesToDocument(Base64.getDecoder().decode(base64)); } /** * Gets a document builder pool (queue) for the given class loader. * * @param loader the class loader * @return a queue of document builders */ private static Queue getDocumentBuilderPool(final ClassLoader loader) { Queue queue = documentBuilders.get(loader); if (queue == null) { queue = new ArrayBlockingQueue<>(parserPoolSize); documentBuilders.put(loader, queue); } return queue; } /** * Gets a document builder from the given queue. If no document builder is available a new one is created. * * @param queue the queue * @return a document builder */ private static DocumentBuilder getDocumentBuilder(final Queue queue) { DocumentBuilder documentBuilder = queue.poll(); if (documentBuilder == null) { documentBuilder = createDocumentBuilder(); } return documentBuilder; } /** * Returns the given document builder to the pool (queue). * * @param documentBuilder the document builder * @param queue the pool */ private static void returnToPool(final DocumentBuilder documentBuilder, final Queue queue) { if (queue != null) { documentBuilder.reset(); queue.offer(documentBuilder); } } // Hidden constructor private DOMUtils() { } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy