ilscipio/scipio-erp

View on GitHub
framework/webapp/src/org/ofbiz/webapp/control/RequestLinkUtil.java

Summary

Maintainability
F
3 days
Test Coverage
package org.ofbiz.webapp.control;

import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.StringUtil;
import org.ofbiz.base.util.UtilGenerics;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.entity.Delegator;
import org.ofbiz.webapp.ExtWebappInfo;
import org.ofbiz.webapp.FullWebappInfo;
import org.ofbiz.webapp.OfbizUrlBuilder;
import org.ofbiz.webapp.WebAppUtil;
import org.ofbiz.webapp.website.WebSiteProperties;
import org.xml.sax.SAXException;

/**
 * SCIPIO: Request link utilities.
 */
public final class RequestLinkUtil {

    private static final Debug.OfbizLogger module = Debug.getOfbizLogger(java.lang.invoke.MethodHandles.lookup().lookupClass());

    private static final Pattern jsessionIdPat = Pattern.compile("((;jsessionid=)([^\\?#]*))");

    public static final String HTTP_PROTO_COLON = "http:";
    public static final String HTTP_PROTO_COLONSLASH = "http://";
    public static final String HTTPS_PROTO_COLON = "https:";
    public static final String HTTPS_PROTO_COLONSLASH = "https://";

    public static final String URL_DELIMS_STR = "/?;#&";
    public static final List<Character> URL_DELIMS_LIST = UtilMisc.unmodifiableArrayListCopy(
            URL_DELIMS_STR.chars().mapToObj(e->(char)e).collect(Collectors.toList()));
    public static final Set<Character> URL_DELIMS_SET = UtilMisc.unmodifiableHashSetCopy(URL_DELIMS_LIST);
    public static final Pattern URL_DELIMS_PAT = Pattern.compile("["+URL_DELIMS_STR+"]");

    public static final String URL_DELIMS_NODIR_STR = "?;#&";
    public static final List<Character> URL_DELIMS_NODIR_LIST = UtilMisc.unmodifiableArrayListCopy(
            URL_DELIMS_NODIR_STR.chars().mapToObj(e->(char)e).collect(Collectors.toList()));
    public static final Set<Character> URL_DELIMS_NODIR_SET = UtilMisc.unmodifiableHashSetCopy(URL_DELIMS_NODIR_LIST);
    public static final Pattern URL_DELIMS_NODIR_PAT = Pattern.compile("["+URL_DELIMS_NODIR_STR+"]");

    private static final Set<String> logCallerExcludeClasses = UtilMisc.toSet(RequestLinkUtil.class.getName());

    private RequestLinkUtil() {
    }

    /**
     * @deprecated 2018-08: In Scipio, there is never a jsessionId added anymore.
     */
    @Deprecated
    public static boolean containsJsessionId(String url) {
        return url.contains(";jsessionid=");
    }

    /**
     * @deprecated 2018-08: In Scipio, there is never a jsessionId added anymore.
     */
    @Deprecated
    public static String removeJsessionId(String url) {
        return jsessionIdPat.matcher(url).replaceFirst("");
    }

    /**
     * @deprecated 2018-08: In Scipio, there is never a jsessionId added anymore.
     */
    @Deprecated
    public static String getSessionId(String url) {
        Matcher m = jsessionIdPat.matcher(url);
        boolean found = m.find();
        if (found) {
            return m.group(3);
        }
        else {
            return null;
        }
    }

    /**
     * @deprecated 2018-08: In Scipio, there is never a jsessionId added anymore.
     */
    @Deprecated
    public static String setJsessionId(String url, String sessionId) {
        String jsessionIdStr = ";jsessionid=" + sessionId;

        Matcher m = jsessionIdPat.matcher(url);
        String regReplUrl = m.replaceFirst(jsessionIdStr);

        if (containsJsessionId(regReplUrl)) {
            return regReplUrl;
        }
        else {
            // This is ofbizUrl/RequestHanlder.makeLink behavior...
            StringBuilder newURL = new StringBuilder(url);

            int questionIndex = newURL.indexOf("?");
            if (questionIndex == -1) {
                newURL.append(jsessionIdStr);
            } else {
                newURL.insert(questionIndex, jsessionIdStr);
            }
            return newURL.toString();
        }
    }

