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}