001: /* ClassWebResource.java
002:
003: {{IS_NOTE
004: Purpose:
005:
006: Description:
007:
008: History:
009: Tue Sep 20 16:49:45 2005, Created by tomyeh
010: }}IS_NOTE
011:
012: Copyright (C) 2005 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.util.resource;
020:
021: import java.util.Map;
022: import java.util.HashMap;
023: import java.net.URL;
024: import java.io.InputStream;
025: import java.io.IOException;
026: import java.io.UnsupportedEncodingException;
027:
028: import javax.servlet.ServletContext;
029: import javax.servlet.ServletRequest;
030: import javax.servlet.ServletResponse;
031: import javax.servlet.ServletOutputStream;
032: import javax.servlet.ServletException;
033: import javax.servlet.RequestDispatcher;
034: import javax.servlet.http.HttpServletRequest;
035: import javax.servlet.http.HttpServletResponse;
036:
037: import org.zkoss.lang.D;
038: import org.zkoss.io.Files;
039: import org.zkoss.util.logging.Log;
040: import org.zkoss.util.media.ContentTypes;
041: import org.zkoss.util.resource.Locator;
042: import org.zkoss.util.resource.Locators;
043:
044: import org.zkoss.web.servlet.Servlets;
045: import org.zkoss.web.servlet.Charsets;
046: import org.zkoss.web.servlet.http.Https;
047: import org.zkoss.web.servlet.http.Encodes;
048:
049: /**
050: * Used to access resouces located in class path and under /web.
051: * It doesn't work alone. Rather, it is a helper for servlet, such as
052: * ZK's update servlet or {@link ClassWebServlet}.
053: *
054: * <p>Typical use:
055: * <ol>
056: * <li>Declares a member in the servlet to
057: * serve the request for the resource located in class path.
058: * <li>Invoke {@link #getInstance} to init the member when
059: * Servlet.init() is called.
060: * <li>Calling {@link #service} when a request is receive.
061: * </ol>
062: *
063: * @author tomyeh
064: */
065: public class ClassWebResource {
066: private static final Log log = Log.lookup(ClassWebResource.class);
067:
068: private final ServletContext _ctx;
069: private final String _mappingURI;
070: private final ClassWebContext _cwc;
071: /** An array of extensions that have to be compressed (with gzip). */
072: private String[] _compressExts;
073: /** Map(String ext, Extendlet). */
074: private final Map _extlets = new HashMap(5);
075:
076: /** The prefix of path of web resources ("/web"). */
077: public static final String PATH_PREFIX = "/web";
078:
079: /** Returns the URL of the resource of the specified URI by searching
080: * the class path (with {@link #PATH_PREFIX}).
081: */
082: public static URL getResource(String uri) {
083: return Locators.getDefault().getResource(PATH_PREFIX + uri);
084: }
085:
086: /** Returns the resource in a stream of the specified URI by searching
087: * the class path (with {@link #PATH_PREFIX}).
088: */
089: public static InputStream getResourceAsStream(String uri) {
090: return Locators.getDefault().getResourceAsStream(
091: PATH_PREFIX + uri);
092: }
093:
094: /** Returns the instance (singlton in the whole app) for
095: * handling resources located in class path.
096: */
097: public static final ClassWebResource getInstance(
098: ServletContext ctx, String mappingURI) {
099: synchronized (ctx) {
100: final ClassWebContext cwc = (ClassWebContext) Servlets
101: .getExtendletContext(ctx, ".");
102: if (cwc != null)
103: return cwc.getClassWebResource();
104:
105: final ClassWebResource cwr = new ClassWebResource(ctx,
106: mappingURI);
107: Servlets.addExtendletContext(ctx, ".", cwr._cwc);
108: return cwr;
109: }
110: }
111:
112: /** Constructor. */
113: private ClassWebResource(ServletContext ctx, String mappingURI) {
114: if (!mappingURI.startsWith("/") || mappingURI.endsWith("/"))
115: throw new IllegalArgumentException(
116: "mappingURI must start with /, but not ends with /");
117: if (ctx == null)
118: throw new IllegalArgumentException("null ctx");
119:
120: _ctx = ctx;
121: _mappingURI = mappingURI;
122: _cwc = new ClassWebContext();
123: addExtendlet("dsp", new DspExtendlet());
124: }
125:
126: /** Process the request by retrieving the path from the path info.
127: * It invokes {@link Https#getThisPathInfo} to retrieve the path info,
128: * and then invoke {@link #service(HttpServletRequest,HttpServletResponse,String)}.
129: *
130: * <p>If the path info is not found, nothing is generated.
131: *
132: * @since 2.4.1
133: */
134: public void service(HttpServletRequest request,
135: HttpServletResponse response) throws ServletException,
136: IOException {
137: final String pi = Https.getThisPathInfo(request);
138: // if (D.ON && log.debugable()) log.debug("Path info: "+pi);
139: if (pi != null)
140: service(request, response, pi.substring(PATH_PREFIX
141: .length()));
142: }
143:
144: /** Process the request with the specified path.
145: *
146: * @param path the path related to the class path
147: * @since 3.0.0
148: */
149: public void service(HttpServletRequest request,
150: HttpServletResponse response, String path)
151: throws ServletException, IOException {
152: final Object old = Charsets.setup(request, response, "UTF-8");
153: try {
154: web(request, response, path);
155: } finally {
156: Charsets.cleanup(request, old);
157: }
158: }
159:
160: /** Returns the Extendlet (aka., resource processor) of the
161: * specified extension, or null if not associated yet.
162: *
163: * @param ext the extension, e.g, "js" and "css".
164: * @return the Extendlet (aka., resource processor),
165: * or null if not associated yet.
166: * @since 2.4.1
167: */
168: public Extendlet getExtendlet(String ext) {
169: if (ext == null)
170: return null;
171:
172: ext = ext.toLowerCase();
173: synchronized (_extlets) {
174: return (Extendlet) _extlets.get(ext);
175: }
176: }
177:
178: /** Adds an Extendlet (aka., resource processor) to process
179: * the resource of the specified extension.
180: *
181: * @param ext the extension, e.g, "js" and "css".
182: * @param extlet the Extendlet (aka., resouce processor) to add
183: * @return the previous Extendlet, or null if not associated before.
184: * @since 2.4.1
185: */
186: public Extendlet addExtendlet(String ext, Extendlet extlet) {
187: if (ext == null || extlet == null)
188: throw new IllegalArgumentException("null");
189:
190: extlet.init(new ExtendletConfig() {
191: public ExtendletContext getExtendletContext() {
192: return _cwc;
193: }
194: });
195:
196: ext = ext.toLowerCase();
197: synchronized (_extlets) {
198: return (Extendlet) _extlets.put(ext, extlet);
199: }
200: }
201:
202: /** Removes the Extendlet (aka., resource processor)
203: * for the specified extension.
204: *
205: * @param ext the extension, e.g, "js" and "css".
206: * @return the previous Extendlet, or null if no Extendlet
207: * was associated with the specified extension.
208: * @since 2.4.1
209: */
210: public Extendlet removeExtendlet(String ext) {
211: if (ext == null)
212: return null;
213:
214: ext = ext.toLowerCase();
215: synchronized (_extlets) {
216: return (Extendlet) _extlets.remove(ext);
217: }
218: }
219:
220: /** Sets the extension that shall be compressed if the browser
221: * supports the compression encoding (accept-encoding).
222: *
223: * <p>Default: null (no compression at all).
224: *
225: * @param exts an array of extensions, e.g., {"js", "css"}.
226: * If null or zero-length, it means no compression at all.
227: *@since 2.4.1
228: */
229: public void setCompress(String[] exts) {
230: _compressExts = exts != null && exts.length > 0 ? exts : null;
231: }
232:
233: /**Returns the extension that shall be compressed if the browser
234: * supports the compression encoding (accept-encoding).
235: *
236: * <p>Default: null (no compression at all).
237: *@since 2.4.1
238: */
239: public String[] getCompress() {
240: return _compressExts;
241: }
242:
243: //-- Work with ClassWebContext --//
244: /** Works with {@link ClassWebContext} to
245: * load resources from class path (thru this servlet).
246: */
247: private void web(HttpServletRequest request,
248: HttpServletResponse response, String pi)
249: throws ServletException, IOException {
250: //A trick used to enforce browser to load new version JavaScript
251: //How it work: client engine prefix URI with /_zver123, where
252: //123 is the build version that changes once reload is required
253: //Then, the server eliminate such prefix before locating resource
254: final String ZVER = "/_zver";
255: if (pi.startsWith(ZVER)) {
256: final int j = pi.indexOf('/', ZVER.length());
257: if (j >= 0)
258: pi = pi.substring(j);
259: else
260: log.warning("Unknown path info: " + pi);
261: }
262:
263: //Notify the browser by calling back the code specified with /_zcb
264: String jsextra = null;
265: final String ZCB = "/_zcb"; //denote callback is required
266: if (pi.startsWith(ZCB)) {
267: final int j = pi.indexOf('/', ZCB.length());
268: if (j >= 0) {
269: jsextra = pi.substring(ZCB.length(), j);
270: pi = pi.substring(j);
271: } else {
272: jsextra = pi.substring(ZCB.length());
273: log.warning("Unknown path info: " + pi);
274: }
275:
276: final int len = jsextra.length();
277: if (len == 0)
278: jsextra = null;
279: else {
280: final char cc = jsextra.charAt(len - 1);
281: if (cc != ';') {
282: if (cc != ')')
283: jsextra += "()";
284: jsextra += ';';
285: }
286: }
287: }
288:
289: final String ext = Servlets.getExtension(pi);
290: if (ext != null) {
291: //Invoke the resource processor (Extendlet)
292: final Extendlet extlet = getExtendlet(ext);
293: if (extlet != null) {
294: extlet.service(request, response, pi, jsextra);
295: return;
296: }
297:
298: if (!Servlets.isIncluded(request)) {
299: final String ctype = ContentTypes.getContentType(ext);
300: if (ctype != null)
301: response.setContentType(ctype);
302: // if (D.ON && log.debugable()) log.debug("Content type: "+ctype+" for "+pi);
303: }
304: }
305:
306: byte[] extra = jsextra != null ? jsextra.getBytes("UTF-8")
307: : null;
308: pi = Servlets.locate(_ctx, request, pi, _cwc.getLocator());
309: final InputStream is = getResourceAsStream(pi);
310: byte[] data;
311: if (is == null) {
312: if ("js".equals(ext)) {
313: //Don't sendError. Reason: 1) IE waits and no onerror fired
314: //2) better to debug (user will tell us what went wrong)
315: data = ("(zk.error?zk.error:alert)('" + pi + " not found');")
316: .getBytes();
317: //FUTURE: zweb shall not depend on zk
318: } else {
319: if (Servlets.isIncluded(request))
320: log.error("Resource not found: " + pi);
321: response.sendError(response.SC_NOT_FOUND, pi);
322: return;
323: }
324: } else {
325: //Note: don't compress images
326: data = shallCompress(request, ext) ? Https.gzip(request,
327: response, is, extra) : null;
328: if (data != null)
329: extra = null; //extra is compressed and output
330: else
331: data = Files.readAll(is);
332: //since what is embedded in the jar is not big, so load completely
333: }
334:
335: int len = data.length;
336: if (extra != null)
337: len += extra.length;
338: response.setContentLength(len);
339:
340: final ServletOutputStream out = response.getOutputStream();
341: out.write(data);
342: if (extra != null)
343: out.write(extra);
344: out.flush();
345: }
346:
347: private boolean shallCompress(ServletRequest request, String ext) {
348: if (ext != null && _compressExts != null
349: && !Servlets.isIncluded(request))
350: for (int j = 0; j < _compressExts.length; ++j)
351: if (ext.equals(_compressExts[j]))
352: return true;
353: return false;
354: }
355:
356: /**
357: * An implementation of ExtendletContext to load resources from
358: * the class path rooted at /web.
359: */
360: private class ClassWebContext implements ExtendletContext {
361: private final Locator _locator = new Locator() {
362: public String getDirectory() {
363: return null;
364: }
365:
366: public URL getResource(String name) {
367: return ClassWebResource.getResource(name);
368: }
369:
370: public InputStream getResourceAsStream(String name) {
371: return ClassWebResource.getResourceAsStream(name);
372: }
373: };
374:
375: /** Returns the associated class web resource. */
376: public ClassWebResource getClassWebResource() {
377: return ClassWebResource.this ;
378: }
379:
380: //-- ExtendletContext --//
381: public ServletContext getServletContext() {
382: return _ctx;
383: }
384:
385: public Locator getLocator() {
386: return _locator;
387: }
388:
389: public boolean shallCompress(ServletRequest request, String ext) {
390: return ClassWebResource.this .shallCompress(request, ext);
391: }
392:
393: public String encodeURL(ServletRequest request,
394: ServletResponse response, String uri)
395: throws ServletException, UnsupportedEncodingException {
396: uri = Servlets.locate(_ctx, request, uri, getLocator()); //resolves "*"
397: uri = _mappingURI + PATH_PREFIX + uri; //prefix with mapping
398:
399: //prefix context path
400: if (request instanceof HttpServletRequest) {
401: String ctxpath = ((HttpServletRequest) request)
402: .getContextPath();
403: final int ctxlen = ctxpath.length();
404: if (ctxlen > 0) {
405: final char cc = ctxpath.charAt(0);
406: if (cc != '/')
407: ctxpath = '/' + ctxpath;
408: //Work around a bug for Pluto's RenderRequest (1.0.1)
409: else if (ctxlen == 1)
410: ctxpath = ""; // "/" => ""
411: //Work around liferay's issue: Upload 1627928 (not verified)
412: }
413: uri = ctxpath + uri;
414: }
415:
416: int j = uri.indexOf('?');
417: if (j < 0) {
418: uri = Encodes.encodeURI(uri);
419: } else {
420: uri = Encodes.encodeURI(uri.substring(0, j))
421: + uri.substring(j);
422: }
423: //encode
424: if (response instanceof HttpServletResponse)
425: uri = ((HttpServletResponse) response).encodeURL(uri);
426: return uri;
427: }
428:
429: public String encodeRedirectURL(HttpServletRequest request,
430: HttpServletResponse response, String uri, Map params,
431: int mode) {
432: return Https.encodeRedirectURL(_ctx, request, response,
433: _mappingURI + PATH_PREFIX + uri, params, mode);
434: }
435:
436: public RequestDispatcher getRequestDispatcher(String uri) {
437: // if (D.ON && log.debugable()) log.debug("getRequestDispatcher: "+uri);
438: return _ctx.getRequestDispatcher(_mappingURI + PATH_PREFIX
439: + uri);
440: }
441:
442: public URL getResource(String uri) {
443: return ClassWebResource.getResource(uri);
444: }
445:
446: public InputStream getResourceAsStream(String uri) {
447: return ClassWebResource.getResourceAsStream(uri);
448: }
449: }
450: }
|