    /**
     * @deprecated 2018-08: In Scipio, there is never a jsessionId added anymore.
     */
    @Deprecated
    public static String encodeURLNoJsessionId(String url, HttpServletResponse response) {
        return RequestLinkUtil.removeJsessionId(response.encodeURL(url));
    }

    public static String checkAddExternalLoginKey(String url, HttpServletRequest request, boolean escaped) {
        return checkAddExternalLoginKey(url, request, escaped ? "&amp;" : "&");
    }

    public static String checkAddExternalLoginKey(String url, HttpServletRequest request, String paramDelim) {
        String extLoginKey = (String) request.getAttribute("externalLoginKey");
        if (extLoginKey != null && !extLoginKey.isEmpty()) {
            url = url + (url.contains("?") ? paramDelim : "?") + "externalLoginKey=" + extLoginKey;
        }
        return url;
    }

    public static String removeQueryStringParam(String queryString, String paramName) {
        // WARNING: UNTESTED
        final Pattern pat = Pattern.compile("(^|[?&])" + paramName + "=[^#?;&]*");

        Matcher m = pat.matcher(queryString);
        String res = m.replaceAll("");

        if (queryString.startsWith("?") && !res.startsWith("?")) {
            return "?" + res;
        }
        else {
            return res;
        }
    }

    /**
     * Makes param string (no starting delimiter); intended specifically for new Scipio
     * link-building facilities, and may slightly differ from stock Ofbiz ones.
     */
    public static String makeParamString(Map<String, Object> params, String delim) {
        StringBuilder sb = new StringBuilder("");
        for(Map.Entry<String, Object> entry : params.entrySet()) {
            appendToParamString(sb, entry.getKey(), entry.getValue(), delim);
        }
        if (sb.length() >= delim.length()) {
            sb.delete(0, delim.length());
        }
        return sb.toString();
    }

    public static void appendToParamString(StringBuilder sb, String name, Object val, String delim) {
        if (val instanceof Collection) { // param with multiple values (rare)
            for(Object subVal : UtilGenerics.checkCollection(val)) {
                appendToParamStringAsString(sb, name, subVal, delim);
            }
        } else {
            appendToParamStringAsString(sb, name, val, delim);
        }
    }

    public static void appendToParamStringAsString(StringBuilder sb, String name, Object val, String delim) {
        sb.append(delim);
        sb.append(name);
        sb.append("=");
        if (val != null) {
            sb.append(val.toString());
        }
    }

    public static Boolean checkFullSecureOrStandard(HttpServletRequest request, HttpServletResponse response,
            Boolean interWebapp, Boolean fullPath, Boolean secure) {
        /* 2018-08-02: the target method does not use this anymore, so don't bother
        WebSiteProperties webSiteProps;
        try {
            webSiteProps = WebSiteProperties.from(request);
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return true;
        }*/
        return RequestHandler.checkFullSecureOrStandard(request, null, null, interWebapp, fullPath, secure);
    }

    public static Boolean checkFullSecureOrStandard(Delegator delegator, WebSiteProperties webSiteProps,
            Boolean interWebapp, Boolean fullPath, Boolean secure, Map<String, Object> context) {
        // what we can do here depends on whether we got a request/response or not
        // checkFullSecureOrStandard should handle the case where request was missing (treats as insecure current request)
        return RequestHandler.checkFullSecureOrStandard(null, webSiteProps, null, interWebapp, fullPath, secure);

        /* old logic, too simplistic (wrong and insecure compared to new RequestHandler logic)
        if (Boolean.TRUE.equals(secure)) {
            return Boolean.TRUE;
        } else if (Boolean.TRUE.equals(fullPath)) {
            return Boolean.FALSE;
        } else {
            return null;
        }
        */
    }

    public static Boolean checkFullSecureOrStandard(Delegator delegator, String webSiteId,
            Boolean interWebapp, Boolean fullPath, Boolean secure, Map<String, Object> context) {
        /* 2018-08-02: the target method does not use this anymore, so don't bother
        WebSiteProperties webSiteProps = null;
        if (webSiteId != null) {
            try {
                webSiteProps = WebSiteProperties.from(delegator, webSiteId);
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
                return true;
            }
        }*/
        return checkFullSecureOrStandard(delegator, (WebSiteProperties) null, interWebapp, fullPath, secure, context);
    }

