1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.tomcat.util.http;
19
20 import java.io.IOException;
21 import java.util.Enumeration;
22 import java.util.Hashtable;
23
24 import org.apache.tomcat.util.buf.ByteChunk;
25 import org.apache.tomcat.util.buf.CharChunk;
26 import org.apache.tomcat.util.buf.MessageBytes;
27 import org.apache.tomcat.util.buf.UDecoder;
28 import org.apache.tomcat.util.collections.MultiMap;
29
30 /**
31 *
32 * @author Costin Manolache
33 */
34 public final class Parameters extends MultiMap {
35
36
37 private static org.apache.juli.logging.Log log=
38 org.apache.juli.logging.LogFactory.getLog(Parameters.class );
39
40 // Transition: we'll use the same Hashtable( String->String[] )
41 // for the beginning. When we are sure all accesses happen through
42 // this class - we can switch to MultiMap
43 private Hashtable<String,String[]> paramHashStringArray =
44 new Hashtable<String,String[]>();
45 private boolean didQueryParameters=false;
46 private boolean didMerge=false;
47
48 MessageBytes queryMB;
49 MimeHeaders headers;
50
51 UDecoder urlDec;
52 MessageBytes decodedQuery=MessageBytes.newInstance();
53
54 public static final int INITIAL_SIZE=4;
55
56 // Garbage-less parameter merging.
57 // In a sub-request with parameters, the new parameters
58 // will be stored in child. When a getParameter happens,
59 // the 2 are merged togheter. The child will be altered
60 // to contain the merged values - the parent is allways the
61 // original request.
62 private Parameters child=null;
63 private Parameters parent=null;
64 private Parameters currentChild=null;
65
66 String encoding=null;
67 String queryStringEncoding=null;
68
69 /**
70 *
71 */
72 public Parameters() {
73 super( INITIAL_SIZE );
74 }
75
76 public void setQuery( MessageBytes queryMB ) {
77 this.queryMB=queryMB;
78 }
79
80 public void setHeaders( MimeHeaders headers ) {
81 this.headers=headers;
82 }
83
84 public void setEncoding( String s ) {
85 encoding=s;
86 if(debug>0) log( "Set encoding to " + s );
87 }
88
89 public void setQueryStringEncoding( String s ) {
90 queryStringEncoding=s;
91 if(debug>0) log( "Set query string encoding to " + s );
92 }
93
94 public void recycle() {
95 super.recycle();
96 paramHashStringArray.clear();
97 didQueryParameters=false;
98 currentChild=null;
99 didMerge=false;
100 encoding=null;
101 decodedQuery.recycle();
102 }
103
104 // -------------------- Sub-request support --------------------
105
106 public Parameters getCurrentSet() {
107 if( currentChild==null )
108 return this;
109 return currentChild;
110 }
111
112 /** Create ( or reuse ) a child that will be used during a sub-request.
113 All future changes ( setting query string, adding parameters )
114 will affect the child ( the parent request is never changed ).
115 Both setters and getters will return the data from the deepest
116 child, merged with data from parents.
117 */
118 public void push() {
119 // We maintain a linked list, that will grow to the size of the
120 // longest include chain.
121 // The list has 2 points of interest:
122 // - request.parameters() is the original request and head,
123 // - request.parameters().currentChild() is the current set.
124 // The ->child and parent<- links are preserved ( currentChild is not
125 // the last in the list )
126
127 // create a new element in the linked list
128 // note that we reuse the child, if any - pop will not
129 // set child to null !
130 if( currentChild==null ) {
131 currentChild=new Parameters();
132 currentChild.setURLDecoder( urlDec );
133 currentChild.parent=this;
134 return;
135 }
136 if( currentChild.child==null ) {
137 currentChild.child=new Parameters();
138 currentChild.setURLDecoder( urlDec );
139 currentChild.child.parent=currentChild;
140 } // it is not null if this object already had a child
141 // i.e. a deeper include() ( we keep it )
142
143 // the head will be the new element.
144 currentChild=currentChild.child;
145 currentChild.setEncoding( encoding );
146 }
147
148 /** Discard the last child. This happens when we return from a
149 sub-request and the parameters are locally modified.
150 */
151 public void pop() {
152 if( currentChild==null ) {
153 throw new RuntimeException( "Attempt to pop without a push" );
154 }
155 currentChild.recycle();
156 currentChild=currentChild.parent;
157 // don't remove the top.
158 }
159
160 // -------------------- Data access --------------------
161 // Access to the current name/values, no side effect ( processing ).
162 // You must explicitely call handleQueryParameters and the post methods.
163
164 // This is the original data representation ( hash of String->String[])
165
166 public void addParameterValues( String key, String[] newValues) {
167 if ( key==null ) return;
168 String values[];
169 if (paramHashStringArray.containsKey(key)) {
170 String oldValues[] = (String[])paramHashStringArray.get(key);
171 values = new String[oldValues.length + newValues.length];
172 for (int i = 0; i < oldValues.length; i++) {
173 values[i] = oldValues[i];
174 }
175 for (int i = 0; i < newValues.length; i++) {
176 values[i+ oldValues.length] = newValues[i];
177 }
178 } else {
179 values = newValues;
180 }
181
182 paramHashStringArray.put(key, values);
183 }
184
185 public String[] getParameterValues(String name) {
186 handleQueryParameters();
187 // sub-request
188 if( currentChild!=null ) {
189 currentChild.merge();
190 return (String[])currentChild.paramHashStringArray.get(name);
191 }
192
193 // no "facade"
194 String values[]=(String[])paramHashStringArray.get(name);
195 return values;
196 }
197
198 public Enumeration getParameterNames() {
199 handleQueryParameters();
200 // Slow - the original code
201 if( currentChild!=null ) {
202 currentChild.merge();
203 return currentChild.paramHashStringArray.keys();
204 }
205
206 // merge in child
207 return paramHashStringArray.keys();
208 }
209
210 /** Combine the parameters from parent with our local ones
211 */
212 private void merge() {
213 // recursive
214 if( debug > 0 ) {
215 log("Before merging " + this + " " + parent + " " + didMerge );
216 log( paramsAsString());
217 }
218 // Local parameters first - they take precedence as in spec.
219 handleQueryParameters();
220
221 // we already merged with the parent
222 if( didMerge ) return;
223
224 // we are the top level
225 if( parent==null ) return;
226
227 // Add the parent props to the child ( lower precedence )
228 parent.merge();
229 Hashtable<String,String[]> parentProps=parent.paramHashStringArray;
230 merge2( paramHashStringArray , parentProps);
231 didMerge=true;
232 if(debug > 0 )
233 log("After " + paramsAsString());
234 }
235
236
237 // Shortcut.
238 public String getParameter(String name ) {
239 String[] values = getParameterValues(name);
240 if (values != null) {
241 if( values.length==0 ) return "";
242 return values[0];
243 } else {
244 return null;
245 }
246 }
247 // -------------------- Processing --------------------
248 /** Process the query string into parameters
249 */
250 public void handleQueryParameters() {
251 if( didQueryParameters ) return;
252
253 didQueryParameters=true;
254
255 if( queryMB==null || queryMB.isNull() )
256 return;
257
258 if( debug > 0 )
259 log( "Decoding query " + decodedQuery + " " + queryStringEncoding);
260
261 try {
262 decodedQuery.duplicate( queryMB );
263 } catch (IOException e) {
264 // Can't happen, as decodedQuery can't overflow
265 e.printStackTrace();
266 }
267 processParameters( decodedQuery, queryStringEncoding );
268 }
269
270 // --------------------
271
272 /** Combine 2 hashtables into a new one.
273 * ( two will be added to one ).
274 * Used to combine child parameters ( RequestDispatcher's query )
275 * with parent parameters ( original query or parent dispatcher )
276 */
277 private static void merge2(Hashtable<String,String[]> one,
278 Hashtable<String,String[]> two ) {
279 Enumeration e = two.keys();
280
281 while (e.hasMoreElements()) {
282 String name = (String) e.nextElement();
283 String[] oneValue = one.get(name);
284 String[] twoValue = two.get(name);
285 String[] combinedValue;
286
287 if (twoValue == null) {
288 continue;
289 } else {
290 if( oneValue==null ) {
291 combinedValue = new String[twoValue.length];
292 System.arraycopy(twoValue, 0, combinedValue,
293 0, twoValue.length);
294 } else {
295 combinedValue = new String[oneValue.length +
296 twoValue.length];
297 System.arraycopy(oneValue, 0, combinedValue, 0,
298 oneValue.length);
299 System.arraycopy(twoValue, 0, combinedValue,
300 oneValue.length, twoValue.length);
301 }
302 one.put(name, combinedValue);
303 }
304 }
305 }
306
307 // incredibly inefficient data representation for parameters,
308 // until we test the new one
309 private void addParam( String key, String value ) {
310 if( key==null ) return;
311 String values[];
312 if (paramHashStringArray.containsKey(key)) {
313 String oldValues[] = (String[])paramHashStringArray.
314 get(key);
315 values = new String[oldValues.length + 1];
316 for (int i = 0; i < oldValues.length; i++) {
317 values[i] = oldValues[i];
318 }
319 values[oldValues.length] = value;
320 } else {
321 values = new String[1];
322 values[0] = value;
323 }
324
325
326 paramHashStringArray.put(key, values);
327 }
328
329 public void setURLDecoder( UDecoder u ) {
330 urlDec=u;
331 }
332
333 // -------------------- Parameter parsing --------------------
334
335 // This code is not used right now - it's the optimized version
336 // of the above.
337
338 // we are called from a single thread - we can do it the hard way
339 // if needed
340 ByteChunk tmpName=new ByteChunk();
341 ByteChunk tmpValue=new ByteChunk();
342 CharChunk tmpNameC=new CharChunk(1024);
343 CharChunk tmpValueC=new CharChunk(1024);
344
345 public void processParameters( byte bytes[], int start, int len ) {
346 processParameters(bytes, start, len, encoding);
347 }
348
349 public void processParameters( byte bytes[], int start, int len,
350 String enc ) {
351 int end=start+len;
352 int pos=start;
353
354 if( debug>0 )
355 log( "Bytes: " + new String( bytes, start, len ));
356
357 do {
358 boolean noEq=false;
359 int valStart=-1;
360 int valEnd=-1;
361
362 int nameStart=pos;
363 int nameEnd=ByteChunk.indexOf(bytes, nameStart, end, '=' );
364 // Workaround for a&b&c encoding
365 int nameEnd2=ByteChunk.indexOf(bytes, nameStart, end, '&' );
366 if( (nameEnd2!=-1 ) &&
367 ( nameEnd==-1 || nameEnd > nameEnd2) ) {
368 nameEnd=nameEnd2;
369 noEq=true;
370 valStart=nameEnd;
371 valEnd=nameEnd;
372 if( debug>0) log("no equal " + nameStart + " " + nameEnd + " " + new String(bytes, nameStart, nameEnd-nameStart) );
373 }
374 if( nameEnd== -1 )
375 nameEnd=end;
376
377 if( ! noEq ) {
378 valStart= (nameEnd < end) ? nameEnd+1 : end;
379 valEnd=ByteChunk.indexOf(bytes, valStart, end, '&');
380 if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart;
381 }
382
383 pos=valEnd+1;
384
385 if( nameEnd<=nameStart ) {
386 log.warn("Parameters: Invalid chunk ignored.");
387 continue;
388 // invalid chunk - it's better to ignore
389 }
390 tmpName.setBytes( bytes, nameStart, nameEnd-nameStart );
391 tmpValue.setBytes( bytes, valStart, valEnd-valStart );
392
393 try {
394 addParam( urlDecode(tmpName, enc), urlDecode(tmpValue, enc) );
395 } catch (IOException e) {
396 // Exception during character decoding: skip parameter
397 log.warn("Parameters: Character decoding failed. " +
398 "Parameter skipped.", e);
399 }
400
401 tmpName.recycle();
402 tmpValue.recycle();
403
404 } while( pos<end );
405 }
406
407 private String urlDecode(ByteChunk bc, String enc)
408 throws IOException {
409 if( urlDec==null ) {
410 urlDec=new UDecoder();
411 }
412 urlDec.convert(bc);
413 String result = null;
414 if (enc != null) {
415 bc.setEncoding(enc);
416 result = bc.toString();
417 } else {
418 CharChunk cc = tmpNameC;
419 int length = bc.getLength();
420 cc.allocate(length, -1);
421 // Default encoding: fast conversion
422 byte[] bbuf = bc.getBuffer();
423 char[] cbuf = cc.getBuffer();
424 int start = bc.getStart();
425 for (int i = 0; i < length; i++) {
426 cbuf[i] = (char) (bbuf[i + start] & 0xff);
427 }
428 cc.setChars(cbuf, 0, length);
429 result = cc.toString();
430 cc.recycle();
431 }
432 return result;
433 }
434
435 public void processParameters( char chars[], int start, int len ) {
436 int end=start+len;
437 int pos=start;
438
439 if( debug>0 )
440 log( "Chars: " + new String( chars, start, len ));
441 do {
442 boolean noEq=false;
443 int nameStart=pos;
444 int valStart=-1;
445 int valEnd=-1;
446
447 int nameEnd=CharChunk.indexOf(chars, nameStart, end, '=' );
448 int nameEnd2=CharChunk.indexOf(chars, nameStart, end, '&' );
449 if( (nameEnd2!=-1 ) &&
450 ( nameEnd==-1 || nameEnd > nameEnd2) ) {
451 nameEnd=nameEnd2;
452 noEq=true;
453 valStart=nameEnd;
454 valEnd=nameEnd;
455 if( debug>0) log("no equal " + nameStart + " " + nameEnd + " " + new String(chars, nameStart, nameEnd-nameStart) );
456 }
457 if( nameEnd== -1 ) nameEnd=end;
458
459 if( ! noEq ) {
460 valStart= (nameEnd < end) ? nameEnd+1 : end;
461 valEnd=CharChunk.indexOf(chars, valStart, end, '&');
462 if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart;
463 }
464
465 pos=valEnd+1;
466
467 if( nameEnd<=nameStart ) {
468 continue;
469 // invalid chunk - no name, it's better to ignore
470 // XXX log it ?
471 }
472
473 try {
474 tmpNameC.append( chars, nameStart, nameEnd-nameStart );
475 tmpValueC.append( chars, valStart, valEnd-valStart );
476
477 if( debug > 0 )
478 log( tmpNameC + "= " + tmpValueC);
479
480 if( urlDec==null ) {
481 urlDec=new UDecoder();
482 }
483
484 urlDec.convert( tmpNameC );
485 urlDec.convert( tmpValueC );
486
487 if( debug > 0 )
488 log( tmpNameC + "= " + tmpValueC);
489
490 addParam( tmpNameC.toString(), tmpValueC.toString() );
491 } catch( IOException ex ) {
492 ex.printStackTrace();
493 }
494
495 tmpNameC.recycle();
496 tmpValueC.recycle();
497
498 } while( pos<end );
499 }
500
501 public void processParameters( MessageBytes data ) {
502 processParameters(data, encoding);
503 }
504
505 public void processParameters( MessageBytes data, String encoding ) {
506 if( data==null || data.isNull() || data.getLength() <= 0 ) return;
507
508 if( data.getType() == MessageBytes.T_BYTES ) {
509 ByteChunk bc=data.getByteChunk();
510 processParameters( bc.getBytes(), bc.getOffset(),
511 bc.getLength(), encoding);
512 } else {
513 if (data.getType()!= MessageBytes.T_CHARS )
514 data.toChars();
515 CharChunk cc=data.getCharChunk();
516 processParameters( cc.getChars(), cc.getOffset(),
517 cc.getLength());
518 }
519 }
520
521 /** Debug purpose
522 */
523 public String paramsAsString() {
524 StringBuffer sb=new StringBuffer();
525 Enumeration en= paramHashStringArray.keys();
526 while( en.hasMoreElements() ) {
527 String k=(String)en.nextElement();
528 sb.append( k ).append("=");
529 String v[]=(String[])paramHashStringArray.get( k );
530 for( int i=0; i<v.length; i++ )
531 sb.append( v[i] ).append(",");
532 sb.append("\n");
533 }
534 return sb.toString();
535 }
536
537 private static int debug=0;
538 private void log(String s ) {
539 if (log.isDebugEnabled())
540 log.debug("Parameters: " + s );
541 }
542
543 // -------------------- Old code, needs rewrite --------------------
544
545 /** Used by RequestDispatcher
546 */
547 public void processParameters( String str ) {
548 int end=str.length();
549 int pos=0;
550 if( debug > 0)
551 log("String: " + str );
552
553 do {
554 boolean noEq=false;
555 int valStart=-1;
556 int valEnd=-1;
557
558 int nameStart=pos;
559 int nameEnd=str.indexOf('=', nameStart );
560 int nameEnd2=str.indexOf('&', nameStart );
561 if( nameEnd2== -1 ) nameEnd2=end;
562 if( (nameEnd2!=-1 ) &&
563 ( nameEnd==-1 || nameEnd > nameEnd2) ) {
564 nameEnd=nameEnd2;
565 noEq=true;
566 valStart=nameEnd;
567 valEnd=nameEnd;
568 if( debug>0) log("no equal " + nameStart + " " + nameEnd + " " + str.substring(nameStart, nameEnd) );
569 }
570
571 if( nameEnd== -1 ) nameEnd=end;
572
573 if( ! noEq ) {
574 valStart=nameEnd+1;
575 valEnd=str.indexOf('&', valStart);
576 if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart;
577 }
578
579 pos=valEnd+1;
580
581 if( nameEnd<=nameStart ) {
582 continue;
583 }
584 if( debug>0)
585 log( "XXX " + nameStart + " " + nameEnd + " "
586 + valStart + " " + valEnd );
587
588 try {
589 tmpNameC.append(str, nameStart, nameEnd-nameStart );
590 tmpValueC.append(str, valStart, valEnd-valStart );
591
592 if( debug > 0 )
593 log( tmpNameC + "= " + tmpValueC);
594
595 if( urlDec==null ) {
596 urlDec=new UDecoder();
597 }
598
599 urlDec.convert( tmpNameC );
600 urlDec.convert( tmpValueC );
601
602 if( debug > 0 )
603 log( tmpNameC + "= " + tmpValueC);
604
605 addParam( tmpNameC.toString(), tmpValueC.toString() );
606 } catch( IOException ex ) {
607 ex.printStackTrace();
608 }
609
610 tmpNameC.recycle();
611 tmpValueC.recycle();
612
613 } while( pos<end );
614 }
615
616
617 }