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