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