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     */
019    package org.apache.wiki.providers;
020    
021    import java.io.File;
022    import java.io.FileInputStream;
023    import java.io.FileNotFoundException;
024    import java.io.FileOutputStream;
025    import java.io.FilenameFilter;
026    import java.io.IOException;
027    import java.io.InputStream;
028    import java.io.OutputStream;
029    import java.util.ArrayList;
030    import java.util.Collection;
031    import java.util.Collections;
032    import java.util.Date;
033    import java.util.Iterator;
034    import java.util.List;
035    import java.util.Properties;
036    import java.util.regex.Matcher;
037    import java.util.regex.Pattern;
038    
039    import org.apache.commons.io.IOUtils;
040    import org.apache.log4j.Logger;
041    import org.apache.wiki.WikiEngine;
042    import org.apache.wiki.WikiPage;
043    import org.apache.wiki.WikiProvider;
044    import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
045    import org.apache.wiki.api.exceptions.ProviderException;
046    import org.apache.wiki.attachment.Attachment;
047    import org.apache.wiki.search.QueryItem;
048    import org.apache.wiki.util.FileUtil;
049    import org.apache.wiki.util.TextUtil;
050    import 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     */
082    public 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