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            for( final Attachment att : c ) {
184                if( name.equals( att.getFileName() ) ) {
185                    return att;
186                }
187            }
188        }
189
190        return null;
191    }
192
193    /**
194     * {@inheritDoc}
195     */
196    @Override
197    public Attachment getAttachmentInfo( final Page page, final String name, final int version ) throws ProviderException {
198        LOG.debug( "Getting attachments for {}, name={}, version={}", page, name, version );
199        //  We don't cache previous versions
200        if( version != WikiProvider.LATEST_VERSION ) {
201            LOG.debug( "...we don't cache old versions" );
202            return provider.getAttachmentInfo( page, name, version );
203        }
204        final Collection< Attachment > c = cachingManager.get( CachingManager.CACHE_ATTACHMENTS_COLLECTION, page.getName(),
205                                                               ()-> provider.listAttachments( page ) );
206        return findAttachmentFromCollection( c, name );
207    }
208
209    /**
210     * {@inheritDoc}
211     */
212    @Override
213    public List< Attachment > getVersionHistory( final Attachment att ) {
214        return provider.getVersionHistory( att );
215    }
216
217    /**
218     * {@inheritDoc}
219     */
220    @Override
221    public void deleteVersion( final Attachment att ) throws ProviderException {
222        // This isn't strictly speaking correct, but it does not really matter
223        cachingManager.remove( CachingManager.CACHE_ATTACHMENTS_COLLECTION, att.getParentName() );
224        provider.deleteVersion( att );
225        if( att.getVersion() == PageProvider.LATEST_VERSION ) {
226            attachments.decrementAndGet();
227        }
228    }
229
230    /**
231     * {@inheritDoc}
232     */
233    @Override
234    public void deleteAttachment( final Attachment att ) throws ProviderException {
235        cachingManager.remove( CachingManager.CACHE_ATTACHMENTS_COLLECTION, att.getParentName() );
236        cachingManager.remove( CachingManager.CACHE_ATTACHMENTS, att.getName() );
237        provider.deleteAttachment( att );
238        attachments.decrementAndGet();
239    }
240
241    /**
242     * Gets the provider class name, and cache statistics (misscount and hitcount of the attachment cache).
243     *
244     * @return A plain string with all the above-mentioned values.
245     */
246    @Override
247    public synchronized String getProviderInfo() {
248        final CacheInfo attCacheInfo = cachingManager.info( CachingManager.CACHE_ATTACHMENTS );
249        final CacheInfo attColCacheInfo = cachingManager.info( CachingManager.CACHE_ATTACHMENTS_COLLECTION );
250        return "Real provider: " + provider.getClass().getName() +
251                ". Attachment cache hits: " + attCacheInfo.getHits() +
252                ". Attachment cache misses: " + attCacheInfo.getMisses() +
253                ". Attachment collection cache hits: " + attColCacheInfo.getHits() +
254                ". Attachment collection cache misses: " + attColCacheInfo.getMisses();
255    }
256
257    /**
258     *  Returns the WikiAttachmentProvider that this caching provider delegates to.
259     *
260     *  @return The real provider underneath this one.
261     */
262    public AttachmentProvider getRealProvider() {
263        return provider;
264    }
265
266    /**
267     * {@inheritDoc}
268     */
269    @Override
270    public void moveAttachmentsForPage( final String oldParent, final String newParent ) throws ProviderException {
271        provider.moveAttachmentsForPage( oldParent, newParent );
272        cachingManager.remove( CachingManager.CACHE_ATTACHMENTS_COLLECTION, newParent );
273        cachingManager.remove( CachingManager.CACHE_ATTACHMENTS_COLLECTION, oldParent );
274
275        // This is a kludge to make sure that the pages are removed from the other cache as well.
276        final String checkName = oldParent + "/";
277        final List< String > names = cachingManager.keys( CachingManager.CACHE_ATTACHMENTS_COLLECTION );
278        for( final String name : names ) {
279            if( name.startsWith( checkName ) ) {
280                cachingManager.remove( CachingManager.CACHE_ATTACHMENTS, name );
281            }
282        }
283    }
284
285}