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        WikiPage page = getPageInfoFromCache(pageName);
327
328        Element cacheElement = m_textCache.get(pageName);
329
330        if (cacheElement != null) {
331            m_cacheHits++;
332            return (String) cacheElement.getObjectValue();
333        }
334        if (pageExists(pageName)) {
335            text = m_provider.getPageText(pageName, WikiPageProvider.LATEST_VERSION);
336            m_textCache.put(new Element(pageName, text));
337            m_cacheMisses++;
338            return text;
339        }
340        //page not found (not in cache, not by real provider)
341        return  null;
342    }
343
344    /**
345     *  {@inheritDoc}
346     */
347    public void putPageText(WikiPage page, String text) throws ProviderException {
348        synchronized (this) {
349            m_provider.putPageText(page, text);
350
351            page.setLastModified(new Date());
352
353            // Refresh caches properly
354
355            m_cache.remove(page.getName());
356            m_textCache.remove(page.getName());
357            m_historyCache.remove(page.getName());
358
359            getPageInfoFromCache(page.getName());
360        }
361    }
362
363    /**
364     *  {@inheritDoc}
365     */
366    public Collection getAllPages() throws ProviderException {
367        Collection all;
368
369        if (m_gotall == false) {
370            all = m_provider.getAllPages();
371
372            // Make sure that all pages are in the cache.
373
374            synchronized (this) {
375                for (Iterator i = all.iterator(); i.hasNext(); ) {
376                    WikiPage p = (WikiPage) i.next();
377
378                    m_cache.put(new Element(p.getName(), p));
379                }
380
381                m_gotall = true;
382            }
383        } else {
384            List<String> keys = m_cache.getKeysWithExpiryCheck();
385            all = new TreeSet<WikiPage>();
386            for (String key : keys) {
387                Element element = m_cache.get(key);
388                Object cachedPage = element.getObjectValue();
389                if (cachedPage != null) {
390                    all.add((WikiPage) cachedPage);
391                }
392            }
393        }
394        
395        if( all.size() >= m_cache.getCacheConfiguration().getMaxEntriesLocalHeap() ) {
396            log.warn( "seems " + m_cache.getName() + " can't hold all pages from your page repository, " +
397                      "so we're delegating on the underlying provider instead. Please consider increasing " +
398                      "your cache sizes on ehcache.xml to avoid this behaviour" );
399            return m_provider.getAllPages();
400        }
401        
402        return all;
403    }
404
405    /**
406     *  {@inheritDoc}
407     */
408    public Collection getAllChangedSince( Date date )
409    {
410        return m_provider.getAllChangedSince( date );
411    }
412
413    /**
414     *  {@inheritDoc}
415     */
416    public int getPageCount()
417        throws ProviderException
418    {
419        return m_provider.getPageCount();
420    }
421
422    /**
423     *  {@inheritDoc}
424     */
425    public Collection findPages( QueryItem[] query )
426    {
427        //
428        //  If the provider is a fast searcher, then
429        //  just pass this request through.
430        //
431        return m_provider.findPages( query );
432
433        // FIXME: Does not implement fast searching
434    }
435
436    //
437    //  FIXME: Kludge: make sure that the page is also parsed and it gets all the
438    //         necessary variables.
439    //
440
441    private void refreshMetadata( WikiPage page )
442    {
443        if( page != null && !page.hasMetadata() )
444        {
445            RenderingManager mgr = m_engine.getRenderingManager();
446
447            try
448            {
449                String data = m_provider.getPageText(page.getName(), page.getVersion());
450
451                WikiContext ctx = new WikiContext( m_engine, page );
452                MarkupParser parser = mgr.getParser( ctx, data );
453
454                parser.parse();
455            }
456            catch( Exception ex )
457            {
458                log.debug("Failed to retrieve variables for wikipage "+page);
459            }
460        }
461    }
462
463    /**
464     *  {@inheritDoc}
465     */
466    public WikiPage getPageInfo( String pageName, int version ) throws ProviderException
467    {
468        WikiPage page = null;
469        WikiPage cached = getPageInfoFromCache( pageName );
470
471        int latestcached = (cached != null) ? cached.getVersion() : Integer.MIN_VALUE;
472
473        if( version == WikiPageProvider.LATEST_VERSION || version == latestcached )
474        {
475            if( cached == null )
476            {
477                WikiPage data = m_provider.getPageInfo( pageName, version );
478
479                if( data != null )
480                {
481                    m_cache.put(new Element(pageName, data));
482                }
483                page = data;
484            }
485            else
486            {
487                page = cached;
488            }
489        }
490        else
491        {
492            // We do not cache old versions.
493            page = m_provider.getPageInfo( pageName, version );
494            //refreshMetadata( page );
495        }
496
497        refreshMetadata( page );
498
499        return page;
500    }
501
502    /**
503     *  {@inheritDoc}
504     */
505    public List getVersionHistory(String pageName) throws ProviderException {
506        List history = null;
507
508        if (pageName == null) return null;
509        Element element = m_historyCache.get(pageName);
510
511        if (element != null) {
512            m_historyCacheHits++;
513            history = (List) element.getObjectValue();
514        } else {
515            history = m_provider.getVersionHistory(pageName);
516            m_historyCache.put( new Element( pageName, history ));
517            m_historyCacheMisses++;
518        }
519
520        return history;
521    }
522
523    /**
524     * Gets the provider class name, and cache statistics (misscount and hitcount of page cache and history cache).
525     *
526     * @return A plain string with all the above mentioned values.
527     */
528    public synchronized String getProviderInfo()
529    {
530        return "Real provider: "+m_provider.getClass().getName()+
531                ". Cache misses: "+m_cacheMisses+
532                ". Cache hits: "+m_cacheHits+
533                ". History cache hits: "+m_historyCacheHits+
534                ". History cache misses: "+m_historyCacheMisses;
535    }
536
537    /**
538     *  {@inheritDoc}
539     */
540    public void deleteVersion( String pageName, int version )
541        throws ProviderException
542    {
543        //
544        //  Luckily, this is such a rare operation it is okay
545        //  to synchronize against the whole thing.
546        //
547        synchronized( this )
548        {
549            WikiPage cached = getPageInfoFromCache( pageName );
550
551            int latestcached = (cached != null) ? cached.getVersion() : Integer.MIN_VALUE;
552
553            //
554            //  If we have this version cached, remove from cache.
555            //
556            if( version == WikiPageProvider.LATEST_VERSION ||
557                version == latestcached )
558            {
559                m_cache.remove(pageName);
560                m_textCache.remove(pageName);
561            }
562
563            m_provider.deleteVersion( pageName, version );
564            m_historyCache.remove(pageName);
565        }
566    }
567
568    /**
569     *  {@inheritDoc}
570     */
571    public void deletePage( String pageName )
572        throws ProviderException
573    {
574        //
575        //  See note in deleteVersion().
576        //
577        synchronized(this)
578        {
579            m_cache.put(new Element(pageName, null));
580            m_textCache.put(new Element( pageName, null ));
581            m_historyCache.put(new Element(pageName, null));
582            m_provider.deletePage(pageName);
583        }
584    }
585
586    /**
587     *  {@inheritDoc}
588     */
589    public void movePage(String from, String to) throws ProviderException {
590        m_provider.movePage(from, to);
591
592        synchronized (this) {
593            // Clear any cached version of the old page and new page
594            m_cache.remove(from);
595            m_textCache.remove(from);
596            m_historyCache.remove(from);
597            log.debug("Removing to page " + to + " from cache");
598            m_cache.remove(to);
599            m_textCache.remove(to);
600            m_historyCache.remove(to);
601        }
602    }
603
604    /**
605     *  Returns the actual used provider.
606     *  @since 2.0
607     *  @return The real provider.
608     */
609    public WikiPageProvider getRealProvider()
610    {
611        return m_provider;
612    }
613
614}