View Javadoc

1   /*
2    * $Id$
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  package org.apache.struts2.dispatcher;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.io.UnsupportedEncodingException;
27  import java.net.URL;
28  import java.net.URLDecoder;
29  import java.util.ArrayList;
30  import java.util.Calendar;
31  import java.util.List;
32  import java.util.StringTokenizer;
33  
34  import javax.servlet.http.HttpServletRequest;
35  import javax.servlet.http.HttpServletResponse;
36  
37  import org.apache.struts2.StrutsConstants;
38  import org.apache.struts2.dispatcher.ng.HostConfig;
39  import org.apache.struts2.util.ClassLoaderUtils;
40  
41  import com.opensymphony.xwork2.inject.Inject;
42  import com.opensymphony.xwork2.util.ClassLoaderUtil;
43  import com.opensymphony.xwork2.util.logging.Logger;
44  import com.opensymphony.xwork2.util.logging.LoggerFactory;
45  
46  public class DefaultStaticContentLoader implements StaticContentLoader {
47      /***
48       * Provide a logging instance.
49       */
50      private Logger log;
51  
52      /***
53       * Store set of path prefixes to use with static resources.
54       */
55      protected String[] pathPrefixes;
56  
57      /***
58       * Store state of StrutsConstants.STRUTS_SERVE_STATIC_CONTENT setting.
59       */
60      protected boolean serveStatic;
61  
62      /***
63       * Store state of StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE setting.
64       */
65      protected boolean serveStaticBrowserCache;
66  
67      /***
68       * Provide a formatted date for setting heading information when caching static content.
69       */
70      protected final Calendar lastModifiedCal = Calendar.getInstance();
71  
72      /***
73       * Store state of StrutsConstants.STRUTS_I18N_ENCODING setting.
74       */
75      protected String encoding;
76  
77  
78      /***
79       * Modify state of StrutsConstants.STRUTS_SERVE_STATIC_CONTENT setting.
80       *
81       * @param val
82       *            New setting
83       */
84      @Inject(StrutsConstants.STRUTS_SERVE_STATIC_CONTENT)
85      public void setServeStaticContent(String val) {
86          serveStatic = "true".equals(val);
87      }
88  
89      /***
90       * Modify state of StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE
91       * setting.
92       *
93       * @param val
94       *            New setting
95       */
96      @Inject(StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE)
97      public void setServeStaticBrowserCache(String val) {
98          serveStaticBrowserCache = "true".equals(val);
99      }
100 
101     /***
102      * Modify state of StrutsConstants.STRUTS_I18N_ENCODING setting.
103      * @param val New setting
104      */
105     @Inject(StrutsConstants.STRUTS_I18N_ENCODING)
106     public void setEncoding(String val) {
107         encoding = val;
108     }
109 
110     /*
111      * (non-Javadoc)
112      *
113      * @see org.apache.struts2.dispatcher.StaticResourceLoader#setHostConfig(javax.servlet.FilterConfig)
114      */
115     public void setHostConfig(HostConfig filterConfig) {
116         String param = filterConfig.getInitParameter("packages");
117         String packages = getAdditionalPackages();
118         if (param != null) {
119             packages = param + " " + packages;
120         }
121         this.pathPrefixes = parse(packages);
122         initLogging(filterConfig);
123     }
124 
125     protected String getAdditionalPackages() {
126         return "org.apache.struts2.static template org.apache.struts2.interceptor.debugging static";
127     }
128 
129     /***
130      * Create a string array from a comma-delimited list of packages.
131      *
132      * @param packages
133      *            A comma-delimited String listing packages
134      * @return A string array of packages
135      */
136     protected String[] parse(String packages) {
137         if (packages == null) {
138             return null;
139         }
140         List<String> pathPrefixes = new ArrayList<String>();
141 
142         StringTokenizer st = new StringTokenizer(packages, ", \n\t");
143         while (st.hasMoreTokens()) {
144             String pathPrefix = st.nextToken().replace('.', '/');
145             if (!pathPrefix.endsWith("/")) {
146                 pathPrefix += "/";
147             }
148             pathPrefixes.add(pathPrefix);
149         }
150 
151         return pathPrefixes.toArray(new String[pathPrefixes.size()]);
152     }
153 
154     /*
155      * (non-Javadoc)
156      *
157      * @see org.apache.struts2.dispatcher.StaticResourceLoader#findStaticResource(java.lang.String,
158      *      javax.servlet.http.HttpServletRequest,
159      *      javax.servlet.http.HttpServletResponse)
160      */
161     public void findStaticResource(String path, HttpServletRequest request, HttpServletResponse response)
162             throws IOException {
163         String name = cleanupPath(path);
164         for (String pathPrefix : pathPrefixes) {
165             URL resourceUrl = findResource(buildPath(name, pathPrefix));
166             if (resourceUrl != null) {
167                 InputStream is = null;
168                 try {
169                     //check that the resource path is under the pathPrefix path
170                     String pathEnding = buildPath(name, pathPrefix);
171                     if (resourceUrl.getFile().endsWith(pathEnding))
172                         is = resourceUrl.openStream();
173                 } catch (Exception ex) {
174                     // just ignore it
175                     continue;
176                 }
177 
178                 //not inside the try block, as this could throw IOExceptions also
179                 if (is != null) {
180                     process(is, path, request, response);
181                     return;
182                 }
183             }
184         }
185 
186         response.sendError(HttpServletResponse.SC_NOT_FOUND);
187     }
188 
189     protected void process(InputStream is, String path, HttpServletRequest request, HttpServletResponse response) throws IOException {
190         if (is != null) {
191             Calendar cal = Calendar.getInstance();
192 
193             // check for if-modified-since, prior to any other headers
194             long ifModifiedSince = 0;
195             try {
196                 ifModifiedSince = request.getDateHeader("If-Modified-Since");
197             } catch (Exception e) {
198                 log.warn("Invalid If-Modified-Since header value: '"
199                         + request.getHeader("If-Modified-Since") + "', ignoring");
200             }
201             long lastModifiedMillis = lastModifiedCal.getTimeInMillis();
202             long now = cal.getTimeInMillis();
203             cal.add(Calendar.DAY_OF_MONTH, 1);
204             long expires = cal.getTimeInMillis();
205 
206             if (ifModifiedSince > 0 && ifModifiedSince <= lastModifiedMillis) {
207                 // not modified, content is not sent - only basic
208                 // headers and status SC_NOT_MODIFIED
209                 response.setDateHeader("Expires", expires);
210                 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
211                 is.close();
212                 return;
213             }
214 
215             // set the content-type header
216             String contentType = getContentType(path);
217             if (contentType != null) {
218                 response.setContentType(contentType);
219             }
220 
221             if (serveStaticBrowserCache) {
222                 // set heading information for caching static content
223                 response.setDateHeader("Date", now);
224                 response.setDateHeader("Expires", expires);
225                 response.setDateHeader("Retry-After", expires);
226                 response.setHeader("Cache-Control", "public");
227                 response.setDateHeader("Last-Modified", lastModifiedMillis);
228             } else {
229                 response.setHeader("Cache-Control", "no-cache");
230                 response.setHeader("Pragma", "no-cache");
231                 response.setHeader("Expires", "-1");
232             }
233 
234             try {
235                 copy(is, response.getOutputStream());
236             } finally {
237                 is.close();
238             }
239             return;
240         }
241     }
242 
243     private void initLogging(HostConfig filterConfig) {
244         String factoryName = filterConfig.getInitParameter("loggerFactory");
245         if (factoryName != null) {
246             try {
247                 Class cls = ClassLoaderUtils.loadClass(factoryName, this.getClass());
248                 LoggerFactory fac = (LoggerFactory)cls.newInstance();
249                 LoggerFactory.setLoggerFactory(fac);
250             } catch (InstantiationException e) {
251                 System.err.println("Unable to instantiate logger factory: "+factoryName+", using default");
252                 e.printStackTrace();
253             } catch (IllegalAccessException e) {
254                 System.err.println("Unable to access logger factory: "+factoryName+", using default");
255                 e.printStackTrace();
256             } catch (ClassNotFoundException e) {
257                 System.err.println("Unable to locate logger factory class: "+factoryName+", using default");
258                 e.printStackTrace();
259             }
260         }
261 
262         log = LoggerFactory.getLogger(FilterDispatcher.class);
263 
264     }
265 
266     /***
267      * Look for a static resource in the classpath.
268      *
269      * @param path The resource path
270      * @return The inputstream of the resource
271      * @throws IOException If there is a problem locating the resource
272      */
273     protected URL findResource(String path) throws IOException {
274         return ClassLoaderUtil.getResource(path, getClass());
275     }
276 
277     /***
278      * @param name resource name
279      * @param packagePrefix The package prefix to use to locate the resource
280      * @return full path
281      * @throws UnsupportedEncodingException
282      * @throws IOException
283      */
284     protected String buildPath(String name, String packagePrefix) throws UnsupportedEncodingException {
285         String resourcePath;
286         if (packagePrefix.endsWith("/") && name.startsWith("/")) {
287             resourcePath = packagePrefix + name.substring(1);
288         } else {
289             resourcePath = packagePrefix + name;
290         }
291 
292         return URLDecoder.decode(resourcePath, encoding);
293     }
294 
295 
296 
297     /***
298      * Determine the content type for the resource name.
299      *
300      * @param name The resource name
301      * @return The mime type
302      */
303     protected String getContentType(String name) {
304         // NOT using the code provided activation.jar to avoid adding yet another dependency
305         // this is generally OK, since these are the main files we server up
306         if (name.endsWith(".js")) {
307             return "text/javascript";
308         } else if (name.endsWith(".css")) {
309             return "text/css";
310         } else if (name.endsWith(".html")) {
311             return "text/html";
312         } else if (name.endsWith(".txt")) {
313             return "text/plain";
314         } else if (name.endsWith(".gif")) {
315             return "image/gif";
316         } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
317             return "image/jpeg";
318         } else if (name.endsWith(".png")) {
319             return "image/png";
320         } else {
321             return null;
322         }
323     }
324 
325     /***
326      * Copy bytes from the input stream to the output stream.
327      *
328      * @param input
329      *            The input stream
330      * @param output
331      *            The output stream
332      * @throws IOException
333      *             If anything goes wrong
334      */
335     protected void copy(InputStream input, OutputStream output) throws IOException {
336         final byte[] buffer = new byte[4096];
337         int n;
338         while (-1 != (n = input.read(buffer))) {
339             output.write(buffer, 0, n);
340         }
341         output.flush();
342     }
343 
344     public boolean canHandle(String resourcePath) {
345         return serveStatic && (resourcePath.startsWith("/struts") || resourcePath.startsWith("/static"));
346     }
347 
348     /***
349      * @param path requested path
350      * @return path without leading "/struts" or "/static"
351      */
352     protected String cleanupPath(String path) {
353         //path will start with "/struts" or "/static", remove them
354         return path.substring(7);
355     }
356 }