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.BufferedInputStream;
022    import java.io.BufferedOutputStream;
023    import java.io.File;
024    import java.io.FileInputStream;
025    import java.io.FileOutputStream;
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.Date;
032    import java.util.Iterator;
033    import java.util.List;
034    import java.util.Properties;
035    
036    import org.apache.commons.io.IOUtils;
037    import org.apache.log4j.Logger;
038    import org.apache.wiki.InternalWikiException;
039    import org.apache.wiki.WikiEngine;
040    import org.apache.wiki.WikiPage;
041    import org.apache.wiki.WikiProvider;
042    import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
043    import org.apache.wiki.api.exceptions.ProviderException;
044    import org.apache.wiki.util.FileUtil;
045    
046    /**
047     *  Provides a simple directory based repository for Wiki pages.
048     *  Pages are held in a directory structure:
049     *  <PRE>
050     *    Main.txt
051     *    Foobar.txt
052     *    OLD/
053     *       Main/
054     *          1.txt
055     *          2.txt
056     *          page.properties
057     *       Foobar/
058     *          page.properties
059     *  </PRE>
060     *
061     *  In this case, "Main" has three versions, and "Foobar" just one version.
062     *  <P>
063     *  The properties file contains the necessary metainformation (such as author)
064     *  information of the page.  DO NOT MESS WITH IT!
065     *
066     *  <P>
067     *  All files have ".txt" appended to make life easier for those
068     *  who insist on using Windows or other software which makes assumptions
069     *  on the files contents based on its name.
070     *
071     */
072    public class VersioningFileProvider
073        extends AbstractFileProvider
074    {
075        private static final Logger     log = Logger.getLogger(VersioningFileProvider.class);
076       
077        /** Name of the directory where the old versions are stored. */
078        public static final String      PAGEDIR      = "OLD";
079        
080        /** Name of the property file which stores the metadata. */
081        public static final String      PROPERTYFILE = "page.properties";
082    
083        private CachedProperties        m_cachedProperties;
084        
085        /**
086         *  {@inheritDoc}
087         */
088        public void initialize( WikiEngine engine, Properties properties )
089            throws NoRequiredPropertyException,
090                   IOException
091        {
092            super.initialize( engine, properties );
093            // some additional sanity checks :
094            File oldpages = new File(getPageDirectory(), PAGEDIR);
095            if (!oldpages.exists())
096            {
097                if (!oldpages.mkdirs())
098                {
099                    throw new IOException("Failed to create page version directory " + oldpages.getAbsolutePath());
100                }
101            }
102            else
103            {
104                if (!oldpages.isDirectory())
105                {
106                    throw new IOException("Page version directory is not a directory: " + oldpages.getAbsolutePath());
107                }
108                if (!oldpages.canWrite())
109                {
110                    throw new IOException("Page version directory is not writable: " + oldpages.getAbsolutePath());
111                }
112            }
113            log.info("Using directory " + oldpages.getAbsolutePath() + " for storing old versions of pages");
114        }
115    
116        /**
117         *  Returns the directory where the old versions of the pages
118         *  are being kept.
119         */
120        private File findOldPageDir( String page )
121        {
122            if( page == null )
123            {
124                throw new InternalWikiException("Page may NOT be null in the provider!");
125            }
126    
127            File oldpages = new File( getPageDirectory(), PAGEDIR );
128    
129            return new File( oldpages, mangleName(page) );
130        }
131        
132        /**
133         *  Goes through the repository and decides which version is
134         *  the newest one in that directory.
135         *
136         *  @return Latest version number in the repository, or -1, if
137         *          there is no page in the repository.
138         */
139    
140        // FIXME: This is relatively slow.
141        /*
142        private int findLatestVersion( String page )
143        {
144            File pageDir = findOldPageDir( page );
145    
146            String[] pages = pageDir.list( new WikiFileFilter() );
147    
148            if( pages == null )
149            {
150                return -1; // No such thing found.
151            }
152    
153            int version = -1;
154    
155            for( int i = 0; i < pages.length; i++ )
156            {
157                int cutpoint = pages[i].indexOf( '.' );
158                if( cutpoint > 0 )
159                {
160                    String pageNum = pages[i].substring( 0, cutpoint );
161    
162                    try
163                    {
164                        int res = Integer.parseInt( pageNum );
165    
166                        if( res > version )
167                        {
168                            version = res;
169                        }
170                    }
171                    catch( NumberFormatException e ) {} // It's okay to skip these.
172                }
173            }
174    
175            return version;
176        }
177    */
178        private int findLatestVersion( String page )
179        {
180            int version = -1;
181            
182            try
183            {
184                Properties props = getPageProperties( page );
185                
186                for( Iterator i = props.keySet().iterator(); i.hasNext(); )
187                {
188                    String key = (String)i.next();
189                    
190                    if( key.endsWith(".author") )
191                    {
192                        int cutpoint = key.indexOf('.');
193                        if( cutpoint > 0 )
194                        {
195                            String pageNum = key.substring(0,cutpoint);
196                            
197                            try
198                            {
199                                int res = Integer.parseInt( pageNum );
200                                
201                                if( res > version )
202                                {
203                                    version = res;
204                                }
205                            }
206                            catch( NumberFormatException e ) {} // It's okay to skip these. 
207                        }
208                    }
209                }
210            }
211            catch( IOException e )
212            {
213                log.error("Unable to figure out latest version - dying...",e);
214            }
215            
216            return version;
217        }
218    
219        /**
220         *  Reads page properties from the file system.
221         */
222        private Properties getPageProperties( String page )
223            throws IOException
224        {
225            File propertyFile = new File( findOldPageDir(page), PROPERTYFILE );
226    
227            if( propertyFile.exists() )
228            {
229                long lastModified = propertyFile.lastModified();
230    
231                //
232                //   The profiler showed that when calling the history of a page the propertyfile
233                //   was read just as much times as there were versions of that file. The loading
234                //   of a propertyfile is a cpu-intensive job. So now hold on to the last propertyfile
235                //   read because the next method will with a high probability ask for the same propertyfile.
236                //   The time it took to show a historypage with 267 versions dropped with 300%. 
237                //
238                
239                CachedProperties cp = m_cachedProperties;
240                
241                if( cp != null 
242                    && cp.m_page.equals(page) 
243                    && cp.m_lastModified == lastModified)
244                {
245                    return cp.m_props;
246                }
247                
248                InputStream in = null;
249                
250                try
251                {
252                    in = new BufferedInputStream(new FileInputStream( propertyFile ));
253    
254                    Properties props = new Properties();
255    
256                    props.load(in);
257    
258                    cp = new CachedProperties( page, props, lastModified );
259                    m_cachedProperties = cp; // Atomic
260    
261                    return props;
262                }
263                finally
264                {
265                    IOUtils.closeQuietly( in );
266                }
267            }
268            
269            return new Properties(); // Returns an empty object
270        }
271    
272        /**
273         *  Writes the page properties back to the file system.
274         *  Note that it WILL overwrite any previous properties.
275         */
276        private void putPageProperties( String page, Properties properties )
277            throws IOException
278        {
279            File propertyFile = new File( findOldPageDir(page), PROPERTYFILE );
280            OutputStream out = null;
281            
282            try
283            {
284                out = new FileOutputStream( propertyFile );
285    
286                properties.store( out, " JSPWiki page properties for "+page+". DO NOT MODIFY!" );
287            }
288            finally
289            {
290                IOUtils.closeQuietly( out );
291            }
292    
293            // The profiler showed the probability was very high that when
294            // calling for the history of a page the propertyfile would be
295            // read as much times as there were versions of that file.
296            // It is statistically likely the propertyfile will be examined
297            // many times before it is updated.
298            CachedProperties cp =
299                    new CachedProperties( page, properties, propertyFile.lastModified() );
300            m_cachedProperties = cp; // Atomic
301        }
302    
303        /**
304         *  Figures out the real version number of the page and also checks
305         *  for its existence.
306         *
307         *  @throws NoSuchVersionException if there is no such version.
308         */
309        private int realVersion( String page, int requestedVersion )
310            throws NoSuchVersionException,
311                   ProviderException
312        {
313            //
314            //  Quickly check for the most common case.
315            //
316            if( requestedVersion == WikiProvider.LATEST_VERSION )
317            {
318                return -1;
319            }
320    
321            int latest = findLatestVersion(page);
322    
323            if( requestedVersion == latest ||
324                (requestedVersion == 1 && latest == -1 ) )
325            {
326                return -1;
327            }
328            else if( requestedVersion <= 0 || requestedVersion > latest )
329            {
330                throw new NoSuchVersionException("Requested version "+requestedVersion+", but latest is "+latest );
331            }
332    
333            return requestedVersion;
334        }
335    
336        /**
337         *  {@inheritDoc}
338         */
339        public synchronized String getPageText( String page, int version )
340            throws ProviderException
341        {
342            File dir = findOldPageDir( page );
343    
344            version = realVersion( page, version );
345            if( version == -1 )
346            {
347                // We can let the FileSystemProvider take care
348                // of these requests.
349                return super.getPageText( page, WikiPageProvider.LATEST_VERSION );
350            }
351    
352            File pageFile = new File( dir, ""+version+FILE_EXT );
353    
354            if( !pageFile.exists() )
355                throw new NoSuchVersionException("Version "+version+"does not exist.");
356            
357            return readFile( pageFile );
358        }
359    
360    
361        // FIXME: Should this really be here?
362        private String readFile( File pagedata )
363            throws ProviderException
364        {
365            String      result = null;
366            InputStream in     = null;
367    
368            if( pagedata.exists() )
369            {
370                if( pagedata.canRead() )
371                {
372                    try
373                    {          
374                        in = new FileInputStream( pagedata );
375                        result = FileUtil.readContents( in, m_encoding );
376                    }
377                    catch( IOException e )
378                    {
379                        log.error("Failed to read", e);
380                        throw new ProviderException("I/O error: "+e.getMessage());
381                    }
382                    finally
383                    {
384                        IOUtils.closeQuietly( in );
385                    }
386                }
387                else
388                {
389                    log.warn("Failed to read page from '"+pagedata.getAbsolutePath()+"', possibly a permissions problem");
390                    throw new ProviderException("I cannot read the requested page.");
391                }
392            }
393            else
394            {
395                // This is okay.
396                // FIXME: is it?
397                log.info("New page");
398            }
399    
400            return result;
401        }
402    
403        // FIXME: This method has no rollback whatsoever.
404        
405        /*
406          This is how the page directory should look like:
407    
408             version    pagedir       olddir
409              none       empty         empty
410               1         Main.txt (1)  empty
411               2         Main.txt (2)  1.txt
412               3         Main.txt (3)  1.txt, 2.txt
413        */
414        /**
415         *  {@inheritDoc}
416         */
417        public synchronized void putPageText( WikiPage page, String text )
418            throws ProviderException
419        {
420            //
421            //  This is a bit complicated.  We'll first need to
422            //  copy the old file to be the newest file.
423            //
424    
425            File pageDir = findOldPageDir( page.getName() );
426    
427            if( !pageDir.exists() )
428            {
429                pageDir.mkdirs();
430            }
431    
432            int  latest  = findLatestVersion( page.getName() );
433    
434            try
435            {
436                //
437                // Copy old data to safety, if one exists.
438                //
439    
440                File oldFile = findPage( page.getName() );
441    
442                // Figure out which version should the old page be?
443                // Numbers should always start at 1.
444                // "most recent" = -1 ==> 1
445                // "first"       = 1  ==> 2
446    
447                int versionNumber = (latest > 0) ? latest : 1;
448                boolean firstUpdate = (versionNumber == 1);
449    
450                if( oldFile != null && oldFile.exists() )
451                {
452                    InputStream in = null;
453                    OutputStream out = null;
454    
455                    try
456                    {
457                        in = new BufferedInputStream( new FileInputStream( oldFile ) );
458                        File pageFile = new File( pageDir, Integer.toString( versionNumber )+FILE_EXT );
459                        out = new BufferedOutputStream( new FileOutputStream( pageFile ) );
460    
461                        FileUtil.copyContents( in, out );
462    
463                        //
464                        // We need also to set the date, since we rely on this.
465                        //
466                        pageFile.setLastModified( oldFile.lastModified() );
467    
468                        //
469                        // Kludge to make the property code to work properly.
470                        //
471                        versionNumber++;
472                    }
473                    finally
474                    {
475                        IOUtils.closeQuietly( out );
476                        IOUtils.closeQuietly( in );
477                    }
478                }
479    
480                //
481                //  Let superclass handler writing data to a new version.
482                //
483    
484                super.putPageText( page, text );
485    
486                //
487                //  Finally, write page version data.
488                //
489    
490                // FIXME: No rollback available.
491                Properties props = getPageProperties( page.getName() );
492    
493                String authorFirst = null;
494                if ( firstUpdate )
495                {
496                    // we might not yet have a versioned author because the
497                    // old page was last maintained by FileSystemProvider
498                    Properties props2 = getHeritagePageProperties( page.getName() );
499    
500                    // remember the simulated original author (or something)
501                    // in the new properties
502                    authorFirst = props2.getProperty( "1.author", "unknown" );
503                    props.setProperty( "1.author", authorFirst );
504                }
505    
506                String newAuthor = page.getAuthor();
507                if ( newAuthor == null )
508                {
509                    newAuthor = ( authorFirst != null ) ? authorFirst : "unknown";
510                }
511                page.setAuthor(newAuthor);
512                props.setProperty( versionNumber + ".author", newAuthor );
513    
514                String changeNote = (String) page.getAttribute(WikiPage.CHANGENOTE);
515                if( changeNote != null )
516                {
517                    props.setProperty( versionNumber+".changenote", changeNote );
518                }
519    
520                putPageProperties( page.getName(), props );
521            }
522            catch( IOException e )
523            {
524                log.error( "Saving failed", e );
525                throw new ProviderException("Could not save page text: "+e.getMessage());
526            }
527        }
528    
529        /**
530         *  {@inheritDoc}
531         */
532        public WikiPage getPageInfo( String page, int version )
533            throws ProviderException
534        {
535            int latest = findLatestVersion(page);
536            int realVersion;
537    
538            WikiPage p = null;
539    
540            if( version == WikiPageProvider.LATEST_VERSION ||
541                version == latest || 
542                (version == 1 && latest == -1) )
543            {
544                //
545                // Yes, we need to talk to the top level directory
546                // to get this version.
547                //
548                // I am listening to Press Play On Tape's guitar version of
549                // the good old C64 "Wizardry" -tune at this moment.
550                // Oh, the memories...
551                //
552                realVersion = (latest >= 0) ? latest : 1;
553    
554                p = super.getPageInfo( page, WikiPageProvider.LATEST_VERSION );
555    
556                if( p != null )
557                {
558                    p.setVersion( realVersion );
559                }
560            }
561            else
562            {
563                //
564                //  The file is not the most recent, so we'll need to
565                //  find it from the deep trenches of the "OLD" directory
566                //  structure.
567                //
568                realVersion = version;
569                File dir = findOldPageDir( page );
570    
571                if( !dir.exists() || !dir.isDirectory() )
572                {
573                    return null;
574                }
575    
576                File file = new File( dir, version+FILE_EXT );
577    
578                if( file.exists() )
579                {
580                    p = new WikiPage( m_engine, page );
581    
582                    p.setLastModified( new Date(file.lastModified()) );
583                    p.setVersion( version );
584                }
585            }
586    
587            //
588            //  Get author and other metadata information
589            //  (Modification date has already been set.)
590            //
591            if( p != null )
592            {
593                try
594                {
595                    Properties props = getPageProperties( page );
596                    String author = props.getProperty( realVersion+".author" );
597                    if ( author == null )
598                    {
599                        // we might not have a versioned author because the
600                        // old page was last maintained by FileSystemProvider
601                        Properties props2 = getHeritagePageProperties( page );
602                        author = props2.getProperty( "author" );
603                    }
604                    if ( author != null )
605                    {
606                        p.setAuthor( author );
607                    }
608    
609                    String changenote = props.getProperty( realVersion+".changenote" );
610                    if( changenote != null ) p.setAttribute( WikiPage.CHANGENOTE, changenote );
611    
612                }
613                catch( IOException e )
614                {
615                    log.error( "Cannot get author for page"+page+": ", e );
616                }
617            }
618    
619            return p;
620        }
621    
622        /**
623         *  {@inheritDoc}
624         */
625        public boolean pageExists( String pageName, int version )
626        {
627            if (version == WikiPageProvider.LATEST_VERSION || version == findLatestVersion( pageName ) ) {
628                return pageExists(pageName);
629            }
630    
631            File dir = findOldPageDir( pageName );
632    
633            if( !dir.exists() || !dir.isDirectory() )
634            {
635                return false;
636            }
637    
638            File file = new File( dir, version+FILE_EXT );
639    
640            return file.exists();
641    
642        }
643    
644        /**
645         *  {@inheritDoc}
646         */
647         // FIXME: Does not get user information.
648        public List getVersionHistory( String page )
649        throws ProviderException
650        {
651            ArrayList<WikiPage> list = new ArrayList<WikiPage>();
652    
653            int latest = findLatestVersion( page );
654    
655            // list.add( getPageInfo(page,WikiPageProvider.LATEST_VERSION) );
656            
657            for( int i = latest; i > 0; i-- )
658            {
659                WikiPage info = getPageInfo( page, i );
660    
661                if( info != null )
662                {
663                    list.add( info );
664                }
665            }
666    
667            return list;
668        }
669    
670        /*
671         * Support for migration of simple properties created by the
672         * FileSystemProvider when coming under Versioning management.
673         * Simulate an initial version.
674         */
675        private Properties getHeritagePageProperties( String page )
676            throws IOException
677        {
678            File propertyFile = new File( getPageDirectory(),
679                            mangleName(page) + FileSystemProvider.PROP_EXT );
680            if ( propertyFile.exists() )
681            {
682                long lastModified = propertyFile.lastModified();
683    
684                CachedProperties cp = m_cachedProperties;
685                if ( cp != null
686                    && cp.m_page.equals(page)
687                    && cp.m_lastModified == lastModified )
688                {
689                    return cp.m_props;
690                }
691    
692                InputStream in = null;
693                try
694                {
695                    in = new BufferedInputStream(
696                                new FileInputStream( propertyFile ));
697    
698                    Properties props = new Properties();
699                    props.load(in);
700    
701                    String originalAuthor = props.getProperty("author");
702                    if ( originalAuthor.length() > 0 )
703                    {
704                        // simulate original author as if already versioned
705                        // but put non-versioned property in special cache too
706                        props.setProperty( "1.author", originalAuthor );
707    
708                        // The profiler showed the probability was very high
709                        // that when calling for the history of a page the
710                        // propertyfile would be read as much times as there were
711                        // versions of that file. It is statistically likely the
712                        // propertyfile will be examined many times before it is updated.
713                        cp = new CachedProperties( page, props, propertyFile.lastModified() );
714                        m_cachedProperties = cp; // Atomic
715                    }
716    
717                    return props;
718                }
719                finally
720                {
721                    IOUtils.closeQuietly( in );
722                }
723            }
724    
725            return new Properties(); // Returns an empty object
726        }
727    
728        /**
729         *  Removes the relevant page directory under "OLD" -directory as well,
730         *  but does not remove any extra subdirectories from it.  It will only
731         *  touch those files that it thinks to be WikiPages.
732         *  
733         *  @param page {@inheritDoc}
734         *  @throws {@inheritDoc}
735         */
736        // FIXME: Should log errors.
737        public void deletePage( String page )
738            throws ProviderException
739        {
740            super.deletePage( page );
741    
742            File dir = findOldPageDir( page );
743    
744            if( dir.exists() && dir.isDirectory() )
745            {
746                File[] files = dir.listFiles( new WikiFileFilter() );
747    
748                for( int i = 0; i < files.length; i++ )
749                {
750                    files[i].delete();
751                }
752    
753                File propfile = new File( dir, PROPERTYFILE );
754    
755                if( propfile.exists() )
756                {
757                    propfile.delete();
758                }
759    
760                dir.delete();
761            }
762        }
763    
764        /**
765         *  {@inheritDoc}
766         *  
767         *  Deleting versions has never really worked,
768         *  JSPWiki assumes that version histories are "not gappy". 
769         *  Using deleteVersion() is definitely not recommended.
770         *  
771         */
772        public void deleteVersion( String page, int version )
773            throws ProviderException
774        {
775            File dir = findOldPageDir( page );
776    
777            int latest = findLatestVersion( page );
778    
779            if( version == WikiPageProvider.LATEST_VERSION ||
780                version == latest || 
781                (version == 1 && latest == -1) )
782            {
783                //
784                //  Delete the properties
785                //
786                try
787                {
788                    Properties props = getPageProperties( page );
789                    props.remove( ((latest > 0) ? latest : 1)+".author" );
790                    putPageProperties( page, props );
791                }
792                catch( IOException e )
793                {
794                    log.error("Unable to modify page properties",e);
795                    throw new ProviderException("Could not modify page properties: " + e.getMessage());
796                }
797    
798                // We can let the FileSystemProvider take care
799                // of the actual deletion
800                super.deleteVersion( page, WikiPageProvider.LATEST_VERSION );
801                
802                //
803                //  Copy the old file to the new location
804                //
805                latest = findLatestVersion( page );
806                
807                File pageDir = findOldPageDir( page );
808                File previousFile = new File( pageDir, Integer.toString(latest)+FILE_EXT );
809    
810                InputStream in = null;
811                OutputStream out = null;
812                
813                try
814                {
815                    if( previousFile.exists() )
816                    {
817                        in = new BufferedInputStream( new FileInputStream( previousFile ) );
818                        File pageFile = findPage(page);
819                        out = new BufferedOutputStream( new FileOutputStream( pageFile ) );
820    
821                        FileUtil.copyContents( in, out );
822    
823                        //
824                        // We need also to set the date, since we rely on this.
825                        //
826                        pageFile.setLastModified( previousFile.lastModified() );
827                    }
828                }
829                catch( IOException e )
830                {
831                    log.fatal("Something wrong with the page directory - you may have just lost data!",e);
832                }
833                finally
834                {
835                    IOUtils.closeQuietly( in );
836                    IOUtils.closeQuietly( out );
837                }
838                
839                return;
840            }
841    
842            File pageFile = new File( dir, ""+version+FILE_EXT );
843    
844            if( pageFile.exists() )
845            {
846                if( !pageFile.delete() )
847                {
848                    log.error("Unable to delete page.");
849                }
850            }
851            else
852            {
853                throw new NoSuchVersionException("Page "+page+", version="+version);
854            }
855        }
856    
857        /**
858         *  {@inheritDoc}
859         */
860        // FIXME: This is kinda slow, we should need to do this only once.
861        public Collection getAllPages() throws ProviderException
862        {
863            Collection pages = super.getAllPages();
864            Collection<WikiPage> returnedPages = new ArrayList<WikiPage>();
865            
866            for( Iterator i = pages.iterator(); i.hasNext(); )
867            {
868                WikiPage page = (WikiPage) i.next();
869                
870                WikiPage info = getPageInfo( page.getName(), WikiProvider.LATEST_VERSION );
871     
872                returnedPages.add( info );
873            }
874            
875            return returnedPages;
876        }
877        
878        /**
879         *  {@inheritDoc}
880         */
881        public String getProviderInfo()
882        {
883            return "";
884        }
885    
886        /**
887         *  {@inheritDoc}
888         */
889        public void movePage( String from,
890                              String to )
891            throws ProviderException
892        {
893            // Move the file itself
894            File fromFile = findPage( from );
895            File toFile = findPage( to );
896    
897            fromFile.renameTo( toFile );
898    
899            // Move any old versions
900            File fromOldDir = findOldPageDir( from );
901            File toOldDir = findOldPageDir( to );
902    
903            fromOldDir.renameTo( toOldDir );
904        }
905    
906        /*
907         * The profiler showed that when calling the history of a page, the
908         * propertyfile was read just as many times as there were versions
909         * of that file. The loading of a propertyfile is a cpu-intensive job.
910         * This Class holds onto the last propertyfile read, because the
911         * probability is high that the next call will with ask for the same
912         * propertyfile. The time it took to show a historypage with 267
913         * versions dropped by 300%. Although each propertyfile in a history
914         * could be cached, there is likely to be little performance gain over
915         * simply keeping the last one requested.
916         */
917        private static class CachedProperties
918        {
919            String m_page;
920            Properties m_props;
921            long m_lastModified;
922    
923            /*
924             * Because a Constructor is inherently synchronised, there is
925             * no need to synchronise the arguments.
926             *
927             * @param engine WikiEngine instance
928             * @param props  Properties to use for initialization
929             */
930            public CachedProperties(String pageName, Properties props,
931                                    long lastModified) {
932                if ( pageName == null )
933                {
934                    throw new NullPointerException ( "pageName must not be null!" );
935                }
936                this.m_page = pageName;
937                if ( props == null )
938                {
939                    throw new NullPointerException ( "properties must not be null!" );
940                }
941                m_props = props;
942                this.m_lastModified = lastModified;
943            }
944        }
945    }