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();
95              if (state == ST_DECODING_ARRAY_STREAM) {
96                  insideString = false;
97                  openBraces = 1;
98              }
99          }
100 
101         // index of next byte to process.
102         int idx = this.idx;
103         int wrtIdx = in.writerIndex();
104 
105         if (wrtIdx > maxObjectLength) {
106             // buffer size exceeded maxObjectLength; discarding the complete buffer.
107             in.skipBytes(in.readableBytes());
108             reset();
109             throw new TooLongFrameException(
110                             "object length exceeds " + maxObjectLength + ": " + wrtIdx + " bytes discarded");
111         }
112 
113         for (/* use current idx */; idx < wrtIdx; idx++) {
114             byte c = in.getByte(idx);
115             if (state == ST_DECODING_NORMAL) {
116                 decodeByte(c, in, idx);
117 
118                 // All opening braces/brackets have been closed. That's enough to conclude
119                 // that the JSON object/array is complete.
120                 if (openBraces == 0) {
121                     ByteBuf json = extractObject(ctx, in, in.readerIndex(), idx + 1 - in.readerIndex());
122                     if (json != null) {
123                         out.add(json);
124                     }
125 
126                     // The JSON object/array was extracted => discard the bytes from
127                     // the input buffer.
128                     in.readerIndex(idx + 1);
129                     // Reset the object state to get ready for the next JSON object/text
130                     // coming along the byte stream.
131                     reset();
132                 }
133             } else if (state == ST_DECODING_ARRAY_STREAM) {
134                 decodeByte(c, in, idx);
135 
136                 if (!insideString && (openBraces == 1 && c == ',' || openBraces == 0 && c == ']')) {
137                     // skip leading spaces. No range check is needed and the loop will terminate
138                     // because the byte at position idx is not a whitespace.
139                     for (int i = in.readerIndex(); Character.isWhitespace(in.getByte(i)); i++) {
140                         in.skipBytes(1);
141                     }
142 
143                     // skip trailing spaces.
144                     int idxNoSpaces = idx - 1;
145                     while (idxNoSpaces >= in.readerIndex() && Character.isWhitespace(in.getByte(idxNoSpaces))) {
146                         idxNoSpaces--;
147                     }
148 
149                     ByteBuf json = extractObject(ctx, in, in.readerIndex(), idxNoSpaces + 1 - in.readerIndex());
150                     if (json != null) {
151                         out.add(json);
152                     }
153 
154                     in.readerIndex(idx + 1);
155 
156                     if (c == ']') {
157                         reset();
158                     }
159                 }
160             // JSON object/array detected. Accumulate bytes until all braces/brackets are closed.
161             } else if (c == '{' || c == '[') {
162                 initDecoding(c);
163 
164                 if (state == ST_DECODING_ARRAY_STREAM) {
165                     // Discard the array bracket
166                     in.skipBytes(1);
167                 }
168             // Discard leading spaces in front of a JSON object/array.
169             } else if (Character.isWhitespace(c)) {
170                 in.skipBytes(1);
171             } else {
172                 state = ST_CORRUPTED;
173                 throw new CorruptedFrameException(
174                         "invalid JSON received at byte position " + idx + ": " + ByteBufUtil.hexDump(in));
175             }
176         }
177 
178         if (in.readableBytes() == 0) {
179             this.idx = 0;
180         } else {
181             this.idx = idx;
182         }
183         this.lastReaderIndex = in.readerIndex();
184     }
185 
186     /**
187      * Override this method if you want to filter the json objects/arrays that get passed through the pipeline.
188      */
189     @SuppressWarnings("UnusedParameters")
190     protected ByteBuf extractObject(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) {
191         return buffer.retainedSlice(index, length);
192     }
193 
194     private void decodeByte(byte c, ByteBuf in, int idx) {
195         if ((c == '{' || c == '[') && !insideString) {
196             openBraces++;
197         } else if ((c == '}' || c == ']') && !insideString) {
198             openBraces--;
199         } else if (c == '"') {
200             // start of a new JSON string. It's necessary to detect strings as they may
201             // also contain braces/brackets and that could lead to incorrect results.
202             if (!insideString) {
203                 insideString = true;
204             } else {
205                 int backslashCount = 0;
206                 idx--;
207                 while (idx >= 0) {
208                     if (in.getByte(idx) == '\\') {
209                         backslashCount++;
210                         idx--;
211                     } else {
212                         break;
213                     }
214                 }
215                 // The double quote isn't escaped only if there are even "\"s.
216                 if (backslashCount % 2 == 0) {
217                     // Since the double quote isn't escaped then this is the end of a string.
218                     insideString = false;
219                 }
220             }
221         }
222     }
223 
224     private void initDecoding(byte openingBrace) {
225         openBraces = 1;
226         if (openingBrace == '[' && streamArrayElements) {
227             state = ST_DECODING_ARRAY_STREAM;
228         } else {
229             state = ST_DECODING_NORMAL;
230         }
231     }
232 
233     private void reset() {
234         insideString = false;
235         state = ST_INIT;
236         openBraces = 0;
237     }
238 }