org.zkoss.web.util.resource.ClassWebResource Maven / Gradle / Ivy
/* ClassWebResource.java
Purpose:
Description:
History:
Tue Sep 20 16:49:45 2005, Created by tomyeh
Copyright (C) 2005 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.web.util.resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.Calendar;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.io.Files;
import org.zkoss.io.WriterOutputStream;
import org.zkoss.lang.Exceptions;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.util.FastReadArray;
import org.zkoss.util.media.ContentTypes;
import org.zkoss.util.resource.Locator;
import org.zkoss.util.resource.Locators;
import org.zkoss.web.Attributes;
import org.zkoss.web.servlet.Charsets;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.web.servlet.http.Encodes;
import org.zkoss.web.servlet.http.Https;
import org.zkoss.xml.XMLs;
/**
* Used to access resources located in class path and under /web.
* It doesn't work alone. Rather, it is a helper for servlet, such as
* ZK's update servlet.
*
* Typical use:
*
* - Declares a member in the servlet to
* serve the request for the resource located in class path.
*
- Invoke {@link #getInstance} to init the member when
* Servlet.init() is called.
*
- Calling {@link #service} when a request is receive.
*
*
* Note: it assumes the file being loaded and the output stream is
* encoded in UTF-8 unless the extendlet handles it differently.
*
* @author tomyeh
*/
public class ClassWebResource {
private static final Logger log = LoggerFactory.getLogger(ClassWebResource.class);
private final ServletContext _ctx;
/** mapping URI including PATH_PREFIX. */
private String _mappingURI;
private final CWC _cwc;
/** An array of extensions that have to be compressed (with gzip). */
private Set _compressExts;
/** Map(String ext, Extendlet). */
private final Map _extlets = new HashMap();
/** Filers for requests. Map(String ext, FastReadArray(Filter)). */
private final Map> _reqfilters = new HashMap>(2);
/** Filers for includes. Map(String ext, FastReadArray(Filter)). */
private final Map> _incfilters = new HashMap>(2);
/** Additional locator. */
private Locator _extraloc;
/** The prefix used to encode URL. */
private String _encURLPrefix;
/** Whether to debug JavaScript files. */
private boolean _debugJS;
/** The prefix of path of web resources ("/web"). */
public static final String PATH_PREFIX = "/web";
/** The prefix of ZK version used to prevent browser cache. */
private static final String ZVER = "/_zv";
/** Indicates that the filter is applicable if the request comes
* directly from the client.
* @see #addFilter
* @since 3.5.1
*/
public static final int FILTER_REQUEST = 0x1;
/** Indicates that the filter is applicable if the request is dispatched
* due to the inclusion. In other words, it is included by other servlet.
* @see #addFilter
* @since 3.5.1
*/
public static final int FILTER_INCLUDE = 0x2;
/**
* Internal used only.
*
* @since 9.6.0
*/
private ConcurrentMap _sourceFromSourceMap; //js path -> js content
/** Returns the URL of the resource of the specified URI by searching
* only the class path (with {@link #PATH_PREFIX}).
* On the other hand, {@link #getResource} will search
* the extra locator first ({@link #getExtraLocator}) and then
* the class path.
* @since 5.0.0
*/
public static URL getClassResource(String uri) {
return Locators.getDefault().getResource(PATH_PREFIX + fixURI(uri));
}
/** Returns the resource in a stream of the specified URI by searching
* only the class path (with {@link #PATH_PREFIX}).
*
On the other hand, {@link #getResourceAsStream} will search
* the extra locator first ({@link #getExtraLocator}) and then
* the class path.
* @since 5.0.0
*/
public static InputStream getClassResourceAsStream(String uri) {
return Locators.getDefault().getResourceAsStream(PATH_PREFIX + fixURI(uri));
}
/** Returns the URL of the resource of the specified URI by searching
* the extra locator, if any, and then the class path
* (with {@link #PATH_PREFIX}).
*
This method becomes non-static since 5.0.0, and it
* will search the extra locator ({@link #getExtraLocator}) first.
*/
public URL getResource(String uri) {
uri = fixURI(uri);
if (_extraloc != null) {
final URL url = _extraloc.getResource(uri);
if (url != null)
return url;
}
return Locators.getDefault().getResource(PATH_PREFIX + uri);
}
/** Returns the resource in a stream of the specified URI by searching
* the extra locator, if any, and then, the class path (with {@link #PATH_PREFIX}).
*
This method becomes non-static since 5.0.0, and it
* will search the extra locator ({@link #getExtraLocator}) first.
*/
public InputStream getResourceAsStream(String uri) {
uri = fixURI(uri);
if (_extraloc != null) {
final InputStream is = _extraloc.getResourceAsStream(uri);
if (is != null)
return is;
}
return Locators.getDefault().getResourceAsStream(PATH_PREFIX + uri);
}
private static String fixURI(String uri) {
int j = uri.lastIndexOf('?');
if (j >= 0)
uri = uri.substring(0, j);
j = uri.lastIndexOf(";jsession");
if (j >= 0)
uri = uri.substring(0, j);
return uri;
}
/** Sets the required headers (e.g., Cache-Control) to notify the
* the client is allowed to cache the content as long as possible.
*/
private static void setClientCacheForever(HttpServletResponse response) {
if (!"false".equals(Library.getProperty("org.zkoss.web.classWebResource.cache"))) {
response.setHeader("Cache-Control", "public, max-age=31536000"); //a year (unit: seconds)
response.setDateHeader("Expires", _expires);
}
}
private static final long _expires;
static {
final Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_YEAR, 365);
_expires = cal.getTime().getTime();
}
/** Returns the instance (singleton in the whole app) for
* handling resources located in class path.
*/
public static final ClassWebResource getInstance(ServletContext ctx, String mappingURI) {
synchronized (ctx) {
final CWC cwc = (CWC) Servlets.getExtendletContext(ctx, ".");
if (cwc != null)
return cwc.getClassWebResource();
final ClassWebResource cwr = new ClassWebResource(ctx, mappingURI);
Servlets.addExtendletContext(ctx, ".", cwr._cwc);
return cwr;
}
}
/** Constructor. */
private ClassWebResource(ServletContext ctx, String mappingURI) {
if (!mappingURI.startsWith("/") || mappingURI.endsWith("/"))
throw new IllegalArgumentException("mappingURI must start with /, but not ends with /");
if (ctx == null)
throw new IllegalArgumentException("null ctx");
_ctx = ctx;
_mappingURI = mappingURI + PATH_PREFIX;
_cwc = new CWC();
addExtendlet("dsp", new DspExtendlet());
}
/** Returns the extra locator, or null if not available.
* The extra locator, if specified, has the higher priority than
* the class path.
*
Default: null.
* @since 5.0.0
*/
public Locator getExtraLocator() {
return _extraloc;
}
/** Sets the extra locator.
* The extra locator, if specified, has the higher priority than
* the class path.
*
Default: null.
* @since 5.0.0
*/
public void setExtraLocator(Locator loc) {
_extraloc = loc;
}
/** Process the request by retrieving the path from the path info.
* It invokes {@link Https#getThisPathInfo} to retrieve the path info,
* and then invoke {@link #service(HttpServletRequest,HttpServletResponse,String)}.
*
*
If the path info is not found, nothing is generated.
*
* @since 2.4.1
*/
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
final String pi = Https.getThisPathInfo(request);
// if (log.isDebugEnabled()) log.debug("Path info: "+pi);
if (pi != null)
service(request, response, pi.substring(PATH_PREFIX.length()));
}
/** Process the request with the specified path.
*
* @param path the path related to the class path
* @since 3.0.0
*/
public void service(HttpServletRequest request, HttpServletResponse response, String path)
throws ServletException, IOException {
Servlets.getBrowser(request); //update request info
final Object old = Charsets.setup(request, response, "UTF-8");
try {
web(request, response, path);
} finally {
Charsets.cleanup(request, old);
}
}
/** Returns the Extendlet (aka., resource processor) of the
* specified extension, or null if not associated.
*
* @param ext the extension, e.g, "js" and "css.dsp".
* @param lookup whether to search different combination of the given
* extension. For example, if the extension is "js.dsp" and
* lookup
is true, it first searches
* any extendlet registered for "js.dsp". If not found, it searches
* any extendlet for "dsp". However, if lookup
is false,
* it searches only "js.dsp".
* @return the Extendlet (aka., resource processor),
* or null if not associated yet.
* @since 6.0.0
*/
public Extendlet getExtendlet(String ext, boolean lookup) {
if (ext == null)
return null;
ext = ext.toLowerCase(java.util.Locale.ENGLISH);
for (;;) {
synchronized (_extlets) {
Extendlet exlet = _extlets.get(ext);
if (!lookup || exlet != null)
return exlet;
}
int j = ext.indexOf('.');
if (j < 0)
return null;
ext = ext.substring(j + 1);
}
}
/** Returns the Extendlet (a.k.a., resource processor) of the
* specified extension, or null if not associated.
*
*
It is a shortcut of getExtendlet(ext, true)
.
*
* @param ext the extension, e.g, "js" and "css.dsp".
* @return the Extendlet (a.k.a., resource processor),
* or null if not associated yet.
* @since 2.4.1
*/
public Extendlet getExtendlet(String ext) {
return getExtendlet(ext, true);
}
/** Adds an {@link Extendlet} (a.k.a., resource processor) to process
* the resource of the specified extension.
*
* @param ext the extension, e.g, "js" and "css".
* @param extlet the Extendlet (a.k.a., resource processor) to add
* @return the previous Extendlet, or null if not associated before.
* @since 2.4.1
*/
public Extendlet addExtendlet(String ext, Extendlet extlet) {
if (ext == null || extlet == null)
throw new IllegalArgumentException("null");
extlet.init(new ExtendletConfig() {
public ExtendletContext getExtendletContext() {
return _cwc;
}
public void addCompressExtension(String ext) {
synchronized (ClassWebResource.this) {
if (_compressExts == null)
_compressExts = new LinkedHashSet();
_compressExts.add(ext);
}
}
});
ext = ext.toLowerCase(java.util.Locale.ENGLISH);
synchronized (_extlets) {
return _extlets.put(ext, extlet);
}
}
/** Removes the {@link Extendlet} (a.k.a., resource processor)
* for the specified extension.
*
* @param ext the extension, e.g, "js" and "css.dsp".
* @return the previous Extendlet, or null if no Extendlet
* was associated with the specified extension.
* @since 2.4.1
*/
public Extendlet removeExtendlet(String ext) {
if (ext == null)
return null;
ext = ext.toLowerCase(java.util.Locale.ENGLISH);
synchronized (_extlets) {
return _extlets.remove(ext);
}
}
/** Returns an array of the filters ({@link Filter}) of the specified
* extension, or null if not associated.
*
* Note: if the extension is "js.dsp", then it searches
* any filters registered for "js.dsp". If not found, it searches
* any filters for "dsp". In other words, "dsp" is searched only
* if no filter is registered for "js.dsp".
*
* @param ext the extension, such as "js" and "css.dsp".
* @param flag either {@link #FILTER_REQUEST} or
* and {@link #FILTER_INCLUDE}. If 0, {@link #FILTER_REQUEST}
* is assumed.
* @since 3.5.1
*/
public Filter[] getFilters(String ext, int flag) {
if (ext == null)
return null;
ext = ext.toLowerCase(java.util.Locale.ENGLISH);
final Map> filters = flag == 0 || (flag & FILTER_REQUEST) != 0 ? _reqfilters
: _incfilters;
if (filters.isEmpty()) //no need to sync
return null; //optimize
for (;;) {
FastReadArray ary;
synchronized (filters) {
ary = filters.get(ext);
}
if (ary != null)
return (Filter[]) ary.toArray();
int j = ext.indexOf('.');
if (j < 0)
return null;
ext = ext.substring(j + 1);
}
}
/** Adds a filter ({@link Filter}) to perform filtering task for
* the resource of the specified extension.
*
* Unlike {@link #addExtendlet}, multiple filters can be applied to
* the same extension. The first one being added will be called first.
*
* @param ext the extension
* @param filter the filter
* @param flags a combination of {@link #FILTER_REQUEST}
* and {@link #FILTER_INCLUDE}. If 0, {@link #FILTER_REQUEST}
* is assumed.
* @since 3.5.1
*/
public void addFilter(String ext, Filter filter, int flags) {
if (ext == null || filter == null)
throw new IllegalArgumentException("null");
filter.init(new FilterConfig() {
public ExtendletContext getExtendletContext() {
return _cwc;
}
});
ext = ext.toLowerCase(java.util.Locale.ENGLISH);
if (flags == 0 || (flags & FILTER_REQUEST) != 0)
addFilter(_reqfilters, ext, filter);
if ((flags & FILTER_INCLUDE) != 0)
addFilter(_incfilters, ext, filter);
}
private static void addFilter(Map> filters, String ext, Filter filter) {
FastReadArray ary;
synchronized (filters) {
ary = filters.get(ext);
if (ary == null)
filters.put(ext, ary = new FastReadArray(Filter.class));
}
ary.add(filter);
}
/** Removes the filter ({@link Filter}) for the specified extension.
* @param flags a combination of {@link #FILTER_REQUEST}
* and {@link #FILTER_INCLUDE}. If 0, {@link #FILTER_REQUEST}
* is assumed.
* @return whether the filter has been removed successfully.
* @since 3.5.1
*/
public boolean removeFilter(String ext, Filter filter, int flags) {
if (ext == null || filter == null)
return false;
ext = ext.toLowerCase(java.util.Locale.ENGLISH);
boolean removed = false;
if (flags == 0 || (flags & FILTER_REQUEST) != 0)
removed = rmFilter(_reqfilters, ext, filter);
if ((flags & FILTER_INCLUDE) != 0)
removed = rmFilter(_incfilters, ext, filter) || removed;
return removed;
}
private static boolean rmFilter(Map> filters, String ext, Filter filter) {
FastReadArray ary;
synchronized (filters) {
ary = filters.get(ext);
}
if (ary != null && ary.remove(filter)) {
if (ary.isEmpty())
synchronized (filters) {
ary = filters.remove(ext);
if (ary != null && !ary.isEmpty())
filters.put(ext, ary); //modify by other, so restore
}
return true;
}
return false;
}
/** Sets the extension that shall be compressed if the browser
* supports the compression encoding (accept-encoding).
*
* Default: null (no compression at all).
*
* @param exts an array of extensions, e.g., {"js", "css", "html"}.
* If null or zero-length, it means no compression at all.
*@since 2.4.1
*/
public void setCompress(String[] exts) {
if (exts != null && exts.length > 0) {
final Set set = new LinkedHashSet();
for (int j = exts.length; --j >= 0;)
set.add(exts[j]);
_compressExts = set;
} else {
_compressExts = null;
}
}
/**Returns the extension that shall be compressed if the browser
* supports the compression encoding (accept-encoding).
*
* Default: null (no compression at all).
*@since 2.4.1
*/
public String[] getCompress() {
return _compressExts != null ? _compressExts.toArray(new String[_compressExts.size()]) : null;
}
/** Returns whether to debug JavaScript files.
* If true, it means the original (i.e., uncompressed) JavaScript files
* shall be loaded instead of compressed JavaScript files.
*
* @since 3.0.4
* @see #setDebugJS
*/
public boolean isDebugJS() {
return _debugJS;
}
/**Sets whether to debug JavaScript files.
*
*
Default: false.
*
*
If true is specified, it will try to load the original
* Java (i.e., uncompressed) file instead of the compressed one.
* For example, if {@link #service} is called to load abc.js,
* and {@link #isDebugJS}, then {@link #service} will try
* to load abc.src.js first. If not found, it loads ab.js instead.
*
*
If {@link #isDebugJS} is false (default),
* abc.js is always loaded.
*
* @param debug whether to debug JavaScript files.
* If true, the original JavaScript files shall be
* loaded instead of the compressed files.
* @since 3.0.4
*/
public void setDebugJS(boolean debug) {
_debugJS = debug;
}
/** Called by WebManager#setUpdateUri when WebManager is created
* by HttpSessionListener#contextInitialized
*
* @param mappingURI mapping URI excluding PATH_PREFIX.
*/
public void setMappingURI(String mappingURI) {
if (!mappingURI.startsWith("/") || mappingURI.endsWith("/"))
throw new IllegalArgumentException("mappingURI must start with /, but not ends with /");
_mappingURI = mappingURI + PATH_PREFIX;
}
//-- Work with CWC --//
/** Works with {@link CWC} to
* load resources from class path (thru this servlet).
*/
private void web(HttpServletRequest request, HttpServletResponse response, String pi)
throws ServletException, IOException {
//A trick used to enforce browser to load new version JavaScript
//How it work: client engine prefix URI with /_zv123, where
//123 is the build version that changes once reload is required
//Then, the server eliminate such prefix before locating resource
if (pi.startsWith(ZVER)) {
final int j = pi.indexOf('/', ZVER.length());
if (j >= 0)
pi = pi.substring(j);
else
log.warn("Unknown path info: " + Encodes.encodeURI(pi));
} else if (_encURLPrefix != null && pi.startsWith(_encURLPrefix)) {
final int len1 = pi.length(), len2 = _encURLPrefix.length();
if (len1 > len2 && pi.charAt(len2) == '/')
pi = pi.substring(len2);
}
final String ext = Servlets.getExtension(pi, false); //complete extension
final Filter[] filters = getFilters(ext, Servlets.isIncluded(request) ? FILTER_INCLUDE : FILTER_REQUEST);
if (filters == null) {
web0(request, response, pi, ext);
} else {
new FilterChainImpl(filters, pi, ext).doFilter(request, response);
}
}
/** Processes the request without calling filter. */
private void web0(HttpServletRequest request, HttpServletResponse response, String pi, String ext)
throws ServletException, IOException {
if (ext != null) {
//Invoke the resource processor (Extendlet)
final Extendlet extlet = getExtendlet(ext);
if (extlet != null) {
extlet.service(request, response, pi);
return;
}
}
//NOTE: Unlike DspExtendlet, the output stream is always UTF-8
//since we load the source directly.
if (!Servlets.isIncluded(request)) {
//Bug 1998613: we have to set content type to avoid Apache's bug
//Apache sets content-type automatically if not set
//but if jsessionid is part of URL, Apache thought it is text/plain
String ctype = ContentTypes.getContentType(ext);
if (ctype == null) {
ctype = ";charset=UTF-8";
} else {
final int k = ctype.indexOf(';');
if (k >= 0)
ctype = ctype.substring(0, k);
if (!ContentTypes.isBinary(ctype))
ctype += ";charset=UTF-8";
}
response.setContentType(ctype);
if (pi.indexOf('*') < 0) //not locale dependent
setClientCacheForever(response);
}
InputStream is = null;
HttpSession session = request.getSession();
byte[] data = new byte[0];
boolean resolved = false;
boolean compressed = false;
if (_debugJS) {
if ("js".equals(ext)) {
final String orgpi = Servlets.locate(_ctx, request, pi.substring(0, pi.length() - 3) + ".src.js",
_cwc.getLocator());
is = getResourceAsStream(orgpi);
if (is != null)
pi = orgpi;
} else if (ext != null && ext.endsWith("src.js") && session.getAttribute("zk$sourcemapsupported") != null) {
String cachedSource = _sourceFromSourceMap.get(pi);
if (cachedSource != null) {
data = cachedSource.getBytes();
resolved = true;
} else
log.debug("No cached source loaded. (" + pi + ")");
}
}
if (!resolved) {
if (is == null) {
final String p = Servlets.locate(_ctx, request, pi, _cwc.getLocator());
is = getResourceAsStream(p);
}
if (is == null) {
if ("js".equals(ext)) {
//Don't sendError. Reason: 1) IE waits and no onerror fired
//2) better to debug (user will tell us what went wrong)
// B65-ZK-1897 Sanitizing pi to prevent possible cross-site scripting vulnerability
data = ("(window.zk&&zk.error?zk.error:alert)('" + XMLs.encodeText(pi) + " not found');")
.getBytes("UTF-8");
//FUTURE: zweb shall not depend on zk
} else {
if (Servlets.isIncluded(request))
log.error("Resource not found: " + Encodes.encodeURI(pi));
response.sendError(HttpServletResponse.SC_NOT_FOUND, XMLs.escapeXML(pi));
return;
}
} else {
//Note: don't compress images
data = shallCompress(request, ext) ? Https.gzip(request, response, is, null) : null;
if (!(compressed = (data != null)))
data = Files.readAll(is);
//since what is embedded in the jar is not big, so load completely
Files.close(is);
}
}
int len = data.length;
response.setContentLength(len);
OutputStream out;
if (Servlets.isIncluded(request)) { //usually getWriter
try {
out = new WriterOutputStream(response.getWriter(), "UTF-8");
//Not response.getCharacterEncoding because it is for "data"
} catch (IllegalStateException ex) {
try {
out = response.getOutputStream();
} catch (Throwable t) {
log.warn("getOutputStream: failed" + Exceptions.getMessage(t));
throw ex;
}
}
} else {
try {
out = response.getOutputStream();
} catch (IllegalStateException ex) {
if (compressed)
throw ex;
try {
out = new WriterOutputStream(response.getWriter(), "UTF-8");
} catch (Throwable t) {
log.warn("getWriter: failed" + Exceptions.getMessage(t));
throw ex;
}
}
}
out.write(data);
out.flush();
}
private boolean shallCompress(ServletRequest request, String ext) {
return _compressExts != null && _compressExts.contains(ext) && !Servlets.isIncluded(request);
}
/** Sets the prefix used to encoding the URL.
* The prefix will be encoded when {@link ExtendletContext#encodeURL}
* is called. The prefix is usually the build number, such that browser
* won't use the wrong cached version
*
*
Default: null (no special encoding).
*
* @param prefix the prefix used to encode URL for the extendlet.
* If prefix is not empty and doesn't start with '/', it will
* be prefixed with '/'.
* @since 3.6.3
*/
public void setEncodeURLPrefix(String prefix) {
if (prefix == null || prefix.length() == 0)
prefix = null;
else if (prefix.charAt(0) != '/')
prefix = '/' + prefix;
_encURLPrefix = prefix;
}
/** Returns the prefix used to encoding the URL, or null if no prefix.
* If an non-null prefix is returned, it must start with '/'.
* @see ExtendletContext#encodeURL
* @since 3.6.3
*/
public String getEncodeURLPrefix() {
return _encURLPrefix;
}
/**
* An implementation of ExtendletContext to load resources from
* the class path rooted at /web.
*/
private class CWC implements ExtendletContext {
private final Locator _locator = new Locator() {
public String getDirectory() {
return null;
}
public URL getResource(String name) {
return ClassWebResource.this.getResource(name);
}
public InputStream getResourceAsStream(String name) {
return ClassWebResource.this.getResourceAsStream(name);
//Note: it doesn't handle _debugJS
}
};
/** Returns the associated class web resource. */
public ClassWebResource getClassWebResource() {
return ClassWebResource.this;
}
//-- ExtendletContext --//
public ServletContext getServletContext() {
return _ctx;
}
public Locator getLocator() {
return _locator;
}
public boolean shallCompress(ServletRequest request, String ext) {
return ClassWebResource.this.shallCompress(request, ext);
}
public String encodeURL(ServletRequest request, ServletResponse response, String uri)
throws ServletException, UnsupportedEncodingException {
uri = Servlets.locate(_ctx, request, uri, getLocator()); //resolves "*"
uri = (_encURLPrefix != null ? _mappingURI + _encURLPrefix : _mappingURI) + uri; //prefix with mapping
//prefix context path
if (request instanceof HttpServletRequest) {
String ctxpath = Https.getThisContextPath(request);
if (ctxpath == null)
throw new NullPointerException(
"HttpServletRequest#getContentPath() returns a null value from [ " + request + " ]");
final int ctxlen = ctxpath.length();
if (ctxlen > 0) {
final char cc = ctxpath.charAt(0);
if (cc != '/')
ctxpath = '/' + ctxpath;
//Work around a bug for Pluto's RenderRequest (1.0.1)
else if (ctxlen == 1)
ctxpath = ""; // "/" => ""
//Work around liferay's issue: Upload 1627928 (not verified)
}
uri = ctxpath + uri;
}
int j = uri.indexOf('?');
if (j < 0) {
uri = Encodes.encodeURI(uri);
} else {
uri = Encodes.encodeURI(uri.substring(0, j)) + uri.substring(j);
}
//encode
if (response instanceof HttpServletResponse)
uri = ((HttpServletResponse) response).encodeURL(uri);
return uri;
}
public String encodeRedirectURL(HttpServletRequest request, HttpServletResponse response, String uri,
Map params, int mode) {
return Https.encodeRedirectURL(_ctx, request, response, _mappingURI + uri, params, mode);
}
public RequestDispatcher getRequestDispatcher(String uri) {
// if (log.isDebugEnabled()) log.debug("getRequestDispatcher: "+uri);
return _ctx.getRequestDispatcher(_mappingURI + uri);
}
public void include(HttpServletRequest request, HttpServletResponse response, String uri, Map params)
throws ServletException, IOException {
//Note: it is caller's job to convert related path to ~./
if (uri.startsWith("~./") && uri.indexOf('?') < 0 && isDirectInclude(uri)) {
Object old = request.getAttribute(Attributes.ARG);
if (params != null)
request.setAttribute(Attributes.ARG, params);
//If params=null, use the 'inherited' one (same as Servlets.include)
final String attrnm = "org.zkoss.web.servlet.include";
request.setAttribute(attrnm, Boolean.TRUE);
//so Servlets.isIncluded returns correctly
try {
service(request, response, Servlets.locate(_ctx, request, uri.substring(2), _cwc.getLocator()));
} finally {
request.removeAttribute(attrnm);
request.setAttribute(Attributes.ARG, old);
}
} else {
Servlets.include(_ctx, request, response, uri, params, Servlets.PASS_THRU_ATTR);
}
}
/** Returns whether the page can be directly included.
*/
private boolean isDirectInclude(String path) {
final String ext = Servlets.getExtension(path, false);
final Extendlet extlet = ext != null ? getExtendlet(ext) : null;
if (extlet != null) {
try {
return extlet.getFeature(Extendlet.ALLOW_DIRECT_INCLUDE);
} catch (Throwable ex) { //backward compatibility
}
}
return true;
}
//Note: Due to the source map since 8.5.0, it doesn't handle _debugJS
public URL getResource(String uri) {
return ClassWebResource.this.getResource(uri);
}
//Note: Due to the source map since 8.5.0, it doesn't handle _debugJS
public InputStream getResourceAsStream(String uri) {
return ClassWebResource.this.getResourceAsStream(uri);
}
}
private class FilterChainImpl implements FilterChain {
private final Filter[] _filters;
private final String _pi, _ext;
/** Which filter to process. */
private int _j;
private FilterChainImpl(Filter[] filters, String pi, String ext) throws ServletException, IOException {
_pi = pi;
_filters = filters;
_ext = ext;
}
public void doFilter(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (_j > _filters.length)
throw new IllegalStateException("Out of bound: " + _j + ", filter=" + Objects.toString(_filters));
final int j = _j++;
if (j == _filters.length) {
web0(request, response, _pi, _ext);
} else {
_filters[j].doFilter(request, response, _pi, this);
}
}
}
/**
* Internal used only.
*
* @since 9.6.0
*/
public ConcurrentMap initSourceCache(int size) {
_sourceFromSourceMap = new ConcurrentHashMap<>(size);
return _sourceFromSourceMap;
}
}