org.openlaszlo.utils.LZHttpUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.openlaszlo.utils.LZHttpUtils.java

Source

/******************************************************************************
 * LZHttpUtils.java
 * ****************************************************************************/

/* J_LZ_COPYRIGHT_BEGIN *******************************************************
* Copyright 2001-2008, 2010, 2011 Laszlo Systems, Inc.  All Rights Reserved.  *
* Use is subject to license terms.                                            *
* J_LZ_COPYRIGHT_END *********************************************************/

package org.openlaszlo.utils;

import java.io.File;
import java.net.URL;
import java.util.Locale;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.Date;
import java.util.Enumeration;
import java.security.*;

import org.apache.log4j.*;

import org.openlaszlo.utils.ChainedException;

/**
 * Utility class for http servlets
 */
public class LZHttpUtils {

    private static Logger mLogger = Logger.getLogger(LZHttpUtils.class);

    public static final String CONTENT_ENCODING = "Content-Encoding";
    public static final String CONTENT_LENGTH = "Content-Length";
    public static final String CONTENT_TYPE = "Content-Type";
    public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
    public static final String LAST_MODIFIED = "Last-Modified";
    public static final String IF_NONE_MATCH = "If-None-Match";
    public static final String TRANSFER_ENCODING = "Transfer-Encoding";
    public static final String HOST = "Host";
    public static final String CONNECTION = "Connection";
    public static final String AUTHORIZATION = "Authorization";
    public static final String COOKIE = "Cookie";
    public static final String CACHE_CONTROL = "Cache-Control";
    public static final String USER_AGENT = "User-Agent";
    public static final String ACCEPT_ENCODING = "Accept-Encoding";
    public static final String RANGE = "Range";
    public static final String ACCEPT_RANGES = "Accept-Ranges";
    public static final String IF_RANGE = "If-Range";

    public static final String NO_STORE = "no-store";
    public static final String NO_CACHE = "no-cache";

    /**
     * @return the URL for the request // with the query string?
     * @param req the request
     */
    public static URL getRequestURL(HttpServletRequest req) {
        StringBuffer surl = req.getRequestURL();
        if (surl.indexOf("https") == 0) {
            try {
                System.setProperty("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol");
                Class<?> provClass = Class.forName("com.sun.net.ssl.internal.ssl.Provider");
                Provider provider = (Provider) provClass.newInstance();
                Security.addProvider(provider);
            } catch (InstantiationException e) {
                throw new ChainedException(e);
            } catch (IllegalAccessException e) {
                throw new ChainedException(e);
            } catch (ClassNotFoundException e) {
                throw new ChainedException(e);
            }
        }
        // surl.append("?");
        // surl.append(req.getQueryString());
        try {
            return new URL(surl.toString());
        } catch (Exception e) {
            throw new ChainedException(e);
        }
    }

    /**
     * For formatting HTTP dates
     */

    /**
     * Return a formatter for HTTP date headers 
     */
    private static SimpleDateFormat getGMTFormatter() {
        SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", java.util.Locale.US);
        TimeZone tz = TimeZone.getTimeZone("GMT");
        dateFormatter.setTimeZone(tz);
        return dateFormatter;
    }

    /**
     * Convert a long utc value to an HTTP Date String (TZ must be GMT)
     */
    public static String getDateString(long d) {
        SimpleDateFormat dateFormatter = getGMTFormatter();
        return dateFormatter.format(new Date(d));
    }

    /**
     * Convert an HTTP Date String to a long 
     * @return the long or -1 if the string doesn't parse correctly.
     */
    public static long getDate(String s) {
        if (s == null || "".equals(s)) {
            return -1;
        }
        try {
            SimpleDateFormat dateFormatter = getGMTFormatter();
            return dateFormatter.parse(s).getTime();
        } catch (java.text.ParseException e) {
            mLogger.warn(
                    /* (non-Javadoc)
                     * @i18n.test
                     * @org-mes="bad date string"
                     */
                    org.openlaszlo.i18n.LaszloMessages.getMessage(LZHttpUtils.class.getName(), "051018-136"), e);
            return -1;
        }
    }

