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