    public static String doLinkURLEncode(HttpServletRequest request, HttpServletResponse response, StringBuilder newURL, boolean interWebapp,
            FullWebappInfo targetWebappInfo, FullWebappInfo currentWebappInfo, boolean didFullStandard, boolean didFullSecure) {
        return RequestHandler.doLinkURLEncode(request, response, newURL, interWebapp, targetWebappInfo, currentWebappInfo, didFullStandard, didFullSecure);
    }

    public static String doLinkURLEncode(Delegator delegator, Locale locale, StringBuilder newURL, FullWebappInfo targetWebappInfo, FullWebappInfo currentWebappInfo,
            boolean didFullStandard, boolean didFullSecure, Map<String, Object> context) {
        return RequestHandler.doLinkURLEncode(delegator, locale, newURL, targetWebappInfo, currentWebappInfo, didFullStandard, didFullSecure, context);
    }

    /**
     * SCIPIO: Helper method, originally derived from catalog URL links, but needed repeatedly.
     * <p>
     * This method only supports linking within the current webapp and encoding using the current webapp.
     */
    public static String buildLinkHostPartAndEncode(HttpServletRequest request, HttpServletResponse response, Locale locale, FullWebappInfo targetWebappInfo, String url,
            Boolean fullPath, Boolean secure, Boolean encode, boolean includeWebappPathPrefix) throws IllegalArgumentException {

        FullWebappInfo currentWebappInfo = FullWebappInfo.fromRequest(request);
        if (targetWebappInfo == null) {
            targetWebappInfo = currentWebappInfo;
        } else {
            // 2018-08-08: force fullPath if server part differs
            if (!targetWebappInfo.equalsProtoHostPortWithHardDefaults(currentWebappInfo)) {
                fullPath = true;
            }
        }

        boolean didFullStandard = false;
        boolean didFullSecure = false;
        StringBuilder newURL = new StringBuilder();

        Boolean secureFullPathFlag = checkFullSecureOrStandard(request, response, false, fullPath, secure);
        if (secureFullPathFlag != null) {
            if (secureFullPathFlag) {
                didFullSecure = true;
            } else {
                didFullStandard = true;
            }
            try {
                targetWebappInfo.getOfbizUrlBuilder().buildHostPart(newURL, url, secureFullPathFlag, false);
            } catch (Exception e) {
                throw new IllegalStateException("Error building link host part for webapp: " + targetWebappInfo, e);
            }
        }

        if (includeWebappPathPrefix) {
            // 2018-07-27: must use webapp path prefix
            try {
                targetWebappInfo.getOfbizUrlBuilder().buildPathPartWithWebappPathPrefix(newURL, url);
            } catch (Exception e) {
                throw new IllegalStateException("Error building link path part for webapp: " + targetWebappInfo, e);
            }
        } else {
            newURL.append(url);
        }

        String res;
        if (!Boolean.FALSE.equals(encode)) {
            res = RequestHandler.doLinkURLEncode(request, response, newURL, false, targetWebappInfo, targetWebappInfo, didFullStandard, didFullSecure);
        } else {
            res = newURL.toString();
        }

        return res;
    }

    public static String buildLinkHostPartAndEncodeSafe(HttpServletRequest request, HttpServletResponse response, Locale locale, FullWebappInfo targetWebappInfo, String url,
            Boolean fullPath, Boolean secure, Boolean encode, boolean includeWebappPathPrefix) {
        try {
            return buildLinkHostPartAndEncode(request, response, locale, targetWebappInfo, url, fullPath, secure, encode, includeWebappPathPrefix);
        } catch(Exception e) {
            Debug.logError(e, module);
            return null;
        }
    }

    @Deprecated
    public static String buildLinkHostPartAndEncode(HttpServletRequest request, HttpServletResponse response, FullWebappInfo targetWebappInfo, String url,
            Boolean fullPath, Boolean secure, Boolean encode) throws WebAppConfigurationException, IOException {
        return buildLinkHostPartAndEncode(request, response, null, targetWebappInfo, url, fullPath, secure, encode, false);
    }

