| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| package org.apache.wiki.providers; |
| |
| import org.apache.log4j.Logger; |
| import org.apache.wiki.InternalWikiException; |
| import org.apache.wiki.WikiPage; |
| import org.apache.wiki.api.core.Engine; |
| import org.apache.wiki.api.core.Page; |
| import org.apache.wiki.api.exceptions.NoRequiredPropertyException; |
| import org.apache.wiki.api.exceptions.ProviderException; |
| import org.apache.wiki.api.providers.PageProvider; |
| import org.apache.wiki.api.providers.WikiProvider; |
| import org.apache.wiki.util.FileUtil; |
| |
| import java.io.BufferedInputStream; |
| import java.io.BufferedOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Properties; |
| |
| /** |
| * Provides a simple directory based repository for Wiki pages. |
| * Pages are held in a directory structure: |
| * <PRE> |
| * Main.txt |
| * Foobar.txt |
| * OLD/ |
| * Main/ |
| * 1.txt |
| * 2.txt |
| * page.properties |
| * Foobar/ |
| * page.properties |
| * </PRE> |
| * |
| * In this case, "Main" has three versions, and "Foobar" just one version. |
| * <P> |
| * The properties file contains the necessary metainformation (such as author) |
| * information of the page. DO NOT MESS WITH IT! |
| * |
| * <P> |
| * All files have ".txt" appended to make life easier for those |
| * who insist on using Windows or other software which makes assumptions |
| * on the files contents based on its name. |
| * |
| */ |
| public class VersioningFileProvider extends AbstractFileProvider { |
| |
| private static final Logger log = Logger.getLogger(VersioningFileProvider.class); |
| |
| /** Name of the directory where the old versions are stored. */ |
| public static final String PAGEDIR = "OLD"; |
| |
| /** Name of the property file which stores the metadata. */ |
| public static final String PROPERTYFILE = "page.properties"; |
| |
| private CachedProperties m_cachedProperties; |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException { |
| super.initialize( engine, properties ); |
| // some additional sanity checks : |
| final File oldpages = new File( getPageDirectory(), PAGEDIR ); |
| if( !oldpages.exists() ) { |
| if( !oldpages.mkdirs() ) { |
| throw new IOException( "Failed to create page version directory " + oldpages.getAbsolutePath() ); |
| } |
| } else { |
| if( !oldpages.isDirectory() ) { |
| throw new IOException( "Page version directory is not a directory: " + oldpages.getAbsolutePath() ); |
| } |
| if( !oldpages.canWrite() ) { |
| throw new IOException( "Page version directory is not writable: " + oldpages.getAbsolutePath() ); |
| } |
| } |
| log.info( "Using directory " + oldpages.getAbsolutePath() + " for storing old versions of pages" ); |
| } |
| |
| /** |
| * Returns the directory where the old versions of the pages |
| * are being kept. |
| */ |
| private File findOldPageDir( final String page ) { |
| if( page == null ) { |
| throw new InternalWikiException( "Page may NOT be null in the provider!" ); |
| } |
| final File oldpages = new File( getPageDirectory(), PAGEDIR ); |
| return new File( oldpages, mangleName( page ) ); |
| } |
| |
| /** |
| * Goes through the repository and decides which version is the newest one in that directory. |
| * |
| * @return Latest version number in the repository, or -1, if there is no page in the repository. |
| */ |
| |
| // FIXME: This is relatively slow. |
| /* |
| private int findLatestVersion( String page ) |
| { |
| File pageDir = findOldPageDir( page ); |
| |
| String[] pages = pageDir.list( new WikiFileFilter() ); |
| |
| if( pages == null ) |
| { |
| return -1; // No such thing found. |
| } |
| |
| int version = -1; |
| |
| for( int i = 0; i < pages.length; i++ ) |
| { |
| int cutpoint = pages[i].indexOf( '.' ); |
| if( cutpoint > 0 ) |
| { |
| String pageNum = pages[i].substring( 0, cutpoint ); |
| |
| try |
| { |
| int res = Integer.parseInt( pageNum ); |
| |
| if( res > version ) |
| { |
| version = res; |
| } |
| } |
| catch( NumberFormatException e ) {} // It's okay to skip these. |
| } |
| } |
| |
| return version; |
| } |
| */ |
| private int findLatestVersion( final String page ) { |
| int version = -1; |
| |
| try { |
| final Properties props = getPageProperties( page ); |
| |
| for( final Object o : props.keySet() ) { |
| final String key = ( String )o; |
| if( key.endsWith( ".author" ) ) { |
| final int cutpoint = key.indexOf( '.' ); |
| if( cutpoint > 0 ) { |
| final String pageNum = key.substring( 0, cutpoint ); |
| |
| try { |
| final int res = Integer.parseInt( pageNum ); |
| |
| if( res > version ) { |
| version = res; |
| } |
| } catch( final NumberFormatException e ) { |
| } // It's okay to skip these. |
| } |
| } |
| } |
| } catch( final IOException e ) { |
| log.error( "Unable to figure out latest version - dying...", e ); |
| } |
| |
| return version; |
| } |
| |
| /** |
| * Reads page properties from the file system. |
| */ |
| private Properties getPageProperties( final String page ) throws IOException { |
| final File propertyFile = new File( findOldPageDir(page), PROPERTYFILE ); |
| if( propertyFile.exists() ) { |
| final long lastModified = propertyFile.lastModified(); |
| |
| // |
| // The profiler showed that when calling the history of a page the propertyfile |
| // was read just as much times as there were versions of that file. The loading |
| // of a propertyfile is a cpu-intensive job. So now hold on to the last propertyfile |
| // read because the next method will with a high probability ask for the same propertyfile. |
| // The time it took to show a historypage with 267 versions dropped with 300%. |
| // |
| |
| CachedProperties cp = m_cachedProperties; |
| |
| if( cp != null && cp.m_page.equals( page ) && cp.m_lastModified == lastModified ) { |
| return cp.m_props; |
| } |
| |
| try( final InputStream in = new BufferedInputStream(new FileInputStream( propertyFile ) ) ) { |
| final Properties props = new Properties(); |
| props.load( in ); |
| cp = new CachedProperties( page, props, lastModified ); |
| m_cachedProperties = cp; // Atomic |
| |
| return props; |
| } |
| } |
| |
| return new Properties(); // Returns an empty object |
| } |
| |
| /** |
| * Writes the page properties back to the file system. |
| * Note that it WILL overwrite any previous properties. |
| */ |
| private void putPageProperties( final String page, final Properties properties ) throws IOException { |
| final File propertyFile = new File( findOldPageDir(page), PROPERTYFILE ); |
| try( final OutputStream out = new FileOutputStream( propertyFile ) ) { |
| properties.store( out, " JSPWiki page properties for "+page+". DO NOT MODIFY!" ); |
| } |
| |
| // The profiler showed the probability was very high that when calling for the history of |
| // a page the propertyfile would be read as much times as there were versions of that file. |
| // It is statistically likely the propertyfile will be examined many times before it is updated. |
| final CachedProperties cp = new CachedProperties( page, properties, propertyFile.lastModified() ); |
| m_cachedProperties = cp; // Atomic |
| } |
| |
| /** |
| * Figures out the real version number of the page and also checks for its existence. |
| * |
| * @throws NoSuchVersionException if there is no such version. |
| */ |
| private int realVersion( final String page, final int requestedVersion ) throws NoSuchVersionException { |
| // Quickly check for the most common case. |
| if( requestedVersion == WikiProvider.LATEST_VERSION ) { |
| return -1; |
| } |
| |
| final int latest = findLatestVersion(page); |
| |
| if( requestedVersion == latest || (requestedVersion == 1 && latest == -1 ) ) { |
| return -1; |
| } else if( requestedVersion <= 0 || requestedVersion > latest ) { |
| throw new NoSuchVersionException( "Requested version " + requestedVersion + ", but latest is " + latest ); |
| } |
| |
| return requestedVersion; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public synchronized String getPageText( final String page, int version ) throws ProviderException { |
| final File dir = findOldPageDir( page ); |
| |
| version = realVersion( page, version ); |
| if( version == -1 ) { |
| // We can let the FileSystemProvider take care of these requests. |
| return super.getPageText( page, PageProvider.LATEST_VERSION ); |
| } |
| |
| final File pageFile = new File( dir, ""+version+FILE_EXT ); |
| if( !pageFile.exists() ) { |
| throw new NoSuchVersionException("Version "+version+"does not exist."); |
| } |
| |
| return readFile( pageFile ); |
| } |
| |
| |
| // FIXME: Should this really be here? |
| private String readFile( final File pagedata ) throws ProviderException { |
| String result = null; |
| if( pagedata.exists() ) { |
| if( pagedata.canRead() ) { |
| try( final InputStream in = new FileInputStream( pagedata ) ) { |
| result = FileUtil.readContents( in, m_encoding ); |
| } catch( final IOException e ) { |
| log.error("Failed to read", e); |
| throw new ProviderException("I/O error: "+e.getMessage()); |
| } |
| } else { |
| log.warn("Failed to read page from '"+pagedata.getAbsolutePath()+"', possibly a permissions problem"); |
| throw new ProviderException("I cannot read the requested page."); |
| } |
| } else { |
| // This is okay. |
| // FIXME: is it? |
| log.info("New page"); |
| } |
| |
| return result; |
| } |
| |
| // FIXME: This method has no rollback whatsoever. |
| |
| /* |
| This is how the page directory should look like: |
| |
| version pagedir olddir |
| none empty empty |
| 1 Main.txt (1) empty |
| 2 Main.txt (2) 1.txt |
| 3 Main.txt (3) 1.txt, 2.txt |
| */ |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public synchronized void putPageText( final Page page, final String text ) throws ProviderException { |
| // This is a bit complicated. We'll first need to copy the old file to be the newest file. |
| final int latest = findLatestVersion( page.getName() ); |
| final File pageDir = findOldPageDir( page.getName() ); |
| if( !pageDir.exists() ) { |
| pageDir.mkdirs(); |
| } |
| |
| try { |
| // Copy old data to safety, if one exists. |
| final File oldFile = findPage( page.getName() ); |
| |
| // Figure out which version should the old page be? Numbers should always start at 1. |
| // "most recent" = -1 ==> 1 |
| // "first" = 1 ==> 2 |
| int versionNumber = (latest > 0) ? latest : 1; |
| final boolean firstUpdate = (versionNumber == 1); |
| |
| if( oldFile != null && oldFile.exists() ) { |
| final File pageFile = new File( pageDir, versionNumber + FILE_EXT ); |
| try( final InputStream in = new BufferedInputStream( new FileInputStream( oldFile ) ); |
| final OutputStream out = new BufferedOutputStream( new FileOutputStream( pageFile ) ) ) { |
| FileUtil.copyContents( in, out ); |
| |
| // We need also to set the date, since we rely on this. |
| pageFile.setLastModified( oldFile.lastModified() ); |
| |
| // Kludge to make the property code to work properly. |
| versionNumber++; |
| } |
| } |
| |
| // Let superclass handler writing data to a new version. |
| super.putPageText( page, text ); |
| |
| // Finally, write page version data. |
| // FIXME: No rollback available. |
| final Properties props = getPageProperties( page.getName() ); |
| |
| String authorFirst = null; |
| // if the following file exists, we are NOT migrating from FileSystemProvider |
| final File pagePropFile = new File(getPageDirectory() + File.separator + PAGEDIR + File.separator + mangleName(page.getName()) + File.separator + "page" + FileSystemProvider.PROP_EXT); |
| if( firstUpdate && ! pagePropFile.exists() ) { |
| // we might not yet have a versioned author because the old page was last maintained by FileSystemProvider |
| final Properties props2 = getHeritagePageProperties( page.getName() ); |
| |
| // remember the simulated original author (or something) in the new properties |
| authorFirst = props2.getProperty( "1.author", "unknown" ); |
| props.setProperty( "1.author", authorFirst ); |
| } |
| |
| String newAuthor = page.getAuthor(); |
| if ( newAuthor == null ) { |
| newAuthor = ( authorFirst != null ) ? authorFirst : "unknown"; |
| } |
| page.setAuthor(newAuthor); |
| props.setProperty( versionNumber + ".author", newAuthor ); |
| |
| final String changeNote = page.getAttribute( Page.CHANGENOTE ); |
| if( changeNote != null ) { |
| props.setProperty( versionNumber + ".changenote", changeNote ); |
| } |
| |
| // Get additional custom properties from page and add to props |
| getCustomProperties( page, props ); |
| putPageProperties( page.getName(), props ); |
| } catch( final IOException e ) { |
| log.error( "Saving failed", e ); |
| throw new ProviderException("Could not save page text: "+e.getMessage()); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Page getPageInfo( final String page, final int version ) throws ProviderException { |
| final int latest = findLatestVersion( page ); |
| final int realVersion; |
| |
| Page p = null; |
| |
| if( version == PageProvider.LATEST_VERSION || version == latest || (version == 1 && latest == -1) ) { |
| // |
| // Yes, we need to talk to the top level directory to get this version. |
| // |
| // I am listening to Press Play On Tape's guitar version of the good old C64 "Wizardry" -tune at this moment. |
| // Oh, the memories... |
| // |
| realVersion = (latest >= 0) ? latest : 1; |
| |
| p = super.getPageInfo( page, PageProvider.LATEST_VERSION ); |
| |
| if( p != null ) { |
| p.setVersion( realVersion ); |
| } |
| } else { |
| // The file is not the most recent, so we'll need to find it from the deep trenches of the "OLD" directory structure. |
| realVersion = version; |
| final File dir = findOldPageDir( page ); |
| if( !dir.exists() || !dir.isDirectory() ) { |
| return null; |
| } |
| |
| final File file = new File( dir, version + FILE_EXT ); |
| if( file.exists() ) { |
| p = new WikiPage( m_engine, page ); |
| |
| p.setLastModified( new Date( file.lastModified() ) ); |
| p.setVersion( version ); |
| } |
| } |
| |
| // Get author and other metadata information (Modification date has already been set.) |
| if( p != null ) { |
| try { |
| final Properties props = getPageProperties( page ); |
| String author = props.getProperty( realVersion + ".author" ); |
| if( author == null ) { |
| // we might not have a versioned author because the old page was last maintained by FileSystemProvider |
| final Properties props2 = getHeritagePageProperties( page ); |
| author = props2.getProperty( Page.AUTHOR ); |
| } |
| if( author != null ) { |
| p.setAuthor( author ); |
| } |
| |
| final String changenote = props.getProperty( realVersion + ".changenote" ); |
| if( changenote != null ) { |
| p.setAttribute( Page.CHANGENOTE, changenote ); |
| } |
| |
| // Set the props values to the page attributes |
| setCustomProperties( p, props ); |
| } catch( final IOException e ) { |
| log.error( "Cannot get author for page" + page + ": ", e ); |
| } |
| } |
| |
| return p; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean pageExists( final String pageName, final int version ) { |
| if (version == PageProvider.LATEST_VERSION || version == findLatestVersion( pageName ) ) { |
| return pageExists(pageName); |
| } |
| |
| final File dir = findOldPageDir( pageName ); |
| if( !dir.exists() || !dir.isDirectory() ) { |
| return false; |
| } |
| |
| return new File( dir, version + FILE_EXT ).exists(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| // FIXME: Does not get user information. |
| @Override |
| public List< Page > getVersionHistory( final String page ) throws ProviderException { |
| final ArrayList< Page > list = new ArrayList<>(); |
| final int latest = findLatestVersion( page ); |
| for( int i = latest; i > 0; i-- ) { |
| final Page info = getPageInfo( page, i ); |
| if( info != null ) { |
| list.add( info ); |
| } |
| } |
| |
| return list; |
| } |
| |
| /* |
| * Support for migration of simple properties created by the FileSystemProvider when coming under Versioning management. |
| * Simulate an initial version. |
| */ |
| private Properties getHeritagePageProperties( final String page ) throws IOException { |
| final File propertyFile = new File( getPageDirectory(), mangleName( page ) + FileSystemProvider.PROP_EXT ); |
| if ( propertyFile.exists() ) { |
| final long lastModified = propertyFile.lastModified(); |
| |
| CachedProperties cp = m_cachedProperties; |
| if ( cp != null && cp.m_page.equals(page) && cp.m_lastModified == lastModified ) { |
| return cp.m_props; |
| } |
| |
| try( final InputStream in = new BufferedInputStream( new FileInputStream( propertyFile ) ) ) { |
| final Properties props = new Properties(); |
| props.load( in ); |
| |
| final String originalAuthor = props.getProperty( Page.AUTHOR ); |
| if ( originalAuthor.length() > 0 ) { |
| // simulate original author as if already versioned but put non-versioned property in special cache too |
| props.setProperty( "1.author", originalAuthor ); |
| |
| // The profiler showed the probability was very high that when calling for the history of a page the |
| // propertyfile would be read as much times as there were versions of that file. It is statistically |
| // likely the propertyfile will be examined many times before it is updated. |
| cp = new CachedProperties( page, props, propertyFile.lastModified() ); |
| m_cachedProperties = cp; // Atomic |
| } |
| |
| return props; |
| } |
| } |
| |
| return new Properties(); // Returns an empty object |
| } |
| |
| /** |
| * Removes the relevant page directory under "OLD" -directory as well, but does not remove any extra subdirectories from it. |
| * It will only touch those files that it thinks to be WikiPages. |
| * |
| * @param page {@inheritDoc} |
| * @throws {@inheritDoc} |
| */ |
| // FIXME: Should log errors. |
| @Override |
| public void deletePage( final String page ) throws ProviderException { |
| super.deletePage( page ); |
| final File dir = findOldPageDir( page ); |
| if( dir.exists() && dir.isDirectory() ) { |
| final File[] files = dir.listFiles( new WikiFileFilter() ); |
| for( int i = 0; i < files.length; i++ ) { |
| files[ i ].delete(); |
| } |
| |
| final File propfile = new File( dir, PROPERTYFILE ); |
| if( propfile.exists() ) { |
| propfile.delete(); |
| } |
| |
| dir.delete(); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * Deleting versions has never really worked, JSPWiki assumes that version histories are "not gappy". Using deleteVersion() is |
| * definitely not recommended. |
| */ |
| @Override |
| public void deleteVersion( final String page, final int version ) throws ProviderException { |
| final File dir = findOldPageDir( page ); |
| int latest = findLatestVersion( page ); |
| if( version == PageProvider.LATEST_VERSION || |
| version == latest || |
| (version == 1 && latest == -1) ) { |
| // Delete the properties |
| try { |
| final Properties props = getPageProperties( page ); |
| props.remove( ((latest > 0) ? latest : 1)+".author" ); |
| putPageProperties( page, props ); |
| } catch( final IOException e ) { |
| log.error("Unable to modify page properties",e); |
| throw new ProviderException("Could not modify page properties: " + e.getMessage()); |
| } |
| |
| // We can let the FileSystemProvider take care of the actual deletion |
| super.deleteVersion( page, PageProvider.LATEST_VERSION ); |
| |
| // Copy the old file to the new location |
| latest = findLatestVersion( page ); |
| |
| final File pageDir = findOldPageDir( page ); |
| final File previousFile = new File( pageDir, latest + FILE_EXT ); |
| final File pageFile = findPage(page); |
| try( final InputStream in = new BufferedInputStream( new FileInputStream( previousFile ) ); |
| final OutputStream out = new BufferedOutputStream( new FileOutputStream( pageFile ) ) ) { |
| if( previousFile.exists() ) { |
| FileUtil.copyContents( in, out ); |
| // We need also to set the date, since we rely on this. |
| pageFile.setLastModified( previousFile.lastModified() ); |
| } |
| } catch( final IOException e ) { |
| log.fatal("Something wrong with the page directory - you may have just lost data!",e); |
| } |
| |
| return; |
| } |
| |
| final File pageFile = new File( dir, ""+version+FILE_EXT ); |
| if( pageFile.exists() ) { |
| if( !pageFile.delete() ) { |
| log.error("Unable to delete page." + pageFile.getPath() ); |
| } |
| } else { |
| throw new NoSuchVersionException("Page "+page+", version="+version); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| // FIXME: This is kinda slow, we should need to do this only once. |
| @Override |
| public Collection< Page > getAllPages() throws ProviderException { |
| final Collection< Page > pages = super.getAllPages(); |
| final Collection< Page > returnedPages = new ArrayList<>(); |
| for( final Page page : pages ) { |
| final Page info = getPageInfo( page.getName(), WikiProvider.LATEST_VERSION ); |
| returnedPages.add( info ); |
| } |
| |
| return returnedPages; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getProviderInfo() |
| { |
| return ""; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void movePage( final String from, final String to ) { |
| // Move the file itself |
| final File fromFile = findPage( from ); |
| final File toFile = findPage( to ); |
| fromFile.renameTo( toFile ); |
| |
| // Move any old versions |
| final File fromOldDir = findOldPageDir( from ); |
| final File toOldDir = findOldPageDir( to ); |
| fromOldDir.renameTo( toOldDir ); |
| } |
| |
| /* |
| * The profiler showed that when calling the history of a page, the propertyfile was read just as many |
| * times as there were versions of that file. The loading of a propertyfile is a cpu-intensive job. |
| * This Class holds onto the last propertyfile read, because the probability is high that the next call |
| * will with ask for the same propertyfile. The time it took to show a historypage with 267 versions dropped |
| * by 300%. Although each propertyfile in a history could be cached, there is likely to be little performance |
| * gain over simply keeping the last one requested. |
| */ |
| private static class CachedProperties { |
| String m_page; |
| Properties m_props; |
| long m_lastModified; |
| |
| /** |
| * Because a Constructor is inherently synchronised, there is no need to synchronise the arguments. |
| * |
| * @param pageName page name |
| * @param props Properties to use for initialization |
| * @param lastModified last modified date |
| */ |
| public CachedProperties( final String pageName, final Properties props, final long lastModified ) { |
| if ( pageName == null ) { |
| throw new NullPointerException ( "pageName must not be null!" ); |
| } |
| this.m_page = pageName; |
| if ( props == null ) { |
| throw new NullPointerException ( "properties must not be null!" ); |
| } |
| m_props = props; |
| this.m_lastModified = lastModified; |
| } |
| } |
| |
| } |