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