    /**
     * SCIPIO: Helper method, originally derived from catalog URL links, but needed repeatedly.
     * <p>
     * This method treats the given webSiteId as the "current" webapp, and links only within this
     * webapp and (theoretically) only encodes using this webapp.
     * WARN: In other words, this is NOT meant to handle inter-webapp links! It only han
     */
    public static String buildLinkHostPartAndEncode(Delegator delegator, Locale locale, FullWebappInfo targetWebappInfo, String url,
            Boolean fullPath, Boolean secure, Boolean encode, boolean includeWebappPathPrefix, FullWebappInfo currentWebappInfo,
            Map<String, Object> context) {

        boolean didFullStandard = false;
        boolean didFullSecure = false;
        StringBuilder newURL = new StringBuilder();

        // NOTE: this is always treated as inter-webapp, because we don't know our webapp
        Boolean secureFullPathFlag = checkFullSecureOrStandard(delegator, targetWebappInfo.getWebSiteProperties(),
                !targetWebappInfo.equals(currentWebappInfo), fullPath, secure, context);

        if (secureFullPathFlag != null) {
            if (secureFullPathFlag) {
                didFullSecure = true;
            } else {
                didFullStandard = true;
            }
            try {
                targetWebappInfo.getOfbizUrlBuilder().buildHostPart(newURL, url, Boolean.TRUE.equals(secureFullPathFlag), false);
            } catch (Exception e) {
                throw new IllegalStateException("Error building link host part for webapp: " + targetWebappInfo, e);
            }
        }

        if (includeWebappPathPrefix) {
            try {
                targetWebappInfo.getOfbizUrlBuilder().buildPathPartWithWebappPathPrefix(newURL, url);
            } catch (Exception e) {
                throw new IllegalStateException("Error building link path part for webapp: " + targetWebappInfo, e);
            }
        } else {
            newURL.append(url);
        }

        String res;
        if (!Boolean.FALSE.equals(encode)) {
            res = RequestHandler.doLinkURLEncode(delegator, locale, newURL, targetWebappInfo,
                    currentWebappInfo, didFullStandard, didFullSecure, context);
        } else {
            res = newURL.toString();
        }
        return res;
    }

    public static String buildLinkHostPartAndEncodeSafe(Delegator delegator, Locale locale, FullWebappInfo targetWebappInfo, String url,
            Boolean fullPath, Boolean secure, Boolean encode, boolean includeWebappPathPrefix, FullWebappInfo currentWebappInfo,
            Map<String, Object> context) {
        try {
            return buildLinkHostPartAndEncode(delegator, locale, targetWebappInfo, url, fullPath, secure, encode, includeWebappPathPrefix,
                    currentWebappInfo, context);
        } catch(Exception e) {
            Debug.logError("Error building link host part: " + e.toString() + getLogSuffix(), module);
            return null;
        }
    }

    @Deprecated
    public static String buildLinkHostPartAndEncode(Delegator delegator, String webSiteId, String url,
            Boolean fullPath, Boolean secure, Boolean encode, Map<String, Object> context) {
        FullWebappInfo webappInfo = FullWebappInfo.fromWebapp(ExtWebappInfo.fromWebSiteId(webSiteId), context);
        return buildLinkHostPartAndEncode(delegator, null, webappInfo, url, fullPath, secure, encode, false, webappInfo, context);
    }

    /**
     * SCIPIO: Rudimentarily builds host part of link, from request, as requested secure or not (or using current if null).
     * This overload uses the CURRENT WebSite in request to determine the host, unless explicit is passed, in which case that one is used.
     * Simple wrapper around {@link org.ofbiz.webapp.OfbizUrlBuilder#buildHostPart(Appendable, Boolean)}.
     * Added 2017-11-18.
     *
     * @param request the request, containing the current webSiteId
     * @param webSiteId optional explicit target webSiteId
     * @param secure if explicit true or false, creates https or http link, respectively; otherwise auto-determines from request ("current")
     */
    public static String buildLinkHostPart(HttpServletRequest request, Locale locale, String webSiteId, Boolean secure, boolean includeWebappPathPrefix) {
        StringBuilder newURL = new StringBuilder();
        FullWebappInfo targetWebappInfo = FullWebappInfo.fromWebapp(ExtWebappInfo.fromWebSiteId(webSiteId), request);
        if (secure == null) secure = RequestLinkUtil.isEffectiveSecure(request);
        try {
            targetWebappInfo.getOfbizUrlBuilder().buildHostPart(newURL, secure);
            if (includeWebappPathPrefix) {
                targetWebappInfo.getOfbizUrlBuilder().buildPathPartWithWebappPathPrefix(newURL, null);
            }
        } catch(Exception e) {
            throw new IllegalArgumentException(e);
        }
        return newURL.toString();
    }

