1 /*
2 * Copyright 2002-2009 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package net.sf.json.util;
17
18 import net.sf.json.JSONArray;
19 import net.sf.json.JSONException;
20 import net.sf.json.JSONNull;
21 import net.sf.json.JSONObject;
22 import net.sf.json.JsonConfig;
23 import net.sf.json.regexp.RegexpUtils;
24
25
26 /**
27 * A JSONTokener takes a source string and extracts characters and tokens from
28 * it. It is used by the JSONObject and JSONArray constructors to parse JSON
29 * source strings.
30 *
31 * @author JSON.org
32 * @version 4
33 */
34 public class JSONTokener {
35 /**
36 * Get the hex value of a character (base16).
37 *
38 * @param c A character between '0' and '9' or between 'A' and 'F' or between
39 * 'a' and 'f'.
40 * @return An int between 0 and 15, or -1 if c was not a hex digit.
41 */
42 public static int dehexchar( char c ) {
43 if( c >= '0' && c <= '9' ){
44 return c - '0';
45 }
46 if( c >= 'A' && c <= 'F' ){
47 return c - ('A' - 10);
48 }
49 if( c >= 'a' && c <= 'f' ){
50 return c - ('a' - 10);
51 }
52 return -1;
53 }
54
55 /**
56 * The index of the next character.
57 */
58 private int myIndex;
59
60 /**
61 * The source string being tokenized.
62 */
63 private String mySource;
64
65 /**
66 * Construct a JSONTokener from a string.
67 *
68 * @param s A source string.
69 */
70 public JSONTokener( String s ) {
71 this.myIndex = 0;
72 if( s!= null ) {
73 s = s.trim();
74 } else {
75 s = "";
76 }
77 if( s.length() > 0 ){
78 char first = s.charAt( 0 );
79 char last = s.charAt( s.length() - 1 );
80 if( first == '[' && last != ']' ) {
81 throw syntaxError( "Found starting '[' but missing ']' at the end." );
82 }
83 if( first == '{' && last != '}' ) {
84 throw syntaxError( "Found starting '{' but missing '}' at the end." );
85 }
86 }
87 this.mySource = s;
88 }
89
90 /**
91 * Back up one character. This provides a sort of lookahead capability, so
92 * that you can test for a digit or letter before attempting to parse the
93 * next number or identifier.
94 */
95 public void back() {
96 if( this.myIndex > 0 ){
97 this.myIndex -= 1;
98 }
99 }
100
101 public int length() {
102 if( this.mySource == null ){
103 return 0;
104 }
105 return this.mySource.length();
106 }
107
108 public boolean matches( String pattern ) {
109 String str = this.mySource.substring( this.myIndex );
110 return RegexpUtils.getMatcher( pattern )
111 .matches( str );
112 }
113
114 /**
115 * Determine if the source string still contains characters that next() can
116 * consume.
117 *
118 * @return true if not yet at the end of the source.
119 */
120 public boolean more() {
121 return this.myIndex < this.mySource.length();
122 }
123
124 /**
125 * Get the next character in the source string.
126 *
127 * @return The next character, or 0 if past the end of the source string.
128 */
129 public char next() {
130 if( more() ){
131 char c = this.mySource.charAt( this.myIndex );
132 this.myIndex += 1;
133 return c;
134 }
135 return 0;
136 }
137
138 /**
139 * Consume the next character, and check that it matches a specified
140 * character.
141 *
142 * @param c The character to match.
143 * @return The character.
144 * @throws JSONException if the character does not match.
145 */
146 public char next( char c ) {
147 char n = next();
148 if( n != c ){
149 throw syntaxError( "Expected '" + c + "' and instead saw '" + n + "'." );
150 }
151 return n;
152 }
153
154 /**
155 * Get the next n characters.
156 *
157 * @param n The number of characters to take.
158 * @return A string of n characters.
159 * @throws JSONException Substring bounds error if there are not n characters
160 * remaining in the source string.
161 */
162 public String next( int n ) {
163 int i = this.myIndex;
164 int j = i + n;
165 if( j >= this.mySource.length() ){
166 throw syntaxError( "Substring bounds error" );
167 }
168 this.myIndex += n;
169 return this.mySource.substring( i, j );
170 }
171
172 /**
173 * Get the next char in the string, skipping whitespace and comments
174 * (slashslash, slashstar, and hash).
175 *
176 * @throws JSONException
177 * @return A character, or 0 if there are no more characters.
178 */
179 public char nextClean() {
180 for( ;; ){
181 char c = next();
182 if( c == '/' ){
183 switch( next() ){
184 case '/':
185 do{
186 c = next();
187 }while( c != '\n' && c != '\r' && c != 0 );
188 break;
189 case '*':
190 for( ;; ){
191 c = next();
192 if( c == 0 ){
193 throw syntaxError( "Unclosed comment." );
194 }
195 if( c == '*' ){
196 if( next() == '/' ){
197 break;
198 }
199 back();
200 }
201 }
202 break;
203 default:
204 back();
205 return '/';
206 }
207 }else if( c == '#' ){
208 do{
209 c = next();
210 }while( c != '\n' && c != '\r' && c != 0 );
211 }else if( c == 0 || c > ' ' ){
212 return c;
213 }
214 }
215 }
216
217 /**
218 * Return the characters up to the next close quote character. Backslash
219 * processing is done. The formal JSON format does not allow strings in
220 * single quotes, but an implementation is allowed to accept them.
221 *
222 * @param quote The quoting character, either <code>"</code> <small>(double
223 * quote)</small> or <code>'</code> <small>(single quote)</small>.
224 * @return A String.
225 * @throws JSONException Unterminated string.
226 */
227 public String nextString( char quote ) {
228 char c;
229 StringBuffer sb = new StringBuffer();
230 for( ;; ){
231 c = next();
232 switch( c ){
233 case 0:
234 case '\n':
235 case '\r':
236 throw syntaxError( "Unterminated string" );
237 case '\\':
238 c = next();
239 switch( c ){
240 case 'b':
241 sb.append( '\b' );
242 break;
243 case 't':
244 sb.append( '\t' );
245 break;
246 case 'n':
247 sb.append( '\n' );
248 break;
249 case 'f':
250 sb.append( '\f' );
251 break;
252 case 'r':
253 sb.append( '\r' );
254 break;
255 case 'u':
256 sb.append( (char) Integer.parseInt( next( 4 ), 16 ) );
257 break;
258 case 'x':
259 sb.append( (char) Integer.parseInt( next( 2 ), 16 ) );
260 break;
261 default:
262 sb.append( c );
263 }
264 break;
265 default:
266 if( c == quote ){
267 return sb.toString();
268 }
269 sb.append( c );
270 }
271 }
272 }
273
274 /**
275 * Get the text up but not including the specified character or the end of
276 * line, whichever comes first.
277 *
278 * @param d A delimiter character.
279 * @return A string.
280 */
281 public String nextTo( char d ) {
282 StringBuffer sb = new StringBuffer();
283 for( ;; ){
284 char c = next();
285 if( c == d || c == 0 || c == '\n' || c == '\r' ){
286 if( c != 0 ){
287 back();
288 }
289 return sb.toString()
290 .trim();
291 }
292 sb.append( c );
293 }
294 }
295
296 /**
297 * Get the text up but not including one of the specified delimeter
298 * characters or the end of line, whichever comes first.
299 *
300 * @param delimiters A set of delimiter characters.
301 * @return A string, trimmed.
302 */
303 public String nextTo( String delimiters ) {
304 char c;
305 StringBuffer sb = new StringBuffer();
306 for( ;; ){
307 c = next();
308 if( delimiters.indexOf( c ) >= 0 || c == 0 || c == '\n' || c == '\r' ){
309 if( c != 0 ){
310 back();
311 }
312 return sb.toString()
313 .trim();
314 }
315 sb.append( c );
316 }
317 }
318
319 /**
320 * Get the next value. The value can be a Boolean, Double, Integer,
321 * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object.
322 *
323 * @throws JSONException If syntax error.
324 * @return An object.
325 */
326 public Object nextValue() {
327 return nextValue( new JsonConfig() );
328 }
329
330 /**
331 * Get the next value. The value can be a Boolean, Double, Integer,
332 * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object.
333 *
334 * @throws JSONException If syntax error.
335 * @return An object.
336 */
337 public Object nextValue( JsonConfig jsonConfig ) {
338 char c = nextClean();
339 String s;
340
341 switch( c ){
342 case '"':
343 case '\'':
344 return nextString( c );
345 case '{':
346 back();
347 return JSONObject.fromObject( this, jsonConfig );
348 case '[':
349 back();
350 return JSONArray.fromObject( this, jsonConfig );
351 default:
352 // empty
353 }
354
355 /*
356 * Handle unquoted text. This could be the values true, false, or null, or
357 * it can be a number. An implementation (such as this one) is allowed to
358 * also accept non-standard forms. Accumulate characters until we reach
359 * the end of the text or a formatting character.
360 */
361
362 StringBuffer sb = new StringBuffer();
363 char b = c;
364 while( c >= ' ' && ",:]}/\\\"[{;=#".indexOf( c ) < 0 ){
365 sb.append( c );
366 c = next();
367 }
368 back();
369
370 /*
371 * If it is true, false, or null, return the proper value.
372 */
373
374 s = sb.toString()
375 .trim();
376 if( s.equals( "" ) ){
377 throw syntaxError( "Missing value." );
378 }
379 if( s.equalsIgnoreCase( "true" ) ){
380 return Boolean.TRUE;
381 }
382 if( s.equalsIgnoreCase( "false" ) ){
383 return Boolean.FALSE;
384 }
385 if( s.equals( "null" ) ||
386 (jsonConfig.isJavascriptCompliant() && s.equals("undefined"))){
387 return JSONNull.getInstance();
388 }
389
390 /*
391 * If it might be a number, try converting it. We support the 0- and 0x-
392 * conventions. If a number cannot be produced, then the value will just
393 * be a string. Note that the 0-, 0x-, plus, and implied string
394 * conventions are non-standard. A JSON parser is free to accept non-JSON
395 * forms as long as it accepts all correct JSON forms.
396 */
397
398 if( (b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+' ){
399 if( b == '0' ){
400 if( s.length() > 2 && (s.charAt( 1 ) == 'x' || s.charAt( 1 ) == 'X') ){
401 try{
402 return new Integer( Integer.parseInt( s.substring( 2 ), 16 ) );
403 }catch( Exception e ){
404 /* Ignore the error */
405 }
406 }else{
407 try{
408 return new Integer( Integer.parseInt( s, 8 ) );
409 }catch( Exception e ){
410 /* Ignore the error */
411 }
412 }
413 }
414 try{
415 return new Integer( s );
416 }catch( Exception e ){
417 try{
418 return new Long( s );
419 }catch( Exception f ){
420 try{
421 return new Double( s );
422 }catch( Exception g ){
423 return s;
424 }
425 }
426 }
427 }
428
429 if( JSONUtils.isFunctionHeader( s ) || JSONUtils.isFunction( s ) ){
430 return s;
431 }
432 switch( peek() ){
433 case ',':
434 case '}':
435 case '{':
436 case '[':
437 case ']':
438 throw new JSONException( "Unquotted string '" + s + "'" );
439 }
440
441 return s;
442 }
443
444 /**
445 * Look at the next character in the source string.
446 *
447 * @return The next character, or 0 if past the end of the source string.
448 */
449 public char peek() {
450 if( more() ){
451 char c = this.mySource.charAt( this.myIndex );
452 return c;
453 }
454 return 0;
455 }
456
457 public void reset() {
458 this.myIndex = 0;
459 }
460
461 /**
462 * Skip characters until past the requested string. If it is not found, we
463 * are left at the end of the source.
464 *
465 * @param to A string to skip past.
466 */
467 public void skipPast( String to ) {
468 this.myIndex = this.mySource.indexOf( to, this.myIndex );
469 if( this.myIndex < 0 ){
470 this.myIndex = this.mySource.length();
471 }else{
472 this.myIndex += to.length();
473 }
474 }
475
476 /**
477 * Skip characters until the next character is the requested character. If
478 * the requested character is not found, no characters are skipped.
479 *
480 * @param to A character to skip to.
481 * @return The requested character, or zero if the requested character is not
482 * found.
483 */
484 public char skipTo( char to ) {
485 char c;
486 int index = this.myIndex;
487 do{
488 c = next();
489 if( c == 0 ){
490 this.myIndex = index;
491 return c;
492 }
493 }while( c != to );
494 back();
495 return c;
496 }
497
498 /**
499 * Make a JSONException to signal a syntax error.
500 *
501 * @param message The error message.
502 * @return A JSONException object, suitable for throwing
503 */
504 public JSONException syntaxError( String message ) {
505 return new JSONException( message + toString() );
506 }
507
508 /**
509 * Make a printable string of this JSONTokener.
510 *
511 * @return " at character [this.myIndex] of [this.mySource]"
512 */
513 public String toString() {
514 return " at character " + this.myIndex + " of " + this.mySource;
515 }
516 }