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