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.codec.xml;
018
019import java.io.InputStream;
020import java.util.ArrayList;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024import java.util.function.Function;
025
026import javax.xml.stream.XMLInputFactory;
027import javax.xml.stream.XMLStreamException;
028import javax.xml.stream.events.XMLEvent;
029import javax.xml.stream.util.XMLEventAllocator;
030
031import com.fasterxml.aalto.AsyncByteBufferFeeder;
032import com.fasterxml.aalto.AsyncXMLInputFactory;
033import com.fasterxml.aalto.AsyncXMLStreamReader;
034import com.fasterxml.aalto.evt.EventAllocatorImpl;
035import com.fasterxml.aalto.stax.InputFactoryImpl;
036import org.reactivestreams.Publisher;
037import reactor.core.Exceptions;
038import reactor.core.publisher.Flux;
039
040import org.springframework.core.ResolvableType;
041import org.springframework.core.codec.AbstractDecoder;
042import org.springframework.core.io.buffer.DataBuffer;
043import org.springframework.core.io.buffer.DataBufferLimitException;
044import org.springframework.core.io.buffer.DataBufferUtils;
045import org.springframework.http.MediaType;
046import org.springframework.lang.Nullable;
047import org.springframework.util.ClassUtils;
048import org.springframework.util.MimeType;
049import org.springframework.util.MimeTypeUtils;
050import org.springframework.util.xml.StaxUtils;
051
052/**
053 * Decodes a {@link DataBuffer} stream into a stream of {@link XMLEvent XMLEvents}.
054 *
055 * <p>Given the following XML:
056 *
057 * <pre class="code">
058 * &lt;root>
059 *     &lt;child&gt;foo&lt;/child&gt;
060 *     &lt;child&gt;bar&lt;/child&gt;
061 * &lt;/root&gt;
062 * </pre>
063 *
064 * this decoder will produce a {@link Flux} with the following events:
065 *
066 * <ol>
067 * <li>{@link javax.xml.stream.events.StartDocument}</li>
068 * <li>{@link javax.xml.stream.events.StartElement} {@code root}</li>
069 * <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
070 * <li>{@link javax.xml.stream.events.Characters} {@code foo}</li>
071 * <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
072 * <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
073 * <li>{@link javax.xml.stream.events.Characters} {@code bar}</li>
074 * <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
075 * <li>{@link javax.xml.stream.events.EndElement} {@code root}</li>
076 * </ol>
077 *
078 * <p>Note that this decoder is not registered by default but is used internally
079 * by other decoders which are registered by default.
080 *
081 * @author Arjen Poutsma
082 * @author Sam Brannen
083 * @since 5.0
084 */
085public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
086
087        private static final XMLInputFactory inputFactory = StaxUtils.createDefensiveInputFactory();
088
089        private static final boolean aaltoPresent = ClassUtils.isPresent(
090                        "com.fasterxml.aalto.AsyncXMLStreamReader", XmlEventDecoder.class.getClassLoader());
091
092        boolean useAalto = aaltoPresent;
093
094        private int maxInMemorySize = 256 * 1024;
095
096
097        public XmlEventDecoder() {
098                super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML, new MediaType("application", "*+xml"));
099        }
100
101
102        /**
103         * Set the max number of bytes that can be buffered by this decoder. This
104         * is either the size the entire input when decoding as a whole, or when
105         * using async parsing via Aalto XML, it is size one top-level XML tree.
106         * When the limit is exceeded, {@link DataBufferLimitException} is raised.
107         * <p>By default this is set to 256K.
108         * @param byteCount the max number of bytes to buffer, or -1 for unlimited
109         * @since 5.1.11
110         */
111        public void setMaxInMemorySize(int byteCount) {
112                this.maxInMemorySize = byteCount;
113        }
114
115        /**
116         * Return the {@link #setMaxInMemorySize configured} byte count limit.
117         * @since 5.1.11
118         */
119        public int getMaxInMemorySize() {
120                return this.maxInMemorySize;
121        }
122
123
124        @Override
125        @SuppressWarnings({"rawtypes", "unchecked", "cast"})  // XMLEventReader is Iterator<Object> on JDK 9
126        public Flux<XMLEvent> decode(Publisher<DataBuffer> input, ResolvableType elementType,
127                        @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
128
129                if (this.useAalto) {
130                        AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent(this.maxInMemorySize);
131                        return Flux.from(input)
132                                        .flatMapIterable(mapper)
133                                        .doFinally(signalType -> mapper.endOfInput());
134                }
135                else {
136                        return DataBufferUtils.join(input, this.maxInMemorySize)
137                                        .flatMapIterable(buffer -> {
138                                                try {
139                                                        InputStream is = buffer.asInputStream();
140                                                        Iterator eventReader = inputFactory.createXMLEventReader(is);
141                                                        List<XMLEvent> result = new ArrayList<>();
142                                                        eventReader.forEachRemaining(event -> result.add((XMLEvent) event));
143                                                        return result;
144                                                }
145                                                catch (XMLStreamException ex) {
146                                                        throw Exceptions.propagate(ex);
147                                                }
148                                                finally {
149                                                        DataBufferUtils.release(buffer);
150                                                }
151                                        });
152                }
153        }
154
155
156        /*
157         * Separate static class to isolate Aalto dependency.
158         */
159        private static class AaltoDataBufferToXmlEvent implements Function<DataBuffer, List<? extends XMLEvent>> {
160
161                private static final AsyncXMLInputFactory inputFactory =
162                                StaxUtils.createDefensiveInputFactory(InputFactoryImpl::new);
163
164                private final AsyncXMLStreamReader<AsyncByteBufferFeeder> streamReader =
165                                inputFactory.createAsyncForByteBuffer();
166
167                private final XMLEventAllocator eventAllocator = EventAllocatorImpl.getDefaultInstance();
168
169                private final int maxInMemorySize;
170
171                private int byteCount;
172
173                private int elementDepth;
174
175
176                public AaltoDataBufferToXmlEvent(int maxInMemorySize) {
177                        this.maxInMemorySize = maxInMemorySize;
178                }
179
180
181                @Override
182                public List<? extends XMLEvent> apply(DataBuffer dataBuffer) {
183                        try {
184                                increaseByteCount(dataBuffer);
185                                this.streamReader.getInputFeeder().feedInput(dataBuffer.asByteBuffer());
186                                List<XMLEvent> events = new ArrayList<>();
187                                while (true) {
188                                        if (this.streamReader.next() == AsyncXMLStreamReader.EVENT_INCOMPLETE) {
189                                                // no more events with what currently has been fed to the reader
190                                                break;
191                                        }
192                                        else {
193                                                XMLEvent event = this.eventAllocator.allocate(this.streamReader);
194                                                events.add(event);
195                                                if (event.isEndDocument()) {
196                                                        break;
197                                                }
198                                                checkDepthAndResetByteCount(event);
199                                        }
200                                }
201                                if (this.maxInMemorySize > 0 && this.byteCount > this.maxInMemorySize) {
202                                        raiseLimitException();
203                                }
204                                return events;
205                        }
206                        catch (XMLStreamException ex) {
207                                throw Exceptions.propagate(ex);
208                        }
209                        finally {
210                                DataBufferUtils.release(dataBuffer);
211                        }
212                }
213
214                private void increaseByteCount(DataBuffer dataBuffer) {
215                        if (this.maxInMemorySize > 0) {
216                                if (dataBuffer.readableByteCount() > Integer.MAX_VALUE - this.byteCount) {
217                                        raiseLimitException();
218                                }
219                                else {
220                                        this.byteCount += dataBuffer.readableByteCount();
221                                }
222                        }
223                }
224
225                private void checkDepthAndResetByteCount(XMLEvent event) {
226                        if (this.maxInMemorySize > 0) {
227                                if (event.isStartElement()) {
228                                        this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount;
229                                        this.elementDepth++;
230                                }
231                                else if (event.isEndElement()) {
232                                        this.elementDepth--;
233                                        this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount;
234                                }
235                        }
236                }
237
238                private void raiseLimitException() {
239                        throw new DataBufferLimitException(
240                                        "Exceeded limit on max bytes per XML top-level node: " + this.maxInMemorySize);
241                }
242
243                public void endOfInput() {
244                        this.streamReader.getInputFeeder().endOfInput();
245                }
246        }
247
248
249
250}