org.springframework.http.codec.json.JsonObjectDecoder.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.http.codec.json.JsonObjectDecoder.java

Source

/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.http.codec.json;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;

import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractDecoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.util.MimeType;

/**
 * Decode an arbitrary split byte stream representing JSON objects to a byte
 * stream where each chunk is a well-formed JSON object.
 *
 * <p>This class does not do any real parsing or validation. A sequence of bytes
 * is considered a JSON object/array if it contains a matching number of opening
 * and closing braces/brackets.
 *
 * <p>Based on <a href="https://github.com/netty/netty/blob/master/codec/src/main/java/io/netty/handler/codec/json/JsonObjectDecoder.java">Netty JsonObjectDecoder</a>
 *
 * @author Sebastien Deleuze
 * @since 5.0
 */
class JsonObjectDecoder extends AbstractDecoder<DataBuffer> {

    private static final int ST_CORRUPTED = -1;

    private static final int ST_INIT = 0;

    private static final int ST_DECODING_NORMAL = 1;

    private static final int ST_DECODING_ARRAY_STREAM = 2;

    private final int maxObjectLength;

    private final boolean streamArrayElements;

    public JsonObjectDecoder() {
        // 1 MB
        this(1024 * 1024);
    }

    public JsonObjectDecoder(int maxObjectLength) {
        this(maxObjectLength, true);
    }

    public JsonObjectDecoder(boolean streamArrayElements) {
        this(1024 * 1024, streamArrayElements);
    }

