001/* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019package org.apache.wiki.providers; 020 021import net.sf.ehcache.Cache; 022import net.sf.ehcache.CacheManager; 023import net.sf.ehcache.Element; 024import org.apache.log4j.Logger; 025import org.apache.wiki.PageManager; 026import org.apache.wiki.WikiContext; 027import org.apache.wiki.WikiEngine; 028import org.apache.wiki.WikiPage; 029import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 030import org.apache.wiki.api.exceptions.ProviderException; 031import org.apache.wiki.parser.MarkupParser; 032import org.apache.wiki.render.RenderingManager; 033import org.apache.wiki.search.QueryItem; 034import org.apache.wiki.util.ClassUtil; 035import org.apache.wiki.util.TextUtil; 036 037import java.io.IOException; 038import java.util.Collection; 039import java.util.Date; 040import java.util.Iterator; 041import java.util.List; 042import java.util.Properties; 043import java.util.TreeSet; 044 045 046/** 047 * Provides a caching page provider. This class rests on top of a 048 * real provider class and provides a cache to speed things up. Only 049 * if the cache copy of the page text has expired, we fetch it from 050 * the provider. 051 * <p> 052 * This class does not detect if someone has modified the page 053 * externally, not through JSPWiki routines. 054 * <p> 055 * Heavily based on ideas by Chris Brooking. 056 * <p> 057 * Since 2.10 uses the Ehcache library. 058 * 059 * @since 1.6.4 060 */ 061// FIXME: Synchronization is a bit inconsistent in places. 062// FIXME: A part of the stuff is now redundant, since we could easily use the text cache 063// for a lot of things. RefactorMe. 064 065public class CachingProvider implements WikiPageProvider { 066 067 private static final Logger log = Logger.getLogger( CachingProvider.class ); 068 069 private CacheManager m_cacheManager = CacheManager.getInstance(); 070 071 private WikiPageProvider m_provider; 072 // FIXME: Find another way to the search engine to use instead of from WikiEngine? 073 private WikiEngine m_engine; 074 075 private Cache m_cache; 076 /** Name of the regular page cache. */ 077 public static final String CACHE_NAME = "jspwiki.pageCache"; 078 079 private Cache m_textCache; 080 /** Name of the page text cache. */ 081 public static final String TEXTCACHE_NAME = "jspwiki.pageTextCache"; 082 083 private Cache m_historyCache; 084 /** Name of the page history cache. */ 085 public static final String HISTORYCACHE_NAME = "jspwiki.pageHistoryCache"; 086 087 private long m_cacheMisses = 0; 088 private long m_cacheHits = 0; 089 090 private long m_historyCacheMisses = 0; 091 private long m_historyCacheHits = 0; 092 093 // FIXME: This MUST be cached somehow. 094 095 private boolean m_gotall = false; 096 097 // The default settings of the caches, if you want something else, provide an "ehcache.xml" file 098 // Please note that JSPWiki ships with a default "ehcache.xml" in the classpath 099 public static final int DEFAULT_CACHECAPACITY = 1000; // Good most wikis 100 public static final int DEFAULT_CACHETIMETOLIVESECONDS = 24*3600; 101 public static final int DEFAULT_CACHETIMETOIDLESECONDS = 24*3600; 102 103 /** 104 * {@inheritDoc} 105 */ 106 public void initialize( WikiEngine engine, Properties properties ) 107 throws NoRequiredPropertyException, IOException { 108 log.debug("Initing CachingProvider"); 109 110 // engine is used for getting the search engine 111 m_engine = engine; 112 113 String cacheName = engine.getApplicationName() + "." + CACHE_NAME; 114 if (m_cacheManager.cacheExists(cacheName)) { 115 m_cache = m_cacheManager.getCache(cacheName); 116 } else { 117 log.info("cache with name " + cacheName + " not found in ehcache.xml, creating it with defaults."); 118 m_cache = new Cache(cacheName, DEFAULT_CACHECAPACITY, false, false, DEFAULT_CACHETIMETOLIVESECONDS, DEFAULT_CACHETIMETOIDLESECONDS); 119 m_cacheManager.addCache(m_cache); 120 } 121 122 String textCacheName = engine.getApplicationName() + "." + TEXTCACHE_NAME; 123 if (m_cacheManager.cacheExists(textCacheName)) { 124 m_textCache= m_cacheManager.getCache(textCacheName); 125 } else { 126 log.info("cache with name " + textCacheName + " not found in ehcache.xml, creating it with defaults."); 127 m_textCache = new Cache(textCacheName, DEFAULT_CACHECAPACITY, false, false, DEFAULT_CACHETIMETOLIVESECONDS, DEFAULT_CACHETIMETOIDLESECONDS); 128 m_cacheManager.addCache(m_textCache); 129 } 130 131 String historyCacheName = engine.getApplicationName() + "." + HISTORYCACHE_NAME; 132 if (m_cacheManager.cacheExists(historyCacheName)) { 133 m_historyCache= m_cacheManager.getCache(historyCacheName); 134 } else { 135 log.info("cache with name " + historyCacheName + " not found in ehcache.xml, creating it with defaults."); 136 m_historyCache = new Cache(historyCacheName, DEFAULT_CACHECAPACITY, false, false, DEFAULT_CACHETIMETOLIVESECONDS, DEFAULT_CACHETIMETOIDLESECONDS); 137 m_cacheManager.addCache(m_historyCache); 138 } 139 140 // 141 // m_cache.getCacheEventNotificationService().registerListener(new CacheItemCollector()); 142 143 // 144 // Find and initialize real provider. 145 // 146 String classname = TextUtil.getRequiredProperty( properties, PageManager.PROP_PAGEPROVIDER ); 147 148 149 try 150 { 151 Class< ? > providerclass = ClassUtil.findClass( "org.apache.wiki.providers", classname); 152 153 m_provider = (WikiPageProvider)providerclass.newInstance(); 154 155 log.debug("Initializing real provider class "+m_provider); 156 m_provider.initialize( engine, properties ); 157 } 158 catch( ClassNotFoundException e ) 159 { 160 log.error("Unable to locate provider class "+classname,e); 161 throw new IllegalArgumentException("no provider class", e); 162 } 163 catch( InstantiationException e ) 164 { 165 log.error("Unable to create provider class "+classname,e); 166 throw new IllegalArgumentException("faulty provider class", e); 167 } 168 catch( IllegalAccessException e ) 169 { 170 log.error("Illegal access to provider class "+classname,e); 171 throw new IllegalArgumentException("illegal provider class", e); 172 } 173 } 174 175 176 private WikiPage getPageInfoFromCache(String name) throws ProviderException { 177 // Sanity check; seems to occur sometimes 178 if (name == null) return null; 179 180 Element cacheElement = m_cache.get(name); 181 if (cacheElement == null) { 182 WikiPage refreshed = m_provider.getPageInfo(name, WikiPageProvider.LATEST_VERSION); 183 if (refreshed != null) { 184 m_cache.put(new Element(name, refreshed)); 185 return refreshed; 186 } else { 187 // page does not exist anywhere 188 return null; 189 } 190 } 191 return (WikiPage) cacheElement.getObjectValue(); 192 } 193 194 195 /** 196 * {@inheritDoc} 197 */ 198 public boolean pageExists( String pageName, int version ) 199 { 200 if( pageName == null ) return false; 201 202 WikiPage p = null; 203 204 try 205 { 206 p = getPageInfoFromCache( pageName ); 207 } 208 catch( ProviderException e ) 209 { 210 log.info("Provider failed while trying to check if page exists: "+pageName); 211 return false; 212 } 213 214 if( p != null ) 215 { 216 int latestVersion = p.getVersion(); 217 218 if( version == latestVersion || version == LATEST_VERSION ) 219 { 220 return true; 221 } 222 223 return m_provider.pageExists( pageName, version ); 224 } 225 226 try 227 { 228 return getPageInfo( pageName, version ) != null; 229 } 230 catch( ProviderException e ) 231 {} 232 233 return false; 234 } 235 236 /** 237 * {@inheritDoc} 238 */ 239 public boolean pageExists( String pageName ) 240 { 241 if( pageName == null ) return false; 242 243 WikiPage p = null; 244 245 try 246 { 247 p = getPageInfoFromCache( pageName ); 248 } 249 catch( ProviderException e ) 250 { 251 log.info("Provider failed while trying to check if page exists: "+pageName); 252 return false; 253 } 254 255 // 256 // A null item means that the page either does not 257 // exist, or has not yet been cached; a non-null 258 // means that the page does exist. 259 // 260 if( p != null ) 261 { 262 return true; 263 } 264 265 // 266 // If we have a list of all pages in memory, then any page 267 // not in the cache must be non-existent. 268 // 269 if( m_gotall ) 270 { 271 return false; 272 } 273 274 // 275 // We could add the page to the cache here as well, 276 // but in order to understand whether that is a 277 // good thing or not we would need to analyze 278 // the JSPWiki calling patterns extensively. Presumably 279 // it would be a good thing if pageExists() is called 280 // many times before the first getPageText() is called, 281 // and the whole page is cached. 282 // 283 return m_provider.pageExists( pageName ); 284 } 285 286 /** 287 * {@inheritDoc} 288 */ 289 public String getPageText( String pageName, int version ) 290 throws ProviderException 291 { 292 String result = null; 293 294 if( pageName == null ) return null; 295 296 if( version == WikiPageProvider.LATEST_VERSION ) 297 { 298 result = getTextFromCache( pageName ); 299 } 300 else 301 { 302 WikiPage p = getPageInfoFromCache( pageName ); 303 304 // 305 // Or is this the latest version fetched by version number? 306 // 307 if( p != null && p.getVersion() == version ) 308 { 309 result = getTextFromCache( pageName ); 310 } 311 else 312 { 313 result = m_provider.getPageText( pageName, version ); 314 } 315 } 316 317 return result; 318 } 319 320 321 private String getTextFromCache(String pageName) throws ProviderException { 322 String text = null; 323 324 if (pageName == null) return null; 325 326 Element cacheElement = m_textCache.get(pageName); 327 328 if (cacheElement != null) { 329 m_cacheHits++; 330 return (String) cacheElement.getObjectValue(); 331 } 332 if (pageExists(pageName)) { 333 text = m_provider.getPageText(pageName, WikiPageProvider.LATEST_VERSION); 334 m_textCache.put(new Element(pageName, text)); 335 m_cacheMisses++; 336 return text; 337 } 338 //page not found (not in cache, not by real provider) 339 return null; 340 } 341 342 /** 343 * {@inheritDoc} 344 */ 345 public void putPageText(WikiPage page, String text) throws ProviderException { 346 synchronized (this) { 347 m_provider.putPageText(page, text); 348 349 page.setLastModified(new Date()); 350 351 // Refresh caches properly 352 353 m_cache.remove(page.getName()); 354 m_textCache.remove(page.getName()); 355 m_historyCache.remove(page.getName()); 356 357 getPageInfoFromCache(page.getName()); 358 } 359 } 360 361 /** 362 * {@inheritDoc} 363 */ 364 public Collection getAllPages() throws ProviderException { 365 Collection all; 366 367 if (m_gotall == false) { 368 all = m_provider.getAllPages(); 369 370 // Make sure that all pages are in the cache. 371 372 synchronized (this) { 373 for (Iterator i = all.iterator(); i.hasNext(); ) { 374 WikiPage p = (WikiPage) i.next(); 375 376 m_cache.put(new Element(p.getName(), p)); 377 } 378 379 m_gotall = true; 380 } 381 } else { 382 List<String> keys = m_cache.getKeysWithExpiryCheck(); 383 all = new TreeSet<WikiPage>(); 384 for (String key : keys) { 385 Element element = m_cache.get(key); 386 Object cachedPage = element.getObjectValue(); 387 if (cachedPage != null) { 388 all.add((WikiPage) cachedPage); 389 } 390 } 391 } 392 393 if( all.size() >= m_cache.getCacheConfiguration().getMaxEntriesLocalHeap() ) { 394 log.warn( "seems " + m_cache.getName() + " can't hold all pages from your page repository, " + 395 "so we're delegating on the underlying provider instead. Please consider increasing " + 396 "your cache sizes on ehcache.xml to avoid this behaviour" ); 397 return m_provider.getAllPages(); 398 } 399 400 return all; 401 } 402 403 /** 404 * {@inheritDoc} 405 */ 406 public Collection getAllChangedSince( Date date ) 407 { 408 return m_provider.getAllChangedSince( date ); 409 } 410 411 /** 412 * {@inheritDoc} 413 */ 414 public int getPageCount() 415 throws ProviderException 416 { 417 return m_provider.getPageCount(); 418 } 419 420 /** 421 * {@inheritDoc} 422 */ 423 public Collection findPages( QueryItem[] query ) 424 { 425 // 426 // If the provider is a fast searcher, then 427 // just pass this request through. 428 // 429 return m_provider.findPages( query ); 430 431 // FIXME: Does not implement fast searching 432 } 433 434 // 435 // FIXME: Kludge: make sure that the page is also parsed and it gets all the 436 // necessary variables. 437 // 438 439 private void refreshMetadata( WikiPage page ) 440 { 441 if( page != null && !page.hasMetadata() ) 442 { 443 RenderingManager mgr = m_engine.getRenderingManager(); 444 445 try 446 { 447 String data = m_provider.getPageText(page.getName(), page.getVersion()); 448 449 WikiContext ctx = new WikiContext( m_engine, page ); 450 MarkupParser parser = mgr.getParser( ctx, data ); 451 452 parser.parse(); 453 } 454 catch( Exception ex ) 455 { 456 log.debug("Failed to retrieve variables for wikipage "+page); 457 } 458 } 459 } 460 461 /** 462 * {@inheritDoc} 463 */ 464 public WikiPage getPageInfo( String pageName, int version ) throws ProviderException 465 { 466 WikiPage page = null; 467 WikiPage cached = getPageInfoFromCache( pageName ); 468 469 int latestcached = (cached != null) ? cached.getVersion() : Integer.MIN_VALUE; 470 471 if( version == WikiPageProvider.LATEST_VERSION || version == latestcached ) 472 { 473 if( cached == null ) 474 { 475 WikiPage data = m_provider.getPageInfo( pageName, version ); 476 477 if( data != null ) 478 { 479 m_cache.put(new Element(pageName, data)); 480 } 481 page = data; 482 } 483 else 484 { 485 page = cached; 486 } 487 } 488 else 489 { 490 // We do not cache old versions. 491 page = m_provider.getPageInfo( pageName, version ); 492 //refreshMetadata( page ); 493 } 494 495 refreshMetadata( page ); 496 497 return page; 498 } 499 500 /** 501 * {@inheritDoc} 502 */ 503 public List getVersionHistory(String pageName) throws ProviderException { 504 List history = null; 505 506 if (pageName == null) return null; 507 Element element = m_historyCache.get(pageName); 508 509 if (element != null) { 510 m_historyCacheHits++; 511 history = (List) element.getObjectValue(); 512 } else { 513 history = m_provider.getVersionHistory(pageName); 514 m_historyCache.put( new Element( pageName, history )); 515 m_historyCacheMisses++; 516 } 517 518 return history; 519 } 520 521 /** 522 * Gets the provider class name, and cache statistics (misscount and hitcount of page cache and history cache). 523 * 524 * @return A plain string with all the above mentioned values. 525 */ 526 public synchronized String getProviderInfo() 527 { 528 return "Real provider: "+m_provider.getClass().getName()+ 529 ". Cache misses: "+m_cacheMisses+ 530 ". Cache hits: "+m_cacheHits+ 531 ". History cache hits: "+m_historyCacheHits+ 532 ". History cache misses: "+m_historyCacheMisses; 533 } 534 535 /** 536 * {@inheritDoc} 537 */ 538 public void deleteVersion( String pageName, int version ) 539 throws ProviderException 540 { 541 // 542 // Luckily, this is such a rare operation it is okay 543 // to synchronize against the whole thing. 544 // 545 synchronized( this ) 546 { 547 WikiPage cached = getPageInfoFromCache( pageName ); 548 549 int latestcached = (cached != null) ? cached.getVersion() : Integer.MIN_VALUE; 550 551 // 552 // If we have this version cached, remove from cache. 553 // 554 if( version == WikiPageProvider.LATEST_VERSION || 555 version == latestcached ) 556 { 557 m_cache.remove(pageName); 558 m_textCache.remove(pageName); 559 } 560 561 m_provider.deleteVersion( pageName, version ); 562 m_historyCache.remove(pageName); 563 } 564 } 565 566 /** 567 * {@inheritDoc} 568 */ 569 public void deletePage( String pageName ) 570 throws ProviderException 571 { 572 // 573 // See note in deleteVersion(). 574 // 575 synchronized(this) 576 { 577 m_cache.put(new Element(pageName, null)); 578 m_textCache.put(new Element( pageName, null )); 579 m_historyCache.put(new Element(pageName, null)); 580 m_provider.deletePage(pageName); 581 } 582 } 583 584 /** 585 * {@inheritDoc} 586 */ 587 public void movePage(String from, String to) throws ProviderException { 588 m_provider.movePage(from, to); 589 590 synchronized (this) { 591 // Clear any cached version of the old page and new page 592 m_cache.remove(from); 593 m_textCache.remove(from); 594 m_historyCache.remove(from); 595 log.debug("Removing to page " + to + " from cache"); 596 m_cache.remove(to); 597 m_textCache.remove(to); 598 m_historyCache.remove(to); 599 } 600 } 601 602 /** 603 * Returns the actual used provider. 604 * @since 2.0 605 * @return The real provider. 606 */ 607 public WikiPageProvider getRealProvider() 608 { 609 return m_provider; 610 } 611 612}