    @Deprecated
    public static String buildLinkHostPart(HttpServletRequest request, String webSiteId, Boolean secure) throws WebAppConfigurationException, IOException {
        return buildLinkHostPart(request, null, webSiteId, secure, false);
    }

    /**
     * SCIPIO: Rudimentarily builds host part of link, from request, as requested secure or not (no automatic logic).
     * Same as {@link #buildLinkHostPart(HttpServletRequest, Locale, String, Boolean, boolean)} but throws no exceptions and returns null instead.
     * Added 2017-11-18.
     */
    public static String buildLinkHostPartSafe(HttpServletRequest request, Locale locale, String webSiteId, Boolean secure, boolean includeWebappPathPrefix) {
        try {
            return buildLinkHostPart(request, locale, webSiteId, secure, includeWebappPathPrefix);
        } catch (Exception e) {
            Debug.logError("Error building link host part: " + e.toString() + getLogSuffix(), module);
            return null; // null may allow default value operators to work in other langs
        }
    }

    @Deprecated
    public static String buildLinkHostPartSafe(HttpServletRequest request, String webSiteId, Boolean secure) {
        return buildLinkHostPartSafe(request, null, webSiteId, secure, false);
    }

    /**
     * SCIPIO: Rudimentarily builds host part of link, statically, as requested secure or not (no automatic logic).
     * This overload uses the explicit WebSite in request to determine the host, or the system defaults (url.properties) if no webSiteId passed.
     * Simple wrapper around {@link org.ofbiz.webapp.OfbizUrlBuilder#buildHostPart(Appendable, Boolean)}.
     * Added 2017-11-18.
     *
     * @param delegator the delegator
     * @param webSiteId optional explicit target webSiteId
     * @param secure if explicit true or false, creates https or http link, respectively; if null, uses a configured or common default
     */
    public static String buildLinkHostPart(Delegator delegator, Locale locale, String webSiteId, Boolean secure,
            boolean includeWebappPathPrefix, FullWebappInfo.Cache webappInfoCache) {
        StringBuilder newURL = new StringBuilder();
        OfbizUrlBuilder builder = FullWebappInfo.getOfbizUrlBuilderFromWebSiteIdOrDefaults(webSiteId, delegator, webappInfoCache);
        try {
            builder.buildHostPart(newURL, secure);
            if (includeWebappPathPrefix) {
                builder.buildPathPartWithWebappPathPrefix(newURL);
            }
        } catch(Exception e) {
            throw new IllegalArgumentException(e);
        }
        return newURL.toString();
    }

    @Deprecated
    public static String buildLinkHostPart(Delegator delegator, String webSiteId, Boolean secure) throws WebAppConfigurationException, IOException {
        return buildLinkHostPart(delegator, null, webSiteId, secure, false, null);
    }

    /**
     * SCIPIO: Rudimentarily builds host part of link, statically, as requested secure or not (no automatic logic).
     * Same as {@link #buildLinkHostPart(Delegator, String, Boolean)} but throws no exceptions and returns null instead.
     * Added 2017-11-18.
     */
    public static String buildLinkHostPartSafe(Delegator delegator, Locale locale, String webSiteId, Boolean secure, boolean includeWebappPathPrefix) {
        try {
            return buildLinkHostPart(delegator, locale, webSiteId, secure, includeWebappPathPrefix, null);
        } catch (Exception e) {
            Debug.logError("Error building link host part: " + e.toString() + getLogSuffix(), module);
            return null; // null may allow default value operators to work in other langs
        }
    }

