    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 org.apache.logging.log4j.LogManager;
    022import org.apache.logging.log4j.Logger;
    023import org.apache.wiki.InternalWikiException;
    024import org.apache.wiki.api.core.Engine;
    025import org.apache.wiki.api.core.Page;
    026import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
    027import org.apache.wiki.api.exceptions.ProviderException;
    028import org.apache.wiki.api.providers.PageProvider;
    029import org.apache.wiki.api.providers.WikiProvider;
    030import org.apache.wiki.api.spi.Wiki;
    031import org.apache.wiki.util.FileUtil;
    032
    033import java.io.BufferedInputStream;
    034import java.io.BufferedOutputStream;
    035import java.io.File;
    036import java.io.IOException;
    037import java.io.InputStream;
    038import java.io.OutputStream;
    039import java.nio.file.Files;
    040import java.util.ArrayList;
    041import java.util.Collection;
    042import java.util.Date;
    043import java.util.List;
    044import java.util.Properties;
    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 extends AbstractFileProvider {
    073
    074    private static final Logger LOG = LogManager.getLogger( VersioningFileProvider.class );
    075
    076    /** Name of the directory where the old versions are stored. */
    077    public static final String PAGEDIR = "OLD";
    078
    079    /** Name of the property file which stores the metadata. */
    080    public static final String PROPERTYFILE = "page.properties";
    081
    082    private CachedProperties m_cachedProperties;
    083
    084    /**
    085     *  {@inheritDoc}
    086     */
    087    @Override
    088    public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException {
    089        super.initialize( engine, properties );
    090        // some additional sanity checks :
    091        final File oldpages = new File( getPageDirectory(), PAGEDIR );
    092        if( !oldpages.exists() ) {
    093            if( !oldpages.mkdirs() ) {
    094                throw new IOException( "Failed to create page version directory " + oldpages.getAbsolutePath() );
    095            }
    096        } else {
    097            if( !oldpages.isDirectory() ) {
    098                throw new IOException( "Page version directory is not a directory: " + oldpages.getAbsolutePath() );
    099            }
    100            if( !oldpages.canWrite() ) {
    101                throw new IOException( "Page version directory is not writable: " + oldpages.getAbsolutePath() );
    102            }
    103        }
    104        LOG.info( "Using directory " + oldpages.getAbsolutePath() + " for storing old versions of pages" );
    105    }
    106
    107    /**
    108     *  Returns the directory where the old versions of the pages
    109     *  are being kept.
    110     */
    111    private File findOldPageDir( final String page ) {
    112        if( page == null ) {
    113            throw new InternalWikiException( "Page may NOT be null in the provider!" );
    114        }
    115        final File oldpages = new File( getPageDirectory(), PAGEDIR );
    116        return new File( oldpages, mangleName( page ) );
    117    }
    118
    119    /**
    120     *  Goes through the repository and decides which version is the newest one in that directory.
    121     *
    122     *  @return Latest version number in the repository, or -1, if there is no page in the repository.
    123     */
    124
    125    // FIXME: This is relatively slow.
    126    /*
    127    private int findLatestVersion( String page )
    128    {
    129        File pageDir = findOldPageDir( page );
    130
    131        String[] pages = pageDir.list( new WikiFileFilter() );
    132
    133        if( pages == null )
    134        {
    135            return -1; // No such thing found.
    136        }
    137
    138        int version = -1;
    139
    140        for( int i = 0; i < pages.length; i++ )
    141        {
    142            int cutpoint = pages[i].indexOf( '.' );
    143            if( cutpoint > 0 )
    144            {
    145                String pageNum = pages[i].substring( 0, cutpoint );
    146
    147                try
    148                {
    149                    int res = Integer.parseInt( pageNum );
    150
    151                    if( res > version )
    152                    {
    153                        version = res;
    154                    }
    155                }
    156                catch( NumberFormatException e ) {} // It's okay to skip these.
    157            }
    158        }
    159
    160        return version;
    161    }
    162*/
    163    private int findLatestVersion( final String page ) {
    164        int version = -1;
    165
    166        try {
    167            final Properties props = getPageProperties( page );
    168
    169            for( final Object o : props.keySet() ) {
    170                final String key = ( String )o;
    171                if( key.endsWith( ".author" ) ) {
    172                    final int cutpoint = key.indexOf( '.' );
    173                    if( cutpoint > 0 ) {
    174                        final String pageNum = key.substring( 0, cutpoint );
    175
    176                        try {
    177                            final int res = Integer.parseInt( pageNum );
    178
    179                            if( res > version ) {
    180                                version = res;
    181                            }
    182                        } catch( final NumberFormatException e ) {
    183                            LOG.debug(e.getMessage(), e);
    184                        } // It's okay to skip these.
    185                    }
    186                }
    187            }
    188        } catch( final IOException e ) {
    189            LOG.error( "Unable to figure out latest version - dying...", e );
    190        }
    191
    192        return version;
    193    }
    194
    195    /**
    196     *  Reads page properties from the file system.
    197     */
    198    private Properties getPageProperties( final String page ) throws IOException {
    199        final File propertyFile = new File( findOldPageDir(page), PROPERTYFILE );
    200        if( propertyFile.exists() ) {
    201            final long lastModified = propertyFile.lastModified();
    202
    203            //
    204            //   The profiler showed that when calling the history of a page the propertyfile
    205            //   was read just as much times as there were versions of that file. The loading
    206            //   of a propertyfile is a cpu-intensive job. So now hold on to the last propertyfile
    207            //   read because the next method will with a high probability ask for the same propertyfile.
    208            //   The time it took to show a historypage with 267 versions dropped with 300%.
    209            //
    210
    211            CachedProperties cp = m_cachedProperties;
    212
    213            if( cp != null && cp.m_page.equals( page ) && cp.m_lastModified == lastModified ) {
    214                return cp.m_props;
    215            }
    216
    217            try( final InputStream in = new BufferedInputStream( Files.newInputStream( propertyFile.toPath() ) ) ) {
    218                final Properties props = new Properties();
    219                props.load( in );
    220                cp = new CachedProperties( page, props, lastModified );
    221                m_cachedProperties = cp; // Atomic
    222
    223                return props;
    224            }
    225        }
    226
    227        return new Properties(); // Returns an empty object
    228    }
    229
    230    /**
    231     *  Writes the page properties back to the file system.
    232     *  Note that it WILL overwrite any previous properties.
    233     */
    234    private void putPageProperties( final String page, final Properties properties ) throws IOException {
    235        final File propertyFile = new File( findOldPageDir(page), PROPERTYFILE );
    236        try( final OutputStream out = Files.newOutputStream( propertyFile.toPath() ) ) {
    237            properties.store( out, " JSPWiki page properties for "+page+". DO NOT MODIFY!" );
    238        }
    239
    240        // The profiler showed the probability was very high that when  calling for the history of
    241        // a page the propertyfile would be read as much times as there were versions of that file.
    242        // It is statistically likely the propertyfile will be examined many times before it is updated.
    243        m_cachedProperties = new CachedProperties( page, properties, propertyFile.lastModified() ); // Atomic
    244    }
    245
    246    /**
    247     *  Figures out the real version number of the page and also checks for its existence.
    248     *
    249     *  @throws NoSuchVersionException if there is no such version.
    250     */
    251    private int realVersion( final String page, final int requestedVersion ) throws NoSuchVersionException {
    252        //  Quickly check for the most common case.
    253        if( requestedVersion == WikiProvider.LATEST_VERSION ) {
    254            return -1;
    255        }
    256
    257        final int latest = findLatestVersion(page);
    258
    259        if( requestedVersion == latest || (requestedVersion == 1 && latest == -1 ) ) {
    260            return -1;
    261        } else if( requestedVersion <= 0 || requestedVersion > latest ) {
    262            throw new NoSuchVersionException( "Requested version " + requestedVersion + ", but latest is " + latest );
    263        }
    264
    265        return requestedVersion;
    266    }
    267
    268    /**
    269     *  {@inheritDoc}
    270     */
    271    @Override
    272    public synchronized String getPageText( final String page, int version ) throws ProviderException {
    273        final File dir = findOldPageDir( page );
    274
    275        version = realVersion( page, version );
    276        if( version == -1 ) {
    277            // We can let the FileSystemProvider take care of these requests.
    278            return super.getPageText( page, PageProvider.LATEST_VERSION );
    279        }
    280
    281        final File pageFile = new File( dir, ""+version+FILE_EXT );
    282        if( !pageFile.exists() ) {
    283            throw new NoSuchVersionException("Version "+version+"does not exist.");
    284        }
    285
    286        return readFile( pageFile );
    287    }
    288
    289
    290    // FIXME: Should this really be here?
    291    private String readFile( final File pagedata ) throws ProviderException {
    292        String result = null;
    293        if( pagedata.exists() ) {
    294            if( pagedata.canRead() ) {
    295                try( final InputStream in = Files.newInputStream( pagedata.toPath() ) ) {
    296                    result = FileUtil.readContents( in, m_encoding );
    297                } catch( final IOException e ) {
    298                    LOG.error("Failed to read", e);
    299                    throw new ProviderException("I/O error: "+e.getMessage());
    300                }
    301            } else {
    302                LOG.warn("Failed to read page from '"+pagedata.getAbsolutePath()+"', possibly a permissions problem");
    303                throw new ProviderException("I cannot read the requested page.");
    304            }
    305        } else {
    306            // This is okay.
    307            // FIXME: is it?
    308            LOG.debug("New page");
    309        }
    310
    311        return result;
    312    }
    313
    314    // FIXME: This method has no rollback whatsoever.
    315
    316    /*
    317      This is how the page directory should look like:
    318
    319         version    pagedir       olddir
    320          none       empty         empty
    321           1         Main.txt (1)  empty
    322           2         Main.txt (2)  1.txt
    323           3         Main.txt (3)  1.txt, 2.txt
    324    */
    325    /**
    326     *  {@inheritDoc}
    327     */
    328    @Override
    329    public synchronized void putPageText( final Page page, final String text ) throws ProviderException {
    330        // This is a bit complicated.  We'll first need to copy the old file to be the newest file.
    331        final int  latest  = findLatestVersion( page.getName() );
    332        final File pageDir = findOldPageDir( page.getName() );
    333        if( !pageDir.exists() ) {
    334            pageDir.mkdirs();
    335        }
    336
    337        try {
    338            // Copy old data to safety, if one exists.
    339            final File oldFile = findPage( page.getName() );
    340
    341            // Figure out which version should the old page be? Numbers should always start at 1.
    342            // "most recent" = -1 ==> 1
    343            // "first"       = 1  ==> 2
    344            int versionNumber = (latest > 0) ? latest : 1;
    345            final boolean firstUpdate = (versionNumber == 1);
    346
    347            if( oldFile != null && oldFile.exists() ) {
    348                final File pageFile = new File( pageDir, versionNumber + FILE_EXT );
    349                try( final InputStream in = new BufferedInputStream( Files.newInputStream( oldFile.toPath() ) );
    350                     final OutputStream out = new BufferedOutputStream( Files.newOutputStream( pageFile.toPath() ) ) ) {
    351                    FileUtil.copyContents( in, out );
    352
    353                    // We need also to set the date, since we rely on this.
    354                    pageFile.setLastModified( oldFile.lastModified() );
    355
    356                    // Kludge to make the property code to work properly.
    357                    versionNumber++;
    358                }
    359            }
    360
    361            //  Let superclass handler writing data to a new version.
    362            super.putPageText( page, text );
    363
    364            //  Finally, write page version data.
    365            // FIXME: No rollback available.
    366            final Properties props = getPageProperties( page.getName() );
    367
    368            String authorFirst = null;
    369            // if the following file exists, we are NOT migrating from FileSystemProvider
    370            final File pagePropFile = new File(getPageDirectory() + File.separator + PAGEDIR + File.separator + mangleName(page.getName()) + File.separator + "page" + FileSystemProvider.PROP_EXT);
    371            if( firstUpdate && ! pagePropFile.exists() ) {
    372                // we might not yet have a versioned author because the old page was last maintained by FileSystemProvider
    373                final Properties props2 = getHeritagePageProperties( page.getName() );
    374
    375                // remember the simulated original author (or something) in the new properties
    376                authorFirst = props2.getProperty( "1.author", "unknown" );
    377                props.setProperty( "1.author", authorFirst );
    378            }
    379
    380            String newAuthor = page.getAuthor();
    381            if ( newAuthor == null ) {
    382                newAuthor = ( authorFirst != null ) ? authorFirst : "unknown";
    383            }
    384            page.setAuthor(newAuthor);
    385            props.setProperty( versionNumber + ".author", newAuthor );
    386
    387            final String changeNote = page.getAttribute( Page.CHANGENOTE );
    388            if( changeNote != null ) {
    389                props.setProperty( versionNumber + ".changenote", changeNote );
    390            }
    391
    392            // Get additional custom properties from page and add to props
    393            getCustomProperties( page, props );
    394            putPageProperties( page.getName(), props );
    395        } catch( final IOException e ) {
    396            LOG.error( "Saving failed", e );
    397            throw new ProviderException("Could not save page text: "+e.getMessage());
    398        }
    399    }
    400
    401    /**
    402     *  {@inheritDoc}
    403     */
    404    @Override
    405    public Page getPageInfo( final String page, final int version ) throws ProviderException {
    406        final int latest = findLatestVersion( page );
    407        final int realVersion;
    408
    409        Page p = null;
    410
    411        if( version == PageProvider.LATEST_VERSION || version == latest || (version == 1 && latest == -1) ) {
    412            //
    413            // Yes, we need to talk to the top level directory to get this version.
    414            //
    415            // I am listening to Press Play On Tape's guitar version of the good old C64 "Wizardry" -tune at this moment.
    416            // Oh, the memories...
    417            //
    418            realVersion = (latest >= 0) ? latest : 1;
    419
    420            p = super.getPageInfo( page, PageProvider.LATEST_VERSION );
    421
    422            if( p != null ) {
    423                p.setVersion( realVersion );
    424            }
    425        } else {
    426            // The file is not the most recent, so we'll need to find it from the deep trenches of the "OLD" directory structure.
    427            realVersion = version;
    428            final File dir = findOldPageDir( page );
    429            if( !dir.exists() || !dir.isDirectory() ) {
    430                return null;
    431            }
    432
    433            final File file = new File( dir, version + FILE_EXT );
    434            if( file.exists() ) {
    435                p = Wiki.contents().page( m_engine, page );
    436
    437                p.setLastModified( new Date( file.lastModified() ) );
    438                p.setVersion( version );
    439            }
    440        }
    441
    442        //  Get author and other metadata information (Modification date has already been set.)
    443        if( p != null ) {
    444            try {
    445                final Properties props = getPageProperties( page );
    446                String author = props.getProperty( realVersion + ".author" );
    447                if( author == null ) {
    448                    // we might not have a versioned author because the old page was last maintained by FileSystemProvider
    449                    final Properties props2 = getHeritagePageProperties( page );
    450                    author = props2.getProperty( Page.AUTHOR );
    451                }
    452                if( author != null ) {
    453                    p.setAuthor( author );
    454                }
    455
    456                final String changenote = props.getProperty( realVersion + ".changenote" );
    457                if( changenote != null ) {
    458                    p.setAttribute( Page.CHANGENOTE, changenote );
    459                }
    460
    461                // Set the props values to the page attributes
    462                setCustomProperties( p, props );
    463            } catch( final IOException e ) {
    464                LOG.error( "Cannot get author for page" + page + ": ", e );
    465            }
    466        }
    467
    468        return p;
    469    }
    470
    471    /**
    472     *  {@inheritDoc}
    473     */
    474    @Override
    475    public boolean pageExists( final String pageName, final int version ) {
    476        if (version == PageProvider.LATEST_VERSION || version == findLatestVersion( pageName ) ) {
    477            return pageExists(pageName);
    478        }
    479
    480        final File dir = findOldPageDir( pageName );
    481        if( !dir.exists() || !dir.isDirectory() ) {
    482            return false;
    483        }
    484
    485        return new File( dir, version + FILE_EXT ).exists();
    486    }
    487
    488    /**
    489     *  {@inheritDoc}
    490     */
    491     // FIXME: Does not get user information.
    492    @Override
    493    public List< Page > getVersionHistory( final String page ) throws ProviderException {
    494        final ArrayList< Page > list = new ArrayList<>();
    495        final int latest = findLatestVersion( page );
    496        for( int i = latest; i > 0; i-- ) {
    497            final Page info = getPageInfo( page, i );
    498            if( info != null ) {
    499                list.add( info );
    500            }
    501        }
    502
    503        return list;
    504    }
    505
    506    /*
    507     * Support for migration of simple properties created by the FileSystemProvider when coming under Versioning management.
    508     * Simulate an initial version.
    509     */
    510    private Properties getHeritagePageProperties( final String page ) throws IOException {
    511        final File propertyFile = new File( getPageDirectory(), mangleName( page ) + FileSystemProvider.PROP_EXT );
    512        if ( propertyFile.exists() ) {
    513            final long lastModified = propertyFile.lastModified();
    514
    515            CachedProperties cp = m_cachedProperties;
    516            if ( cp != null && cp.m_page.equals(page) && cp.m_lastModified == lastModified ) {
    517                return cp.m_props;
    518            }
    519
    520            try( final InputStream in = new BufferedInputStream( Files.newInputStream( propertyFile.toPath() ) ) ) {
    521                final Properties props = new Properties();
    522                props.load( in );
    523
    524                final String originalAuthor = props.getProperty( Page.AUTHOR );
    525                if ( !originalAuthor.isEmpty() ) {
    526                    // simulate original author as if already versioned but put non-versioned property in special cache too
    527                    props.setProperty( "1.author", originalAuthor );
    528
    529                    // The profiler showed the probability was very high that when calling for the history of a page the
    530                    // propertyfile would be read as much times as there were versions of that file. It is statistically
    531                    // likely the propertyfile will be examined many times before it is updated.
    532                    cp = new CachedProperties( page, props, propertyFile.lastModified() );
    533                    m_cachedProperties = cp; // Atomic
    534                }
    535
    536                return props;
    537            }
    538        }
    539
    540        return new Properties(); // Returns an empty object
    541    }
    542
    543    /**
    544     *  Removes the relevant page directory under "OLD" -directory as well, but does not remove any extra subdirectories from it.
    545     *  It will only touch those files that it thinks to be WikiPages.
    546     *
    547     *  @param page {@inheritDoc}
    548     *  @throws {@inheritDoc}
    549     */
    550    @Override
    551    public void deletePage( final String page ) throws ProviderException {
    552        super.deletePage( page );
    553        final File dir = findOldPageDir( page );
    554        if( dir.exists() && dir.isDirectory() ) {
    555            final File[] files = dir.listFiles( new WikiFileFilter() );
    556            for( final File file : files ) {
    557                if (!file.delete()) {
    558                    LOG.warn("Failed to delete " + file.getAbsolutePath());
    559                }
    560            }
    561
    562            final File propfile = new File( dir, PROPERTYFILE );
    563            if( propfile.exists() ) {
    564                if (!propfile.delete()) {
    565                    LOG.warn("Failed to delete " + propfile.getAbsolutePath());
    566                }
    567            }
    568
    569            dir.delete();
    570        }
    571    }
    572
    573    /**
    574     *  {@inheritDoc}
    575     *
    576     *  Deleting versions has never really worked, JSPWiki assumes that version histories are "not gappy". Using deleteVersion() is
    577     *  definitely not recommended.
    578     */
    579    @Override
    580    public void deleteVersion( final String page, final int version ) throws ProviderException {
    581        final File dir = findOldPageDir( page );
    582        int latest = findLatestVersion( page );
    583        if( version == PageProvider.LATEST_VERSION ||
    584            version == latest ||
    585            (version == 1 && latest == -1) ) {
    586            //  Delete the properties
    587            try {
    588                final Properties props = getPageProperties( page );
    589                props.remove( ((latest > 0) ? latest : 1)+".author" );
    590                putPageProperties( page, props );
    591            } catch( final IOException e ) {
    592                LOG.error("Unable to modify page properties",e);
    593                throw new ProviderException("Could not modify page properties: " + e.getMessage());
    594            }
    595
    596            // We can let the FileSystemProvider take care of the actual deletion
    597            super.deleteVersion( page, PageProvider.LATEST_VERSION );
    598
    599            //  Copy the old file to the new location
    600            latest = findLatestVersion( page );
    601
    602            final File pageDir = findOldPageDir( page );
    603            final File previousFile = new File( pageDir, latest + FILE_EXT );
    604            final File pageFile = findPage(page);
    605            try( final InputStream in = new BufferedInputStream( Files.newInputStream( previousFile.toPath() ) );
    606                 final OutputStream out = new BufferedOutputStream( Files.newOutputStream( pageFile.toPath() ) ) ) {
    607                if( previousFile.exists() ) {
    608                    FileUtil.copyContents( in, out );
    609                    // We need also to set the date, since we rely on this.
    610                    pageFile.setLastModified( previousFile.lastModified() );
    611                }
    612            } catch( final IOException e ) {
    613                LOG.fatal("Something wrong with the page directory - you may have just lost data!",e);
    614            }
    615
    616            return;
    617        }
    618
    619        final File pageFile = new File( dir, ""+version+FILE_EXT );
    620        if( pageFile.exists() ) {
    621            if( !pageFile.delete() ) {
    622                LOG.error("Unable to delete page." + pageFile.getPath() );
    623            }
    624        } else {
    625            throw new NoSuchVersionException("Page "+page+", version="+version);
    626        }
    627    }
    628
    629    /**
    630     *  {@inheritDoc}
    631     */
    632    // FIXME: This is kinda slow, we should need to do this only once.
    633    @Override
    634    public Collection< Page > getAllPages() throws ProviderException {
    635        final Collection< Page > pages = super.getAllPages();
    636        final Collection< Page > returnedPages = new ArrayList<>();
    637        for( final Page page : pages ) {
    638            final Page info = getPageInfo( page.getName(), WikiProvider.LATEST_VERSION );
    639            returnedPages.add( info );
    640        }
    641
    642        return returnedPages;
    643    }
    644
    645    /**
    646     *  {@inheritDoc}
    647     */
    648    @Override
    649    public String getProviderInfo()
    650    {
    651        return "";
    652    }
    653
    654    /**
    655     *  {@inheritDoc}
    656     */
    657    @Override
    658    public void movePage( final String from, final String to ) {
    659        // Move the file itself
    660        final File fromFile = findPage( from );
    661        final File toFile = findPage( to );
    662        fromFile.renameTo( toFile );
    663
    664        // Move any old versions
    665        final File fromOldDir = findOldPageDir( from );
    666        final File toOldDir = findOldPageDir( to );
    667        fromOldDir.renameTo( toOldDir );
    668    }
    669
    670    /*
    671     * The profiler showed that when calling the history of a page, the propertyfile was read just as many
    672     * times as there were versions of that file. The loading of a propertyfile is a cpu-intensive job.
    673     * This Class holds onto the last propertyfile read, because the probability is high that the next call
    674     * will with ask for the same propertyfile. The time it took to show a historypage with 267 versions dropped
    675     * by 300%. Although each propertyfile in a history could be cached, there is likely to be little performance
    676     * gain over simply keeping the last one requested.
    677     */
    678    private static class CachedProperties {
    679        String m_page;
    680        Properties m_props;
    681        long m_lastModified;
    682
    683        /**
    684         * Because a Constructor is inherently synchronised, there is no need to synchronise the arguments.
    685         *
    686         * @param pageName page name
    687         * @param props  Properties to use for initialization
    688         * @param lastModified last modified date
    689         */
    690        public CachedProperties( final String pageName, final Properties props, final long lastModified ) {
    691            if ( pageName == null ) {
    692                throw new NullPointerException ( "pageName must not be null!" );
    693            }
    694            this.m_page = pageName;
    695            if ( props == null ) {
    696                throw new NullPointerException ( "properties must not be null!" );
    697            }
    698            m_props = props;
    699            this.m_lastModified = lastModified;
    700        }
    701    }
    702
    703}