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