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