    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}