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