    /** From RFC2616, 14.10:        
     *
     * HTTP/1.1 proxies MUST parse the Connection header field before a message
     * is forwarded and, for each connection-token in this field, remove any
     * header field(s) from the message with the same name as the
     * connection-token. Connection options are signaled by the presence of a
     * connection-token in the Connection header field, not by any corresponding
     * additional header field(s), since the additional header field may not be
     * sent if there are no parameters associated with that connection
     * option. */
    static public boolean allowForward(String header, Enumeration<?> connEnum) {
        if (header.toLowerCase(Locale.ENGLISH).startsWith("content-"))
            return false;

        if (header.equalsIgnoreCase(CONNECTION))
            return false;

        if (header.equalsIgnoreCase(HOST))
            return false;

        if (header.equalsIgnoreCase(TRANSFER_ENCODING))
            return false;

        if (header.equalsIgnoreCase(IF_MODIFIED_SINCE))
            return false;

        if (header.equalsIgnoreCase(LAST_MODIFIED))
            return false;

        if (header.equalsIgnoreCase(IF_NONE_MATCH))
            return false;

        if (header.equalsIgnoreCase(ACCEPT_ENCODING))
            return false;

        // Someday we may allow these only when the proxy is non-transcoding
        if (header.equalsIgnoreCase(RANGE))
            return false;

        // Someday we may allow these only when the proxy is non-transcoding
        if (header.equalsIgnoreCase(ACCEPT_RANGES))
            return false;

        // Someday we may allow these only when the proxy is non-transcoding
        if (header.equalsIgnoreCase(IF_RANGE))
            return false;

        // Don't forward any headers that have the same name as a connection
        // token.
        if (connEnum != null) {
            while (connEnum.hasMoreElements()) {
                String token = (String) connEnum.nextElement();
                if (header.equalsIgnoreCase(token))
                    return false;
            }
        }

        return true;
    }

    /** Add request headers into method. 
     *
     * @param req http servlet request object
     * @param method method to insert request headers into 
     */
    static public void proxyRequestHeaders(HttpServletRequest req, HttpMethodBase method) {
        mLogger.debug("proxyRequestHeaders");

        // Copy all headers, if the servlet container allows, otherwise just
        // copy the cookie header and log a message.
        Enumeration<?> headerNames = req.getHeaderNames();
        if (headerNames != null) {

            // Connection header tokens not to forward
            Enumeration<?> connEnum = req.getHeaders(CONNECTION);

            while (headerNames.hasMoreElements()) {
                String key = (String) headerNames.nextElement();
                if (allowForward(key, connEnum)) {
                    String val = (String) req.getHeader(key);
                    method.addRequestHeader(key, val);
                    mLogger.debug("  " + key + "=" + val);
                }
            }

        } else {
            mLogger.warn(
                    /* (non-Javadoc)
                     * @i18n.test
                     * @org-mes="Can't get header names to proxy request headers"
                     */
                    org.openlaszlo.i18n.LaszloMessages.getMessage(LZHttpUtils.class.getName(), "051018-237"));

            Enumeration<?> cookieEnum = req.getHeaders(COOKIE);
            if (cookieEnum != null) {
                while (cookieEnum.hasMoreElements()) {
                    String val = (String) cookieEnum.nextElement();
                    method.addRequestHeader(COOKIE, val);
                    mLogger.debug("  Cookie=" + val);
                }
            }

            Enumeration<?> authEnum = req.getHeaders(AUTHORIZATION);
            if (authEnum != null) {
                while (authEnum.hasMoreElements()) {
                    String val = (String) authEnum.nextElement();
                    method.addRequestHeader(AUTHORIZATION, val);
                    mLogger.debug("  Authorization=" + val);
                }
            }
        }
    }

