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}