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