001: package org.apache.velocity.tools.view;
002:
003: /*
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021:
022: import java.io.BufferedReader;
023: import java.io.ByteArrayOutputStream;
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.io.InputStreamReader;
027: import java.io.PrintWriter;
028: import java.io.Reader;
029: import java.io.StringReader;
030: import java.io.StringWriter;
031: import java.io.UnsupportedEncodingException;
032: import java.net.HttpURLConnection;
033: import java.net.URL;
034: import java.net.URLConnection;
035: import java.util.Locale;
036:
037: import javax.servlet.RequestDispatcher;
038: import javax.servlet.ServletContext;
039: import javax.servlet.ServletOutputStream;
040: import javax.servlet.http.HttpServletRequest;
041: import javax.servlet.http.HttpServletResponse;
042: import javax.servlet.http.HttpServletResponseWrapper;
043:
044: import org.apache.commons.logging.Log;
045: import org.apache.commons.logging.LogFactory;
046:
047: /**
048: * <p>Provides methods to import arbitrary local or remote resources as strings.</p>
049: * <p>Based on ImportSupport from the JSTL taglib by Shawn Bayern</p>
050: *
051: * @author <a href="mailto:marinoj@centrum.is">Marino A. Jonsson</a>
052: * @since VelocityTools 1.1
053: * @version $Revision: 477914 $ $Date: 2006-11-21 13:52:11 -0800 (Tue, 21 Nov 2006) $
054: */
055: public abstract class ImportSupport {
056:
057: protected static final Log LOG = LogFactory
058: .getLog(ImportSupport.class);
059:
060: protected ServletContext application;
061: protected HttpServletRequest request;
062: protected HttpServletResponse response;
063:
064: protected static final String VALID_SCHEME_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-";
065:
066: /** Default character encoding for response. */
067: protected static final String DEFAULT_ENCODING = "ISO-8859-1";
068:
069: //*********************************************************************
070: // URL importation logic
071:
072: /*
073: * Overall strategy: we have two entry points, acquireString() and
074: * acquireReader(). The latter passes data through unbuffered if
075: * possible (but note that it is not always possible -- specifically
076: * for cases where we must use the RequestDispatcher. The remaining
077: * methods handle the common.core logic of loading either a URL or a local
078: * resource.
079: *
080: * We consider the 'natural' form of absolute URLs to be Readers and
081: * relative URLs to be Strings. Thus, to avoid doing extra work,
082: * acquireString() and acquireReader() delegate to one another as
083: * appropriate. (Perhaps I could have spelled things out more clearly,
084: * but I thought this implementation was instructive, not to mention
085: * somewhat cute...)
086: */
087:
088: /**
089: *
090: * @param url the URL resource to return as string
091: * @return the URL resource as string
092: * @throws IOException
093: * @throws java.lang.Exception
094: */
095: protected String acquireString(String url) throws IOException,
096: Exception {
097: // Record whether our URL is absolute or relative
098: if (isAbsoluteUrl(url)) {
099: // for absolute URLs, delegate to our peer
100: BufferedReader r = null;
101: try {
102: r = new BufferedReader(acquireReader(url));
103: StringBuffer sb = new StringBuffer();
104: int i;
105: // under JIT, testing seems to show this simple loop is as fast
106: // as any of the alternatives
107: while ((i = r.read()) != -1) {
108: sb.append((char) i);
109: }
110: return sb.toString();
111: } finally {
112: if (r != null) {
113: try {
114: r.close();
115: } catch (IOException ioe) {
116: LOG.error("Could not close reader.", ioe);
117: }
118: }
119: }
120: } else // handle relative URLs ourselves
121: {
122: // URL is relative, so we must be an HTTP request
123: if (!(request instanceof HttpServletRequest && response instanceof HttpServletResponse)) {
124: throw new Exception(
125: "Relative import from non-HTTP request not allowed");
126: }
127:
128: // retrieve an appropriate ServletContext
129: // normalize the URL if we have an HttpServletRequest
130: if (!url.startsWith("/")) {
131: String sp = ((HttpServletRequest) request)
132: .getServletPath();
133: url = sp.substring(0, sp.lastIndexOf('/')) + '/' + url;
134: }
135:
136: // strip the session id from the url
137: url = stripSession(url);
138:
139: // from this context, get a dispatcher
140: RequestDispatcher rd = application
141: .getRequestDispatcher(url);
142: if (rd == null) {
143: throw new Exception(
144: "Couldn't get a RequestDispatcher for \"" + url
145: + "\"");
146: }
147:
148: // include the resource, using our custom wrapper
149: ImportResponseWrapper irw = new ImportResponseWrapper(
150: (HttpServletResponse) response);
151: try {
152: rd.include(request, irw);
153: } catch (IOException ex) {
154: throw new Exception(
155: "Problem importing the relative URL \"" + url
156: + "\". " + ex);
157: } catch (RuntimeException ex) {
158: throw new Exception(
159: "Problem importing the relative URL \"" + url
160: + "\". " + ex);
161: }
162:
163: // disallow inappropriate response codes per JSTL spec
164: if (irw.getStatus() < 200 || irw.getStatus() > 299) {
165: throw new Exception("Invalid response code '"
166: + irw.getStatus() + "' for \"" + url + "\"");
167: }
168:
169: // recover the response String from our wrapper
170: return irw.getString();
171: }
172: }
173:
174: /**
175: *
176: * @param url the URL to read
177: * @return a Reader for the InputStream created from the supplied URL
178: * @throws IOException
179: * @throws java.lang.Exception
180: */
181: protected Reader acquireReader(String url) throws IOException,
182: Exception {
183: if (!isAbsoluteUrl(url)) {
184: // for relative URLs, delegate to our peer
185: return new StringReader(acquireString(url));
186: } else {
187: // absolute URL
188: URLConnection uc = null;
189: HttpURLConnection huc = null;
190: InputStream i = null;
191:
192: try {
193: // handle absolute URLs ourselves, using java.net.URL
194: URL u = new URL(url);
195: // URL u = new URL("http", "proxy.hi.is", 8080, target);
196: uc = u.openConnection();
197: i = uc.getInputStream();
198:
199: // check response code for HTTP URLs, per spec,
200: if (uc instanceof HttpURLConnection) {
201: huc = (HttpURLConnection) uc;
202:
203: int status = huc.getResponseCode();
204: if (status < 200 || status > 299) {
205: throw new Exception(status + " " + url);
206: }
207: }
208:
209: // okay, we've got a stream; encode it appropriately
210: Reader r = null;
211: String charSet;
212:
213: // charSet extracted according to RFC 2045, section 5.1
214: String contentType = uc.getContentType();
215: if (contentType != null) {
216: charSet = ImportSupport.getContentTypeAttribute(
217: contentType, "charset");
218: if (charSet == null) {
219: charSet = DEFAULT_ENCODING;
220: }
221: } else {
222: charSet = DEFAULT_ENCODING;
223: }
224:
225: try {
226: r = new InputStreamReader(i, charSet);
227: } catch (UnsupportedEncodingException ueex) {
228: r = new InputStreamReader(i, DEFAULT_ENCODING);
229: }
230:
231: if (huc == null) {
232: return r;
233: } else {
234: return new SafeClosingHttpURLConnectionReader(r,
235: huc);
236: }
237: } catch (IOException ex) {
238: if (i != null) {
239: try {
240: i.close();
241: } catch (IOException ioe) {
242: LOG.error("Could not close InputStream", ioe);
243: }
244: }
245:
246: if (huc != null) {
247: huc.disconnect();
248: }
249: throw new Exception(
250: "Problem accessing the absolute URL \"" + url
251: + "\". " + ex);
252: } catch (RuntimeException ex) {
253: if (i != null) {
254: try {
255: i.close();
256: } catch (IOException ioe) {
257: LOG.error("Could not close InputStream", ioe);
258: }
259: }
260:
261: if (huc != null) {
262: huc.disconnect();
263: }
264: // because the spec makes us
265: throw new Exception(
266: "Problem accessing the absolute URL \"" + url
267: + "\". " + ex);
268: }
269: }
270: }
271:
272: protected static class SafeClosingHttpURLConnectionReader extends
273: Reader {
274: private HttpURLConnection huc;
275: private Reader wrappedReader;
276:
277: SafeClosingHttpURLConnectionReader(Reader r,
278: HttpURLConnection huc) {
279: this .wrappedReader = r;
280: this .huc = huc;
281: }
282:
283: public void close() throws IOException {
284: if (null != huc) {
285: huc.disconnect();
286: }
287:
288: wrappedReader.close();
289: }
290:
291: // Pass-through methods.
292: public void mark(int readAheadLimit) throws IOException {
293: wrappedReader.mark(readAheadLimit);
294: }
295:
296: public boolean markSupported() {
297: return wrappedReader.markSupported();
298: }
299:
300: public int read() throws IOException {
301: return wrappedReader.read();
302: }
303:
304: public int read(char[] buf) throws IOException {
305: return wrappedReader.read(buf);
306: }
307:
308: public int read(char[] buf, int off, int len)
309: throws IOException {
310: return wrappedReader.read(buf, off, len);
311: }
312:
313: public boolean ready() throws IOException {
314: return wrappedReader.ready();
315: }
316:
317: public void reset() throws IOException {
318: wrappedReader.reset();
319: }
320:
321: public long skip(long n) throws IOException {
322: return wrappedReader.skip(n);
323: }
324: }
325:
326: /** Wraps responses to allow us to retrieve results as Strings. */
327: protected class ImportResponseWrapper extends
328: HttpServletResponseWrapper {
329: /*
330: * We provide either a Writer or an OutputStream as requested.
331: * We actually have a true Writer and an OutputStream backing
332: * both, since we don't want to use a character encoding both
333: * ways (Writer -> OutputStream -> Writer). So we use no
334: * encoding at all (as none is relevant) when the target resource
335: * uses a Writer. And we decode the OutputStream's bytes
336: * using OUR tag's 'charEncoding' attribute, or ISO-8859-1
337: * as the default. We thus ignore setLocale() and setContentType()
338: * in this wrapper.
339: *
340: * In other words, the target's asserted encoding is used
341: * to convert from a Writer to an OutputStream, which is typically
342: * the medium through with the target will communicate its
343: * ultimate response. Since we short-circuit that mechanism
344: * and read the target's characters directly if they're offered
345: * as such, we simply ignore the target's encoding assertion.
346: */
347:
348: /** The Writer we convey. */
349: private StringWriter sw;
350:
351: /** A buffer, alternatively, to accumulate bytes. */
352: private ByteArrayOutputStream bos;
353:
354: /** 'True' if getWriter() was called; false otherwise. */
355: private boolean isWriterUsed;
356:
357: /** 'True if getOutputStream() was called; false otherwise. */
358: private boolean isStreamUsed;
359:
360: /** The HTTP status set by the target. */
361: private int status = 200;
362:
363: //************************************************************
364: // Constructor and methods
365:
366: /**
367: * Constructs a new ImportResponseWrapper.
368: * @param response the response to wrap
369: */
370: public ImportResponseWrapper(HttpServletResponse response) {
371: super (response);
372: }
373:
374: /**
375: * @return a Writer designed to buffer the output.
376: */
377: public PrintWriter getWriter() {
378: if (isStreamUsed) {
379: throw new IllegalStateException(
380: "Unexpected internal error during import: "
381: + "Target servlet called getWriter(), then getOutputStream()");
382: }
383: isWriterUsed = true;
384: if (sw == null) {
385: sw = new StringWriter();
386: }
387: return new PrintWriter(sw);
388: }
389:
390: /**
391: * @return a ServletOutputStream designed to buffer the output.
392: */
393: public ServletOutputStream getOutputStream() {
394: if (isWriterUsed) {
395: throw new IllegalStateException(
396: "Unexpected internal error during import: "
397: + "Target servlet called getOutputStream(), then getWriter()");
398: }
399: isStreamUsed = true;
400: if (bos == null) {
401: bos = new ByteArrayOutputStream();
402: }
403: ServletOutputStream sos = new ServletOutputStream() {
404: public void write(int b) throws IOException {
405: bos.write(b);
406: }
407: };
408: return sos;
409: }
410:
411: /** Has no effect. */
412: public void setContentType(String x) {
413: // ignore
414: }
415:
416: /** Has no effect. */
417: public void setLocale(Locale x) {
418: // ignore
419: }
420:
421: /**
422: * Sets the status of the response
423: * @param status the status code
424: */
425: public void setStatus(int status) {
426: this .status = status;
427: }
428:
429: /**
430: * @return the status of the response
431: */
432: public int getStatus() {
433: return status;
434: }
435:
436: /**
437: * Retrieves the buffered output, using the containing tag's
438: * 'charEncoding' attribute, or the tag's default encoding,
439: * <b>if necessary</b>.
440: * @return the buffered output
441: * @throws UnsupportedEncodingException if the encoding is not supported
442: */
443: public String getString() throws UnsupportedEncodingException {
444: if (isWriterUsed) {
445: return sw.toString();
446: } else if (isStreamUsed) {
447: return bos.toString(this .getCharacterEncoding());
448: } else {
449: return ""; // target didn't write anything
450: }
451: }
452: }
453:
454: //*********************************************************************
455: // Public utility methods
456:
457: /**
458: * Returns <tt>true</tt> if our current URL is absolute,
459: * <tt>false</tt> otherwise.
460: *
461: * @param url the url to check out
462: * @return true if the url is absolute
463: */
464: public static boolean isAbsoluteUrl(String url) {
465: // a null URL is not absolute, by our definition
466: if (url == null) {
467: return false;
468: }
469:
470: // do a fast, simple check first
471: int colonPos;
472: if ((colonPos = url.indexOf(":")) == -1) {
473: return false;
474: }
475:
476: // if we DO have a colon, make sure that every character
477: // leading up to it is a valid scheme character
478: for (int i = 0; i < colonPos; i++) {
479: if (VALID_SCHEME_CHARS.indexOf(url.charAt(i)) == -1) {
480: return false;
481: }
482: }
483: // if so, we've got an absolute url
484: return true;
485: }
486:
487: /**
488: * Strips a servlet session ID from <tt>url</tt>. The session ID
489: * is encoded as a URL "path parameter" beginning with "jsessionid=".
490: * We thus remove anything we find between ";jsessionid=" (inclusive)
491: * and either EOS or a subsequent ';' (exclusive).
492: *
493: * @param url the url to strip the session id from
494: * @return the stripped url
495: */
496: public static String stripSession(String url) {
497: StringBuffer u = new StringBuffer(url);
498: int sessionStart;
499: while ((sessionStart = u.toString().indexOf(";jsessionid=")) != -1) {
500: int sessionEnd = u.toString()
501: .indexOf(";", sessionStart + 1);
502: if (sessionEnd == -1) {
503: sessionEnd = u.toString()
504: .indexOf("?", sessionStart + 1);
505: }
506: if (sessionEnd == -1) {
507: // still
508: sessionEnd = u.length();
509: }
510: u.delete(sessionStart, sessionEnd);
511: }
512: return u.toString();
513: }
514:
515: /**
516: * Get the value associated with a content-type attribute.
517: * Syntax defined in RFC 2045, section 5.1.
518: *
519: * @param input the string containing the attributes
520: * @param name the name of the content-type attribute
521: * @return the value associated with a content-type attribute
522: */
523: public static String getContentTypeAttribute(String input,
524: String name) {
525: int begin;
526: int end;
527: int index = input.toUpperCase().indexOf(name.toUpperCase());
528: if (index == -1) {
529: return null;
530: }
531: index = index + name.length(); // positioned after the attribute name
532: index = input.indexOf('=', index); // positioned at the '='
533: if (index == -1) {
534: return null;
535: }
536: index += 1; // positioned after the '='
537: input = input.substring(index).trim();
538:
539: if (input.charAt(0) == '"') {
540: // attribute value is a quoted string
541: begin = 1;
542: end = input.indexOf('"', begin);
543: if (end == -1) {
544: return null;
545: }
546: } else {
547: begin = 0;
548: end = input.indexOf(';');
549: if (end == -1) {
550: end = input.indexOf(' ');
551: }
552: if (end == -1) {
553: end = input.length();
554: }
555: }
556: return input.substring(begin, end).trim();
557: }
558:
559: }
|