View Javadoc

1   package org.apache.velocity.tools.view;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.BufferedReader;
23  import java.io.ByteArrayOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.io.PrintWriter;
28  import java.io.Reader;
29  import java.io.StringReader;
30  import java.io.StringWriter;
31  import java.io.UnsupportedEncodingException;
32  import java.net.HttpURLConnection;
33  import java.net.URL;
34  import java.net.URLConnection;
35  import java.util.Locale;
36  import javax.servlet.RequestDispatcher;
37  import javax.servlet.ServletContext;
38  import javax.servlet.ServletOutputStream;
39  import javax.servlet.http.HttpServletRequest;
40  import javax.servlet.http.HttpServletResponse;
41  import javax.servlet.http.HttpServletResponseWrapper;
42  import org.apache.velocity.runtime.log.Log;
43  
44  /**
45   * <p>Provides methods to import arbitrary local or remote resources as strings.</p>
46   * <p>Based on ImportSupport from the JSTL taglib by Shawn Bayern</p>
47   *
48   * @author <a href="mailto:marinoj@centrum.is">Marino A. Jonsson</a>
49   * @since VelocityTools 2.0
50   * @version $Revision: 591088 $ $Date: 2007-11-01 10:11:41 -0700 (Thu, 01 Nov 2007) $
51   */
52  public abstract class ImportSupport
53  {
54      protected static final String VALID_SCHEME_CHARS =
55          "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-";
56  
57      /** Default character encoding for response. */
58      protected static final String DEFAULT_ENCODING = "ISO-8859-1";
59  
60      protected Log LOG;
61      protected ServletContext application;
62      protected HttpServletRequest request;
63      protected HttpServletResponse response;
64  
65  
66      // --------------------------------------- Setup Methods -------------
67  
68      public void setLog(Log log)
69      {
70          if (log == null)
71          {
72              throw new NullPointerException("log should not be set to null");
73          }
74          this.LOG = log;
75      }
76  
77      /**
78       * Sets the current {@link HttpServletRequest}. This is required
79       * for this tool to operate and will throw a NullPointerException
80       * if this is not set or is set to {@code null}.
81       */
82      public void setRequest(HttpServletRequest request)
83      {
84          if (request == null)
85          {
86              throw new NullPointerException("request should not be null");
87          }
88          this.request = request;
89      }
90  
91      /**
92       * Sets the current {@link HttpServletResponse}. This is required
93       * for this tool to operate and will throw a NullPointerException
94       * if this is not set or is set to {@code null}.
95       */
96      public void setResponse(HttpServletResponse response)
97      {
98          if (response == null)
99          {
100             throw new NullPointerException("response should not be null");
101         }
102         this.response = response;
103     }
104 
105     /**
106      * Sets the {@link ServletContext}. This is required
107      * for this tool to operate and will throw a NullPointerException
108      * if this is not set or is set to {@code null}.
109      */
110     public void setServletContext(ServletContext application)
111     {
112         if (application == null)
113         {
114             throw new NullPointerException("servlet context should not be null");
115         }
116         this.application = application;
117     }
118 
119     //*********************************************************************
120     // URL importation logic
121 
122     /*
123      * Overall strategy:  we have two entry points, acquireString() and
124      * acquireReader().  The latter passes data through unbuffered if
125      * possible (but note that it is not always possible -- specifically
126      * for cases where we must use the RequestDispatcher.  The remaining
127      * methods handle the common.core logic of loading either a URL or a local
128      * resource.
129      *
130      * We consider the 'natural' form of absolute URLs to be Readers and
131      * relative URLs to be Strings.  Thus, to avoid doing extra work,
132      * acquireString() and acquireReader() delegate to one another as
133      * appropriate.  (Perhaps I could have spelled things out more clearly,
134      * but I thought this implementation was instructive, not to mention
135      * somewhat cute...)
136      */
137 
138     /**
139      *
140      * @param url the URL resource to return as string
141      * @return the URL resource as string
142      * @throws IOException
143      * @throws java.lang.Exception
144      */
145     protected String acquireString(String url) throws IOException, Exception {
146         // Record whether our URL is absolute or relative
147         if (isAbsoluteUrl(url))
148         {
149             // for absolute URLs, delegate to our peer
150             BufferedReader r = null;
151             try
152             {
153                 r = new BufferedReader(acquireReader(url));
154                 StringBuilder sb = new StringBuilder();
155                 int i;
156                 // under JIT, testing seems to show this simple loop is as fast
157                 // as any of the alternatives
158                 while ((i = r.read()) != -1)
159                 {
160                     sb.append((char)i);
161                 }
162                 return sb.toString();
163             }
164             finally
165             {
166                 if(r != null)
167                 {
168                     try
169                     {
170                         r.close();
171                     }
172                     catch (IOException ioe)
173                     {
174                         LOG.error("ImportSupport : Could not close reader.", ioe);
175                     }
176                 }
177 	        }
178         }
179         else // handle relative URLs ourselves
180         {
181             // URL is relative, so we must be an HTTP request
182             if (!(request instanceof HttpServletRequest
183                   && response instanceof HttpServletResponse))
184             {
185                 throw new Exception("Relative import from non-HTTP request not allowed");
186             }
187 
188             // retrieve an appropriate ServletContext
189             // normalize the URL if we have an HttpServletRequest
190             if (!url.startsWith("/"))
191             {
192                 String sp = ((HttpServletRequest)request).getServletPath();
193                 url = sp.substring(0, sp.lastIndexOf('/')) + '/' + url;
194             }
195 
196             // strip the session id from the url
197             url = stripSession(url);
198 
199             // from this context, get a dispatcher
200             RequestDispatcher rd = application.getRequestDispatcher(url);
201             if (rd == null)
202             {
203                 throw new Exception("Couldn't get a RequestDispatcher for \""
204                                     + url + "\"");
205             }
206 
207             // include the resource, using our custom wrapper
208             ImportResponseWrapper irw =
209                 new ImportResponseWrapper((HttpServletResponse)response);
210             try
211             {
212                 rd.include(request, irw);
213             }
214             catch (IOException ex)
215             {
216                 throw new Exception("Problem importing the relative URL \""
217                                     + url + "\". " + ex);
218             }
219             catch (RuntimeException ex)
220             {
221                 throw new Exception("Problem importing the relative URL \""
222                                     + url + "\". " + ex);
223             }
224 
225             // disallow inappropriate response codes per JSTL spec
226             if (irw.getStatus() < 200 || irw.getStatus() > 299)
227             {
228                 throw new Exception("Invalid response code '" + irw.getStatus()
229                                     + "' for \"" + url + "\"");
230             }
231 
232             // recover the response String from our wrapper
233             return irw.getString();
234         }
235     }
236 
237     /**
238      *
239      * @param url the URL to read
240      * @return a Reader for the InputStream created from the supplied URL
241      * @throws IOException
242      * @throws java.lang.Exception
243      */
244     protected Reader acquireReader(String url) throws IOException, Exception
245     {
246         if (!isAbsoluteUrl(url))
247         {
248             // for relative URLs, delegate to our peer
249             return new StringReader(acquireString(url));
250         }
251         else
252         {
253             // absolute URL
254             URLConnection uc = null;
255             HttpURLConnection huc = null;
256             InputStream i = null;
257 
258             try
259             {
260                 // handle absolute URLs ourselves, using java.net.URL
261                 URL u = new URL(url);
262                 // URL u = new URL("http", "proxy.hi.is", 8080, target);
263                 uc = u.openConnection();
264                 i = uc.getInputStream();
265 
266                 // check response code for HTTP URLs, per spec,
267                 if (uc instanceof HttpURLConnection)
268                 {
269                     huc = (HttpURLConnection)uc;
270 
271                     int status = huc.getResponseCode();
272                     if (status < 200 || status > 299)
273                     {
274                         throw new Exception(status + " " + url);
275                     }
276                 }
277 
278                 // okay, we've got a stream; encode it appropriately
279                 Reader r = null;
280                 String charSet;
281 
282                 // charSet extracted according to RFC 2045, section 5.1
283                 String contentType = uc.getContentType();
284                 if (contentType != null)
285                 {
286                     charSet = ImportSupport.getContentTypeAttribute(contentType, "charset");
287                     if (charSet == null)
288                     {
289                         charSet = DEFAULT_ENCODING;
290                     }
291                 }
292                 else
293                 {
294                     charSet = DEFAULT_ENCODING;
295                 }
296 
297                 try
298                 {
299                     r = new InputStreamReader(i, charSet);
300                 }
301                 catch (UnsupportedEncodingException ueex)
302                 {
303                     r = new InputStreamReader(i, DEFAULT_ENCODING);
304                 }
305 
306                 if (huc == null)
307                 {
308                     return r;
309                 }
310                 else
311                 {
312                     return new SafeClosingHttpURLConnectionReader(r, huc);
313                 }
314             }
315             catch (IOException ex)
316             {
317                 if (i != null)
318                 {
319                     try
320                     {
321                         i.close();
322                     }
323                     catch (IOException ioe)
324                     {
325                         LOG.error("ImportSupport : Could not close InputStream", ioe);
326                     }
327                 }
328 
329                 if (huc != null)
330                 {
331                     huc.disconnect();
332                 }
333                 throw new Exception("Problem accessing the absolute URL \""
334                                     + url + "\". " + ex);
335             }
336             catch (RuntimeException ex)
337             {
338                 if (i != null)
339                 {
340                     try
341                     {
342                         i.close();
343                     }
344                     catch (IOException ioe)
345                     {
346                         LOG.error("ImportSupport : Could not close InputStream", ioe);
347                     }
348                 }
349 
350                 if (huc != null)
351                 {
352                     huc.disconnect();
353                 }
354                 // because the spec makes us
355                 throw new Exception("Problem accessing the absolute URL \""
356                                     + url + "\". " + ex);
357             }
358         }
359     }
360 
361     protected static class SafeClosingHttpURLConnectionReader extends Reader
362     {
363         private final HttpURLConnection huc;
364         private final Reader wrappedReader;
365 
366         SafeClosingHttpURLConnectionReader(Reader r, HttpURLConnection huc)
367         {
368             this.wrappedReader = r;
369             this.huc = huc;
370         }
371 
372         public void close() throws IOException
373         {
374             if(null != huc)
375             {
376                 huc.disconnect();
377             }
378 
379             wrappedReader.close();
380         }
381 
382         // Pass-through methods.
383         public void mark(int readAheadLimit) throws IOException
384         {
385             wrappedReader.mark(readAheadLimit);
386         }
387 
388         public boolean markSupported()
389         {
390             return wrappedReader.markSupported();
391         }
392 
393         public int read() throws IOException
394         {
395             return wrappedReader.read();
396         }
397 
398         public int read(char[] buf) throws IOException
399         {
400             return wrappedReader.read(buf);
401         }
402 
403         public int read(char[] buf, int off, int len) throws IOException
404         {
405             return wrappedReader.read(buf, off, len);
406         }
407 
408         public boolean ready() throws IOException
409         {
410             return wrappedReader.ready();
411         }
412 
413         public void reset() throws IOException
414         {
415             wrappedReader.reset();
416         }
417 
418         public long skip(long n) throws IOException
419         {
420             return wrappedReader.skip(n);
421         }
422     }
423 
424 
425     /** Wraps responses to allow us to retrieve results as Strings. */
426     protected static class ImportResponseWrapper extends HttpServletResponseWrapper
427     {
428         /*
429          * We provide either a Writer or an OutputStream as requested.
430          * We actually have a true Writer and an OutputStream backing
431          * both, since we don't want to use a character encoding both
432          * ways (Writer -> OutputStream -> Writer).  So we use no
433          * encoding at all (as none is relevant) when the target resource
434          * uses a Writer.  And we decode the OutputStream's bytes
435          * using OUR tag's 'charEncoding' attribute, or ISO-8859-1
436          * as the default.  We thus ignore setLocale() and setContentType()
437          * in this wrapper.
438          *
439          * In other words, the target's asserted encoding is used
440          * to convert from a Writer to an OutputStream, which is typically
441          * the medium through with the target will communicate its
442          * ultimate response.  Since we short-circuit that mechanism
443          * and read the target's characters directly if they're offered
444          * as such, we simply ignore the target's encoding assertion.
445          */
446 
447         /** The Writer we convey. */
448         private StringWriter sw;
449 
450         /** A buffer, alternatively, to accumulate bytes. */
451         private ByteArrayOutputStream bos;
452 
453         /** 'True' if getWriter() was called; false otherwise. */
454         private boolean isWriterUsed;
455 
456         /** 'True if getOutputStream() was called; false otherwise. */
457         private boolean isStreamUsed;
458 
459         /** The HTTP status set by the target. */
460         private int status = 200;
461 
462         //************************************************************
463         // Constructor and methods
464 
465         /**
466          * Constructs a new ImportResponseWrapper.
467          * @param response the response to wrap
468          */
469         public ImportResponseWrapper(HttpServletResponse response)
470         {
471             super(response);
472         }
473 
474         /**
475          * @return a Writer designed to buffer the output.
476          */
477         public PrintWriter getWriter()
478         {
479             if (isStreamUsed)
480             {
481                 throw new IllegalStateException("Unexpected internal error during import: "
482                                                 + "Target servlet called getWriter(), then getOutputStream()");
483             }
484             isWriterUsed = true;
485             if (sw == null)
486             {
487                 sw = new StringWriter();
488             }
489             return new PrintWriter(sw);
490         }
491 
492         /**
493          * @return a ServletOutputStream designed to buffer the output.
494          */
495         public ServletOutputStream getOutputStream()
496         {
497             if (isWriterUsed)
498             {
499                 throw new IllegalStateException("Unexpected internal error during import: "
500                                                 + "Target servlet called getOutputStream(), then getWriter()");
501             }
502             isStreamUsed = true;
503             if (bos == null)
504             {
505                 bos = new ByteArrayOutputStream();
506             }
507             ServletOutputStream sos = new ServletOutputStream()
508                 {
509                     public void write(int b) throws IOException
510                     {
511                         bos.write(b);
512                     }
513                 };
514             return sos;
515         }
516 
517         /** Has no effect. */
518         public void setContentType(String x)
519         {
520             // ignore
521         }
522 
523         /** Has no effect. */
524         public void setLocale(Locale x)
525         {
526             // ignore
527         }
528 
529         /**
530          * Sets the status of the response
531          * @param status the status code
532          */
533         public void setStatus(int status)
534         {
535             this.status = status;
536         }
537 
538         /**
539          * @return the status of the response
540          */
541         public int getStatus()
542         {
543             return status;
544         }
545 
546         /**
547          * Retrieves the buffered output, using the containing tag's
548          * 'charEncoding' attribute, or the tag's default encoding,
549          * <b>if necessary</b>.
550          * @return the buffered output
551          * @throws UnsupportedEncodingException if the encoding is not supported
552          */
553         public String getString() throws UnsupportedEncodingException
554         {
555             if (isWriterUsed)
556             {
557                 return sw.toString();
558             }
559             else if (isStreamUsed)
560             {
561                 return bos.toString(this.getCharacterEncoding());
562             }
563             else
564             {
565                 return ""; // target didn't write anything
566             }
567         }
568     }
569 
570     //*********************************************************************
571     // Public utility methods
572 
573     /**
574      * Returns <tt>true</tt> if our current URL is absolute,
575      * <tt>false</tt> otherwise.
576      *
577      * @param url the url to check out
578      * @return true if the url is absolute
579      */
580     public static boolean isAbsoluteUrl(String url) {
581         // a null URL is not absolute, by our definition
582         if (url == null)
583         {
584             return false;
585         }
586 
587         // do a fast, simple check first
588         int colonPos;
589         if ((colonPos = url.indexOf(':')) == -1)
590         {
591             return false;
592         }
593 
594         // if we DO have a colon, make sure that every character
595         // leading up to it is a valid scheme character
596         for (int i = 0; i < colonPos; i++)
597         {
598             if (VALID_SCHEME_CHARS.indexOf(url.charAt(i)) == -1)
599             {
600                 return false;
601             }
602         }
603         // if so, we've got an absolute url
604         return true;
605     }
606 
607     /**
608      * Strips a servlet session ID from <tt>url</tt>.  The session ID
609      * is encoded as a URL "path parameter" beginning with "jsessionid=".
610      * We thus remove anything we find between ";jsessionid=" (inclusive)
611      * and either EOS or a subsequent ';' (exclusive).
612      *
613      * @param url the url to strip the session id from
614      * @return the stripped url
615      */
616     public static String stripSession(String url)
617     {
618         StringBuilder u = new StringBuilder(url);
619         int sessionStart;
620         while ((sessionStart = u.toString().indexOf(";jsessionid=")) != -1)
621         {
622             int sessionEnd = u.toString().indexOf(";", sessionStart + 1);
623             if (sessionEnd == -1)
624             {
625                 sessionEnd = u.toString().indexOf("?", sessionStart + 1);
626             }
627             if (sessionEnd == -1)
628             {
629                 // still
630                 sessionEnd = u.length();
631             }
632             u.delete(sessionStart, sessionEnd);
633         }
634         return u.toString();
635     }
636 
637     /**
638      * Get the value associated with a content-type attribute.
639      * Syntax defined in RFC 2045, section 5.1.
640      *
641      * @param input the string containing the attributes
642      * @param name the name of the content-type attribute
643      * @return the value associated with a content-type attribute
644      */
645     public static String getContentTypeAttribute(String input, String name)
646     {
647         int begin;
648         int end;
649         int index = input.toUpperCase().indexOf(name.toUpperCase());
650         if (index == -1)
651         {
652             return null;
653         }
654         index = index + name.length(); // positioned after the attribute name
655         index = input.indexOf('=', index); // positioned at the '='
656         if (index == -1)
657         {
658             return null;
659         }
660         index += 1; // positioned after the '='
661         input = input.substring(index).trim();
662 
663         if (input.charAt(0) == '"')
664         {
665             // attribute value is a quoted string
666             begin = 1;
667             end = input.indexOf('"', begin);
668             if (end == -1)
669             {
670                 return null;
671             }
672         }
673         else
674         {
675             begin = 0;
676             end = input.indexOf(';');
677             if (end == -1)
678             {
679                 end = input.indexOf(' ');
680             }
681             if (end == -1)
682             {
683                 end = input.length();
684             }
685         }
686         return input.substring(begin, end).trim();
687     }
688 
689 }