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 }