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}