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 java.io.File;
022import java.io.FileInputStream;
023import java.io.FileNotFoundException;
024import java.io.FileOutputStream;
025import java.io.FilenameFilter;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStream;
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.Date;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Properties;
036import java.util.regex.Matcher;
037import java.util.regex.Pattern;
038
039import org.apache.commons.io.IOUtils;
040import org.apache.log4j.Logger;
041import org.apache.wiki.WikiEngine;
042import org.apache.wiki.WikiPage;
043import org.apache.wiki.WikiProvider;
044import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
045import org.apache.wiki.api.exceptions.ProviderException;
046import org.apache.wiki.attachment.Attachment;
047import org.apache.wiki.search.QueryItem;
048import org.apache.wiki.util.FileUtil;
049import org.apache.wiki.util.TextUtil;
050import org.apache.wiki.util.comparators.PageTimeComparator;
051
052/**
053 *  Provides basic, versioning attachments.
054 *
055 *  <PRE>
056 *   Structure is as follows:
057 *      attachment_dir/
058 *         ThisPage/
059 *            attachment.doc/
060 *               attachment.properties
061 *               1.doc
062 *               2.doc
063 *               3.doc
064 *            picture.png/
065 *               attachment.properties
066 *               1.png
067 *               2.png
068 *         ThatPage/
069 *            picture.png/
070 *               attachment.properties
071 *               1.png
072 *             
073 *  </PRE>
074 *
075 *  The names of the directories will be URLencoded.
076 *  <p>
077 *  "attachment.properties" consists of the following items:
078 *  <UL>
079 *   <LI>1.author = author name for version 1 (etc)
080 *  </UL>
081 */
082public class BasicAttachmentProvider
083    implements WikiAttachmentProvider
084{
085    private WikiEngine         m_engine;
086    private String             m_storageDir;
087    
088    /** The property name for where the attachments should be stored.  Value is <tt>{@value}</tt>. */
089    public static final String PROP_STORAGEDIR = "jspwiki.basicAttachmentProvider.storageDir";
090    
091    /*
092     * Disable client cache for files with patterns
093     * since 2.5.96
094     */
095    private Pattern            m_disableCache = null;
096    
097    /** The property name for specifying which attachments are not cached.  Value is <tt>{@value}</tt>. */
098    public static final String PROP_DISABLECACHE = "jspwiki.basicAttachmentProvider.disableCache";
099
100    /** The name of the property file. */
101    public static final String PROPERTY_FILE   = "attachment.properties";
102
103    /** The default extension for the page attachment directory name. */
104    public static final String DIR_EXTENSION   = "-att";
105    
106    /** The default extension for the attachment directory. */
107    public static final String ATTDIR_EXTENSION = "-dir";
108    
109    static final Logger log = Logger.getLogger( BasicAttachmentProvider.class );
110
111    /**
112     *  {@inheritDoc}
113     */
114    public void initialize( WikiEngine engine, Properties properties ) 
115        throws NoRequiredPropertyException,
116               IOException
117    {
118        m_engine = engine;
119        m_storageDir = TextUtil.getCanonicalFilePathProperty(properties, PROP_STORAGEDIR,
120                System.getProperty("user.home") + File.separator + "jspwiki-files");
121
122        String patternString = engine.getWikiProperties().getProperty( PROP_DISABLECACHE );
123        if ( patternString != null )
124        {
125            m_disableCache = Pattern.compile(patternString);
126        }
127
128        //
129        //  Check if the directory exists - if it doesn't, create it.
130        //
131        File f = new File( m_storageDir );
132
133        if( !f.exists() )
134        {
135            f.mkdirs();
136        }
137
138        //
139        // Some sanity checks
140        //
141        if( !f.exists() ) 
142            throw new IOException("Could not find or create attachment storage directory '"+m_storageDir+"'");
143
144        if( !f.canWrite() ) 
145            throw new IOException("Cannot write to the attachment storage directory '"+m_storageDir+"'");
146        
147        if( !f.isDirectory() )
148            throw new IOException("Your attachment storage points to a file, not a directory: '"+m_storageDir+"'");
149    }
150
151    /**
152     *  Finds storage dir, and if it exists, makes sure that it is valid.
153     *
154     *  @param wikipage Page to which this attachment is attached.
155     */
156    private File findPageDir( String wikipage )
157        throws ProviderException
158    {
159        wikipage = mangleName( wikipage );
160
161        File f = new File( m_storageDir, wikipage+DIR_EXTENSION );
162
163        if( f.exists() && !f.isDirectory() )
164        {
165            throw new ProviderException("Storage dir '"+f.getAbsolutePath()+"' is not a directory!");
166        }
167
168        return f;
169    }
170
171    private static String mangleName( String wikiname )
172    {
173        String res = TextUtil.urlEncodeUTF8( wikiname );
174
175        return res;
176    }
177
178    private static String unmangleName( String filename )
179    {
180        return TextUtil.urlDecodeUTF8( filename );
181    }
182    
183    /**
184     *  Finds the dir in which the attachment lives.
185     */
186    private File findAttachmentDir( Attachment att )
187        throws ProviderException
188    {
189        File f = new File( findPageDir(att.getParentName()), 
190                           mangleName(att.getFileName()+ATTDIR_EXTENSION) );
191
192        //
193        //  Migration code for earlier versions of JSPWiki.
194        //  Originally, we used plain filename.  Then we realized we need
195        //  to urlencode it.  Then we realized that we have to use a
196        //  postfix to make sure illegal file names are never formed.
197        //
198        if( !f.exists() )
199        {
200            File oldf = new File( findPageDir( att.getParentName() ),
201                                  mangleName( att.getFileName() ) );
202            if( oldf.exists() )
203            {
204                f = oldf;
205            }
206            else
207            {
208                oldf = new File( findPageDir( att.getParentName() ),
209                                 att.getFileName() );
210
211                if( oldf.exists() )
212                {
213                    f = oldf;
214                }
215            }
216        }
217
218        return f;
219    }
220
221    /**
222     *  Goes through the repository and decides which version is
223     *  the newest one in that directory.
224     *
225     *  @return Latest version number in the repository, or 0, if
226     *          there is no page in the repository.
227     */
228    private int findLatestVersion( Attachment att )
229        throws ProviderException
230    {
231        // File pageDir = findPageDir( att.getName() );
232        File attDir  = findAttachmentDir( att );
233
234        // log.debug("Finding pages in "+attDir.getAbsolutePath());
235        String[] pages = attDir.list( new AttachmentVersionFilter() );
236
237        if( pages == null )
238        {
239            return 0; // No such thing found.
240        }
241
242        int version = 0;
243
244        for( int i = 0; i < pages.length; i++ )
245        {
246            // log.debug("Checking: "+pages[i]);
247            int cutpoint = pages[i].indexOf( '.' );
248                String pageNum = ( cutpoint > 0 ) ? pages[i].substring( 0, cutpoint ) : pages[i] ;
249
250                try
251                {
252                    int res = Integer.parseInt( pageNum );
253
254                    if( res > version )
255                    {
256                        version = res;
257                    }
258                }
259                catch( NumberFormatException e ) {} // It's okay to skip these.
260            }
261
262        return version;
263    }
264
265    /**
266     *  Returns the file extension.  For example "test.png" returns "png".
267     *  <p>
268     *  If file has no extension, will return "bin"
269     *  
270     *  @param filename The file name to check
271     *  @return The extension.  If no extension is found, returns "bin".
272     */
273    protected static String getFileExtension( String filename )
274    {
275        String fileExt = "bin";
276
277        int dot = filename.lastIndexOf('.');
278        if( dot >= 0 && dot < filename.length()-1 )
279        {
280            fileExt = mangleName( filename.substring( dot+1 ) );
281        }
282
283        return fileExt;
284    }
285
286    /**
287     *  Writes the page properties back to the file system.
288     *  Note that it WILL overwrite any previous properties.
289     */
290    private void putPageProperties( Attachment att, Properties properties ) throws IOException, ProviderException {
291        File attDir = findAttachmentDir( att );
292        File propertyFile = new File( attDir, PROPERTY_FILE );
293
294        OutputStream out = null;
295        
296        try {
297            out = new FileOutputStream( propertyFile );
298            properties.store( out, " JSPWiki page properties for " + att.getName() + ". DO NOT MODIFY!" );
299        } catch ( IOException ioe ) {
300            IOUtils.closeQuietly( out );
301            throw ioe;
302        } finally {
303            IOUtils.closeQuietly( out );
304        }
305    }
306
307    /**
308     *  Reads page properties from the file system.
309     */
310    private Properties getPageProperties( Attachment att ) throws IOException, ProviderException {
311        Properties props = new Properties();
312
313        File propertyFile = new File( findAttachmentDir(att), PROPERTY_FILE );
314
315        if( propertyFile.exists() ) {
316            InputStream in = null;
317            try {
318                in = new FileInputStream( propertyFile );
319                props.load( in );
320            } catch ( IOException ioe ) {
321                IOUtils.closeQuietly( in );
322                throw ioe;
323            } finally {
324                IOUtils.closeQuietly( in );
325            }
326        }
327        
328        return props;
329    }
330
331    /**
332     *  {@inheritDoc}
333     */
334    public void putAttachmentData( Attachment att, InputStream data ) throws ProviderException, IOException {
335        OutputStream out = null;
336        File attDir = findAttachmentDir( att );
337
338        if(!attDir.exists())
339        {
340            attDir.mkdirs();
341        }
342
343        int latestVersion = findLatestVersion( att );
344
345        // System.out.println("Latest version is "+latestVersion);
346
347        try
348        {
349            int versionNumber = latestVersion+1;
350
351            File newfile = new File( attDir, versionNumber+"."+
352                                     getFileExtension(att.getFileName()) );
353
354            log.info("Uploading attachment "+att.getFileName()+" to page "+att.getParentName());
355            log.info("Saving attachment contents to "+newfile.getAbsolutePath());
356            out = new FileOutputStream(newfile);
357
358            FileUtil.copyContents( data, out );
359
360            Properties props = getPageProperties( att );
361
362            String author = att.getAuthor();
363
364            if( author == null )
365            {
366                author = "unknown"; // FIXME: Should be localized, but cannot due to missing WikiContext
367            }
368
369            props.setProperty( versionNumber+".author", author );
370            
371            String changeNote = (String)att.getAttribute(WikiPage.CHANGENOTE);
372            if( changeNote != null )
373            {
374                props.setProperty( versionNumber+".changenote", changeNote );
375            }
376            
377            putPageProperties( att, props );
378        }
379        catch( IOException e )
380        {
381            log.error( "Could not save attachment data: ", e );
382            IOUtils.closeQuietly( out );
383            throw (IOException) e.fillInStackTrace();
384        }
385        finally
386        {
387            IOUtils.closeQuietly( out );
388        }
389    }
390
391    /**
392     *  {@inheritDoc}
393     */
394    public String getProviderInfo()
395    {
396        return "";
397    }
398
399    private File findFile( File dir, Attachment att )
400        throws FileNotFoundException,
401               ProviderException
402    {
403        int version = att.getVersion();
404
405        if( version == WikiProvider.LATEST_VERSION )
406        {
407            version = findLatestVersion( att );
408        }
409
410        String ext = getFileExtension( att.getFileName() );
411        File f = new File( dir, version+"."+ext );
412
413        if( !f.exists() )
414        {
415            if ("bin".equals(ext))
416            {
417                File fOld = new File( dir, version+"." );
418                if (fOld.exists())
419                    f = fOld;
420            }
421            if( !f.exists() )
422            {
423                throw new FileNotFoundException("No such file: "+f.getAbsolutePath()+" exists.");
424            }
425        }
426
427        return f;
428    }
429
430    /**
431     *  {@inheritDoc}
432     */
433    public InputStream getAttachmentData( Attachment att )
434        throws IOException,
435               ProviderException
436    {
437        File attDir = findAttachmentDir( att );
438
439        try
440        {
441            File f = findFile( attDir, att );
442
443            return new FileInputStream( f );
444        }
445        catch( FileNotFoundException e )
446        {
447            log.error("File not found: "+e.getMessage());
448            throw new ProviderException("No such page was found.");
449        }
450    }
451
452    /**
453     *  {@inheritDoc}
454     */
455    public Collection listAttachments( WikiPage page )
456        throws ProviderException
457    {
458        Collection<Attachment> result = new ArrayList<Attachment>();
459
460        File dir = findPageDir( page.getName() );
461
462        if( dir != null )
463        {
464            String[] attachments = dir.list();
465
466            if( attachments != null )
467            {
468                //
469                //  We now have a list of all potential attachments in 
470                //  the directory.
471                //
472                for( int i = 0; i < attachments.length; i++ )
473                {
474                    File f = new File( dir, attachments[i] );
475
476                    if( f.isDirectory() )
477                    {
478                        String attachmentName = unmangleName( attachments[i] );
479
480                        //
481                        //  Is it a new-stylea attachment directory?  If yes,
482                        //  we'll just deduce the name.  If not, however,
483                        //  we'll check if there's a suitable property file
484                        //  in the directory.
485                        //
486                        if( attachmentName.endsWith( ATTDIR_EXTENSION ) )
487                        {
488                            attachmentName = attachmentName.substring( 0, attachmentName.length()-ATTDIR_EXTENSION.length() );
489                        }
490                        else
491                        {
492                            File propFile = new File( f, PROPERTY_FILE );
493
494                            if( !propFile.exists() )
495                            {
496                                //
497                                //  This is not obviously a JSPWiki attachment,
498                                //  so let's just skip it.
499                                //
500                                continue;
501                            }
502                        }
503
504                        Attachment att = getAttachmentInfo( page, attachmentName,
505                                                            WikiProvider.LATEST_VERSION );
506
507                        //
508                        //  Sanity check - shouldn't really be happening, unless
509                        //  you mess with the repository directly.
510                        //
511                        if( att == null )
512                        {
513                            throw new ProviderException("Attachment disappeared while reading information:"+
514                                                        " if you did not touch the repository, there is a serious bug somewhere. "+
515                                                        "Attachment = "+attachments[i]+
516                                                        ", decoded = "+attachmentName );
517                        }
518
519                        result.add( att );
520                    }
521                }
522            }
523        }
524
525        return result;
526    }
527
528    /**
529     *  {@inheritDoc}
530     */
531    public Collection findAttachments( QueryItem[] query )
532    {
533        return null;
534    }
535
536    /**
537     *  {@inheritDoc}
538     */
539    // FIXME: Very unoptimized.
540    public List listAllChanged( Date timestamp )
541        throws ProviderException
542    {
543        File attDir = new File( m_storageDir );
544
545        if( !attDir.exists() )
546        {
547            throw new ProviderException("Specified attachment directory "+m_storageDir+" does not exist!");
548        }
549
550        ArrayList<Attachment> list = new ArrayList<Attachment>();
551
552        String[] pagesWithAttachments = attDir.list( new AttachmentFilter() );
553
554        for( int i = 0; i < pagesWithAttachments.length; i++ )
555        {
556            String pageId = unmangleName( pagesWithAttachments[i] );
557            pageId = pageId.substring( 0, pageId.length()-DIR_EXTENSION.length() );
558            
559            Collection c = listAttachments( new WikiPage( m_engine, pageId ) );
560
561            for( Iterator it = c.iterator(); it.hasNext(); )
562            {
563                Attachment att = (Attachment) it.next();
564
565                if( att.getLastModified().after( timestamp ) )
566                {
567                    list.add( att );
568                }
569            }
570        }
571
572        Collections.sort( list, new PageTimeComparator() );
573
574        return list;
575    }
576
577    /**
578     *  {@inheritDoc}
579     */
580    public Attachment getAttachmentInfo( WikiPage page, String name, int version )
581        throws ProviderException
582    {
583        Attachment att = new Attachment( m_engine, page.getName(), name );
584        File dir = findAttachmentDir( att );
585
586        if( !dir.exists() )
587        {
588            // log.debug("Attachment dir not found - thus no attachment can exist.");
589            return null;
590        }
591        
592        if( version == WikiProvider.LATEST_VERSION )
593        {
594            version = findLatestVersion(att);
595        }
596
597        att.setVersion( version );
598        
599        // Should attachment be cachable by the client (browser)?
600        if (m_disableCache != null) 
601        {
602            Matcher matcher = m_disableCache.matcher(name);
603            if (matcher.matches()) 
604            {
605                att.setCacheable(false);
606            }
607        }
608        
609
610        // System.out.println("Fetching info on version "+version);
611        try
612        {
613            Properties props = getPageProperties(att);
614
615            att.setAuthor( props.getProperty( version+".author" ) );
616
617            String changeNote = props.getProperty( version+".changenote" );
618            if( changeNote != null )
619            {
620                att.setAttribute(WikiPage.CHANGENOTE, changeNote);
621            }
622            
623            File f = findFile( dir, att );
624
625            att.setSize( f.length() );
626            att.setLastModified( new Date(f.lastModified()) );
627        }
628        catch( FileNotFoundException e )
629        {
630            log.error( "Can't get attachment properties for " + att, e );
631            return null;
632        }
633        catch( IOException e )
634        {
635            log.error("Can't read page properties", e );
636            throw new ProviderException("Cannot read page properties: "+e.getMessage());
637        }
638        // FIXME: Check for existence of this particular version.
639
640        return att;
641    }
642
643    /**
644     *  {@inheritDoc}
645     */
646    public List getVersionHistory( Attachment att )
647    {
648        ArrayList<Attachment> list = new ArrayList<Attachment>();
649
650        try
651        {
652            int latest = findLatestVersion( att );
653
654            for( int i = latest; i >= 1; i-- )
655            {
656                Attachment a = getAttachmentInfo( new WikiPage( m_engine, att.getParentName() ), 
657                                                  att.getFileName(), i );
658
659                if( a != null )
660                {
661                    list.add( a );
662                }
663            }
664        }
665        catch( ProviderException e )
666        {
667            log.error("Getting version history failed for page: "+att,e);
668            // FIXME: SHould this fail?
669        }
670
671        return list;
672    }
673
674    /**
675     *  {@inheritDoc}
676     */
677    public void deleteVersion( Attachment att )
678        throws ProviderException
679    {
680        // FIXME: Does nothing yet.
681    }
682
683    /**
684     *  {@inheritDoc}
685     */
686    public void deleteAttachment( Attachment att )
687        throws ProviderException
688    {
689        File dir = findAttachmentDir( att );
690        String[] files = dir.list();
691
692        for( int i = 0; i < files.length; i++ )
693        {
694            File file = new File( dir.getAbsolutePath() + "/" + files[i] );
695            file.delete();
696        }
697        dir.delete();
698    }
699
700
701    /**
702     *  Returns only those directories that contain attachments.
703     */
704    public static class AttachmentFilter
705        implements FilenameFilter
706    {
707        /**
708         *  {@inheritDoc}
709         */
710        public boolean accept( File dir, String name )
711        {
712            return name.endsWith( DIR_EXTENSION );
713        }
714    }
715
716    /**
717     *  Accepts only files that are actual versions, no control files.
718     */
719    public static class AttachmentVersionFilter
720        implements FilenameFilter
721    {
722        /**
723         *  {@inheritDoc}
724         */
725        public boolean accept( File dir, String name )
726        {
727            return !name.equals( PROPERTY_FILE );
728        }
729    }
730
731    /**
732     *  {@inheritDoc}
733     */
734
735    public void moveAttachmentsForPage( String oldParent, String newParent )
736        throws ProviderException
737    {
738        File srcDir = findPageDir( oldParent );
739        File destDir = findPageDir( newParent );
740
741        log.debug("Trying to move all attachments from "+srcDir+" to "+destDir);
742
743        // If it exists, we're overwriting an old page (this has already been
744        // confirmed at a higher level), so delete any existing attachments.
745        if (destDir.exists())
746        {
747            log.error("Page rename failed because target dirctory "+destDir+" exists");
748        }
749        else
750        {
751            //destDir.getParentFile().mkdir();
752            srcDir.renameTo(destDir);
753        }
754    }
755}
756