org.apache.velocity.tools.view.ImportSupport.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.velocity.tools.view.ImportSupport.java

Source

package org.apache.velocity.tools.view;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Locale;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <p>Provides methods to import arbitrary local or remote resources as strings.</p>
 * <p>Based on ImportSupport from the JSTL taglib by Shawn Bayern</p>
 *
 * @author <a href="mailto:marinoj@centrum.is">Marino A. Jonsson</a>
 * @since VelocityTools 1.1
 * @version $Revision$ $Date$
 */
public abstract class ImportSupport {

    protected static final Log LOG = LogFactory.getLog(ImportSupport.class);

    protected ServletContext application;
    protected HttpServletRequest request;
    protected HttpServletResponse response;

    protected static final String VALID_SCHEME_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-";

    /** Default character encoding for response. */
    protected static final String DEFAULT_ENCODING = "ISO-8859-1";

    //*********************************************************************
    // URL importation logic

    /*
     * Overall strategy:  we have two entry points, acquireString() and
     * acquireReader().  The latter passes data through unbuffered if
     * possible (but note that it is not always possible -- specifically
     * for cases where we must use the RequestDispatcher.  The remaining
     * methods handle the common.core logic of loading either a URL or a local
     * resource.
     *
     * We consider the 'natural' form of absolute URLs to be Readers and
     * relative URLs to be Strings.  Thus, to avoid doing extra work,
     * acquireString() and acquireReader() delegate to one another as
     * appropriate.  (Perhaps I could have spelled things out more clearly,
     * but I thought this implementation was instructive, not to mention
     * somewhat cute...)
     */

    /**
     *
     * @param url the URL resource to return as string
     * @return the URL resource as string
     * @throws IOException
     * @throws java.lang.Exception
     */
    protected String acquireString(String url) throws IOException, Exception {
        // Record whether our URL is absolute or relative
        if (isAbsoluteUrl(url)) {
            // for absolute URLs, delegate to our peer
            BufferedReader r = null;
            try {
                r = new BufferedReader(acquireReader(url));
                StringBuffer sb = new StringBuffer();
                int i;
                // under JIT, testing seems to show this simple loop is as fast
                // as any of the alternatives
                while ((i = r.read()) != -1) {
                    sb.append((char) i);
                }
                return sb.toString();
            } finally {
                if (r != null) {
                    try {
                        r.close();
                    } catch (IOException ioe) {
                        LOG.error("Could not close reader.", ioe);
                    }
                }
            }
        } else // handle relative URLs ourselves
        {
            // URL is relative, so we must be an HTTP request
            if (!(request instanceof HttpServletRequest && response instanceof HttpServletResponse)) {
                throw new Exception("Relative import from non-HTTP request not allowed");
            }

            // retrieve an appropriate ServletContext
            // normalize the URL if we have an HttpServletRequest
            if (!url.startsWith("/")) {
                String sp = ((HttpServletRequest) request).getServletPath();
                url = sp.substring(0, sp.lastIndexOf('/')) + '/' + url;
            }

            // strip the session id from the url
            url = stripSession(url);

            // from this context, get a dispatcher
            RequestDispatcher rd = application.getRequestDispatcher(url);
            if (rd == null) {
                throw new Exception("Couldn't get a RequestDispatcher for \"" + url + "\"");
            }

            // include the resource, using our custom wrapper
            ImportResponseWrapper irw = new ImportResponseWrapper((HttpServletResponse) response);
            try {
                rd.include(request, irw);
            } catch (IOException ex) {
                throw new Exception("Problem importing the relative URL \"" + url + "\". " + ex);
            } catch (RuntimeException ex) {
                throw new Exception("Problem importing the relative URL \"" + url + "\". " + ex);
            }

            // disallow inappropriate response codes per JSTL spec
            if (irw.getStatus() < 200 || irw.getStatus() > 299) {
                throw new Exception("Invalid response code '" + irw.getStatus() + "' for \"" + url + "\"");
            }

            // recover the response String from our wrapper
            return irw.getString();
        }
    }

