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