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