From a9de6888f932bda80ba30aa1ef341e463d3113a8 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 31 Jul 2008 11:12:56 +0000 Subject: [PATCH] SWS-404 --- .../AbstractDom4jPayloadEndpoint.java | 48 +++ .../endpoint/AbstractDomPayloadEndpoint.java | 71 +++- .../endpoint/AbstractJDomPayloadEndpoint.java | 57 +++- .../endpoint/AbstractXomPayloadEndpoint.java | 322 +++++++++++++----- .../endpoint/XomPayloadEndpointTest.java | 7 - 5 files changed, 398 insertions(+), 107 deletions(-) diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractDom4jPayloadEndpoint.java b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractDom4jPayloadEndpoint.java index 5297546e..6802c455 100644 --- a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractDom4jPayloadEndpoint.java +++ b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractDom4jPayloadEndpoint.java @@ -17,12 +17,17 @@ package org.springframework.ws.server.endpoint; import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.dom.DOMSource; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; +import org.dom4j.io.DOMReader; import org.dom4j.io.DocumentResult; import org.dom4j.io.DocumentSource; +import org.w3c.dom.Node; + import org.springframework.xml.transform.TransformerObjectSupport; /** @@ -38,6 +43,17 @@ import org.springframework.xml.transform.TransformerObjectSupport; */ public abstract class AbstractDom4jPayloadEndpoint extends TransformerObjectSupport implements PayloadEndpoint { + private boolean alwaysTransform = false; + + /** + * Set if the request {@link Source} should always be transformed into a new {@link DocumentResult}. + *

+ * Default is {@code false}, which is faster. + */ + public void setAlwaysTransform(boolean alwaysTransform) { + this.alwaysTransform = alwaysTransform; + } + public final Source invoke(Source request) throws Exception { Element requestElement = null; if (request != null) { @@ -50,6 +66,38 @@ public abstract class AbstractDom4jPayloadEndpoint extends TransformerObjectSupp return responseElement != null ? new DocumentSource(responseElement) : null; } + /** + * Returns the payload element of the given source. + *

+ * Default implementation checks whether the source is a {@link javax.xml.transform.dom.DOMSource}, and uses a + * {@link org.jdom.input.DOMBuilder} to create a JDOM {@link org.jdom.Element}. In all other cases, or when + * {@linkplain #setAlwaysTransform(boolean) alwaysTransform} is {@code true}, the source is transformed into a + * {@link org.jdom.transform.JDOMResult}, which is more expensive. If the passed source is {@code null}, {@code + * null} is returned. + * + * @param source the source to return the root element of; can be {@code null} + * @return the document element + * @throws javax.xml.transform.TransformerException + * in case of errors + */ + protected Element getDocumentElement(Source source) throws TransformerException { + if (source == null) { + return null; + } + if (!alwaysTransform && source instanceof DOMSource) { + Node node = ((DOMSource) source).getNode(); + if (node.getNodeType() == Node.DOCUMENT_NODE) { + DOMReader domReader = new DOMReader(); + Document document = domReader.read((org.w3c.dom.Document) node); + return document.getRootElement(); + } + } + // we have no other option than to transform + DocumentResult dom4jResult = new DocumentResult(); + transform(source, dom4jResult); + return dom4jResult.getDocument().getRootElement(); + } + /** * Template method. Subclasses must implement this. Offers the request payload as a dom4j Element, and * allows subclasses to return a response Element. diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractDomPayloadEndpoint.java b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractDomPayloadEndpoint.java index 2553d8a5..bac6ba0c 100644 --- a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractDomPayloadEndpoint.java +++ b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractDomPayloadEndpoint.java @@ -20,20 +20,23 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; -import org.springframework.xml.transform.TransformerObjectSupport; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import org.springframework.xml.transform.TransformerObjectSupport; /** * Abstract base class for endpoints that handle the message payload as DOM elements. *

- *

Offers the message payload as a DOM Element, and allows subclasses to create a response by returning - * an Element. + * Offers the message payload as a DOM Element, and allows subclasses to create a response by returning an + * Element. *

- *

An AbstractDomPayloadEndpoint only accept one payload element. Multiple payload elements are + * An AbstractDomPayloadEndpoint only accept one payload element. Multiple payload elements are * not in accordance with WS-I. * * @author Arjen Poutsma @@ -49,6 +52,8 @@ public abstract class AbstractDomPayloadEndpoint extends TransformerObjectSuppor private boolean namespaceAware = true; + private boolean alwaysTransform = false; + /** Set whether or not the XML parser should be XML namespace aware. Default is true. */ public void setNamespaceAware(boolean namespaceAware) { this.namespaceAware = namespaceAware; @@ -59,26 +64,24 @@ public abstract class AbstractDomPayloadEndpoint extends TransformerObjectSuppor this.validating = validating; } + /** + * Set if the request {@link Source} should always be transformed into a new {@link DOMResult}. + *

+ * Default is {@code false}, which is faster. + */ + public void setAlwaysTransform(boolean alwaysTransform) { + this.alwaysTransform = alwaysTransform; + } + public final Source invoke(Source request) throws Exception { if (documentBuilderFactory == null) { documentBuilderFactory = createDocumentBuilderFactory(); } DocumentBuilder documentBuilder = createDocumentBuilder(documentBuilderFactory); - Element requestElement = null; - if (request != null) { - Document requestDocument = documentBuilder.newDocument(); - DOMResult domResult = new DOMResult(requestDocument); - transform(request, domResult); - requestElement = (Element) requestDocument.getFirstChild(); - } + Element requestElement = getDocumentElement(request, documentBuilder); Document responseDocument = documentBuilder.newDocument(); Element responseElement = invokeInternal(requestElement, responseDocument); - if (responseElement != null) { - return new DOMSource(responseElement); - } - else { - return null; - } + return responseElement != null ? new DOMSource(responseElement) : null; } /** @@ -109,6 +112,39 @@ public abstract class AbstractDomPayloadEndpoint extends TransformerObjectSuppor return factory; } + /** + * Returns the payload element of the given source. + *

+ * Default implementation checks whether the source is a {@link DOMSource}, and returns the {@linkplain + * DOMSource#getNode() node} of that. In all other cases, or when {@linkplain #setAlwaysTransform(boolean) + * alwaysTransform} is {@code true}, the source is transformed into a {@link DOMResult}, which is more expensive. If + * the passed source is {@code null}, {@code null} is returned. + * + * @param source the source to return the root element of; can be {@code null} + * @param documentBuilder the document builder to be used for transformations + * @return the document element + * @throws TransformerException in case of errors + */ + protected Element getDocumentElement(Source source, DocumentBuilder documentBuilder) throws TransformerException { + if (source == null) { + return null; + } + if (!alwaysTransform && source instanceof DOMSource) { + Node node = ((DOMSource) source).getNode(); + if (node.getNodeType() == Node.ELEMENT_NODE) { + return (Element) node; + } + else if (node.getNodeType() == Node.DOCUMENT_NODE) { + return ((Document) node).getDocumentElement(); + } + } + // we have no other option than to transform + Document requestDocument = documentBuilder.newDocument(); + DOMResult domResult = new DOMResult(requestDocument); + transform(source, domResult); + return requestDocument.getDocumentElement(); + } + /** * Template method that subclasses must implement to process the request. *

@@ -123,4 +159,5 @@ public abstract class AbstractDomPayloadEndpoint extends TransformerObjectSuppor * @return the response element. Can be null to specify no response. */ protected abstract Element invokeInternal(Element requestElement, Document responseDocument) throws Exception; + } diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractJDomPayloadEndpoint.java b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractJDomPayloadEndpoint.java index 2ec8df7d..587026f9 100644 --- a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractJDomPayloadEndpoint.java +++ b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractJDomPayloadEndpoint.java @@ -17,10 +17,16 @@ package org.springframework.ws.server.endpoint; import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.dom.DOMSource; +import org.jdom.Document; import org.jdom.Element; +import org.jdom.input.DOMBuilder; import org.jdom.transform.JDOMResult; import org.jdom.transform.JDOMSource; +import org.w3c.dom.Node; + import org.springframework.xml.transform.TransformerObjectSupport; /** @@ -37,17 +43,56 @@ import org.springframework.xml.transform.TransformerObjectSupport; */ public abstract class AbstractJDomPayloadEndpoint extends TransformerObjectSupport implements PayloadEndpoint { + private boolean alwaysTransform = false; + + /** + * Set if the request {@link Source} should always be transformed into a new {@link JDOMResult}. + *

+ * Default is {@code false}, which is faster. + */ + public void setAlwaysTransform(boolean alwaysTransform) { + this.alwaysTransform = alwaysTransform; + } + public final Source invoke(Source request) throws Exception { - Element requestElement = null; - if (request != null) { - JDOMResult jdomResult = new JDOMResult(); - transform(request, jdomResult); - requestElement = jdomResult.getDocument().getRootElement(); - } + Element requestElement = getDocumentElement(request); Element responseElement = invokeInternal(requestElement); return responseElement != null ? new JDOMSource(responseElement) : null; } + /** + * Returns the payload element of the given source. + *

+ * Default implementation checks whether the source is a {@link DOMSource}, and uses a {@link DOMBuilder} to create + * a JDOM {@link Element}. In all other cases, or when {@linkplain #setAlwaysTransform(boolean) alwaysTransform} is + * {@code true}, the source is transformed into a {@link JDOMResult}, which is more expensive. If the passed source + * is {@code null}, {@code null} is returned. + * + * @param source the source to return the root element of; can be {@code null} + * @return the document element + * @throws TransformerException in case of errors + */ + protected Element getDocumentElement(Source source) throws TransformerException { + if (source == null) { + return null; + } + if (!alwaysTransform && source instanceof DOMSource) { + Node node = ((DOMSource) source).getNode(); + DOMBuilder domBuilder = new DOMBuilder(); + if (node.getNodeType() == Node.ELEMENT_NODE) { + return domBuilder.build((org.w3c.dom.Element) node); + } + else if (node.getNodeType() == Node.DOCUMENT_NODE) { + Document document = domBuilder.build((org.w3c.dom.Document) node); + return document.getRootElement(); + } + } + // we have no other option than to transform + JDOMResult jdomResult = new JDOMResult(); + transform(source, jdomResult); + return jdomResult.getDocument().getRootElement(); + } + /** * Template method. Subclasses must implement this. Offers the request payload as a JDOM Element, and * allows subclasses to return a response Element. diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractXomPayloadEndpoint.java b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractXomPayloadEndpoint.java index 6c61f45b..146db163 100644 --- a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractXomPayloadEndpoint.java +++ b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractXomPayloadEndpoint.java @@ -16,23 +16,39 @@ package org.springframework.ws.server.endpoint; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.util.Locale; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; import javax.xml.transform.Source; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamSource; +import nu.xom.Attribute; import nu.xom.Builder; import nu.xom.Document; import nu.xom.Element; +import nu.xom.NodeFactory; +import nu.xom.ParentNode; import nu.xom.ParsingException; +import nu.xom.Serializer; +import nu.xom.ValidityException; import nu.xom.converters.DOMConverter; import org.w3c.dom.Node; import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.springframework.core.NestedRuntimeException; +import org.springframework.xml.namespace.QNameUtils; import org.springframework.xml.transform.TransformerObjectSupport; +import org.springframework.xml.transform.TraxUtils; /** * Abstract base class for endpoints that handle the message payload as XOM elements. Offers the message payload as a @@ -47,91 +63,45 @@ import org.springframework.xml.transform.TransformerObjectSupport; */ public abstract class AbstractXomPayloadEndpoint extends TransformerObjectSupport implements PayloadEndpoint { - private DocumentBuilderFactory documentBuilderFactory; - public final Source invoke(Source request) throws Exception { Element requestElement = null; if (request != null) { - if (request instanceof DOMSource) { - requestElement = handleDomSource((DOMSource) request); + XomSourceCallback sourceCallback = new XomSourceCallback(); + try { + TraxUtils.doWithSource(request, sourceCallback); } - else if (request instanceof SAXSource) { - requestElement = handleSaxSource((SAXSource) request); - } - else if (request instanceof StreamSource) { - requestElement = handleStreamSource((StreamSource) request); - } - else { - throw new IllegalArgumentException("Source [" + request.getClass().getName() + - "] is neither SAXSource, DOMSource, nor StreamSource"); + catch (XomParsingException ex) { + throw (ParsingException) ex.getCause(); } + requestElement = sourceCallback.element; } Element responseElement = invokeInternal(requestElement); - Source result; - if (responseElement != null) { - if (documentBuilderFactory == null) { - createDocumentBuilderFactory(); - } - DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); - Document responseDocument = new Document(responseElement); - org.w3c.dom.Document w3cDocument = - DOMConverter.convert(responseDocument, documentBuilder.getDOMImplementation()); - return new DOMSource(w3cDocument); - } - else { - result = null; - } - return result; + return responseElement != null ? convertResponse(responseElement) : null; } - private Element handleDomSource(DOMSource request) { - Node w3cNode = request.getNode(); - org.w3c.dom.Element w3cElement = null; - if (w3cNode.getNodeType() == Node.ELEMENT_NODE) { - w3cElement = (org.w3c.dom.Element) w3cNode; + private Source convertResponse(Element responseElement) throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Serializer serializer = createSerializer(os); + Document document = responseElement.getDocument(); + if (document == null) { + document = new Document(responseElement); } - else if (w3cNode.getNodeType() == Node.DOCUMENT_NODE) { - org.w3c.dom.Document w3cDocument = (org.w3c.dom.Document) w3cNode; - w3cElement = w3cDocument.getDocumentElement(); - } - return DOMConverter.convert(w3cElement); + serializer.write(document); + byte[] bytes = os.toByteArray(); + return new StreamSource(new ByteArrayInputStream(bytes)); } - private Element handleSaxSource(SAXSource request) throws ParsingException, IOException { - Builder builder = new Builder(request.getXMLReader()); - InputSource inputSource = request.getInputSource(); - Document document; - if (inputSource.getByteStream() != null) { - document = builder.build(inputSource.getByteStream()); - } - else if (inputSource.getCharacterStream() != null) { - document = builder.build(inputSource.getCharacterStream()); - } - else { - throw new IllegalArgumentException( - "InputSource in SAXSource contains neither byte stream nor " + "character stream"); - } - return document.getRootElement(); - } - - private Element handleStreamSource(StreamSource request) throws ParsingException, IOException { - Builder builder = new Builder(); - Document document; - if (request.getInputStream() != null) { - document = builder.build(request.getInputStream()); - } - else if (request.getReader() != null) { - document = builder.build(request.getReader()); - } - else { - throw new IllegalArgumentException("StreamSource contains neither byte stream nor character stream"); - } - return document.getRootElement(); - } - - private void createDocumentBuilderFactory() { - documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setNamespaceAware(true); + /** + * Creates a {@link Serializer} to be used for writing the response to. + *

+ * Default implementation uses the UTF-8 encoding and does not set any options, but this may be changed in + * subclasses. + * + * @param outputStream the output stream to serialize to + * @return the serializer + */ + protected Serializer createSerializer(OutputStream outputStream) { + return new Serializer(outputStream); } /** @@ -143,5 +113,203 @@ public abstract class AbstractXomPayloadEndpoint extends TransformerObjectSuppor */ protected abstract Element invokeInternal(Element requestElement) throws Exception; + private static class XomSourceCallback implements TraxUtils.SourceCallback { + private Element element; + + public void domSource(Node node) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + element = DOMConverter.convert((org.w3c.dom.Element) node); + } + else if (node.getNodeType() == Node.DOCUMENT_NODE) { + Document document = DOMConverter.convert((org.w3c.dom.Document) node); + element = document.getRootElement(); + } + else { + throw new IllegalArgumentException("DOMSource contains neither Document nor Element"); + } + } + + public void saxSource(XMLReader reader, InputSource inputSource) throws IOException, SAXException { + try { + Builder builder = new Builder(reader); + Document document; + if (inputSource.getByteStream() != null) { + document = builder.build(inputSource.getByteStream()); + } + else if (inputSource.getCharacterStream() != null) { + document = builder.build(inputSource.getCharacterStream()); + } + else { + throw new IllegalArgumentException( + "InputSource in SAXSource contains neither byte stream nor character stream"); + } + element = document.getRootElement(); + } + catch (ValidityException ex) { + throw new XomParsingException(ex); + } + catch (ParsingException ex) { + throw new XomParsingException(ex); + } + } + + public void staxSource(XMLEventReader eventReader) throws XMLStreamException { + throw new IllegalArgumentException("XMLEventReader not supported"); + } + + public void staxSource(XMLStreamReader streamReader) throws XMLStreamException { + Document document = StaxStreamConverter.convert(streamReader); + element = document.getRootElement(); + } + + public void streamSource(InputStream inputStream) throws IOException { + try { + Builder builder = new Builder(); + Document document = builder.build(inputStream); + element = document.getRootElement(); + } + catch (ParsingException ex) { + throw new XomParsingException(ex); + } + } + + public void streamSource(Reader reader) throws IOException { + try { + Builder builder = new Builder(); + Document document = builder.build(reader); + element = document.getRootElement(); + } + catch (ParsingException ex) { + throw new XomParsingException(ex); + } + } + } + + private static class XomParsingException extends NestedRuntimeException { + + private XomParsingException(ParsingException ex) { + super(ex.getMessage(), ex); + } + } + + private static class StaxStreamConverter { + + private static Document convert(XMLStreamReader streamReader) throws XMLStreamException { + NodeFactory nodeFactory = new NodeFactory(); + Document document = null; + Element element = null; + ParentNode parent = null; + boolean documentFinished = false; + while (streamReader.hasNext()) { + int event = streamReader.next(); + switch (event) { + case XMLStreamConstants.START_DOCUMENT: + document = nodeFactory.startMakingDocument(); + parent = document; + break; + case XMLStreamConstants.END_DOCUMENT: + nodeFactory.finishMakingDocument(document); + documentFinished = true; + break; + case XMLStreamConstants.START_ELEMENT: + if (document == null) { + document = nodeFactory.startMakingDocument(); + parent = document; + } + String name = QNameUtils.toQualifiedName(streamReader.getName()); + if (element == null) { + element = nodeFactory.makeRootElement(name, streamReader.getNamespaceURI()); + document.setRootElement(element); + } + else { + element = nodeFactory.startMakingElement(name, streamReader.getNamespaceURI()); + parent.appendChild(element); + } + convertNamespaces(streamReader, element); + convertAttributes(streamReader, nodeFactory); + parent = element; + break; + case XMLStreamConstants.END_ELEMENT: + nodeFactory.finishMakingElement(element); + parent = parent.getParent(); + break; + case XMLStreamConstants.ATTRIBUTE: + convertAttributes(streamReader, nodeFactory); + break; + case XMLStreamConstants.CHARACTERS: + nodeFactory.makeText(streamReader.getText()); + break; + case XMLStreamConstants.COMMENT: + nodeFactory.makeComment(streamReader.getText()); + break; + default: + break; + } + } + if (!documentFinished) { + nodeFactory.finishMakingDocument(document); + } + return document; + } + + private static void convertNamespaces(XMLStreamReader streamReader, Element element) { + for (int i = 0; i < streamReader.getNamespaceCount(); i++) { + String uri = streamReader.getNamespaceURI(i); + String prefix = streamReader.getNamespacePrefix(i); + + element.addNamespaceDeclaration(prefix, uri); + } + + } + + private static void convertAttributes(XMLStreamReader streamReader, NodeFactory nodeFactory) { + for (int i = 0; i < streamReader.getAttributeCount(); i++) { + String name = QNameUtils.toQualifiedName(streamReader.getAttributeName(i)); + String uri = streamReader.getAttributeNamespace(i); + String value = streamReader.getAttributeValue(i); + Attribute.Type type = convertAttributeType(streamReader.getAttributeType(i)); + + nodeFactory.makeAttribute(name, uri, value, type); + } + } + + private static Attribute.Type convertAttributeType(String type) { + type = type.toUpperCase(Locale.ENGLISH); + if ("CDATA".equals(type)) { + return Attribute.Type.CDATA; + } + else if ("ENTITIES".equals(type)) { + return Attribute.Type.ENTITIES; + } + else if ("ENTITY".equals(type)) { + return Attribute.Type.ENTITY; + } + else if ("ENUMERATION".equals(type)) { + return Attribute.Type.ENUMERATION; + } + else if ("ID".equals(type)) { + return Attribute.Type.ID; + } + else if ("IDREF".equals(type)) { + return Attribute.Type.IDREF; + } + else if ("IDREFS".equals(type)) { + return Attribute.Type.IDREFS; + } + else if ("NMTOKEN".equals(type)) { + return Attribute.Type.NMTOKEN; + } + else if ("NMTOKENS".equals(type)) { + return Attribute.Type.NMTOKENS; + } + else if ("NOTATION".equals(type)) { + return Attribute.Type.NOTATION; + } + else { + return Attribute.Type.UNDECLARED; + } + } + + } } diff --git a/core/src/test/java/org/springframework/ws/server/endpoint/XomPayloadEndpointTest.java b/core/src/test/java/org/springframework/ws/server/endpoint/XomPayloadEndpointTest.java index e15cc98d..1441489e 100644 --- a/core/src/test/java/org/springframework/ws/server/endpoint/XomPayloadEndpointTest.java +++ b/core/src/test/java/org/springframework/ws/server/endpoint/XomPayloadEndpointTest.java @@ -59,11 +59,4 @@ public class XomPayloadEndpointTest extends AbstractPayloadEndpointTestCase { // overriden, because XOM doesn not support it } - public void testStaxSourceStreamReader() throws Exception { - // overriden, because XOM doesn not support it - } - - public void testStaxSourceStreamReaderJaxp14() throws Exception { - // overriden, because XOM doesn not support it - } }