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