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    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<Object> 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 the following file exists, we are NOT migrating from FileSystemProvider
495            File pagePropFile = new File(getPageDirectory() + File.separator + PAGEDIR + File.separator + mangleName(page.getName()) + File.separator + "page" + FileSystemProvider.PROP_EXT);
496            if ( firstUpdate && ! pagePropFile.exists())
497            {
498                // we might not yet have a versioned author because the
499                // old page was last maintained by FileSystemProvider
500                Properties props2 = getHeritagePageProperties( page.getName() );
501
502                // remember the simulated original author (or something)
503                // in the new properties
504                authorFirst = props2.getProperty( "1.author", "unknown" );
505                props.setProperty( "1.author", authorFirst );
506            }
507
508            String newAuthor = page.getAuthor();
509            if ( newAuthor == null )
510            {
511                newAuthor = ( authorFirst != null ) ? authorFirst : "unknown";
512            }
513            page.setAuthor(newAuthor);
514            props.setProperty( versionNumber + ".author", newAuthor );
515
516            String changeNote = (String) page.getAttribute(WikiPage.CHANGENOTE);
517            if( changeNote != null )
518            {
519                props.setProperty( versionNumber+".changenote", changeNote );
520            }
521
522            // Get additional custom properties from page and add to props
523            getCustomProperties(page, props);
524
525            putPageProperties( page.getName(), props );
526        }
527        catch( IOException e )
528        {
529            log.error( "Saving failed", e );
530            throw new ProviderException("Could not save page text: "+e.getMessage());
531        }
532    }
533
534    /**
535     *  {@inheritDoc}
536     */
537    public WikiPage getPageInfo( String page, int version )
538        throws ProviderException
539    {
540        int latest = findLatestVersion(page);
541        int realVersion;
542
543        WikiPage p = null;
544
545        if( version == WikiPageProvider.LATEST_VERSION ||
546            version == latest ||
547            (version == 1 && latest == -1) )
548        {
549            //
550            // Yes, we need to talk to the top level directory
551            // to get this version.
552            //
553            // I am listening to Press Play On Tape's guitar version of
554            // the good old C64 "Wizardry" -tune at this moment.
555            // Oh, the memories...
556            //
557            realVersion = (latest >= 0) ? latest : 1;
558
559            p = super.getPageInfo( page, WikiPageProvider.LATEST_VERSION );
560
561            if( p != null )
562            {
563                p.setVersion( realVersion );
564            }
565        }
566        else
567        {
568            //
569            //  The file is not the most recent, so we'll need to
570            //  find it from the deep trenches of the "OLD" directory
571            //  structure.
572            //
573            realVersion = version;
574            File dir = findOldPageDir( page );
575
576            if( !dir.exists() || !dir.isDirectory() )
577            {
578                return null;
579            }
580
581            File file = new File( dir, version+FILE_EXT );
582
583            if( file.exists() )
584            {
585                p = new WikiPage( m_engine, page );
586
587                p.setLastModified( new Date(file.lastModified()) );
588                p.setVersion( version );
589            }
590        }
591
592        //
593        //  Get author and other metadata information
594        //  (Modification date has already been set.)
595        //
596        if( p != null )
597        {
598            try
599            {
600                Properties props = getPageProperties( page );
601                String author = props.getProperty( realVersion+".author" );
602                if ( author == null )
603                {
604                    // we might not have a versioned author because the
605                    // old page was last maintained by FileSystemProvider
606                    Properties props2 = getHeritagePageProperties( page );
607                    author = props2.getProperty( WikiPage.AUTHOR );
608                }
609                if ( author != null )
610                {
611                    p.setAuthor( author );
612                }
613
614                String changenote = props.getProperty( realVersion+".changenote" );
615                if( changenote != null ) p.setAttribute( WikiPage.CHANGENOTE, changenote );
616
617                // Set the props values to the page attributes
618                setCustomProperties(p, props);
619            }
620            catch( IOException e )
621            {
622                log.error( "Cannot get author for page"+page+": ", e );
623            }
624        }
625
626        return p;
627    }
628
629    /**
630     *  {@inheritDoc}
631     */
632    public boolean pageExists( String pageName, int version )
633    {
634        if (version == WikiPageProvider.LATEST_VERSION || version == findLatestVersion( pageName ) ) {
635            return pageExists(pageName);
636        }
637
638        File dir = findOldPageDir( pageName );
639
640        if( !dir.exists() || !dir.isDirectory() )
641        {
642            return false;
643        }
644
645        File file = new File( dir, version+FILE_EXT );
646
647        return file.exists();
648
649    }
650
651    /**
652     *  {@inheritDoc}
653     */
654     // FIXME: Does not get user information.
655    public List getVersionHistory( String page )
656    throws ProviderException
657    {
658        ArrayList<WikiPage> list = new ArrayList<WikiPage>();
659
660        int latest = findLatestVersion( page );
661
662        // list.add( getPageInfo(page,WikiPageProvider.LATEST_VERSION) );
663
664        for( int i = latest; i > 0; i-- )
665        {
666            WikiPage info = getPageInfo( page, i );
667
668            if( info != null )
669            {
670                list.add( info );
671            }
672        }
673
674        return list;
675    }
676
677    /*
678     * Support for migration of simple properties created by the
679     * FileSystemProvider when coming under Versioning management.
680     * Simulate an initial version.
681     */
682    private Properties getHeritagePageProperties( String page )
683        throws IOException
684    {
685        File propertyFile = new File( getPageDirectory(),
686                        mangleName(page) + FileSystemProvider.PROP_EXT );
687        if ( propertyFile.exists() )
688        {
689            long lastModified = propertyFile.lastModified();
690
691            CachedProperties cp = m_cachedProperties;
692            if ( cp != null
693                && cp.m_page.equals(page)
694                && cp.m_lastModified == lastModified )
695            {
696                return cp.m_props;
697            }
698
699            InputStream in = null;
700            try
701            {
702                in = new BufferedInputStream(
703                            new FileInputStream( propertyFile ));
704
705                Properties props = new Properties();
706                props.load(in);
707
708                String originalAuthor = props.getProperty(WikiPage.AUTHOR);
709                if ( originalAuthor.length() > 0 )
710                {
711                    // simulate original author as if already versioned
712                    // but put non-versioned property in special cache too
713                    props.setProperty( "1.author", originalAuthor );
714
715                    // The profiler showed the probability was very high
716                    // that when calling for the history of a page the
717                    // propertyfile would be read as much times as there were
718                    // versions of that file. It is statistically likely the
719                    // propertyfile will be examined many times before it is updated.
720                    cp = new CachedProperties( page, props, propertyFile.lastModified() );
721                    m_cachedProperties = cp; // Atomic
722                }
723
724                return props;
725            }
726            finally
727            {
728                IOUtils.closeQuietly( in );
729            }
730        }
731
732        return new Properties(); // Returns an empty object
733    }
734
735    /**
736     *  Removes the relevant page directory under "OLD" -directory as well,
737     *  but does not remove any extra subdirectories from it.  It will only
738     *  touch those files that it thinks to be WikiPages.
739     *
740     *  @param page {@inheritDoc}
741     *  @throws {@inheritDoc}
742     */
743    // FIXME: Should log errors.
744    public void deletePage( String page )
745        throws ProviderException
746    {
747        super.deletePage( page );
748
749        File dir = findOldPageDir( page );
750
751        if( dir.exists() && dir.isDirectory() )
752        {
753            File[] files = dir.listFiles( new WikiFileFilter() );
754
755            for( int i = 0; i < files.length; i++ )
756            {
757                files[i].delete();
758            }
759
760            File propfile = new File( dir, PROPERTYFILE );
761
762            if( propfile.exists() )
763            {
764                propfile.delete();
765            }
766
767            dir.delete();
768        }
769    }
770
771    /**
772     *  {@inheritDoc}
773     *
774     *  Deleting versions has never really worked,
775     *  JSPWiki assumes that version histories are "not gappy".
776     *  Using deleteVersion() is definitely not recommended.
777     *
778     */
779    public void deleteVersion( String page, int version )
780        throws ProviderException
781    {
782        File dir = findOldPageDir( page );
783
784        int latest = findLatestVersion( page );
785
786        if( version == WikiPageProvider.LATEST_VERSION ||
787            version == latest ||
788            (version == 1 && latest == -1) )
789        {
790            //
791            //  Delete the properties
792            //
793            try
794            {
795                Properties props = getPageProperties( page );
796                props.remove( ((latest > 0) ? latest : 1)+".author" );
797                putPageProperties( page, props );
798            }
799            catch( IOException e )
800            {
801                log.error("Unable to modify page properties",e);
802                throw new ProviderException("Could not modify page properties: " + e.getMessage());
803            }
804
805            // We can let the FileSystemProvider take care
806            // of the actual deletion
807            super.deleteVersion( page, WikiPageProvider.LATEST_VERSION );
808
809            //
810            //  Copy the old file to the new location
811            //
812            latest = findLatestVersion( page );
813
814            File pageDir = findOldPageDir( page );
815            File previousFile = new File( pageDir, Integer.toString(latest)+FILE_EXT );
816
817            InputStream in = null;
818            OutputStream out = null;
819
820            try
821            {
822                if( previousFile.exists() )
823                {
824                    in = new BufferedInputStream( new FileInputStream( previousFile ) );
825                    File pageFile = findPage(page);
826                    out = new BufferedOutputStream( new FileOutputStream( pageFile ) );
827
828                    FileUtil.copyContents( in, out );
829
830                    //
831                    // We need also to set the date, since we rely on this.
832                    //
833                    pageFile.setLastModified( previousFile.lastModified() );
834                }
835            }
836            catch( IOException e )
837            {
838                log.fatal("Something wrong with the page directory - you may have just lost data!",e);
839            }
840            finally
841            {
842                IOUtils.closeQuietly( in );
843                IOUtils.closeQuietly( out );
844            }
845
846            return;
847        }
848
849        File pageFile = new File( dir, ""+version+FILE_EXT );
850
851        if( pageFile.exists() )
852        {
853            if( !pageFile.delete() )
854            {
855                log.error("Unable to delete page.");
856            }
857        }
858        else
859        {
860            throw new NoSuchVersionException("Page "+page+", version="+version);
861        }
862    }
863
864    /**
865     *  {@inheritDoc}
866     */
867    // FIXME: This is kinda slow, we should need to do this only once.
868    public Collection getAllPages() throws ProviderException
869    {
870        Collection pages = super.getAllPages();
871        Collection<WikiPage> returnedPages = new ArrayList<WikiPage>();
872
873        for( Iterator i = pages.iterator(); i.hasNext(); )
874        {
875            WikiPage page = (WikiPage) i.next();
876
877            WikiPage info = getPageInfo( page.getName(), WikiProvider.LATEST_VERSION );
878
879            returnedPages.add( info );
880        }
881
882        return returnedPages;
883    }
884
885    /**
886     *  {@inheritDoc}
887     */
888    public String getProviderInfo()
889    {
890        return "";
891    }
892
893    /**
894     *  {@inheritDoc}
895     */
896    public void movePage( String from,
897                          String to )
898        throws ProviderException
899    {
900        // Move the file itself
901        File fromFile = findPage( from );
902        File toFile = findPage( to );
903
904        fromFile.renameTo( toFile );
905
906        // Move any old versions
907        File fromOldDir = findOldPageDir( from );
908        File toOldDir = findOldPageDir( to );
909
910        fromOldDir.renameTo( toOldDir );
911    }
912
913    /*
914     * The profiler showed that when calling the history of a page, the
915     * propertyfile was read just as many times as there were versions
916     * of that file. The loading of a propertyfile is a cpu-intensive job.
917     * This Class holds onto the last propertyfile read, because the
918     * probability is high that the next call will with ask for the same
919     * propertyfile. The time it took to show a historypage with 267
920     * versions dropped by 300%. Although each propertyfile in a history
921     * could be cached, there is likely to be little performance gain over
922     * simply keeping the last one requested.
923     */
924    private static class CachedProperties
925    {
926        String m_page;
927        Properties m_props;
928        long m_lastModified;
929
930        /*
931         * Because a Constructor is inherently synchronised, there is
932         * no need to synchronise the arguments.
933         *
934         * @param engine WikiEngine instance
935         * @param props  Properties to use for initialization
936         */
937        public CachedProperties(String pageName, Properties props,
938                                long lastModified) {
939            if ( pageName == null )
940            {
941                throw new NullPointerException ( "pageName must not be null!" );
942            }
943            this.m_page = pageName;
944            if ( props == null )
945            {
946                throw new NullPointerException ( "properties must not be null!" );
947            }
948            m_props = props;
949            this.m_lastModified = lastModified;
950        }
951    }
952}