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