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 org.apache.logging.log4j.LogManager; 022import org.apache.logging.log4j.Logger; 023import org.apache.wiki.api.core.Context; 024import org.apache.wiki.api.core.Engine; 025import org.apache.wiki.api.core.Page; 026import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 027import org.apache.wiki.api.exceptions.ProviderException; 028import org.apache.wiki.api.providers.PageProvider; 029import org.apache.wiki.api.search.QueryItem; 030import org.apache.wiki.api.search.SearchResult; 031import org.apache.wiki.api.spi.Wiki; 032import org.apache.wiki.cache.CacheInfo; 033import org.apache.wiki.cache.CachingManager; 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; 047import java.util.concurrent.atomic.AtomicBoolean; 048import java.util.concurrent.atomic.AtomicLong; 049 050 051/** 052 * Provides a caching page provider. This class rests on top of a real provider class and provides a cache to speed things up. Only 053 * if the cache copy of the page text has expired, we fetch it from the provider. 054 * <p> 055 * This class does not detect if someone has modified the page 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 */ 063public class CachingProvider implements PageProvider { 064 065 private static final Logger LOG = LogManager.getLogger( CachingProvider.class ); 066 067 private CachingManager cachingManager; 068 private PageProvider provider; 069 private Engine engine; 070 071 private final AtomicBoolean allRequested = new AtomicBoolean(); 072 private final AtomicLong pages = new AtomicLong( 0L ); 073 074 /** 075 * {@inheritDoc} 076 */ 077 @Override 078 public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException { 079 LOG.debug( "Initing CachingProvider" ); 080 081 // engine is used for getting the search engine 082 this.engine = engine; 083 cachingManager = this.engine.getManager( CachingManager.class ); 084 cachingManager.registerListener( CachingManager.CACHE_PAGES, "expired", allRequested ); 085 086 // Find and initialize real provider. 087 final String classname; 088 try { 089 classname = TextUtil.getRequiredProperty( properties, PageManager.PROP_PAGEPROVIDER ); 090 } catch( final NoSuchElementException e ) { 091 throw new NoRequiredPropertyException( e.getMessage(), PageManager.PROP_PAGEPROVIDER ); 092 } 093 094 try { 095 provider = ClassUtil.buildInstance( "org.apache.wiki.providers", classname ); 096 LOG.debug( "Initializing real provider class {}", provider ); 097 provider.initialize( engine, properties ); 098 } catch( final ReflectiveOperationException e ) { 099 LOG.error( "Unable to instantiate provider class {}", classname, e ); 100 throw new IllegalArgumentException( "illegal provider class", e ); 101 } 102 } 103 104 private Page getPageInfoFromCache( final String name ) throws ProviderException { 105 // Sanity check; seems to occur sometimes 106 if( name == null ) { 107 return null; 108 } 109 return cachingManager.get( CachingManager.CACHE_PAGES, name, () -> provider.getPageInfo( name, PageProvider.LATEST_VERSION ) ); 110 } 111 112 113 /** 114 * {@inheritDoc} 115 */ 116 @Override 117 public boolean pageExists( final String pageName, final int version ) { 118 if( pageName == null ) { 119 return false; 120 } 121 122 final Page p; 123 try { 124 p = getPageInfoFromCache( pageName ); 125 } catch( final ProviderException e ) { 126 LOG.info( "Provider failed while trying to check if page exists: {}", pageName ); 127 return false; 128 } 129 130 if( p != null ) { 131 final int latestVersion = p.getVersion(); 132 if( version == latestVersion || version == LATEST_VERSION ) { 133 return true; 134 } 135 136 return provider.pageExists( pageName, version ); 137 } 138 139 try { 140 return getPageInfo( pageName, version ) != null; 141 } catch( final ProviderException e ) { 142 LOG.info( "Provider failed while retrieving {}", pageName ); 143 } 144 145 return false; 146 } 147 148 /** 149 * {@inheritDoc} 150 */ 151 @Override 152 public boolean pageExists( final String pageName ) { 153 if( pageName == null ) { 154 return false; 155 } 156 157 final Page p; 158 try { 159 p = getPageInfoFromCache( pageName ); 160 } catch( final ProviderException e ) { 161 LOG.info( "Provider failed while trying to check if page exists: {}", pageName ); 162 return false; 163 } 164 165 // 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. 166 if( p != null ) { 167 return true; 168 } 169 170 // If we have a list of all pages in memory, then any page not in the cache must be non-existent. 171 if( pages.get() < cachingManager.info( CachingManager.CACHE_PAGES ).getMaxElementsAllowed() ) { 172 return false; 173 } 174 175 // 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 176 // need to analyze the JSPWiki calling patterns extensively. Presumably it would be a good thing if pageExists() is called 177 // many times before the first getPageText() is called, and the whole page is cached. 178 return provider.pageExists( pageName ); 179 } 180 181 /** 182 * {@inheritDoc} 183 */ 184 @Override 185 public String getPageText( final String pageName, final int version ) throws ProviderException { 186 if( pageName == null ) { 187 return null; 188 } 189 190 final String result; 191 if( version == PageProvider.LATEST_VERSION ) { 192 result = getTextFromCache( pageName ); 193 } else { 194 final Page p = getPageInfoFromCache( pageName ); 195 196 // Or is this the latest version fetched by version number? 197 if( p != null && p.getVersion() == version ) { 198 result = getTextFromCache( pageName ); 199 } else { 200 result = provider.getPageText( pageName, version ); 201 } 202 } 203 204 return result; 205 } 206 207 private String getTextFromCache( final String pageName ) throws ProviderException { 208 if( pageName == null ) { 209 return null; 210 } 211 212 return cachingManager.get( CachingManager.CACHE_PAGES_TEXT, pageName, () -> { 213 if( pageExists( pageName ) ) { 214 return provider.getPageText( pageName, PageProvider.LATEST_VERSION ); 215 } 216 return null; 217 } ); 218 } 219 220 /** 221 * {@inheritDoc} 222 */ 223 @Override 224 public void putPageText( final Page page, final String text ) throws ProviderException { 225 synchronized( this ) { 226 provider.putPageText( page, text ); 227 page.setLastModified( new Date() ); 228 229 // Refresh caches properly 230 cachingManager.remove( CachingManager.CACHE_PAGES, page.getName() ); 231 cachingManager.remove( CachingManager.CACHE_PAGES_TEXT, page.getName() ); 232 cachingManager.remove( CachingManager.CACHE_PAGES_HISTORY, page.getName() ); 233 234 getPageInfoFromCache( page.getName() ); 235 } 236 pages.incrementAndGet(); 237 } 238 239 /** 240 * {@inheritDoc} 241 */ 242 @Override 243 public Collection< Page > getAllPages() throws ProviderException { 244 final Collection< Page > all; 245 if ( !allRequested.get() ) { 246 all = provider.getAllPages(); 247 // Make sure that all pages are in the cache. 248 synchronized( this ) { 249 for( final Page p : all ) { 250 cachingManager.put( CachingManager.CACHE_PAGES, p.getName(), p ); 251 } 252 allRequested.set( true ); 253 } 254 pages.set( all.size() ); 255 } else { 256 final List< String > keys = cachingManager.keys( CachingManager.CACHE_PAGES ); 257 all = new TreeSet<>(); 258 for( final String key : keys ) { 259 final Page cachedPage = cachingManager.get( CachingManager.CACHE_PAGES, key, () -> null ); 260 if( cachedPage != null ) { 261 all.add( cachedPage ); 262 } 263 } 264 } 265 266 if( cachingManager.enabled( CachingManager.CACHE_PAGES ) 267 && pages.get() >= cachingManager.info( CachingManager.CACHE_PAGES ).getMaxElementsAllowed() ) { 268 LOG.warn( "seems {} can't hold all pages from your page repository, " + 269 "so we're delegating on the underlying provider instead. Please consider increasing " + 270 "your cache sizes on the ehcache configuration file to avoid this behaviour", CachingManager.CACHE_PAGES ); 271 return provider.getAllPages(); 272 } 273 274 return all; 275 } 276 277 /** 278 * {@inheritDoc} 279 */ 280 @Override 281 public Collection< Page > getAllChangedSince( final Date date ) { 282 return provider.getAllChangedSince( date ); 283 } 284 285 /** 286 * {@inheritDoc} 287 */ 288 @Override 289 public int getPageCount() throws ProviderException { 290 return provider.getPageCount(); 291 } 292 293 /** 294 * {@inheritDoc} 295 */ 296 @Override 297 public Collection< SearchResult > findPages( final QueryItem[] query ) { 298 // If the provider is a fast searcher, then just pass this request through. 299 return provider.findPages( query ); 300 // FIXME: Does not implement fast searching 301 } 302 303 // FIXME: Kludge: make sure that the page is also parsed and it gets all the necessary variables. 304 private void refreshMetadata( final Page page ) { 305 if( page != null && !page.hasMetadata() ) { 306 final RenderingManager mgr = engine.getManager( RenderingManager.class ); 307 try { 308 final String data = provider.getPageText( page.getName(), page.getVersion() ); 309 final Context ctx = Wiki.context().create( engine, page ); 310 final MarkupParser parser = mgr.getParser( ctx, data ); 311 312 parser.parse(); 313 } catch( final Exception ex ) { 314 LOG.debug( "Failed to retrieve variables for wikipage {}", page ); 315 } 316 } 317 } 318 319 /** 320 * {@inheritDoc} 321 */ 322 @Override 323 public Page getPageInfo( final String pageName, final int version ) throws ProviderException { 324 final Page page; 325 final Page cached = getPageInfoFromCache( pageName ); 326 final int latestcached = ( cached != null ) ? cached.getVersion() : Integer.MIN_VALUE; 327 if( version == PageProvider.LATEST_VERSION || version == latestcached ) { 328 page = cached; 329 } else { 330 // We do not cache old versions. 331 page = provider.getPageInfo( pageName, version ); 332 } 333 refreshMetadata( page ); 334 return page; 335 } 336 337 /** 338 * {@inheritDoc} 339 */ 340 @Override 341 public List< Page > getVersionHistory( final String pageName) throws ProviderException { 342 if( pageName == null ) { 343 return null; 344 } 345 return cachingManager.get( CachingManager.CACHE_PAGES_HISTORY, pageName, () -> provider.getVersionHistory( pageName ) ); 346 } 347 348 /** 349 * Gets the provider class name, and cache statistics (misscount and hitcount of page cache and history cache). 350 * 351 * @return A plain string with all the above-mentioned values. 352 */ 353 @Override 354 public synchronized String getProviderInfo() { 355 final CacheInfo pageCacheInfo = cachingManager.info( CachingManager.CACHE_PAGES ); 356 final CacheInfo pageHistoryCacheInfo = cachingManager.info( CachingManager.CACHE_PAGES_HISTORY ); 357 return "Real provider: " + provider.getClass().getName()+ 358 ". Page cache hits: " + pageCacheInfo.getHits() + 359 ". Page cache misses: " + pageCacheInfo.getMisses() + 360 ". History cache hits: " + pageHistoryCacheInfo.getHits() + 361 ". History cache misses: " + pageHistoryCacheInfo.getMisses(); 362 } 363 364 /** 365 * {@inheritDoc} 366 */ 367 @Override 368 public void deleteVersion( final String pageName, final int version ) throws ProviderException { 369 // Luckily, this is such a rare operation it is okay to synchronize against the whole thing. 370 synchronized( this ) { 371 final Page cached = getPageInfoFromCache( pageName ); 372 final int latestcached = ( cached != null ) ? cached.getVersion() : Integer.MIN_VALUE; 373 374 // If we have this version cached, remove from cache. 375 if( version == PageProvider.LATEST_VERSION || version == latestcached ) { 376 cachingManager.remove( CachingManager.CACHE_PAGES, pageName ); 377 cachingManager.remove( CachingManager.CACHE_PAGES_TEXT, pageName ); 378 } 379 380 provider.deleteVersion( pageName, version ); 381 cachingManager.remove( CachingManager.CACHE_PAGES_HISTORY, pageName ); 382 } 383 if( version == PageProvider.LATEST_VERSION ) { 384 pages.decrementAndGet(); 385 } 386 } 387 388 /** 389 * {@inheritDoc} 390 */ 391 @Override 392 public void deletePage( final String pageName ) throws ProviderException { 393 // See note in deleteVersion(). 394 synchronized( this ) { 395 cachingManager.put( CachingManager.CACHE_PAGES, pageName, null ); 396 cachingManager.put( CachingManager.CACHE_PAGES_TEXT, pageName, null ); 397 cachingManager.put( CachingManager.CACHE_PAGES_HISTORY, pageName, null ); 398 provider.deletePage( pageName ); 399 } 400 pages.decrementAndGet(); 401 } 402 403 /** 404 * {@inheritDoc} 405 */ 406 @Override 407 public void movePage( final String from, final String to ) throws ProviderException { 408 provider.movePage( from, to ); 409 synchronized( this ) { 410 // Clear any cached version of the old page and new page 411 cachingManager.remove( CachingManager.CACHE_PAGES, from ); 412 cachingManager.remove( CachingManager.CACHE_PAGES_TEXT, from ); 413 cachingManager.remove( CachingManager.CACHE_PAGES_HISTORY, from ); 414 LOG.debug( "Removing to page {} from cache", to ); 415 cachingManager.remove( CachingManager.CACHE_PAGES, to ); 416 cachingManager.remove( CachingManager.CACHE_PAGES_TEXT, to ); 417 cachingManager.remove( CachingManager.CACHE_PAGES_HISTORY, to ); 418 } 419 } 420 421 /** 422 * Returns the actual used provider. 423 * 424 * @since 2.0 425 * @return The real provider. 426 */ 427 public PageProvider getRealProvider() { 428 return provider; 429 } 430 431}