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.WikiEngine; 026import org.apache.wiki.WikiPage; 027import org.apache.wiki.WikiProvider; 028import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 029import org.apache.wiki.api.exceptions.ProviderException; 030import org.apache.wiki.attachment.Attachment; 031import org.apache.wiki.attachment.AttachmentManager; 032import org.apache.wiki.search.QueryItem; 033import org.apache.wiki.util.ClassUtil; 034import org.apache.wiki.util.TextUtil; 035 036import java.io.IOException; 037import java.io.InputStream; 038import java.util.ArrayList; 039import java.util.Collection; 040import java.util.Date; 041import java.util.Iterator; 042import java.util.List; 043import java.util.NoSuchElementException; 044import java.util.Properties; 045 046 047/** 048 * Provides a caching attachment provider. This class rests on top of a 049 * real provider class and provides a cache to speed things up. Only the 050 * Attachment objects are cached; the actual attachment contents are 051 * fetched always from the provider. 052 * 053 * @since 2.1.64. 054 */ 055 056// EntryRefreshPolicy for that. 057public class CachingAttachmentProvider 058 implements WikiAttachmentProvider 059{ 060 private static final Logger log = Logger.getLogger(CachingAttachmentProvider.class); 061 062 private WikiAttachmentProvider m_provider; 063 064 private CacheManager m_cacheManager = CacheManager.getInstance(); 065 066 /** Default cache capacity for now. */ 067 public static final int m_capacity = 1000; 068 069 /** 070 * The cache contains Collection objects which contain Attachment objects. 071 * The key is the parent wiki page name (String). 072 */ 073 private Cache m_cache; 074 /** Name of the attachment cache. */ 075 public static final String ATTCOLLCACHE_NAME = "jspwiki.attachmentCollectionsCache"; 076 077 /** 078 * This cache contains Attachment objects and is keyed by attachment name. 079 * This provides for quickly giving recently changed attachments (for the RecentChanges plugin) 080 */ 081 private Cache m_attCache; 082 /** Name of the attachment cache. */ 083 public static final String ATTCACHE_NAME = "jspwiki.attachmentsCache"; 084 085 private long m_cacheMisses = 0; 086 private long m_cacheHits = 0; 087 088 /** The extension to append to directory names to denote an attachment directory. */ 089 public static final String DIR_EXTENSION = "-att"; 090 091 /** Property that supplies the directory used to store attachments. */ 092 public static final String PROP_STORAGEDIR = "jspwiki.basicAttachmentProvider.storageDir"; 093 094 private boolean m_gotall = false; 095 096 /** 097 * {@inheritDoc} 098 */ 099 @Override 100 public void initialize( WikiEngine engine, Properties properties ) throws NoRequiredPropertyException, IOException { 101 log.info("Initing CachingAttachmentProvider"); 102 103 String attCollCacheName = engine.getApplicationName() + "." + ATTCOLLCACHE_NAME; 104 if (m_cacheManager.cacheExists(attCollCacheName)) { 105 m_cache = m_cacheManager.getCache(attCollCacheName); 106 } else { 107 m_cache = new Cache(attCollCacheName, m_capacity, false, false, 0, 0); 108 m_cacheManager.addCache(m_cache); 109 } 110 111 // 112 // cache for the individual Attachment objects, attachment name is key, the Attachment object is the cached object 113 // 114 String attCacheName = engine.getApplicationName() + "." + ATTCACHE_NAME; 115 if (m_cacheManager.cacheExists(attCacheName)) { 116 m_attCache = m_cacheManager.getCache(attCacheName); 117 } else { 118 m_attCache = new Cache(attCacheName, m_capacity, false, false, 0, 0); 119 m_cacheManager.addCache(m_attCache); 120 } 121 // 122 // Find and initialize real provider. 123 // 124 final String classname; 125 try { 126 classname = TextUtil.getRequiredProperty( properties, AttachmentManager.PROP_PROVIDER ); 127 } catch( final NoSuchElementException e ) { 128 throw new NoRequiredPropertyException( e.getMessage(), AttachmentManager.PROP_PROVIDER ); 129 } 130 131 try 132 { 133 Class<?> providerclass = ClassUtil.findClass( "org.apache.wiki.providers", classname); 134 135 m_provider = (WikiAttachmentProvider)providerclass.newInstance(); 136 137 log.debug("Initializing real provider class "+m_provider); 138 m_provider.initialize( engine, properties ); 139 } 140 catch( ClassNotFoundException e ) 141 { 142 log.error("Unable to locate provider class "+classname,e); 143 throw new IllegalArgumentException("no provider class", e); 144 } 145 catch( InstantiationException e ) 146 { 147 log.error("Unable to create provider class "+classname,e); 148 throw new IllegalArgumentException("faulty provider class", e); 149 } 150 catch( IllegalAccessException e ) 151 { 152 log.error("Illegal access to provider class "+classname,e); 153 throw new IllegalArgumentException("illegal provider class", e); 154 } 155 156 } 157 158 /** 159 * {@inheritDoc} 160 */ 161 @Override 162 public void putAttachmentData( Attachment att, InputStream data ) 163 throws ProviderException, IOException { 164 m_provider.putAttachmentData( att, data ); 165 166 m_cache.remove(att.getParentName()); 167 att.setLastModified(new Date()); 168 m_attCache.put(new Element(att.getName(), att)); 169 } 170 171 /** 172 * {@inheritDoc} 173 */ 174 @Override 175 public InputStream getAttachmentData( Attachment att ) 176 throws ProviderException, IOException { 177 return m_provider.getAttachmentData( att ); 178 } 179 180 /** 181 * {@inheritDoc} 182 */ 183 @Override 184 public List< Attachment > listAttachments(WikiPage page) throws ProviderException { 185 log.debug("Listing attachments for " + page); 186 Element element = m_cache.get(page.getName()); 187 188 if (element != null) { 189 @SuppressWarnings("unchecked") 190 List< Attachment > c = ( List< Attachment > )element.getObjectValue(); 191 log.debug("LIST from cache, " + page.getName() + ", size=" + c.size()); 192 return cloneCollection(c); 193 } 194 195 log.debug("list NOT in cache, " + page.getName()); 196 197 return refresh(page); 198 } 199 200 private <T> List<T> cloneCollection( Collection<T> c ) 201 { 202 ArrayList<T> list = new ArrayList<>(); 203 204 list.addAll( c ); 205 206 return list; 207 } 208 209 /** 210 * {@inheritDoc} 211 */ 212 @Override 213 public Collection< Attachment > findAttachments( QueryItem[] query ) 214 { 215 return m_provider.findAttachments( query ); 216 } 217 218 /** 219 * {@inheritDoc} 220 */ 221 @Override 222 public List<Attachment> listAllChanged(Date timestamp) throws ProviderException { 223 List< Attachment > all = null; 224 // 225 // we do a one-time build up of the cache, after this the cache is updated for every attachment add/delete 226 if (m_gotall == false) { 227 all = m_provider.listAllChanged(timestamp); 228 229 // Put all pages in the cache : 230 231 synchronized (this) { 232 for (Iterator< Attachment > i = all.iterator(); i.hasNext(); ) { 233 Attachment att = i.next(); 234 m_attCache.put(new Element(att.getName(), att)); 235 } 236 m_gotall = true; 237 } 238 } else { 239 @SuppressWarnings("unchecked") 240 List< String > keys = m_attCache.getKeysWithExpiryCheck(); 241 all = new ArrayList<>(); 242 for (String key : keys) { 243 Element element = m_attCache.get(key); 244 Attachment cachedAttachment = ( Attachment )element.getObjectValue(); 245 if (cachedAttachment != null) { 246 all.add(cachedAttachment); 247 } 248 } 249 } 250 251 return all; 252 } 253 254 /** 255 * Simply goes through the collection and attempts to locate the 256 * given attachment of that name. 257 * 258 * @return null, if no such attachment was in this collection. 259 */ 260 private Attachment findAttachmentFromCollection( Collection< Attachment > c, String name ) { 261 for( Attachment att : new ArrayList< >( c ) ) { 262 if( name.equals( att.getFileName() ) ) { 263 return att; 264 } 265 } 266 267 return null; 268 } 269 270 /** 271 * Refreshes the cache content and updates counters. 272 * 273 * @return The newly fetched object from the provider. 274 */ 275 private List<Attachment> refresh( WikiPage page ) throws ProviderException 276 { 277 List<Attachment> c = m_provider.listAttachments( page ); 278 m_cache.put(new Element(page.getName(), c)); 279 280 return c; 281 } 282 283 /** 284 * {@inheritDoc} 285 */ 286 @SuppressWarnings("unchecked") 287 @Override 288 public Attachment getAttachmentInfo(WikiPage page, String name, int version) throws ProviderException { 289 if (log.isDebugEnabled()) { 290 log.debug("Getting attachments for " + page + ", name=" + name + ", version=" + version); 291 } 292 293 // 294 // We don't cache previous versions 295 // 296 if (version != WikiProvider.LATEST_VERSION) { 297 log.debug("...we don't cache old versions"); 298 return m_provider.getAttachmentInfo(page, name, version); 299 } 300 301 Collection<Attachment> c = null; 302 Element element = m_cache.get(page.getName()); 303 304 if (element == null) { 305 log.debug(page.getName() + " wasn't in the cache"); 306 c = refresh(page); 307 308 if (c == null) { 309 return null; // No such attachment 310 } 311 } else { 312 log.debug(page.getName() + " FOUND in the cache"); 313 c = (Collection<Attachment>) element.getObjectValue(); 314 } 315 316 return findAttachmentFromCollection(c, name); 317 } 318 319 /** 320 * {@inheritDoc} 321 */ 322 @Override 323 public List<Attachment> getVersionHistory( Attachment att ) 324 { 325 return m_provider.getVersionHistory( att ); 326 } 327 328 /** 329 * {@inheritDoc} 330 */ 331 @Override 332 public void deleteVersion( Attachment att ) throws ProviderException 333 { 334 // This isn't strictly speaking correct, but it does not really matter 335 m_cache.remove(att.getParentName()); 336 m_provider.deleteVersion( att ); 337 } 338 339 /** 340 * {@inheritDoc} 341 */ 342 @Override 343 public void deleteAttachment( Attachment att ) throws ProviderException 344 { 345 m_cache.remove(att.getParentName()); 346 m_attCache.remove(att.getName()); 347 m_provider.deleteAttachment( att ); 348 } 349 350 351 /** 352 * Gets the provider class name, and cache statistics (misscount and,hitcount of the attachment cache). 353 * 354 * @return A plain string with all the above mentioned values. 355 */ 356 @Override 357 public synchronized String getProviderInfo() 358 { 359 return "Real provider: "+m_provider.getClass().getName()+ 360 ". Cache misses: "+m_cacheMisses+ 361 ". Cache hits: "+m_cacheHits; 362 } 363 364 /** 365 * Returns the WikiAttachmentProvider that this caching provider delegates to. 366 * 367 * @return The real provider underneath this one. 368 */ 369 public WikiAttachmentProvider getRealProvider() 370 { 371 return m_provider; 372 } 373 374 /** 375 * {@inheritDoc} 376 */ 377 @Override 378 public void moveAttachmentsForPage( String oldParent, String newParent ) throws ProviderException 379 { 380 m_provider.moveAttachmentsForPage(oldParent, newParent); 381 m_cache.remove(newParent); 382 m_cache.remove(oldParent); 383 384 // 385 // This is a kludge to make sure that the pages are removed 386 // from the other cache as well. 387 // 388 String checkName = oldParent + "/"; 389 390 @SuppressWarnings("unchecked") 391 List< String > names = m_cache.getKeysWithExpiryCheck(); 392 for( String name : names ) 393 { 394 if( name.startsWith( checkName ) ) 395 { 396 m_attCache.remove(name); 397 } 398 } 399 } 400 401}