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.OutputStreamWriter;
029    import java.io.PrintWriter;
030    import java.io.UnsupportedEncodingException;
031    import java.util.ArrayList;
032    import java.util.Collection;
033    import java.util.Date;
034    import java.util.List;
035    import java.util.Properties;
036    import java.util.TreeSet;
037    
038    import org.apache.log4j.Logger;
039    import org.apache.wiki.InternalWikiException;
040    import org.apache.wiki.WikiEngine;
041    import org.apache.wiki.WikiPage;
042    import org.apache.wiki.WikiProvider;
043    import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
044    import org.apache.wiki.api.exceptions.ProviderException;
045    import org.apache.wiki.search.QueryItem;
046    import org.apache.wiki.search.SearchMatcher;
047    import org.apache.wiki.search.SearchResult;
048    import org.apache.wiki.search.SearchResultComparator;
049    import org.apache.wiki.util.FileUtil;
050    import org.apache.wiki.util.TextUtil;
051    
052    /**
053     *  Provides a simple directory based repository for Wiki pages.
054     *  <P>
055     *  All files have ".txt" appended to make life easier for those
056     *  who insist on using Windows or other software which makes assumptions
057     *  on the files contents based on its name.
058     *  <p>
059     *  This class functions as a superclass to all file based providers.
060     *
061     *  @since 2.1.21.
062     *
063     */
064    public abstract class AbstractFileProvider
065        implements WikiPageProvider
066    {
067        private static final Logger   log = Logger.getLogger(AbstractFileProvider.class);
068        private String m_pageDirectory = "/tmp/";
069        
070        protected String m_encoding;
071        
072        protected WikiEngine m_engine;
073    
074        /**
075         *  Name of the property that defines where page directories are.
076         */
077        public static final String      PROP_PAGEDIR = "jspwiki.fileSystemProvider.pageDir";
078    
079        /**
080         *  All files should have this extension to be recognized as JSPWiki files.
081         *  We default to .txt, because that is probably easiest for Windows users,
082         *  and guarantees correct handling.
083         */
084        public static final String FILE_EXT = ".txt";
085    
086        /** The default encoding. */
087        public static final String DEFAULT_ENCODING = "ISO-8859-1";
088    
089        private boolean m_windowsHackNeeded = false;
090        
091        /**
092         *  {@inheritDoc}
093         *  @throws FileNotFoundException If the specified page directory does not exist.
094         *  @throws IOException In case the specified page directory is a file, not a directory.
095         */
096        public void initialize( WikiEngine engine, Properties properties )
097            throws NoRequiredPropertyException,
098                   IOException, FileNotFoundException
099        {
100            log.debug("Initing FileSystemProvider");
101            m_pageDirectory = TextUtil.getCanonicalFilePathProperty(properties, PROP_PAGEDIR,
102                    System.getProperty("user.home") + File.separator + "jspwiki-files");
103    
104            File f = new File(m_pageDirectory);
105    
106            if( !f.exists() )
107            {
108                if( !f.mkdirs() )
109                {
110                    throw new IOException( "Failed to create page directory " + f.getAbsolutePath() + " , please check property "
111                                           + PROP_PAGEDIR );
112                }
113            }
114            else
115            {
116                if( !f.isDirectory() )
117                {
118                    throw new IOException( "Page directory is not a directory: " + f.getAbsolutePath() );
119                }
120                if( !f.canWrite() )
121                {
122                    throw new IOException( "Page directory is not writable: " + f.getAbsolutePath() );
123                }
124            }
125    
126            m_engine = engine;
127    
128            m_encoding = properties.getProperty( WikiEngine.PROP_ENCODING, 
129                                                 DEFAULT_ENCODING );
130    
131            String os = System.getProperty( "os.name" ).toLowerCase();
132            
133            if( os.startsWith("windows") || os.equals("nt") )
134            {
135                m_windowsHackNeeded = true;
136            }
137            
138            log.info( "Wikipages are read from '" + m_pageDirectory + "'" );
139        }
140    
141    
142        String getPageDirectory()
143        {
144            return m_pageDirectory;
145        }
146    
147        private static final String[] WINDOWS_DEVICE_NAMES =
148        {
149            "con", "prn", "nul", "aux", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9",
150            "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9"
151        };
152        
153        /**
154         *  This makes sure that the queried page name
155         *  is still readable by the file system.  For example, all XML entities
156         *  and slashes are encoded with the percent notation.
157         *  
158         *  @param pagename The name to mangle
159         *  @return The mangled name.
160         */
161        protected String mangleName( String pagename )
162        {
163            pagename = TextUtil.urlEncode( pagename, m_encoding );
164            
165            pagename = TextUtil.replaceString( pagename, "/", "%2F" );
166    
167            //
168            //  Names which start with a dot must be escaped to prevent problems.
169            //  Since we use URL encoding, this is invisible in our unescaping.
170            //
171            if( pagename.startsWith( "." ) )
172            {
173                pagename = "%2E" + pagename.substring( 1 );
174            }
175            
176            if( m_windowsHackNeeded )
177            {
178                String pn = pagename.toLowerCase();
179                for( int i = 0; i < WINDOWS_DEVICE_NAMES.length; i++ )
180                {
181                    if( WINDOWS_DEVICE_NAMES[i].equals(pn) )
182                    {
183                        pagename = "$$$" + pagename;
184                    }
185                }
186            }
187            
188            return pagename;
189        }
190    
191        /**
192         *  This makes the reverse of mangleName.
193         *  
194         *  @param filename The filename to unmangle
195         *  @return The unmangled name.
196         */
197        protected String unmangleName( String filename )
198        {
199            // The exception should never happen.
200            try
201            {
202                if( m_windowsHackNeeded && filename.startsWith( "$$$") && filename.length() > 3 )
203                {
204                    filename = filename.substring(3);
205                }
206                
207                return TextUtil.urlDecode( filename, m_encoding );
208            }
209            catch( UnsupportedEncodingException e ) 
210            {
211                throw new InternalWikiException("Faulty encoding; should never happen");
212            }
213        }
214        
215        /**
216         *  Finds a Wiki page from the page repository.
217         *  
218         *  @param page The name of the page.
219         *  @return A File to the page.  May be null.
220         */
221        protected File findPage( String page )
222        {
223            return new File( m_pageDirectory, mangleName(page)+FILE_EXT );
224        }
225    
226        /**
227         *  {@inheritDoc}
228         */
229        public boolean pageExists( String page )
230        {
231            File pagefile = findPage( page );
232    
233            return pagefile.exists();
234        }
235    
236        /**
237         *  {@inheritDoc}
238         */
239        public boolean pageExists( String page, int version )
240        {
241            return pageExists (page);
242        }
243    
244        /**
245         *  This implementation just returns the current version, as filesystem
246         *  does not provide versioning information for now.
247         *  
248         *  @param page {@inheritDoc}
249         *  @param version {@inheritDoc}
250         *  @throws {@inheritDoc}
251         */
252        public String getPageText( String page, int version )
253            throws ProviderException
254        {
255            return getPageText( page );
256        }
257    
258        /**
259         *  Read the text directly from the correct file.
260         */
261        private String getPageText( String page )
262        {
263            String result  = null;
264            InputStream in = null;
265    
266            File pagedata = findPage( page );
267    
268            if( pagedata.exists() )
269            {
270                if( pagedata.canRead() )
271                {
272                    try
273                    {          
274                        in = new FileInputStream( pagedata );
275                        result = FileUtil.readContents( in, m_encoding );
276                    }
277                    catch( IOException e )
278                    {
279                        log.error("Failed to read", e);
280                    }
281                    finally
282                    {
283                        try
284                        {
285                            if( in  != null ) in.close();
286                        }
287                        catch( Exception e ) 
288                        {
289                            log.fatal("Closing failed",e);
290                        }
291                    }
292                }
293                else
294                {
295                    log.warn("Failed to read page '"+page+"' from '"+pagedata.getAbsolutePath()+"', possibly a permissions problem");
296                }
297            }
298            else
299            {
300                // This is okay.
301                log.info("New page '"+page+"'");
302            }
303    
304            return result;
305        }
306    
307        /**
308         *  {@inheritDoc}
309         */
310        public void putPageText( WikiPage page, String text )        
311            throws ProviderException
312        {
313            File file = findPage( page.getName() );
314            PrintWriter out = null;
315    
316            try
317            {
318                out = new PrintWriter(new OutputStreamWriter( new FileOutputStream( file ),
319                                                              m_encoding ));
320    
321                out.print( text );
322            }
323            catch( IOException e )
324            {
325                log.error( "Saving failed" );
326            }
327            finally
328            {
329                if( out != null ) out.close();
330            }
331        }
332    
333        /**
334         *  {@inheritDoc}
335         */
336        public Collection getAllPages()
337            throws ProviderException
338        {
339            log.debug("Getting all pages...");
340    
341            ArrayList<WikiPage> set = new ArrayList<WikiPage>();
342    
343            File wikipagedir = new File( m_pageDirectory );
344    
345            File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() );
346    
347            if( wikipages == null )
348            {
349                log.error("Wikipages directory '" + m_pageDirectory + "' does not exist! Please check " + PROP_PAGEDIR + " in jspwiki.properties.");
350                throw new InternalWikiException("Page directory does not exist");
351            }
352    
353            for( int i = 0; i < wikipages.length; i++ )
354            {
355                String wikiname = wikipages[i].getName();
356                int cutpoint = wikiname.lastIndexOf( FILE_EXT );
357    
358                WikiPage page = getPageInfo( unmangleName(wikiname.substring(0,cutpoint)),
359                                             WikiPageProvider.LATEST_VERSION );
360                if( page == null )
361                {
362                    // This should not really happen.
363                    // FIXME: Should we throw an exception here?
364                    log.error("Page "+wikiname+" was found in directory listing, but could not be located individually.");
365                    continue;
366                }
367                
368                set.add( page );
369            }
370    
371            return set;        
372        }
373    
374        /**
375         *  Does not work.
376         *  
377         *  @param date {@inheritDoc}
378         *  @return {@inheritDoc}
379         */
380        public Collection getAllChangedSince( Date date )
381        {
382            return new ArrayList(); // FIXME
383        }
384    
385        /**
386         *  {@inheritDoc}
387         */
388        public int getPageCount()
389        {
390            File wikipagedir = new File( m_pageDirectory );
391    
392            File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() );
393    
394            return wikipages.length;
395        }
396    
397        /**
398         * Iterates through all WikiPages, matches them against the given query,
399         * and returns a Collection of SearchResult objects.
400         * 
401         * @param query {@inheritDoc}
402         * @return {@inheritDoc}
403         */
404        public Collection findPages( QueryItem[] query )
405        {
406            File wikipagedir = new File( m_pageDirectory );
407            TreeSet<SearchResult> res = new TreeSet<SearchResult>( new SearchResultComparator() );
408            SearchMatcher matcher = new SearchMatcher( m_engine, query );
409    
410            File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() );
411    
412            for( int i = 0; i < wikipages.length; i++ )
413            {
414                FileInputStream input = null;
415    
416                // log.debug("Searching page "+wikipages[i].getPath() );
417    
418                String filename = wikipages[i].getName();
419                int cutpoint    = filename.lastIndexOf( FILE_EXT );
420                String wikiname = filename.substring( 0, cutpoint );
421    
422                wikiname = unmangleName( wikiname );
423    
424                try
425                {
426                    input = new FileInputStream( wikipages[i] );
427                    String pagetext = FileUtil.readContents( input, m_encoding );
428                    SearchResult comparison = matcher.matchPageContent( wikiname, pagetext );
429                    if( comparison != null )
430                    {
431                        res.add( comparison );
432                    }
433                }
434                catch( IOException e )
435                {
436                    log.error( "Failed to read " + filename, e );
437                }
438                finally
439                {
440                    try
441                    {
442                        if( input != null ) input.close();
443                    }
444                    catch( IOException e ) {} // It's fine to fail silently.
445                }
446            }
447    
448            return res;
449        }
450    
451        /**
452         *  Always returns the latest version, since FileSystemProvider
453         *  does not support versioning.
454         *  
455         *  @param page {@inheritDoc}
456         *  @param version {@inheritDoc}
457         *  @return {@inheritDoc}
458         *  @throws {@inheritDoc}
459         */
460        public WikiPage getPageInfo( String page, int version )
461            throws ProviderException
462        {
463            File file = findPage( page );
464    
465            if( !file.exists() )
466            {
467                return null;
468            }
469    
470            WikiPage p = new WikiPage( m_engine, page );
471            p.setLastModified( new Date(file.lastModified()) );
472    
473            return p;
474        }
475    
476        /**
477         *  The FileSystemProvider provides only one version.
478         *  
479         *  @param page {@inheritDoc}
480         *  @throws {@inheritDoc}
481         *  @return {@inheritDoc}
482         */
483        public List getVersionHistory( String page )
484            throws ProviderException
485        {
486            ArrayList<WikiPage> list = new ArrayList<WikiPage>();
487    
488            list.add( getPageInfo( page, WikiPageProvider.LATEST_VERSION ) );
489    
490            return list;
491        }
492    
493        /**
494         *  {@inheritDoc}
495         */
496        public String getProviderInfo()
497        {
498            return "";
499        }
500    
501        /**
502         *  {@inheritDoc}
503         */
504        public void deleteVersion( String pageName, int version )
505            throws ProviderException
506        {
507            if( version == WikiProvider.LATEST_VERSION )
508            {
509                File f = findPage( pageName );
510    
511                f.delete();
512            }
513        }
514    
515        /**
516         *  {@inheritDoc}
517         */
518        public void deletePage( String pageName )
519            throws ProviderException
520        {
521            File f = findPage( pageName );
522    
523            f.delete();
524        }
525    
526        /**
527         *  A simple filter which filters only those filenames which correspond to the
528         *  file extension used.
529         */
530        public static class WikiFileFilter
531            implements FilenameFilter
532        {
533            /**
534             *  {@inheritDoc}
535             */
536            public boolean accept( File dir, String name )
537            {
538                return name.endsWith( FILE_EXT );
539            }
540        }
541    }