View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.handler.codec.json;
18  
19  import static io.netty.util.internal.ObjectUtil.checkPositive;
20  
21  import io.netty.buffer.ByteBuf;
22  import io.netty.buffer.ByteBufUtil;
23  import io.netty.channel.ChannelHandlerContext;
24  import io.netty.handler.codec.ByteToMessageDecoder;
25  import io.netty.handler.codec.CorruptedFrameException;
26  import io.netty.handler.codec.TooLongFrameException;
27  import io.netty.channel.ChannelPipeline;
28  
29  import java.util.List;
30  
31  /**
32   * Splits a byte stream of JSON objects and arrays into individual objects/arrays and passes them up the
33   * {@link ChannelPipeline}.
34   * <p>
35   * The byte stream is expected to be in UTF-8 character encoding or ASCII. The current implementation
36   * uses direct {@code byte} to {@code char} cast and then compares that {@code char} to a few low range
37   * ASCII characters like {@code '{'}, {@code '['} or {@code '"'}. UTF-8 is not using low range [0..0x7F]
38   * byte values for multibyte codepoint representations therefore fully supported by this implementation.
39   * <p>
40   * This class does not do any real parsing or validation. A sequence of bytes is considered a JSON object/array
41   * if it contains a matching number of opening and closing braces/brackets. It's up to a subsequent
42   * {@link ChannelHandler} to parse the JSON text into a more usable form i.e. a POJO.
43   */
44  public class JsonObjectDecoder extends ByteToMessageDecoder {
45  
46      private static final int ST_CORRUPTED = -1;
47      private static final int ST_INIT = 0;
48      private static final int ST_DECODING_NORMAL = 1;
49      private static final int ST_DECODING_ARRAY_STREAM = 2;
50  
51      private int openBraces;
52      private int idx;
53  
54      private int lastReaderIndex;
55  
56      private int state;
57      private boolean insideString;
58  
59      private final int maxObjectLength;
60      private final boolean streamArrayElements;
61  
62      public JsonObjectDecoder() {
63          // 1 MB
64          this(1024 * 1024);
65      }
66  
67      public JsonObjectDecoder(int maxObjectLength) {
68          this(maxObjectLength, false);
69      }
70  
71      public JsonObjectDecoder(boolean streamArrayElements) {
72          this(1024 * 1024, streamArrayElements);
73      }
74  
75      /**
76       * @param maxObjectLength   maximum number of bytes a JSON object/array may use (including braces and all).
77       *                             Objects exceeding this length are dropped and an {@link TooLongFrameException}
78       *                             is thrown.
79       * @param streamArrayElements   if set to true and the "top level" JSON object is an array, each of its entries
80       *                                  is passed through the pipeline individually and immediately after it was fully
81       *                                  received, allowing for arrays with "infinitely" many elements.
82       *
83       */
84      public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) {
85          this.maxObjectLength = checkPositive(maxObjectLength, "maxObjectLength");
86          this.streamArrayElements = streamArrayElements;
87      }
88  
89      @Override
90      protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
91          if (state == ST_CORRUPTED) {
92              in.skipBytes(in.readableBytes());
93              return;
94          }
95  
96          if (this.idx > in.readerIndex() && lastReaderIndex != in.readerIndex()) {
97              this.idx = in.readerIndex() + (idx - lastReaderIndex);
98          }
99  
100         // index of next byte to process.
101         int idx = this.idx;
102         int wrtIdx = in.writerIndex();
103 
104         if (wrtIdx > maxObjectLength) {
105             // buffer size exceeded maxObjectLength; discarding the complete buffer.
106             in.skipBytes(in.readableBytes());
107             reset();
108             throw new TooLongFrameException(
109                             "object length exceeds " + maxObjectLength + ": " + wrtIdx + " bytes discarded");
110         }
111 
112         for (/* use current idx */; idx < wrtIdx; idx++) {
113             byte c = in.getByte(idx);
114             if (state == ST_DECODING_NORMAL) {
115                 decodeByte(c, in, idx);
116 
117                 // All opening braces/brackets have been closed. That's enough to conclude
118                 // that the JSON object/array is complete.
119                 if (openBraces == 0) {
120                     ByteBuf json = extractObject(ctx, in, in.readerIndex(), idx + 1 - in.readerIndex());
121                     if (json != null) {
122                         out.add(json);
123                     }
124 
125                     // The JSON object/array was extracted => discard the bytes from
126                     // the input buffer.
127                     in.readerIndex(idx + 1);
128                     // Reset the object state to get ready for the next JSON object/text
129                     // coming along the byte stream.
130                     reset();
131                 }
132             } else if (state == ST_DECODING_ARRAY_STREAM) {
133                 decodeByte(c, in, idx);
134 
135                 if (!insideString && (openBraces == 1 && c == ',' || openBraces == 0 && c == ']')) {
136                     // skip leading spaces. No range check is needed and the loop will terminate
137                     // because the byte at position idx is not a whitespace.
138                     for (int i = in.readerIndex(); Character.isWhitespace(in.getByte(i)); i++) {
139                         in.skipBytes(1);
140                     }
141 
142                     // skip trailing spaces.
143                     int idxNoSpaces = idx - 1;
144                     while (idxNoSpaces >= in.readerIndex() && Character.isWhitespace(in.getByte(idxNoSpaces))) {
145                         idxNoSpaces--;
146                     }
147 
148                     ByteBuf json = extractObject(ctx, in, in.readerIndex(), idxNoSpaces + 1 - in.readerIndex());
149                     if (json != null) {
150                         out.add(json);
151                     }
152 
153                     in.readerIndex(idx + 1);
154 
155                     if (c == ']') {
156                         reset();
157                     }
158                 }
159             // JSON object/array detected. Accumulate bytes until all braces/brackets are closed.
160             } else if (c == '{' || c == '[') {
161                 initDecoding(c);
162 
163                 if (state == ST_DECODING_ARRAY_STREAM) {
164                     // Discard the array bracket
165                     in.skipBytes(1);
166                 }
167             // Discard leading spaces in front of a JSON object/array.
168             } else if (Character.isWhitespace(c)) {
169                 in.skipBytes(1);
170             } else {
171                 state = ST_CORRUPTED;
172                 throw new CorruptedFrameException(
173                         "invalid JSON received at byte position " + idx + ": " + ByteBufUtil.hexDump(in));
174             }
175         }
176 
177         if (in.readableBytes() == 0) {
178             this.idx = 0;
179         } else {
180             this.idx = idx;
181         }
182         this.lastReaderIndex = in.readerIndex();
183     }
184 
185     /**
186      * Override this method if you want to filter the json objects/arrays that get passed through the pipeline.
187      */
188     @SuppressWarnings("UnusedParameters")
189     protected ByteBuf extractObject(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) {
190         return buffer.retainedSlice(index, length);
191     }
192 
193     private void decodeByte(byte c, ByteBuf in, int idx) {
194         if ((c == '{' || c == '[') && !insideString) {
195             openBraces++;
196         } else if ((c == '}' || c == ']') && !insideString) {
197             openBraces--;
198         } else if (c == '"') {
199             // start of a new JSON string. It's necessary to detect strings as they may
200             // also contain braces/brackets and that could lead to incorrect results.
201             if (!insideString) {
202                 insideString = true;
203             } else {
204                 int backslashCount = 0;
205                 idx--;
206                 while (idx >= 0) {
207                     if (in.getByte(idx) == '\\') {
208                         backslashCount++;
209                         idx--;
210                     } else {
211                         break;
212                     }
213                 }
214                 // The double quote isn't escaped only if there are even "\"s.
215                 if (backslashCount % 2 == 0) {
216                     // Since the double quote isn't escaped then this is the end of a string.
217                     insideString = false;
218                 }
219             }
220         }
221     }
222 
223     private void initDecoding(byte openingBrace) {
224         openBraces = 1;
225         if (openingBrace == '[' && streamArrayElements) {
226             state = ST_DECODING_ARRAY_STREAM;
227         } else {
228             state = ST_DECODING_NORMAL;
229         }
230     }
231 
232     private void reset() {
233         insideString = false;
234         state = ST_INIT;
235         openBraces = 0;
236     }
237 }