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 WikiPage page = getPageInfoFromCache(pageName); 327 328 Element cacheElement = m_textCache.get(pageName); 329 330 if (cacheElement != null) { 331 m_cacheHits++; 332 return (String) cacheElement.getObjectValue(); 333 } 334 if (pageExists(pageName)) { 335 text = m_provider.getPageText(pageName, WikiPageProvider.LATEST_VERSION); 336 m_textCache.put(new Element(pageName, text)); 337 m_cacheMisses++; 338 return text; 339 } 340 //page not found (not in cache, not by real provider) 341 return null; 342 } 343 344 /** 345 * {@inheritDoc} 346 */ 347 public void putPageText(WikiPage page, String text) throws ProviderException { 348 synchronized (this) { 349 m_provider.putPageText(page, text); 350 351 page.setLastModified(new Date()); 352 353 // Refresh caches properly 354 355 m_cache.remove(page.getName()); 356 m_textCache.remove(page.getName()); 357 m_historyCache.remove(page.getName()); 358 359 getPageInfoFromCache(page.getName()); 360 } 361 } 362 363 /** 364 * {@inheritDoc} 365 */ 366 public Collection getAllPages() throws ProviderException { 367 Collection all; 368 369 if (m_gotall == false) { 370 all = m_provider.getAllPages(); 371 372 // Make sure that all pages are in the cache. 373 374 synchronized (this) { 375 for (Iterator i = all.iterator(); i.hasNext(); ) { 376 WikiPage p = (WikiPage) i.next(); 377 378 m_cache.put(new Element(p.getName(), p)); 379 } 380 381 m_gotall = true; 382 } 383 } else { 384 List<String> keys = m_cache.getKeysWithExpiryCheck(); 385 all = new TreeSet<WikiPage>(); 386 for (String key : keys) { 387 Element element = m_cache.get(key); 388 Object cachedPage = element.getObjectValue(); 389 if (cachedPage != null) { 390 all.add((WikiPage) cachedPage); 391 } 392 } 393 } 394 395 if( all.size() >= m_cache.getCacheConfiguration().getMaxEntriesLocalHeap() ) { 396 log.warn( "seems " + m_cache.getName() + " can't hold all pages from your page repository, " + 397 "so we're delegating on the underlying provider instead. Please consider increasing " + 398 "your cache sizes on ehcache.xml to avoid this behaviour" ); 399 return m_provider.getAllPages(); 400 } 401 402 return all; 403 } 404 405 /** 406 * {@inheritDoc} 407 */ 408 public Collection getAllChangedSince( Date date ) 409 { 410 return m_provider.getAllChangedSince( date ); 411 } 412 413 /** 414 * {@inheritDoc} 415 */ 416 public int getPageCount() 417 throws ProviderException 418 { 419 return m_provider.getPageCount(); 420 } 421 422 /** 423 * {@inheritDoc} 424 */ 425 public Collection findPages( QueryItem[] query ) 426 { 427 // 428 // If the provider is a fast searcher, then 429 // just pass this request through. 430 // 431 return m_provider.findPages( query ); 432 433 // FIXME: Does not implement fast searching 434 } 435 436 // 437 // FIXME: Kludge: make sure that the page is also parsed and it gets all the 438 // necessary variables. 439 // 440 441 private void refreshMetadata( WikiPage page ) 442 { 443 if( page != null && !page.hasMetadata() ) 444 { 445 RenderingManager mgr = m_engine.getRenderingManager(); 446 447 try 448 { 449 String data = m_provider.getPageText(page.getName(), page.getVersion()); 450 451 WikiContext ctx = new WikiContext( m_engine, page ); 452 MarkupParser parser = mgr.getParser( ctx, data ); 453 454 parser.parse(); 455 } 456 catch( Exception ex ) 457 { 458 log.debug("Failed to retrieve variables for wikipage "+page); 459 } 460 } 461 } 462 463 /** 464 * {@inheritDoc} 465 */ 466 public WikiPage getPageInfo( String pageName, int version ) throws ProviderException 467 { 468 WikiPage page = null; 469 WikiPage cached = getPageInfoFromCache( pageName ); 470 471 int latestcached = (cached != null) ? cached.getVersion() : Integer.MIN_VALUE; 472 473 if( version == WikiPageProvider.LATEST_VERSION || version == latestcached ) 474 { 475 if( cached == null ) 476 { 477 WikiPage data = m_provider.getPageInfo( pageName, version ); 478 479 if( data != null ) 480 { 481 m_cache.put(new Element(pageName, data)); 482 } 483 page = data; 484 } 485 else 486 { 487 page = cached; 488 } 489 } 490 else 491 { 492 // We do not cache old versions. 493 page = m_provider.getPageInfo( pageName, version ); 494 //refreshMetadata( page ); 495 } 496 497 refreshMetadata( page ); 498 499 return page; 500 } 501 502 /** 503 * {@inheritDoc} 504 */ 505 public List getVersionHistory(String pageName) throws ProviderException { 506 List history = null; 507 508 if (pageName == null) return null; 509 Element element = m_historyCache.get(pageName); 510 511 if (element != null) { 512 m_historyCacheHits++; 513 history = (List) element.getObjectValue(); 514 } else { 515 history = m_provider.getVersionHistory(pageName); 516 m_historyCache.put( new Element( pageName, history )); 517 m_historyCacheMisses++; 518 } 519 520 return history; 521 } 522 523 /** 524 * Gets the provider class name, and cache statistics (misscount and hitcount of page cache and history cache). 525 * 526 * @return A plain string with all the above mentioned values. 527 */ 528 public synchronized String getProviderInfo() 529 { 530 return "Real provider: "+m_provider.getClass().getName()+ 531 ". Cache misses: "+m_cacheMisses+ 532 ". Cache hits: "+m_cacheHits+ 533 ". History cache hits: "+m_historyCacheHits+ 534 ". History cache misses: "+m_historyCacheMisses; 535 } 536 537 /** 538 * {@inheritDoc} 539 */ 540 public void deleteVersion( String pageName, int version ) 541 throws ProviderException 542 { 543 // 544 // Luckily, this is such a rare operation it is okay 545 // to synchronize against the whole thing. 546 // 547 synchronized( this ) 548 { 549 WikiPage cached = getPageInfoFromCache( pageName ); 550 551 int latestcached = (cached != null) ? cached.getVersion() : Integer.MIN_VALUE; 552 553 // 554 // If we have this version cached, remove from cache. 555 // 556 if( version == WikiPageProvider.LATEST_VERSION || 557 version == latestcached ) 558 { 559 m_cache.remove(pageName); 560 m_textCache.remove(pageName); 561 } 562 563 m_provider.deleteVersion( pageName, version ); 564 m_historyCache.remove(pageName); 565 } 566 } 567 568 /** 569 * {@inheritDoc} 570 */ 571 public void deletePage( String pageName ) 572 throws ProviderException 573 { 574 // 575 // See note in deleteVersion(). 576 // 577 synchronized(this) 578 { 579 m_cache.put(new Element(pageName, null)); 580 m_textCache.put(new Element( pageName, null )); 581 m_historyCache.put(new Element(pageName, null)); 582 m_provider.deletePage(pageName); 583 } 584 } 585 586 /** 587 * {@inheritDoc} 588 */ 589 public void movePage(String from, String to) throws ProviderException { 590 m_provider.movePage(from, to); 591 592 synchronized (this) { 593 // Clear any cached version of the old page and new page 594 m_cache.remove(from); 595 m_textCache.remove(from); 596 m_historyCache.remove(from); 597 log.debug("Removing to page " + to + " from cache"); 598 m_cache.remove(to); 599 m_textCache.remove(to); 600 m_historyCache.remove(to); 601 } 602 } 603 604 /** 605 * Returns the actual used provider. 606 * @since 2.0 607 * @return The real provider. 608 */ 609 public WikiPageProvider getRealProvider() 610 { 611 return m_provider; 612 } 613 614}