1 //
2 // ========================================================================
3 // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4 // ------------------------------------------------------------------------
5 // All rights reserved. This program and the accompanying materials
6 // are made available under the terms of the Eclipse Public License v1.0
7 // and Apache License v2.0 which accompanies this distribution.
8 //
9 // The Eclipse Public License is available at
10 // http://www.eclipse.org/legal/epl-v10.html
11 //
12 // The Apache License v2.0 is available at
13 // http://www.opensource.org/licenses/apache2.0.php
14 //
15 // You may elect to redistribute this code under either of these licenses.
16 // ========================================================================
17 //
18
19 package org.eclipse.jetty.util;
20
21 import java.io.IOException;
22 import java.util.Arrays;
23 import java.util.NoSuchElementException;
24 import java.util.StringTokenizer;
25
26 /* ------------------------------------------------------------ */
27 /** StringTokenizer with Quoting support.
28 *
29 * This class is a copy of the java.util.StringTokenizer API and
30 * the behaviour is the same, except that single and double quoted
31 * string values are recognised.
32 * Delimiters within quotes are not considered delimiters.
33 * Quotes can be escaped with '\'.
34 *
35 * @see java.util.StringTokenizer
36 *
37 */
38 public class QuotedStringTokenizer
39 extends StringTokenizer
40 {
41 private final static String __delim="\t\n\r";
42 private String _string;
43 private String _delim = __delim;
44 private boolean _returnQuotes=false;
45 private boolean _returnDelimiters=false;
46 private StringBuffer _token;
47 private boolean _hasToken=false;
48 private int _i=0;
49 private int _lastStart=0;
50 private boolean _double=true;
51 private boolean _single=true;
52
53 /* ------------------------------------------------------------ */
54 public QuotedStringTokenizer(String str,
55 String delim,
56 boolean returnDelimiters,
57 boolean returnQuotes)
58 {
59 super("");
60 _string=str;
61 if (delim!=null)
62 _delim=delim;
63 _returnDelimiters=returnDelimiters;
64 _returnQuotes=returnQuotes;
65
66 if (_delim.indexOf('\'')>=0 ||
67 _delim.indexOf('"')>=0)
68 throw new Error("Can't use quotes as delimiters: "+_delim);
69
70 _token=new StringBuffer(_string.length()>1024?512:_string.length()/2);
71 }
72
73 /* ------------------------------------------------------------ */
74 public QuotedStringTokenizer(String str,
75 String delim,
76 boolean returnDelimiters)
77 {
78 this(str,delim,returnDelimiters,false);
79 }
80
81 /* ------------------------------------------------------------ */
82 public QuotedStringTokenizer(String str,
83 String delim)
84 {
85 this(str,delim,false,false);
86 }
87
88 /* ------------------------------------------------------------ */
89 public QuotedStringTokenizer(String str)
90 {
91 this(str,null,false,false);
92 }
93
94 /* ------------------------------------------------------------ */
95 @Override
96 public boolean hasMoreTokens()
97 {
98 // Already found a token
99 if (_hasToken)
100 return true;
101
102 _lastStart=_i;
103
104 int state=0;
105 boolean escape=false;
106 while (_i<_string.length())
107 {
108 char c=_string.charAt(_i++);
109
110 switch (state)
111 {
112 case 0: // Start
113 if(_delim.indexOf(c)>=0)
114 {
115 if (_returnDelimiters)
116 {
117 _token.append(c);
118 return _hasToken=true;
119 }
120 }
121 else if (c=='\'' && _single)
122 {
123 if (_returnQuotes)
124 _token.append(c);
125 state=2;
126 }
127 else if (c=='\"' && _double)
128 {
129 if (_returnQuotes)
130 _token.append(c);
131 state=3;
132 }
133 else
134 {
135 _token.append(c);
136 _hasToken=true;
137 state=1;
138 }
139 break;
140
141 case 1: // Token
142 _hasToken=true;
143 if(_delim.indexOf(c)>=0)
144 {
145 if (_returnDelimiters)
146 _i--;
147 return _hasToken;
148 }
149 else if (c=='\'' && _single)
150 {
151 if (_returnQuotes)
152 _token.append(c);
153 state=2;
154 }
155 else if (c=='\"' && _double)
156 {
157 if (_returnQuotes)
158 _token.append(c);
159 state=3;
160 }
161 else
162 {
163 _token.append(c);
164 }
165 break;
166
167 case 2: // Single Quote
168 _hasToken=true;
169 if (escape)
170 {
171 escape=false;
172 _token.append(c);
173 }
174 else if (c=='\'')
175 {
176 if (_returnQuotes)
177 _token.append(c);
178 state=1;
179 }
180 else if (c=='\\')
181 {
182 if (_returnQuotes)
183 _token.append(c);
184 escape=true;
185 }
186 else
187 {
188 _token.append(c);
189 }
190 break;
191
192 case 3: // Double Quote
193 _hasToken=true;
194 if (escape)
195 {
196 escape=false;
197 _token.append(c);
198 }
199 else if (c=='\"')
200 {
201 if (_returnQuotes)
202 _token.append(c);
203 state=1;
204 }
205 else if (c=='\\')
206 {
207 if (_returnQuotes)
208 _token.append(c);
209 escape=true;
210 }
211 else
212 {
213 _token.append(c);
214 }
215 break;
216 }
217 }
218
219 return _hasToken;
220 }
221
222 /* ------------------------------------------------------------ */
223 @Override
224 public String nextToken()
225 throws NoSuchElementException
226 {
227 if (!hasMoreTokens() || _token==null)
228 throw new NoSuchElementException();
229 String t=_token.toString();
230 _token.setLength(0);
231 _hasToken=false;
232 return t;
233 }
234
235 /* ------------------------------------------------------------ */
236 @Override
237 public String nextToken(String delim)
238 throws NoSuchElementException
239 {
240 _delim=delim;
241 _i=_lastStart;
242 _token.setLength(0);
243 _hasToken=false;
244 return nextToken();
245 }
246
247 /* ------------------------------------------------------------ */
248 @Override
249 public boolean hasMoreElements()
250 {
251 return hasMoreTokens();
252 }
253
254 /* ------------------------------------------------------------ */
255 @Override
256 public Object nextElement()
257 throws NoSuchElementException
258 {
259 return nextToken();
260 }
261
262 /* ------------------------------------------------------------ */
263 /** Not implemented.
264 */
265 @Override
266 public int countTokens()
267 {
268 return -1;
269 }
270
271
272 /* ------------------------------------------------------------ */
273 /** Quote a string.
274 * The string is quoted only if quoting is required due to
275 * embedded delimiters, quote characters or the
276 * empty string.
277 * @param s The string to quote.
278 * @param delim the delimiter to use to quote the string
279 * @return quoted string
280 */
281 public static String quoteIfNeeded(String s, String delim)
282 {
283 if (s==null)
284 return null;
285 if (s.length()==0)
286 return "\"\"";
287
288
289 for (int i=0;i<s.length();i++)
290 {
291 char c = s.charAt(i);
292 if (c=='\\' || c=='"' || c=='\'' || Character.isWhitespace(c) || delim.indexOf(c)>=0)
293 {
294 StringBuffer b=new StringBuffer(s.length()+8);
295 quote(b,s);
296 return b.toString();
297 }
298 }
299
300 return s;
301 }
302
303 /* ------------------------------------------------------------ */
304 /** Quote a string.
305 * The string is quoted only if quoting is required due to
306 * embeded delimiters, quote characters or the
307 * empty string.
308 * @param s The string to quote.
309 * @return quoted string
310 */
311 public static String quote(String s)
312 {
313 if (s==null)
314 return null;
315 if (s.length()==0)
316 return "\"\"";
317
318 StringBuffer b=new StringBuffer(s.length()+8);
319 quote(b,s);
320 return b.toString();
321
322 }
323
324 private static final char[] escapes = new char[32];
325 static
326 {
327 Arrays.fill(escapes, (char)0xFFFF);
328 escapes['\b'] = 'b';
329 escapes['\t'] = 't';
330 escapes['\n'] = 'n';
331 escapes['\f'] = 'f';
332 escapes['\r'] = 'r';
333 }
334
335 /* ------------------------------------------------------------ */
336 /** Quote a string into an Appendable.
337 * Only quotes and backslash are escaped.
338 * @param buffer The Appendable
339 * @param input The String to quote.
340 */
341 public static void quoteOnly(Appendable buffer, String input)
342 {
343 if(input==null)
344 return;
345
346 try
347 {
348 buffer.append('"');
349 for (int i = 0; i < input.length(); ++i)
350 {
351 char c = input.charAt(i);
352 if (c == '"' || c == '\\')
353 buffer.append('\\');
354 buffer.append(c);
355 }
356 buffer.append('"');
357 }
358 catch (IOException x)
359 {
360 throw new RuntimeException(x);
361 }
362 }
363
364 /* ------------------------------------------------------------ */
365 /** Quote a string into an Appendable.
366 * The characters ", \, \n, \r, \t, \f and \b are escaped
367 * @param buffer The Appendable
368 * @param input The String to quote.
369 */
370 public static void quote(Appendable buffer, String input)
371 {
372 if(input==null)
373 return;
374
375 try
376 {
377 buffer.append('"');
378 for (int i = 0; i < input.length(); ++i)
379 {
380 char c = input.charAt(i);
381 if (c >= 32)
382 {
383 if (c == '"' || c == '\\')
384 buffer.append('\\');
385 buffer.append(c);
386 }
387 else
388 {
389 char escape = escapes[c];
390 if (escape == 0xFFFF)
391 {
392 // Unicode escape
393 buffer.append('\\').append('u').append('0').append('0');
394 if (c < 0x10)
395 buffer.append('0');
396 buffer.append(Integer.toString(c, 16));
397 }
398 else
399 {
400 buffer.append('\\').append(escape);
401 }
402 }
403 }
404 buffer.append('"');
405 }
406 catch (IOException x)
407 {
408 throw new RuntimeException(x);
409 }
410 }
411
412
413 /* ------------------------------------------------------------ */
414 public static String unquoteOnly(String s)
415 {
416 return unquoteOnly(s, false);
417 }
418
419
420 /* ------------------------------------------------------------ */
421 /** Unquote a string, NOT converting unicode sequences
422 * @param s The string to unquote.
423 * @param lenient if true, will leave in backslashes that aren't valid escapes
424 * @return quoted string
425 */
426 public static String unquoteOnly(String s, boolean lenient)
427 {
428 if (s==null)
429 return null;
430 if (s.length()<2)
431 return s;
432
433 char first=s.charAt(0);
434 char last=s.charAt(s.length()-1);
435 if (first!=last || (first!='"' && first!='\''))
436 return s;
437
438 StringBuilder b = new StringBuilder(s.length() - 2);
439 boolean escape=false;
440 for (int i=1;i<s.length()-1;i++)
441 {
442 char c = s.charAt(i);
443
444 if (escape)
445 {
446 escape=false;
447 if (lenient && !isValidEscaping(c))
448 {
449 b.append('\\');
450 }
451 b.append(c);
452 }
453 else if (c=='\\')
454 {
455 escape=true;
456 }
457 else
458 {
459 b.append(c);
460 }
461 }
462
463 return b.toString();
464 }
465
466 /* ------------------------------------------------------------ */
467 public static String unquote(String s)
468 {
469 return unquote(s,false);
470 }
471
472 /* ------------------------------------------------------------ */
473 /** Unquote a string.
474 * @param s The string to unquote.
475 * @param lenient true if unquoting should be lenient to escaped content, leaving some alone, false if string unescaping
476 * @return quoted string
477 */
478 public static String unquote(String s, boolean lenient)
479 {
480 if (s==null)
481 return null;
482 if (s.length()<2)
483 return s;
484
485 char first=s.charAt(0);
486 char last=s.charAt(s.length()-1);
487 if (first!=last || (first!='"' && first!='\''))
488 return s;
489
490 StringBuilder b = new StringBuilder(s.length() - 2);
491 boolean escape=false;
492 for (int i=1;i<s.length()-1;i++)
493 {
494 char c = s.charAt(i);
495
496 if (escape)
497 {
498 escape=false;
499 switch (c)
500 {
501 case 'n':
502 b.append('\n');
503 break;
504 case 'r':
505 b.append('\r');
506 break;
507 case 't':
508 b.append('\t');
509 break;
510 case 'f':
511 b.append('\f');
512 break;
513 case 'b':
514 b.append('\b');
515 break;
516 case '\\':
517 b.append('\\');
518 break;
519 case '/':
520 b.append('/');
521 break;
522 case '"':
523 b.append('"');
524 break;
525 case 'u':
526 b.append((char)(
527 (TypeUtil.convertHexDigit((byte)s.charAt(i++))<<24)+
528 (TypeUtil.convertHexDigit((byte)s.charAt(i++))<<16)+
529 (TypeUtil.convertHexDigit((byte)s.charAt(i++))<<8)+
530 (TypeUtil.convertHexDigit((byte)s.charAt(i++)))
531 )
532 );
533 break;
534 default:
535 if (lenient && !isValidEscaping(c))
536 {
537 b.append('\\');
538 }
539 b.append(c);
540 }
541 }
542 else if (c=='\\')
543 {
544 escape=true;
545 }
546 else
547 {
548 b.append(c);
549 }
550 }
551
552 return b.toString();
553 }
554
555
556 /* ------------------------------------------------------------ */
557 /** Check that char c (which is preceded by a backslash) is a valid
558 * escape sequence.
559 * @param c
560 * @return
561 */
562 private static boolean isValidEscaping(char c)
563 {
564 return ((c == 'n') || (c == 'r') || (c == 't') ||
565 (c == 'f') || (c == 'b') || (c == '\\') ||
566 (c == '/') || (c == '"') || (c == 'u'));
567 }
568
569 /* ------------------------------------------------------------ */
570 public static boolean isQuoted(String s)
571 {
572 return s!=null && s.length()>0 && s.charAt(0)=='"' && s.charAt(s.length()-1)=='"';
573 }
574
575 /* ------------------------------------------------------------ */
576 /**
577 * @return handle double quotes if true
578 */
579 public boolean getDouble()
580 {
581 return _double;
582 }
583
584 /* ------------------------------------------------------------ */
585 /**
586 * @param d handle double quotes if true
587 */
588 public void setDouble(boolean d)
589 {
590 _double=d;
591 }
592
593 /* ------------------------------------------------------------ */
594 /**
595 * @return handle single quotes if true
596 */
597 public boolean getSingle()
598 {
599 return _single;
600 }
601
602 /* ------------------------------------------------------------ */
603 /**
604 * @param single handle single quotes if true
605 */
606 public void setSingle(boolean single)
607 {
608 _single=single;
609 }
610 }