
org.zkoss.web.servlet.http.Encodes Maven / Gradle / Ivy
/* Encodes.java
Purpose:
Description:
History:
Fri Jun 21 14:13:50 2002, Created by tomyeh
Copyright (C) 2002 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.http;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.lang.SystemException;
import org.zkoss.web.servlet.Charsets;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.web.util.resource.ExtendletContext;
/**
* Encoding utilities for servlets.
*
* @author tomyeh
* @see Https
*/
public class Encodes {
private static final Logger log = LoggerFactory.getLogger(Encodes.class);
protected Encodes() {
} //prevent from instantiation
/** Encodes a string to HTTP URI compliance by use of
* {@link Charsets#getURICharset}.
*
* Besides two-byte characters, it also encodes any character found
* in unsafes.
*
* @param unsafes the set of characters that must be encoded; never null.
* It must be sorted.
*/
private static final String encodeURI0(String s, char[] unsafes) throws UnsupportedEncodingException {
if (s == null)
return null;
final String charset = Charsets.getURICharset();
final byte[] in = s.getBytes(charset);
final byte[] out = new byte[in.length * 3]; //at most: %xx
int j = 0, k = 0;
for (; j < in.length; ++j) {
//Though it is ok to use '+' for ' ', Jetty has problem to
//handle space between Chinese characters.
final char cc = (char) (((int) in[j]) & 0xff);
if (cc >= 0x80 || cc <= ' ' || Arrays.binarySearch(unsafes, cc) >= 0) {
out[k++] = (byte) '%';
String cvt = Integer.toHexString(cc);
if (cvt.length() == 1) {
out[k++] = (byte) '0';
out[k++] = (byte) cvt.charAt(0);
} else {
out[k++] = (byte) cvt.charAt(0);
out[k++] = (byte) cvt.charAt(1);
}
} else {
out[k++] = in[j];
}
}
return j == k ? s : new String(out, 0, k, charset);
}
/** unsafe character when that are used in url's location. */
private static final char[] URI_UNSAFE;
/** unsafe character when that are used in url's query. */
private static final char[] URI_COMP_UNSAFE;
static {
URI_UNSAFE = "`%^{}[]\\\"<>|".toCharArray();
Arrays.sort(URI_UNSAFE);
URI_COMP_UNSAFE = "`%^{}[]\\\"<>|$&,/:;=?#".toCharArray();
Arrays.sort(URI_COMP_UNSAFE);
}
/** Does the HTTP encoding for the URI location.
* For example, '%' is translated to '%25'.
*
*
Since {@link #encodeURL(ServletContext, ServletRequest, ServletResponse, String)}
* will invoke this method automatically, you rarely need this method.
*
* @param s the string to encode; null is OK
* @return the encoded string or null if s is null
* @see #encodeURIComponent
*/
public static final String encodeURI(String s) throws UnsupportedEncodingException {
return encodeURI0(s, URI_UNSAFE);
}
/** Does the HTTP encoding for an URI query parameter.
* For example, '/' is translated to '%2F'.
* Both name and value must be encoded separately. Example,
* encodeURIComponent(name) + '=' + encodeURIComponent(value)
.
*
*
Since {@link #encodeURL(ServletContext, ServletRequest, ServletResponse, String)}
* will not invoke this method automatically, you'd better
* to encode each query parameter by this method or
* {@link #addToQueryString(StringBuffer,Map)}.
*
* @param s the string to encode; null is OK
* @return the encoded string or null if s is null
* @see #addToQueryString(StringBuffer,String,Object)
* @see #encodeURI
*/
public static final String encodeURIComponent(String s) throws UnsupportedEncodingException {
return encodeURI0(s, URI_COMP_UNSAFE);
}
/**
/** Appends a map of parameters (name=value) to a query string.
* It returns the query string preceding with '?' if any parameter exists,
* or an empty string if no parameter at all. Thus, you could do
*
*
request.getRequestDispatcher(
* addToQueryStirng(new StringBuffer(uri), params).toString());
*
*
Since RequestDispatcher.include and forward do not allow wrapping
* the request and response -- see spec and the implementation of both
* Jetty and Catalina, we have to use this method to pass the parameters.
*
* @param params a map of parameters; format: (String, Object) or
* (String, Object[]); null is OK
*/
public static final StringBuffer addToQueryString(StringBuffer sb, Map params) throws UnsupportedEncodingException {
if (params != null) {
for (Iterator it = params.entrySet().iterator(); it.hasNext();) {
final Map.Entry me = (Map.Entry) it.next();
addToQueryString(sb, (String) me.getKey(), me.getValue());
}
}
return sb;
}
/** Appends a parameter (name=value) to a query string.
* This method automatically detects whether other query is already
* appended. If so, & is used instead of ?.
*
*
The query string might contain servlet path and other parts.
* This method starts the searching from the first '?'.
* If the query string doesn't contain '?', it is assumed to be a string
* without query's name/value pairs.
*
* @param value the value. If it is null, only name is appended.
* If it is an array of objects, multiple pairs of name=value[j] will
* be appended.
*/
public static final StringBuffer addToQueryString(StringBuffer sb, String name, Object value)
throws UnsupportedEncodingException {
if (value instanceof Object[]) {
final Object[] vals = (Object[]) value;
if (vals.length == 0) {
value = null; //only append name
} else {
for (int j = 0; j < vals.length; ++j)
addToQueryString(sb, name, vals[j]);
return sb; //done
}
}
sb.append(next(sb, '?', 0) >= sb.length() ? '?' : '&');
sb.append(encodeURIComponent(name)).append('=');
//NOTE: jetty with jboss3.0.6 ignore parameters without '=',
//so we always append '=' even value is null
if (value != null)
sb.append(encodeURIComponent(Objects.toString(value)));
return sb;
}
/** Returns the first occurrence starting from j, or sb.length().
*/
private static final int next(StringBuffer sb, char cc, int j) {
for (final int len = sb.length(); j < len; ++j)
if (sb.charAt(j) == cc)
break;
return j;
}
/**
* Sets the parameter (name=value) to a query string.
* If the name already exists in the query string, it will be removed first.
* If your name has appeared in the string, it will replace
* your new value to the query string.
* Otherwise, it will append the name/value to the new string.
*
*
The query string might contain servlet path and other parts.
* This method starts the searching from the first '?'.
* If the query string doesn't contain '?', it is assumed to be a string
* without query's name/value pairs.
*
* @param str The query string like xxx?xxx=xxx&xxx=xxx or null.
* @param name The get parameter name.
* @param value The value associated with the get parameter name.
* @return The new or result query string with your name/value.
* @see #addToQueryString
*/
public static final String setToQueryString(String str, String name, Object value)
throws UnsupportedEncodingException {
final StringBuffer sb = new StringBuffer();
if (str != null)
sb.append(str);
return setToQueryString(sb, name, value).toString();
}
/**
* Sets the parameter (name=value) to a query string.
* If the name already exists in the query string, it will be
* removed first.
* @see #addToQueryString
*/
public static final StringBuffer setToQueryString(StringBuffer sb, String name, Object value)
throws UnsupportedEncodingException {
removeFromQueryString(sb, name);
return addToQueryString(sb, name, value);
}
/**
* Sets a map of parameters (name=value) to a query string.
* If the name already exists in the query string, it will be removed first.
* @see #addToQueryString
*/
public static final String setToQueryString(String str, Map params) throws UnsupportedEncodingException {
return setToQueryString(new StringBuffer(str), params).toString();
}
/**
* Sets a map of parameters (name=value) to a query string.
* If the name already exists in the query string, it will be removed first.
* @see #addToQueryString
*/
public static final StringBuffer setToQueryString(StringBuffer sb, Map params) throws UnsupportedEncodingException {
if (params != null) {
for (Iterator it = params.entrySet().iterator(); it.hasNext();) {
final Map.Entry me = (Map.Entry) it.next();
setToQueryString(sb, (String) me.getKey(), me.getValue());
}
}
return sb;
}
/** Tests whether a parameter exists in the query string.
*/
public static final boolean containsQuery(String str, String name) {
int j = str.indexOf(name);
if (j <= 0)
return false;
char cc = str.charAt(j - 1);
if (cc != '?' && cc != '&')
return false;
j += name.length();
if (j >= str.length())
return true;
cc = str.charAt(j);
return cc == '=' || cc == '&';
}
/** Remove all name/value pairs of the specified name from a string.
*
*
The query string might contain servlet path and other parts.
* This method starts the searching from the last '?'.
* If the query string doesn't contain '?', it is assumed to be a string
* without query's name/value pairs.
* @see #addToQueryString
*/
public static final String removeFromQueryString(String str, String name) throws UnsupportedEncodingException {
if (str == null)
return null;
int j = str.indexOf('?');
if (j < 0)
return str;
final StringBuffer sb = new StringBuffer(str);
removeFromQueryString(sb, name);
return sb.length() == str.length() ? str : sb.toString();
}
/** Remove all name/value pairs of the specified name from a string.
* @see #addToQueryString
*/
public static final StringBuffer removeFromQueryString(StringBuffer sb, String name)
throws UnsupportedEncodingException {
name = encodeURIComponent(name);
if (name == null || name.isEmpty())
return sb;
int j = sb.indexOf("?");
if (j < 0)
return sb; //no '?'
j = sb.indexOf(name, j + 1);
if (j < 0)
return sb; //no name
final int len = name.length();
do {
//1. make sure left is & or ?
int k = j + len;
char cc = sb.charAt(j - 1);
if (cc != '&' && cc != '?') {
j = k;
continue;
}
//2. make sure right is = or & or end-of-string
if (k < sb.length()) {
cc = sb.charAt(k);
if (cc != '=' && cc != '&') {
j = k;
continue;
}
}
//3. remove it until next & or end-of-string
k = next(sb, '&', k);
if (k < sb.length())
sb.delete(j, k + 1); //also remove '&'
else
sb.delete(j - 1, k); //also preceding '?' or '&'
} while ((j = sb.indexOf(name, j)) > 0);
return sb;
}
/** Encodes an URL.
* It resolves "*" contained in URI, if any, to the proper Locale,
* and the browser code.
* Refer to {@link Servlets#locate(ServletContext, ServletRequest, String, Locator)}
* for details.
*
*
In additions, if uri starts with "/", the context path, e.g.,
* /zkdemo, is prefixed.
* In other words, "/ab/cd" means it is relevant to the servlet
* context path (say, "/zkdemo").
*
*
If uri starts with "~abc/", it means it is relevant to a foreign
* Web context called /abc. And, it will be converted to "/abc/" first
* (without prefix request.getContextPath()).
*
*
Finally, the uri is encoded by HttpServletResponse.encodeURL.
*
*
This method invokes {@link #encodeURI} for any characters
* before '?'. However, it does NOT encode any character after '?'. Thus,
* you might have to invoke
* {@link #encodeURIComponent} or {@link #addToQueryString(StringBuffer,Map)}
* to encode the query parameters.
*
*
The URL Prefix and Encoder
*
* In a sophisticated environment, e.g.,
* Reverse Proxy,
* the encoded URL might have to be prefixed with some special prefix.
* To do that, you can implement {@link URLEncoder}, and then
* specify the class with the library property called
* org.zkoss.web.servlet.http.URLEncoder
.
* When {@link #encodeURL} encodes an URL, it will invoke
* {@link URLEncoder#encodeURL} such you can do customized encoding,
* such as insert a special prefix.
*
* @param request the request; never null
* @param response the response; never null
* @param uri it must be null, empty or starts with "/". It might contain
* "*" for current browser code and Locale.
* @param ctx the servlet context; used only if "*" is contained in uri
* @exception IndexOutOfBoundException if uri is empty
* @see org.zkoss.web.servlet.Servlets#locate
* @see org.zkoss.web.servlet.Servlets#generateURI
*/
public static final String encodeURL(ServletContext ctx, ServletRequest request, ServletResponse response,
String uri) throws ServletException {
try {
return urlEncoder().encodeURL(ctx, request, response, uri, _urlenc0);
} catch (Exception ex) {
log.error("", ex);
throw new ServletException("Unable to encode " + uri, ex);
}
}
private static URLEncoder urlEncoder() {
if (_urlenc == null) {
final String cls = Library.getProperty("org.zkoss.web.servlet.http.URLEncoder");
if (cls != null && cls.length() > 0) {
try {
_urlenc = (URLEncoder) Classes.newInstanceByThread(cls);
} catch (Throwable ex) {
throw SystemException.Aide.wrap(ex, "Unable to instantiate " + cls);
}
} else {
_urlenc = _urlenc0;
}
}
return _urlenc;
}
private static URLEncoder _urlenc;
private static final URLEncoder _urlenc0 = new URLEncoder() {
public String encodeURL(ServletContext ctx, ServletRequest request, ServletResponse response, String uri,
URLEncoder defaultEncoder) throws Exception {
return encodeURL0(ctx, request, response, uri);
}
};
private static final String encodeURL0(ServletContext ctx, ServletRequest request, ServletResponse response,
String uri) throws Exception {
if (uri == null || uri.length() == 0)
return uri; //keep as it is
boolean ctxpathSpecified = false;
if (uri.charAt(0) != '/') { //NOT relative to context path
if (Servlets.isUniversalURL(uri))
return uri; //nothing to do
if (uri.charAt(0) == '~') { //foreign context
final String ctxroot;
if (uri.length() == 1) {
ctxroot = uri = "/";
} else if (uri.charAt(1) == '/') {
ctxroot = "/";
uri = uri.substring(1);
} else {
uri = '/' + uri.substring(1);
final int j = uri.indexOf('/', 1);
ctxroot = j >= 0 ? uri.substring(0, j) : uri;
}
final ExtendletContext extctx = Servlets.getExtendletContext(ctx, ctxroot.substring(1));
if (extctx != null) {
final int j = uri.indexOf('/', 1);
return extctx.encodeURL(request, response, j >= 0 ? uri.substring(j) : "/");
}
final ServletContext newctx = ctx.getContext(ctxroot);
if (newctx != null) {
ctx = newctx;
} else if (log.isDebugEnabled()) {
log.debug("Context not found: " + ctxroot);
}
ctxpathSpecified = true;
} else if (Https.isIncluded(request) || Https.isForwarded(request)) {
//if relative URI and being included/forwarded,
//converts to absolute
String pgpath = Https.getThisServletPath(request);
if (pgpath != null) {
int j = pgpath.lastIndexOf('/');
if (j >= 0) {
uri = pgpath.substring(0, j + 1) + uri;
} else {
log.warn("The current page doesn't contain '/':" + pgpath);
}
}
}
}
//locate by locale and browser if necessary
uri = Servlets.locate(ctx, request, uri, null);
//prefix context path
if (!ctxpathSpecified && uri.charAt(0) == '/'
//ZK-3131: do not prefix context path to relative protocol urls that starts with //
&& !(uri.length() > 1 && uri.charAt(1) == '/') && (request instanceof HttpServletRequest)) {
//Work around with a bug when we wrap Pluto's RenderRequest (1.0.1)
String ctxpath = Https.getThisContextPath(request);
if (ctxpath.length() > 0 && ctxpath.charAt(0) != '/')
ctxpath = '/' + ctxpath;
//Some Web server's ctxpath is "/"
final int last = ctxpath.length() - 1;
if (last >= 0 && ctxpath.charAt(last) == '/')
ctxpath = ctxpath.substring(0, last);
uri = ctxpath + uri;
}
int j = uri.indexOf('?');
if (j < 0) {
uri = encodeURI(uri);
} else {
uri = encodeURI(uri.substring(0, j)) + uri.substring(j);
}
//encode
if (response instanceof HttpServletResponse)
uri = ((HttpServletResponse) response).encodeURL(uri);
return uri;
}
/** The URL encoder used to encode URL by including the session ID,
* and servlet context path.
* It is a plugin that {@link Encodes#encodeURL} will call
* to encode the URL.
*
*
In a sophisticated environment, e.g.,
* Reverse Proxy,
* the encoded URL might have to be prefixed with some special prefix.
* To do that, you can implement {@link URLEncoder}, and then
* specify the class with the library property called
* org.zkoss.web.servlet.http.URLEncoder
.
* When {@link #encodeURL} encodes an URL, it will invoke
* {@link URLEncoder#encodeURL} such you can do customized encoding,
* such as insert a special prefix.
*
* @since 3.0.1
* @see Encodes#encodeURL
*/
public static interface URLEncoder {
/** Encodes the specified URL by including the session ID and
* the servlet context path, if necessary.
*
*
Notice that url might contain "~" and other special
* characters that the Web server won't support.
* The implementation might invoke back the default encoding
* by use of the defaultEcoder parameter.
*
* @param url the URL to encode. It shall not include
* the servlet context path.
* @param defaultEncoder the default encoder (never null).
* @since 5.0.0
*/
public String encodeURL(ServletContext ctx, ServletRequest request, ServletResponse response, String url,
URLEncoder defaultEncoder) throws Exception;
}
}