    /**
     * @param maxObjectLength maximum number of bytes a JSON object/array may
     * use (including braces and all). Objects exceeding this length are dropped
     * and an {@link IllegalStateException} is thrown.
     * @param streamArrayElements if set to true and the "top level" JSON object
     * is an array, each of its entries is passed through the pipeline individually
     * and immediately after it was fully received, allowing for arrays with
     */
    public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) {
        super(new MimeType("application", "json", StandardCharsets.UTF_8),
                new MimeType("application", "*+json", StandardCharsets.UTF_8));
        if (maxObjectLength < 1) {
            throw new IllegalArgumentException("maxObjectLength must be a positive int");
        }
        this.maxObjectLength = maxObjectLength;
        this.streamArrayElements = streamArrayElements;
    }

    @Override
    public Flux<DataBuffer> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType, MimeType mimeType,
            Map<String, Object> hints) {

        return Flux.from(inputStream).flatMap(new Function<DataBuffer, Publisher<? extends DataBuffer>>() {

            int openBraces;
            int index;
            int state;
            boolean insideString;
            ByteBuf input;
            Integer writerIndex;

            @Override
            public Publisher<? extends DataBuffer> apply(DataBuffer buffer) {
                List<DataBuffer> chunks = new ArrayList<>();
                if (this.input == null) {
                    this.input = Unpooled.copiedBuffer(buffer.asByteBuffer());
                    DataBufferUtils.release(buffer);
                    this.writerIndex = this.input.writerIndex();
                } else {
                    this.index = this.index - this.input.readerIndex();
                    this.input = Unpooled.copiedBuffer(this.input, Unpooled.copiedBuffer(buffer.asByteBuffer()));
                    DataBufferUtils.release(buffer);
                    this.writerIndex = this.input.writerIndex();
                }
                if (this.state == ST_CORRUPTED) {
                    this.input.skipBytes(this.input.readableBytes());
                    return Flux.error(new IllegalStateException("Corrupted stream"));
                }
                if (this.writerIndex > maxObjectLength) {
                    // buffer size exceeded maxObjectLength; discarding the complete buffer.
                    this.input.skipBytes(this.input.readableBytes());
                    reset();
                    return Flux.error(new IllegalStateException("object length exceeds " + maxObjectLength + ": "
                            + this.writerIndex + " bytes discarded"));
                }
                DataBufferFactory dataBufferFactory = buffer.factory();
                for (/* use current index */; this.index < this.writerIndex; this.index++) {
                    byte c = this.input.getByte(this.index);
                    if (this.state == ST_DECODING_NORMAL) {
                        decodeByte(c, this.input, this.index);

                        // All opening braces/brackets have been closed. That's enough to conclude
                        // that the JSON object/array is complete.
                        if (this.openBraces == 0) {
                            ByteBuf json = extractObject(this.input, this.input.readerIndex(),
                                    this.index + 1 - this.input.readerIndex());
                            if (json != null) {
                                chunks.add(dataBufferFactory.wrap(json.nioBuffer()));
                            }

                            // The JSON object/array was extracted => discard the bytes from
                            // the input buffer.
                            this.input.readerIndex(this.index + 1);
                            // Reset the object state to get ready for the next JSON object/text
                            // coming along the byte stream.
                            reset();
                        }
                    } else if (this.state == ST_DECODING_ARRAY_STREAM) {
                        decodeByte(c, this.input, this.index);

                        if (!this.insideString
                                && (this.openBraces == 1 && c == ',' || this.openBraces == 0 && c == ']')) {
                            // skip leading spaces. No range check is needed and the loop will terminate
                            // because the byte at position index is not a whitespace.
                            for (int i = this.input.readerIndex(); Character
                                    .isWhitespace(this.input.getByte(i)); i++) {
                                this.input.skipBytes(1);
                            }

                            // skip trailing spaces.
                            int idxNoSpaces = this.index - 1;
                            while (idxNoSpaces >= this.input.readerIndex()
                                    && Character.isWhitespace(this.input.getByte(idxNoSpaces))) {

                                idxNoSpaces--;
                            }

                            ByteBuf json = extractObject(this.input, this.input.readerIndex(),
                                    idxNoSpaces + 1 - this.input.readerIndex());

                            if (json != null) {
                                chunks.add(dataBufferFactory.wrap(json.nioBuffer()));
                            }

                            this.input.readerIndex(this.index + 1);

                            if (c == ']') {
                                reset();
                            }
                        }
                        // JSON object/array detected. Accumulate bytes until all braces/brackets are closed.
                    } else if (c == '{' || c == '[') {
                        initDecoding(c, streamArrayElements);

                        if (this.state == ST_DECODING_ARRAY_STREAM) {
                            // Discard the array bracket
                            this.input.skipBytes(1);
                        }
                        // Discard leading spaces in front of a JSON object/array.
                    } else if (Character.isWhitespace(c)) {
                        this.input.skipBytes(1);
                    } else {
                        this.state = ST_CORRUPTED;
                        return Flux.error(new IllegalStateException("invalid JSON received at byte position "
                                + this.index + ": " + ByteBufUtil.hexDump(this.input)));
                    }
                }

                return Flux.fromIterable(chunks);
            }

            /**
             * Override this method if you want to filter the json objects/arrays that
             * get passed through the pipeline.
             */
            protected ByteBuf extractObject(ByteBuf buffer, int index, int length) {
                return buffer.slice(index, length).retain();
            }

            private void decodeByte(byte c, ByteBuf input, int index) {
                if ((c == '{' || c == '[') && !this.insideString) {
                    this.openBraces++;
                } else if ((c == '}' || c == ']') && !this.insideString) {
                    this.openBraces--;
                } else if (c == '"') {
                    // start of a new JSON string. It's necessary to detect strings as they may
                    // also contain braces/brackets and that could lead to incorrect results.
                    if (!this.insideString) {
                        this.insideString = true;
                        // If the double quote wasn't escaped then this is the end of a string.
                    } else if (input.getByte(index - 1) != '\\') {
                        this.insideString = false;
                    }
                }
            }

            private void initDecoding(byte openingBrace, boolean streamArrayElements) {
                this.openBraces = 1;
                if (openingBrace == '[' && streamArrayElements) {
                    this.state = ST_DECODING_ARRAY_STREAM;
                } else {
                    this.state = ST_DECODING_NORMAL;
                }
            }

            private void reset() {
                this.insideString = false;
                this.state = ST_INIT;
                this.openBraces = 0;
            }
        });
    }

}