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}