org.zkoss.web.servlet.Servlets Maven / Gradle / Ivy
/* Servlets.java
Purpose:
Description:
History:
90/12/10 22:24:28, Create, Tom M. Yeh.
Copyright (C) 2001 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.servlet;
import static org.zkoss.lang.Generics.cast;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.idom.Element;
import org.zkoss.idom.input.SAXBuilder;
import org.zkoss.lang.SystemException;
import org.zkoss.util.CacheMap;
import org.zkoss.util.Checksums;
import org.zkoss.util.Locales;
import org.zkoss.util.resource.Locator;
import org.zkoss.util.resource.Locators;
import org.zkoss.web.Attributes;
import org.zkoss.web.servlet.http.Encodes;
import org.zkoss.web.util.resource.ExtendletContext;
import org.zkoss.web.util.resource.ServletContextLocator;
/**
* The servlet relevant utilities.
*
* @author tomyeh
* @see org.zkoss.web.servlet.http.Https
* @see org.zkoss.web.servlet.Charsets
*/
public class Servlets {
private static final Logger log = LoggerFactory.getLogger(Servlets.class);
private static ClientIdentifier _clientId;
private static final Pattern _rwebkit = Pattern.compile(".*(webkit)[ /]([\\w.]+).*"),
_ropera = Pattern.compile(".*(opera)(?:.*version)?[ /]([\\w.]+).*"),
_rmsie = Pattern.compile(".*(msie) ([\\w.]+).*"),
_rmozilla = Pattern.compile(".*(mozilla)(?:.*? rv:([\\w.]+))?.*"),
_rchrome = Pattern.compile(".*(chrome)[ /]([\\w.]+).*"),
_randroid = Pattern.compile(".*(android)[ /]([\\w.]+).*"),
_redge = Pattern.compile(".*(edg)[ /]([\\w.]+).*"),
_redgeLegacy = Pattern.compile(".*(edge)[ /]([\\w.]+).*"),
_rsafari = Pattern.compile(".*(safari)[ /]([\\w.]+).*"), _rtrident = Pattern.compile("trident/([0-9\\.]+)");
private static final boolean _svl24, _svl23, _svl3;
static {
boolean b = false;
try {
HttpSession.class.forName("javax.servlet.annotation.WebServlet");
b = true;
} catch (Throwable ex) {
// expected
}
_svl3 = b;
if (!b) {
try {
ServletResponse.class.getMethod("getContentType", new Class[0]);
b = true;
} catch (Throwable ex) {
// expected
}
}
_svl24 = b;
if (!b) {
try {
HttpSession.class.getMethod("getServletContext", new Class[0]);
b = true;
} catch (Throwable ex) {
// expected
}
}
_svl23 = b;
}
/** Utilities; no instantiation required. */
protected Servlets() {
}
/** Returns whether a URL starts with xxx://, mailto:, about:,
* javascript:, data:, //
*/
public static final boolean isUniversalURL(String uri) {
if (uri == null || uri.length() == 0)
return false;
final char cc = uri.charAt(0);
return cc >= 'a' && cc <= 'z' && (uri.indexOf("://") > 0 || uri.startsWith("mailto:")
|| uri.startsWith("javascript:") || uri.startsWith("about:") || uri.startsWith("data:")) || uri.startsWith("//");
}
/** Returns whether the current Web server supports Servlet 3.0 or above.
*
* @since 6.0.0
*/
public static final boolean isServlet3() {
return _svl3;
}
/** Returns whether the current Web server supports Servlet 2.4 or above.
*
* @since 3.0.0
*/
public static final boolean isServlet24() {
return _svl24;
}
/** Returns whether the current Web server supports Servlet 2.3 or above.
* Thus, if {@link #isServlet24} returns true, {@link #isServlet23}
* must return true, too.
*
* @since 3.0.0
*/
public static final boolean isServlet23() {
return _svl23;
}
//-- resource locator --//
/** Locates a page based on the current Locale. It never returns null.
*
* Notice that it cannot resolve a path starting with '~', and containing
* '*', because it cannot access the content of the other servlet context.
*
*
If an URI contains "*", it will be replaced with a proper Locale.
* For example, if the current Locale is zh_TW and the resource is
* named "ab*.cd", then it searches "ab_zh_TW.cd", "ab_zh.cd" and
* then "ab.cd", until any of them is found.
*
*
Note: "*" must be right before ".", or the last character.
* For example, "ab*.cd" and "ab*" are both correct, while
* "ab*cd" and "ab*\/cd" are ignored.
*
* If an URI contains two "*", the first "*" will be replaced with
* a browser code and the second with a proper locale.
* The browser code depends on what browser
* the user are used to visit the web site.
* Currently, the code for Internet Explorer is "ie", Safari is "saf",
* Opera is "opr" and all others are "moz".
* Thus, in the above example, if the resource is named "ab**.cd"
* and Firefox is used, then it searches "abmoz_zh_TW.cd", "abmoz_zh.cd"
* and then "abmoz.cd", until any of them is found.
*
*
Note: it assumes the path as name_lang_cn_var.ext where
* ".ext" is optional. Example, my_zh_tw.html.jsp.
*
* @param ctx the servlet context to locate pages
* @param pgpath the page path excluding servlet name. It is OK to have
* the query string. It might contain "*" for current browser code and Locale.
* @param locator the locator used to locate resource. If null, ctx
* is assumed.
* @return the path that matches the wildcard; pgpath
, otherwise
* never null
* @see Locales#getCurrent
*/
@SuppressWarnings("unchecked")
public static final String locate(ServletContext ctx, ServletRequest request, String pgpath, Locator locator)
throws ServletException {
if (pgpath == null)
return pgpath;
final int f = pgpath.indexOf('*');
if (f < 0 || isUniversalURL(pgpath))
return pgpath;
final int jquest = pgpath.indexOf('?');
if (jquest >= 0 && f > jquest)
return pgpath;
//optimize the case that no "*" at all
final String qstr;
if (jquest >= 0) {
qstr = pgpath.substring(jquest);
pgpath = pgpath.substring(0, jquest);
} else {
qstr = null;
}
//by browser?
int l = pgpath.lastIndexOf('*');
if (l > f) { //two '*'
final String bc = Servlets.isExplorer(request) ? "ie"
: Servlets.isSafari(request) ? "saf" : Servlets.isOpera(request) ? "opr" : "moz";
l += bc.length() - 1;
pgpath = pgpath.substring(0, f) + bc + pgpath.substring(f + 1);
}
//remove "*"
pgpath = pgpath.substring(0, l) + pgpath.substring(l + 1); //remove
//by locale? 1) before the first dot, 2) the last char if no dot
boolean byLocale = l == pgpath.length() || (pgpath.charAt(l) == '.' && pgpath.indexOf('/', l + 1) < 0);
if (byLocale) {
//make sure no dot before it
for (int j = l; --j >= 0;) {
final char cc = pgpath.charAt(j);
if (cc == '.') {
byLocale = false;
break;
} else if (cc == '/') {
break;
}
}
}
if (!byLocale)
return qstr != null ? pgpath + qstr : pgpath; //not by locale
final String PGPATH_CACHE = "org.zkoss.web.pgpath.cache";
Map map = (Map) ctx.getAttribute(PGPATH_CACHE);
if (map == null) {
map = Collections.synchronizedMap( //10 min
new CacheMap(500, 10 * 60 * 1000));
ctx.setAttribute(PGPATH_CACHE, map);
}
final Locale locale = Locales.getCurrent();
final URIIndex index = new URIIndex(pgpath, locale);
String uri = map.get(index);
if (uri == null) {
final Locators.URLLocation loc = Locators.locate(pgpath, locale,
locator != null ? locator : new ServletContextLocator(ctx));
uri = loc != null ? loc.file : pgpath;
map.put(index, uri);
}
return qstr != null ? uri + qstr : uri;
}
private static class URIIndex {
private final String _uri;
private final Locale _locale;
private URIIndex(String uri, Locale locale) {
if (uri == null || locale == null)
throw new IllegalArgumentException("null");
_uri = uri;
_locale = locale;
}
public int hashCode() {
return _uri.hashCode();
}
public boolean equals(Object o) {
if (this == o)
return true;
//To speed up, don't check whether o is the right class
final URIIndex idx = (URIIndex) o;
return _uri.equals(idx._uri) && _locale.equals(idx._locale);
}
}
/** Sets the client identifier that is used to assist {@link #isBrowser}
* to identify a client.
*
* Notice that the client identifier must be thread-safe.
*
* @param clientId the client identifier. If null, only the default types
* are recognized.
* @see ClientIdentifier
* @since 6.0.0
*/
public static void setClientIdentifier(ClientIdentifier clientId) {
_clientId = clientId;
}
/** Returns the client identifier, or null if no such plugin.
* @see ClientIdentifier
* @since 6.0.0
*/
public static ClientIdentifier getClientIdentifier() {
return _clientId;
}
/** Returns the version of the given browser name, or null if the client
* is not the given browsers.
*
*
Notice that, after this method is called, an attribute named zk
* will be stored to the request, such that you can retrieve
* the browser information by use of EL, such as
* ${zk.ie > 7}
.
*
* @param request the request.
* @param name the browser's name. It includes "ie", "ff", "gecko",
* "webkit", "safari", "opera", "android", "mobile", "ios", "iphone", "ipad"
* and "ipod".
* And, "ff" is the same as "gecko", and "webkit" is the same as "safari".
* @since 6.0.0
*/
public static Double getBrowser(ServletRequest request, String name) {
return (Double) browserInfo(request).get(name);
}
/** Returns the name of the browser, or null if not identifiable.
* @since 6.0.0
*/
public static String getBrowser(ServletRequest request) {
return (String) ((Map) browserInfo(request).get("browser")).get("name");
}
/** Returns the version of the given browser name, or null if the client
* is not the given browsers.
*
*
Notice that, after this method is called, an attribute named zk
* will be stored to the request, such that you can retrieve
* the browser information by use of EL, such as
* ${zk.ie > 7}
.
*
* @param userAgent the user agent (i.e., the user-agent header in HTTP).
* @param name the browser's name. It includes "ie", "ff", "gecko",
* "webkit", "safari", "opera", "android", "mobile", "ios", "iphone", "ipad"
* and "ipod".
* And, "ff" is the same as "gecko", and "webit" is the same as "safari".
* @since 6.0.0
*/
public static Double getBrowser(String userAgent, String name) {
final Map zk = new HashMap();
browserInfo(zk, userAgent);
return (Double) zk.get(name);
}
/** Returns the name of the browser, or null if not identifiable.
* @param userAgent the user agent (i.e., the user-agent header in HTTP).
* @since 6.0.0
*/
public static String getBrowser(String userAgent) {
final Map zk = new HashMap();
browserInfo(zk, userAgent);
return (String) ((Map) zk.get("browser")).get("name");
}
private static Map browserInfo(ServletRequest request) {
//No need synchronized since the servlet request is executed synchronously
Map zk = cast((Map) request.getAttribute("zk"));
if (zk == null)
request.setAttribute("zk", zk = new HashMap(4));
if (!zk.containsKey("browser"))
browserInfo(zk, getUserAgent(request));
return zk;
}
private static void browserInfo(Map zk, String ua) {
if (ua != null) {
// ZK-1822: In locale Turkish, it can prevent 'I'.toLowerCase becomes 'i' without dot.
ua = ua.toLowerCase(Locale.ENGLISH);
if (_clientId != null) {
final ClientIdentifier ci = _clientId.matches(ua);
if (ci != null) {
browserInfo(zk, ci.getName(), ci.getVersion());
return;
}
}
Matcher m = _randroid.matcher(ua);
if (m.matches()) {
double v = getVersion(m);
zk.put("android", v);
zk.put("mobile", v);
}
m = _rwebkit.matcher(ua);
if (m.matches()) {
double version;
browserInfo(zk, "webkit", version = getVersion(m));
//B70-ZK-2088: make sure Browser is not chrome
boolean matchChrome = false;
m = _rchrome.matcher(ua);
if (m.matches()) {
zk.put("chrome", getVersion(m));
matchChrome = true;
}
m = _rsafari.matcher(ua);
if (!matchChrome && m.matches())
zk.put("safari", getVersion(m));
m = _redge.matcher(ua);
if (m.matches()) {
zk.put("edge", getVersion(m));
}
m = _redgeLegacy.matcher(ua);
if (m.matches()) {
zk.put("edge_legacy", getVersion(m));
}
for (int j = _ios.length; --j >= 0;)
if (ua.indexOf(_ios[j]) >= 0) {
zk.put(_ios[j], version);
zk.put("ios", version);
zk.put("mobile", version);
return;
}
return;
}
m = _ropera.matcher(ua);
if (m.matches()) {
browserInfo(zk, "opera", getVersion(m));
return;
}
m = _rmsie.matcher(ua);
if (m.matches()) {
browserInfo(zk, "ie", getVersion(m));
return;
}
if (ua.indexOf("compatible") < 0) {
m = _rmozilla.matcher(ua);
if (m.matches()) {
double version = getVersion(m);
if (version < 5) { //http://www.useragentstring.com/_uas_Firefox_version_5.0.php
int j = ua.indexOf("firefox/");
if (j >= 0) {
int k = ua.indexOf('.', j += 8);
if (k >= 0) {
for (int len = ua.length(); ++k < len;) {
final char cc = ua.charAt(k);
if (cc < '0' || cc > '9')
break;
}
try {
version = Double.parseDouble(ua.substring(j, k));
} catch (Throwable ex) {
// expected
}
}
}
}
// ie11~
if (ua.contains("trident")) {
browserInfo(zk, "ie", version);
return;
}
//the version after gecko/* is confusing, so we
//use firefox's version instead
browserInfo(zk, "gecko", version);
zk.put("ff", version);
return;
}
}
}
zk.put("browser", Collections.emptyMap()); //none matched
}
private static final String[] _ios = { "ipod", "iphone", "ipad" };
private static void browserInfo(Map zk, String name, double version) {
final Map bi = new HashMap(4);
bi.put("name", name);
bi.put("version", version);
zk.put("browser", bi);
zk.put(name, version);
}
private static double getVersion(Matcher m) {
return m.groupCount() < 2 ? 1/*ignore it*/ : getVersion(m.group(2));
}
private static double getVersion(String version) {
try {
int j = version.indexOf('.');
if (j >= 0) {
j = version.indexOf('.', j + 1);
if (j >= 0)
version = version.substring(0, j);
}
return Double.parseDouble(version);
} catch (Throwable t) {
return 1; //ignore it
}
}
/**
* Returns whether the client is a browser of the specified type.
* To more accurately specify the browser, please use {@link #getUserAgent(ServletRequest)} instead.
* @param type the type of the browser.
* The syntax: <browser-name>[<version-number>];[-]
.
* For example, ie9, ios and ie6-.
* And, ie9
means Internet Explorer 9 and later, while
* ie6-
means Internet Explorer 6 (not prior, nor later).
* @since 3.5.1
*/
public static boolean isBrowser(ServletRequest req, String type) {
return (req instanceof HttpServletRequest) && isBrowser(getUserAgent(req), type);
}
/** Returns whether the user agent is a browser of the specified type.
* To more accurately specify the browser, please use {@link #getUserAgent(ServletRequest)} instead.
* @param userAgent represents a client.
* For HTTP clients, It is the user-agent header.
* @param type the type of the browser, or a list of types separated by comma.
* The syntax: <browser-name>[<version-number>];[-]
.
* For example, ie9, ios and ie6-.
* And, ie9
means Internet Explorer 9 and later, while
* ie6-
means Internet Explorer 6 (not prior, nor later).
* If a list of types are specified (such as ie6-,ie7-
),
* this method returns true if any of them matches (i.e., OR condition).
* @since 3.5.1
*/
public static boolean isBrowser(String userAgent, String type) {
String[] types = type.split(",");
for (int j = 0; j < types.length; ++j)
if (browser(userAgent, types[j]))
return true; //OR
return false;
}
private static boolean browser(String userAgent, String type) {
if (userAgent == null) //Bug ZK-1582: userAgent could be null if it is robot.
return false;
int last = (type = type.trim()).length();
if (last == 0)
return false;
char cc = type.charAt(last - 1);
final boolean equals = cc == '-' || cc == '_';
if (equals || cc == '+')
last--;
int j;
for (j = last; j > 0; --j)
if ((cc = type.charAt(j - 1)) != '.' && (cc < '0' || cc > '9'))
break;
Double vtype = null;
if (j < last) {
try {
vtype = Double.parseDouble(type.substring(j, last));
} catch (Throwable t) {
// expected
}
last = j;
}
String btype = type.substring(0, last);
Double vclient = getBrowser(userAgent, btype);
if (vclient == null && (userAgent.indexOf(btype + ("ie".equals(btype) ? ' ' : "")) < 0)) //Bug ZK-1289: for Viewpad and IE mixed bug
return false; //not matched
if (vtype == null)
return true; //not care about version
if (vclient == null)
return false; //not matched for Bug ZK-1930
double v1 = vclient.doubleValue(), v2 = vtype.doubleValue();
return equals ? v1 == v2 : v1 >= v2;
}
/** Returns whether the client is a robot (such as Web crawlers).
*
* Because there are too many robots, it returns true if the user-agent
* is not recognized.
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isRobot(ServletRequest req) {
return (req instanceof HttpServletRequest) && isRobot(getUserAgent(req));
}
/** Returns whether the client is a robot (such as Web crawlers).
*
*
Because there are too many robots, it returns true if the user-agent
* is not recognized.
*
* @param userAgent represents a client.
* For HTTP clients, It is the user-agent header.
* @since 3.5.1
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isRobot(String userAgent) {
if (userAgent == null)
return false;
boolean ie = userAgent.indexOf("MSIE ") >= 0;
//Bug 3107026: in Turkish, "MSIE".toLowerCase(java.util.Locale.ENGLISH) is NOT "msie"
userAgent = userAgent.toLowerCase(java.util.Locale.ENGLISH);
return !ie && userAgent.indexOf("msie ") < 0 && userAgent.indexOf("opera") < 0
&& userAgent.indexOf("gecko/") < 0 && userAgent.indexOf("safari") < 0 && userAgent.indexOf("zk") < 0
&& userAgent.indexOf("rmil") < 0;
}
/** Returns whether the browser is Internet Explorer.
* If true, it also implies {@link #isExplorer7} is true.
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isExplorer(ServletRequest req) {
return (req instanceof HttpServletRequest) && isExplorer(getUserAgent(req));
}
/** Returns whether the browser is Internet Explorer.
* If true, it also implies {@link #isExplorer7} is true.
*
* @param userAgent represents a client.
* For HTTP clients, It is the user-agent header.
* @since 3.5.1
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isExplorer(String userAgent) {
if (userAgent == null)
return false;
boolean ie = userAgent.indexOf("MSIE ") >= 0;
//Bug 3107026: in Turkish, "MSIE".toLowerCase(java.util.Locale.ENGLISH) is NOT "msie"
userAgent = userAgent.toLowerCase(java.util.Locale.ENGLISH);
return (ie || userAgent.indexOf("msie ") >= 0) && userAgent.indexOf("opera") < 0;
}
/** Returns whether the browser is Explorer 7 or later.
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isExplorer7(ServletRequest req) {
return (req instanceof HttpServletRequest) && isExplorer7(getUserAgent(req));
}
/** Returns whether the browser is Explorer 7 or later.
*
* @param userAgent represents a client.
* For HTTP clients, It is the user-agent header.
* @since 3.5.1
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isExplorer7(String userAgent) {
return isBrowser(userAgent, "ie7");
}
/** Returns whether the browser is Gecko based, such as Mozilla, Firefox and Camino
* If true, it also implies {@link #isGecko3} is true.
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isGecko(ServletRequest req) {
return (req instanceof HttpServletRequest) && isGecko(getUserAgent(req));
}
/** Returns whether the browser is Gecko based, such as Mozilla, Firefox and Camino
* If true, it also implies {@link #isGecko3} is true.
*
* @param userAgent represents a client.
* For HTTP clients, It is the user-agent header.
* @since 3.5.1
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isGecko(String userAgent) {
if (userAgent == null)
return false;
userAgent = userAgent.toLowerCase(java.util.Locale.ENGLISH);
return userAgent.indexOf("gecko/") >= 0 && userAgent.indexOf("safari") < 0;
}
/** Returns whether the browser is Gecko 3 based, such as Firefox 3.
* @since 3.5.0
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isGecko3(ServletRequest req) {
return (req instanceof HttpServletRequest) && isGecko3(getUserAgent(req));
}
/** Returns whether the browser is Gecko 3 based, such as Firefox 3.
*
* @param userAgent represents a client.
* For HTTP clients, It is the user-agent header.
* @since 3.5.1
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isGecko3(String userAgent) {
return isBrowser(userAgent, "gecko3");
}
/** Returns whether the browser is Safari.
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isSafari(ServletRequest req) {
return (req instanceof HttpServletRequest) && isSafari(getUserAgent(req));
}
/** Returns whether the browser is Safari.
*
* @param userAgent represents a client.
* For HTTP clients, It is the user-agent header.
* @since 3.5.1
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isSafari(String userAgent) {
if (userAgent == null)
return false;
userAgent = userAgent.toLowerCase(java.util.Locale.ENGLISH);
return userAgent.indexOf("safari") >= 0;
}
/** Returns whether the browser is Opera.
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isOpera(ServletRequest req) {
return (req instanceof HttpServletRequest) && isOpera(getUserAgent(req));
}
/** Returns whether the browser is Opera.
*
* @param userAgent represents a client.
* For HTTP clients, It is the user-agent header.
* @since 3.5.1
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isOpera(String userAgent) {
if (userAgent == null)
return false;
userAgent = userAgent.toLowerCase(java.util.Locale.ENGLISH);
return userAgent.indexOf("opera") >= 0;
}
/** Returns whether the client is a mobile device supporting HIL
* (Handset Interactive Language).
* For example, ZK Mobile for Android.
*
* @since 3.0.2
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isHilDevice(ServletRequest req) {
return (req instanceof HttpServletRequest) && isHilDevice(getUserAgent(req));
}
/** Returns whether the client is a mobile device supporting HIL
* (Handset Interactive Language).
* For example, ZK Mobile for Android.
*
* @param userAgent represents a client.
* For HTTP clients, It is the user-agent header.
* @since 3.5.1
* @deprecated As of release 6.0.0, replaced with {@link #getBrowser}.
*/
public static final boolean isHilDevice(String userAgent) {
if (userAgent == null)
return false;
//ZK Mobile for Android 1.0 (RMIL; RHIL)
userAgent = userAgent.toLowerCase(java.util.Locale.ENGLISH);
return userAgent.indexOf("zk") >= 0 && userAgent.indexOf("rhil") >= 0;
}
/**
* Returns the IE compatibility information.
* @param request
* @return three double values in array [ie version, trident version, ie real version]
* @since 6.5.5
*/
public static double[] getIECompatibilityInfo(ServletRequest request) {
final String s = ((HttpServletRequest) request).getHeader("user-agent");
if (isBrowser(s, "ie")) {
final String ua = s.toLowerCase(Locale.ENGLISH);
Matcher tridentMatcher = _rtrident.matcher(ua);
if (tridentMatcher.find()) {
double tridentVersion = Double.parseDouble(tridentMatcher.group(1));
Matcher msie = _rmsie.matcher(ua);
Matcher ie = _rmozilla.matcher(ua); // ie11~
Matcher ieMatcher = msie.matches() ? msie : (ie.matches() ? ie : null);
if (ieMatcher != null) {
double ieVersion = getVersion(ieMatcher);
double ieVersionReal = tridentVersion + 4.0;
return new double[] { ieVersion, tridentVersion, ieVersionReal };
}
}
}
return null;
}
/** Returns the user-agent header, which indicates what the client is,
* or an empty string if not available.
*
*
Note: it doesn't return null, so it is easy to test what
* the client is with {@link String#indexOf}.
*
* @since 3.0.2
*/
public static final String getUserAgent(ServletRequest req) {
if (req instanceof HttpServletRequest) {
String s = ((HttpServletRequest) req).getHeader("user-agent");
if (s != null) {
String cache = (String) req.getAttribute("$$zkagent$$");
if (cache != null)
return cache;
double[] ieCompatibilityInfo = getIECompatibilityInfo(req);
if (ieCompatibilityInfo != null) {
s += "; MSIE " + ieCompatibilityInfo[2];
}
req.setAttribute("$$zkagent$$", s);
return s;
}
}
return "";
}
/**
* Tests whether this page is included by another page.
*/
public static final boolean isIncluded(ServletRequest request) {
return request.getAttribute(Attributes.INCLUDE_CONTEXT_PATH) != null
|| request.getAttribute("org.zkoss.web.servlet.include") != null;
//org.zkoss.web.servlet.include is used by ZK (or others)
//to 'simulate' inclusion
}
/**
* Tests whether this page is forwarded by another page.
*/
public static final boolean isForwarded(ServletRequest request) {
return request.getAttribute(Attributes.FORWARD_CONTEXT_PATH) != null
|| request.getAttribute("org.zkoss.web.servlet.forward") != null;
}
/**
* Forward to the specified URI.
* It enhances RequestDispatcher in the following ways.
*
*
* - It handles "~ctx"" where ctx is the context path of the
* foreign context. It is called foreign URI.
* - It detects whether the page calling this method
* is included by another servlet/page. If so, it uses
* RequestDispatcher.include() instead of RequestDispatcher.forward().
* - The forwarded page could accept additional parameters --
* actually converting parameters to a query string
* and appending it to uri.
* - In additions, it does HTTP encoding, i.e., converts '+' and other
* characters to comply HTTP.
*
*
* NOTE: don't include query parameters in uri.
*
* @param ctx the servlet context. If null, uri cannot be foreign URI.
* It is ignored if URI is relevant (neither starts with '/' nor '~').
* @param uri the URI to include. It is OK to relevant (without leading
* '/'). If starts with "/", the context path of request is assumed.
* To reference to foreign context, use "~ctx" where ctx is the
* context path of the foreign context (without leading '/').
* If it could be any context path recognized by the Web container or
* any name registered with {@link #addExtendletContext}.
*
Notice that, since 3.6.3, uri
could contain
* '*' (to denote locale and browser). Refer to {@link #locate}.
* @param params the parameter map; null to ignore
* @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM},
* and {@link #APPEND_PARAM}. It defines how to handle if both uri
* and params contains the same parameter.
*/
public static final void forward(ServletContext ctx, ServletRequest request, ServletResponse response, String uri,
Map params, int mode) throws IOException, ServletException {
// if (log.isDebugEnabled()) log.debug("Forwarding "+uri);
//include or foward depending whether this page is included or not
if (isIncluded(request)) {
include(ctx, request, response, uri, params, mode);
return;
}
uri = locate(ctx, request, uri, null);
final RequestDispatcher disp = getRequestDispatcher(ctx, request, uri, params, mode);
if (disp == null)
throw new ServletException("No dispatcher available to forward to " + uri);
if (mode == PASS_THRU_ATTR && params != null && !params.isEmpty()) {
final Map old = setPassThruAttr(request, params);
try {
disp.forward(request, response);
} catch (ClassCastException ex) {
//Tom M. Yeh, 2006/09/21: Bug 1548478
//Cause: http://issues.apache.org/bugzilla/show_bug.cgi?id=39417
//
//Bug or limitation of Catalina: not accepting HttpServletRequest
//othere than the original one or wrapper of original one
//
//Real Cause: org.apache.catalina.core.ApplicationDispatcher
//call unwrapRequest() twice, and then causes ClassCastException
//
//Resolution: since it is the almost last statement, it is safe
//to ignore this exception
if (!(request instanceof org.zkoss.web.portlet.RenderHttpServletRequest))
throw ex; //not the case described above
} finally {
restorePassThruAttr(request, old);
}
} else {
disp.forward(request, response);
}
}
/** A shortcut of forward(request, response, uri, null, 0).
*/
public static final void forward(ServletContext ctx, ServletRequest request, ServletResponse response, String uri)
throws IOException, ServletException {
forward(ctx, request, response, uri, null, 0);
}
/**
* Includes the resource at the specified URI.
* It enhances RequestDispatcher to allow the inclusion with
* a parameter map -- acutually converting parameters to a query string
* and appending it to uri.
*
*
NOTE: don't include query parameters in uri.
*
* @param ctx the servlet context. If null, uri cannot be foreign URI.
* It is ignored if URI is relevant (neither starts with '/' nor '~').
* @param uri the URI to include. It is OK to relevant (without leading
* '/'). If starts with "/", the context path of request is assumed.
* To reference to foreign context, use "~ctx/" where ctx is the
* context path of the foreign context (without leading '/').
*
Notice that, since 3.6.3, uri
could contain
* '*' (to denote locale and browser). Refer to {@link #locate}.
* @param params the parameter map; null to ignore
* @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM},
* and {@link #APPEND_PARAM}. It defines how to handle if both uri
* and params contains the same parameter.
*/
public static final void include(ServletContext ctx, ServletRequest request, ServletResponse response, String uri,
Map params, int mode) throws IOException, ServletException {
// if (log.isDebugEnabled()) log.debug("Including "+uri+" at "+ctx);
//Note: we don't optimize the include to call ClassWebResource here
//since 1) it is too low level (might have some risk)
//2) no clean way to access ClassWebResouce here
//20050606: Tom Yeh
//We have to set this special attribute for jetty
//Otherwise, if including a page crossing context might not return
//the same session
request.setAttribute("org.mortbay.jetty.servlet.Dispatcher.shared_session", Boolean.TRUE);
uri = locate(ctx, request, uri, null);
final RequestDispatcher disp = getRequestDispatcher(ctx, request, uri, params, mode);
if (disp == null)
throw new ServletException("No dispatcher available to include " + uri);
if (mode == PASS_THRU_ATTR && params != null && !params.isEmpty()) {
final Map old = setPassThruAttr(request, params);
try {
disp.include(request, response);
} finally {
restorePassThruAttr(request, old);
}
} else {
disp.include(request, response);
}
}
/** A shortcut of include(request, response, uri, null, 0).
*/
public static final void include(ServletContext ctx, ServletRequest request, ServletResponse response, String uri)
throws IOException, ServletException {
include(ctx, request, response, uri, null, 0);
}
/** Sets the arg attribute to pass parameters thru request's attribute.
*/
private static final Map setPassThruAttr(ServletRequest request, Map params) {
final Map old = (Map) request.getAttribute(Attributes.ARG);
request.setAttribute(Attributes.ARG, params);
return old;
}
/** Restores what has been done by {@link #setPassThruAttr}.
*/
private static final void restorePassThruAttr(ServletRequest request, Map old) {
if (old != null)
request.setAttribute(Attributes.ARG, old);
else
request.removeAttribute(Attributes.ARG);
}
/** Returns the request dispatch of the specified URI.
*
* @param ctx the servlet context. If null, uri cannot be foreign URI.
* It is ignored if uri is relevant (neither starts with '/' nor '~').
* @param request the request. If null, uri cannot be relevant.
* It is used only if uri is relevant.
* @param uri the URI to include. It is OK to relevant (without leading
* '/'). If starts with "/", the context path of request is assumed.
* To reference to foreign context, use "~ctx/" where ctx is the
* context path of the foreign context (without leading '/').
* @param params the parameter map; null to ignore
* @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM},
* and {@link #APPEND_PARAM}. It defines how to handle if both uri
* and params contains the same parameter.
*/
public static final RequestDispatcher getRequestDispatcher(ServletContext ctx, ServletRequest request, String uri,
Map params, int mode) throws ServletException {
final char cc = uri.length() > 0 ? uri.charAt(0) : (char) 0;
if (ctx == null || (cc != '/' && cc != '~')) { //... or relevant
if (request == null)
throw new IllegalArgumentException(ctx == null ? "Servlet context and request cannot be both null"
: "Request is required to use revalant URI: " + uri);
if (cc == '~')
throw new IllegalArgumentException("Servlet context is required to use foreign URI: " + uri);
uri = generateURI(uri, params, mode);
return request.getRequestDispatcher(uri);
}
//NO NEED to encodeURL since it is forward/include
return new ParsedURI(ctx, uri).getRequestDispatcher(params, mode);
}
/** Returns the resource of the specified uri.
* Unlike ServletContext.getResource, it handles "~" like
* {@link #getRequestDispatcher} did.
*
Since 5.0.7, file://, http://, https:// and ftp:// are supported.
*/
public static final URL getResource(ServletContext ctx, String uri) throws UnsupportedEncodingException {
try {
if (uri != null && uri.toLowerCase(java.util.Locale.ENGLISH).startsWith("file://")) {
final File file = new File(new URI(uri));
return file.exists() ? file.toURI().toURL() : null;
//spec: return null if not found
}
URL url = toURL(uri);
if (url != null)
return url; //unfortunately, we cannot detect if it exists
return new ParsedURI(ctx, uri).getResource();
} catch (Throwable ex) {
log.warn("Ignored: failed to load " + Encodes.encodeURI(uri), ex);
return null; //spec: return null if not found
}
}
/** Returns the resource stream of the specified uri.
* Unlike ServletContext.getResource, it handles "~" like
* {@link #getRequestDispatcher} did.
*
Since 5.0.7, file://, http://, https:// and ftp:// are supported.
*/
public static final InputStream getResourceAsStream(ServletContext ctx, String uri) throws IOException {
try {
if (uri != null && uri.toLowerCase(java.util.Locale.ENGLISH).startsWith("file://")) {
final File file = new File(new URI(uri));
return file.exists() ? new BufferedInputStream(new FileInputStream(file)) : null;
//spec: return null if not found
}
URL url = toURL(uri);
if (url != null)
return url.openStream();
return new ParsedURI(ctx, uri).getResourceAsStream();
} catch (Throwable ex) {
log.warn("Ignored: failed to load " + Encodes.encodeURI(uri), ex);
return null; //spec: return null if not found
}
}
/** Converts URI to URL if starts with http:/, https:/ or ftp:/ */
private static URL toURL(String uri) throws MalformedURLException {
String s;
if (uri != null && ((s = uri.toLowerCase(java.util.Locale.ENGLISH)).startsWith("http://")
|| s.startsWith("https://") || s.startsWith("ftp://")))
return new URL(uri);
return null;
}
/** Used to resolve "~" in URI. */
private static class ParsedURI {
private ServletContext _svlctx;
private ExtendletContext _extctx;
private String _uri;
private ParsedURI(final ServletContext ctx, final String uri) {
if (uri != null && uri.length() > 0 && uri.charAt(0) == '~') { //refer to foreign context
final int j = uri.indexOf('/', 1);
final String ctxroot;
if (j >= 0) {
ctxroot = "/" + uri.substring(1, j);
_uri = uri.substring(j);
} else {
ctxroot = "/" + uri.substring(1);
_uri = "/";
}
_extctx = getExtendletContext(ctx, ctxroot.substring(1));
if (_extctx == null) {
_svlctx = ctx.getContext(ctxroot);
if (_svlctx == null)
throw new SystemException("Context not found or not visible to " + ctx + ": " + ctxroot);
}
} else {
_svlctx = ctx;
_uri = uri;
}
}
private RequestDispatcher getRequestDispatcher(Map params, int mode) {
if (_extctx == null && _svlctx == null) //not found
return null;
final String uri = generateURI(_uri, params, mode);
return _svlctx != null ? _svlctx.getRequestDispatcher(uri) : _extctx.getRequestDispatcher(uri);
}
private URL getResource() throws MalformedURLException {
return _svlctx != null ? _svlctx.getResource(_uri) : _extctx != null ? _extctx.getResource(_uri) : null;
}
private InputStream getResourceAsStream() {
return _svlctx != null ? _svlctx.getResourceAsStream(_uri)
: _extctx != null ? _extctx.getResourceAsStream(_uri) : null;
}
}
/** Whether to overwrite uri if both uri and params contain the same
* parameter.
* Used by {@link #generateURI}
*/
public static final int OVERWRITE_URI = 0;
/** Whether to ignore params if both uri and params contain the same
* parameter.
* Used by {@link #generateURI}
*/
public static final int IGNORE_PARAM = 1;
/** Whether to append params if both uri and params contain the same
* parameter. In other words, they both appear as the final query string.
* Used by {@link #generateURI}
*/
public static final int APPEND_PARAM = 2;
/** Whether the specified parameters shall be passed thru the request
* attribute called arg.
*/
public static final int PASS_THRU_ATTR = 3;
/** Generates URI by appending the parameters.
* Note: it doesn't support the ~xxx/ format.
*
* @param params the parameters to apend to the query string
* @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM},
* {@link #APPEND_PARAM} and {@link #PASS_THRU_ATTR}.
* It defines how to handle if both uri and params contains the same
* parameter.
* mode is used only if both uri contains query string and params is
* not empty.
* @see Encodes#encodeURL(ServletContext, ServletRequest, ServletResponse, String)
*/
public static final String generateURI(String uri, Map params, int mode) {
if (uri.startsWith("~"))
throw new IllegalArgumentException("~ctx not supported here: " + uri);
final int j = uri.indexOf('?');
String qstr = null;
if (j >= 0) {
qstr = uri.substring(j);
uri = uri.substring(0, j);
}
try {
uri = Encodes.encodeURI(uri);
final boolean noQstr = qstr == null;
final boolean noParams = mode == PASS_THRU_ATTR || params == null || params.isEmpty();
if (noQstr && noParams)
return uri;
if (noQstr != noParams)
mode = APPEND_PARAM;
final StringBuffer sb = new StringBuffer(80).append(uri);
if (qstr != null)
sb.append(qstr);
switch (mode) {
case IGNORE_PARAM:
//removing params that is conflict
for (final Iterator it = params.entrySet().iterator(); it.hasNext();) {
final Map.Entry me = (Map.Entry) it.next();
final String nm = (String) me.getKey();
if (Encodes.containsQuery(qstr, nm))
it.remove();
}
//flow thru
case OVERWRITE_URI:
return Encodes.setToQueryString(sb, params).toString();
case APPEND_PARAM:
return Encodes.addToQueryString(sb, params).toString();
default:
throw new IllegalArgumentException("Unknown mode: " + mode);
}
} catch (UnsupportedEncodingException ex) {
throw new SystemException(ex);
}
}
/** A list of context root paths (e.g., "/abc"). */
private static volatile List _ctxroots;
/** Returns a list of context paths (e.g., "/abc") that this application
* has. This implementation parse application.xml. For war that doesn't
* contain application.xml might have to override this method and
* parse another file to know what context being loaded.
*/
public static final List getContextPaths() {
if (_ctxroots != null)
return _ctxroots;
try {
synchronized (Servlets.class) {
if (_ctxroots == null)
_ctxroots = myGetContextPaths();
}
return _ctxroots;
} catch (Exception ex) {
throw SystemException.Aide.wrap(ex);
}
}
private static final List myGetContextPaths() throws Exception {
final String APP_XML = "/META-INF/application.xml";
final List ctxroots = new LinkedList();
final URL xmlURL = Locators.getDefault().getResource(APP_XML);
if (xmlURL == null)
throw new SystemException("File not found: " + APP_XML);
// if (log.isDebugEnabled()) log.debug("Parsing "+APP_XML);
final Element root = new SAXBuilder(false, false, true).build(xmlURL).getRootElement();
for (Element e : root.getElements("module")) {
final String ctxroot = (String) e.getContent("web/context-root");
if (ctxroot == null) {
// if (log.finerable()) log.finer("Skip non-web: "+e.getContent("java"));
continue;
}
ctxroots.add(ctxroot.startsWith("/") ? ctxroot : "/" + ctxroot);
}
// log.info("Context found: "+ctxroots);
return new ArrayList(ctxroots);
}
/** Returns a token to represent a limit-time offer.
* It is mainly used as an parameter value (mostlycalled zk_lto), and then
* you could verify whether it is expired by {@link #isOfferExpired}.
*/
public static final String getLimitTimeOffer() {
final String lto = Long.toHexString(System.currentTimeMillis());
return lto + Checksums.getChecksum(lto, "");
}
/** Returns whether a token returned by getLimitTimeOffer expired.
* @param timeout how long the office shall expire, unit: seconds.
*/
public static final boolean isOfferExpired(String lto, int timeout) {
final int len = lto != null ? lto.length() : 0;
if (len <= 1)
return true;
final char cksm = lto.charAt(len - 1);
lto = lto.substring(0, len - 1);
if (cksm != Checksums.getChecksum(lto, ""))
return true;
try {
return Long.parseLong(lto, 16) + timeout * 1000L < System.currentTimeMillis();
} catch (NumberFormatException ex) {
return true;
}
}
/** Adds an extended context.
* @return the previous extended context, if any, associated with
* the specified name.
*/
public static final ExtendletContext addExtendletContext(ServletContext ctx, String name, ExtendletContext extctx) {
if (name == null || extctx == null)
throw new IllegalArgumentException("null");
return getExtWebCtxs(ctx).put(name, extctx);
}
/** Removes an extended context of the specified name.
*/
public static final ExtendletContext removeExtendletContext(ServletContext ctx, String name) {
return getExtWebCtxs(ctx).remove(name);
}
/** Returns the extended context of the specified name.
*/
public static final ExtendletContext getExtendletContext(ServletContext ctx, String name) {
return getExtWebCtxs(ctx).get(name);
}
@SuppressWarnings("unchecked")
private static final Map getExtWebCtxs(ServletContext ctx) {
final String attr = "javax.zkoss.web.servlets.ExtendletContexts";
//such that it could be shared among portlets
synchronized (Servlets.class) { //don't use ctx because it might be a proxy (in portlet)
Map ctxs = (Map) ctx.getAttribute(attr);
if (ctxs == null)
ctx.setAttribute(attr, ctxs = Collections.synchronizedMap(new HashMap(4)));
return ctxs;
}
}
/** Returns the file/path extension of the specified path (excluding dot),
* or null if no extension at all.
*
* Note: the extension is converted to the lower case.
*
* @param path the path. If path is null, null is returned.
* @since 2.4.1
* @see #getExtension(String, boolean)
*/
public static final String getExtension(String path) {
if (path != null) {
int j = path.lastIndexOf('.');
if (j >= 0 && path.indexOf('/', j + 1) < 0)
return path.substring(j + 1).toLowerCase(java.util.Locale.ENGLISH);
//don't worry jsessionid since it is handled by container
}
return null;
}
/** Returns the file/path extension of the specified path (excluding dot),
* or null if no extension at all.
*
*
Note: the extension is converted to the lower case.
*
*
The result is the same for both {@link #getExtension(String)}
* and {@link #getExtension(String, boolean)}, if the path
* has only one dot. However, if there are more than one dot, e.g.,
* /a/b.c.d, then {@link #getExtension(String)} retrieves the last
* extension, that is, d in this example.
* On the other hand, if you invoke getExtension(path, false),
* it returns the complete extension, that is, c.d in this example.
*
* @param path the path. If path is null, null is returned.
* @param lastOnly whether to return the last portion of extensioin
* if there are two or more dots.
* In other wors, getExtension(path) is the same as
* getExtension(path, true).
* @since 3.5.1
*/
public static final String getExtension(String path, boolean lastOnly) {
if (lastOnly)
return getExtension(path);
if (path == null)
return null;
int dot = -1;
for (int j = path.length(); --j >= 0;) {
final char cc = path.charAt(j);
if (cc == '.')
dot = j;
else if (cc == '/')
break;
}
return dot >= 0 ? path.substring(dot + 1).toLowerCase(java.util.Locale.ENGLISH) : "";
}
/** Returns the request detail information.
* It is used to log the debug info.
* @since 3.0.5
*/
public static String getDetail(ServletRequest request) {
final HttpServletRequest hreq = request instanceof HttpServletRequest ? (HttpServletRequest) request : null;
final StringBuffer sb = new StringBuffer(128);
if (hreq != null) {
sb.append(" sid: ").append(hreq.getHeader("ZK-SID")).append('\n');
addHeaderInfo(sb, hreq, "user-agent");
addHeaderInfo(sb, hreq, "content-length");
addHeaderInfo(sb, hreq, "content-type");
// sb.append(" method: ").append(hreq.getMethod());
}
sb.append(" ip: ").append(request.getRemoteAddr());
return sb.toString();
}
private static void addHeaderInfo(StringBuffer sb, HttpServletRequest request, String header) {
sb.append(' ').append(header).append(": ").append(request.getHeader(header)).append('\n');
}
/** A plugin used to provide additional browser information for
* {@link #getBrowser}.
* @since 6.0.0
* @see #setClientIdentifier
*/
public static interface ClientIdentifier {
/** Returns the information of the client if userAgent matches
* this client, or null if not matched.
* @param userAgent represents a client.
*/
public ClientIdentifier matches(String userAgent);
/** Returns the name of the browser.
* It is called only against the instance returned by {@link #matches}.
*/
public String getName();
/** Returns the version of the browser.
* It is called only against the instance returned by {@link #matches}.
*/
public double getVersion();
}
/** Returns the normal path; that is, will elminate the double dots
* ".."(parent) and single dot "."(current) in the path as possible. e.g.
* /abc/../def would be normalized to /def; /abc/./def would be
* normalized to /abc/def; /abc//def would be normalized to /abc/def.
*
Note that if found no way to navigate the path, it is deemed as an illegal path. e.g.
* /../abc or /../../abc is deemed as illegal path since we don't
* know how to continue doing the normalize.
* @since 3.6.2
*/
public static String getNormalPath(String path) {
final StringBuffer sb = new StringBuffer(path);
final IntStack slashes = new IntStack(32); //most 32 slash in a path
slashes.push(-1);
int j = 0, colon = -100, dot1 = -100, dot2 = -100;
for (; j < sb.length(); ++j) {
final char c = sb.charAt(j);
switch (c) {
case '/':
if (dot1 >= 0) { //single dot or double dots
if (dot2 >= 0) { //double dots
int preslash = slashes.pop();
if (preslash == 0) { //special case "/../"
throw new IllegalArgumentException("Illegal path: " + path);
}
if (slashes.isEmpty()) {
slashes.push(-1);
}
dot2 = -100;
}
int b = slashes.peek();
sb.delete(b + 1, j + 1);
j = b;
dot1 = -100;
} else { //no dot
int s = slashes.peek();
if (s >= 0) {
if (j == (s + 1)) { //consequtive slashs
if (colon == (s - 1)) { //e.g. "http://abc"
slashes.clear();
slashes.push(-1);
slashes.push(j);
} else {
--j;
sb.delete(j, j + 1);
}
continue;
}
}
slashes.push(j);
}
break;
case '.':
if (dot1 < 0) {
if (slashes.peek() == (j - 1))
dot1 = j;
} else if (dot2 < 0) {
dot2 = j;
} else { //more than 2 consecutive dots
throw new IllegalArgumentException("Illegal path: " + path);
}
break;
case ':':
if (colon >= 0) {
throw new IllegalArgumentException("Illegal path: " + path);
}
colon = j;
default:
dot1 = dot2 = -100;
}
}
return sb.toString();
}
private static class IntStack {
private int _top = -1;
private int[] _value;
public IntStack(int sz) {
_value = new int[sz];
}
public boolean isEmpty() {
return _top < 0;
}
public int peek() {
return _top >= 0 && _top < _value.length ? _value[_top] : -100;
}
public int pop() {
return _value[_top--];
}
public void push(int val) {
_value[++_top] = val;
}
public void clear() {
_top = -1;
}
}
}