001: /* Servlets.java
002:
003: {{IS_NOTE
004: Purpose:
005: Description:
006: History:
007: 90/12/10 22:24:28, Create, Tom M. Yeh.
008: }}IS_NOTE
009:
010: Copyright (C) 2001 Potix Corporation. All Rights Reserved.
011:
012: {{IS_RIGHT
013: This program is distributed under GPL Version 2.0 in the hope that
014: it will be useful, but WITHOUT ANY WARRANTY.
015: }}IS_RIGHT
016: */
017: package org.zkoss.web.servlet;
018:
019: import java.util.List;
020: import java.util.LinkedList;
021: import java.util.ArrayList;
022: import java.util.Iterator;
023: import java.util.Map;
024: import java.util.HashMap;
025: import java.util.Collections;
026: import java.util.Locale;
027: import java.util.Properties;
028: import java.io.InputStream;
029: import java.io.IOException;
030: import java.io.StringWriter;
031: import java.io.PrintWriter;
032: import java.io.UnsupportedEncodingException;
033: import java.net.URL;
034: import java.net.MalformedURLException;
035:
036: import javax.servlet.ServletContext;
037: import javax.servlet.ServletRequest;
038: import javax.servlet.ServletResponse;
039: import javax.servlet.ServletException;
040: import javax.servlet.RequestDispatcher;
041: import javax.servlet.http.HttpSession;
042: import javax.servlet.http.HttpServletRequest;
043:
044: import org.zkoss.mesg.MCommon;
045: import org.zkoss.lang.D;
046: import org.zkoss.lang.Objects;
047: import org.zkoss.lang.Classes;
048: import org.zkoss.lang.Exceptions;
049: import org.zkoss.lang.SystemException;
050: import org.zkoss.util.Checksums;
051: import org.zkoss.util.CacheMap;
052: import org.zkoss.util.CollectionsX;
053: import org.zkoss.util.Locales;
054: import org.zkoss.util.logging.Log;
055: import org.zkoss.util.resource.Locator;
056: import org.zkoss.util.resource.Locators;
057: import org.zkoss.util.resource.PropertyBundle;
058: import org.zkoss.idom.Element;
059: import org.zkoss.idom.input.SAXBuilder;
060:
061: import org.zkoss.web.Attributes;
062: import org.zkoss.web.servlet.http.Encodes;
063: import org.zkoss.web.util.resource.ExtendletContext;
064: import org.zkoss.web.util.resource.ServletContextLocator;
065:
066: /**
067: * The servlet relevant utilities.
068: *
069: * @author tomyeh
070: * @see org.zkoss.web.servlet.http.Https
071: * @see org.zkoss.web.servlet.Charsets
072: */
073: public class Servlets {
074: // private static final Log log = Log.lookup(Servlets.class);
075:
076: private static final boolean _svl24, _svl23;
077: static {
078: boolean b = false;
079: try {
080: ServletResponse.class.getMethod("getContentType",
081: new Class[0]);
082: b = true;
083: } catch (Throwable ex) {
084: }
085: _svl24 = b;
086:
087: if (!b) {
088: try {
089: HttpSession.class.getMethod("getServletContext",
090: new Class[0]);
091: b = true;
092: } catch (Throwable ex) {
093: }
094: }
095: _svl23 = b;
096: }
097:
098: /** Utilities; no instantiation required. */
099: protected Servlets() {
100: }
101:
102: /** Returns whether a URL starts with xxx://, mailto:, about:,
103: * javascript:
104: */
105: public static final boolean isUniversalURL(String uri) {
106: if (uri == null || uri.length() == 0)
107: return false;
108:
109: final char cc = uri.charAt(0);
110: return cc >= 'a'
111: && cc <= 'z'
112: && (uri.indexOf("://") > 0 || uri.startsWith("mailto:")
113: || uri.startsWith("javascript:") || uri
114: .startsWith("about:"));
115: }
116:
117: /** Returns whether the current Web server supports Servlet 2.4 or above.
118: *
119: * @since 3.0.0
120: */
121: public static final boolean isServlet24() {
122: return _svl24;
123: }
124:
125: /** Returns whether the current Web server supports Servlet 2.3 or above.
126: * Thus, if {@link #isServlet24} returns true, {@link #isServlet23}
127: * must return true, too.
128: *
129: * @since 3.0.0
130: */
131: public static final boolean isServlet23() {
132: return _svl23;
133: }
134:
135: //-- resource locator --//
136: /** Locates a page based on the specified Locale. It never returns null.
137: *
138: * <p>If an URI contains "*", it will be replaced with a proper Locale.
139: * For example, if the current Locale is zh_TW and the resource is
140: * named "ab*.cd", then it searches "ab_zh_TW.cd", "ab_zh.cd" and
141: * then "ab.cd", until any of them is found.
142: *
143: * <blockquote>Note: "*" must be right before ".", or the last character.
144: * For example, "ab*.cd" and "ab*" are both correct, while
145: * "ab*cd" and "ab*\/cd" are ignored.</blockquote>
146: *
147: * <p>If an URI contains two "*", the first "*" will be replaced with
148: * a browser code and the second with a proper locale.
149: * The browser code depends on what browser
150: * the user are used to visit the web site.
151: * Currently, the code for Internet Explorer is "ie", Safari is "saf",
152: * Opera is "opr" and all others are "moz".
153: * Thus, in the above example, if the resource is named "ab**.cd"
154: * and Firefox is used, then it searches "abmoz_zh_TW.cd", "abmoz_zh.cd"
155: * and then "abmoz.cd", until any of them is found.
156: *
157: * <p>Note: it assumes the path as name_lang_cn_var.ext where
158: * ".ext" is optional. Example, my_zh_tw.html.jsp.
159: *
160: * <p>Note: unlike {@link Encodes#encodeURL(ServletContext, ServletRequest, ServletResponse, String)},
161: * it always locates the Locale, without handling "*".
162: *
163: * @param ctx the servlet context to locate pages
164: * @param pgpath the page path excluding servlet name. It is OK to have
165: * the query string. It might contain "*" for current browser code and Locale.
166: * @param locator the locator used to locate resource. If null, ctx
167: * is assumed.
168: * @return pgpath if the original one matches; others if locale matches;
169: * never null
170: */
171: public static final String locate(ServletContext ctx,
172: ServletRequest request, String pgpath, Locator locator)
173: throws ServletException {
174: if (isUniversalURL(pgpath))
175: return pgpath;
176:
177: final int jquest = pgpath.indexOf('?');
178: final int f = pgpath.indexOf('*');
179: if (f < 0 || (jquest >= 0 && f > jquest))
180: return pgpath;
181: //optimize the case that no "*" at all
182:
183: final String qstr;
184: if (jquest >= 0) {
185: qstr = pgpath.substring(jquest);
186: pgpath = pgpath.substring(0, jquest);
187: } else {
188: qstr = null;
189: }
190:
191: //by browser?
192: int l = pgpath.lastIndexOf('*');
193: if (l > f) { //two '*'
194: final String bc = Servlets.isExplorer(request) ? "ie"
195: : Servlets.isSafari(request) ? "saf" : Servlets
196: .isOpera(request) ? "opr" : "moz";
197: l += bc.length() - 1;
198: pgpath = pgpath.substring(0, f) + bc
199: + pgpath.substring(f + 1);
200: }
201:
202: //remove "*"
203: pgpath = pgpath.substring(0, l) + pgpath.substring(l + 1); //remove
204:
205: //by locale? 1) before the first dot, 2) the last char if no dot
206: boolean byLocale = l == pgpath.length()
207: || (pgpath.charAt(l) == '.' && pgpath.indexOf('/',
208: l + 1) < 0);
209: if (byLocale) {
210: //make sure no dot before it
211: for (int j = l; --j >= 0;) {
212: final char cc = pgpath.charAt(j);
213: if (cc == '.') {
214: byLocale = false;
215: break;
216: } else if (cc == '/') {
217: break;
218: }
219: }
220: }
221: if (!byLocale)
222: return qstr != null ? pgpath + qstr : pgpath; //not by locale
223:
224: final String PGPATH_CACHE = "s_pgpath_cache";
225: Map map = (Map) ctx.getAttribute(PGPATH_CACHE);
226: if (map == null) {
227: map = Collections.synchronizedMap( //10 min
228: new CacheMap(500, 10 * 60 * 1000));
229: ctx.setAttribute(PGPATH_CACHE, map);
230: }
231:
232: final Locale locale = Locales.getCurrent();
233: final URIIndex index = new URIIndex(pgpath, locale);
234:
235: String uri = (String) map.get(index);
236: if (uri == null) {
237: final Locators.URLLocation loc = Locators.locate(pgpath,
238: locale, locator != null ? locator
239: : new ServletContextLocator(ctx));
240: uri = loc != null ? loc.file : pgpath;
241: map.put(index, uri);
242: }
243:
244: return qstr != null ? uri + qstr : uri;
245: }
246:
247: private static class URIIndex {
248: private final String _uri;
249: private final Locale _locale;
250:
251: private URIIndex(String uri, Locale locale) {
252: if (uri == null || locale == null)
253: throw new IllegalArgumentException("null");
254: _uri = uri;
255: _locale = locale;
256: }
257:
258: public int hashCode() {
259: return _uri.hashCode();
260: }
261:
262: public boolean equals(Object o) {
263: //To speed up, don't check whether o is the right class
264: final URIIndex idx = (URIIndex) o;
265: return _uri.equals(idx._uri) && _locale.equals(idx._locale);
266: }
267: }
268:
269: /** Returns whether the client is a robot (such as Web crawlers).
270: *
271: * <p>Because there are too many robots, it returns true if the user-agent
272: * is not recognized.
273: */
274: public static final boolean isRobot(ServletRequest req) {
275: String agt = req instanceof HttpServletRequest ? ((HttpServletRequest) req)
276: .getHeader("user-agent")
277: : null;
278: if (agt == null)
279: return false;
280:
281: agt = agt.toLowerCase();
282: return agt.indexOf("msie") < 0 && agt.indexOf("opera") < 0
283: && agt.indexOf("gecko/") < 0
284: && agt.indexOf("safari") < 0;
285: }
286:
287: /** Returns whether the browser is Internet Explorer.
288: * If true, it also implies {@link #isExplorer7} is true.
289: */
290: public static final boolean isExplorer(ServletRequest req) {
291: String agt = req instanceof HttpServletRequest ? ((HttpServletRequest) req)
292: .getHeader("user-agent")
293: : null;
294: if (agt == null)
295: return false;
296:
297: agt = agt.toLowerCase();
298: return agt.indexOf("msie") >= 0 && agt.indexOf("opera") < 0;
299: }
300:
301: /** Returns whether the browser is Explorer 7 or later.
302: */
303: public static final boolean isExplorer7(ServletRequest req) {
304: String agt = req instanceof HttpServletRequest ? ((HttpServletRequest) req)
305: .getHeader("user-agent")
306: : null;
307: if (agt == null)
308: return false;
309:
310: agt = agt.toLowerCase();
311: return agt.indexOf("msie 7") >= 0;
312: }
313:
314: /** Returns whether the browser is Gecko based, such as Mozilla, Firefox and Camino
315: */
316: public static final boolean isGecko(ServletRequest req) {
317: String agt = req instanceof HttpServletRequest ? ((HttpServletRequest) req)
318: .getHeader("user-agent")
319: : null;
320: if (agt == null)
321: return false;
322:
323: agt = agt.toLowerCase();
324: return agt.indexOf("gecko/") >= 0 && agt.indexOf("safari") < 0;
325: }
326:
327: /** Returns whether the browser is Safari.
328: */
329: public static final boolean isSafari(ServletRequest req) {
330: String agt = req instanceof HttpServletRequest ? ((HttpServletRequest) req)
331: .getHeader("user-agent")
332: : null;
333: if (agt == null)
334: return false;
335:
336: agt = agt.toLowerCase();
337: return agt.indexOf("safari") >= 0;
338: }
339:
340: /** Returns whether the browser is Opera.
341: */
342: public static final boolean isOpera(ServletRequest req) {
343: String agt = req instanceof HttpServletRequest ? ((HttpServletRequest) req)
344: .getHeader("user-agent")
345: : null;
346: if (agt == null)
347: return false;
348:
349: agt = agt.toLowerCase();
350: return agt.indexOf("opera") >= 0;
351: }
352:
353: /** Returns whether the client is a mobile device supporting MIL
354: * (Mobile Interactive Language).
355: * @since 2.4.0
356: */
357: public static final boolean isMilDevice(ServletRequest req) {
358: String agt = req instanceof HttpServletRequest ? ((HttpServletRequest) req)
359: .getHeader("user-agent")
360: : null;
361: if (agt == null)
362: return false;
363:
364: //ZK Mobile/1.0 (RMIL)
365: agt = agt.toLowerCase();
366: return agt.indexOf("zk") >= 0 && agt.indexOf("rmil") >= 0;
367: }
368:
369: /** Returns whether the client is a mobile device supporting HIL
370: * (Handset Interactive Language).
371: *
372: * <p>Note: ZK Mobile for Android supports both MIL and HIL.
373: * That is, both {@link #isHilDevice} and {@link #isMilDevice}
374: * return true.
375: *
376: * @since 3.0.2
377: */
378: public static final boolean isHilDevice(ServletRequest req) {
379: String agt = req instanceof HttpServletRequest ? ((HttpServletRequest) req)
380: .getHeader("user-agent")
381: : null;
382: if (agt == null)
383: return false;
384:
385: //ZK Mobile for Android 1.0 (RMIL; RHIL)
386: agt = agt.toLowerCase();
387: return agt.indexOf("zk") >= 0 && agt.indexOf("rhil") >= 0;
388: }
389:
390: /** Returns the user-agent header, which indicates what the client is,
391: * or an empty string if not available.
392: *
393: * <p>Note: it doesn't return null, so it is easy to test what
394: * the client is with {@link String#indexOf}.
395: *
396: * @since 3.0.2
397: */
398: public static final String getUserAgent(ServletRequest req) {
399: if (req instanceof HttpServletRequest) {
400: final String s = ((HttpServletRequest) req)
401: .getHeader("user-agent");
402: if (s != null)
403: return s;
404: }
405: return "";
406: }
407:
408: /**
409: * Tests whether this page is included by another page.
410: */
411: public static final boolean isIncluded(ServletRequest request) {
412: return request.getAttribute(Attributes.INCLUDE_CONTEXT_PATH) != null;
413: }
414:
415: /**
416: * Tests whether this page is forwarded by another page.
417: */
418: public static final boolean isForwarded(ServletRequest request) {
419: return request.getAttribute(Attributes.FORWARD_CONTEXT_PATH) != null;
420: }
421:
422: /**
423: * Forward to the specified URI.
424: * It enhances RequestDispatcher in the following ways.
425: *
426: * <ul>
427: * <li>It handles "~ctx"" where ctx is the context path of the
428: * foreign context. It is called foriegn URI.</li>
429: * <li>It detects whether the page calling this method
430: * is included by another servlet/page. If so, it uses
431: * RequestDispatcher.include() instead of RequestDispatcher.forward().</li>
432: * <li>The forwarded page could accept additional parameters --
433: * acutually converting parameters to a query string
434: * and appending it to uri.</li>
435: * <li>In additions, it does HTTP encoding, i.e., converts '+' and other
436: * characters to comply HTTP.</li>
437: * </ul>
438: *
439: * <p>NOTE: don't include query parameters in uri.
440: *
441: * @param ctx the servlet context. If null, uri cannot be foreign URI.
442: * It is ignored if URI is relevant (neither starts with '/' nor '~').
443: * @param uri the URI to include. It is OK to relevant (without leading
444: * '/'). If starts with "/", the context path of request is assumed.
445: * To reference to foreign context, use "~ctx" where ctx is the
446: * context path of the foreign context (without leading '/').
447: * If it could be any context path recognized by the Web container or
448: * any name registered with {@link #addExtendletContext}.
449: * @param params the parameter map; null to ignore
450: * @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM},
451: * and {@link #APPEND_PARAM}. It defines how to handle if both uri
452: * and params contains the same parameter.
453: */
454: public static final void forward(ServletContext ctx,
455: ServletRequest request, ServletResponse response,
456: String uri, Map params, int mode) throws IOException,
457: ServletException {
458: // if (D.ON && log.debugable()) log.debug("Forwarding "+uri);
459:
460: //include or foward depending whether this page is included or not
461: if (isIncluded(request)) {
462: include(ctx, request, response, uri, params, mode);
463: return;
464: }
465:
466: final RequestDispatcher disp = getRequestDispatcher(ctx,
467: request, uri, params, mode);
468: if (disp == null)
469: throw new ServletException(
470: "No dispatcher available to forward to " + uri);
471:
472: if (mode == PASS_THRU_ATTR && params != null
473: && !params.isEmpty()) {
474: final Map old = setPassThruAttr(request, params);
475: try {
476: disp.forward(request, response);
477: } catch (ClassCastException ex) {
478: //Tom M. Yeh, 2006/09/21: Bug 1548478
479: //Cause: http://issues.apache.org/bugzilla/show_bug.cgi?id=39417
480: //
481: //Bug or limitation of Catalina: not accepting HttpServletRequest
482: //othere than the original one or wrapper of original one
483: //
484: //Real Cause: org.apache.catalina.core.ApplicationDispatcher
485: //call unwrapRequest() twice, and then causes ClassCastException
486: //
487: //Resolution: since it is the almost last statement, it is safe
488: //to ignore this exception
489: if (!(request instanceof org.zkoss.web.portlet.RenderHttpServletRequest))
490: throw ex; //not the case described above
491: } finally {
492: restorePassThruAttr(request, old);
493: }
494: } else {
495: disp.forward(request, response);
496: }
497: }
498:
499: /** A shortcut of forward(request, response, uri, null, 0).
500: */
501: public static final void forward(ServletContext ctx,
502: ServletRequest request, ServletResponse response, String uri)
503: throws IOException, ServletException {
504: forward(ctx, request, response, uri, null, 0);
505: }
506:
507: /**
508: * Includes the resource at the specified URI.
509: * It enhances RequestDispatcher to allow the inclusion with
510: * a parameter map -- acutually converting parameters to a query string
511: * and appending it to uri.
512: *
513: * <p>NOTE: don't include query parameters in uri.
514: *
515: * @param ctx the servlet context. If null, uri cannot be foreign URI.
516: * It is ignored if URI is relevant (neither starts with '/' nor '~').
517: * @param uri the URI to include. It is OK to relevant (without leading
518: * '/'). If starts with "/", the context path of request is assumed.
519: * To reference to foreign context, use "~ctx/" where ctx is the
520: * context path of the foreign context (without leading '/').
521: * @param params the parameter map; null to ignore
522: * @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM},
523: * and {@link #APPEND_PARAM}. It defines how to handle if both uri
524: * and params contains the same parameter.
525: */
526: public static final void include(ServletContext ctx,
527: ServletRequest request, ServletResponse response,
528: String uri, Map params, int mode) throws IOException,
529: ServletException {
530: // if (D.ON && log.debugable()) log.debug("Including "+uri+" at "+ctx);
531:
532: //20050606: Tom Yeh
533: //We have to set this special attribute for jetty
534: //Otherwise, if including a page crossing context might not return
535: //the same session
536: request.setAttribute(
537: "org.mortbay.jetty.servlet.Dispatcher.shared_session",
538: Boolean.TRUE);
539:
540: final RequestDispatcher disp = getRequestDispatcher(ctx,
541: request, uri, params, mode);
542: if (disp == null)
543: throw new ServletException(
544: "No dispatcher available to include " + uri);
545:
546: if (mode == PASS_THRU_ATTR && params != null
547: && !params.isEmpty()) {
548: final Map old = setPassThruAttr(request, params);
549: try {
550: disp.include(request, response);
551: } finally {
552: restorePassThruAttr(request, old);
553: }
554: } else {
555: disp.include(request, response);
556: }
557: }
558:
559: /** A shortcut of include(request, response, uri, null, 0).
560: */
561: public static final void include(ServletContext ctx,
562: ServletRequest request, ServletResponse response, String uri)
563: throws IOException, ServletException {
564: include(ctx, request, response, uri, null, 0);
565: }
566:
567: /** Sets the arg attribute to pass parameters thru request's attribute.
568: */
569: private static final Map setPassThruAttr(ServletRequest request,
570: Map params) {
571: final Map old = (Map) request.getAttribute(Attributes.ARG);
572: request.setAttribute(Attributes.ARG, params);
573: return old;
574: }
575:
576: /** Restores what has been done by {@link #setPassThruAttr}.
577: */
578: private static final void restorePassThruAttr(
579: ServletRequest request, Map old) {
580: if (old != null)
581: request.setAttribute(Attributes.ARG, old);
582: else
583: request.removeAttribute(Attributes.ARG);
584: }
585:
586: /** Returns the request dispatch of the specified URI.
587: *
588: * @param ctx the servlet context. If null, uri cannot be foreign URI.
589: * It is ignored if uri is relevant (neither starts with '/' nor '~').
590: * @param request the request. If null, uri cannot be relevant.
591: * It is used only if uri is relevant.
592: * @param uri the URI to include. It is OK to relevant (without leading
593: * '/'). If starts with "/", the context path of request is assumed.
594: * To reference to foreign context, use "~ctx/" where ctx is the
595: * context path of the foreign context (without leading '/').
596: * @param params the parameter map; null to ignore
597: * @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM},
598: * and {@link #APPEND_PARAM}. It defines how to handle if both uri
599: * and params contains the same parameter.
600: */
601: public static final RequestDispatcher getRequestDispatcher(
602: ServletContext ctx, ServletRequest request, String uri,
603: Map params, int mode) throws ServletException {
604: final char cc = uri.length() > 0 ? uri.charAt(0) : (char) 0;
605: if (ctx == null || (cc != '/' && cc != '~')) {//... or relevant
606: if (request == null)
607: throw new IllegalArgumentException(
608: ctx == null ? "Servlet context and request cannot be both null"
609: : "Request is required to use revalant URI: "
610: + uri);
611: if (cc == '~')
612: throw new IllegalArgumentException(
613: "Servlet context is required to use foreign URI: "
614: + uri);
615: uri = generateURI(uri, params, mode);
616: return request.getRequestDispatcher(uri);
617: }
618:
619: //NO NEED to encodeURL since it is forward/include
620: return new ParsedURI(ctx, uri).getRequestDispatcher(params,
621: mode);
622: }
623:
624: /** Returns the resource of the specified uri.
625: * Unlike ServletContext.getResource, it handles "~" like
626: * {@link #getRequestDispatcher} did.
627: */
628: public static final URL getResource(ServletContext ctx, String uri)
629: throws MalformedURLException {
630: return new ParsedURI(ctx, uri).getResource();
631: }
632:
633: /** Returns the resource stream of the specified uri.
634: * Unlike ServletContext.getResource, it handles "~" like
635: * {@link #getRequestDispatcher} did.
636: */
637: public static final InputStream getResourceAsStream(
638: ServletContext ctx, String uri) {
639: return new ParsedURI(ctx, uri).getResourceAsStream();
640: }
641:
642: /** Used to resolve "~" in URI. */
643: private static class ParsedURI {
644: private ServletContext _svlctx;
645: private ExtendletContext _extctx;
646: private String _uri;
647:
648: private ParsedURI(final ServletContext ctx, final String uri) {
649: if (uri != null && uri.startsWith("~")) { //refer to foreign context
650: final int j = uri.indexOf('/', 1);
651: final String ctxroot;
652: if (j >= 0) {
653: ctxroot = "/" + uri.substring(1, j);
654: _uri = uri.substring(j);
655: } else {
656: ctxroot = "/" + uri.substring(1);
657: _uri = "/";
658: }
659:
660: _extctx = getExtendletContext(ctx, ctxroot.substring(1));
661: if (_extctx == null) {
662: _svlctx = ctx.getContext(ctxroot);
663: if (_svlctx == null)
664: throw new SystemException(
665: "Context not found or not visible to "
666: + ctx + ": " + ctxroot);
667: }
668: } else {
669: _svlctx = ctx;
670: _uri = uri;
671: }
672: }
673:
674: private RequestDispatcher getRequestDispatcher(Map params,
675: int mode) {
676: if (_extctx == null && _svlctx == null) //not found
677: return null;
678:
679: final String uri = generateURI(_uri, params, mode);
680: return _svlctx != null ? _svlctx.getRequestDispatcher(uri)
681: : _extctx.getRequestDispatcher(uri);
682: }
683:
684: private URL getResource() throws MalformedURLException {
685: return _svlctx != null ? _svlctx.getResource(_uri)
686: : _extctx != null ? _extctx.getResource(_uri)
687: : null;
688: }
689:
690: private InputStream getResourceAsStream() {
691: return _svlctx != null ? _svlctx.getResourceAsStream(_uri)
692: : _extctx != null ? _extctx
693: .getResourceAsStream(_uri) : null;
694: }
695: }
696:
697: /** Whether to overwrite uri if both uri and params contain the same
698: * parameter.
699: * Used by {@link #generateURI}
700: */
701: public static final int OVERWRITE_URI = 0;
702: /** Whether to ignore params if both uri and params contain the same
703: * parameter.
704: * Used by {@link #generateURI}
705: */
706: public static final int IGNORE_PARAM = 1;
707: /** Whether to append params if both uri and params contain the same
708: * parameter. In other words, they both appear as the final query string.
709: * Used by {@link #generateURI}
710: */
711: public static final int APPEND_PARAM = 2;
712: /** Whether the specified parameters shall be passed thru the request
713: * attribute called arg.
714: */
715: public static final int PASS_THRU_ATTR = 3;
716:
717: /** Generates URI by appending the parameters.
718: * Note: it doesn't support the ~xxx/ format.
719: *
720: * @param params the parameters to apend to the query string
721: * @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM},
722: * {@link #APPEND_PARAM} and {@link #PASS_THRU_ATTR}.
723: * It defines how to handle if both uri and params contains the same
724: * parameter.
725: * mode is used only if both uri contains query string and params is
726: * not empty.
727: * @see Encodes#encodeURL(ServletContext, ServletRequest, ServletResponse, String)
728: */
729: public static final String generateURI(String uri, Map params,
730: int mode) {
731: if (uri.startsWith("~"))
732: throw new IllegalArgumentException(
733: "~ctx not supported here: " + uri);
734:
735: final int j = uri.indexOf('?');
736: String qstr = null;
737: if (j >= 0) {
738: qstr = uri.substring(j);
739: uri = uri.substring(0, j);
740: }
741:
742: //if (D.ON && uri.indexOf('%') >= 0)
743: // log.warning(new IllegalStateException("You might encode URL twice: "+uri));
744: //might too annoying
745:
746: try {
747: uri = Encodes.encodeURI(uri);
748: final boolean noQstr = qstr == null;
749: final boolean noParams = mode == PASS_THRU_ATTR
750: || params == null || params.isEmpty();
751: if (noQstr && noParams)
752: return uri;
753:
754: if (noQstr != noParams)
755: mode = APPEND_PARAM;
756:
757: final StringBuffer sb = new StringBuffer(80).append(uri);
758: if (qstr != null)
759: sb.append(qstr);
760:
761: switch (mode) {
762: case IGNORE_PARAM:
763: //removing params that is conflict
764: for (final Iterator it = params.entrySet().iterator(); it
765: .hasNext();) {
766: final Map.Entry me = (Map.Entry) it.next();
767: final String nm = (String) me.getKey();
768: if (Encodes.containsQuery(qstr, nm))
769: it.remove();
770: }
771: //flow thru
772: case OVERWRITE_URI:
773: return Encodes.setToQueryString(sb, params).toString();
774: case APPEND_PARAM:
775: return Encodes.addToQueryString(sb, params).toString();
776: default:
777: throw new IllegalArgumentException("Unknown mode: "
778: + mode);
779: }
780: } catch (UnsupportedEncodingException ex) {
781: throw new SystemException(ex);
782: }
783: }
784:
785: /** A list of context root paths (e.g., "/abc"). */
786: private static List _ctxroots;
787:
788: /** Returns a list of context paths (e.g., "/abc") that this application
789: * has. This implementation parse application.xml. For war that doesn't
790: * contain application.xml might have to override this method and
791: * parse another file to know what context being loaded.
792: */
793: public static final List getContextPaths() {
794: if (_ctxroots != null)
795: return _ctxroots;
796:
797: try {
798: synchronized (Servlets.class) {
799: return _ctxroots = myGetContextPaths();
800: }
801: } catch (Exception ex) {
802: throw SystemException.Aide.wrap(ex);
803: }
804: }
805:
806: private static final List myGetContextPaths() throws Exception {
807: final String APP_XML = "/META-INF/application.xml";
808: final List ctxroots = new LinkedList();
809: final URL xmlURL = Locators.getDefault().getResource(APP_XML);
810: if (xmlURL == null)
811: throw new SystemException("File not found: " + APP_XML);
812:
813: // if (log.debugable()) log.debug("Parsing "+APP_XML);
814: final Element root = new SAXBuilder(false, false, true).build(
815: xmlURL).getRootElement();
816:
817: for (Iterator it = root.getElements("module").iterator(); it
818: .hasNext();) {
819: final Element e = (Element) it.next();
820: final String ctxroot = (String) e
821: .getContent("web/context-root");
822: if (ctxroot == null) {
823: // if (D.ON && log.finerable()) log.finer("Skip non-web: "+e.getContent("java"));
824: continue;
825: }
826:
827: ctxroots.add(ctxroot.startsWith("/") ? ctxroot : "/"
828: + ctxroot);
829: }
830:
831: // log.info("Context found: "+ctxroots);
832: return new ArrayList(ctxroots);
833: }
834:
835: /** Returns a token to represent a limit-time offer.
836: * It is mainly used as an parameter value (mostlycalled zk_lto), and then
837: * you could verify whether it is expired by {@link #isOfferExpired}.
838: */
839: public static final String getLimitTimeOffer() {
840: final String lto = Long.toHexString(System.currentTimeMillis());
841: return lto + Checksums.getChecksum(lto, "");
842: }
843:
844: /** Returns whether a token returned by getLimitTimeOffer expired.
845: * @param timeout how long the office shall expire, unit: seconds.
846: */
847: public static final boolean isOfferExpired(String lto, int timeout) {
848: final int len = lto != null ? lto.length() : 0;
849: if (len <= 1)
850: return true;
851:
852: final char cksm = lto.charAt(len - 1);
853: lto = lto.substring(0, len - 1);
854: if (cksm != Checksums.getChecksum(lto, ""))
855: return true;
856:
857: try {
858: return Long.parseLong(lto, 16) + timeout * 1000L < System
859: .currentTimeMillis();
860: } catch (NumberFormatException ex) {
861: return true;
862: }
863: }
864:
865: /** Adds an extended context.
866: * @return the previous extended context, if any, associated with
867: * the specified name.
868: */
869: public static final ExtendletContext addExtendletContext(
870: ServletContext ctx, String name, ExtendletContext extctx) {
871: if (name == null || extctx == null)
872: throw new IllegalArgumentException("null");
873: return (ExtendletContext) getExtWebCtxs(ctx).put(name, extctx);
874: }
875:
876: /** Removes an extended context of the specified name.
877: */
878: public static final ExtendletContext removeExtendletContext(
879: ServletContext ctx, String name) {
880: return (ExtendletContext) getExtWebCtxs(ctx).remove(name);
881: }
882:
883: /** Returns the extended context of the specified name.
884: */
885: public static final ExtendletContext getExtendletContext(
886: ServletContext ctx, String name) {
887: return (ExtendletContext) getExtWebCtxs(ctx).get(name);
888: }
889:
890: private static final Map getExtWebCtxs(ServletContext ctx) {
891: synchronized (Servlets.class) { //don't use ctx because it might be a proxy (in portlet)
892: final String attr = "javax.zkoss.web.servlets.ExtendletContexts";
893: //such that it could be shared among portlets
894: Map ctxs = (Map) ctx.getAttribute(attr);
895: if (ctxs == null)
896: ctx.setAttribute(attr, ctxs = Collections
897: .synchronizedMap(new HashMap(5)));
898: return ctxs;
899: }
900: }
901:
902: /** Returns the file extension of the specified path, or null
903: * if no extension at all.
904: *
905: * <p>Note: the extension is converted to the lower case.
906: *
907: * <p>Note: it assumes the session ID, if any, starts with semicolon.
908: * For example, the path could be "/a/b.zul;jsession=xxx".
909: *
910: * @param path the path. If path is null, null is returned.
911: * @since 2.4.1
912: */
913: public static final String getExtension(String path) {
914: if (path != null) {
915: int j = path.lastIndexOf('.');
916: if (j >= 0 && path.indexOf('/', j + 1) < 0) {
917: final String ext = path.substring(j + 1);
918: j = ext.indexOf(';');
919: return j >= 0 ? ext.substring(0, j).toLowerCase() : ext
920: .toLowerCase();
921: }
922: }
923: return null;
924: }
925: }
|