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}