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