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