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.attachment;
020
021import net.sf.ehcache.Cache;
022import net.sf.ehcache.CacheManager;
023import net.sf.ehcache.Element;
024import org.apache.log4j.Logger;
025import org.apache.wiki.api.core.Attachment;
026import org.apache.wiki.api.core.Context;
027import org.apache.wiki.api.core.Engine;
028import org.apache.wiki.api.core.Page;
029import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
030import org.apache.wiki.api.exceptions.ProviderException;
031import org.apache.wiki.api.providers.AttachmentProvider;
032import org.apache.wiki.api.spi.Wiki;
033import org.apache.wiki.pages.PageManager;
034import org.apache.wiki.parser.MarkupParser;
035import org.apache.wiki.references.ReferenceManager;
036import org.apache.wiki.search.SearchManager;
037import org.apache.wiki.util.ClassUtil;
038import org.apache.wiki.util.TextUtil;
039
040import java.io.IOException;
041import java.io.InputStream;
042import java.util.ArrayList;
043import java.util.Collection;
044import java.util.Comparator;
045import java.util.Date;
046import java.util.List;
047import java.util.Properties;
048
049
050/**
051 *  Default implementation for {@link AttachmentManager}
052 *
053 * {@inheritDoc}
054 *
055 *  @since 1.9.28
056 */
057public class DefaultAttachmentManager implements AttachmentManager {
058
059    /** List of attachment types which are forced to be downloaded */
060    private String[] m_forceDownloadPatterns;
061
062    private static final Logger log = Logger.getLogger( DefaultAttachmentManager.class );
063    private AttachmentProvider m_provider;
064    private Engine m_engine;
065    private CacheManager m_cacheManager = CacheManager.getInstance();
066    private Cache m_dynamicAttachments;
067
068    /**
069     *  Creates a new AttachmentManager.  Note that creation will never fail, but it's quite likely that attachments do not function.
070     *  <p><b>DO NOT CREATE</b> an AttachmentManager on your own, unless you really know what you're doing. Just use
071     *  Wikiengine.getManager( AttachmentManager.class ) if you're making a module for JSPWiki.
072     *
073     *  @param engine The wikiengine that owns this attachment manager.
074     *  @param props A list of properties from which the AttachmentManager will seek its configuration. Typically this is the "jspwiki.properties".
075     */
076    // FIXME: Perhaps this should fail somehow.
077    public DefaultAttachmentManager( final Engine engine, final Properties props ) {
078        m_engine = engine;
079
080        //  If user wants to use a cache, then we'll use the CachingProvider.
081        final boolean useCache = "true".equals( props.getProperty( PageManager.PROP_USECACHE ) );
082
083        final String classname;
084        if( useCache ) {
085            classname = "org.apache.wiki.providers.CachingAttachmentProvider";
086        } else {
087            classname = props.getProperty( PROP_PROVIDER );
088        }
089
090        //  If no class defined, then will just simply fail.
091        if( classname == null ) {
092            log.info( "No attachment provider defined - disabling attachment support." );
093            return;
094        }
095
096        //  Create and initialize the provider.
097        final String cacheName = engine.getApplicationName() + "." + CACHE_NAME;
098        try {
099            if( m_cacheManager.cacheExists( cacheName ) ) {
100                m_dynamicAttachments = m_cacheManager.getCache( cacheName );
101            } else {
102                log.info( "cache with name " + cacheName + " not found in ehcache.xml, creating it with defaults." );
103                m_dynamicAttachments = new Cache( cacheName, DEFAULT_CACHECAPACITY, false, false, 0, 0 );
104                m_cacheManager.addCache( m_dynamicAttachments );
105            }
106
107            final Class< ? > providerclass = ClassUtil.findClass( "org.apache.wiki.providers", classname );
108
109            m_provider = ( AttachmentProvider )providerclass.newInstance();
110            m_provider.initialize( m_engine, props );
111        } catch( final ClassNotFoundException e ) {
112            log.error( "Attachment provider class not found",e);
113        } catch( final InstantiationException e ) {
114            log.error( "Attachment provider could not be created", e );
115        } catch( final IllegalAccessException e ) {
116            log.error( "You may not access the attachment provider class", e );
117        } catch( final NoRequiredPropertyException e ) {
118            log.error( "Attachment provider did not find a property that it needed: " + e.getMessage(), e );
119            m_provider = null; // No, it did not work.
120        } catch( final IOException e ) {
121            log.error( "Attachment provider reports IO error", e );
122            m_provider = null;
123        }
124
125        final String forceDownload = TextUtil.getStringProperty( props, PROP_FORCEDOWNLOAD, null );
126        if( forceDownload != null && forceDownload.length() > 0 ) {
127            m_forceDownloadPatterns = forceDownload.toLowerCase().split( "\\s" );
128        } else {
129            m_forceDownloadPatterns = new String[ 0 ];
130        }
131    }
132
133    /** {@inheritDoc} */
134    @Override
135    public boolean attachmentsEnabled() {
136        return m_provider != null;
137    }
138
139    /** {@inheritDoc} */
140    @Override
141    public String getAttachmentInfoName( final Context context, final String attachmentname ) {
142        final Attachment att;
143        try {
144            att = getAttachmentInfo( context, attachmentname );
145        } catch( final ProviderException e ) {
146            log.warn( "Finding attachments failed: ", e );
147            return null;
148        }
149
150        if( att != null ) {
151            return att.getName();
152        } else if( attachmentname.indexOf( '/' ) != -1 ) {
153            return attachmentname;
154        }
155
156        return null;
157    }
158
159    /** {@inheritDoc} */
160    @Override
161    public Attachment getAttachmentInfo( final Context context, String attachmentname, final int version ) throws ProviderException {
162        if( m_provider == null ) {
163            return null;
164        }
165
166        Page currentPage = null;
167
168        if( context != null ) {
169            currentPage = context.getPage();
170        }
171
172        //  Figure out the parent page of this attachment.  If we can't find it, we'll assume this refers directly to the attachment.
173        final int cutpt = attachmentname.lastIndexOf( '/' );
174        if( cutpt != -1 ) {
175            String parentPage = attachmentname.substring( 0, cutpt );
176            parentPage = MarkupParser.cleanLink( parentPage );
177            attachmentname = attachmentname.substring( cutpt + 1 );
178
179            // If we for some reason have an empty parent page name; this can't be an attachment
180            if( parentPage.length() == 0 ) {
181                return null;
182            }
183
184            currentPage = m_engine.getManager( PageManager.class ).getPage( parentPage );
185
186            // Go check for legacy name
187            // FIXME: This should be resolved using CommandResolver, not this adhoc way.  This also assumes that the
188            //        legacy charset is a subset of the full allowed set.
189            if( currentPage == null ) {
190                currentPage = m_engine.getManager( PageManager.class ).getPage( MarkupParser.wikifyLink( parentPage ) );
191            }
192        }
193
194        //  If the page cannot be determined, we cannot possibly find the attachments.
195        if( currentPage == null || currentPage.getName().length() == 0 ) {
196            return null;
197        }
198
199        //  Finally, figure out whether this is a real attachment or a generated attachment.
200        Attachment att = getDynamicAttachment( currentPage.getName() + "/" + attachmentname );
201        if( att == null ) {
202            att = m_provider.getAttachmentInfo( currentPage, attachmentname, version );
203        }
204
205        return att;
206    }
207
208    /** {@inheritDoc} */
209    @Override
210    public List< Attachment > listAttachments( final Page wikipage ) throws ProviderException {
211        if( m_provider == null ) {
212            return new ArrayList<>();
213        }
214
215        final List< Attachment > atts = new ArrayList<>( m_provider.listAttachments( wikipage ) );
216        atts.sort( Comparator.comparing( Attachment::getName, m_engine.getManager( PageManager.class ).getPageSorter() ) );
217
218        return atts;
219    }
220
221    /** {@inheritDoc} */
222    @Override
223    public boolean forceDownload( String name ) {
224        if( name == null || name.length() == 0 ) {
225            return false;
226        }
227
228        name = name.toLowerCase();
229        if( name.indexOf( '.' ) == -1 ) {
230            return true;  // force download on attachments without extension or type indication
231        }
232
233        for( final String forceDownloadPattern : m_forceDownloadPatterns ) {
234            if( name.endsWith( forceDownloadPattern ) && forceDownloadPattern.length() > 0 ) {
235                return true;
236            }
237        }
238
239        return false;
240    }
241
242    /** {@inheritDoc} */
243    @Override
244    public InputStream getAttachmentStream( final Context ctx, final Attachment att ) throws ProviderException, IOException {
245        if( m_provider == null ) {
246            return null;
247        }
248
249        if( att instanceof DynamicAttachment ) {
250            return ( ( DynamicAttachment )att ).getProvider().getAttachmentData( ctx, att );
251        }
252
253        return m_provider.getAttachmentData( att );
254    }
255
256    /** {@inheritDoc} */
257    @Override
258    public void storeDynamicAttachment( final Context ctx, final DynamicAttachment att ) {
259        m_dynamicAttachments.put( new Element( att.getName(), att ) );
260    }
261
262    /** {@inheritDoc} */
263    @Override
264    public DynamicAttachment getDynamicAttachment( final String name ) {
265        final Element element = m_dynamicAttachments.get( name );
266        if( element != null ) {
267            return ( DynamicAttachment )element.getObjectValue();
268        } else {
269            // Remove from cache, it has expired.
270            m_dynamicAttachments.put( new Element( name, null ) );
271            return null;
272        }
273    }
274
275    /** {@inheritDoc} */
276    @Override
277    public void storeAttachment( final Attachment att, final InputStream in ) throws IOException, ProviderException {
278        if( m_provider == null ) {
279            return;
280        }
281
282        // Checks if the actual, real page exists without any modifications or aliases. We cannot store an attachment to a non-existent page.
283        if( !m_engine.getManager( PageManager.class ).pageExists( att.getParentName() ) ) {
284            // the caller should catch the exception and use the exception text as an i18n key
285            throw new ProviderException( "attach.parent.not.exist" );
286        }
287
288        m_provider.putAttachmentData( att, in );
289        m_engine.getManager( ReferenceManager.class ).updateReferences( att.getName(), new ArrayList<>() );
290
291        final Page parent = Wiki.contents().page( m_engine, att.getParentName() );
292        m_engine.getManager( ReferenceManager.class ).updateReferences( parent );
293        m_engine.getManager( SearchManager.class ).reindexPage( att );
294    }
295
296    /** {@inheritDoc} */
297    @Override
298    public List< Attachment > getVersionHistory( final String attachmentName ) throws ProviderException {
299        if( m_provider == null ) {
300            return null;
301        }
302
303        final Attachment att = getAttachmentInfo( null, attachmentName );
304        if( att != null ) {
305            return m_provider.getVersionHistory( att );
306        }
307
308        return null;
309    }
310
311    /** {@inheritDoc} */
312    @Override
313    public Collection<Attachment> getAllAttachments() throws ProviderException {
314        if( attachmentsEnabled() ) {
315            return m_provider.listAllChanged( new Date( 0L ) );
316        }
317
318        return new ArrayList<>();
319    }
320
321    /** {@inheritDoc} */
322    @Override
323    public AttachmentProvider getCurrentProvider() {
324        return m_provider;
325    }
326
327    /** {@inheritDoc} */
328    @Override
329    public void deleteVersion( final Attachment att ) throws ProviderException {
330        if( m_provider == null ) {
331            return;
332        }
333
334        m_provider.deleteVersion( att );
335    }
336
337    /** {@inheritDoc} */
338    @Override
339    // FIXME: Should also use events!
340    public void deleteAttachment( final Attachment att ) throws ProviderException {
341        if( m_provider == null ) {
342            return;
343        }
344
345        m_provider.deleteAttachment( att );
346        m_engine.getManager( SearchManager.class ).pageRemoved( att );
347        m_engine.getManager( ReferenceManager.class ).clearPageEntries( att.getName() );
348    }
349
350}