    /** Pull response headers from method and put into 
     * servlet response object.
     *
     * @param method method to proxy from
     * @param res http servlet response object to proxy to
     * @param isSecure true if get method is secure
     */
    static public void proxyResponseHeaders(HttpMethodBase meth, HttpServletResponse res, boolean isSecure) {
        mLogger.debug("proxyResponseHeaders");

        Header[] hedz = meth.getResponseHeaders();

        for (int i = 0; i < hedz.length; i++) {
            String name = hedz[i].getName();
            String value = hedz[i].getValue();
            // Content length passed back to swf app will be different
            if (allowForward(name, null)) {
                // Don't send no-cache headers request is SSL; IE 6 has
                // problems.
                if (isSecure) {
                    if (name.equals("Pragma") && value.equals("no-cache"))
                        continue;
                    if (name.equals("Cache-Control") && value.equals("no-cache"))
                        continue;
                }

                mLogger.debug("  " + name + "=" + value);

                try {
                    if (name.equals("Date") || name.equals("Server")) {
                        res.setHeader(name, value);
                    } else {
                        res.addHeader(name, value);
                    }
                } catch (Exception e) {
                    mLogger.error(
                            /* (non-Javadoc)
                             * @i18n.test
                             * @org-mes="Exception when proxying a response header: " + p[0]
                             */
                            org.openlaszlo.i18n.LaszloMessages.getMessage(LZHttpUtils.class.getName(), "051018-304",
                                    new Object[] { e.getMessage() }));
                }
            }
        }
    }

