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 * @see org.opencms.cache.I_CmsLruCacheObject#removeFromLruCache() 329 */ 330 public void removeFromLruCache() { 331 332 if ((m_variationMap != null) && (m_variationKey != null)) { 333 m_variationMap.remove(m_variationKey); 334 } 335 if (LOG.isDebugEnabled()) { 336 LOG.debug( 337 Messages.get().getBundle().key( 338 Messages.LOG_FLEXCACHEENTRY_REMOVED_ENTRY_FOR_VARIATION_1, 339 m_variationKey)); 340 } 341 } 342 343 /** 344 * Processing method for this cached entry.<p> 345 * 346 * If this method is called, it delivers the contents of 347 * the cached entry to the given request / response. 348 * This includes calls to all included resources.<p> 349 * 350 * @param req the request from the client 351 * @param res the server response 352 * 353 * @throws CmsFlexCacheException is thrown when problems writing to the response output-stream occur 354 * @throws ServletException might be thrown from call to RequestDispatcher.include() 355 * @throws IOException might be thrown from call to RequestDispatcher.include() or from Response.sendRedirect() 356 */ 357 public void service(CmsFlexRequest req, CmsFlexResponse res) 358 throws CmsFlexCacheException, ServletException, IOException { 359 360 if (!m_completed) { 361 return; 362 } 363 364 if (m_redirectTarget != null) { 365 res.setOnlyBuffering(false); 366 res.setCmsCachingRequired(false); 367 // redirect the response, no further output required 368 res.sendRedirect(m_redirectTarget, m_redirectPermanent); 369 } else { 370 // process cached headers first 371 CmsFlexResponse.processHeaders(m_headers, res); 372 // check if this cache entry is a "leaf" (i.e. no further includes) 373 boolean hasNoSubElements = (m_elements.size() == 1); 374 // write output to stream and process all included elements 375 for (int i = 0; i < m_elements.size(); i++) { 376 Object o = m_elements.get(i); 377 if (o instanceof String) { 378 // handle cached parameters 379 i++; 380 Map<String, String[]> paramMap = CmsCollectionsGenericWrapper.map(m_elements.get(i)); 381 Map<String, String[]> oldParamMap = null; 382 if (paramMap.size() > 0) { 383 oldParamMap = req.getParameterMap(); 384 req.addParameterMap(paramMap); 385 } 386 // handle cached attributes 387 i++; 388 Map<String, Object> attrMap = CmsCollectionsGenericWrapper.map(m_elements.get(i)); 389 Map<String, Object> oldAttrMap = null; 390 if (attrMap.size() > 0) { 391 oldAttrMap = req.getAttributeMap(); 392 // to avoid issues with multi threading, try to clone the attribute instances 393 req.addAttributeMap(cloneAttributes(attrMap)); 394 //req.addAttributeMap(attrMap); 395 } 396 // do the include call 397 req.getRequestDispatcher((String)o).include(req, res); 398 // reset parameters if necessary 399 if (oldParamMap != null) { 400 req.setParameterMap(oldParamMap); 401 } 402 // reset attributes if necessary 403 if (oldAttrMap != null) { 404 req.setAttributeMap(oldAttrMap); 405 } 406 } else { 407 try { 408 res.writeToOutputStream((byte[])o, hasNoSubElements); 409 } catch (IOException e) { 410 CmsMessageContainer message = Messages.get().container( 411 Messages.LOG_FLEXCACHEKEY_NOT_FOUND_1, 412 getClass().getName()); 413 if (LOG.isDebugEnabled()) { 414 LOG.debug(message.key()); 415 } 416 417 throw new CmsFlexCacheException(message, e); 418 } 419 } 420 } 421 } 422 } 423 424 /** 425 * Sets the bucket set for this flex cache entry.<p> 426 * 427 * @param bucketSet the bucket set to set 428 */ 429 public void setBucketSet(BucketSet bucketSet) { 430 431 m_bucketSet = bucketSet; 432 } 433 434 /** 435 * Sets the expiration date of this Flex cache entry exactly to the 436 * given time.<p> 437 * 438 * @param dateExpires the time to expire this cache entry 439 */ 440 public void setDateExpires(long dateExpires) { 441 442 m_dateExpires = dateExpires; 443 if (LOG.isDebugEnabled()) { 444 long now = System.currentTimeMillis(); 445 LOG.debug( 446 Messages.get().getBundle().key( 447 Messages.LOG_FLEXCACHEENTRY_SET_EXPIRATION_DATE_3, 448 new Long(m_dateExpires), 449 new Long(now), 450 new Long(m_dateExpires - now))); 451 } 452 } 453 454 /** 455 * Sets an expiration date for this cache entry to the next timeout, 456 * which indicates the time this entry becomes invalid.<p> 457 * 458 * The timeout parameter represents the minute - interval in which the cache entry 459 * is to be cleared. 460 * The interval always starts at 0.00h. 461 * A value of 60 would indicate that this entry will reach it's expiration date at the beginning of the next 462 * 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> 463 * 464 * @param timeout the timeout value to be set 465 */ 466 public void setDateExpiresToNextTimeout(long timeout) { 467 468 if ((timeout < 0) || !m_completed) { 469 return; 470 } 471 472 long now = System.currentTimeMillis(); 473 long daytime = now % 86400000; 474 long timeoutMinutes = timeout * 60000; 475 setDateExpires((now - (daytime % timeoutMinutes)) + timeoutMinutes); 476 } 477 478 /** 479 * Sets the "last modified" date for this Flex cache entry with the given value.<p> 480 * 481 * @param dateLastModified the value to set for the "last modified" date 482 */ 483 public void setDateLastModified(long dateLastModified) { 484 485 m_dateLastModified = dateLastModified; 486 } 487 488 /** 489 * Sets the "last modified" date for this Flex cache entry by using the last passed timeout value.<p> 490 * 491 * If a cache entry uses the timeout feature, it becomes invalid every time the timeout interval 492 * passes. Thus the "last modified" date is the time the last timeout passed.<p> 493 * 494 * @param timeout the timeout value to use to calculate the date last modified 495 */ 496 public void setDateLastModifiedToPreviousTimeout(long timeout) { 497 498 long now = System.currentTimeMillis(); 499 long daytime = now % 86400000; 500 long timeoutMinutes = timeout * 60000; 501 setDateLastModified(now - (daytime % timeoutMinutes)); 502 } 503 504 /** 505 * @see org.opencms.cache.I_CmsLruCacheObject#setNextLruObject(org.opencms.cache.I_CmsLruCacheObject) 506 */ 507 public void setNextLruObject(I_CmsLruCacheObject theNextEntry) { 508 509 m_next = theNextEntry; 510 } 511 512 /** 513 * @see org.opencms.cache.I_CmsLruCacheObject#setPreviousLruObject(org.opencms.cache.I_CmsLruCacheObject) 514 */ 515 public void setPreviousLruObject(I_CmsLruCacheObject thePreviousEntry) { 516 517 m_previous = thePreviousEntry; 518 } 519 520 /** 521 * Set a redirect target for this cache entry.<p> 522 * 523 * <b>Important:</b> 524 * When a redirect target is set, all saved data is thrown away, 525 * and new data will not be saved in the cache entry. 526 * This is so since with a redirect nothing will be displayed 527 * in the browser anyway, so there is no point in saving the data.<p> 528 * 529 * @param target The redirect target (must be a valid URL). 530 * @param permanent true if this is a permanent redirect 531 */ 532 public void setRedirect(String target, boolean permanent) { 533 534 if (m_completed || (target == null)) { 535 return; 536 } 537 m_redirectTarget = target; 538 m_redirectPermanent = permanent; 539 m_byteSize = 512 + CmsMemoryMonitor.getMemorySize(target); 540 // If we have a redirect we don't need any other output or headers 541 m_elements = null; 542 m_headers = null; 543 } 544 545 /** 546 * Stores a backward reference to the map and key where this cache entry is stored.<p> 547 * 548 * This is required for the FlexCache.<p> 549 * 550 * @param theVariationKey the variation key 551 * @param theVariationMap the variation map 552 */ 553 public void setVariationData(String theVariationKey, Map<String, I_CmsLruCacheObject> theVariationMap) { 554 555 m_variationKey = theVariationKey; 556 m_variationMap = theVariationMap; 557 } 558 559 /** 560 * @see java.lang.Object#toString() 561 * 562 * @return a basic String representation of this CmsFlexCache entry 563 */ 564 @Override 565 public String toString() { 566 567 String str = null; 568 if (m_redirectTarget == null) { 569 str = "CmsFlexCacheEntry [" + m_elements.size() + " Elements/" + getLruCacheCosts() + " bytes]\n"; 570 Iterator<Object> i = m_elements.iterator(); 571 int count = 0; 572 while (i.hasNext()) { 573 count++; 574 Object o = i.next(); 575 if (o instanceof String) { 576 str += "" + count + " - <cms:include target=" + o + ">\n"; 577 } else if (o instanceof byte[]) { 578 str += "" + count + " - <![CDATA[" + new String((byte[])o) + "]]>\n"; 579 } else { 580 str += "<!--[" + o.toString() + "]-->"; 581 } 582 } 583 } else { 584 str = "CmsFlexCacheEntry [Redirect to target=" + m_redirectTarget + "]"; 585 } 586 return str; 587 } 588 589 /** 590 * Clones the attribute instances if possible.<p> 591 * 592 * @param attrs the attributes 593 * 594 * @return a new map instance with the cloned attributes 595 */ 596 private Map<String, Object> cloneAttributes(Map<String, Object> attrs) { 597 598 Map<String, Object> result = new HashMap<String, Object>(); 599 for (Entry<String, Object> entry : attrs.entrySet()) { 600 if (entry.getValue() instanceof CmsJspStandardContextBean) { 601 result.put(entry.getKey(), ((CmsJspStandardContextBean)entry.getValue()).createCopy()); 602 } else if (entry.getValue() instanceof Cloneable) { 603 Object clone = null; 604 try { 605 clone = ObjectUtils.clone(entry.getValue()); 606 } catch (Exception e) { 607 LOG.info(e.getMessage(), e); 608 } 609 610 result.put(entry.getKey(), clone != null ? clone : entry.getValue()); 611 } else { 612 result.put(entry.getKey(), entry.getValue()); 613 } 614 615 } 616 617 return result; 618 } 619 620}