View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.renderkit.html.util;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.myfaces.shared_tomahawk.util.ClassUtils;
24  
25  import javax.servlet.ServletContext;
26  import javax.servlet.ServletOutputStream;
27  import javax.servlet.http.HttpServletRequest;
28  import javax.servlet.http.HttpServletResponse;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.text.ParseException;
32  import java.text.SimpleDateFormat;
33  import java.util.Calendar;
34  import java.util.Date;
35  import java.util.ResourceBundle;
36  import java.net.HttpURLConnection;
37  
38  /**
39   * A ResourceLoader capable of fetching resources from the classpath,
40   * but only for classes under package org.apache.myfaces.custom.
41   * <p>
42   * The URI is expected to contain two pieces of information: the
43   * tomahawk class the resource is associated with, and a relative path
44   * from that class to the resource.
45   *
46   * @author Mathias Broekelmann (latest modification by $Author$)
47   * @version $Revision$ $Date$
48   */
49  public class MyFacesResourceLoader implements ResourceLoader
50  {
51      protected static final Log log = LogFactory.getLog(MyFacesResourceLoader.class);
52  
53      static final String ORG_APACHE_MYFACES_CUSTOM = "org.apache.myfaces.custom";
54  
55      private static long lastModified = 0;
56  
57      /**
58       * Get the last-modified time of the resource.
59       * <p>
60       * Unfortunately this is not possible with files inside jars. Instead, the
61       * MyFaces build process ensures that there is a file AddResource.properties
62       * which has the datestamp of the time the build process was run. This method
63       * simply gets that value and returns it.
64       * <p>
65       * Note that this method is not related to the generation of "cache key"
66       * values by the AddResource class, nor does it affect the caching behaviour
67       * of web browsers. This value simply goes into the http headers as the
68       * last-modified time of the specified resource.
69       */
70      private static long getLastModified()
71      {
72          if (lastModified == 0)
73          {
74              final String format = "yyyy-MM-dd HH:mm:ss Z"; // Must match the one used in the build file
75              final String bundleName = AddResource.class.getName();
76              ResourceBundle resources = ResourceBundle.getBundle(bundleName);
77              String sLastModified = resources.getString("lastModified");
78              try
79              {
80                  lastModified = new SimpleDateFormat(format).parse(sLastModified).getTime();
81              }
82              catch (ParseException e)
83              {
84                  lastModified = new Date().getTime();
85                  log.warn("Unparsable lastModified : " + sLastModified);
86              }
87          }
88  
89          return lastModified;
90      }
91  
92      /**
93       * Given a URI of form "{partial.class.name}/{resourceName}", locate the
94       * specified file within the current classpath and write it to the
95       * response object.
96       * <p>
97       * The partial class name has "org.apache.myfaces.custom." prepended
98       * to it to form the fully qualified classname. This class object is
99       * loaded, and Class.getResourceAsStream is called on it, passing
100      * a uri of "resource/" + {resourceName}.
101      * <p>
102      * The data written to the response stream includes http headers
103      * which define the mime content-type; this is deduced from the
104      * filename suffix of the resource.
105      * <p>
106      * @see org.apache.myfaces.renderkit.html.util.ResourceLoader#serveResource(javax.servlet.ServletContext,
107      *     javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.String)
108      */
109     public void serveResource(ServletContext context, HttpServletRequest request,
110             HttpServletResponse response, String resourceUri) throws IOException
111     {
112         String[] uriParts = resourceUri.split("/", 2);
113 
114         String component = uriParts[0];
115         if (component == null || component.trim().length() == 0)
116         {
117             response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid request");
118             log.error("Could not find parameter for component to load a resource.");
119             return;
120         }
121         Class componentClass;
122         String className = ORG_APACHE_MYFACES_CUSTOM + "." + component;
123         try
124         {
125             componentClass = loadComponentClass(className);
126         }
127         catch (ClassNotFoundException e)
128         {
129             response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
130             log.error("Could not find the class for component " + className
131                     + " to load a resource.");
132             return;
133         }
134         String resource = uriParts[1];
135         if (resource == null || resource.trim().length() == 0)
136         {
137             response.sendError(HttpServletResponse.SC_BAD_REQUEST, "No resource defined");
138             log.error("No resource defined component class " + className);
139             return;
140         }
141 
142         InputStream is = null;
143 
144         try
145         {
146             ResourceProvider resourceProvider;
147             if (ResourceProvider.class.isAssignableFrom(componentClass))
148             {
149                 try
150                 {
151                     resourceProvider = (ResourceProvider) componentClass.newInstance();
152                 }
153                 catch (InstantiationException e)
154                 {
155                     response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Unable to instantiate resource provider for resource "
156                             + resource + " for component " + component);
157                     log.error("Unable to instantiate resource provider for resource " + resource + " for component " + component, e);
158                     return;
159                 }
160                 catch (IllegalAccessException e)
161                 {
162                     response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Unable to instantiate resource provider for resource "
163                             + resource + " for component " + component);
164                     log.error("Unable to instantiate resource provider for resource " + resource + " for component " + component, e);
165                     return;
166                 }
167             }
168             else
169             {
170                 resourceProvider = new DefaultResourceProvider(componentClass);
171             }
172 
173             if (!resourceProvider.exists(context, resource))
174             {
175                 response.sendError(HttpServletResponse.SC_NOT_FOUND, "Unable to find resource "
176                         + resource + " for component " + component
177                         + ". Check that this file is available " + "in the classpath in sub-directory "
178                         + "/resource of the package-directory.");
179                 log.error("Unable to find resource " + resource + " for component " + component
180                         + ". Check that this file is available " + "in the classpath in sub-directory "
181                         + "/resource of the package-directory.");
182             }
183             else
184             {
185                 // URLConnection con = url.openConnection();
186 
187                 long lastModified = resourceProvider.getLastModified(context, resource);
188                 if (lastModified < 1)
189                 {
190                     // fallback
191                     lastModified = getLastModified();
192                 }
193 
194                 long browserDate = request.getDateHeader("If-Modified-Since");
195                 if (browserDate > -1)
196                 {
197                     // normalize to seconds - this should work with any os
198                     lastModified = (lastModified / 1000) * 1000;
199                     browserDate = (browserDate / 1000) * 1000;
200 
201                     if (lastModified == browserDate)
202                     {
203                         // the browser already has the correct version
204 
205                         response.setStatus(HttpURLConnection.HTTP_NOT_MODIFIED);
206                         return;
207                     }
208                 }
209 
210 
211                 int contentLength = resourceProvider.getContentLength(context, resource);
212                 String contentEncoding = resourceProvider.getEncoding(context, resource);
213 
214                 is = resourceProvider.getInputStream(context, resource);
215 
216                 defineContentHeaders(request, response, resource, contentLength, contentEncoding);
217                 defineCaching(request, response, resource, lastModified);
218                 writeResource(request, response, is);
219             }
220         }
221         finally
222         {
223             // nothing to do here..
224         }
225     }
226 
227     /**
228      * Copy the content of the specified input stream to the servlet response.
229      */
230     protected void writeResource(HttpServletRequest request, HttpServletResponse response,
231             InputStream in) throws IOException
232     {
233         ServletOutputStream out = response.getOutputStream();
234         try
235         {
236             byte[] buffer = new byte[1024];
237             for (int size = in.read(buffer); size != -1; size = in.read(buffer))
238             {
239                 out.write(buffer, 0, size);
240             }
241             out.flush();
242         }
243         catch(IOException e)
244         {
245             // This happens sometimes with Microsft Internet Explorer. It would
246             // appear (guess) that when javascript creates multiple dom nodes
247             // referring to the same remote resource then IE stupidly opens 
248             // multiple sockets and requests that resource multiple times. But
249             // when the first request completes, it then realises its stupidity
250             // and forcibly closes all the other sockets. But here we are trying
251             // to service those requests, and so get a "broken pipe" failure 
252             // on write. The only thing to do here is to silently ignore the issue,
253             // ie suppress the exception. Note that it is also possible for the
254             // above code to succeed (ie this exception clause is not run) but
255             // for a later flush to get the "broken pipe"; this is either due
256             // just to timing, or possibly IE is closing sockets after receiving
257             // a complete file for some types (gif?) rather than waiting for the
258             // server to close it. We throw a special exception here to inform
259             // callers that they should NOT flush anything - though that is
260             // dangerous no matter what IOException subclass is thrown.
261             log.debug("Unable to send resource data to client", e);
262             throw new ResourceLoader.ClosedSocketException();
263         }
264     }
265 
266     /**
267      * Output http headers telling the browser (and possibly intermediate caches) how
268      * to cache this data.
269      * <p>
270      * The expiry time in this header info is set to 7 days. This is not a problem as
271      * the overall URI contains a "cache key" that changes whenever the webapp is
272      * redeployed (see AddResource.getCacheKey), meaning that all browsers will
273      * effectively reload files on webapp redeploy.
274      */
275     protected void defineCaching(HttpServletRequest request, HttpServletResponse response,
276             String resource, long lastModified)
277     {
278         response.setDateHeader("Last-Modified", lastModified);
279 
280         Calendar expires = Calendar.getInstance();
281         expires.add(Calendar.DAY_OF_YEAR, 7);
282         response.setDateHeader("Expires", expires.getTimeInMillis());
283 
284         //12 hours: 43200 = 60s * 60 * 12
285         response.setHeader("Cache-Control", "max-age=43200");
286         response.setHeader("Pragma", "");
287     }
288 
289     /**
290      * Output http headers indicating the mime-type of the content being served.
291      * The mime-type output is determined by the resource filename suffix.
292      */
293     protected void defineContentHeaders(HttpServletRequest request, HttpServletResponse response,
294                                         String resource, int contentLength, String contentEncoding)
295     {
296         String charset = "";
297         if (contentEncoding != null)
298         {
299             charset = "; charset=" + contentEncoding;
300         }
301         if (contentLength > -1)
302         {
303             response.setContentLength(contentLength);
304         }
305 
306         if (resource.endsWith(".js"))
307             response.setContentType(
308                 org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT + charset);
309         else if (resource.endsWith(".css"))
310             response.setContentType(
311                 org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.STYLE_TYPE_TEXT_CSS + charset);
312         else if (resource.endsWith(".gif"))
313             response.setContentType("image/gif");
314         else if (resource.endsWith(".png"))
315             response.setContentType("image/png");
316         else if (resource.endsWith(".jpg") || resource.endsWith(".jpeg"))
317             response.setContentType("image/jpeg");
318         else if (resource.endsWith(".xml") || resource.endsWith(".xsl"))
319             response.setContentType("text/xml"); // XSL has to be served as XML.
320     }
321 
322     protected Class loadComponentClass(String componentClass) throws ClassNotFoundException
323     {
324         return ClassUtils.classForName(componentClass);
325     }
326 
327     // NOTE: This method is not being used. Perhaps it can be removed?
328     protected void validateCustomComponent(Class myfacesCustomComponent)
329     {
330         if (!myfacesCustomComponent.getName().startsWith(ORG_APACHE_MYFACES_CUSTOM + "."))
331         {
332             throw new IllegalArgumentException(
333                     "expected a myfaces custom component class in package "
334                             + ORG_APACHE_MYFACES_CUSTOM);
335         }
336     }
337 }