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 state;
49      private boolean insideString;
50  
51      private final int maxObjectLength;
52      private final boolean streamArrayElements;
53  
54      public JsonObjectDecoder() {
55          // 1 MB
56          this(1024 * 1024);
57      }
58  
59      public JsonObjectDecoder(int maxObjectLength) {
60          this(maxObjectLength, false);
61      }
62  
63      public JsonObjectDecoder(boolean streamArrayElements) {
64          this(1024 * 1024, streamArrayElements);
65      }
66  
67      /**
68       * @param maxObjectLength   maximum number of bytes a JSON object/array may use (including braces and all).
69       *                             Objects exceeding this length are dropped and an {@link TooLongFrameException}
70       *                             is thrown.
71       * @param streamArrayElements   if set to true and the "top level" JSON object is an array, each of its entries
72       *                                  is passed through the pipeline individually and immediately after it was fully
73       *                                  received, allowing for arrays with "infinitely" many elements.
74       *
75       */
76      public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) {
77          if (maxObjectLength < 1) {
78              throw new IllegalArgumentException("maxObjectLength must be a positive int");
79          }
80          this.maxObjectLength = maxObjectLength;
81          this.streamArrayElements = streamArrayElements;
82      }
83  
84      @Override
85      protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
86          if (state == ST_CORRUPTED) {
87              in.skipBytes(in.readableBytes());
88              return;
89          }
90  
91          // index of next byte to process.
92          int idx = this.idx;
93          int wrtIdx = in.writerIndex();
94  
95          if (wrtIdx > maxObjectLength) {
96              // buffer size exceeded maxObjectLength; discarding the complete buffer.
97              in.skipBytes(in.readableBytes());
98              reset();
99              throw new TooLongFrameException(
100                             "object length exceeds " + maxObjectLength + ": " + wrtIdx + " bytes discarded");
101         }
102 
103         for (/* use current idx */; idx < wrtIdx; idx++) {
104             byte c = in.getByte(idx);
105             if (state == ST_DECODING_NORMAL) {
106                 decodeByte(c, in, idx);
107 
108                 // All opening braces/brackets have been closed. That's enough to conclude
109                 // that the JSON object/array is complete.
110                 if (openBraces == 0) {
111                     ByteBuf json = extractObject(ctx, in, in.readerIndex(), idx + 1 - in.readerIndex());
112                     if (json != null) {
113                         out.add(json);
114                     }
115 
116                     // The JSON object/array was extracted => discard the bytes from
117                     // the input buffer.
118                     in.readerIndex(idx + 1);
119                     // Reset the object state to get ready for the next JSON object/text
120                     // coming along the byte stream.
121                     reset();
122                 }
123             } else if (state == ST_DECODING_ARRAY_STREAM) {
124                 decodeByte(c, in, idx);
125 
126                 if (!insideString && (openBraces == 1 && c == ',' || openBraces == 0 && c == ']')) {
127                     // skip leading spaces. No range check is needed and the loop will terminate
128                     // because the byte at position idx is not a whitespace.
129                     for (int i = in.readerIndex(); Character.isWhitespace(in.getByte(i)); i++) {
130                         in.skipBytes(1);
131                     }
132 
133                     // skip trailing spaces.
134                     int idxNoSpaces = idx - 1;
135                     while (idxNoSpaces >= in.readerIndex() && Character.isWhitespace(in.getByte(idxNoSpaces))) {
136                         idxNoSpaces--;
137                     }
138 
139                     ByteBuf json = extractObject(ctx, in, in.readerIndex(), idxNoSpaces + 1 - in.readerIndex());
140                     if (json != null) {
141                         out.add(json);
142                     }
143 
144                     in.readerIndex(idx + 1);
145 
146                     if (c == ']') {
147                         reset();
148                     }
149                 }
150             // JSON object/array detected. Accumulate bytes until all braces/brackets are closed.
151             } else if (c == '{' || c == '[') {
152                 initDecoding(c);
153 
154                 if (state == ST_DECODING_ARRAY_STREAM) {
155                     // Discard the array bracket
156                     in.skipBytes(1);
157                 }
158             // Discard leading spaces in front of a JSON object/array.
159             } else if (Character.isWhitespace(c)) {
160                 in.skipBytes(1);
161             } else {
162                 state = ST_CORRUPTED;
163                 throw new CorruptedFrameException(
164                         "invalid JSON received at byte position " + idx + ": " + ByteBufUtil.hexDump(in));
165             }
166         }
167 
168         if (in.readableBytes() == 0) {
169             this.idx = 0;
170         } else {
171             this.idx = idx;
172         }
173     }
174 
175     /**
176      * Override this method if you want to filter the json objects/arrays that get passed through the pipeline.
177      */
178     @SuppressWarnings("UnusedParameters")
179     protected ByteBuf extractObject(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) {
180         return buffer.slice(index, length).retain();
181     }
182 
183     private void decodeByte(byte c, ByteBuf in, int idx) {
184         if ((c == '{' || c == '[') && !insideString) {
185             openBraces++;
186         } else if ((c == '}' || c == ']') && !insideString) {
187             openBraces--;
188         } else if (c == '"') {
189             // start of a new JSON string. It's necessary to detect strings as they may
190             // also contain braces/brackets and that could lead to incorrect results.
191             if (!insideString) {
192                 insideString = true;
193             // If the double quote wasn't escaped then this is the end of a string.
194             } else if (in.getByte(idx - 1) != '\\') {
195                 insideString = false;
196             }
197         }
198     }
199 
200     private void initDecoding(byte openingBrace) {
201         openBraces = 1;
202         if (openingBrace == '[' && streamArrayElements) {
203             state = ST_DECODING_ARRAY_STREAM;
204         } else {
205             state = ST_DECODING_NORMAL;
206         }
207     }
208 
209     private void reset() {
210         insideString = false;
211         state = ST_INIT;
212         openBraces = 0;
213     }
214 }