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;
026
027import javax.xml.parsers.DocumentBuilder;
028import javax.xml.parsers.DocumentBuilderFactory;
029import javax.xml.parsers.ParserConfigurationException;
030import javax.xml.stream.XMLInputFactory;
031import javax.xml.stream.XMLResolver;
032import javax.xml.stream.XMLStreamException;
033import javax.xml.stream.XMLStreamReader;
034import javax.xml.transform.Result;
035import javax.xml.transform.Source;
036import javax.xml.transform.TransformerException;
037import javax.xml.transform.TransformerFactory;
038import javax.xml.transform.dom.DOMSource;
039import javax.xml.transform.sax.SAXSource;
040import javax.xml.transform.stax.StAXSource;
041import javax.xml.transform.stream.StreamResult;
042import javax.xml.transform.stream.StreamSource;
043
044import org.w3c.dom.Document;
045import org.xml.sax.EntityResolver;
046import org.xml.sax.InputSource;
047import org.xml.sax.SAXException;
048import org.xml.sax.XMLReader;
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.HttpMessageNotReadableException;
055import org.springframework.http.converter.HttpMessageNotWritableException;
056import org.springframework.lang.Nullable;
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 * @param <T> the converted object type
067 */
068public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMessageConverter<T> {
069
070        private static final EntityResolver NO_OP_ENTITY_RESOLVER =
071                        (publicId, systemId) -> new InputSource(new StringReader(""));
072
073        private static final XMLResolver NO_OP_XML_RESOLVER =
074                        (publicID, systemID, base, ns) -> StreamUtils.emptyInput();
075
076        private static final Set<Class<?>> SUPPORTED_CLASSES = new HashSet<>(8);
077
078        static {
079                SUPPORTED_CLASSES.add(DOMSource.class);
080                SUPPORTED_CLASSES.add(SAXSource.class);
081                SUPPORTED_CLASSES.add(StAXSource.class);
082                SUPPORTED_CLASSES.add(StreamSource.class);
083                SUPPORTED_CLASSES.add(Source.class);
084        }
085
086
087        private final TransformerFactory transformerFactory = TransformerFactory.newInstance();
088
089        private boolean supportDtd = false;
090
091        private boolean processExternalEntities = false;
092
093
094        /**
095         * Sets the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes}
096         * to {@code text/xml} and {@code application/xml}, and {@code application/*-xml}.
097         */
098        public SourceHttpMessageConverter() {
099                super(MediaType.APPLICATION_XML, MediaType.TEXT_XML, new MediaType("application", "*+xml"));
100        }
101
102
103        /**
104         * Indicate whether DTD parsing should be supported.
105         * <p>Default is {@code false} meaning that DTD is disabled.
106         */
107        public void setSupportDtd(boolean supportDtd) {
108                this.supportDtd = supportDtd;
109        }
110
111        /**
112         * Return whether DTD parsing is supported.
113         */
114        public boolean isSupportDtd() {
115                return this.supportDtd;
116        }
117
118        /**
119         * Indicate whether external XML entities are processed when converting to a Source.
120         * <p>Default is {@code false}, meaning that external entities are not resolved.
121         * <p><strong>Note:</strong> setting this option to {@code true} also
122         * automatically sets {@link #setSupportDtd} to {@code true}.
123         */
124        public void setProcessExternalEntities(boolean processExternalEntities) {
125                this.processExternalEntities = processExternalEntities;
126                if (processExternalEntities) {
127                        this.supportDtd = true;
128                }
129        }
130
131        /**
132         * Return whether XML external entities are allowed.
133         */
134        public boolean isProcessExternalEntities() {
135                return this.processExternalEntities;
136        }
137
138
139        @Override
140        public boolean supports(Class<?> clazz) {
141                return SUPPORTED_CLASSES.contains(clazz);
142        }
143
144        @Override
145        @SuppressWarnings("unchecked")
146        protected T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
147                        throws IOException, HttpMessageNotReadableException {
148
149                InputStream body = inputMessage.getBody();
150                if (DOMSource.class == clazz) {
151                        return (T) readDOMSource(body, inputMessage);
152                }
153                else if (SAXSource.class == clazz) {
154                        return (T) readSAXSource(body, inputMessage);
155                }
156                else if (StAXSource.class == clazz) {
157                        return (T) readStAXSource(body, inputMessage);
158                }
159                else if (StreamSource.class == clazz || Source.class == clazz) {
160                        return (T) readStreamSource(body);
161                }
162                else {
163                        throw new HttpMessageNotReadableException("Could not read class [" + clazz +
164                                        "]. Only DOMSource, SAXSource, StAXSource, and StreamSource are supported.", inputMessage);
165                }
166        }
167
168        private DOMSource readDOMSource(InputStream body, HttpInputMessage inputMessage) throws IOException {
169                try {
170                        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
171                        documentBuilderFactory.setNamespaceAware(true);
172                        documentBuilderFactory.setFeature(
173                                        "http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
174                        documentBuilderFactory.setFeature(
175                                        "http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
176                        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
177                        if (!isProcessExternalEntities()) {
178                                documentBuilder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
179                        }
180                        Document document = documentBuilder.parse(body);
181                        return new DOMSource(document);
182                }
183                catch (NullPointerException ex) {
184                        if (!isSupportDtd()) {
185                                throw new HttpMessageNotReadableException("NPE while unmarshalling: This can happen " +
186                                                "due to the presence of DTD declarations which are disabled.", ex, inputMessage);
187                        }
188                        throw ex;
189                }
190                catch (ParserConfigurationException ex) {
191                        throw new HttpMessageNotReadableException(
192                                        "Could not set feature: " + ex.getMessage(), ex, inputMessage);
193                }
194                catch (SAXException ex) {
195                        throw new HttpMessageNotReadableException(
196                                        "Could not parse document: " + ex.getMessage(), ex, inputMessage);
197                }
198        }
199
200        @SuppressWarnings("deprecation")  // on JDK 9
201        private SAXSource readSAXSource(InputStream body, HttpInputMessage inputMessage) throws IOException {
202                try {
203                        XMLReader xmlReader = org.xml.sax.helpers.XMLReaderFactory.createXMLReader();
204                        xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
205                        xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
206                        if (!isProcessExternalEntities()) {
207                                xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
208                        }
209                        byte[] bytes = StreamUtils.copyToByteArray(body);
210                        return new SAXSource(xmlReader, new InputSource(new ByteArrayInputStream(bytes)));
211                }
212                catch (SAXException ex) {
213                        throw new HttpMessageNotReadableException(
214                                        "Could not parse document: " + ex.getMessage(), ex, inputMessage);
215                }
216        }
217
218        private Source readStAXSource(InputStream body, HttpInputMessage inputMessage) {
219                try {
220                        XMLInputFactory inputFactory = XMLInputFactory.newInstance();
221                        inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, isSupportDtd());
222                        inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, isProcessExternalEntities());
223                        if (!isProcessExternalEntities()) {
224                                inputFactory.setXMLResolver(NO_OP_XML_RESOLVER);
225                        }
226                        XMLStreamReader streamReader = inputFactory.createXMLStreamReader(body);
227                        return new StAXSource(streamReader);
228                }
229                catch (XMLStreamException ex) {
230                        throw new HttpMessageNotReadableException(
231                                        "Could not parse document: " + ex.getMessage(), ex, inputMessage);
232                }
233        }
234
235        private StreamSource readStreamSource(InputStream body) throws IOException {
236                byte[] bytes = StreamUtils.copyToByteArray(body);
237                return new StreamSource(new ByteArrayInputStream(bytes));
238        }
239
240        @Override
241        @Nullable
242        protected Long getContentLength(T t, @Nullable MediaType contentType) {
243                if (t instanceof DOMSource) {
244                        try {
245                                CountingOutputStream os = new CountingOutputStream();
246                                transform(t, new StreamResult(os));
247                                return os.count;
248                        }
249                        catch (TransformerException ex) {
250                                // ignore
251                        }
252                }
253                return null;
254        }
255
256        @Override
257        protected void writeInternal(T t, HttpOutputMessage outputMessage)
258                        throws IOException, HttpMessageNotWritableException {
259                try {
260                        Result result = new StreamResult(outputMessage.getBody());
261                        transform(t, result);
262                }
263                catch (TransformerException ex) {
264                        throw new HttpMessageNotWritableException("Could not transform [" + t + "] to output message", ex);
265                }
266        }
267
268        private void transform(Source source, Result result) throws TransformerException {
269                this.transformerFactory.newTransformer().transform(source, result);
270        }
271
272
273        private static class CountingOutputStream extends OutputStream {
274
275                long count = 0;
276
277                @Override
278                public void write(int b) throws IOException {
279                        this.count++;
280                }
281
282                @Override
283                public void write(byte[] b) throws IOException {
284                        this.count += b.length;
285                }
286
287                @Override
288                public void write(byte[] b, int off, int len) throws IOException {
289                        this.count += len;
290                }
291        }
292
293}