1 /******************************************************************************
2 
3     URI query parameter parser
4 
5     - QueryParams splits an URI query parameter list into key/value pairs.
6     - QueryParamSet parses an URI query parameter list and memorizes the values
7       corresponding to keys in a list provided at instantiation.
8 
9     TODO: The QueryParams class may be moved to ocean.text.
10 
11     Copyright:
12         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
13         All rights reserved.
14 
15     License:
16         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
17         Alternatively, this file may be distributed under the terms of the Tango
18         3-Clause BSD License (see LICENSE_BSD.txt for details).
19 
20  ******************************************************************************/
21 
22 module ocean.net.util.QueryParams;
23 
24 
25 import ocean.meta.types.Qualifiers;
26 
27 import ocean.core.Verify;
28 
29 import ocean.net.util.ParamSet;
30 
31 import ocean.text.util.SplitIterator: ChrSplitIterator;
32 
33 import ocean.util.container.AppendBuffer: AppendBuffer, IAppendBufferReader;
34 
35 version (unittest) import ocean.core.Test;
36 
37 /******************************************************************************
38 
39     The QueryParams class is memory-friendly and therefore suitable for stack
40     allocated 'scope' instances.
41 
42  ******************************************************************************/
43 
44 class QueryParams
45 {
46     /**************************************************************************
47 
48         Delimiter of elements, where each element is a key/value pair, and
49         between key and value of an element.
50 
51         Treatment of special cases:
52 
53         - If an element does not contain a keyval_delim character, it is treated
54           as a key without a value; a null value is then reported.
55         - If an element contains more than one keyval_delim character, the first
56           occurrence is used as delimiter so that the value contains
57           keyval_delim characters but not the key.
58         - If the last character of an element is a keyval_delim character and
59           this is the only occurrence, the value is a non-null empty string.
60 
61         Must be specified in the constructor but may be modified at any time.
62 
63         Note that changing the delimiters during an iteration becomes effective
64         when the next iteration is started.
65 
66      **************************************************************************/
67 
68     public char element_delim, keyval_delim;
69 
70     /**************************************************************************
71 
72         Option to trim whitespace from keys and values, enabled by default.
73 
74         Note that changing this option during an iteration becomes effective
75         when the next iteration is started.
76 
77      **************************************************************************/
78 
79     public bool trim_whitespace = true;
80 
81     /**************************************************************************
82 
83         Current query string to parse and iterate over
84 
85      **************************************************************************/
86 
87     private cstring query;
88 
89     /**************************************************************************
90 
91         Debug flag to prevent calling any method during a 'foreach' iteration.
92 
93      **************************************************************************/
94 
95     private bool iterating = false;
96 
97     invariant ( )
98     {
99         assert (!this.iterating, typeof (this).stringof ~
100                                  " method called during 'foreach' iteration");
101     }
102 
103     /**************************************************************************
104 
105         Constructor
106 
107         Params:
108             element_delim = delimiter between elements
109             keyval_delim  = delimiter between key and value of an element
110 
111      **************************************************************************/
112 
113     public this ( char element_delim, char keyval_delim )
114     {
115         this.element_delim = element_delim;
116         this.keyval_delim = keyval_delim;
117     }
118 
119     /**************************************************************************
120 
121         Sets the URI query string to parse
122 
123         Params:
124             query = query string to parse
125 
126         Returns:
127             this instance
128 
129      **************************************************************************/
130 
131     public typeof (this) set ( cstring query )
132     {
133         this.query = query;
134 
135         return this;
136     }
137 
138     /**************************************************************************
139 
140         'foreach' iteration over the URI query parameter list items, each one
141         split into a key/value pair. key and value slice the string passed to
142         query() so DO NOT MODIFY THEM. (You may, however, modify their content;
143         this will modify the string passed to query() in-place.)
144 
145         Note that during iteration, no public method may be called nor may a
146         nested iteration be started.
147 
148      **************************************************************************/
149 
150     public int opApply ( scope int delegate ( ref cstring key, ref cstring value ) ext_dg )
151     {
152         this.iterating = true;
153 
154         scope (exit) this.iterating = false;
155 
156         scope trim_dg = (ref cstring key, ref cstring value)
157         {
158             auto tkey = ChrSplitIterator.trim(key),
159                  tval = ChrSplitIterator.trim(value);
160             return ext_dg(tkey, tval);
161         };
162 
163         scope dg = this.trim_whitespace ? trim_dg : ext_dg;
164 
165         scope split_paramlist = new ChrSplitIterator(this.element_delim),
166               split_param     = new ChrSplitIterator(this.keyval_delim);
167 
168         split_paramlist.collapse = true;
169 
170         split_param.include_remaining = false;
171 
172         split_paramlist.reset(this.query);
173 
174         return split_paramlist.opApply((ref cstring param)
175         {
176             cstring value = null;
177 
178             foreach (key; split_param.reset(param))
179             {
180                 value = split_param.remaining;
181 
182                 return dg(key, value);
183             }
184 
185             verify(!split_param.n);
186 
187             return dg(param, value);
188         });
189     }
190 }
191 
192 /******************************************************************************/
193 
194 class QueryParamSet: ParamSet
195 {
196     /**************************************************************************
197 
198         Delimiter of elements, where each element is a key/value pair, and
199         between key and value of an element.
200 
201         Treatment of special cases:
202 
203         - If an element does not contain a keyval_delim character, it is treated
204           as a key without a value; a null value is then reported.
205         - If an element contains more than one keyval_delim character, the first
206           occurrence is used as delimiter so that the value contains
207           keyval_delim characters but not the key.
208         - If the last character of an element is a keyval_delim character and
209           this is the only occurrence, the value is a non-null empty string.
210 
211         Must be specified in the constructor but may be modified at any time.
212 
213      **************************************************************************/
214 
215     public char element_delim, keyval_delim;
216 
217     /**************************************************************************
218 
219         Constructor
220 
221         Params:
222             element_delim = delimiter between elements
223             keyval_delim  = delimiter between key and value of an element
224             keys          = parameter keys of interest (case-insensitive)
225 
226      **************************************************************************/
227 
228     public this ( char element_delim, char keyval_delim, in istring[] keys ... )
229     {
230         this.addKeys(keys);
231 
232         this.rehash();
233 
234         this.element_delim = element_delim;
235         this.keyval_delim   = keyval_delim;
236     }
237 
238     /**************************************************************************
239 
240         Parses query and memorizes the values corresponding to the keys provided
241         to the constructor. query will be sliced.
242 
243         Params:
244             query = query string to parse
245 
246      **************************************************************************/
247 
248     public void parse ( cstring query )
249     {
250         this.reset();
251 
252         scope query_params = new QueryParams(this.element_delim, this.keyval_delim);
253 
254         foreach (key, val; query_params.set(query))
255         {
256             this.set(key, val);
257         }
258     }
259 }
260 
261 unittest
262 {
263     scope qp = new QueryParams(';', '=');
264 
265     test (qp.trim_whitespace);
266 
267     {
268         uint i = 0;
269 
270         foreach (key, val; qp.set(" Die Katze = tritt ;\n\tdie= Treppe;krumm.= "))
271         {
272             switch (i++)
273             {
274                 case 0:
275                     test (key == "Die Katze");
276                     test (val == "tritt");
277                     break;
278                 case 1:
279                     test (key == "die");
280                     test (val == "Treppe");
281                     break;
282                 case 2:
283                     test (key == "krumm.");
284                     test (!val.length);
285                     break;
286                 default:
287                     test(0);
288             }
289         }
290     }
291 
292     {
293         qp.trim_whitespace = false;
294 
295         uint i = 0;
296 
297         foreach (key, val; qp.set(" Die Katze = tritt ;\n\tdie= Treppe;krumm.= "))
298         {
299             switch (i++)
300             {
301                 case 0:
302                     test (key == " Die Katze ");
303                     test (val == " tritt ");
304                     break;
305                 case 1:
306                     test (key == "\n\tdie");
307                     test (val == " Treppe");
308                     break;
309                 case 2:
310                     test (key == "krumm.");
311                     test (val == " ");
312                     break;
313                 default:
314                     test(0);
315             }
316         }
317     }
318 }