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