View Javadoc

1   /*
2    * Copyright 2009 Red Hat, Inc.
3    *
4    * Red Hat licenses this file to you under the Apache License, version 2.0
5    * (the "License"); you may not use this file except in compliance with the
6    * 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  package org.jboss.netty.handler.codec.http;
17  
18  import java.io.UnsupportedEncodingException;
19  import java.net.URI;
20  import java.net.URLDecoder;
21  import java.nio.charset.Charset;
22  import java.nio.charset.UnsupportedCharsetException;
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.LinkedHashMap;
26  import java.util.List;
27  import java.util.Map;
28  
29  /**
30   * Splits an HTTP query string into a path string and key-value parameter pairs.
31   * This decoder is for one time use only.  Create a new instance for each URI:
32   * <pre>
33   * {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("/hello?recipient=world");
34   * assert decoder.getPath().equals("/hello");
35   * assert decoder.getParameters().get("recipient").equals("world");
36   * </pre>
37   *
38   * @author <a href="http://www.jboss.org/netty/">The Netty Project</a>
39   * @author Andy Taylor (andy.taylor@jboss.org)
40   * @author <a href="http://gleamynode.net/">Trustin Lee</a>
41   * @author <a href="http://tsunanet.net/">Benoit Sigoure</a>
42   * @version $Rev: 2302 $, $Date: 2010-06-14 20:07:44 +0900 (Mon, 14 Jun 2010) $
43   *
44   * @see QueryStringEncoder
45   *
46   * @apiviz.stereotype utility
47   * @apiviz.has        org.jboss.netty.handler.codec.http.HttpRequest oneway - - decodes
48   */
49  public class QueryStringDecoder {
50  
51      private final Charset charset;
52      private final String uri;
53      private String path;
54      private Map<String, List<String>> params;
55  
56      /**
57       * Creates a new decoder that decodes the specified URI. The decoder will
58       * assume that the query string is encoded in UTF-8.
59       */
60      public QueryStringDecoder(String uri) {
61          this(uri, HttpCodecUtil.DEFAULT_CHARSET);
62      }
63  
64      /**
65       * Creates a new decoder that decodes the specified URI encoded in the
66       * specified charset.
67       */
68      public QueryStringDecoder(String uri, Charset charset) {
69          if (uri == null) {
70              throw new NullPointerException("uri");
71          }
72          if (charset == null) {
73              throw new NullPointerException("charset");
74          }
75  
76          this.uri = uri;
77          this.charset = charset;
78      }
79  
80      /**
81       * @deprecated Use {@link #QueryStringDecoder(String, Charset)} instead.
82       */
83      @Deprecated
84      public QueryStringDecoder(String uri, String charset) {
85          this(uri, Charset.forName(charset));
86      }
87  
88      /**
89       * Creates a new decoder that decodes the specified URI. The decoder will
90       * assume that the query string is encoded in UTF-8.
91       */
92      public QueryStringDecoder(URI uri) {
93          this(uri, HttpCodecUtil.DEFAULT_CHARSET);
94      }
95  
96      /**
97       * Creates a new decoder that decodes the specified URI encoded in the
98       * specified charset.
99       */
100     public QueryStringDecoder(URI uri, Charset charset){
101         if (uri == null) {
102             throw new NullPointerException("uri");
103         }
104         if (charset == null) {
105             throw new NullPointerException("charset");
106         }
107 
108         this.uri = uri.toASCIIString();
109         this.charset = charset;
110     }
111 
112     /**
113      * @deprecated Use {@link #QueryStringDecoder(URI, Charset)} instead.
114      */
115     @Deprecated
116     public QueryStringDecoder(URI uri, String charset){
117         this(uri, Charset.forName(charset));
118     }
119 
120     /**
121      * Returns the decoded path string of the URI.
122      */
123     public String getPath() {
124         if (path == null) {
125             int pathEndPos = uri.indexOf('?');
126             if (pathEndPos < 0) {
127                 path = uri;
128             }
129             else {
130                 return path = uri.substring(0, pathEndPos);
131             }
132         }
133         return path;
134     }
135 
136     /**
137      * Returns the decoded key-value parameter pairs of the URI.
138      */
139     public Map<String, List<String>> getParameters() {
140         if (params == null) {
141             int pathLength = getPath().length();
142             if (uri.length() == pathLength) {
143                 return Collections.emptyMap();
144             }
145             params = decodeParams(uri.substring(pathLength + 1));
146         }
147         return params;
148     }
149 
150     private Map<String, List<String>> decodeParams(String s) {
151         Map<String, List<String>> params = new LinkedHashMap<String, List<String>>();
152         String name = null;
153         int pos = 0; // Beginning of the unprocessed region
154         int i;       // End of the unprocessed region
155         char c = 0;  // Current character
156         for (i = 0; i < s.length(); i++) {
157             c = s.charAt(i);
158             if (c == '=' && name == null) {
159                 if (pos != i) {
160                     name = decodeComponent(s.substring(pos, i), charset);
161                 }
162                 pos = i + 1;
163             } else if (c == '&') {
164                 if (name == null && pos != i) {
165                     // We haven't seen an `=' so far but moved forward.
166                     // Must be a param of the form '&a&' so add it with
167                     // an empty value.
168                     addParam(params, decodeComponent(s.substring(pos, i), charset), "");
169                 } else if (name != null) {
170                     addParam(params, name, decodeComponent(s.substring(pos, i), charset));
171                     name = null;
172                 }
173                 pos = i + 1;
174             }
175         }
176 
177         if (pos != i) {  // Are there characters we haven't dealt with?
178             if (name == null) {     // Yes and we haven't seen any `='.
179                 addParam(params, decodeComponent(s.substring(pos, i), charset), "");
180             } else {                // Yes and this must be the last value.
181                 addParam(params, name, decodeComponent(s.substring(pos, i), charset));
182             }
183         } else if (name != null) {  // Have we seen a name without value?
184             addParam(params, name, "");
185         }
186 
187         return params;
188     }
189 
190     private static String decodeComponent(String s, Charset charset) {
191         if (s == null) {
192             return "";
193         }
194 
195         try {
196             return URLDecoder.decode(s, charset.name());
197         } catch (UnsupportedEncodingException e) {
198             throw new UnsupportedCharsetException(charset.name());
199         }
200     }
201 
202     private static void addParam(Map<String, List<String>> params, String name, String value) {
203         List<String> values = params.get(name);
204         if (values == null) {
205             values = new ArrayList<String>(1);  // Often there's only 1 value.
206             params.put(name, values);
207         }
208         values.add(value);
209     }
210 }