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