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