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.IOException;
020import java.io.StringReader;
021import javax.xml.bind.JAXBElement;
022import javax.xml.bind.JAXBException;
023import javax.xml.bind.MarshalException;
024import javax.xml.bind.Marshaller;
025import javax.xml.bind.PropertyException;
026import javax.xml.bind.UnmarshalException;
027import javax.xml.bind.Unmarshaller;
028import javax.xml.bind.annotation.XmlRootElement;
029import javax.xml.bind.annotation.XmlType;
030import javax.xml.transform.Result;
031import javax.xml.transform.Source;
032import javax.xml.transform.sax.SAXSource;
033import javax.xml.transform.stream.StreamSource;
034
035import org.xml.sax.EntityResolver;
036import org.xml.sax.InputSource;
037import org.xml.sax.SAXException;
038import org.xml.sax.XMLReader;
039import org.xml.sax.helpers.XMLReaderFactory;
040
041import org.springframework.core.annotation.AnnotationUtils;
042import org.springframework.http.HttpHeaders;
043import org.springframework.http.MediaType;
044import org.springframework.http.converter.HttpMessageConversionException;
045import org.springframework.http.converter.HttpMessageNotReadableException;
046import org.springframework.http.converter.HttpMessageNotWritableException;
047import org.springframework.util.ClassUtils;
048
049/**
050 * Implementation of {@link org.springframework.http.converter.HttpMessageConverter
051 * HttpMessageConverter} that can read and write XML using JAXB2.
052 *
053 * <p>This converter can read classes annotated with {@link XmlRootElement} and
054 * {@link XmlType}, and write classes annotated with {@link XmlRootElement},
055 * or subclasses thereof.
056 *
057 * <p>Note: When using Spring's Marshaller/Unmarshaller abstractions from {@code spring-oxm},
058 * you should use the {@link MarshallingHttpMessageConverter} instead.
059 *
060 * @author Arjen Poutsma
061 * @author Sebastien Deleuze
062 * @author Rossen Stoyanchev
063 * @since 3.0
064 * @see MarshallingHttpMessageConverter
065 */
066public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessageConverter<Object> {
067
068        private boolean supportDtd = false;
069
070        private boolean processExternalEntities = false;
071
072
073        /**
074         * Indicate whether DTD parsing should be supported.
075         * <p>Default is {@code false} meaning that DTD is disabled.
076         */
077        public void setSupportDtd(boolean supportDtd) {
078                this.supportDtd = supportDtd;
079        }
080
081        /**
082         * Return whether DTD parsing is supported.
083         */
084        public boolean isSupportDtd() {
085                return this.supportDtd;
086        }
087
088        /**
089         * Indicate whether external XML entities are processed when converting to a Source.
090         * <p>Default is {@code false}, meaning that external entities are not resolved.
091         * <p><strong>Note:</strong> setting this option to {@code true} also
092         * automatically sets {@link #setSupportDtd} to {@code true}.
093         */
094        public void setProcessExternalEntities(boolean processExternalEntities) {
095                this.processExternalEntities = processExternalEntities;
096                if (processExternalEntities) {
097                        setSupportDtd(true);
098                }
099        }
100
101        /**
102         * Return whether XML external entities are allowed.
103         */
104        public boolean isProcessExternalEntities() {
105                return this.processExternalEntities;
106        }
107
108
109        @Override
110        public boolean canRead(Class<?> clazz, MediaType mediaType) {
111                return (clazz.isAnnotationPresent(XmlRootElement.class) || clazz.isAnnotationPresent(XmlType.class)) &&
112                                canRead(mediaType);
113        }
114
115        @Override
116        public boolean canWrite(Class<?> clazz, MediaType mediaType) {
117                return (AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null && canWrite(mediaType));
118        }
119
120        @Override
121        protected boolean supports(Class<?> clazz) {
122                // should not be called, since we override canRead/Write
123                throw new UnsupportedOperationException();
124        }
125
126        @Override
127        protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source source) throws IOException {
128                try {
129                        source = processSource(source);
130                        Unmarshaller unmarshaller = createUnmarshaller(clazz);
131                        if (clazz.isAnnotationPresent(XmlRootElement.class)) {
132                                return unmarshaller.unmarshal(source);
133                        }
134                        else {
135                                JAXBElement<?> jaxbElement = unmarshaller.unmarshal(source, clazz);
136                                return jaxbElement.getValue();
137                        }
138                }
139                catch (NullPointerException ex) {
140                        if (!isSupportDtd()) {
141                                throw new HttpMessageNotReadableException("NPE while unmarshalling. " +
142                                                "This can happen due to the presence of DTD declarations which are disabled.", ex);
143                        }
144                        throw ex;
145                }
146                catch (UnmarshalException ex) {
147                        throw new HttpMessageNotReadableException("Could not unmarshal to [" + clazz + "]: " + ex.getMessage(), ex);
148                }
149                catch (JAXBException ex) {
150                        throw new HttpMessageConversionException("Invalid JAXB setup: " + ex.getMessage(), ex);
151                }
152        }
153
154        protected Source processSource(Source source) {
155                if (source instanceof StreamSource) {
156                        StreamSource streamSource = (StreamSource) source;
157                        InputSource inputSource = new InputSource(streamSource.getInputStream());
158                        try {
159                                XMLReader xmlReader = XMLReaderFactory.createXMLReader();
160                                xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
161                                String featureName = "http://xml.org/sax/features/external-general-entities";
162                                xmlReader.setFeature(featureName, isProcessExternalEntities());
163                                if (!isProcessExternalEntities()) {
164                                        xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
165                                }
166                                return new SAXSource(xmlReader, inputSource);
167                        }
168                        catch (SAXException ex) {
169                                logger.warn("Processing of external entities could not be disabled", ex);
170                                return source;
171                        }
172                }
173                else {
174                        return source;
175                }
176        }
177
178        @Override
179        protected void writeToResult(Object o, HttpHeaders headers, Result result) throws IOException {
180                try {
181                        Class<?> clazz = ClassUtils.getUserClass(o);
182                        Marshaller marshaller = createMarshaller(clazz);
183                        setCharset(headers.getContentType(), marshaller);
184                        marshaller.marshal(o, result);
185                }
186                catch (MarshalException ex) {
187                        throw new HttpMessageNotWritableException("Could not marshal [" + o + "]: " + ex.getMessage(), ex);
188                }
189                catch (JAXBException ex) {
190                        throw new HttpMessageConversionException("Invalid JAXB setup: " + ex.getMessage(), ex);
191                }
192        }
193
194        private void setCharset(MediaType contentType, Marshaller marshaller) throws PropertyException {
195                if (contentType != null && contentType.getCharset() != null) {
196                        marshaller.setProperty(Marshaller.JAXB_ENCODING, contentType.getCharset().name());
197                }
198        }
199
200
201        private static final EntityResolver NO_OP_ENTITY_RESOLVER = new EntityResolver() {
202                @Override
203                public InputSource resolveEntity(String publicId, String systemId) {
204                        return new InputSource(new StringReader(""));
205                }
206        };
207
208}