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 org.apache.logging.log4j.LogManager; 022import org.apache.logging.log4j.Logger; 023import org.apache.wiki.api.core.Attachment; 024import org.apache.wiki.api.core.Engine; 025import org.apache.wiki.api.core.Page; 026import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 027import org.apache.wiki.api.exceptions.ProviderException; 028import org.apache.wiki.api.providers.AttachmentProvider; 029import org.apache.wiki.api.providers.PageProvider; 030import org.apache.wiki.api.providers.WikiProvider; 031import org.apache.wiki.api.search.QueryItem; 032import org.apache.wiki.attachment.AttachmentManager; 033import org.apache.wiki.cache.CacheInfo; 034import org.apache.wiki.cache.CachingManager; 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.Collections; 043import java.util.Date; 044import java.util.List; 045import java.util.NoSuchElementException; 046import java.util.Properties; 047import java.util.concurrent.atomic.AtomicBoolean; 048import java.util.concurrent.atomic.AtomicLong; 049 050 051/** 052 * Provides a caching attachment provider. This class rests on top of a real provider class and provides a cache to speed things up. 053 * Only the Attachment objects are cached; the actual attachment contents are fetched always from the provider. 054 * 055 * @since 2.1.64. 056 */ 057public class CachingAttachmentProvider implements AttachmentProvider { 058 059 private static final Logger LOG = LogManager.getLogger( CachingAttachmentProvider.class ); 060 061 private AttachmentProvider provider; 062 private CachingManager cachingManager; 063 private final AtomicBoolean allRequested = new AtomicBoolean(); 064 private final AtomicLong attachments = new AtomicLong( 0L ); 065 066 /** 067 * {@inheritDoc} 068 */ 069 @Override 070 public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException { 071 LOG.info( "Initing CachingAttachmentProvider" ); 072 cachingManager = engine.getManager( CachingManager.class ); 073 cachingManager.registerListener( CachingManager.CACHE_ATTACHMENTS, "expired", allRequested ); 074 075 // Find and initialize real provider. 076 final String classname; 077 try { 078 classname = TextUtil.getRequiredProperty( properties, AttachmentManager.PROP_PROVIDER, AttachmentManager.PROP_PROVIDER_DEPRECATED ); 079 } catch( final NoSuchElementException e ) { 080 throw new NoRequiredPropertyException( e.getMessage(), AttachmentManager.PROP_PROVIDER ); 081 } 082 083 try { 084 provider = ClassUtil.buildInstance( "org.apache.wiki.providers", classname ); 085 LOG.debug( "Initializing real provider class {}", provider ); 086 provider.initialize( engine, properties ); 087 } catch( final ReflectiveOperationException e ) { 088 LOG.error( "Unable to instantiate provider class {}", classname, e ); 089 throw new IllegalArgumentException( "illegal provider class", e ); 090 } 091 } 092 093 /** 094 * {@inheritDoc} 095 */ 096 @Override 097 public void putAttachmentData( final Attachment att, final InputStream data ) throws ProviderException, IOException { 098 provider.putAttachmentData( att, data ); 099 cachingManager.remove( CachingManager.CACHE_ATTACHMENTS_COLLECTION, att.getParentName() ); 100 att.setLastModified( new Date() ); 101 cachingManager.put( CachingManager.CACHE_ATTACHMENTS, att.getName(), att ); 102 attachments.incrementAndGet(); 103 } 104 105 /** 106 * {@inheritDoc} 107 */ 108 @Override 109 public InputStream getAttachmentData( final Attachment att ) throws ProviderException, IOException { 110 return provider.getAttachmentData( att ); 111 } 112 113 /** 114 * {@inheritDoc} 115 */ 116 @Override 117 public List< Attachment > listAttachments( final Page page ) throws ProviderException { 118 LOG.debug( "Listing attachments for {}", page ); 119 final List< Attachment > atts = cachingManager.get( CachingManager.CACHE_ATTACHMENTS_COLLECTION, page.getName(), 120 () -> provider.listAttachments( page ) ); 121 return cloneCollection( atts ); 122 } 123 124 private < T > List< T > cloneCollection( final Collection< T > c ) { 125 return c != null ? new ArrayList<>( c ) : Collections.emptyList(); 126 } 127 128 /** 129 * {@inheritDoc} 130 */ 131 @Override 132 public Collection< Attachment > findAttachments( final QueryItem[] query ) { 133 return provider.findAttachments( query ); 134 } 135 136 /** 137 * {@inheritDoc} 138 */ 139 @Override 140 public List< Attachment > listAllChanged( final Date timestamp ) throws ProviderException { 141 final List< Attachment > all; 142 if ( !allRequested.get() ) { 143 all = provider.listAllChanged( timestamp ); 144 145 // Make sure that all attachments are in the cache. 146 synchronized( this ) { 147 for( final Attachment att : all ) { 148 cachingManager.put( CachingManager.CACHE_ATTACHMENTS, att.getName(), att ); 149 } 150 if( timestamp.getTime() == 0L ) { // all attachments requested 151 allRequested.set( true ); 152 attachments.set( all.size() ); 153 } 154 } 155 } else { 156 final List< String > keys = cachingManager.keys( CachingManager.CACHE_ATTACHMENTS ); 157 all = new ArrayList<>(); 158 for( final String key : keys) { 159 final Attachment cachedAttachment = cachingManager.get( CachingManager.CACHE_ATTACHMENTS, key, () -> null ); 160 if( cachedAttachment != null ) { 161 all.add( cachedAttachment ); 162 } 163 } 164 } 165 166 if( cachingManager.enabled( CachingManager.CACHE_ATTACHMENTS ) 167 && attachments.get() >= cachingManager.info( CachingManager.CACHE_ATTACHMENTS ).getMaxElementsAllowed() ) { 168 LOG.warn( "seems {} can't hold all attachments from your page repository, " + 169 "so we're delegating on the underlying provider instead. Please consider increasing " + 170 "your cache sizes on the ehcache configuration file to avoid this behaviour", CachingManager.CACHE_ATTACHMENTS ); 171 return provider.listAllChanged( timestamp ); 172 } 173 174 return all; 175 } 176 177 /** 178 * Simply goes through the collection and attempts to locate the 179 * given attachment of that name. 180 * 181 * @return null, if no such attachment was in this collection. 182 */ 183 private Attachment findAttachmentFromCollection( final Collection< Attachment > c, final String name ) { 184 if( c != null ) { 185 return c.stream().filter(att -> name.equals(att.getFileName())).findFirst().orElse(null); 186 } 187 188 return null; 189 } 190 191 /** 192 * {@inheritDoc} 193 */ 194 @Override 195 public Attachment getAttachmentInfo( final Page page, final String name, final int version ) throws ProviderException { 196 LOG.debug( "Getting attachments for {}, name={}, version={}", page, name, version ); 197 // We don't cache previous versions 198 if( version != WikiProvider.LATEST_VERSION ) { 199 LOG.debug( "...we don't cache old versions" ); 200 return provider.getAttachmentInfo( page, name, version ); 201 } 202 final Collection< Attachment > c = cachingManager.get( CachingManager.CACHE_ATTACHMENTS_COLLECTION, page.getName(), 203 ()-> provider.listAttachments( page ) ); 204 return findAttachmentFromCollection( c, name ); 205 } 206 207 /** 208 * {@inheritDoc} 209 */ 210 @Override 211 public List< Attachment > getVersionHistory( final Attachment att ) { 212 return provider.getVersionHistory( att ); 213 } 214 215 /** 216 * {@inheritDoc} 217 */ 218 @Override 219 public void deleteVersion( final Attachment att ) throws ProviderException { 220 // This isn't strictly speaking correct, but it does not really matter 221 cachingManager.remove( CachingManager.CACHE_ATTACHMENTS_COLLECTION, att.getParentName() ); 222 provider.deleteVersion( att ); 223 if( att.getVersion() == PageProvider.LATEST_VERSION ) { 224 attachments.decrementAndGet(); 225 } 226 } 227 228 /** 229 * {@inheritDoc} 230 */ 231 @Override 232 public void deleteAttachment( final Attachment att ) throws ProviderException { 233 cachingManager.remove( CachingManager.CACHE_ATTACHMENTS_COLLECTION, att.getParentName() ); 234 cachingManager.remove( CachingManager.CACHE_ATTACHMENTS, att.getName() ); 235 provider.deleteAttachment( att ); 236 attachments.decrementAndGet(); 237 } 238 239 /** 240 * Gets the provider class name, and cache statistics (misscount and hitcount of the attachment cache). 241 * 242 * @return A plain string with all the above-mentioned values. 243 */ 244 @Override 245 public synchronized String getProviderInfo() { 246 final CacheInfo attCacheInfo = cachingManager.info( CachingManager.CACHE_ATTACHMENTS ); 247 final CacheInfo attColCacheInfo = cachingManager.info( CachingManager.CACHE_ATTACHMENTS_COLLECTION ); 248 return "Real provider: " + provider.getClass().getName() + 249 ". Attachment cache hits: " + attCacheInfo.getHits() + 250 ". Attachment cache misses: " + attCacheInfo.getMisses() + 251 ". Attachment collection cache hits: " + attColCacheInfo.getHits() + 252 ". Attachment collection cache misses: " + attColCacheInfo.getMisses(); 253 } 254 255 /** 256 * Returns the WikiAttachmentProvider that this caching provider delegates to. 257 * 258 * @return The real provider underneath this one. 259 */ 260 public AttachmentProvider getRealProvider() { 261 return provider; 262 } 263 264 /** 265 * {@inheritDoc} 266 */ 267 @Override 268 public void moveAttachmentsForPage( final String oldParent, final String newParent ) throws ProviderException { 269 provider.moveAttachmentsForPage( oldParent, newParent ); 270 cachingManager.remove( CachingManager.CACHE_ATTACHMENTS_COLLECTION, newParent ); 271 cachingManager.remove( CachingManager.CACHE_ATTACHMENTS_COLLECTION, oldParent ); 272 273 // This is a kludge to make sure that the pages are removed from the other cache as well. 274 final String checkName = oldParent + "/"; 275 final List< String > names = cachingManager.keys( CachingManager.CACHE_ATTACHMENTS_COLLECTION ); 276 for( final String name : names ) { 277 if( name.startsWith( checkName ) ) { 278 cachingManager.remove( CachingManager.CACHE_ATTACHMENTS, name ); 279 } 280 } 281 } 282 283}