| /* |
| * 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. |
| */ |
| |
| package org.apache.taglibs.standard.tag.common.core; |
| |
| import jakarta.servlet.RequestDispatcher; |
| import jakarta.servlet.ServletContext; |
| import jakarta.servlet.ServletException; |
| import jakarta.servlet.ServletOutputStream; |
| import jakarta.servlet.WriteListener; |
| import jakarta.servlet.http.HttpServletRequest; |
| import jakarta.servlet.http.HttpServletResponse; |
| import jakarta.servlet.http.HttpServletResponseWrapper; |
| import jakarta.servlet.jsp.JspException; |
| import jakarta.servlet.jsp.JspTagException; |
| import jakarta.servlet.jsp.PageContext; |
| import jakarta.servlet.jsp.tagext.BodyTagSupport; |
| import jakarta.servlet.jsp.tagext.TryCatchFinally; |
| import org.apache.taglibs.standard.resources.Resources; |
| import org.apache.taglibs.standard.util.UrlUtil; |
| |
| 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; |
| |
| /** |
| * <p>Support for tag handlers for <import>, the general-purpose |
| * text-importing mechanism for JSTL 1.0. The rtexprvalue and expression- |
| * evaluating libraries each have handlers that extend this class.</p> |
| * |
| * @author Shawn Bayern |
| */ |
| |
| public abstract class ImportSupport extends BodyTagSupport |
| implements TryCatchFinally, ParamParent { |
| |
| //********************************************************************* |
| // Public constants |
| |
| /** |
| * Default character encoding for response. |
| */ |
| public static final String DEFAULT_ENCODING = "ISO-8859-1"; |
| |
| //********************************************************************* |
| // Protected state |
| |
| protected String url; // 'url' attribute |
| protected String context; // 'context' attribute |
| protected String charEncoding; // 'charEncoding' attrib. |
| |
| //********************************************************************* |
| // Private state (implementation details) |
| |
| private String var; // 'var' attribute |
| private int scope; // processed 'scope' attribute |
| private String varReader; // 'varReader' attribute |
| private Reader r; // exposed reader, if relevant |
| private boolean isAbsoluteUrl; // is our URL absolute? |
| private ParamSupport.ParamManager params; // parameters |
| private String urlWithParams; // URL with parameters, if applicable |
| |
| //********************************************************************* |
| // Constructor and initialization |
| |
| public ImportSupport() { |
| super(); |
| init(); |
| } |
| |
| private void init() { |
| url = var = varReader = context = charEncoding = urlWithParams = null; |
| params = null; |
| scope = PageContext.PAGE_SCOPE; |
| } |
| |
| |
| //********************************************************************* |
| // Tag logic |
| |
| // determines what kind of import and variable exposure to perform |
| |
| @Override |
| public int doStartTag() throws JspException { |
| // Sanity check |
| if (context != null |
| && (!context.startsWith("/") || !url.startsWith("/"))) { |
| throw new JspTagException( |
| Resources.getMessage("IMPORT_BAD_RELATIVE")); |
| } |
| |
| // reset parameter-related state |
| urlWithParams = null; |
| params = new ParamSupport.ParamManager(); |
| |
| // check the URL |
| if (url == null || url.equals("")) { |
| throw new NullAttributeException("import", "url"); |
| } |
| |
| // Record whether our URL is absolute or relative |
| isAbsoluteUrl = UrlUtil.isAbsoluteUrl(url); |
| |
| try { |
| // If we need to expose a Reader, we've got to do it right away |
| if (varReader != null) { |
| r = acquireReader(); |
| pageContext.setAttribute(varReader, r); |
| } |
| } catch (IOException ex) { |
| throw new JspTagException(ex.toString(), ex); |
| } |
| |
| return EVAL_BODY_INCLUDE; |
| } |
| |
| // manages connections as necessary (creating or destroying) |
| |
| @Override |
| public int doEndTag() throws JspException { |
| try { |
| // If we didn't expose a Reader earlier... |
| if (varReader == null) { |
| // ... store it in 'var', if available ... |
| if (var != null) { |
| pageContext.setAttribute(var, acquireString(), scope); |
| } |
| // ... or simply output it, if we have nowhere to expose it |
| else { |
| pageContext.getOut().print(acquireString()); |
| } |
| } |
| return EVAL_PAGE; |
| } catch (IOException ex) { |
| throw new JspTagException(ex.toString(), ex); |
| } |
| } |
| |
| // simply rethrows its exception |
| |
| public void doCatch(Throwable t) throws Throwable { |
| throw t; |
| } |
| |
| // cleans up if appropriate |
| |
| public void doFinally() { |
| try { |
| // If we exposed a Reader in doStartTag(), close it. |
| if (varReader != null) { |
| // 'r' can be null if an exception was thrown... |
| if (r != null) { |
| r.close(); |
| } |
| pageContext.removeAttribute(varReader, PageContext.PAGE_SCOPE); |
| } |
| } catch (IOException ex) { |
| // ignore it; close() failed, but there's nothing more we can do |
| } |
| } |
| |
| // Releases any resources we may have (or inherit) |
| |
| @Override |
| public void release() { |
| init(); |
| super.release(); |
| } |
| |
| //********************************************************************* |
| // Tag attributes known at translation time |
| |
| public void setVar(String var) { |
| this.var = var; |
| } |
| |
| public void setVarReader(String varReader) { |
| this.varReader = varReader; |
| } |
| |
| public void setScope(String scope) { |
| this.scope = Util.getScope(scope); |
| } |
| |
| |
| //********************************************************************* |
| // Collaboration with subtags |
| |
| // inherit Javadoc |
| |
| public void addParameter(String name, String value) { |
| params.addParameter(name, value); |
| } |
| |
| //********************************************************************* |
| // Actual 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...) |
| */ |
| |
| private String acquireString() throws IOException, JspException { |
| if (isAbsoluteUrl) { |
| // for absolute URLs, delegate to our peer |
| BufferedReader r = new BufferedReader(acquireReader()); |
| StringBuilder sb = new StringBuilder(); |
| 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(); |
| } else { |
| // handle relative URLs ourselves |
| |
| // URL is relative, so we must be an HTTP request |
| if (!(pageContext.getRequest() instanceof HttpServletRequest |
| && pageContext.getResponse() instanceof HttpServletResponse)) { |
| throw new JspTagException( |
| Resources.getMessage("IMPORT_REL_WITHOUT_HTTP")); |
| } |
| |
| // retrieve an appropriate ServletContext |
| ServletContext c = null; |
| String targetUrl = targetUrl(); |
| if (context != null) { |
| c = pageContext.getServletContext().getContext(context); |
| } else { |
| c = pageContext.getServletContext(); |
| |
| // normalize the URL if we have an HttpServletRequest |
| if (!targetUrl.startsWith("/")) { |
| String sp = ((HttpServletRequest) |
| pageContext.getRequest()).getServletPath(); |
| targetUrl = sp.substring(0, sp.lastIndexOf('/')) |
| + '/' + targetUrl; |
| } |
| } |
| |
| if (c == null) { |
| throw new JspTagException( |
| Resources.getMessage( |
| "IMPORT_REL_WITHOUT_DISPATCHER", context, targetUrl)); |
| } |
| |
| // from this context, get a dispatcher |
| RequestDispatcher rd = c.getRequestDispatcher(stripSession(targetUrl)); |
| if (rd == null) { |
| throw new JspTagException(stripSession(targetUrl)); |
| } |
| |
| // Wrap the response so we capture the capture the output. |
| // This relies on the underlying container to return content even if this is a HEAD |
| // request. Some containers (e.g. Tomcat versions without the fix for |
| // https://bz.apache.org/bugzilla/show_bug.cgi?id=57601 ) may not do that. |
| ImportResponseWrapper irw = |
| new ImportResponseWrapper((HttpServletResponse) pageContext.getResponse()); |
| |
| // spec mandates specific error handling from include() |
| try { |
| rd.include(pageContext.getRequest(), irw); |
| } catch (IOException | RuntimeException ex) { |
| throw new JspException(ex); |
| } catch (ServletException ex) { |
| Throwable rc = ex.getRootCause(); |
| while (rc instanceof ServletException) { |
| rc = ((ServletException) rc).getRootCause(); |
| } |
| if (rc == null) { |
| throw new JspException(ex); |
| } else { |
| throw new JspException(rc); |
| } |
| } |
| |
| // disallow inappropriate response codes per JSTL spec |
| if (irw.getStatus() < 200 || irw.getStatus() > 299) { |
| throw new JspTagException(irw.getStatus() + " " + |
| stripSession(targetUrl)); |
| } |
| |
| // recover the response String from our wrapper |
| return irw.getString(); |
| } |
| } |
| |
| private Reader acquireReader() throws IOException, JspException { |
| if (!isAbsoluteUrl) { |
| // for relative URLs, delegate to our peer |
| return new StringReader(acquireString()); |
| } else { |
| // absolute URL |
| String target = targetUrl(); |
| try { |
| // handle absolute URLs ourselves, using java.net.URL |
| URL u = new URL(target); |
| URLConnection uc = u.openConnection(); |
| InputStream i = uc.getInputStream(); |
| |
| // okay, we've got a stream; encode it appropriately |
| Reader r = null; |
| String charSet; |
| if (charEncoding != null && !charEncoding.equals("")) { |
| charSet = charEncoding; |
| } else { |
| // charSet extracted according to RFC 2045, section 5.1 |
| String contentType = uc.getContentType(); |
| if (contentType != null) { |
| charSet = Util.getContentTypeAttribute(contentType, "charset"); |
| if (charSet == null) { |
| charSet = DEFAULT_ENCODING; |
| } |
| } else { |
| charSet = DEFAULT_ENCODING; |
| } |
| } |
| try { |
| r = new InputStreamReader(i, charSet); |
| } catch (Exception ex) { |
| r = new InputStreamReader(i, DEFAULT_ENCODING); |
| } |
| |
| // check response code for HTTP URLs before returning, per spec, |
| // before returning |
| if (uc instanceof HttpURLConnection) { |
| int status = ((HttpURLConnection) uc).getResponseCode(); |
| if (status < 200 || status > 299) { |
| throw new JspTagException(status + " " + target); |
| } |
| } |
| |
| return r; |
| } catch (IOException ex) { |
| throw new JspException( |
| Resources.getMessage("IMPORT_ABS_ERROR", target, ex), ex); |
| } catch (RuntimeException ex) { // because the spec makes us |
| throw new JspException( |
| Resources.getMessage("IMPORT_ABS_ERROR", target, ex), ex); |
| } |
| } |
| } |
| |
| /** |
| * Wraps responses to allow us to retrieve results as Strings. |
| */ |
| private class ImportResponseWrapper extends HttpServletResponseWrapper { |
| |
| //************************************************************ |
| // Overview |
| |
| /* |
| * 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. |
| */ |
| |
| //************************************************************ |
| // Data |
| |
| /** |
| * The Writer we convey. |
| */ |
| private StringWriter sw = new StringWriter(); |
| |
| /** |
| * A buffer, alternatively, to accumulate bytes. |
| */ |
| private ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
| |
| /** |
| * A ServletOutputStream we convey, tied to this Writer. |
| */ |
| private ServletOutputStream sos = new ServletOutputStream() { |
| |
| private WriteListener writeListener; |
| |
| @Override |
| public void write(int b) throws IOException { |
| try { |
| bos.write(b); |
| } catch ( Exception e ) { |
| if(this.writeListener!=null) { |
| this.writeListener.onError( e ); |
| } |
| throw new IOException(e); |
| } |
| } |
| |
| @Override |
| public boolean isReady() { |
| return true; |
| } |
| |
| @Override |
| public void setWriteListener( WriteListener writeListener ) { |
| this.writeListener = writeListener; |
| } |
| }; |
| |
| /** |
| * '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. |
| */ |
| public ImportResponseWrapper(HttpServletResponse response) { |
| super(response); |
| } |
| |
| /** |
| * Returns a Writer designed to buffer the output. |
| */ |
| @Override |
| public PrintWriter getWriter() { |
| if (isStreamUsed) { |
| throw new IllegalStateException( |
| Resources.getMessage("IMPORT_ILLEGAL_STREAM")); |
| } |
| isWriterUsed = true; |
| return new PrintWriter(sw); |
| } |
| |
| /** |
| * Returns a ServletOutputStream designed to buffer the output. |
| */ |
| @Override |
| public ServletOutputStream getOutputStream() { |
| if (isWriterUsed) { |
| throw new IllegalStateException( |
| Resources.getMessage("IMPORT_ILLEGAL_WRITER")); |
| } |
| isStreamUsed = true; |
| return sos; |
| } |
| |
| /** |
| * Has no effect. |
| */ |
| @Override |
| public void setContentType(String x) { |
| // ignore |
| } |
| |
| /** |
| * Has no effect. |
| */ |
| @Override |
| public void setLocale(Locale x) { |
| // ignore |
| } |
| |
| @Override |
| public void setStatus(int status) { |
| this.status = status; |
| } |
| |
| 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>. |
| */ |
| // not simply toString() because we need to throw |
| // UnsupportedEncodingException |
| public String getString() throws UnsupportedEncodingException { |
| if (isWriterUsed) { |
| return sw.toString(); |
| } else if (isStreamUsed) { |
| if (charEncoding != null && !charEncoding.equals("")) { |
| return bos.toString(charEncoding); |
| } else { |
| return bos.toString(DEFAULT_ENCODING); |
| } |
| } else { |
| return ""; |
| } // target didn't write anything |
| } |
| } |
| |
| //********************************************************************* |
| // Some private utility methods |
| |
| /** |
| * Returns our URL (potentially with parameters) |
| */ |
| private String targetUrl() { |
| if (urlWithParams == null) { |
| urlWithParams = params.aggregateParams(url); |
| } |
| return urlWithParams; |
| } |
| |
| |
| //********************************************************************* |
| // Public utility methods |
| |
| /** |
| * 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). |
| */ |
| public static String stripSession(String url) { |
| StringBuilder u = new StringBuilder(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(); |
| } |
| } |