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}