001: /* Encodes.java
002:
003: {{IS_NOTE
004: Purpose:
005:
006: Description:
007:
008: History:
009: Fri Jun 21 14:13:50 2002, Created by tomyeh
010: }}IS_NOTE
011:
012: Copyright (C) 2002 Potix Corporation. All Rights Reserved.
013:
014: {{IS_RIGHT
015: This program is distributed under GPL Version 2.0 in the hope that
016: it will be useful, but WITHOUT ANY WARRANTY.
017: }}IS_RIGHT
018: */
019: package org.zkoss.web.servlet.http;
020:
021: import java.util.Arrays;
022: import java.util.Map;
023: import java.util.Set;
024: import java.util.HashSet;
025: import java.util.Locale;
026: import java.util.Iterator;
027: import java.io.UnsupportedEncodingException;
028:
029: import javax.servlet.ServletRequest;
030: import javax.servlet.ServletResponse;
031: import javax.servlet.ServletException;
032: import javax.servlet.http.HttpServletRequest;
033: import javax.servlet.http.HttpServletResponse;
034: import javax.servlet.ServletContext;
035:
036: import org.zkoss.lang.D;
037: import org.zkoss.lang.Objects;
038: import org.zkoss.util.logging.Log;
039:
040: import org.zkoss.web.Attributes;
041: import org.zkoss.web.servlet.Servlets;
042: import org.zkoss.web.servlet.Charsets;
043: import org.zkoss.web.util.resource.ExtendletContext;
044:
045: /**
046: * Encoding utilities for servlets.
047: *
048: * @author tomyeh
049: * @see Https
050: */
051: public class Encodes {
052: private static final Log log = Log.lookup(Encodes.class);
053:
054: protected Encodes() {
055: } //prevent from instantiation
056:
057: /** The URL encoder. */
058: private static URLEncoder _urlEncoder;
059:
060: /** Encodes a string to HTTP URI compliant by use of
061: * {@link Charsets#getURICharset}.
062: *
063: * <p>Besides two-byte characters, it also encodes any character found
064: * in unsafes.
065: *
066: * @param unsafes the set of characters that must be encoded; never null.
067: * It must be sorted.
068: */
069: private static final String encodeURI0(String s, char[] unsafes)
070: throws UnsupportedEncodingException {
071: if (s == null)
072: return null;
073:
074: final String charset = Charsets.getURICharset();
075: final byte[] in = s.getBytes(charset);
076: final byte[] out = new byte[in.length * 3];//at most: %xx
077: int j = 0, k = 0;
078: for (; j < in.length; ++j) {
079: //Though it is ok to use '+' for ' ', Jetty has problem to
080: //handle space between chinese characters.
081: final char cc = (char) (((int) in[j]) & 0xff);
082: if (cc >= 0x80 || cc <= ' '
083: || Arrays.binarySearch(unsafes, cc) >= 0) {
084: out[k++] = (byte) '%';
085: String cvt = Integer.toHexString(cc);
086: if (cvt.length() == 1) {
087: out[k++] = (byte) '0';
088: out[k++] = (byte) cvt.charAt(0);
089: } else {
090: out[k++] = (byte) cvt.charAt(0);
091: out[k++] = (byte) cvt.charAt(1);
092: }
093: } else {
094: out[k++] = in[j];
095: }
096: }
097: return j == k ? s : new String(out, 0, k, charset);
098: }
099:
100: /** unsafe character when that are used in url's localtion. */
101: private static final char[] URI_UNSAFE;
102: /** unsafe character when that are used in url's query. */
103: private static final char[] URI_COMP_UNSAFE;
104: static {
105: URI_UNSAFE = "`%^{}[]\\\"<>|".toCharArray();
106: Arrays.sort(URI_UNSAFE);
107:
108: URI_COMP_UNSAFE = "`%^{}[]\\\"<>|$&,/:;=?".toCharArray();
109: Arrays.sort(URI_COMP_UNSAFE);
110: }
111:
112: /** Does the HTTP encoding for the URI location.
113: * For example, '%' is translated to '%25'.
114: *
115: * <p>Since {@link #encodeURL(ServletContext, ServletRequest, ServletResponse, String)}
116: * will invoke this method automatically, you rarely need this method.
117: *
118: * @param s the string to encode; null is OK
119: * @return the encoded string or null if s is null
120: * @see #encodeURIComponent
121: */
122: public static final String encodeURI(String s)
123: throws UnsupportedEncodingException {
124: return encodeURI0(s, URI_UNSAFE);
125: }
126:
127: /** Does the HTTP encoding for an URI query parameter.
128: * For example, '/' is translated to '%2F'.
129: * Both name and value must be encoded seperately. Example,
130: * <code>encodeURIComponent(name) + '=' + encodeURIComponent(value)</code>.
131: *
132: * <p>Since {@link #encodeURL(ServletContext, ServletRequest, ServletResponse, String)}
133: * will <i>not</i> invoke this method automatically, you'd better
134: * to encode each query parameter by this method or
135: * {@link #addToQueryString(StringBuffer,Map)}.
136: *
137: * @param s the string to encode; null is OK
138: * @return the encoded string or null if s is null
139: * @see #addToQueryString(StringBuffer,String,Object)
140: * @see #encodeURI
141: */
142: public static final String encodeURIComponent(String s)
143: throws UnsupportedEncodingException {
144: return encodeURI0(s, URI_COMP_UNSAFE);
145: }
146:
147: /**
148: /** Appends a map of parameters (name=value) to a query string.
149: * It returns the query string preceding with '?' if any parameter exists,
150: * or an empty string if no parameter at all. Thus, you could do
151: *
152: * <p><code>request.getRequestDispatcher(<br>
153: * addToQueryStirng(new StringBuffer(uri), params).toString());</code>
154: *
155: * <p>Since RequestDispatcher.include and forward do not allow wrapping
156: * the request and response -- see spec and the implementation of both
157: * Jetty and Catalina, we have to use this method to pass the parameters.
158: *
159: * @param params a map of parameters; format: (String, Object) or
160: * (String, Object[]); null is OK
161: */
162: public static final StringBuffer addToQueryString(StringBuffer sb,
163: Map params) throws UnsupportedEncodingException {
164: if (params != null) {
165: for (Iterator it = params.entrySet().iterator(); it
166: .hasNext();) {
167: final Map.Entry me = (Map.Entry) it.next();
168: addToQueryString(sb, (String) me.getKey(), me
169: .getValue());
170: }
171: }
172: return sb;
173: }
174:
175: /** Appends a parameter (name=value) to a query string.
176: * This method automatically detects whether other query is already
177: * appended. If so, & is used instead of ?.
178: *
179: * <p>The query string might contain servlet path and other parts.
180: * This method starts the searching from the first '?'.
181: * If the query string doesn't contain '?', it is assumed to be a string
182: * without query's name/value pairs.
183: *
184: * @param value the value. If it is null, only name is appened.
185: * If it is an array of objects, multipe pairs of name=value[j] will
186: * be appended.
187: */
188: public static final StringBuffer addToQueryString(StringBuffer sb,
189: String name, Object value)
190: throws UnsupportedEncodingException {
191: if (value instanceof Object[]) {
192: final Object[] vals = (Object[]) value;
193: if (vals.length == 0) {
194: value = null; //only append name
195: } else {
196: for (int j = 0; j < vals.length; ++j)
197: addToQueryString(sb, name, vals[j]);
198: return sb; //done
199: }
200: }
201:
202: sb.append(next(sb, '?', 0) >= sb.length() ? '?' : '&');
203: sb.append(encodeURIComponent(name)).append('=');
204: //NOTE: jetty with jboss3.0.6 ignore parameters without '=',
205: //so we always append '=' even value is null
206: if (value != null)
207: sb.append(encodeURIComponent(Objects.toString(value)));
208:
209: return sb;
210: }
211:
212: /** Returns the first occurrence starting from j, or sb.length().
213: */
214: private static final int next(StringBuffer sb, char cc, int j) {
215: for (final int len = sb.length(); j < len; ++j)
216: if (sb.charAt(j) == cc)
217: break;
218: return j;
219: }
220:
221: /**
222: * Sets the parameter (name=value) to a query string.
223: * If the name already exists in the query string, it will be removed first.
224: * If your name has appeared in the string, it will replace
225: * your new value to the query string.
226: * Otherwise, it will append the name/value to the new string.
227: *
228: * <p>The query string might contain servlet path and other parts.
229: * This method starts the searching from the first '?'.
230: * If the query string doesn't contain '?', it is assumed to be a string
231: * without query's name/value pairs.
232: *
233: * @param str The query string like xxx?xxx=xxx&xxx=xxx or null.
234: * @param name The get parameter name.
235: * @param value The value associated with the get parameter name.
236: * @return The new or result query string with your name/value.
237: * @see #addToQueryString
238: */
239: public static final String setToQueryString(String str,
240: String name, Object value)
241: throws UnsupportedEncodingException {
242: final StringBuffer sb = new StringBuffer();
243: if (str != null)
244: sb.append(str);
245: return setToQueryString(sb, name, value).toString();
246: }
247:
248: /**
249: * Sets the parameter (name=value) to a query string.
250: * If the name already exists in the query string, it will be
251: * removed first.
252: * @see #addToQueryString
253: */
254: public static final StringBuffer setToQueryString(StringBuffer sb,
255: String name, Object value)
256: throws UnsupportedEncodingException {
257: removeFromQueryString(sb, name);
258: return addToQueryString(sb, name, value);
259: }
260:
261: /**
262: * Sets a map of parameters (name=value) to a query string.
263: * If the name already exists in the query string, it will be removed first.
264: * @see #addToQueryString
265: */
266: public static final String setToQueryString(String str, Map params)
267: throws UnsupportedEncodingException {
268: return setToQueryString(new StringBuffer(str), params)
269: .toString();
270: }
271:
272: /**
273: * Sets a map of parameters (name=value) to a query string.
274: * If the name already exists in the query string, it will be removed first.
275: * @see #addToQueryString
276: */
277: public static final StringBuffer setToQueryString(StringBuffer sb,
278: Map params) throws UnsupportedEncodingException {
279: if (params != null) {
280: for (Iterator it = params.entrySet().iterator(); it
281: .hasNext();) {
282: final Map.Entry me = (Map.Entry) it.next();
283: setToQueryString(sb, (String) me.getKey(), me
284: .getValue());
285: }
286: }
287: return sb;
288: }
289:
290: /** Tests whether a parameter exists in the query string.
291: */
292: public static final boolean containsQuery(String str, String name) {
293: int j = str.indexOf(name);
294: if (j <= 0)
295: return false;
296:
297: char cc = str.charAt(j - 1);
298: if (cc != '?' && cc != '&')
299: return false;
300:
301: j += name.length();
302: if (j >= str.length())
303: return true;
304: cc = str.charAt(j);
305: return cc == '=' || cc == '&';
306: }
307:
308: /** Remove all name/value pairs of the specified name from a string.
309: *
310: * <p>The query string might contain servlet path and other parts.
311: * This method starts the searching from the last '?'.
312: * If the query string doesn't contain '?', it is assumed to be a string
313: * without query's name/value pairs.
314: * @see #addToQueryString
315: */
316: public static final String removeFromQueryString(String str,
317: String name) throws UnsupportedEncodingException {
318: if (str == null)
319: return null;
320:
321: int j = str.indexOf('?');
322: if (j < 0)
323: return str;
324:
325: final StringBuffer sb = new StringBuffer(str);
326: removeFromQueryString(sb, name);
327: return sb.length() == str.length() ? str : sb.toString();
328: }
329:
330: /** Remove all name/value pairs of the specified name from a string.
331: * @see #addToQueryString
332: */
333: public static final StringBuffer removeFromQueryString(
334: StringBuffer sb, String name)
335: throws UnsupportedEncodingException {
336: name = encodeURIComponent(name);
337: int j = sb.indexOf("?");
338: if (j < 0)
339: return sb; //no '?'
340:
341: j = sb.indexOf(name, j + 1);
342: if (j < 0)
343: return sb; //no name
344:
345: final int len = name.length();
346: do {
347: //1. make sure left is & or ?
348: int k = j + len;
349: char cc = sb.charAt(j - 1);
350: if (cc != '&' && cc != '?') {
351: j = k;
352: continue;
353: }
354:
355: //2. make sure right is = or & or end-of-string
356: if (k < sb.length()) {
357: cc = sb.charAt(k);
358: if (cc != '=' && cc != '&') {
359: j = k;
360: continue;
361: }
362: }
363:
364: //3. remove it until next & or end-of-string
365: k = next(sb, '&', k);
366: if (k < sb.length())
367: sb.delete(j, k + 1); //also remove '&'
368: else
369: sb.delete(j - 1, k); //also preceding '?' or '&'
370: } while ((j = sb.indexOf(name, j)) > 0);
371: return sb;
372: }
373:
374: /** Encodes an URL.
375: * It resolves "*" contained in URI, if any, to the proper Locale,
376: * and the browser code.
377: * Refer to {@link Servlets#locate(ServletContext, ServletRequest, String, Locator)}
378: * for details.
379: *
380: * <p>In additions, if uri starts with "/", the context path, e.g.,
381: * /zkdemo, is prefixed.
382: * In other words, "/ab/cd" means it is relevant to the servlet
383: * context path (say, "/zkdemo").
384: *
385: * <p>If uri starts with "~abc/", it means it is relevant to a foreign
386: * Web context called /abc. And, it will be converted to "/abc/" first
387: * (without prefix request.getContextPath()).
388: *
389: * <p>Finally, the uri is encoded by HttpServletResponse.encodeURL.
390: *
391: * <p>This method invokes {@link #encodeURI} for any characters
392: * before '?'. However, it does NOT encode any character after '?'. Thus,
393: * you might hvae to invoke
394: * {@link #encodeURIComponent} or {@link #addToQueryString(StringBuffer,Map)}
395: * to encode the query parameters.
396: *
397: * <h3>The URL Prefix and Encoder</h3>
398: *
399: * <p>In a sophisticated environment, e.g.,
400: * <a href="http://en.wikipedia.org/wiki/Reverse_proxy">Reverse Proxy</a>,
401: * the encoded URL might have to be prefixed with some special prefix.
402: * To do that, you can implement {@link URLEncoder}, and then
403: * specify it with {@link #setURLEncoder}.
404: * When {@link #encodeURL} encodes an URL, it will check
405: * any URL encoder is defined (by {@link #setURLEncoder}.
406: * If any, it will invoke {@link URLEncoder#encodeURL} with
407: * the encoded URL to give it the last chance to post-process it, such
408: * as inserting a special prefix.
409: *
410: * @param request the request; never null
411: * @param response the response; never null
412: * @param uri it must be null, empty or starts with "/". It might contain
413: * "*" for current browser code and Locale.
414: * @param ctx the servlet context; used only if "*" is contained in uri
415: * @exception IndexOutOfBoundException if uri is empty
416: * @see org.zkoss.web.servlet.Servlets#locate
417: * @see org.zkoss.web.servlet.Servlets#generateURI
418: */
419: public static final String encodeURL(ServletContext ctx,
420: ServletRequest request, ServletResponse response, String uri)
421: throws ServletException {
422: try {
423: final String url = encodeURL0(ctx, request, response, uri);
424: return _urlEncoder != null ? _urlEncoder.encodeURL(ctx,
425: request, response, url) : url;
426: } catch (Exception ex) {
427: log.realCause(ex);
428: throw new ServletException("Unable to encode " + uri, ex);
429: }
430: }
431:
432: private static final String encodeURL0(ServletContext ctx,
433: ServletRequest request, ServletResponse response, String uri)
434: throws Exception {
435: if (uri == null || uri.length() == 0)
436: return uri; //keep as it is
437:
438: boolean ctxpathSpecified = false;
439: if (uri.charAt(0) != '/') { //NOT relative to context path
440: if (Servlets.isUniversalURL(uri))
441: return uri; //nothing to do
442:
443: if (uri.charAt(0) == '~') { //foreign context
444: final String ctxroot;
445: if (uri.length() == 1) {
446: ctxroot = uri = "/";
447: } else if (uri.charAt(1) == '/') {
448: ctxroot = "/";
449: uri = uri.substring(1);
450: } else {
451: uri = '/' + uri.substring(1);
452: final int j = uri.indexOf('/', 1);
453: ctxroot = j >= 0 ? uri.substring(0, j) : uri;
454: }
455:
456: final ExtendletContext extctx = Servlets
457: .getExtendletContext(ctx, ctxroot.substring(1));
458: if (extctx != null) {
459: final int j = uri.indexOf('/', 1);
460: return extctx.encodeURL(request, response,
461: j >= 0 ? uri.substring(j) : "/");
462: }
463:
464: final ServletContext newctx = ctx.getContext(ctxroot);
465: if (newctx != null) {
466: ctx = newctx;
467: } else if (D.ON && log.debugable()) {
468: log.debug("Context not found: " + ctxroot);
469: }
470: ctxpathSpecified = true;
471: } else if (Https.isIncluded(request)
472: || Https.isForwarded(request)) {
473: //if reletive URI and being included/forwarded,
474: //converts to absolute
475: String pgpath = Https.getThisServletPath(request);
476: if (pgpath != null) {
477: int j = pgpath.lastIndexOf('/');
478: if (j >= 0) {
479: uri = pgpath.substring(0, j + 1) + uri;
480: } else {
481: log
482: .warning("The current page doesn't contain '/':"
483: + pgpath);
484: }
485: }
486: }
487: }
488:
489: //locate by locale and browser if necessary
490: uri = Servlets.locate(ctx, request, uri, null);
491:
492: //prefix context path
493: if (!ctxpathSpecified && uri.charAt(0) == '/'
494: && (request instanceof HttpServletRequest)) {
495: //Work around with a bug when we wrap Pluto's RenderRequest (1.0.1)
496: String ctxpath = ((HttpServletRequest) request)
497: .getContextPath();
498: if (ctxpath.length() > 0 && ctxpath.charAt(0) != '/')
499: ctxpath = '/' + ctxpath;
500: uri = ctxpath + uri;
501: }
502:
503: int j = uri.indexOf('?');
504: if (j < 0) {
505: uri = encodeURI(uri);
506: } else {
507: uri = encodeURI(uri.substring(0, j)) + uri.substring(j);
508: }
509: //encode
510: if (response instanceof HttpServletResponse)
511: uri = ((HttpServletResponse) response).encodeURL(uri);
512: return uri;
513: }
514:
515: /** Sets the URI encoder.
516: *
517: * <p>Default: null
518: *
519: * <p>The URI encoder is used to post process the encoded URL
520: * returned by {@link #encodeURL}.
521: *
522: * <p>When {@link #encodeURL} encodes an URL, it will check
523: * any URL encoder is defined (by {@link #setURLEncoder}.
524: * If any, it will invoke {@link URLEncoder#encodeURL} with
525: * the encoded URL to give it the last chance to 'manipulate' it.
526: *
527: * @since 3.0.1
528: * @see #encodeURL
529: */
530: public static void setURLEncoder(URLEncoder encoder) {
531: _urlEncoder = encoder;
532: }
533:
534: /** Returns the URI encoder, or null if no uri encoder.
535: * @since 3.0.1
536: * @see #setURLEncoder
537: * @see #encodeURL
538: */
539: public static URLEncoder getURLEncoder() {
540: return _urlEncoder;
541: }
542:
543: /** The URL encoder used to post-process the encoded URL of
544: * {@link Encodes#encodeURL} before returning.
545: *
546: * <p>When {@link Encodes#encodeURL} encodes an URL, it will check
547: * any URL encoder is defined (by {@link #setURLEncoder}.
548: * If any, it will invoke {@link #encodeURL} with
549: * the encoded URL to give it the last chance to 'manipulate' it.
550: *
551: * @since 3.0.1
552: * @see #setURLEncoder
553: * @see Encodes#encodeURL
554: */
555: public static interface URLEncoder {
556: /** Returns the encoded URL.
557: * @param uri it must be null, empty, starts with "/", or
558: * starts with "xxx:" (e.g., "http://", "javascript:"
559: */
560: public String encodeURL(ServletContext ctx,
561: ServletRequest request, ServletResponse response,
562: String uri);
563: }
564: }
|