001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (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.URI; 037import java.net.URL; 038import java.net.URLConnection; 039 040import javax.servlet.http.HttpServletRequest; 041import javax.servlet.http.HttpServletResponse; 042 043import org.apache.commons.logging.Log; 044 045/** 046 * Handles the requests for static resources located in the classpath.<p> 047 */ 048public class CmsStaticResourceHandler implements I_CmsRequestHandler { 049 050 /** The handler name. */ 051 public static final String HANDLER_NAME = "Static"; 052 053 /** The static resource prefix '/handleStatic'. */ 054 public static final String STATIC_RESOURCE_PREFIX = OpenCmsServlet.HANDLE_PATH + HANDLER_NAME; 055 056 /** The default output buffer size. */ 057 private static final int DEFAULT_BUFFER_SIZE = 32 * 1024; 058 059 /** Default cache lifetime in seconds. */ 060 private static final int DEFAULT_CACHE_TIME = 3600; 061 062 /** The handler names. */ 063 private static final String[] HANDLER_NAMES = new String[] {HANDLER_NAME}; 064 065 /** The log object for this class. */ 066 private static final Log LOG = CmsLog.getLog(CmsStaticResourceHandler.class); 067 068 /** The regular expression to remove the static resource path prefix. */ 069 private static String m_removePrefixRegex; 070 071 /** The regular expression to identify static resource paths. */ 072 private static String m_staticResourceRegex; 073 074 /** The opencms path prefix for static resources. */ 075 private static final String OPENCMS_PATH_PREFIX = "OPENCMS/"; 076 077 /** 078 * Returns the context for static resources served from the class path, e.g. "/opencms/handleStatic/v5976v".<p> 079 * 080 * @param opencmsContext the OpenCms context 081 * @param opencmsVersion the OpenCms version 082 * 083 * @return the static resource context 084 */ 085 public static String getStaticResourceContext(String opencmsContext, String opencmsVersion) { 086 087 return opencmsContext + STATIC_RESOURCE_PREFIX + "/v" + opencmsVersion.hashCode() + "v"; 088 } 089 090 /** 091 * Returns the URL to a static resource.<p> 092 * 093 * @param resourcePath the static resource path 094 * 095 * @return the resource URL 096 */ 097 public static URL getStaticResourceURL(String resourcePath) { 098 099 URL resourceURL = null; 100 if (isStaticResourceUri(resourcePath)) { 101 String path = removeStaticResourcePrefix(resourcePath); 102 path = CmsStringUtil.joinPaths(OPENCMS_PATH_PREFIX, path); 103 resourceURL = OpenCms.getSystemInfo().getClass().getClassLoader().getResource(path); 104 } 105 return resourceURL; 106 } 107 108 /** 109 * Returns if the given URI points to a static resource.<p> 110 * 111 * @param path the path to test 112 * 113 * @return <code>true</code> in case the given URI points to a static resource 114 */ 115 public static boolean isStaticResourceUri(String path) { 116 117 return (path != null) && path.matches(getStaticResourceRegex()); 118 119 } 120 121 /** 122 * Returns if the given URI points to a static resource.<p> 123 * 124 * @param uri the URI to test 125 * 126 * @return <code>true</code> in case the given URI points to a static resource 127 */ 128 public static boolean isStaticResourceUri(URI uri) { 129 130 return (uri != null) && isStaticResourceUri(uri.getPath()); 131 132 } 133 134 /** 135 * Removes the static resource path prefix.<p> 136 * 137 * @param path the path 138 * 139 * @return the modified path 140 */ 141 public static String removeStaticResourcePrefix(String path) { 142 143 return path.replaceFirst(getRemovePrefixRegex(), ""); 144 } 145 146 /** 147 * Returns the regular expression to remove the static resource path prefix.<p> 148 * 149 * @return the regular expression to remove the static resource path prefix 150 */ 151 private static String getRemovePrefixRegex() { 152 153 if (m_removePrefixRegex == null) { 154 m_removePrefixRegex = "^(" 155 + OpenCms.getStaticExportManager().getVfsPrefix() 156 + ")?" 157 + STATIC_RESOURCE_PREFIX 158 + "(/v-?\\d+v/)?"; 159 } 160 return m_removePrefixRegex; 161 } 162 163 /** 164 * Returns the regular expression to identify static resource paths.<p> 165 * 166 * @return the regular expression to identify static resource paths 167 */ 168 private static String getStaticResourceRegex() { 169 170 if (m_staticResourceRegex == null) { 171 m_staticResourceRegex = getRemovePrefixRegex() + ".*"; 172 } 173 return m_staticResourceRegex; 174 } 175 176 /** 177 * @see org.opencms.main.I_CmsRequestHandler#getHandlerNames() 178 */ 179 public String[] getHandlerNames() { 180 181 return HANDLER_NAMES; 182 } 183 184 /** 185 * @see org.opencms.main.I_CmsRequestHandler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.String) 186 */ 187 public void handle(HttpServletRequest request, HttpServletResponse response, String name) throws IOException { 188 189 String path = OpenCmsCore.getInstance().getPathInfo(request); 190 URL resourceURL = getStaticResourceURL(path); 191 if (resourceURL != null) { 192 setResponseHeaders(request, response, path, resourceURL); 193 writeStaticResourceResponse(request, response, resourceURL); 194 } else { 195 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 196 } 197 } 198 199 /** 200 * Returns whether this servlet should attempt to serve a precompressed 201 * version of the given static resource. If this method returns true, the 202 * suffix {@code .gz} is appended to the URL and the corresponding resource 203 * is served if it exists. It is assumed that the compression method used is 204 * gzip. If this method returns false or a compressed version is not found, 205 * the original URL is used.<p> 206 * 207 * The base implementation of this method returns true if and only if the 208 * request indicates that the client accepts gzip compressed responses and 209 * the filename extension of the requested resource is .js, .css, or .html.<p> 210 * 211 * @param request the request for the resource 212 * @param url the URL of the requested resource 213 * 214 * @return true if the servlet should attempt to serve a precompressed version of the resource, false otherwise 215 */ 216 protected boolean allowServePrecompressedResource(HttpServletRequest request, String url) { 217 218 String accept = request.getHeader("Accept-Encoding"); 219 return (accept != null) 220 && accept.contains("gzip") 221 && (url.endsWith(".js") || url.endsWith(".css") || url.endsWith(".html")); 222 } 223 224 /** 225 * Calculates the cache lifetime for the given filename in seconds. By 226 * default filenames containing ".nocache." return 0, filenames containing 227 * ".cache." return one year, all other return the value defined in the 228 * web.xml using resourceCacheTime (defaults to 1 hour).<p> 229 * 230 * @param filename the file name 231 * 232 * @return cache lifetime for the given filename in seconds 233 */ 234 protected int getCacheTime(String filename) { 235 236 /* 237 * GWT conventions: 238 * 239 * - files containing .nocache. will not be cached. 240 * 241 * - files containing .cache. will be cached for one year. 242 * 243 * https://developers.google.com/web-toolkit/doc/latest/ 244 * DevGuideCompilingAndDebugging#perfect_caching 245 */ 246 if (filename.contains(".nocache.")) { 247 return 0; 248 } 249 if (filename.contains(".cache.")) { 250 return 60 * 60 * 24 * 365; 251 } 252 /* 253 * For all other files, the browser is allowed to cache for 1 hour 254 * without checking if the file has changed. This forces browsers to 255 * fetch a new version when the Vaadin version is updated. This will 256 * cause more requests to the servlet than without this but for high 257 * volume sites the static files should never be served through the 258 * servlet. 259 */ 260 return DEFAULT_CACHE_TIME; 261 } 262 263 /** 264 * Sets the response headers.<p> 265 * 266 * @param request the request 267 * @param response the response 268 * @param filename the file name 269 * @param resourceURL the resource URL 270 */ 271 protected void setResponseHeaders( 272 HttpServletRequest request, 273 HttpServletResponse response, 274 String filename, 275 URL resourceURL) { 276 277 String cacheControl = "public, max-age=0, must-revalidate"; 278 int resourceCacheTime = getCacheTime(filename); 279 if (resourceCacheTime > 0) { 280 cacheControl = "max-age=" + String.valueOf(resourceCacheTime); 281 } 282 response.setHeader("Cache-Control", cacheControl); 283 response.setDateHeader("Expires", System.currentTimeMillis() + (resourceCacheTime * 1000)); 284 285 // Find the modification timestamp 286 long lastModifiedTime = 0; 287 URLConnection connection = null; 288 try { 289 connection = resourceURL.openConnection(); 290 lastModifiedTime = connection.getLastModified(); 291 // Remove milliseconds to avoid comparison problems (milliseconds 292 // are not returned by the browser in the "If-Modified-Since" 293 // header). 294 lastModifiedTime = lastModifiedTime - (lastModifiedTime % 1000); 295 response.setDateHeader("Last-Modified", lastModifiedTime); 296 297 if (browserHasNewestVersion(request, lastModifiedTime)) { 298 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 299 return; 300 } 301 } catch (Exception e) { 302 // Failed to find out last modified timestamp. Continue without it. 303 LOG.debug("Failed to find out last modified timestamp. Continuing without it.", e); 304 } finally { 305 try { 306 if (connection != null) { 307 // Explicitly close the input stream to prevent it 308 // from remaining hanging 309 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4257700 310 InputStream is = connection.getInputStream(); 311 if (is != null) { 312 is.close(); 313 } 314 } 315 } catch (Exception e) { 316 LOG.info("Error closing URLConnection input stream", e); 317 } 318 } 319 320 // Set type mime type if we can determine it based on the filename 321 String mimetype = OpenCms.getResourceManager().getMimeType(filename, "UTF-8"); 322 if (mimetype != null) { 323 response.setContentType(mimetype); 324 } 325 } 326 327 /** 328 * Writes the contents of the given resourceUrl in the response. Can be 329 * overridden to add/modify response headers and similar.<p> 330 * 331 * @param request the request for the resource 332 * @param response the response 333 * @param resourceUrl the url to send 334 * 335 * @throws IOException in case writing the response fails 336 */ 337 protected void writeStaticResourceResponse( 338 HttpServletRequest request, 339 HttpServletResponse response, 340 URL resourceUrl) 341 throws IOException { 342 343 URLConnection connection = null; 344 InputStream is = null; 345 String urlStr = resourceUrl.toExternalForm(); 346 try { 347 if (allowServePrecompressedResource(request, urlStr)) { 348 // try to serve a precompressed version if available 349 try { 350 connection = new URL(urlStr + ".gz").openConnection(); 351 is = connection.getInputStream(); 352 // set gzip headers 353 response.setHeader("Content-Encoding", "gzip"); 354 } catch (Exception e) { 355 LOG.debug("Unexpected exception looking for gzipped version of resource " + urlStr, e); 356 } 357 } 358 if (is == null) { 359 // precompressed resource not available, get non compressed 360 connection = resourceUrl.openConnection(); 361 try { 362 is = connection.getInputStream(); 363 } catch (FileNotFoundException e) { 364 LOG.debug(e.getMessage(), e); 365 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 366 return; 367 } 368 } 369 370 try { 371 @SuppressWarnings("null") 372 int length = connection.getContentLength(); 373 if (length >= 0) { 374 response.setContentLength(length); 375 } 376 } catch (Throwable e) { 377 LOG.debug(e.getMessage(), e); 378 // This can be ignored, content length header is not required. 379 // Need to close the input stream because of 380 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4257700 to 381 // prevent it from hanging, but that is done below. 382 } 383 384 streamContent(response, is); 385 } finally { 386 if (is != null) { 387 is.close(); 388 } 389 } 390 } 391 392 /** 393 * Checks if the browser has an up to date cached version of requested 394 * resource. Currently the check is performed using the "If-Modified-Since" 395 * header. Could be expanded if needed.<p> 396 * 397 * @param request the HttpServletRequest from the browser 398 * @param resourceLastModifiedTimestamp the timestamp when the resource was last modified. 0 if the last modification time is unknown 399 * 400 * @return true if the If-Modified-Since header tells the cached version in the browser is up to date, false otherwise 401 */ 402 private boolean browserHasNewestVersion(HttpServletRequest request, long resourceLastModifiedTimestamp) { 403 404 if (resourceLastModifiedTimestamp < 1) { 405 // We do not know when it was modified so the browser cannot have an 406 // up-to-date version 407 return false; 408 } 409 /* 410 * The browser can request the resource conditionally using an 411 * If-Modified-Since header. Check this against the last modification 412 * time. 413 */ 414 try { 415 // If-Modified-Since represents the timestamp of the version cached 416 // in the browser 417 long headerIfModifiedSince = request.getDateHeader("If-Modified-Since"); 418 419 if (headerIfModifiedSince >= resourceLastModifiedTimestamp) { 420 // Browser has this an up-to-date version of the resource 421 return true; 422 } 423 } catch (Exception e) { 424 // Failed to parse header. Fail silently - the browser does not have 425 // an up-to-date version in its cache. 426 } 427 return false; 428 } 429 430 /** 431 * Streams the input stream to the response.<p> 432 * 433 * @param response the response 434 * @param is the input stream 435 * 436 * @throws IOException in case writing to the response fails 437 */ 438 private void streamContent(HttpServletResponse response, InputStream is) throws IOException { 439 440 OutputStream os = response.getOutputStream(); 441 try { 442 byte buffer[] = new byte[DEFAULT_BUFFER_SIZE]; 443 int bytes; 444 while ((bytes = is.read(buffer)) >= 0) { 445 os.write(buffer, 0, bytes); 446 } 447 } finally { 448 os.close(); 449 } 450 } 451}