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