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}