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 net.sf.ehcache.Cache;
022import net.sf.ehcache.CacheManager;
023import net.sf.ehcache.Element;
024import org.apache.logging.log4j.LogManager;
025import org.apache.logging.log4j.Logger;
026import org.apache.wiki.api.core.Context;
027import org.apache.wiki.api.core.Engine;
028import org.apache.wiki.api.core.Page;
029import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
030import org.apache.wiki.api.exceptions.ProviderException;
031import org.apache.wiki.api.providers.PageProvider;
032import org.apache.wiki.api.search.QueryItem;
033import org.apache.wiki.api.search.SearchResult;
034import org.apache.wiki.api.spi.Wiki;
035import org.apache.wiki.pages.PageManager;
036import org.apache.wiki.parser.MarkupParser;
037import org.apache.wiki.render.RenderingManager;
038import org.apache.wiki.util.ClassUtil;
039import org.apache.wiki.util.TextUtil;
040
041import java.io.IOException;
042import java.util.Collection;
043import java.util.Date;
044import java.util.List;
045import java.util.NoSuchElementException;
046import java.util.Properties;
047import java.util.TreeSet;
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 */
062// FIXME: Synchronization is a bit inconsistent in places.
063// FIXME: A part of the stuff is now redundant, since we could easily use the text cache for a lot of things.  RefactorMe.
064public class CachingProvider implements PageProvider {
065
066    private static final Logger log = LogManager.getLogger( CachingProvider.class );
067
068    private final CacheManager m_cacheManager = CacheManager.getInstance();
069
070    private PageProvider m_provider;
071    // FIXME: Find another way to the search engine to use instead of from Engine?
072    private Engine m_engine;
073
074    private Cache m_cache;
075    /** Name of the regular page cache. */
076    public static final String CACHE_NAME = "jspwiki.pageCache";
077
078    private Cache            m_textCache;
079    /** Name of the page text cache. */
080    public static final String TEXTCACHE_NAME = "jspwiki.pageTextCache";
081
082    private Cache            m_historyCache;
083    /** Name of the page history cache. */
084    public static final String HISTORYCACHE_NAME = "jspwiki.pageHistoryCache";
085
086    private long             m_cacheMisses;
087    private long             m_cacheHits;
088
089    private long             m_historyCacheMisses;
090    private long             m_historyCacheHits;
091
092    // FIXME: This MUST be cached somehow.
093
094    private boolean          m_gotall;
095
096    // The default settings of the caches, if you want something else, provide an "ehcache.xml" file
097    // Please note that JSPWiki ships with a default "ehcache.xml" in the classpath
098    public static final int   DEFAULT_CACHECAPACITY   = 1000; // Good most wikis
099    public static final int   DEFAULT_CACHETIMETOLIVESECONDS = 24*3600;
100    public static final int   DEFAULT_CACHETIMETOIDLESECONDS = 24*3600;
101
102    /**
103     *  {@inheritDoc}
104     */
105    @Override
106    public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException {
107        log.debug("Initing CachingProvider");
108
109        // engine is used for getting the search engine
110        m_engine = engine;
111
112        final String cacheName = engine.getApplicationName() + "." + CACHE_NAME;
113        if (m_cacheManager.cacheExists(cacheName)) {
114            m_cache = m_cacheManager.getCache(cacheName);
115        } else {
116            log.info("cache with name " + cacheName +  " not found in ehcache.xml, creating it with defaults.");
117            m_cache = new Cache(cacheName, DEFAULT_CACHECAPACITY, false, false, DEFAULT_CACHETIMETOLIVESECONDS, DEFAULT_CACHETIMETOIDLESECONDS);
118            m_cacheManager.addCache(m_cache);
119        }
120
121        final String textCacheName = engine.getApplicationName() + "." + TEXTCACHE_NAME;
122        if (m_cacheManager.cacheExists(textCacheName)) {
123            m_textCache= m_cacheManager.getCache(textCacheName);
124        } else {
125            log.info("cache with name " + textCacheName +  " not found in ehcache.xml, creating it with defaults.");
126            m_textCache = new Cache(textCacheName, DEFAULT_CACHECAPACITY, false, false, DEFAULT_CACHETIMETOLIVESECONDS, DEFAULT_CACHETIMETOIDLESECONDS);
127            m_cacheManager.addCache(m_textCache);
128        }
129
130        final String historyCacheName = engine.getApplicationName() + "." + HISTORYCACHE_NAME;
131        if (m_cacheManager.cacheExists(historyCacheName)) {
132            m_historyCache= m_cacheManager.getCache(historyCacheName);
133        } else {
134            log.info("cache with name " + historyCacheName +  " not found in ehcache.xml, creating it with defaults.");
135            m_historyCache = new Cache(historyCacheName, DEFAULT_CACHECAPACITY, false, false, DEFAULT_CACHETIMETOLIVESECONDS, DEFAULT_CACHETIMETOIDLESECONDS);
136            m_cacheManager.addCache(m_historyCache);
137        }
138
139        //
140        // m_cache.getCacheEventNotificationService().registerListener(new CacheItemCollector());
141
142        //
143        //  Find and initialize real provider.
144        //
145        final String classname;
146        try {
147            classname = TextUtil.getRequiredProperty( properties, PageManager.PROP_PAGEPROVIDER );
148        } catch( final NoSuchElementException e ) {
149            throw new NoRequiredPropertyException( e.getMessage(), PageManager.PROP_PAGEPROVIDER );
150        }
151
152        try {
153            final Class< ? > providerclass = ClassUtil.findClass( "org.apache.wiki.providers", classname );
154            m_provider = ( PageProvider )providerclass.newInstance();
155
156            log.debug( "Initializing real provider class " + m_provider );
157            m_provider.initialize( engine, properties );
158        } catch( final ClassNotFoundException e ) {
159            log.error( "Unable to locate provider class " + classname, e );
160            throw new IllegalArgumentException( "no provider class", e );
161        } catch( final InstantiationException e ) {
162            log.error( "Unable to create provider class " + classname, e );
163            throw new IllegalArgumentException( "faulty provider class", e );
164        } catch( final IllegalAccessException e ) {
165            log.error( "Illegal access to provider class " + classname, e );
166            throw new IllegalArgumentException( "illegal provider class", e );
167        }
168    }
169
170    private Page getPageInfoFromCache( final String name) throws ProviderException {
171        // Sanity check; seems to occur sometimes
172        if( name == null ) {
173            return null;
174        }
175
176        final Element cacheElement = m_cache.get( name );
177        if( cacheElement == null ) {
178            final Page refreshed = m_provider.getPageInfo( name, PageProvider.LATEST_VERSION );
179            if( refreshed != null ) {
180                m_cache.put( new Element( name, refreshed ) );
181                return refreshed;
182            } else {
183                // page does not exist anywhere
184                return null;
185            }
186        }
187        return ( Page )cacheElement.getObjectValue();
188    }
189
190
191    /**
192     *  {@inheritDoc}
193     */
194    @Override
195    public boolean pageExists( final String pageName, final int version ) {
196        if( pageName == null ) {
197            return false;
198        }
199
200        final Page p;
201        try {
202            p = getPageInfoFromCache( pageName );
203        } catch( final ProviderException e ) {
204            log.info( "Provider failed while trying to check if page exists: " + pageName );
205            return false;
206        }
207
208        if( p != null ) {
209            final int latestVersion = p.getVersion();
210            if( version == latestVersion || version == LATEST_VERSION ) {
211                return true;
212            }
213
214            return m_provider.pageExists( pageName, version );
215        }
216
217        try {
218            return getPageInfo( pageName, version ) != null;
219        } catch( final ProviderException e ) {
220        }
221
222        return false;
223    }
224
225    /**
226     *  {@inheritDoc}
227     */
228    @Override
229    public boolean pageExists( final String pageName ) {
230        if( pageName == null ) {
231            return false;
232        }
233
234        final Page p;
235        try {
236            p = getPageInfoFromCache( pageName );
237        } catch( final ProviderException e ) {
238            log.info( "Provider failed while trying to check if page exists: " + pageName );
239            return false;
240        }
241
242        //  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.
243        if( p != null ) {
244            return true;
245        }
246
247        //  If we have a list of all pages in memory, then any page not in the cache must be non-existent.
248        if( m_gotall ) {
249            return false;
250        }
251
252        //  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
253        //  need to analyze the JSPWiki calling patterns extensively.  Presumably it would be a good thing if pageExists() is called
254        //  many times before the first getPageText() is called, and the whole page is cached.
255        return m_provider.pageExists( pageName );
256    }
257
258    /**
259     *  {@inheritDoc}
260     */
261    @Override
262    public String getPageText( final String pageName, final int version ) throws ProviderException {
263        if( pageName == null ) {
264            return null;
265        }
266
267        final String result;
268        if( version == PageProvider.LATEST_VERSION ) {
269            result = getTextFromCache( pageName );
270        } else {
271            final Page p = getPageInfoFromCache( pageName );
272
273            //  Or is this the latest version fetched by version number?
274            if( p != null && p.getVersion() == version ) {
275                result = getTextFromCache( pageName );
276            } else {
277                result = m_provider.getPageText( pageName, version );
278            }
279        }
280
281        return result;
282    }
283
284
285    private String getTextFromCache( final String pageName ) throws ProviderException {
286        if (pageName == null) {
287            return null;
288        }
289
290        final String text;
291        final Element cacheElement = m_textCache.get(pageName);
292        if( cacheElement != null ) {
293            m_cacheHits++;
294            return ( String )cacheElement.getObjectValue();
295        }
296        if( pageExists( pageName ) ) {
297            text = m_provider.getPageText( pageName, PageProvider.LATEST_VERSION );
298            m_textCache.put( new Element( pageName, text ) );
299            m_cacheMisses++;
300            return text;
301        }
302        //page not found (not in cache, not by real provider)
303        return  null;
304    }
305
306    /**
307     *  {@inheritDoc}
308     */
309    @Override
310    public void putPageText( final Page page, final String text ) throws ProviderException {
311        synchronized( this ) {
312            m_provider.putPageText( page, text );
313            page.setLastModified( new Date() );
314
315            // Refresh caches properly
316            m_cache.remove( page.getName() );
317            m_textCache.remove( page.getName() );
318            m_historyCache.remove( page.getName() );
319
320            getPageInfoFromCache( page.getName() );
321        }
322    }
323
324    /**
325     *  {@inheritDoc}
326     */
327    @Override
328    public Collection< Page > getAllPages() throws ProviderException {
329        final Collection< Page > all;
330
331        if ( !m_gotall ) {
332            all = m_provider.getAllPages();
333
334            // Make sure that all pages are in the cache.
335            synchronized( this ) {
336                for( final Page p : all ) {
337                    m_cache.put( new Element( p.getName(), p ) );
338                }
339
340                m_gotall = true;
341            }
342        } else {
343            @SuppressWarnings("unchecked") final List< String > keys = m_cache.getKeysWithExpiryCheck();
344            all = new TreeSet<>();
345            for( final String key : keys ) {
346                final Element element = m_cache.get( key );
347                final Page cachedPage = ( Page )element.getObjectValue();
348                if( cachedPage != null ) {
349                    all.add( cachedPage );
350                }
351            }
352        }
353
354        if( all.size() >= m_cache.getCacheConfiguration().getMaxEntriesLocalHeap() ) {
355            log.warn( "seems " + m_cache.getName() + " can't hold all pages from your page repository, " +
356                      "so we're delegating on the underlying provider instead. Please consider increasing " +
357                      "your cache sizes on ehcache.xml to avoid this behaviour" );
358            return m_provider.getAllPages();
359        }
360
361        return all;
362    }
363
364    /**
365     *  {@inheritDoc}
366     */
367    @Override
368    public Collection< Page > getAllChangedSince( final Date date ) {
369        return m_provider.getAllChangedSince( date );
370    }
371
372    /**
373     *  {@inheritDoc}
374     */
375    @Override
376    public int getPageCount() throws ProviderException {
377        return m_provider.getPageCount();
378    }
379
380    /**
381     *  {@inheritDoc}
382     */
383    @Override
384    public Collection< SearchResult > findPages( final QueryItem[] query ) {
385        //  If the provider is a fast searcher, then just pass this request through.
386        return m_provider.findPages( query );
387        // FIXME: Does not implement fast searching
388    }
389
390    //  FIXME: Kludge: make sure that the page is also parsed and it gets all the necessary variables.
391    private void refreshMetadata( final Page page ) {
392        if( page != null && !page.hasMetadata() ) {
393            final RenderingManager mgr = m_engine.getManager( RenderingManager.class );
394            try {
395                final String data = m_provider.getPageText( page.getName(), page.getVersion() );
396                final Context ctx = Wiki.context().create( m_engine, page );
397                final MarkupParser parser = mgr.getParser( ctx, data );
398
399                parser.parse();
400            } catch( final Exception ex ) {
401                log.debug( "Failed to retrieve variables for wikipage " + page );
402            }
403        }
404    }
405
406    /**
407     *  {@inheritDoc}
408     */
409    @Override
410    public Page getPageInfo( final String pageName, final int version ) throws ProviderException {
411        final Page page;
412        final Page cached = getPageInfoFromCache( pageName );
413        final int latestcached = ( cached != null ) ? cached.getVersion() : Integer.MIN_VALUE;
414        if( version == PageProvider.LATEST_VERSION || version == latestcached ) {
415            if( cached == null ) {
416                final Page data = m_provider.getPageInfo( pageName, version );
417                if( data != null ) {
418                    m_cache.put( new Element( pageName, data ) );
419                }
420                page = data;
421            } else {
422                page = cached;
423            }
424        } else {
425            // We do not cache old versions.
426            page = m_provider.getPageInfo( pageName, version );
427        }
428        refreshMetadata( page );
429        return page;
430    }
431
432    /**
433     *  {@inheritDoc}
434     */
435    @SuppressWarnings("unchecked")
436    @Override
437    public List< Page > getVersionHistory( final String pageName) throws ProviderException {
438        final List< Page > history;
439        if( pageName == null ) {
440            return null;
441        }
442        final Element element = m_historyCache.get( pageName );
443        if( element != null ) {
444            m_historyCacheHits++;
445            history = ( List< Page > )element.getObjectValue();
446        } else {
447            history = m_provider.getVersionHistory( pageName );
448            m_historyCache.put( new Element( pageName, history ) );
449            m_historyCacheMisses++;
450        }
451
452        return history;
453    }
454
455    /**
456     * Gets the provider class name, and cache statistics (misscount and hitcount of page cache and history cache).
457     *
458     * @return A plain string with all the above mentioned values.
459     */
460    @Override
461    public synchronized String getProviderInfo() {
462        return "Real provider: " + m_provider.getClass().getName()+
463                ". Cache misses: " + m_cacheMisses+
464                ". Cache hits: " + m_cacheHits+
465                ". History cache hits: " + m_historyCacheHits+
466                ". History cache misses: " + m_historyCacheMisses;
467    }
468
469    /**
470     *  {@inheritDoc}
471     */
472    @Override
473    public void deleteVersion( final String pageName, final int version ) throws ProviderException {
474        //  Luckily, this is such a rare operation it is okay to synchronize against the whole thing.
475        synchronized( this ) {
476            final Page cached = getPageInfoFromCache( pageName );
477            final int latestcached = ( cached != null ) ? cached.getVersion() : Integer.MIN_VALUE;
478
479            //  If we have this version cached, remove from cache.
480            if( version == PageProvider.LATEST_VERSION || version == latestcached ) {
481                m_cache.remove( pageName );
482                m_textCache.remove( pageName );
483            }
484
485            m_provider.deleteVersion( pageName, version );
486            m_historyCache.remove( pageName );
487        }
488    }
489
490    /**
491     *  {@inheritDoc}
492     */
493    @Override
494    public void deletePage( final String pageName ) throws ProviderException {
495        //  See note in deleteVersion().
496        synchronized( this ) {
497            m_cache.put( new Element( pageName, null ) );
498            m_textCache.put( new Element( pageName, null ) );
499            m_historyCache.put( new Element( pageName, null ) );
500            m_provider.deletePage( pageName );
501        }
502    }
503
504    /**
505     *  {@inheritDoc}
506     */
507    @Override
508    public void movePage( final String from, final String to ) throws ProviderException {
509        m_provider.movePage( from, to );
510
511        synchronized( this ) {
512            // Clear any cached version of the old page and new page
513            m_cache.remove( from );
514            m_textCache.remove( from );
515            m_historyCache.remove( from );
516            log.debug( "Removing to page " + to + " from cache" );
517            m_cache.remove( to );
518            m_textCache.remove( to );
519            m_historyCache.remove( to );
520        }
521    }
522
523    /**
524     *  Returns the actual used provider.
525     *
526     *  @since 2.0
527     *  @return The real provider.
528     */
529    public PageProvider getRealProvider() {
530        return m_provider;
531    }
532
533}