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