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}