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