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