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