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