    /**
     * Fetch cookie value for a particular cookie name.
     *
     * @param req servlet request.
     * @param name name of cookie key to fetch.
     */
    static public String getCookie(HttpServletRequest req, String name) {
        javax.servlet.http.Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (int i = 0; i < cookies.length; i++) {
                javax.servlet.http.Cookie cookie = cookies[i];
                if (cookie.getName().equals(name)) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }

    /** 
     * Replace real path forward slash characters to back-slash for Windoze.
     * This is to get around a WebSphere problem (see bug 988). Note that if the
     * web application content is being served directly from a .war file, this
     * method will return null. See ServletContext.getRealPath() for more
     * details.
     *
     * @param ctxt servlet context
     * @param path virtual webapp path to resolve into a real path
     * @return the real path, or null if the translation cannot be performed
     */
    static public String getRealPath(ServletContext ctxt, String path) {
        String realPath = ctxt.getRealPath(path);
        if (realPath != null && File.separatorChar == '\\')
            realPath = realPath.replace('/', '\\');
        try {
            return new File(realPath).getCanonicalPath();
        } catch (java.io.IOException e) {
            throw new org.openlaszlo.utils.ChainedException(e);
        }
    }

    /**
     * Replace real path forward slash characters to back-slash for Windoze.
     *
     * @param ctxt servlet context
     * @param req generating request
     * @return the real path, or null if the translation cannot be performed
     */
    static public String getRealPath(ServletContext ctxt, HttpServletRequest req) {
        String uriwithoutcontext = req.getRequestURI().substring(req.getContextPath().length());
        if (uriwithoutcontext != null && File.separatorChar == '\\') {
            uriwithoutcontext = uriwithoutcontext.replace('/', '\\');
        }
        return getRealPath(ctxt, "/") + uriwithoutcontext;
    }

    private static String WEBAPP = "/@WEBAPP@/";

    /** 
     * If a URL contains <code>/@WEBAPP@</code>, replaces that string with
     * context path. If context path is <code>/</code>, the function just
     * removes the <code>/@WEBAPP@</code> string.
     *
     * @param url URL to check if <code>/@WEBAPP@</code> token exists.
     * @return if <code>/@WEBAPP@</code> exists, new modified URL else old URL.
     */
    public static String modifyWEBAPP(HttpServletRequest req, String url) {
        mLogger.debug("modifyWEBAPP");
        if (url.startsWith(WEBAPP)) {
            mLogger.debug("    Old URL: " + url);
            String protocol = (req.isSecure() ? "https" : "http");
            String host = req.getServerName();
            int port = req.getServerPort();
            String cp = req.getContextPath();
            url = protocol + "://" + host + ":" + port + cp + url.substring(WEBAPP.length() - 1);
            mLogger.debug("    New URL: " + url);
        }
        return url;
    }

    /**
     * Mark response with no-store cache control
     */
    static public void noStore(HttpServletResponse res) {
        if (res.containsHeader(LZHttpUtils.CACHE_CONTROL)) {
            mLogger.warn(
                    /* (non-Javadoc)
                     * @i18n.test
                     * @org-mes="over-riding back-end cache-control header to: no-store"
                     */
                    org.openlaszlo.i18n.LaszloMessages.getMessage(LZHttpUtils.class.getName(), "051018-408"));
        }
        res.setHeader("Cache-Control", "cache, must-revalidate");
        res.setHeader("Pragma", "public");
        //        res.setHeader(CACHE_CONTROL, NO_STORE);
    }

    /**
     * Return a URI object, escaping input only if needed
     */
    static public URI newURI(String s) throws URIException {
        try {
            return new URI(s, true);
        } catch (URIException urie) {
            // Try escaping
            try {
                return new URI(s, false);
            } catch (Exception e) {
                // Escaping failed, throw the original error
                throw urie;
            }
        }
    }

    /**
       Parse out options value from lzoptions, of the form
       ?lzoptions=runtime(dhtml),wrapper(html),debug(false),proxy(true)
       For boolean options, we default that if they are present they are true, if not they are false so:
       ?lzoptions=runtime(dhtml),wrapper(html),proxy
       would be the same as the above.
        
       multiple comma separated values can be passed in args list, e.g.,
        
       ?lzoptions=runtime(swf10),package(widget,android)
        
       @return hashmap of key=>String
     */
    private static final int KEY = 1;
    private static final int ARGS = 2;

    public static HashMap<String, String> getLzRequestOptions(String query) {
        HashMap<String, String> options = new HashMap<String, String>();
        int mystate = KEY;
        StringBuilder vals = null;
        String lastkey = null;
        int nvals = 0;

        StringTokenizer st = new StringTokenizer(query, ",()", true);
        while (st.hasMoreTokens()) {
            String token = st.nextToken();
            switch (mystate) {
            case KEY:
                if (token.equals(",")) {
                    // we only saw key name, but no value, so give it an implicit true value.
                    if (lastkey != null && nvals == 0) {
                        options.put(lastkey, "true");
                    }
                } else if (token.equals("(")) {
                    mystate = ARGS;
                    vals = new StringBuilder();
                } else {
                    lastkey = token;
                }
                break;
            case ARGS:
                if (token.equals(")")) {
                    options.put(lastkey, vals.toString());
                    lastkey = null;
                    mystate = KEY;
                    nvals = 0;
                } else if (token.equals(",")) {
                    vals.append(",");
                } else {
                    vals.append(token);
                    nvals++;
                }
                break;
            }
        }
        if (lastkey != null && nvals == 0) {
            options.put(lastkey, "true");
        }

        return options;
    }

    /* look up KEY and return String containing comma separated list of values */
    public static String getLzOption(String key, HashMap<String, String> options) {
        return options.get(key);
    }

    /* Returns list of one or more value for KEY */
    public static List<String> getLzOptionList(String key, HashMap<String, String> options) {
        String vals = (options.get(key));
        if (vals == null) {
            return null;
        } else {
            return Arrays.asList(vals.split(","));
        }
    }

    /* mapping of new lzoption names to old query arg names.
       Keep this in sync with the map in WEB-INF/lps/lfc/services/LzBrowser.lzs #getLzOption
    */
    static final HashMap<String, String> optionNameMap = new HashMap<String, String>();
    static {
        optionNameMap.put("runtime", "lzr");
        optionNameMap.put("backtrace", "lzbacktrace");
        optionNameMap.put("proxied", "lzproxied");
        optionNameMap.put("usemastersprite", "lzusemastersprite");
    }

    // Look for new style lzoptions, then fall back to old style
    // discrete query args.
    //
    // Since we moved to the 'lzoptions' string, some options have
    // changed name.  We make these substitutions if we're falling
    // back to look at query args instead of lzoptions string:
    //
    // runtime => lzr
    // backtrace => lzbacktrace
    //
    public static String getLzOption(String key, HttpServletRequest req) {
        String lzoptions = req.getParameter("lzoptions");
        String val = null;
        if (lzoptions != null) {
            HashMap<String, String> optionsmap = LZHttpUtils.getLzRequestOptions(lzoptions);
            val = LZHttpUtils.getLzOption(key, optionsmap);
            if (val != null) {
                return val;
            }
        }

        // Fallback to looking up key in regular query args
        // "lzr" is the old way of specifying 'runtime' as a query arg
        String mkey = optionNameMap.get(key);
        if (mkey != null) {
            key = mkey;
        }

        return req.getParameter(key);
    }

}