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}