    /**
     *
     * @param url the URL to read
     * @return a Reader for the InputStream created from the supplied URL
     * @throws IOException
     * @throws java.lang.Exception
     */
    protected Reader acquireReader(String url) throws IOException, Exception {
        if (!isAbsoluteUrl(url)) {
            // for relative URLs, delegate to our peer
            return new StringReader(acquireString(url));
        } else {
            // absolute URL
            URLConnection uc = null;
            HttpURLConnection huc = null;
            InputStream i = null;

            try {
                // handle absolute URLs ourselves, using java.net.URL
                URL u = new URL(url);
                // URL u = new URL("http", "proxy.hi.is", 8080, target);
                uc = u.openConnection();
                i = uc.getInputStream();

                // check response code for HTTP URLs, per spec,
                if (uc instanceof HttpURLConnection) {
                    huc = (HttpURLConnection) uc;

                    int status = huc.getResponseCode();
                    if (status < 200 || status > 299) {
                        throw new Exception(status + " " + url);
                    }
                }

                // okay, we've got a stream; encode it appropriately
                Reader r = null;
                String charSet;

                // charSet extracted according to RFC 2045, section 5.1
                String contentType = uc.getContentType();
                if (contentType != null) {
                    charSet = ImportSupport.getContentTypeAttribute(contentType, "charset");
                    if (charSet == null) {
                        charSet = DEFAULT_ENCODING;
                    }
                } else {
                    charSet = DEFAULT_ENCODING;
                }

                try {
                    r = new InputStreamReader(i, charSet);
                } catch (UnsupportedEncodingException ueex) {
                    r = new InputStreamReader(i, DEFAULT_ENCODING);
                }

                if (huc == null) {
                    return r;
                } else {
                    return new SafeClosingHttpURLConnectionReader(r, huc);
                }
            } catch (IOException ex) {
                if (i != null) {
                    try {
                        i.close();
                    } catch (IOException ioe) {
                        LOG.error("Could not close InputStream", ioe);
                    }
                }

                if (huc != null) {
                    huc.disconnect();
                }
                throw new Exception("Problem accessing the absolute URL \"" + url + "\". " + ex);
            } catch (RuntimeException ex) {
                if (i != null) {
                    try {
                        i.close();
                    } catch (IOException ioe) {
                        LOG.error("Could not close InputStream", ioe);
                    }
                }

                if (huc != null) {
                    huc.disconnect();
                }
                // because the spec makes us
                throw new Exception("Problem accessing the absolute URL \"" + url + "\". " + ex);
            }
        }
    }

    protected static class SafeClosingHttpURLConnectionReader extends Reader {
        private HttpURLConnection huc;
        private Reader wrappedReader;

        SafeClosingHttpURLConnectionReader(Reader r, HttpURLConnection huc) {
            this.wrappedReader = r;
            this.huc = huc;
        }

        public void close() throws IOException {
            if (null != huc) {
                huc.disconnect();
            }

            wrappedReader.close();
        }

        // Pass-through methods.
        public void mark(int readAheadLimit) throws IOException {
            wrappedReader.mark(readAheadLimit);
        }

        public boolean markSupported() {
            return wrappedReader.markSupported();
        }

        public int read() throws IOException {
            return wrappedReader.read();
        }

        public int read(char[] buf) throws IOException {
            return wrappedReader.read(buf);
        }

        public int read(char[] buf, int off, int len) throws IOException {
            return wrappedReader.read(buf, off, len);
        }

        public boolean ready() throws IOException {
            return wrappedReader.ready();
        }

        public void reset() throws IOException {
            wrappedReader.reset();
        }

        public long skip(long n) throws IOException {
            return wrappedReader.skip(n);
        }
    }

    /** Wraps responses to allow us to retrieve results as Strings. */
    protected class ImportResponseWrapper extends HttpServletResponseWrapper {
        /*
         * We provide either a Writer or an OutputStream as requested.
         * We actually have a true Writer and an OutputStream backing
         * both, since we don't want to use a character encoding both
         * ways (Writer -> OutputStream -> Writer).  So we use no
         * encoding at all (as none is relevant) when the target resource
         * uses a Writer.  And we decode the OutputStream's bytes
         * using OUR tag's 'charEncoding' attribute, or ISO-8859-1
         * as the default.  We thus ignore setLocale() and setContentType()
         * in this wrapper.
         *
         * In other words, the target's asserted encoding is used
         * to convert from a Writer to an OutputStream, which is typically
         * the medium through with the target will communicate its
         * ultimate response.  Since we short-circuit that mechanism
         * and read the target's characters directly if they're offered
         * as such, we simply ignore the target's encoding assertion.
         */

        /** The Writer we convey. */
        private StringWriter sw;

        /** A buffer, alternatively, to accumulate bytes. */
        private ByteArrayOutputStream bos;

        /** 'True' if getWriter() was called; false otherwise. */
        private boolean isWriterUsed;

        /** 'True if getOutputStream() was called; false otherwise. */
        private boolean isStreamUsed;

        /** The HTTP status set by the target. */
        private int status = 200;

        //************************************************************
        // Constructor and methods

        /**
         * Constructs a new ImportResponseWrapper.
         * @param response the response to wrap
         */
        public ImportResponseWrapper(HttpServletResponse response) {
            super(response);
        }

