001: // ========================================================================
002: // Copyright 2004-2005 Mort Bay Consulting Pty. Ltd.
003: // ------------------------------------------------------------------------
004: // Licensed under the Apache License, Version 2.0 (the "License");
005: // you may not use this file except in compliance with the License.
006: // You may obtain a copy of the License at
007: // http://www.apache.org/licenses/LICENSE-2.0
008: // Unless required by applicable law or agreed to in writing, software
009: // distributed under the License is distributed on an "AS IS" BASIS,
010: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011: // See the License for the specific language governing permissions and
012: // limitations under the License.
013: // ========================================================================
014:
015: package org.mortbay.util;
016:
017: import java.io.IOException;
018: import java.io.InputStream;
019: import java.io.UnsupportedEncodingException;
020: import java.util.Iterator;
021: import java.util.Map;
022:
023: import org.mortbay.log.Log;
024:
025: /* ------------------------------------------------------------ */
026: /** Handles coding of MIME "x-www-form-urlencoded".
027: * This class handles the encoding and decoding for either
028: * the query string of a URL or the _content of a POST HTTP request.
029: *
030: * <p><h4>Notes</h4>
031: * The hashtable either contains String single values, vectors
032: * of String or arrays of Strings.
033: *
034: * This class is only partially synchronised. In particular, simple
035: * get operations are not protected from concurrent updates.
036: *
037: * @see java.net.URLEncoder
038: * @author Greg Wilkins (gregw)
039: */
040: public class UrlEncoded extends MultiMap {
041:
042: /* ----------------------------------------------------------------- */
043: public UrlEncoded(UrlEncoded url) {
044: super (url);
045: }
046:
047: /* ----------------------------------------------------------------- */
048: public UrlEncoded() {
049: super (6);
050: }
051:
052: /* ----------------------------------------------------------------- */
053: public UrlEncoded(String s) {
054: super (6);
055: decode(s, StringUtil.__UTF8);
056: }
057:
058: /* ----------------------------------------------------------------- */
059: public UrlEncoded(String s, String charset) {
060: super (6);
061: decode(s, charset);
062: }
063:
064: /* ----------------------------------------------------------------- */
065: public void decode(String query) {
066: decodeTo(query, this , StringUtil.__UTF8);
067: }
068:
069: /* ----------------------------------------------------------------- */
070: public void decode(String query, String charset) {
071: decodeTo(query, this , charset);
072: }
073:
074: /* -------------------------------------------------------------- */
075: /** Encode Hashtable with % encoding.
076: */
077: public String encode() {
078: return encode(StringUtil.__UTF8, false);
079: }
080:
081: /* -------------------------------------------------------------- */
082: /** Encode Hashtable with % encoding.
083: */
084: public String encode(String charset) {
085: return encode(charset, false);
086: }
087:
088: /* -------------------------------------------------------------- */
089: /** Encode Hashtable with % encoding.
090: * @param equalsForNullValue if True, then an '=' is always used, even
091: * for parameters without a value. e.g. "blah?a=&b=&c=".
092: */
093: public synchronized String encode(String charset,
094: boolean equalsForNullValue) {
095: return encode(this , charset, equalsForNullValue);
096: }
097:
098: /* -------------------------------------------------------------- */
099: /** Encode Hashtable with % encoding.
100: * @param equalsForNullValue if True, then an '=' is always used, even
101: * for parameters without a value. e.g. "blah?a=&b=&c=".
102: */
103: public static String encode(MultiMap map, String charset,
104: boolean equalsForNullValue) {
105: if (charset == null)
106: charset = StringUtil.__UTF8;
107:
108: StringBuffer result = new StringBuffer(128);
109: synchronized (result) {
110: Iterator iter = map.entrySet().iterator();
111: while (iter.hasNext()) {
112: Map.Entry entry = (Map.Entry) iter.next();
113:
114: String key = entry.getKey().toString();
115: Object list = entry.getValue();
116: int s = LazyList.size(list);
117:
118: if (s == 0) {
119: result.append(encodeString(key, charset));
120: if (equalsForNullValue)
121: result.append('=');
122: } else {
123: for (int i = 0; i < s; i++) {
124: if (i > 0)
125: result.append('&');
126: Object val = LazyList.get(list, i);
127: result.append(encodeString(key, charset));
128:
129: if (val != null) {
130: String str = val.toString();
131: if (str.length() > 0) {
132: result.append('=');
133: result
134: .append(encodeString(str,
135: charset));
136: } else if (equalsForNullValue)
137: result.append('=');
138: } else if (equalsForNullValue)
139: result.append('=');
140: }
141: }
142: if (iter.hasNext())
143: result.append('&');
144: }
145: return result.toString();
146: }
147: }
148:
149: /* -------------------------------------------------------------- */
150: /** Decoded parameters to Map.
151: * @param content the string containing the encoded parameters
152: */
153: public static void decodeTo(String content, MultiMap map,
154: String charset) {
155: if (charset == null)
156: charset = StringUtil.__UTF8;
157:
158: synchronized (map) {
159: String key = null;
160: String value = null;
161: int mark = -1;
162: boolean encoded = false;
163: for (int i = 0; i < content.length(); i++) {
164: char c = content.charAt(i);
165: switch (c) {
166: case '&':
167: int l = i - mark - 1;
168: value = l == 0 ? "" : (encoded ? decodeString(
169: content, mark + 1, l, charset) : content
170: .substring(mark + 1, i));
171: mark = i;
172: encoded = false;
173: if (key != null) {
174: map.add(key, value);
175: } else if (value != null && value.length() > 0) {
176: map.add(value, "");
177: }
178: key = null;
179: value = null;
180: break;
181: case '=':
182: if (key != null)
183: break;
184: key = encoded ? decodeString(content, mark + 1, i
185: - mark - 1, charset) : content.substring(
186: mark + 1, i);
187: mark = i;
188: encoded = false;
189: break;
190: case '+':
191: encoded = true;
192: break;
193: case '%':
194: encoded = true;
195: break;
196: }
197: }
198:
199: if (key != null) {
200: int l = content.length() - mark - 1;
201: value = l == 0 ? "" : (encoded ? decodeString(content,
202: mark + 1, l, charset) : content
203: .substring(mark + 1));
204: map.add(key, value);
205: } else if (mark < content.length()) {
206: key = encoded ? decodeString(content, mark + 1, content
207: .length()
208: - mark - 1, charset) : content
209: .substring(mark + 1);
210: map.add(key, "");
211: }
212: }
213: }
214:
215: /* -------------------------------------------------------------- */
216: /** Decoded parameters to Map.
217: * @param data the byte[] containing the encoded parameters
218: */
219: public static void decodeUtf8To(byte[] raw, int offset, int length,
220: MultiMap map) {
221: synchronized (map) {
222: Utf8StringBuffer buffer = new Utf8StringBuffer();
223: String key = null;
224: String value = null;
225:
226: // TODO cache of parameter names ???
227: int end = offset + length;
228: for (int i = offset; i < end; i++) {
229: byte b = raw[i];
230: switch ((char) (0xff & b)) {
231: case '&':
232: value = buffer.length() == 0 ? "" : buffer
233: .toString();
234: buffer.reset();
235: if (key != null) {
236: map.add(key, value);
237: } else if (value != null && value.length() > 0) {
238: map.add(value, "");
239: }
240: key = null;
241: value = null;
242: break;
243:
244: case '=':
245: if (key != null) {
246: buffer.append(b);
247: break;
248: }
249: key = buffer.toString();
250: buffer.reset();
251: break;
252:
253: case '+':
254: buffer.append((byte) ' ');
255: break;
256:
257: case '%':
258: if (i + 2 < end)
259: buffer
260: .append((byte) ((TypeUtil
261: .convertHexDigit(raw[++i]) << 4) + TypeUtil
262: .convertHexDigit(raw[++i])));
263: break;
264: default:
265: buffer.append(b);
266: break;
267: }
268: }
269:
270: if (key != null) {
271: value = buffer.length() == 0 ? "" : buffer.toString();
272: buffer.reset();
273: map.add(key, value);
274: } else if (buffer.length() > 0) {
275: map.add(buffer.toString(), "");
276: }
277: }
278: }
279:
280: /* -------------------------------------------------------------- */
281: /** Decoded parameters to Map.
282: * @param in InputSteam to read
283: * @param map MultiMap to add parameters to
284: * @param maxLength maximum length of conent to read 0r -1 for no limit
285: */
286: public static void decodeUtf8To(InputStream in, MultiMap map,
287: int maxLength) throws IOException {
288: synchronized (map) {
289: Utf8StringBuffer buffer = new Utf8StringBuffer();
290: String key = null;
291: String value = null;
292:
293: int b;
294:
295: // TODO cache of parameter names ???
296: int totalLength = 0;
297: while ((b = in.read()) >= 0) {
298: switch ((char) b) {
299: case '&':
300: value = buffer.length() == 0 ? "" : buffer
301: .toString();
302: buffer.reset();
303: if (key != null) {
304: map.add(key, value);
305: } else if (value != null && value.length() > 0) {
306: map.add(value, "");
307: }
308: key = null;
309: value = null;
310: break;
311:
312: case '=':
313: if (key != null) {
314: buffer.append((byte) b);
315: break;
316: }
317: key = buffer.toString();
318: buffer.reset();
319: break;
320:
321: case '+':
322: buffer.append((byte) ' ');
323: break;
324:
325: case '%':
326: int dh = in.read();
327: int dl = in.read();
328: if (dh < 0 || dl < 0)
329: break;
330: buffer
331: .append((byte) ((TypeUtil
332: .convertHexDigit((byte) dh) << 4) + TypeUtil
333: .convertHexDigit((byte) dl)));
334: break;
335: default:
336: buffer.append((byte) b);
337: break;
338: }
339: if (maxLength >= 0 && (++totalLength > maxLength))
340: throw new IllegalStateException("Form too large");
341: }
342:
343: if (key != null) {
344: value = buffer.length() == 0 ? "" : buffer.toString();
345: buffer.reset();
346: map.add(key, value);
347: } else if (buffer.length() > 0) {
348: map.add(buffer.toString(), "");
349: }
350: }
351: }
352:
353: /* -------------------------------------------------------------- */
354: /** Decoded parameters to Map.
355: * @param in the stream containing the encoded parameters
356: */
357: public static void decodeTo(InputStream in, MultiMap map,
358: String charset, int maxLength) throws IOException {
359: if (charset == null
360: || StringUtil.__UTF8.equalsIgnoreCase(charset)) {
361: decodeUtf8To(in, map, maxLength);
362: return;
363: }
364:
365: synchronized (map) {
366: ByteArrayOutputStream2 buf = new ByteArrayOutputStream2(256);
367: String key = null;
368: String value = null;
369:
370: int c;
371: int digit = 0;
372: int digits = 0;
373:
374: // TODO cache of parameter names ???
375: byte[] bytes = new byte[256]; // TODO Configure ?? size??? tune??? // reuse???
376: int l = -1;
377: int totalLength = 0;
378: while ((l = in.read(bytes)) >= 0) {
379: for (int i = 0; i < l; i++) {
380: c = bytes[i];
381: switch ((char) c) {
382: case '&':
383: value = buf.size() == 0 ? "" : new String(buf
384: .getBuf(), 0, buf.size(), charset);
385: buf.reset();
386: if (key != null) {
387: map.add(key, value);
388: } else if (value != null && value.length() > 0) {
389: map.add(value, "");
390: }
391: key = null;
392: value = null;
393: break;
394: case '=':
395: if (key != null) {
396: buf.write(c);
397: break;
398: }
399: key = new String(buf.getBuf(), 0, buf.size(),
400: charset);
401: buf.reset();
402: break;
403: case '+':
404: buf.write(' ');
405: break;
406: case '%':
407: digits = 2;
408: break;
409: default:
410: if (digits == 2) {
411: digit = TypeUtil.convertHexDigit((byte) c);
412: digits = 1;
413: } else if (digits == 1) {
414: buf.write((digit << 4)
415: + TypeUtil
416: .convertHexDigit((byte) c));
417: digits = 0;
418: } else
419: buf.write(c);
420: break;
421: }
422: }
423:
424: totalLength += l;
425: if (maxLength >= 0 && totalLength > maxLength)
426: throw new IllegalStateException("Form too large");
427: }
428:
429: if (key != null) {
430: value = buf.size() == 0 ? "" : new String(buf.getBuf(),
431: 0, buf.size(), charset);
432: buf.reset();
433: map.add(key, value);
434: } else if (buf.size() > 0) {
435: map
436: .add(new String(buf.getBuf(), 0, buf.size(),
437: charset), "");
438: }
439:
440: }
441: }
442:
443: /* -------------------------------------------------------------- */
444: /** Decode String with % encoding.
445: * This method makes the assumption that the majority of calls
446: * will need no decoding.
447: */
448: public static String decodeString(String encoded, int offset,
449: int length, String charset) {
450: if (charset == null)
451: charset = StringUtil.__UTF8;
452: byte[] bytes = null;
453: int n = 0;
454:
455: for (int i = 0; i < length; i++) {
456: char c = encoded.charAt(offset + i);
457: if (c < 0 || c > 0xff)
458: throw new IllegalArgumentException("Not encoded");
459:
460: if (c == '+') {
461: if (bytes == null) {
462: bytes = new byte[length * 2];
463: encoded.getBytes(offset, offset + i, bytes, 0);
464: n = i;
465: }
466: bytes[n++] = (byte) ' ';
467: } else if (c == '%' && (i + 2) < length) {
468: byte b;
469: char cn = encoded.charAt(offset + i + 1);
470: if (cn >= 'a' && cn <= 'z')
471: b = (byte) (10 + cn - 'a');
472: else if (cn >= 'A' && cn <= 'Z')
473: b = (byte) (10 + cn - 'A');
474: else
475: b = (byte) (cn - '0');
476: cn = encoded.charAt(offset + i + 2);
477: if (cn >= 'a' && cn <= 'z')
478: b = (byte) (b * 16 + 10 + cn - 'a');
479: else if (cn >= 'A' && cn <= 'Z')
480: b = (byte) (b * 16 + 10 + cn - 'A');
481: else
482: b = (byte) (b * 16 + cn - '0');
483:
484: if (bytes == null) {
485: bytes = new byte[length];
486: encoded.getBytes(offset, offset + i, bytes, 0);
487: n = i;
488: }
489: i += 2;
490: bytes[n++] = b;
491: } else if (n > 0)
492: bytes[n++] = (byte) c;
493: }
494:
495: if (bytes == null) {
496: if (offset == 0 && encoded.length() == length)
497: return encoded;
498: return encoded.substring(offset, offset + length);
499: }
500:
501: try {
502: return new String(bytes, 0, n, charset);
503: } catch (UnsupportedEncodingException e) {
504: Log.warn(e.toString());
505: Log.debug(e);
506: return new String(bytes, 0, n);
507: }
508:
509: }
510:
511: /* ------------------------------------------------------------ */
512: /** Perform URL encoding.
513: * Assumes 8859 charset
514: * @param string
515: * @return encoded string.
516: */
517: public static String encodeString(String string) {
518: return encodeString(string, StringUtil.__UTF8);
519: }
520:
521: /* ------------------------------------------------------------ */
522: /** Perform URL encoding.
523: * @param string
524: * @return encoded string.
525: */
526: public static String encodeString(String string, String charset) {
527: if (charset == null)
528: charset = StringUtil.__UTF8;
529: byte[] bytes = null;
530: try {
531: bytes = string.getBytes(charset);
532: } catch (UnsupportedEncodingException e) {
533: // Log.warn(LogSupport.EXCEPTION,e);
534: bytes = string.getBytes();
535: }
536:
537: int len = bytes.length;
538: byte[] encoded = new byte[bytes.length * 3];
539: int n = 0;
540: boolean noEncode = true;
541:
542: for (int i = 0; i < len; i++) {
543: byte b = bytes[i];
544:
545: if (b == ' ') {
546: noEncode = false;
547: encoded[n++] = (byte) '+';
548: } else if (b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z'
549: || b >= '0' && b <= '9') {
550: encoded[n++] = b;
551: } else {
552: noEncode = false;
553: encoded[n++] = (byte) '%';
554: byte nibble = (byte) ((b & 0xf0) >> 4);
555: if (nibble >= 10)
556: encoded[n++] = (byte) ('A' + nibble - 10);
557: else
558: encoded[n++] = (byte) ('0' + nibble);
559: nibble = (byte) (b & 0xf);
560: if (nibble >= 10)
561: encoded[n++] = (byte) ('A' + nibble - 10);
562: else
563: encoded[n++] = (byte) ('0' + nibble);
564: }
565: }
566:
567: if (noEncode)
568: return string;
569:
570: try {
571: return new String(encoded, 0, n, charset);
572: } catch (UnsupportedEncodingException e) {
573: // Log.warn(LogSupport.EXCEPTION,e);
574: return new String(encoded, 0, n);
575: }
576: }
577:
578: /* ------------------------------------------------------------ */
579: /**
580: */
581: public Object clone() {
582: return new UrlEncoded(this);
583: }
584: }
|