001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * Portions Copyright Apache Software Foundation.
007: *
008: * The contents of this file are subject to the terms of either the GNU
009: * General Public License Version 2 only ("GPL") or the Common Development
010: * and Distribution License("CDDL") (collectively, the "License"). You
011: * may not use this file except in compliance with the License. You can obtain
012: * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
013: * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
014: * language governing permissions and limitations under the License.
015: *
016: * When distributing the software, include this License Header Notice in each
017: * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
018: * Sun designates this particular file as subject to the "Classpath" exception
019: * as provided by Sun in the GPL Version 2 section of the License file that
020: * accompanied this code. If applicable, add the following below the License
021: * Header, with the fields enclosed by brackets [] replaced by your own
022: * identifying information: "Portions Copyrighted [year]
023: * [name of copyright owner]"
024: *
025: * Contributor(s):
026: *
027: * If you wish your version of this file to be governed by only the CDDL or
028: * only the GPL Version 2, indicate your decision by adding "[Contributor]
029: * elects to include this software in this distribution under the [CDDL or GPL
030: * Version 2] license." If you don't indicate a single choice of license, a
031: * recipient has the option to distribute your version of this file under
032: * either the CDDL, the GPL Version 2 or to extend the choice of license to
033: * its licensees as provided above. However, if you add GPL Version 2 code
034: * and therefore, elected the GPL Version 2 license, then the option applies
035: * only if the new code is made subject to such option by the copyright
036: * holder.
037: */
038:
039: package org.apache.taglibs.standard.tag.common.core;
040:
041: import java.io.BufferedReader;
042: import java.io.ByteArrayOutputStream;
043: import java.io.IOException;
044: import java.io.InputStream;
045: import java.io.InputStreamReader;
046: import java.io.Writer;
047: import java.io.PrintWriter;
048: import java.io.Reader;
049: import java.io.StringReader;
050: import java.io.StringWriter;
051: import java.io.UnsupportedEncodingException;
052: import java.net.HttpURLConnection;
053: import java.net.URL;
054: import java.net.URLConnection;
055: import java.util.Locale;
056:
057: import javax.servlet.RequestDispatcher;
058: import javax.servlet.ServletContext;
059: import javax.servlet.ServletException;
060: import javax.servlet.ServletOutputStream;
061: import javax.servlet.http.HttpServletRequest;
062: import javax.servlet.http.HttpServletResponse;
063: import javax.servlet.http.HttpServletResponseWrapper;
064: import javax.servlet.jsp.JspException;
065: import javax.servlet.jsp.JspTagException;
066: import javax.servlet.jsp.PageContext;
067: import javax.servlet.jsp.tagext.BodyTagSupport;
068: import javax.servlet.jsp.tagext.TryCatchFinally;
069:
070: import org.apache.taglibs.standard.resources.Resources;
071:
072: /**
073: * <p>Support for tag handlers for <import>, the general-purpose
074: * text-importing mechanism for JSTL 1.0. The rtexprvalue and expression-
075: * evaluating libraries each have handlers that extend this class.</p>
076: *
077: * @author Shawn Bayern
078: */
079:
080: public abstract class ImportSupport extends BodyTagSupport implements
081: TryCatchFinally, ParamParent {
082:
083: //*********************************************************************
084: // Public constants
085:
086: /** <p>Valid characters in a scheme.</p>
087: * <p>RFC 1738 says the following:</p>
088: * <blockquote>
089: * Scheme names consist of a sequence of characters. The lower
090: * case letters "a"--"z", digits, and the characters plus ("+"),
091: * period ("."), and hyphen ("-") are allowed. For resiliency,
092: * programs interpreting URLs should treat upper case letters as
093: * equivalent to lower case in scheme names (e.g., allow "HTTP" as
094: * well as "http").
095: * </blockquote>
096: * <p>We treat as absolute any URL that begins with such a scheme name,
097: * followed by a colon.</p>
098: */
099: public static final String VALID_SCHEME_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-";
100:
101: /** Default character encoding for response. */
102: public static final String DEFAULT_ENCODING = "ISO-8859-1";
103:
104: //*********************************************************************
105: // Protected state
106:
107: protected String url; // 'url' attribute
108: protected String context; // 'context' attribute
109: protected String charEncoding; // 'charEncoding' attrib.
110:
111: //*********************************************************************
112: // Private state (implementation details)
113:
114: private String var; // 'var' attribute
115: private int scope; // processed 'scope' attribute
116: private String varReader; // 'varReader' attribute
117: private Reader r; // exposed reader, if relevant
118: private boolean isAbsoluteUrl; // is our URL absolute?
119: private ParamSupport.ParamManager params; // parameters
120: private String urlWithParams; // URL with parameters, if applicable
121:
122: //*********************************************************************
123: // Constructor and initialization
124:
125: public ImportSupport() {
126: super ();
127: init();
128: }
129:
130: private void init() {
131: url = var = varReader = context = charEncoding = urlWithParams = null;
132: params = null;
133: scope = PageContext.PAGE_SCOPE;
134: }
135:
136: //*********************************************************************
137: // Tag logic
138:
139: // determines what kind of import and variable exposure to perform
140: public int doStartTag() throws JspException {
141: // Sanity check
142: if (context != null
143: && (!context.startsWith("/") || !url.startsWith("/"))) {
144: throw new JspTagException(Resources
145: .getMessage("IMPORT_BAD_RELATIVE"));
146: }
147:
148: // reset parameter-related state
149: urlWithParams = null;
150: params = new ParamSupport.ParamManager();
151:
152: // check the URL
153: if (url == null || url.equals(""))
154: throw new NullAttributeException("import", "url");
155:
156: // Record whether our URL is absolute or relative
157: isAbsoluteUrl = isAbsoluteUrl();
158:
159: try {
160: // If we need to expose a Reader, we've got to do it right away
161: if (varReader != null) {
162: r = acquireReader();
163: pageContext.setAttribute(varReader, r);
164: }
165: } catch (IOException ex) {
166: throw new JspTagException(ex.toString(), ex);
167: }
168:
169: return EVAL_BODY_INCLUDE;
170: }
171:
172: // manages connections as necessary (creating or destroying)
173: public int doEndTag() throws JspException {
174: try {
175: // If we didn't expose a Reader earlier...
176: if (varReader == null) {
177: // ... store it in 'var', if available ...
178: if (var != null)
179: pageContext.setAttribute(var, acquireString(),
180: scope);
181: // ... or simply output it, if we have nowhere to expose it
182: else
183: pageContext.getOut().print(acquireString());
184: }
185: return EVAL_PAGE;
186: } catch (IOException ex) {
187: throw new JspTagException(ex.toString(), ex);
188: }
189: }
190:
191: // simply rethrows its exception
192: public void doCatch(Throwable t) throws Throwable {
193: throw t;
194: }
195:
196: // cleans up if appropriate
197: public void doFinally() {
198: try {
199: // If we exposed a Reader in doStartTag(), close it.
200: if (varReader != null) {
201: // 'r' can be null if an exception was thrown...
202: if (r != null)
203: r.close();
204: pageContext.removeAttribute(varReader,
205: PageContext.PAGE_SCOPE);
206: }
207: } catch (IOException ex) {
208: // ignore it; close() failed, but there's nothing more we can do
209: }
210: }
211:
212: // Releases any resources we may have (or inherit)
213: public void release() {
214: init();
215: super .release();
216: }
217:
218: //*********************************************************************
219: // Tag attributes known at translation time
220:
221: public void setVar(String var) {
222: this .var = var;
223: }
224:
225: public void setVarReader(String varReader) {
226: this .varReader = varReader;
227: }
228:
229: public void setScope(String scope) {
230: this .scope = Util.getScope(scope);
231: }
232:
233: //*********************************************************************
234: // Collaboration with subtags
235:
236: // inherit Javadoc
237: public void addParameter(String name, String value) {
238: params.addParameter(name, value);
239: }
240:
241: //*********************************************************************
242: // Actual URL importation logic
243:
244: /*
245: * Overall strategy: we have two entry points, acquireString() and
246: * acquireReader(). The latter passes data through unbuffered if
247: * possible (but note that it is not always possible -- specifically
248: * for cases where we must use the RequestDispatcher. The remaining
249: * methods handle the common.core logic of loading either a URL or a local
250: * resource.
251: *
252: * We consider the 'natural' form of absolute URLs to be Readers and
253: * relative URLs to be Strings. Thus, to avoid doing extra work,
254: * acquireString() and acquireReader() delegate to one another as
255: * appropriate. (Perhaps I could have spelled things out more clearly,
256: * but I thought this implementation was instructive, not to mention
257: * somewhat cute...)
258: */
259:
260: private String acquireString() throws IOException, JspException {
261: if (isAbsoluteUrl) {
262: // for absolute URLs, delegate to our peer
263: BufferedReader r = new BufferedReader(acquireReader());
264: StringBuffer sb = new StringBuffer();
265: int i;
266:
267: // under JIT, testing seems to show this simple loop is as fast
268: // as any of the alternatives
269: //
270: // gmurray71 : putting in try/catch/finally block to make sure the
271: // reader is closed to fix a bug with file descriptors being left open
272: try {
273: while ((i = r.read()) != -1)
274: sb.append((char) i);
275: } catch (IOException iox) {
276: throw iox;
277: } finally {
278: r.close();
279: }
280:
281: return sb.toString();
282: } else {
283: // handle relative URLs ourselves
284:
285: // URL is relative, so we must be an HTTP request
286: if (!(pageContext.getRequest() instanceof HttpServletRequest && pageContext
287: .getResponse() instanceof HttpServletResponse))
288: throw new JspTagException(Resources
289: .getMessage("IMPORT_REL_WITHOUT_HTTP"));
290:
291: // retrieve an appropriate ServletContext
292: ServletContext c = null;
293: String targetUrl = targetUrl();
294: if (context != null)
295: c = pageContext.getServletContext().getContext(context);
296: else {
297: c = pageContext.getServletContext();
298:
299: // normalize the URL if we have an HttpServletRequest
300: if (!targetUrl.startsWith("/")) {
301: String sp = ((HttpServletRequest) pageContext
302: .getRequest()).getServletPath();
303: targetUrl = sp.substring(0, sp.lastIndexOf('/'))
304: + '/' + targetUrl;
305: }
306: }
307:
308: if (c == null) {
309: throw new JspTagException(Resources.getMessage(
310: "IMPORT_REL_WITHOUT_DISPATCHER", context,
311: targetUrl));
312: }
313:
314: // from this context, get a dispatcher
315: RequestDispatcher rd = c
316: .getRequestDispatcher(stripSession(targetUrl));
317: if (rd == null)
318: throw new JspTagException(stripSession(targetUrl));
319:
320: // include the resource, using our custom wrapper
321: ImportResponseWrapper irw = new ImportResponseWrapper(
322: pageContext);
323:
324: // spec mandates specific error handling form include()
325: try {
326: rd.include(pageContext.getRequest(), irw);
327: } catch (IOException ex) {
328: throw new JspException(ex);
329: } catch (RuntimeException ex) {
330: throw new JspException(ex);
331: } catch (ServletException ex) {
332: Throwable rc = ex.getRootCause();
333: if (rc == null)
334: throw new JspException(ex);
335: else
336: throw new JspException(rc);
337: }
338:
339: // disallow inappropriate response codes per JSTL spec
340: if (irw.getStatus() < 200 || irw.getStatus() > 299) {
341: throw new JspTagException(irw.getStatus() + " "
342: + stripSession(targetUrl));
343: }
344:
345: // recover the response String from our wrapper
346: return irw.getString();
347: }
348: }
349:
350: private Reader acquireReader() throws IOException, JspException {
351: if (!isAbsoluteUrl) {
352: // for relative URLs, delegate to our peer
353: return new StringReader(acquireString());
354: } else {
355: // absolute URL
356: String target = targetUrl();
357: try {
358: // handle absolute URLs ourselves, using java.net.URL
359: URL u = new URL(target);
360: URLConnection uc = u.openConnection();
361: InputStream i = uc.getInputStream();
362:
363: // okay, we've got a stream; encode it appropriately
364: Reader r = null;
365: String charSet;
366: if (charEncoding != null && !charEncoding.equals("")) {
367: charSet = charEncoding;
368: } else {
369: // charSet extracted according to RFC 2045, section 5.1
370: String contentType = uc.getContentType();
371: if (contentType != null) {
372: charSet = Util.getContentTypeAttribute(
373: contentType, "charset");
374: if (charSet == null)
375: charSet = DEFAULT_ENCODING;
376: } else {
377: charSet = DEFAULT_ENCODING;
378: }
379: }
380: try {
381: r = new InputStreamReader(i, charSet);
382: } catch (Exception ex) {
383: r = new InputStreamReader(i, DEFAULT_ENCODING);
384: }
385:
386: // check response code for HTTP URLs before returning, per spec,
387: // before returning
388: if (uc instanceof HttpURLConnection) {
389: int status = ((HttpURLConnection) uc)
390: .getResponseCode();
391: if (status < 200 || status > 299)
392: throw new JspTagException(status + " " + target);
393: }
394: return r;
395: } catch (IOException ex) {
396: throw new JspException(Resources.getMessage(
397: "IMPORT_ABS_ERROR", target, ex), ex);
398: } catch (RuntimeException ex) { // because the spec makes us
399: throw new JspException(Resources.getMessage(
400: "IMPORT_ABS_ERROR", target, ex), ex);
401: }
402: }
403: }
404:
405: /** Wraps responses to allow us to retrieve results as Strings. */
406: private class ImportResponseWrapper extends
407: HttpServletResponseWrapper {
408:
409: //************************************************************
410: // Overview
411:
412: /*
413: * We provide either a Writer or an OutputStream as requested.
414: * We actually have a true Writer and an OutputStream backing
415: * both, since we don't want to use a character encoding both
416: * ways (Writer -> OutputStream -> Writer). So we use no
417: * encoding at all (as none is relevant) when the target resource
418: * uses a Writer. And we decode the OutputStream's bytes
419: * using OUR tag's 'charEncoding' attribute, or ISO-8859-1
420: * as the default. We thus ignore setLocale() and setContentType()
421: * in this wrapper.
422: *
423: * In other words, the target's asserted encoding is used
424: * to convert from a Writer to an OutputStream, which is typically
425: * the medium through with the target will communicate its
426: * ultimate response. Since we short-circuit that mechanism
427: * and read the target's characters directly if they're offered
428: * as such, we simply ignore the target's encoding assertion.
429: */
430:
431: //************************************************************
432: // Data
433: /** The Writer we convey. */
434: private StringWriter sw = new StringWriter();
435:
436: /** A buffer, alternatively, to accumulate bytes. */
437: private ByteArrayOutputStream bos = new ByteArrayOutputStream();
438:
439: /** A ServletOutputStream we convey, tied to this Writer. */
440: private ServletOutputStream sos = new ServletOutputStream() {
441: public void write(int b) throws IOException {
442: bos.write(b);
443: }
444: };
445:
446: /** 'True' if getWriter() was called; false otherwise. */
447: private boolean isWriterUsed;
448:
449: /** 'True if getOutputStream() was called; false otherwise. */
450: private boolean isStreamUsed;
451:
452: /** The HTTP status set by the target. */
453: private int status = 200;
454:
455: private PageContext pageContext;
456:
457: //************************************************************
458: // Constructor and methods
459:
460: /** Constructs a new ImportResponseWrapper. */
461: public ImportResponseWrapper(PageContext pageContext) {
462: super ((HttpServletResponse) pageContext.getResponse());
463: this .pageContext = pageContext;
464: }
465:
466: /** Returns a Writer designed to buffer the output. */
467: public PrintWriter getWriter() throws IOException {
468: if (isStreamUsed)
469: throw new IllegalStateException(Resources
470: .getMessage("IMPORT_ILLEGAL_STREAM"));
471: isWriterUsed = true;
472: return new PrintWriterWrapper(sw, pageContext.getOut());
473: }
474:
475: /** Returns a ServletOutputStream designed to buffer the output. */
476: public ServletOutputStream getOutputStream() {
477: if (isWriterUsed)
478: throw new IllegalStateException(Resources
479: .getMessage("IMPORT_ILLEGAL_WRITER"));
480: isStreamUsed = true;
481: return sos;
482: }
483:
484: /** Has no effect. */
485: public void setContentType(String x) {
486: // ignore
487: }
488:
489: /** Has no effect. */
490: public void setLocale(Locale x) {
491: // ignore
492: }
493:
494: public void setStatus(int status) {
495: this .status = status;
496: }
497:
498: public int getStatus() {
499: return status;
500: }
501:
502: /**
503: * Retrieves the buffered output, using the containing tag's
504: * 'charEncoding' attribute, or the tag's default encoding,
505: * <b>if necessary</b>.
506: */
507: // not simply toString() because we need to throw
508: // UnsupportedEncodingException
509: public String getString() throws UnsupportedEncodingException {
510: if (isWriterUsed)
511: return sw.toString();
512: else if (isStreamUsed) {
513: if (charEncoding != null && !charEncoding.equals(""))
514: return bos.toString(charEncoding);
515: else
516: return bos.toString(DEFAULT_ENCODING);
517: } else
518: return ""; // target didn't write anything
519: }
520: }
521:
522: private static class PrintWriterWrapper extends PrintWriter {
523:
524: private StringWriter out;
525: private Writer parentWriter;
526:
527: public PrintWriterWrapper(StringWriter out, Writer parentWriter) {
528: super (out);
529: this .out = out;
530: this .parentWriter = parentWriter;
531: }
532:
533: public void flush() {
534: try {
535: parentWriter.write(out.toString());
536: StringBuffer sb = out.getBuffer();
537: sb.delete(0, sb.length());
538: } catch (IOException ex) {
539: }
540: }
541: }
542:
543: //*********************************************************************
544: // Some private utility methods
545:
546: /** Returns our URL (potentially with parameters) */
547: private String targetUrl() {
548: if (urlWithParams == null)
549: urlWithParams = params.aggregateParams(url);
550: return urlWithParams;
551: }
552:
553: /**
554: * Returns <tt>true</tt> if our current URL is absolute,
555: * <tt>false</tt> otherwise.
556: */
557: private boolean isAbsoluteUrl() throws JspTagException {
558: return isAbsoluteUrl(url);
559: }
560:
561: //*********************************************************************
562: // Public utility methods
563:
564: /**
565: * Returns <tt>true</tt> if our current URL is absolute,
566: * <tt>false</tt> otherwise.
567: */
568: public static boolean isAbsoluteUrl(String url) {
569: // a null URL is not absolute, by our definition
570: if (url == null)
571: return false;
572:
573: // do a fast, simple check first
574: int colonPos;
575: if ((colonPos = url.indexOf(":")) == -1)
576: return false;
577:
578: // if we DO have a colon, make sure that every character
579: // leading up to it is a valid scheme character
580: for (int i = 0; i < colonPos; i++)
581: if (VALID_SCHEME_CHARS.indexOf(url.charAt(i)) == -1)
582: return false;
583:
584: // if so, we've got an absolute url
585: return true;
586: }
587:
588: /**
589: * Strips a servlet session ID from <tt>url</tt>. The session ID
590: * is encoded as a URL "path parameter" beginning with "jsessionid=".
591: * We thus remove anything we find between ";jsessionid=" (inclusive)
592: * and either EOS or a subsequent ';' (exclusive).
593: */
594: public static String stripSession(String url) {
595: StringBuffer u = new StringBuffer(url);
596: int sessionStart;
597: while ((sessionStart = u.toString().indexOf(";jsessionid=")) != -1) {
598: int sessionEnd = u.toString()
599: .indexOf(";", sessionStart + 1);
600: if (sessionEnd == -1)
601: sessionEnd = u.toString()
602: .indexOf("?", sessionStart + 1);
603: if (sessionEnd == -1) // still
604: sessionEnd = u.length();
605: u.delete(sessionStart, sessionEnd);
606: }
607: return u.toString();
608: }
609: }
|