        /**
         * @return a Writer designed to buffer the output.
         */
        public PrintWriter getWriter() {
            if (isStreamUsed) {
                throw new IllegalStateException("Unexpected internal error during import: "
                        + "Target servlet called getWriter(), then getOutputStream()");
            }
            isWriterUsed = true;
            if (sw == null) {
                sw = new StringWriter();
            }
            return new PrintWriter(sw);
        }

        /**
         * @return a ServletOutputStream designed to buffer the output.
         */
        public ServletOutputStream getOutputStream() {
            if (isWriterUsed) {
                throw new IllegalStateException("Unexpected internal error during import: "
                        + "Target servlet called getOutputStream(), then getWriter()");
            }
            isStreamUsed = true;
            if (bos == null) {
                bos = new ByteArrayOutputStream();
            }
            ServletOutputStream sos = new ServletOutputStream() {
                public void write(int b) throws IOException {
                    bos.write(b);
                }
            };
            return sos;
        }

        /** Has no effect. */
        public void setContentType(String x) {
            // ignore
        }

        /** Has no effect. */
        public void setLocale(Locale x) {
            // ignore
        }

        /**
         * Sets the status of the response
         * @param status the status code
         */
        public void setStatus(int status) {
            this.status = status;
        }

        /**
         * @return the status of the response
         */
        public int getStatus() {
            return status;
        }

        /**
         * Retrieves the buffered output, using the containing tag's
         * 'charEncoding' attribute, or the tag's default encoding,
         * <b>if necessary</b>.
         * @return the buffered output
         * @throws UnsupportedEncodingException if the encoding is not supported
         */
        public String getString() throws UnsupportedEncodingException {
            if (isWriterUsed) {
                return sw.toString();
            } else if (isStreamUsed) {
                return bos.toString(this.getCharacterEncoding());
            } else {
                return ""; // target didn't write anything
            }
        }
    }

    //*********************************************************************
    // Public utility methods

    /**
     * Returns <tt>true</tt> if our current URL is absolute,
     * <tt>false</tt> otherwise.
     *
     * @param url the url to check out
     * @return true if the url is absolute
     */
    public static boolean isAbsoluteUrl(String url) {
        // a null URL is not absolute, by our definition
        if (url == null) {
            return false;
        }

        // do a fast, simple check first
        int colonPos;
        if ((colonPos = url.indexOf(":")) == -1) {
            return false;
        }

        // if we DO have a colon, make sure that every character
        // leading up to it is a valid scheme character
        for (int i = 0; i < colonPos; i++) {
            if (VALID_SCHEME_CHARS.indexOf(url.charAt(i)) == -1) {
                return false;
            }
        }
        // if so, we've got an absolute url
        return true;
    }

    /**
     * Strips a servlet session ID from <tt>url</tt>.  The session ID
     * is encoded as a URL "path parameter" beginning with "jsessionid=".
     * We thus remove anything we find between ";jsessionid=" (inclusive)
     * and either EOS or a subsequent ';' (exclusive).
     *
     * @param url the url to strip the session id from
     * @return the stripped url
     */
    public static String stripSession(String url) {
        StringBuffer u = new StringBuffer(url);
        int sessionStart;
        while ((sessionStart = u.toString().indexOf(";jsessionid=")) != -1) {
            int sessionEnd = u.toString().indexOf(";", sessionStart + 1);
            if (sessionEnd == -1) {
                sessionEnd = u.toString().indexOf("?", sessionStart + 1);
            }
            if (sessionEnd == -1) {
                // still
                sessionEnd = u.length();
            }
            u.delete(sessionStart, sessionEnd);
        }
        return u.toString();
    }

    /**
     * Get the value associated with a content-type attribute.
     * Syntax defined in RFC 2045, section 5.1.
     *
     * @param input the string containing the attributes
     * @param name the name of the content-type attribute
     * @return the value associated with a content-type attribute
     */
    public static String getContentTypeAttribute(String input, String name) {
        int begin;
        int end;
        int index = input.toUpperCase().indexOf(name.toUpperCase());
        if (index == -1) {
            return null;
        }
        index = index + name.length(); // positioned after the attribute name
        index = input.indexOf('=', index); // positioned at the '='
        if (index == -1) {
            return null;
        }
        index += 1; // positioned after the '='
        input = input.substring(index).trim();

        if (input.charAt(0) == '"') {
            // attribute value is a quoted string
            begin = 1;
            end = input.indexOf('"', begin);
            if (end == -1) {
                return null;
            }
        } else {
            begin = 0;
            end = input.indexOf(';');
            if (end == -1) {
                end = input.indexOf(' ');
            }
            if (end == -1) {
                end = input.length();
            }
        }
        return input.substring(begin, end).trim();
    }

}