001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2005 Janne Jalkanen (Janne.Jalkanen@iki.fi)
005:
006: This program is free software; you can redistribute it and/or modify
007: it under the terms of the GNU Lesser General Public License as published by
008: the Free Software Foundation; either version 2.1 of the License, or
009: (at your option) any later version.
010:
011: This program is distributed in the hope that it will be useful,
012: but WITHOUT ANY WARRANTY; without even the implied warranty of
013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: GNU Lesser General Public License for more details.
015:
016: You should have received a copy of the GNU Lesser General Public License
017: along with this program; if not, write to the Free Software
018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: */
020: package com.ecyrd.jspwiki.providers;
021:
022: import java.io.IOException;
023: import java.util.*;
024:
025: import org.apache.log4j.Logger;
026:
027: import com.ecyrd.jspwiki.*;
028: import com.ecyrd.jspwiki.parser.MarkupParser;
029: import com.ecyrd.jspwiki.render.RenderingManager;
030: import com.ecyrd.jspwiki.util.ClassUtil;
031: import com.opensymphony.oscache.base.Cache;
032: import com.opensymphony.oscache.base.NeedsRefreshException;
033: import com.opensymphony.oscache.base.events.*;
034:
035: /**
036: * Provides a caching page provider. This class rests on top of a
037: * real provider class and provides a cache to speed things up. Only
038: * if the cache copy of the page text has expired, we fetch it from
039: * the provider.
040: * <p>
041: * This class also detects if someone has modified the page
042: * externally, not through JSPWiki routines, and throws the proper
043: * RepositoryModifiedException.
044: * <p>
045: * Heavily based on ideas by Chris Brooking.
046: * <p>
047: * Since 2.1.52 uses the OSCache library from OpenSymphony.
048: *
049: * @author Janne Jalkanen
050: * @since 1.6.4
051: * @see RepositoryModifiedException
052: */
053: // FIXME: Synchronization is a bit inconsistent in places.
054: // FIXME: A part of the stuff is now redundant, since we could easily use the text cache
055: // for a lot of things. RefactorMe.
056: public class CachingProvider implements WikiPageProvider,
057: VersioningProvider {
058: private static final Logger log = Logger
059: .getLogger(CachingProvider.class);
060:
061: private WikiPageProvider m_provider;
062: // FIXME: Find another way to the search engine to use instead of from WikiEngine?
063: private WikiEngine m_engine;
064:
065: private Cache m_cache;
066: private Cache m_negCache; // Cache for holding non-existing pages
067:
068: private Cache m_textCache;
069: private Cache m_historyCache;
070:
071: private long m_cacheMisses = 0;
072: private long m_cacheHits = 0;
073:
074: private long m_historyCacheMisses = 0;
075: private long m_historyCacheHits = 0;
076:
077: private int m_expiryPeriod = 30;
078:
079: /**
080: * This can be very long, as normally all modifications are noticed in an earlier
081: * stage.
082: */
083: private int m_pageContentExpiryPeriod = 24 * 60 * 60;
084:
085: // FIXME: This MUST be cached somehow.
086:
087: private boolean m_gotall = false;
088:
089: private CacheItemCollector m_allCollector = new CacheItemCollector();
090:
091: /**
092: * Defines, in seconds, the amount of time a text will live in the cache
093: * at most before requiring a refresh.
094: */
095:
096: public static final String PROP_CACHECHECKINTERVAL = "jspwiki.cachingProvider.cacheCheckInterval";
097: public static final String PROP_CACHECAPACITY = "jspwiki.cachingProvider.capacity";
098:
099: private static final int DEFAULT_CACHECAPACITY = 1000; // Good most wikis
100:
101: private static final String OSCACHE_ALGORITHM = "com.opensymphony.oscache.base.algorithm.LRUCache";
102:
103: public void initialize(WikiEngine engine, Properties properties)
104: throws NoRequiredPropertyException, IOException {
105: log.debug("Initing CachingProvider");
106:
107: // engine is used for getting the search engine
108: m_engine = engine;
109:
110: //
111: // Cache consistency checks
112: //
113: m_expiryPeriod = TextUtil.getIntegerProperty(properties,
114: PROP_CACHECHECKINTERVAL, m_expiryPeriod);
115:
116: log.debug("Cache expiry period is " + m_expiryPeriod + " s");
117:
118: //
119: // Text cache capacity
120: //
121: int capacity = TextUtil.getIntegerProperty(properties,
122: PROP_CACHECAPACITY, DEFAULT_CACHECAPACITY);
123:
124: log.debug("Cache capacity " + capacity + " pages.");
125:
126: m_cache = new Cache(true, false, false);
127:
128: //
129: // OSCache documentation sucks big time. The clazz-parameter is completely
130: // undefined; I had to read the source code to figure out that you need
131: // to declare what type of a listener you are adding by sending the type
132: // of the interface.
133: //
134: m_cache.addCacheEventListener(m_allCollector,
135: CacheEntryEventListener.class);
136:
137: //
138: // FIXME: There's an interesting issue here... It would probably be
139: // possible to DOS a JSPWiki instance by bombarding it with names that
140: // do not exist, as they would fill the negcache. Will need to
141: // think about this some more...
142: //
143: m_negCache = new Cache(true, false, false);
144:
145: m_textCache = new Cache(true, false, false, false,
146: OSCACHE_ALGORITHM, capacity);
147:
148: m_historyCache = new Cache(true, false, false, false,
149: OSCACHE_ALGORITHM, capacity);
150:
151: //
152: // Find and initialize real provider.
153: //
154: String classname = WikiEngine.getRequiredProperty(properties,
155: PageManager.PROP_PAGEPROVIDER);
156:
157: try {
158: Class providerclass = ClassUtil.findClass(
159: "com.ecyrd.jspwiki.providers", classname);
160:
161: m_provider = (WikiPageProvider) providerclass.newInstance();
162:
163: log.debug("Initializing real provider class " + m_provider);
164: m_provider.initialize(engine, properties);
165: } catch (ClassNotFoundException e) {
166: log
167: .error("Unable to locate provider class "
168: + classname, e);
169: throw new IllegalArgumentException("no provider class");
170: } catch (InstantiationException e) {
171: log
172: .error("Unable to create provider class "
173: + classname, e);
174: throw new IllegalArgumentException("faulty provider class");
175: } catch (IllegalAccessException e) {
176: log.error("Illegal access to provider class " + classname,
177: e);
178: throw new IllegalArgumentException("illegal provider class");
179: }
180: }
181:
182: private WikiPage getPageInfoFromCache(String name)
183: throws ProviderException, RepositoryModifiedException {
184: boolean wasUpdated = false;
185: // Sanity check; seems to occur sometimes
186: if (name == null)
187: return null;
188:
189: try {
190: WikiPage item = (WikiPage) m_cache.getFromCache(name,
191: m_expiryPeriod);
192:
193: wasUpdated = true;
194:
195: if (item != null)
196: return item;
197:
198: return null;
199: } catch (NeedsRefreshException e) {
200: WikiPage cached = (WikiPage) e.getCacheContent();
201:
202: // int version = (cached != null) ? cached.getVersion() : WikiPageProvider.LATEST_VERSION;
203:
204: WikiPage refreshed;
205:
206: //
207: // Just be careful that we don't accidentally leave the cache in a
208: // hung state
209: //
210:
211: refreshed = m_provider.getPageInfo(name,
212: WikiPageProvider.LATEST_VERSION);
213:
214: if (refreshed == null && cached != null) {
215: // Page has been removed evilly by a goon from outer space
216:
217: log.debug("Page " + name
218: + " has been removed externally.");
219:
220: m_cache.putInCache(name, null);
221: m_textCache.putInCache(name, null);
222: m_historyCache.putInCache(name, null);
223: // We cache a page miss
224: m_negCache.putInCache(name, name);
225: wasUpdated = true;
226:
227: throw new RepositoryModifiedException("Removed: "
228: + name, name);
229: } else if (cached == null) {
230: // The page did not exist in the first place
231:
232: if (refreshed != null) {
233: // We must now add it
234: m_cache.putInCache(name, refreshed);
235: // Requests for this page are now no longer denied
236: m_negCache.putInCache(name, null);
237: wasUpdated = true;
238:
239: throw new RepositoryModifiedException("Added: "
240: + name, name);
241: }
242:
243: // Cache page miss
244: m_negCache.putInCache(name, name);
245: } else if (refreshed != null
246: && cached.getVersion() != refreshed.getVersion()) {
247: // The newest version has been deleted, but older versions still remain
248: log.debug("Page " + cached.getName()
249: + " newest version deleted, reloading...");
250:
251: m_cache.putInCache(name, refreshed);
252: // Requests for this page are now no longer denied
253: m_negCache.putInCache(name, null);
254:
255: m_textCache.flushEntry(name);
256: m_historyCache.flushEntry(name);
257: wasUpdated = true;
258:
259: return refreshed;
260: } else if (refreshed != null
261: && Math.abs(refreshed.getLastModified().getTime()
262: - cached.getLastModified().getTime()) > 1000L) {
263: // Yes, the page has been modified externally and nobody told us
264:
265: log.info("Page " + cached.getName()
266: + " changed, reloading...");
267:
268: m_cache.putInCache(name, refreshed);
269: // Requests for this page are now no longer denied
270: m_negCache.putInCache(name, null);
271: m_textCache.flushEntry(name);
272: m_historyCache.flushEntry(name);
273: wasUpdated = true;
274:
275: throw new RepositoryModifiedException("Modified: "
276: + name, name);
277: } else {
278: // Refresh the cache by putting the same object back
279: m_cache.putInCache(name, cached);
280: // Requests for this page are now no longer denied
281: m_negCache.putInCache(name, null);
282: wasUpdated = true;
283: }
284:
285: return cached;
286: } finally {
287: if (!wasUpdated)
288: m_cache.cancelUpdate(name);
289: }
290: }
291:
292: public boolean pageExists(String pageName, int version) {
293: if (pageName == null)
294: return false;
295:
296: //
297: // First, check the negative cache if we've seen it before
298: //
299: try {
300: String isNonExistant = (String) m_negCache.getFromCache(
301: pageName, m_expiryPeriod);
302:
303: if (isNonExistant != null)
304: return false; // No such page
305: } catch (NeedsRefreshException e) {
306: m_negCache.cancelUpdate(pageName);
307: }
308:
309: WikiPage p = null;
310:
311: try {
312: p = getPageInfoFromCache(pageName);
313: } catch (RepositoryModifiedException e) {
314: // The repository was modified, we need to check now if the page was removed or
315: // added.
316: // TODO: This information would be available in the exception, but we would
317: // need to subclass.
318:
319: try {
320: p = getPageInfoFromCache(pageName);
321: } catch (Exception ex) {
322: // This should not happen
323: return false;
324: }
325: } catch (ProviderException e) {
326: log
327: .info("Provider failed while trying to check if page exists: "
328: + pageName);
329: return false;
330: }
331:
332: if (p != null) {
333: int latestVersion = p.getVersion();
334:
335: if (version == latestVersion || version == LATEST_VERSION) {
336: return true;
337: }
338:
339: if (m_provider instanceof VersioningProvider)
340: return ((VersioningProvider) m_provider).pageExists(
341: pageName, version);
342: }
343:
344: try {
345: return getPageInfo(pageName, version) != null;
346: } catch (ProviderException e) {
347: }
348:
349: return false;
350: }
351:
352: public boolean pageExists(String pageName) {
353: if (pageName == null)
354: return false;
355:
356: //
357: // First, check the negative cache if we've seen it before
358: //
359: try {
360: String isNonExistant = (String) m_negCache.getFromCache(
361: pageName, m_expiryPeriod);
362:
363: if (isNonExistant != null)
364: return false; // No such page
365: } catch (NeedsRefreshException e) {
366: m_negCache.cancelUpdate(pageName);
367: }
368:
369: WikiPage p = null;
370:
371: try {
372: p = getPageInfoFromCache(pageName);
373: } catch (RepositoryModifiedException e) {
374: // The repository was modified, we need to check now if the page was removed or
375: // added.
376: // TODO: This information would be available in the exception, but we would
377: // need to subclass.
378:
379: try {
380: p = getPageInfoFromCache(pageName);
381: } catch (Exception ex) {
382: return false; // This should not happen
383: }
384: } catch (ProviderException e) {
385: log
386: .info("Provider failed while trying to check if page exists: "
387: + pageName);
388: return false;
389: }
390:
391: //
392: // A null item means that the page either does not
393: // exist, or has not yet been cached; a non-null
394: // means that the page does exist.
395: //
396: if (p != null) {
397: return true;
398: }
399:
400: //
401: // If we have a list of all pages in memory, then any page
402: // not in the cache must be non-existent.
403: //
404: // FIXME: There's a problem here; if someone modifies the
405: // repository by adding a page outside JSPWiki,
406: // we won't notice it.
407:
408: if (m_gotall) {
409: return false;
410: }
411:
412: //
413: // We could add the page to the cache here as well,
414: // but in order to understand whether that is a
415: // good thing or not we would need to analyze
416: // the JSPWiki calling patterns extensively. Presumably
417: // it would be a good thing if pageExists() is called
418: // many times before the first getPageText() is called,
419: // and the whole page is cached.
420: //
421: return m_provider.pageExists(pageName);
422: }
423:
424: /**
425: * @throws RepositoryModifiedException If the page has been externally modified.
426: */
427: public String getPageText(String pageName, int version)
428: throws ProviderException, RepositoryModifiedException {
429: String result = null;
430:
431: if (pageName == null)
432: return null;
433:
434: if (version == WikiPageProvider.LATEST_VERSION) {
435: result = getTextFromCache(pageName);
436: } else {
437: WikiPage p = getPageInfoFromCache(pageName);
438:
439: //
440: // Or is this the latest version fetched by version number?
441: //
442: if (p != null && p.getVersion() == version) {
443: result = getTextFromCache(pageName);
444: } else {
445: result = m_provider.getPageText(pageName, version);
446: }
447: }
448:
449: return result;
450: }
451:
452: /**
453: * @throws RepositoryModifiedException If the page has been externally modified.
454: */
455: private String getTextFromCache(String pageName)
456: throws ProviderException, RepositoryModifiedException {
457: String text;
458: boolean wasUpdated = false;
459:
460: if (pageName == null)
461: return null;
462:
463: WikiPage page = getPageInfoFromCache(pageName);
464:
465: try {
466: text = (String) m_textCache.getFromCache(pageName,
467: m_pageContentExpiryPeriod);
468: wasUpdated = true;
469:
470: if (text == null) {
471: if (page != null) {
472: text = m_provider.getPageText(pageName,
473: WikiPageProvider.LATEST_VERSION);
474:
475: m_textCache.putInCache(pageName, text);
476:
477: m_cacheMisses++;
478: } else {
479: return null;
480: }
481: } else {
482: m_cacheHits++;
483: }
484: } catch (NeedsRefreshException e) {
485: if (pageExists(pageName)) {
486: text = m_provider.getPageText(pageName,
487: WikiPageProvider.LATEST_VERSION);
488:
489: m_textCache.putInCache(pageName, text);
490: wasUpdated = true;
491:
492: m_cacheMisses++;
493: } else {
494: m_textCache.putInCache(pageName, null);
495: wasUpdated = true;
496: return null; // No page exists
497: }
498: } finally {
499: if (!wasUpdated)
500: m_textCache.cancelUpdate(pageName);
501: }
502:
503: return text;
504: }
505:
506: public void putPageText(WikiPage page, String text)
507: throws ProviderException {
508: synchronized (this ) {
509: m_provider.putPageText(page, text);
510:
511: page.setLastModified(new Date());
512:
513: // Refresh caches properly
514:
515: m_cache.flushEntry(page.getName());
516: m_textCache.flushEntry(page.getName());
517: m_historyCache.flushEntry(page.getName());
518: m_negCache.flushEntry(page.getName());
519:
520: // Refresh caches
521: try {
522: getPageInfoFromCache(page.getName());
523: } catch (RepositoryModifiedException e) {
524: } // Expected
525: }
526: }
527:
528: public Collection getAllPages() throws ProviderException {
529: Collection all;
530:
531: if (m_gotall == false) {
532: all = m_provider.getAllPages();
533:
534: // Make sure that all pages are in the cache.
535:
536: synchronized (this ) {
537: for (Iterator i = all.iterator(); i.hasNext();) {
538: WikiPage p = (WikiPage) i.next();
539:
540: m_cache.putInCache(p.getName(), p);
541: // Requests for this page are now no longer denied
542: m_negCache.putInCache(p.getName(), null);
543: }
544:
545: m_gotall = true;
546: }
547: } else {
548: all = m_allCollector.getAllItems();
549: }
550:
551: return all;
552: }
553:
554: public Collection getAllChangedSince(Date date) {
555: return m_provider.getAllChangedSince(date);
556: }
557:
558: public int getPageCount() throws ProviderException {
559: return m_provider.getPageCount();
560: }
561:
562: public Collection findPages(QueryItem[] query) {
563: //
564: // If the provider is a fast searcher, then
565: // just pass this request through.
566: //
567: return m_provider.findPages(query);
568:
569: // FIXME: Does not implement fast searching
570: }
571:
572: //
573: // FIXME: Kludge: make sure that the page is also parsed and it gets all the
574: // necessary variables.
575: //
576:
577: private void refreshMetadata(WikiPage page) {
578: if (page != null && !page.hasMetadata()) {
579: RenderingManager mgr = m_engine.getRenderingManager();
580:
581: try {
582: String data = m_provider.getPageText(page.getName(),
583: page.getVersion());
584:
585: WikiContext ctx = new WikiContext(m_engine, page);
586: MarkupParser parser = mgr.getParser(ctx, data);
587:
588: parser.parse();
589: } catch (Exception ex) {
590: log.debug("Failed to retrieve variables for wikipage "
591: + page);
592: }
593: }
594: }
595:
596: public WikiPage getPageInfo(String pageName, int version)
597: throws ProviderException, RepositoryModifiedException {
598: WikiPage page = null;
599: WikiPage cached = getPageInfoFromCache(pageName);
600:
601: int latestcached = (cached != null) ? cached.getVersion()
602: : Integer.MIN_VALUE;
603:
604: if (version == WikiPageProvider.LATEST_VERSION
605: || version == latestcached) {
606: if (cached == null) {
607: WikiPage data = m_provider.getPageInfo(pageName,
608: version);
609:
610: if (data != null) {
611: m_cache.putInCache(pageName, data);
612: // Requests for this page are now no longer denied
613: m_negCache.putInCache(pageName, null);
614: }
615: page = data;
616: } else {
617: page = cached;
618: }
619: } else {
620: // We do not cache old versions.
621: page = m_provider.getPageInfo(pageName, version);
622: //refreshMetadata( page );
623: }
624:
625: refreshMetadata(page);
626:
627: return page;
628: }
629:
630: public List getVersionHistory(String pageName)
631: throws ProviderException {
632: List history = null;
633: boolean wasUpdated = false;
634:
635: if (pageName == null)
636: return null;
637: try {
638: history = (List) m_historyCache.getFromCache(pageName,
639: m_expiryPeriod);
640:
641: log.debug("History cache hit for page " + pageName);
642: m_historyCacheHits++;
643: wasUpdated = true;
644: } catch (NeedsRefreshException e) {
645: history = m_provider.getVersionHistory(pageName);
646:
647: m_historyCache.putInCache(pageName, history);
648:
649: log.debug("History cache miss for page " + pageName);
650: m_historyCacheMisses++;
651: wasUpdated = true;
652: } finally {
653: if (!wasUpdated)
654: m_historyCache.cancelUpdate(pageName);
655: }
656:
657: return history;
658: }
659:
660: public synchronized String getProviderInfo() {
661: return "Real provider: " + m_provider.getClass().getName()
662: + ". Cache misses: " + m_cacheMisses + ". Cache hits: "
663: + m_cacheHits + ". History cache hits: "
664: + m_historyCacheHits + ". History cache misses: "
665: + m_historyCacheMisses + ". Cache consistency checks: "
666: + m_expiryPeriod + "s";
667: }
668:
669: public void deleteVersion(String pageName, int version)
670: throws ProviderException {
671: //
672: // Luckily, this is such a rare operation it is okay
673: // to synchronize against the whole thing.
674: //
675: synchronized (this ) {
676: WikiPage cached = getPageInfoFromCache(pageName);
677:
678: int latestcached = (cached != null) ? cached.getVersion()
679: : Integer.MIN_VALUE;
680:
681: //
682: // If we have this version cached, remove from cache.
683: //
684: if (version == WikiPageProvider.LATEST_VERSION
685: || version == latestcached) {
686: m_cache.flushEntry(pageName);
687: m_textCache.putInCache(pageName, null);
688: m_historyCache.putInCache(pageName, null);
689: }
690:
691: m_provider.deleteVersion(pageName, version);
692: }
693: }
694:
695: public void deletePage(String pageName) throws ProviderException {
696: //
697: // See note in deleteVersion().
698: //
699: synchronized (this ) {
700: m_cache.putInCache(pageName, null);
701: m_textCache.putInCache(pageName, null);
702: m_historyCache.putInCache(pageName, null);
703: m_negCache.putInCache(pageName, pageName);
704: m_provider.deletePage(pageName);
705: }
706: }
707:
708: public void movePage(String from, String to)
709: throws ProviderException {
710: m_provider.movePage(from, to);
711:
712: synchronized (this ) {
713: // Clear any cached version of the old page
714: log.debug("Removing page " + from + " from cache");
715: m_cache.flushEntry(from);
716:
717: // Clear the cache for the to page, if that page already exists
718: //if ( m_cache.get( to ) != null )
719: //{
720: log.debug("Removing page " + to + " from cache");
721: m_cache.flushEntry(to);
722: //}
723: }
724: }
725:
726: /**
727: * Returns the actual used provider.
728: * @since 2.0
729: */
730: public WikiPageProvider getRealProvider() {
731: return m_provider;
732: }
733:
734: /**
735: * This is a simple class that keeps a list of all WikiPages that
736: * we have in memory. Because the OSCache cannot give us a list
737: * of all pages currently in cache, we'll have to check this
738: * ourselves.
739: *
740: * @author jalkanen
741: *
742: * @since 2.4
743: */
744: private static class CacheItemCollector implements
745: CacheEntryEventListener {
746: private Map m_allItems = new HashMap();
747:
748: /**
749: * Returns a clone of the set - you cannot manipulate this.
750: *
751: * @return
752: */
753: public Set getAllItems() {
754: Set ret = new TreeSet();
755: ret.addAll(m_allItems.values());
756:
757: return ret;
758: }
759:
760: public void cacheEntryAdded(CacheEntryEvent arg0) {
761: cacheEntryUpdated(arg0);
762: }
763:
764: public void cachePatternFlushed(CachePatternEvent ev) {
765: }
766:
767: public void cacheGroupFlushed(CacheGroupEvent ev) {
768: }
769:
770: public void cacheFlushed(CachewideEvent ev) {
771: }
772:
773: public void cacheEntryFlushed(CacheEntryEvent arg0) {
774: cacheEntryRemoved(arg0);
775: }
776:
777: public void cacheEntryRemoved(CacheEntryEvent arg0) {
778: WikiPage item = (WikiPage) arg0.getEntry().getContent();
779:
780: if (item != null) {
781: m_allItems.remove(item);
782: }
783: }
784:
785: public void cacheEntryUpdated(CacheEntryEvent arg0) {
786: WikiPage item = (WikiPage) arg0.getEntry().getContent();
787:
788: if (item != null) {
789: // Item added or replaced.
790: m_allItems.put(item.getName(), item);
791: } else {
792: // Removed item
793: // FIXME: If the page system is changed during this time, we'll just fail gracefully
794:
795: m_allItems.remove(arg0.getKey());
796: }
797: }
798: }
799: }
|