1//
2//  ========================================================================
3//  Copyright (c) 1995-2014 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
19package org.eclipse.jetty.util;
20
21import java.io.IOException;
22import java.util.Arrays;
23import java.util.NoSuchElementException;
24import 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 */
38public 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     * The characters ", \, \n, \r, \t, \f and \b are escaped
338     * @param buffer The Appendable
339     * @param input The String to quote.
340     */
341    public static void quote(Appendable buffer, String input)
342    {
343        try
344        {
345            buffer.append('"');
346            for (int i = 0; i < input.length(); ++i)
347            {
348                char c = input.charAt(i);
349                if (c >= 32)
350                {
351                    if (c == '"' || c == '\\')
352                        buffer.append('\\');
353                    buffer.append(c);
354                }
355                else
356                {
357                    char escape = escapes[c];
358                    if (escape == 0xFFFF)
359                    {
360                        // Unicode escape
361                        buffer.append('\\').append('u').append('0').append('0');
362                        if (c < 0x10)
363                            buffer.append('0');
364                        buffer.append(Integer.toString(c, 16));
365                    }
366                    else
367                    {
368                        buffer.append('\\').append(escape);
369                    }
370                }
371            }
372            buffer.append('"');
373        }
374        catch (IOException x)
375        {
376            throw new RuntimeException(x);
377        }
378    }
379
380    /* ------------------------------------------------------------ */
381    /** Quote a string into a StringBuffer only if needed.
382     * Quotes are forced if any delim characters are present.
383     *
384     * @param buf The StringBuffer
385     * @param s The String to quote.
386     * @param delim String of characters that must be quoted.
387     * @return true if quoted;
388     */
389    public static boolean quoteIfNeeded(Appendable buf, String s,String delim)
390    {
391        for (int i=0;i<s.length();i++)
392        {
393            char c = s.charAt(i);
394            if (delim.indexOf(c)>=0)
395            {
396            	quote(buf,s);
397            	return true;
398            }
399        }
400
401        try
402        {
403            buf.append(s);
404            return false;
405        }
406        catch(IOException e)
407        {
408            throw new RuntimeException(e);
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     * @return quoted string
476     */
477    public static String unquote(String s, boolean lenient)
478    {
479        if (s==null)
480            return null;
481        if (s.length()<2)
482            return s;
483
484        char first=s.charAt(0);
485        char last=s.charAt(s.length()-1);
486        if (first!=last || (first!='"' && first!='\''))
487            return s;
488
489        StringBuilder b = new StringBuilder(s.length() - 2);
490        boolean escape=false;
491        for (int i=1;i<s.length()-1;i++)
492        {
493            char c = s.charAt(i);
494
495            if (escape)
496            {
497                escape=false;
498                switch (c)
499                {
500                    case 'n':
501                        b.append('\n');
502                        break;
503                    case 'r':
504                        b.append('\r');
505                        break;
506                    case 't':
507                        b.append('\t');
508                        break;
509                    case 'f':
510                        b.append('\f');
511                        break;
512                    case 'b':
513                        b.append('\b');
514                        break;
515                    case '\\':
516                        b.append('\\');
517                        break;
518                    case '/':
519                        b.append('/');
520                        break;
521                    case '"':
522                        b.append('"');
523                        break;
524                    case 'u':
525                        b.append((char)(
526                                (TypeUtil.convertHexDigit((byte)s.charAt(i++))<<24)+
527                                (TypeUtil.convertHexDigit((byte)s.charAt(i++))<<16)+
528                                (TypeUtil.convertHexDigit((byte)s.charAt(i++))<<8)+
529                                (TypeUtil.convertHexDigit((byte)s.charAt(i++)))
530                                )
531                        );
532                        break;
533                    default:
534                        if (lenient && !isValidEscaping(c))
535                        {
536                            b.append('\\');
537                        }
538                        b.append(c);
539                }
540            }
541            else if (c=='\\')
542            {
543                escape=true;
544            }
545            else
546            {
547                b.append(c);
548            }
549        }
550
551        return b.toString();
552    }
553
554
555    /* ------------------------------------------------------------ */
556    /** Check that char c (which is preceded by a backslash) is a valid
557     * escape sequence.
558     * @param c
559     * @return
560     */
561    private static boolean isValidEscaping(char c)
562    {
563        return ((c == 'n') || (c == 'r') || (c == 't') ||
564                 (c == 'f') || (c == 'b') || (c == '\\') ||
565                 (c == '/') || (c == '"') || (c == 'u'));
566    }
567
568    /* ------------------------------------------------------------ */
569    /**
570     * @return handle double quotes if true
571     */
572    public boolean getDouble()
573    {
574        return _double;
575    }
576
577    /* ------------------------------------------------------------ */
578    /**
579     * @param d handle double quotes if true
580     */
581    public void setDouble(boolean d)
582    {
583        _double=d;
584    }
585
586    /* ------------------------------------------------------------ */
587    /**
588     * @return handle single quotes if true
589     */
590    public boolean getSingle()
591    {
592        return _single;
593    }
594
595    /* ------------------------------------------------------------ */
596    /**
597     * @param single handle single quotes if true
598     */
599    public void setSingle(boolean single)
600    {
601        _single=single;
602    }
603}
604