    @Deprecated
    public static String buildLinkHostPartSafe(Delegator delegator, String webSiteId, Boolean secure) {
        return buildLinkHostPartSafe(delegator, null, webSiteId, secure, false);
    }

    public static String getWebSiteContextPath(Delegator delegator, String webSiteId) throws IllegalArgumentException {
        try {
            return WebAppUtil.getWebappInfoFromWebsiteId(webSiteId).getContextRoot();
        } catch (SAXException e) {
            throw new IllegalArgumentException(e);
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Builds link using RequestHandler.makeLinkAuto logic, convenience wrapper.
     */
    public static String makeLinkAuto(ServletContext servletContext, HttpServletRequest request, HttpServletResponse response, String uri) {
        return RequestHandler.makeLinkAuto(request, response, uri, null, null, null, null, null, null, null);
    }

    /**
     * Builds link using RequestHandler.makeLinkAuto logic, convenience wrapper.
     */
    public static String makeLinkAuto(ServletContext servletContext, HttpServletRequest request, HttpServletResponse response,
            String uri, Boolean fullPath, Boolean secure, Boolean encode) {
        return RequestHandler.makeLinkAuto(request, response, uri, null, null, null, null, fullPath, secure, encode);
    }

    /**
     * Builds link using RequestHandler.makeLinkAuto logic, convenience wrapper.
     */
    public static String makeLinkAuto(ServletContext servletContext, HttpServletRequest request, HttpServletResponse response,
            String uri, Boolean absPath, Boolean interWebapp, String webSiteId, Boolean controller, Boolean fullPath, Boolean secure, Boolean encode) {
        return RequestHandler.makeLinkAuto(request, response, uri, absPath, interWebapp, webSiteId, controller, fullPath, secure, encode);
    }

    /**
     * Builds link using RequestHandler.makeLinkAuto logic, convenience wrapper.
     */
    public static String makeLinkAuto(HttpServletRequest request, HttpServletResponse response, String uri) {
        return RequestHandler.makeLinkAuto(request, response, uri, null, null, null, null, null, null, null);
    }

    /**
     * Builds link using RequestHandler.makeLinkAuto logic, convenience wrapper.
     */
    public static String makeLinkAuto(HttpServletRequest request, HttpServletResponse response,
            String uri, Boolean fullPath, Boolean secure, Boolean encode) {
        return RequestHandler.makeLinkAuto(request, response, uri, null, null, null, null, fullPath, secure, encode);
    }

    /**
     * Builds link using RequestHandler.makeLinkAuto logic, convenience wrapper.
     */
    public static String makeLinkAuto(HttpServletRequest request, HttpServletResponse response,
            String uri, Boolean absPath, Boolean interWebapp, String webSiteId, Boolean controller, Boolean fullPath, Boolean secure, Boolean encode) {
        return RequestHandler.makeLinkAuto(request, response, uri, absPath, interWebapp, webSiteId, controller, fullPath, secure, encode);
    }

    /**
     * Returns {@link javax.servlet.http.HttpServletRequest#getRequestURL} + query string.
     */
    public static String getFullRequestURL(HttpServletRequest request) {
        StringBuffer url = request.getRequestURL();
        if (request.getQueryString() != null) url.append("?").append(request.getQueryString());
        return url.toString();
    }

    /**
     * Best-effort rebuilds the incoming URL that was requested by the client, before any forwards, but with support
     * for specifying a different protocol and port.
     * <p>
     * Implementation derived from {@link org.apache.catalina.connector.Request#getRequestURL()}.
     *
     * @param request
     * @param response
     * @param secure true: use https; false: use http; null: use same as original request url
     * @param staticHost true: use host from url.properties/WebSite; false/null: use host from request.getServerName
     * @param includeWebappPathPrefix true: include webapp path prefix from WebSite or url.properties configuration
     * @param useQueryString
     * @param handleErrors if true, swallow errors and use best available; if false, throw IllegalStateException
     * @throws IllegalStateException if handleErrors false and any entity or parsing error
     */
    public static String rebuildOriginalRequestURL(HttpServletRequest request, HttpServletResponse response, Locale locale,
            Boolean secure, Boolean staticHost, boolean includeWebappPathPrefix, boolean useQueryString, boolean handleErrors) throws IllegalStateException {
        // NOTE: derived from Tomcat 8 implementation of getRequestURL
        StringBuffer url = new StringBuffer();
        // DEV NOTE: it's better to go through webappInfo now, since
        // we have easier choice of WebSiteProperties or OfbizUrlBuilder through it
        FullWebappInfo webappInfo = null;
        WebSiteProperties props = null;
        String scheme;
        int port;
        if (secure != null) {
            scheme = secure ? "https" : "http";
            port = -1;
            try {
                webappInfo = FullWebappInfo.fromRequest(request);
                props = webappInfo.getWebSiteProperties();
                String portStr = secure ? props.getHttpsPort() : props.getHttpPort();
                if (portStr != null && !portStr.isEmpty()) {
                    port = Integer.parseInt(portStr);
                }
            } catch (Exception e) {
                if (handleErrors) {
                    Debug.logError("rebuildOriginalRequestURL: Error getting web site properties for http/https port: " 
                            + e.toString() + getLogSuffix(), module);
                } else {
                    throw new IllegalStateException("Error getting web site properties for http/https port: " + e.getMessage(), e);
                }
            }
            if (port < 0) {
                port = secure ? 443 : 80;
            }
        } else {
            scheme = request.getScheme();
            port = request.getServerPort();
        }
        if (port < 0) {
            port = 80;
        }
        url.append(scheme).append("://");
        String host;
        if (Boolean.TRUE.equals(staticHost)) {
            if (webappInfo == null) {
                try {
                    webappInfo = FullWebappInfo.fromRequest(request);
                    props = webappInfo.getWebSiteProperties();
                } catch (Exception e) {
                    if (handleErrors) {
                        Debug.logError("rebuildOriginalRequestURL: Error getting web site properties for host name"
                                + " (using request.getServerName() instead): " + e.toString() + getLogSuffix(), module);
                    } else {
                        throw new IllegalStateException("Error getting web site properties for host name: " + e.getMessage(), e);
                    }
                }
            }
            if (props != null) {
                host = scheme.equals("https") ? props.getHttpsHost() : props.getHttpHost();
                if (host == null || host.isEmpty()) host = request.getServerName();
            } else {
                host = request.getServerName();
            }
        } else {
            host = request.getServerName();
        }
        url.append(host);
        if ((scheme.equals("http") && (port != 80))
            || (scheme.equals("https") && (port != 443))) {
            url.append(':').append(port);
        }

        String requestURI = (String) request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
        String queryString = (String) request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING);
        if (requestURI == null) { // NOTE: must use this test for the queryString too
            requestURI = request.getRequestURI();
            queryString = request.getQueryString();
        }
        if (includeWebappPathPrefix) {
            OfbizUrlBuilder urlBuilder = null;
            try {
                if (webappInfo == null) {
                    webappInfo = FullWebappInfo.fromRequest(request);
                }
                urlBuilder = webappInfo.getOfbizUrlBuilder();
            } catch (Exception e) {
                if (handleErrors) {
                    Debug.logError("rebuildOriginalRequestURL: Error getting url builder for webapp " + webappInfo 
                            + ": " + e.toString() + getLogSuffix(), module);
                } else {
                    throw new IllegalStateException("Error getting url builder webapp " + webappInfo + ": " + e.getMessage(), e);
                }
            }
            if (urlBuilder != null) {
                try {
                    urlBuilder.buildPathPartWithWebappPathPrefix(url);
                } catch(Exception e) {
                    if (handleErrors) {
                        Debug.logError("rebuildOriginalRequestURL: Error building webappPathPrefix for webapp " + webappInfo 
                                + ": " + e.toString() + getLogSuffix(), module);
                    } else {
                        throw new IllegalStateException("Error building webappPathPrefix for webapp " + webappInfo + ": " + e.getMessage(), e);
                    }
                }
            }
        }
        url.append(requestURI);
        if (useQueryString && queryString != null) {
            url.append("?").append(queryString);
        }
        return url.toString();
    }

    @Deprecated
    public static String rebuildOriginalRequestURL(HttpServletRequest request, HttpServletResponse response,
            Boolean secure, boolean useQueryString) {
        return rebuildOriginalRequestURL(request, response, null, secure, null, true, useQueryString, true);
    }

    public static String getServletAndPathInfo(HttpServletRequest request) {
        if (request.getPathInfo() != null) return request.getServletPath() + request.getPathInfo();
        else return request.getServletPath();
    }

    /**
     * Returns the first path elem after the slash, or null if empty or root request.
     * WARN: assumes the first char is a slash - meant for servlet API methods: getServletPath(), getPathInfo().
     */
    public static String getFirstPathElem(String path) {
        if (path == null || path.length() <= 1) return null;

        int secondSlashIndex = path.indexOf('/', 1);
        if (secondSlashIndex >= 1) {
            path = path.substring(1, secondSlashIndex);
        } else {
            path = path.substring(1);
        }

        return path;
    }

    public static String getFirstPathInfoElem(HttpServletRequest request) {
        return getFirstPathElem(request.getPathInfo());
    }

    public static String getFirstServletAndPathInfoElem(HttpServletRequest request) {
        return getFirstPathElem(getServletAndPathInfo(request));
    }

    /**
     * Simple reusable check for <code>X-Forwarded-Proto</code> HTTPS header.
     * Does NOT include <code>request.isSecure()</code> check, do separate.
     * Added 2017-11-18.
     */
    public static boolean isForwardedSecure(HttpServletRequest request) {
        String forwardedProto = request.getHeader("X-Forwarded-Proto");
        return forwardedProto != null && "https".equals(forwardedProto.toLowerCase());
    }

    /**
     * Abstracted secure flag check. Currently checks:
     * <ul>
     * <li><code>X-Forwarded-Proto</code> HTTPS header <strong>OR</strong></li>
     * <li><code>request.isSecure()</code></li>
     * </ul>
     * Added 2017-11-18.
     */
    public static boolean isEffectiveSecure(HttpServletRequest request) {
        return request.isSecure() || isForwardedSecure(request);
    }

    /**
     * Checks if the given URL starts with specified protocol, fast.
     * @param url the url
     * @param protocol the protocol, lowercase (must be)
     */
    public static boolean isUrlProtocol(final String url, final String protocol) {
        final int protocolLength = protocol.length();
        if (url.length() < (protocolLength + 1)) return false;
        if (url.charAt(protocolLength) != ':') return false;
        return protocol.equalsIgnoreCase(url.substring(0, protocolLength)); // substring+equal avoid iterating whole url
    }

    public static boolean isUrlDelim(char urlChar) {
        return (URL_DELIMS_STR.indexOf(urlChar) >= 0);
    }

    public static boolean isUrlDelimNonDir(char urlChar) {
        return (URL_DELIMS_NODIR_STR.indexOf(urlChar) >= 0);
    }

    /**
     * Returns true if the path would need a dir separator to be prefixed
     * to the path, assuming the prefix did not end with a slash.
     * <p>
     * NOTE: This assumes that the URL we're appending to does not end with a slash (caller must ensure).
     */
    public static boolean isUrlAppendNeedsDirSep(CharSequence path) {
        return (path.length() > 0) && !isUrlDelim(path.charAt(0));
    }

    /**
     * Returns true if needs path sep, false if it doesn't, or null if prefix ends with and path starts with path seps already,
     * or also true if neither prefix nor path appears to contain the starting root slash.
     * <p>
     * <code>url</code> is assumed to be an absolute URL from protocol or absolute URL (starting with slash) from server root.
     * It will not work properly if it does not contain the webappPathPrefix and contextRoot already; also
     * protocol if used should already be in the url (not prefixed afterward).
     */
    public static Boolean isUrlAppendNeedsDirSep(CharSequence path, CharSequence url) {
        if (url.length() == 0) {
            return !StringUtil.startsWith(path, '/'); // NOTE: If path empty or ?x=y, returns true so we always produce a relative link
        }
        if (path.length() == 0) {
            return false;
        }
        if (url.charAt(url.length() - 1) == '/') {
            if (path.charAt(0) == '/') {
                return null;
            }
            return false;
        }
        return !isUrlDelim(path.charAt(0));
    }

    private static String getLogSuffix() { // SCIPIO: better info when logging link errors
        return " (" + Debug.getCallerShortInfo(logCallerExcludeClasses) + ")";
    }
}