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 GmbH & Co. KG, 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.flex; 029 030import org.opencms.cache.I_CmsLruCacheObject; 031import org.opencms.file.CmsResource; 032import org.opencms.i18n.CmsMessageContainer; 033import org.opencms.jsp.util.CmsJspStandardContextBean; 034import org.opencms.main.CmsLog; 035import org.opencms.monitor.CmsMemoryMonitor; 036import org.opencms.monitor.I_CmsMemoryMonitorable; 037import org.opencms.util.CmsCollectionsGenericWrapper; 038 039import java.io.IOException; 040import java.util.ArrayList; 041import java.util.Collections; 042import java.util.HashMap; 043import java.util.Iterator; 044import java.util.List; 045import java.util.Map; 046import java.util.Map.Entry; 047 048import javax.servlet.ServletException; 049 050import org.apache.commons.lang.ObjectUtils; 051import org.apache.commons.logging.Log; 052 053/** 054 * Contains the contents of a cached resource.<p> 055 * 056 * It is basically a list of pre-generated output, 057 * include() calls to other resources (with request parameters) and http headers that this 058 * resource requires to be set.<p> 059 * 060 * A CmsFlexCacheEntry might also describe a redirect-call, but in this case 061 * nothing else will be cached.<p> 062 * 063 * The pre-generated output is saved in <code>byte[]</code> arrays. 064 * The include() calls are saved as Strings of the included resource name, 065 * the parameters for the calls are saved in a HashMap. 066 * The headers are saved in a HashMap. 067 * In case of a redirect, the redirect target is cached in a String.<p> 068 * 069 * The CmsFlexCacheEntry can also have an expire date value, which indicates the time 070 * that his entry will become invalid and should thus be cleared from the cache.<p> 071 * 072 * @since 6.0.0 073 * 074 * @see org.opencms.cache.I_CmsLruCacheObject 075 */ 076public class CmsFlexCacheEntry implements I_CmsLruCacheObject, I_CmsMemoryMonitorable { 077 078 /** Initial size for lists. */ 079 public static final int INITIAL_CAPACITY_LISTS = 10; 080 081 /** The log object for this class. */ 082 private static final Log LOG = CmsLog.getLog(CmsFlexCacheEntry.class); 083 084 /** The CacheEntry's size in bytes. */ 085 private int m_byteSize; 086 087 /** Indicates if this cache entry is completed. */ 088 private boolean m_completed; 089 090 /** The "expires" date for this Flex cache entry. */ 091 private long m_dateExpires; 092 093 /** The "last modified" date for this Flex cache entry. */ 094 private long m_dateLastModified; 095 096 /** The list of items for this resource. */ 097 private List<Object> m_elements; 098 099 /** A Map of cached headers for this resource. */ 100 private Map<String, List<String>> m_headers; 101 102 /** Pointer to the next cache entry in the LRU cache. */ 103 private I_CmsLruCacheObject m_next; 104 105 /** Pointer to the previous cache entry in the LRU cache. */ 106 private I_CmsLruCacheObject m_previous; 107 108 /** Flag which indicates whether a cached redirect is permanent. */ 109 private boolean m_redirectPermanent; 110 111 /** A redirection target (if redirection is set). */ 112 private String m_redirectTarget; 113 114 /** The key under which this cache entry is stored in the variation map. */ 115 private String m_variationKey; 116 117 /** The variation map where this cache entry is stored. */ 118 private Map<String, I_CmsLruCacheObject> m_variationMap; 119 120 /** 121 * Constructor for class CmsFlexCacheEntry.<p> 122 * 123 * The way to use this class is to first use this empty constructor 124 * and later add data with the various add methods. 125 */ 126 public CmsFlexCacheEntry() { 127 128 m_elements = new ArrayList<Object>(INITIAL_CAPACITY_LISTS); 129 m_dateExpires = CmsResource.DATE_EXPIRED_DEFAULT; 130 m_dateLastModified = -1; 131 // base memory footprint of this object with all referenced objects 132 m_byteSize = 1024; 133 134 setNextLruObject(null); 135 setPreviousLruObject(null); 136 } 137 138 /** 139 * Adds an array of bytes to this cache entry, 140 * this will usually be the result of some kind of output - stream.<p> 141 * 142 * @param bytes the output to save in the cache 143 */ 144 public void add(byte[] bytes) { 145 146 if (m_completed) { 147 return; 148 } 149 if (m_redirectTarget == null) { 150 // Add only if not already redirected 151 m_elements.add(bytes); 152 m_byteSize += CmsMemoryMonitor.getMemorySize(bytes); 153 } 154 } 155 156 /** 157 * Add an include - call target resource to this cache entry.<p> 158 * 159 * @param resource a name of a resource in the OpenCms VFS 160 * @param parameters a map of parameters specific to this include call 161 * @param attrs a map of request attributes specific to this include call 162 */ 163 public void add(String resource, Map<String, String[]> parameters, Map<String, Object> attrs) { 164 165 if (m_completed) { 166 return; 167 } 168 if (m_redirectTarget == null) { 169 // Add only if not already redirected 170 m_elements.add(resource); 171 m_byteSize += CmsMemoryMonitor.getMemorySize(resource); 172 if (parameters == null) { 173 parameters = Collections.emptyMap(); 174 } 175 m_elements.add(parameters); 176 m_byteSize += CmsMemoryMonitor.getValueSize(parameters); 177 if (attrs == null) { 178 attrs = Collections.emptyMap(); 179 } 180 m_elements.add(attrs); 181 m_byteSize += CmsMemoryMonitor.getValueSize(attrs); 182 } 183 } 184 185 /** 186 * Add a map of headers to this cache entry, 187 * which are usually collected in the class CmsFlexResponse first.<p> 188 * 189 * @param headers the map of headers to add to the entry 190 */ 191 public void addHeaders(Map<String, List<String>> headers) { 192 193 if (m_completed) { 194 return; 195 } 196 m_headers = headers; 197 198 Iterator<String> allHeaders = m_headers.keySet().iterator(); 199 while (allHeaders.hasNext()) { 200 m_byteSize += CmsMemoryMonitor.getMemorySize(allHeaders.next()); 201 } 202 } 203 204 /** 205 * @see org.opencms.cache.I_CmsLruCacheObject#addToLruCache() 206 */ 207 public void addToLruCache() { 208 209 // do nothing here... 210 if (LOG.isDebugEnabled()) { 211 LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEENTRY_ADDED_ENTRY_1, this)); 212 } 213 } 214 215 /** 216 * Completes this cache entry.<p> 217 * 218 * A completed cache entry is made "unmodifiable", 219 * so that no further data can be added and existing data can not be changed.<p> 220 * 221 * This is to prevent the (unlikely) case that some user-written class 222 * tries to make changes to a cache entry.<p> 223 */ 224 public void complete() { 225 226 m_completed = true; 227 // Prevent changing of the cached lists 228 if (m_headers != null) { 229 m_headers = Collections.unmodifiableMap(m_headers); 230 } 231 if (m_elements != null) { 232 m_elements = Collections.unmodifiableList(m_elements); 233 } 234 if (LOG.isDebugEnabled()) { 235 LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEENTRY_ENTRY_COMPLETED_1, toString())); 236 } 237 } 238 239 /** 240 * Returns the list of data entries of this cache entry.<p> 241 * 242 * Data entries are byte arrays representing some kind of output 243 * or Strings representing include calls to other resources.<p> 244 * 245 * @return the list of data elements of this cache entry 246 */ 247 public List<Object> elements() { 248 249 return m_elements; 250 } 251 252 /** 253 * Returns the expiration date of this cache entry, 254 * this is set to the time when the entry becomes invalid.<p> 255 * 256 * @return the expiration date value for this resource 257 */ 258 public long getDateExpires() { 259 260 return m_dateExpires; 261 } 262 263 /** 264 * Returns the "last modified" date for this Flex cache entry.<p> 265 * 266 * @return the "last modified" date for this Flex cache entry 267 */ 268 public long getDateLastModified() { 269 270 return m_dateLastModified; 271 } 272 273 /** 274 * @see org.opencms.cache.I_CmsLruCacheObject#getLruCacheCosts() 275 */ 276 public int getLruCacheCosts() { 277 278 return m_byteSize; 279 } 280 281 /** 282 * @see org.opencms.monitor.I_CmsMemoryMonitorable#getMemorySize() 283 */ 284 public int getMemorySize() { 285 286 return getLruCacheCosts(); 287 } 288 289 /** 290 * @see org.opencms.cache.I_CmsLruCacheObject#getNextLruObject() 291 */ 292 public I_CmsLruCacheObject getNextLruObject() { 293 294 return m_next; 295 } 296 297 /** 298 * @see org.opencms.cache.I_CmsLruCacheObject#getPreviousLruObject() 299 */ 300 public I_CmsLruCacheObject getPreviousLruObject() { 301 302 return m_previous; 303 } 304 305 /** 306 * @see org.opencms.cache.I_CmsLruCacheObject#getValue() 307 */ 308 public Object getValue() { 309 310 return m_elements; 311 } 312 313 /** 314 * @see org.opencms.cache.I_CmsLruCacheObject#removeFromLruCache() 315 */ 316 public void removeFromLruCache() { 317 318 if ((m_variationMap != null) && (m_variationKey != null)) { 319 m_variationMap.remove(m_variationKey); 320 } 321 if (LOG.isDebugEnabled()) { 322 LOG.debug( 323 Messages.get().getBundle().key( 324 Messages.LOG_FLEXCACHEENTRY_REMOVED_ENTRY_FOR_VARIATION_1, 325 m_variationKey)); 326 } 327 } 328 329 /** 330 * Processing method for this cached entry.<p> 331 * 332 * If this method is called, it delivers the contents of 333 * the cached entry to the given request / response. 334 * This includes calls to all included resources.<p> 335 * 336 * @param req the request from the client 337 * @param res the server response 338 * 339 * @throws CmsFlexCacheException is thrown when problems writing to the response output-stream occur 340 * @throws ServletException might be thrown from call to RequestDispatcher.include() 341 * @throws IOException might be thrown from call to RequestDispatcher.include() or from Response.sendRedirect() 342 */ 343 public void service(CmsFlexRequest req, CmsFlexResponse res) 344 throws CmsFlexCacheException, ServletException, IOException { 345 346 if (!m_completed) { 347 return; 348 } 349 350 if (m_redirectTarget != null) { 351 res.setOnlyBuffering(false); 352 res.setCmsCachingRequired(false); 353 // redirect the response, no further output required 354 res.sendRedirect(m_redirectTarget, m_redirectPermanent); 355 } else { 356 // process cached headers first 357 CmsFlexResponse.processHeaders(m_headers, res); 358 // check if this cache entry is a "leaf" (i.e. no further includes) 359 boolean hasNoSubElements = (m_elements.size() == 1); 360 // write output to stream and process all included elements 361 for (int i = 0; i < m_elements.size(); i++) { 362 Object o = m_elements.get(i); 363 if (o instanceof String) { 364 // handle cached parameters 365 i++; 366 Map<String, String[]> paramMap = CmsCollectionsGenericWrapper.map(m_elements.get(i)); 367 Map<String, String[]> oldParamMap = null; 368 if (paramMap.size() > 0) { 369 oldParamMap = req.getParameterMap(); 370 req.addParameterMap(paramMap); 371 } 372 // handle cached attributes 373 i++; 374 Map<String, Object> attrMap = CmsCollectionsGenericWrapper.map(m_elements.get(i)); 375 Map<String, Object> oldAttrMap = null; 376 if (attrMap.size() > 0) { 377 oldAttrMap = req.getAttributeMap(); 378 // to avoid issues with multi threading, try to clone the attribute instances 379 req.addAttributeMap(cloneAttributes(attrMap)); 380 //req.addAttributeMap(attrMap); 381 } 382 // do the include call 383 req.getRequestDispatcher((String)o).include(req, res); 384 // reset parameters if necessary 385 if (oldParamMap != null) { 386 req.setParameterMap(oldParamMap); 387 } 388 // reset attributes if necessary 389 if (oldAttrMap != null) { 390 req.setAttributeMap(oldAttrMap); 391 } 392 } else { 393 try { 394 res.writeToOutputStream((byte[])o, hasNoSubElements); 395 } catch (IOException e) { 396 CmsMessageContainer message = Messages.get().container( 397 Messages.LOG_FLEXCACHEKEY_NOT_FOUND_1, 398 getClass().getName()); 399 if (LOG.isDebugEnabled()) { 400 LOG.debug(message.key()); 401 } 402 403 throw new CmsFlexCacheException(message, e); 404 } 405 } 406 } 407 } 408 } 409 410 /** 411 * Sets the expiration date of this Flex cache entry exactly to the 412 * given time.<p> 413 * 414 * @param dateExpires the time to expire this cache entry 415 */ 416 public void setDateExpires(long dateExpires) { 417 418 m_dateExpires = dateExpires; 419 if (LOG.isDebugEnabled()) { 420 long now = System.currentTimeMillis(); 421 LOG.debug( 422 Messages.get().getBundle().key( 423 Messages.LOG_FLEXCACHEENTRY_SET_EXPIRATION_DATE_3, 424 new Long(m_dateExpires), 425 new Long(now), 426 new Long(m_dateExpires - now))); 427 } 428 } 429 430 /** 431 * Sets an expiration date for this cache entry to the next timeout, 432 * which indicates the time this entry becomes invalid.<p> 433 * 434 * The timeout parameter represents the minute - interval in which the cache entry 435 * is to be cleared. 436 * The interval always starts at 0.00h. 437 * A value of 60 would indicate that this entry will reach it's expiration date at the beginning of the next 438 * full hour, a timeout of 20 would indicate that the entry is invalidated at x.00, x.20 and x.40 of every hour etc.<p> 439 * 440 * @param timeout the timeout value to be set 441 */ 442 public void setDateExpiresToNextTimeout(long timeout) { 443 444 if ((timeout < 0) || !m_completed) { 445 return; 446 } 447 448 long now = System.currentTimeMillis(); 449 long daytime = now % 86400000; 450 long timeoutMinutes = timeout * 60000; 451 setDateExpires((now - (daytime % timeoutMinutes)) + timeoutMinutes); 452 } 453 454 /** 455 * Sets the "last modified" date for this Flex cache entry with the given value.<p> 456 * 457 * @param dateLastModified the value to set for the "last modified" date 458 */ 459 public void setDateLastModified(long dateLastModified) { 460 461 m_dateLastModified = dateLastModified; 462 } 463 464 /** 465 * Sets the "last modified" date for this Flex cache entry by using the last passed timeout value.<p> 466 * 467 * If a cache entry uses the timeout feature, it becomes invalid every time the timeout interval 468 * passes. Thus the "last modified" date is the time the last timeout passed.<p> 469 * 470 * @param timeout the timeout value to use to calculate the date last modified 471 */ 472 public void setDateLastModifiedToPreviousTimeout(long timeout) { 473 474 long now = System.currentTimeMillis(); 475 long daytime = now % 86400000; 476 long timeoutMinutes = timeout * 60000; 477 setDateLastModified(now - (daytime % timeoutMinutes)); 478 } 479 480 /** 481 * @see org.opencms.cache.I_CmsLruCacheObject#setNextLruObject(org.opencms.cache.I_CmsLruCacheObject) 482 */ 483 public void setNextLruObject(I_CmsLruCacheObject theNextEntry) { 484 485 m_next = theNextEntry; 486 } 487 488 /** 489 * @see org.opencms.cache.I_CmsLruCacheObject#setPreviousLruObject(org.opencms.cache.I_CmsLruCacheObject) 490 */ 491 public void setPreviousLruObject(I_CmsLruCacheObject thePreviousEntry) { 492 493 m_previous = thePreviousEntry; 494 } 495 496 /** 497 * Set a redirect target for this cache entry.<p> 498 * 499 * <b>Important:</b> 500 * When a redirect target is set, all saved data is thrown away, 501 * and new data will not be saved in the cache entry. 502 * This is so since with a redirect nothing will be displayed 503 * in the browser anyway, so there is no point in saving the data.<p> 504 * 505 * @param target The redirect target (must be a valid URL). 506 * @param permanent true if this is a permanent redirect 507 */ 508 public void setRedirect(String target, boolean permanent) { 509 510 if (m_completed || (target == null)) { 511 return; 512 } 513 m_redirectTarget = target; 514 m_redirectPermanent = permanent; 515 m_byteSize = 512 + CmsMemoryMonitor.getMemorySize(target); 516 // If we have a redirect we don't need any other output or headers 517 m_elements = null; 518 m_headers = null; 519 } 520 521 /** 522 * Stores a backward reference to the map and key where this cache entry is stored.<p> 523 * 524 * This is required for the FlexCache.<p> 525 * 526 * @param theVariationKey the variation key 527 * @param theVariationMap the variation map 528 */ 529 public void setVariationData(String theVariationKey, Map<String, I_CmsLruCacheObject> theVariationMap) { 530 531 m_variationKey = theVariationKey; 532 m_variationMap = theVariationMap; 533 } 534 535 /** 536 * @see java.lang.Object#toString() 537 * 538 * @return a basic String representation of this CmsFlexCache entry 539 */ 540 @Override 541 public String toString() { 542 543 String str = null; 544 if (m_redirectTarget == null) { 545 str = "CmsFlexCacheEntry [" + m_elements.size() + " Elements/" + getLruCacheCosts() + " bytes]\n"; 546 Iterator<Object> i = m_elements.iterator(); 547 int count = 0; 548 while (i.hasNext()) { 549 count++; 550 Object o = i.next(); 551 if (o instanceof String) { 552 str += "" + count + " - <cms:include target=" + o + ">\n"; 553 } else if (o instanceof byte[]) { 554 str += "" + count + " - <![CDATA[" + new String((byte[])o) + "]]>\n"; 555 } else { 556 str += "<!--[" + o.toString() + "]-->"; 557 } 558 } 559 } else { 560 str = "CmsFlexCacheEntry [Redirect to target=" + m_redirectTarget + "]"; 561 } 562 return str; 563 } 564 565 /** 566 * Clones the attribute instances if possible.<p> 567 * 568 * @param attrs the attributes 569 * 570 * @return a new map instance with the cloned attributes 571 */ 572 private Map<String, Object> cloneAttributes(Map<String, Object> attrs) { 573 574 Map<String, Object> result = new HashMap<String, Object>(); 575 for (Entry<String, Object> entry : attrs.entrySet()) { 576 if (entry.getValue() instanceof CmsJspStandardContextBean) { 577 result.put(entry.getKey(), ((CmsJspStandardContextBean)entry.getValue()).createCopy()); 578 } else if (entry.getValue() instanceof Cloneable) { 579 Object clone = null; 580 try { 581 clone = ObjectUtils.clone(entry.getValue()); 582 } catch (Exception e) { 583 LOG.info(e.getMessage(), e); 584 } 585 586 result.put(entry.getKey(), clone != null ? clone : entry.getValue()); 587 } else { 588 result.put(entry.getKey(), entry.getValue()); 589 } 590 591 } 592 593 return result; 594 } 595}