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