001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.main; 029 030import org.opencms.util.CmsStringUtil; 031 032import java.io.FileNotFoundException; 033import java.io.IOException; 034import java.io.InputStream; 035import java.io.OutputStream; 036import java.net.URL; 037import java.net.URLConnection; 038 039import javax.servlet.http.HttpServletRequest; 040import javax.servlet.http.HttpServletResponse; 041 042import org.apache.commons.logging.Log; 043 044/** 045 * Handles the requests for static resources located in the classpath.<p> 046 */ 047public class CmsStaticResourceHandler implements I_CmsRequestHandler { 048 049 /** The handler name. */ 050 public static final String HANDLER_NAME = "Static"; 051 052 /** The default output buffer size. */ 053 private static final int DEFAULT_BUFFER_SIZE = 32 * 1024; 054 055 /** Default cache lifetime in seconds. */ 056 private static final int DEFAULT_CACHE_TIME = 3600; 057 058 /** The handler names. */ 059 private static final String[] HANDLER_NAMES = new String[] {HANDLER_NAME}; 060 061 /** The log object for this class. */ 062 private static final Log LOG = CmsLog.getLog(CmsStaticResourceHandler.class); 063 064 /** The opencms path prefix for static resources. */ 065 private static final String OPENCMS_PATH_PREFIX = "OPENCMS/"; 066 067 /** 068 * Returns the URL to a static resource.<p> 069 * 070 * @param resourcePath the static resource path 071 * 072 * @return the resource URL 073 */ 074 public static URL getStaticResourceURL(String resourcePath) { 075 076 URL resourceURL = null; 077 String prefix = OpenCmsServlet.HANDLE_PATH + HANDLER_NAME + "/"; 078 int index = resourcePath.indexOf(prefix); 079 if (index >= 0) { 080 String path = resourcePath.substring(index + prefix.length()); 081 path = CmsStringUtil.joinPaths(OPENCMS_PATH_PREFIX, path); 082 resourceURL = OpenCms.getSystemInfo().getClass().getClassLoader().getResource(path); 083 } 084 return resourceURL; 085 } 086 087 /** 088 * @see org.opencms.main.I_CmsRequestHandler#getHandlerNames() 089 */ 090 public String[] getHandlerNames() { 091 092 return HANDLER_NAMES; 093 } 094 095 /** 096 * @see org.opencms.main.I_CmsRequestHandler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.String) 097 */ 098 public void handle(HttpServletRequest request, HttpServletResponse response, String name) throws IOException { 099 100 String path = OpenCmsCore.getInstance().getPathInfo(request); 101 URL resourceURL = getStaticResourceURL(path); 102 if (resourceURL != null) { 103 setResponseHeaders(request, response, path, resourceURL); 104 writeStaticResourceResponse(request, response, resourceURL); 105 } else { 106 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 107 } 108 } 109 110 /** 111 * Returns whether this servlet should attempt to serve a precompressed 112 * version of the given static resource. If this method returns true, the 113 * suffix {@code .gz} is appended to the URL and the corresponding resource 114 * is served if it exists. It is assumed that the compression method used is 115 * gzip. If this method returns false or a compressed version is not found, 116 * the original URL is used.<p> 117 * 118 * The base implementation of this method returns true if and only if the 119 * request indicates that the client accepts gzip compressed responses and 120 * the filename extension of the requested resource is .js, .css, or .html.<p> 121 * 122 * @param request the request for the resource 123 * @param url the URL of the requested resource 124 * 125 * @return true if the servlet should attempt to serve a precompressed version of the resource, false otherwise 126 */ 127 protected boolean allowServePrecompressedResource(HttpServletRequest request, String url) { 128 129 String accept = request.getHeader("Accept-Encoding"); 130 return (accept != null) 131 && accept.contains("gzip") 132 && (url.endsWith(".js") || url.endsWith(".css") || url.endsWith(".html")); 133 } 134 135 /** 136 * Calculates the cache lifetime for the given filename in seconds. By 137 * default filenames containing ".nocache." return 0, filenames containing 138 * ".cache." return one year, all other return the value defined in the 139 * web.xml using resourceCacheTime (defaults to 1 hour).<p> 140 * 141 * @param filename the file name 142 * 143 * @return cache lifetime for the given filename in seconds 144 */ 145 protected int getCacheTime(String filename) { 146 147 /* 148 * GWT conventions: 149 * 150 * - files containing .nocache. will not be cached. 151 * 152 * - files containing .cache. will be cached for one year. 153 * 154 * https://developers.google.com/web-toolkit/doc/latest/ 155 * DevGuideCompilingAndDebugging#perfect_caching 156 */ 157 if (filename.contains(".nocache.")) { 158 return 0; 159 } 160 if (filename.contains(".cache.")) { 161 return 60 * 60 * 24 * 365; 162 } 163 /* 164 * For all other files, the browser is allowed to cache for 1 hour 165 * without checking if the file has changed. This forces browsers to 166 * fetch a new version when the Vaadin version is updated. This will 167 * cause more requests to the servlet than without this but for high 168 * volume sites the static files should never be served through the 169 * servlet. 170 */ 171 return DEFAULT_CACHE_TIME; 172 } 173 174 /** 175 * Sets the response headers.<p> 176 * 177 * @param request the request 178 * @param response the response 179 * @param filename the file name 180 * @param resourceURL the resource URL 181 */ 182 protected void setResponseHeaders( 183 HttpServletRequest request, 184 HttpServletResponse response, 185 String filename, 186 URL resourceURL) { 187 188 String cacheControl = "public, max-age=0, must-revalidate"; 189 int resourceCacheTime = getCacheTime(filename); 190 if (resourceCacheTime > 0) { 191 cacheControl = "max-age=" + String.valueOf(resourceCacheTime); 192 } 193 response.setHeader("Cache-Control", cacheControl); 194 response.setDateHeader("Expires", System.currentTimeMillis() + (resourceCacheTime * 1000)); 195 196 // Find the modification timestamp 197 long lastModifiedTime = 0; 198 URLConnection connection = null; 199 try { 200 connection = resourceURL.openConnection(); 201 lastModifiedTime = connection.getLastModified(); 202 // Remove milliseconds to avoid comparison problems (milliseconds 203 // are not returned by the browser in the "If-Modified-Since" 204 // header). 205 lastModifiedTime = lastModifiedTime - (lastModifiedTime % 1000); 206 response.setDateHeader("Last-Modified", lastModifiedTime); 207 208 if (browserHasNewestVersion(request, lastModifiedTime)) { 209 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 210 return; 211 } 212 } catch (Exception e) { 213 // Failed to find out last modified timestamp. Continue without it. 214 LOG.debug("Failed to find out last modified timestamp. Continuing without it.", e); 215 } finally { 216 try { 217 if (connection != null) { 218 // Explicitly close the input stream to prevent it 219 // from remaining hanging 220 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4257700 221 InputStream is = connection.getInputStream(); 222 if (is != null) { 223 is.close(); 224 } 225 } 226 } catch (Exception e) { 227 LOG.info("Error closing URLConnection input stream", e); 228 } 229 } 230 231 // Set type mime type if we can determine it based on the filename 232 String mimetype = OpenCms.getResourceManager().getMimeType(filename, "UTF-8"); 233 if (mimetype != null) { 234 response.setContentType(mimetype); 235 } 236 } 237 238 /** 239 * Writes the contents of the given resourceUrl in the response. Can be 240 * overridden to add/modify response headers and similar.<p> 241 * 242 * @param request the request for the resource 243 * @param response the response 244 * @param resourceUrl the url to send 245 * 246 * @throws IOException in case writing the response fails 247 */ 248 protected void writeStaticResourceResponse( 249 HttpServletRequest request, 250 HttpServletResponse response, 251 URL resourceUrl) throws IOException { 252 253 URLConnection connection = null; 254 InputStream is = null; 255 String urlStr = resourceUrl.toExternalForm(); 256 try { 257 if (allowServePrecompressedResource(request, urlStr)) { 258 // try to serve a precompressed version if available 259 try { 260 connection = new URL(urlStr + ".gz").openConnection(); 261 is = connection.getInputStream(); 262 // set gzip headers 263 response.setHeader("Content-Encoding", "gzip"); 264 } catch (Exception e) { 265 LOG.debug("Unexpected exception looking for gzipped version of resource " + urlStr, e); 266 } 267 } 268 if (is == null) { 269 // precompressed resource not available, get non compressed 270 connection = resourceUrl.openConnection(); 271 try { 272 is = connection.getInputStream(); 273 } catch (FileNotFoundException e) { 274 LOG.debug(e.getMessage(), e); 275 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 276 return; 277 } 278 } 279 280 try { 281 @SuppressWarnings("null") 282 int length = connection.getContentLength(); 283 if (length >= 0) { 284 response.setContentLength(length); 285 } 286 } catch (Throwable e) { 287 LOG.debug(e.getMessage(), e); 288 // This can be ignored, content length header is not required. 289 // Need to close the input stream because of 290 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4257700 to 291 // prevent it from hanging, but that is done below. 292 } 293 294 streamContent(response, is); 295 } finally { 296 if (is != null) { 297 is.close(); 298 } 299 } 300 } 301 302 /** 303 * Checks if the browser has an up to date cached version of requested 304 * resource. Currently the check is performed using the "If-Modified-Since" 305 * header. Could be expanded if needed.<p> 306 * 307 * @param request the HttpServletRequest from the browser 308 * @param resourceLastModifiedTimestamp the timestamp when the resource was last modified. 0 if the last modification time is unknown 309 * 310 * @return true if the If-Modified-Since header tells the cached version in the browser is up to date, false otherwise 311 */ 312 private boolean browserHasNewestVersion(HttpServletRequest request, long resourceLastModifiedTimestamp) { 313 314 if (resourceLastModifiedTimestamp < 1) { 315 // We do not know when it was modified so the browser cannot have an 316 // up-to-date version 317 return false; 318 } 319 /* 320 * The browser can request the resource conditionally using an 321 * If-Modified-Since header. Check this against the last modification 322 * time. 323 */ 324 try { 325 // If-Modified-Since represents the timestamp of the version cached 326 // in the browser 327 long headerIfModifiedSince = request.getDateHeader("If-Modified-Since"); 328 329 if (headerIfModifiedSince >= resourceLastModifiedTimestamp) { 330 // Browser has this an up-to-date version of the resource 331 return true; 332 } 333 } catch (@SuppressWarnings("unused") Exception e) { 334 // Failed to parse header. Fail silently - the browser does not have 335 // an up-to-date version in its cache. 336 } 337 return false; 338 } 339 340 /** 341 * Streams the input stream to the response.<p> 342 * 343 * @param response the response 344 * @param is the input stream 345 * 346 * @throws IOException in case writing to the response fails 347 */ 348 private void streamContent(HttpServletResponse response, InputStream is) throws IOException { 349 350 OutputStream os = response.getOutputStream(); 351 try { 352 byte buffer[] = new byte[DEFAULT_BUFFER_SIZE]; 353 int bytes; 354 while ((bytes = is.read(buffer)) >= 0) { 355 os.write(buffer, 0, bytes); 356 } 357 } finally { 358 os.close(); 359 } 360 } 361}