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