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