001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.http.converter.xml;
018
019import java.io.ByteArrayInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.OutputStream;
023import java.io.StringReader;
024import java.util.HashSet;
025import java.util.Set;
026import javax.xml.parsers.DocumentBuilder;
027import javax.xml.parsers.DocumentBuilderFactory;
028import javax.xml.parsers.ParserConfigurationException;
029import javax.xml.stream.XMLInputFactory;
030import javax.xml.stream.XMLResolver;
031import javax.xml.stream.XMLStreamException;
032import javax.xml.stream.XMLStreamReader;
033import javax.xml.transform.Result;
034import javax.xml.transform.Source;
035import javax.xml.transform.TransformerException;
036import javax.xml.transform.TransformerFactory;
037import javax.xml.transform.dom.DOMSource;
038import javax.xml.transform.sax.SAXSource;
039import javax.xml.transform.stax.StAXSource;
040import javax.xml.transform.stream.StreamResult;
041import javax.xml.transform.stream.StreamSource;
042
043import org.w3c.dom.Document;
044import org.xml.sax.EntityResolver;
045import org.xml.sax.InputSource;
046import org.xml.sax.SAXException;
047import org.xml.sax.XMLReader;
048import org.xml.sax.helpers.XMLReaderFactory;
049
050import org.springframework.http.HttpInputMessage;
051import org.springframework.http.HttpOutputMessage;
052import org.springframework.http.MediaType;
053import org.springframework.http.converter.AbstractHttpMessageConverter;
054import org.springframework.http.converter.HttpMessageConversionException;
055import org.springframework.http.converter.HttpMessageNotReadableException;
056import org.springframework.http.converter.HttpMessageNotWritableException;
057import org.springframework.util.StreamUtils;
058
059/**
060 * Implementation of {@link org.springframework.http.converter.HttpMessageConverter}
061 * that can read and write {@link Source} objects.
062 *
063 * @author Arjen Poutsma
064 * @author Rossen Stoyanchev
065 * @since 3.0
066 */
067public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMessageConverter<T> {
068
069        private static final Set<Class<?>> SUPPORTED_CLASSES = new HashSet<Class<?>>(5);
070
071        static {
072                SUPPORTED_CLASSES.add(DOMSource.class);
073                SUPPORTED_CLASSES.add(SAXSource.class);
074                SUPPORTED_CLASSES.add(StAXSource.class);
075                SUPPORTED_CLASSES.add(StreamSource.class);
076                SUPPORTED_CLASSES.add(Source.class);
077        }
078
079
080        private final TransformerFactory transformerFactory = TransformerFactory.newInstance();
081
082        private boolean supportDtd = false;
083
084        private boolean processExternalEntities = false;
085
086
087        /**
088         * Sets the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes}
089         * to {@code text/xml} and {@code application/xml}, and {@code application/*-xml}.
090         */
091        public SourceHttpMessageConverter() {
092                super(MediaType.APPLICATION_XML, MediaType.TEXT_XML, new MediaType("application", "*+xml"));
093        }
094
095
096        /**
097         * Indicate whether DTD parsing should be supported.
098         * <p>Default is {@code false} meaning that DTD is disabled.
099         */
100        public void setSupportDtd(boolean supportDtd) {
101                this.supportDtd = supportDtd;
102        }
103
104        /**
105         * Return whether DTD parsing is supported.
106         */
107        public boolean isSupportDtd() {
108                return this.supportDtd;
109        }
110
111        /**
112         * Indicate whether external XML entities are processed when converting to a Source.
113         * <p>Default is {@code false}, meaning that external entities are not resolved.
114         * <p><strong>Note:</strong> setting this option to {@code true} also
115         * automatically sets {@link #setSupportDtd} to {@code true}.
116         */
117        public void setProcessExternalEntities(boolean processExternalEntities) {
118                this.processExternalEntities = processExternalEntities;
119                if (processExternalEntities) {
120                        setSupportDtd(true);
121                }
122        }
123
124        /**
125         * Return whether XML external entities are allowed.
126         */
127        public boolean isProcessExternalEntities() {
128                return this.processExternalEntities;
129        }
130
131
132        @Override
133        public boolean supports(Class<?> clazz) {
134                return SUPPORTED_CLASSES.contains(clazz);
135        }
136
137        @Override
138        @SuppressWarnings("unchecked")
139        protected T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
140                        throws IOException, HttpMessageNotReadableException {
141
142                InputStream body = inputMessage.getBody();
143                if (DOMSource.class == clazz) {
144                        return (T) readDOMSource(body);
145                }
146                else if (SAXSource.class == clazz) {
147                        return (T) readSAXSource(body);
148                }
149                else if (StAXSource.class == clazz) {
150                        return (T) readStAXSource(body);
151                }
152                else if (StreamSource.class == clazz || Source.class == clazz) {
153                        return (T) readStreamSource(body);
154                }
155                else {
156                        throw new HttpMessageConversionException("Could not read class [" + clazz +
157                                        "]. Only DOMSource, SAXSource, StAXSource, and StreamSource are supported.");
158                }
159        }
160
161        private DOMSource readDOMSource(InputStream body) throws IOException {
162                try {
163                        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
164                        documentBuilderFactory.setNamespaceAware(true);
165                        documentBuilderFactory.setFeature(
166                                        "http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
167                        documentBuilderFactory.setFeature(
168                                        "http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
169                        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
170                        if (!isProcessExternalEntities()) {
171                                documentBuilder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
172                        }
173                        Document document = documentBuilder.parse(body);
174                        return new DOMSource(document);
175                }
176                catch (NullPointerException ex) {
177                        if (!isSupportDtd()) {
178                                throw new HttpMessageNotReadableException("NPE while unmarshalling: " +
179                                                "This can happen due to the presence of DTD declarations which are disabled.", ex);
180                        }
181                        throw ex;
182                }
183                catch (ParserConfigurationException ex) {
184                        throw new HttpMessageNotReadableException("Could not set feature: " + ex.getMessage(), ex);
185                }
186                catch (SAXException ex) {
187                        throw new HttpMessageNotReadableException("Could not parse document: " + ex.getMessage(), ex);
188                }
189        }
190
191        private SAXSource readSAXSource(InputStream body) throws IOException {
192                try {
193                        XMLReader xmlReader = XMLReaderFactory.createXMLReader();
194                        xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
195                        xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
196                        if (!isProcessExternalEntities()) {
197                                xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
198                        }
199                        byte[] bytes = StreamUtils.copyToByteArray(body);
200                        return new SAXSource(xmlReader, new InputSource(new ByteArrayInputStream(bytes)));
201                }
202                catch (SAXException ex) {
203                        throw new HttpMessageNotReadableException("Could not parse document: " + ex.getMessage(), ex);
204                }
205        }
206
207        private Source readStAXSource(InputStream body) {
208                try {
209                        XMLInputFactory inputFactory = XMLInputFactory.newInstance();
210                        inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, isSupportDtd());
211                        inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, isProcessExternalEntities());
212                        if (!isProcessExternalEntities()) {
213                                inputFactory.setXMLResolver(NO_OP_XML_RESOLVER);
214                        }
215                        XMLStreamReader streamReader = inputFactory.createXMLStreamReader(body);
216                        return new StAXSource(streamReader);
217                }
218                catch (XMLStreamException ex) {
219                        throw new HttpMessageNotReadableException("Could not parse document: " + ex.getMessage(), ex);
220                }
221        }
222
223        private StreamSource readStreamSource(InputStream body) throws IOException {
224                byte[] bytes = StreamUtils.copyToByteArray(body);
225                return new StreamSource(new ByteArrayInputStream(bytes));
226        }
227
228        @Override
229        protected Long getContentLength(T t, MediaType contentType) {
230                if (t instanceof DOMSource) {
231                        try {
232                                CountingOutputStream os = new CountingOutputStream();
233                                transform(t, new StreamResult(os));
234                                return os.count;
235                        }
236                        catch (TransformerException ex) {
237                                // ignore
238                        }
239                }
240                return null;
241        }
242
243        @Override
244        protected void writeInternal(T t, HttpOutputMessage outputMessage)
245                        throws IOException, HttpMessageNotWritableException {
246                try {
247                        Result result = new StreamResult(outputMessage.getBody());
248                        transform(t, result);
249                }
250                catch (TransformerException ex) {
251                        throw new HttpMessageNotWritableException("Could not transform [" + t + "] to output message", ex);
252                }
253        }
254
255        private void transform(Source source, Result result) throws TransformerException {
256                this.transformerFactory.newTransformer().transform(source, result);
257        }
258
259
260        private static class CountingOutputStream extends OutputStream {
261
262                long count = 0;
263
264                @Override
265                public void write(int b) throws IOException {
266                        this.count++;
267                }
268
269                @Override
270                public void write(byte[] b) throws IOException {
271                        this.count += b.length;
272                }
273
274                @Override
275                public void write(byte[] b, int off, int len) throws IOException {
276                        this.count += len;
277                }
278        }
279
280
281        private static final EntityResolver NO_OP_ENTITY_RESOLVER = new EntityResolver() {
282                @Override
283                public InputSource resolveEntity(String publicId, String systemId) {
284                        return new InputSource(new StringReader(""));
285                }
286        };
287
288        private static final XMLResolver NO_OP_XML_RESOLVER = new XMLResolver() {
289                @Override
290                public Object resolveEntity(String publicID, String systemID, String base, String ns) {
291                        return StreamUtils.emptyInput();
292                }
293        };
294
295}