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