001/* 002 * Copyright 2002-2018 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.lang.reflect.ParameterizedType; 021import java.lang.reflect.Type; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.LinkedHashSet; 025import java.util.List; 026import java.util.SortedSet; 027import java.util.TreeSet; 028import javax.xml.bind.JAXBException; 029import javax.xml.bind.UnmarshalException; 030import javax.xml.bind.Unmarshaller; 031import javax.xml.bind.annotation.XmlRootElement; 032import javax.xml.bind.annotation.XmlType; 033import javax.xml.stream.XMLInputFactory; 034import javax.xml.stream.XMLResolver; 035import javax.xml.stream.XMLStreamException; 036import javax.xml.stream.XMLStreamReader; 037import javax.xml.transform.Result; 038import javax.xml.transform.Source; 039 040import org.springframework.http.HttpHeaders; 041import org.springframework.http.HttpInputMessage; 042import org.springframework.http.HttpOutputMessage; 043import org.springframework.http.MediaType; 044import org.springframework.http.converter.GenericHttpMessageConverter; 045import org.springframework.http.converter.HttpMessageConversionException; 046import org.springframework.http.converter.HttpMessageNotReadableException; 047import org.springframework.http.converter.HttpMessageNotWritableException; 048import org.springframework.util.StreamUtils; 049 050/** 051 * An {@code HttpMessageConverter} that can read XML collections using JAXB2. 052 * 053 * <p>This converter can read {@linkplain Collection collections} that contain classes 054 * annotated with {@link XmlRootElement} and {@link XmlType}. Note that this converter 055 * does not support writing. 056 * 057 * @author Arjen Poutsma 058 * @author Rossen Stoyanchev 059 * @since 3.2 060 */ 061@SuppressWarnings("rawtypes") 062public class Jaxb2CollectionHttpMessageConverter<T extends Collection> 063 extends AbstractJaxb2HttpMessageConverter<T> implements GenericHttpMessageConverter<T> { 064 065 private final XMLInputFactory inputFactory = createXmlInputFactory(); 066 067 068 /** 069 * Always returns {@code false} since Jaxb2CollectionHttpMessageConverter 070 * required generic type information in order to read a Collection. 071 */ 072 @Override 073 public boolean canRead(Class<?> clazz, MediaType mediaType) { 074 return false; 075 } 076 077 /** 078 * {@inheritDoc} 079 * <p>Jaxb2CollectionHttpMessageConverter can read a generic 080 * {@link Collection} where the generic type is a JAXB type annotated with 081 * {@link XmlRootElement} or {@link XmlType}. 082 */ 083 @Override 084 public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) { 085 if (!(type instanceof ParameterizedType)) { 086 return false; 087 } 088 ParameterizedType parameterizedType = (ParameterizedType) type; 089 if (!(parameterizedType.getRawType() instanceof Class)) { 090 return false; 091 } 092 Class<?> rawType = (Class<?>) parameterizedType.getRawType(); 093 if (!(Collection.class.isAssignableFrom(rawType))) { 094 return false; 095 } 096 if (parameterizedType.getActualTypeArguments().length != 1) { 097 return false; 098 } 099 Type typeArgument = parameterizedType.getActualTypeArguments()[0]; 100 if (!(typeArgument instanceof Class)) { 101 return false; 102 } 103 Class<?> typeArgumentClass = (Class<?>) typeArgument; 104 return (typeArgumentClass.isAnnotationPresent(XmlRootElement.class) || 105 typeArgumentClass.isAnnotationPresent(XmlType.class)) && canRead(mediaType); 106 } 107 108 /** 109 * Always returns {@code false} since Jaxb2CollectionHttpMessageConverter 110 * does not convert collections to XML. 111 */ 112 @Override 113 public boolean canWrite(Class<?> clazz, MediaType mediaType) { 114 return false; 115 } 116 117 /** 118 * Always returns {@code false} since Jaxb2CollectionHttpMessageConverter 119 * does not convert collections to XML. 120 */ 121 @Override 122 public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) { 123 return false; 124 } 125 126 @Override 127 protected boolean supports(Class<?> clazz) { 128 // should not be called, since we override canRead/Write 129 throw new UnsupportedOperationException(); 130 } 131 132 @Override 133 protected T readFromSource(Class<? extends T> clazz, HttpHeaders headers, Source source) throws IOException { 134 // should not be called, since we return false for canRead(Class) 135 throw new UnsupportedOperationException(); 136 } 137 138 @Override 139 @SuppressWarnings("unchecked") 140 public T read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) 141 throws IOException, HttpMessageNotReadableException { 142 143 ParameterizedType parameterizedType = (ParameterizedType) type; 144 T result = createCollection((Class<?>) parameterizedType.getRawType()); 145 Class<?> elementClass = (Class<?>) parameterizedType.getActualTypeArguments()[0]; 146 147 try { 148 Unmarshaller unmarshaller = createUnmarshaller(elementClass); 149 XMLStreamReader streamReader = this.inputFactory.createXMLStreamReader(inputMessage.getBody()); 150 int event = moveToFirstChildOfRootElement(streamReader); 151 152 while (event != XMLStreamReader.END_DOCUMENT) { 153 if (elementClass.isAnnotationPresent(XmlRootElement.class)) { 154 result.add(unmarshaller.unmarshal(streamReader)); 155 } 156 else if (elementClass.isAnnotationPresent(XmlType.class)) { 157 result.add(unmarshaller.unmarshal(streamReader, elementClass).getValue()); 158 } 159 else { 160 // should not happen, since we check in canRead(Type) 161 throw new HttpMessageConversionException("Could not unmarshal to [" + elementClass + "]"); 162 } 163 event = moveToNextElement(streamReader); 164 } 165 return result; 166 } 167 catch (UnmarshalException ex) { 168 throw new HttpMessageNotReadableException( 169 "Could not unmarshal to [" + elementClass + "]: " + ex.getMessage(), ex); 170 } 171 catch (JAXBException ex) { 172 throw new HttpMessageConversionException("Invalid JAXB setup: " + ex.getMessage(), ex); 173 } 174 catch (XMLStreamException ex) { 175 throw new HttpMessageConversionException(ex.getMessage(), ex); 176 } 177 } 178 179 /** 180 * Create a Collection of the given type, with the given initial capacity 181 * (if supported by the Collection type). 182 * @param collectionClass the type of Collection to instantiate 183 * @return the created Collection instance 184 */ 185 @SuppressWarnings("unchecked") 186 protected T createCollection(Class<?> collectionClass) { 187 if (!collectionClass.isInterface()) { 188 try { 189 return (T) collectionClass.newInstance(); 190 } 191 catch (Throwable ex) { 192 throw new IllegalArgumentException( 193 "Could not instantiate collection class: " + collectionClass.getName(), ex); 194 } 195 } 196 else if (List.class == collectionClass) { 197 return (T) new ArrayList(); 198 } 199 else if (SortedSet.class == collectionClass) { 200 return (T) new TreeSet(); 201 } 202 else { 203 return (T) new LinkedHashSet(); 204 } 205 } 206 207 private int moveToFirstChildOfRootElement(XMLStreamReader streamReader) throws XMLStreamException { 208 // root 209 int event = streamReader.next(); 210 while (event != XMLStreamReader.START_ELEMENT) { 211 event = streamReader.next(); 212 } 213 214 // first child 215 event = streamReader.next(); 216 while ((event != XMLStreamReader.START_ELEMENT) && (event != XMLStreamReader.END_DOCUMENT)) { 217 event = streamReader.next(); 218 } 219 return event; 220 } 221 222 private int moveToNextElement(XMLStreamReader streamReader) throws XMLStreamException { 223 int event = streamReader.getEventType(); 224 while (event != XMLStreamReader.START_ELEMENT && event != XMLStreamReader.END_DOCUMENT) { 225 event = streamReader.next(); 226 } 227 return event; 228 } 229 230 @Override 231 public void write(T t, Type type, MediaType contentType, HttpOutputMessage outputMessage) 232 throws IOException, HttpMessageNotWritableException { 233 234 throw new UnsupportedOperationException(); 235 } 236 237 @Override 238 protected void writeToResult(T t, HttpHeaders headers, Result result) throws IOException { 239 throw new UnsupportedOperationException(); 240 } 241 242 /** 243 * Create a {@code XMLInputFactory} that this converter will use to create {@link 244 * javax.xml.stream.XMLStreamReader} and {@link javax.xml.stream.XMLEventReader} objects. 245 * <p>Can be overridden in subclasses, adding further initialization of the factory. 246 * The resulting factory is cached, so this method will only be called once. 247 */ 248 protected XMLInputFactory createXmlInputFactory() { 249 XMLInputFactory inputFactory = XMLInputFactory.newInstance(); 250 inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); 251 inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); 252 inputFactory.setXMLResolver(NO_OP_XML_RESOLVER); 253 return inputFactory; 254 } 255 256 257 private static final XMLResolver NO_OP_XML_RESOLVER = new XMLResolver() { 258 @Override 259 public Object resolveEntity(String publicID, String systemID, String base, String ns) { 260 return StreamUtils.emptyInput(); 261 } 262 }; 263 264}