blob: 297f652714f219063a0dd9ac426ac44175c4afa2 [file] [log] [blame]
/*
JSPWiki - a JSP-based WikiWiki clone.
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 com.ecyrd.jspwiki.providers;
import java.io.*;
import java.util.Collection;
import java.util.Iterator;
import java.util.Properties;
import java.util.Date;
import java.util.ArrayList;
import java.util.List;
import org.apache.jspwiki.api.WikiPage;
import com.ecyrd.jspwiki.content.ContentManager;
import com.ecyrd.jspwiki.content.WikiName;
import com.ecyrd.jspwiki.log.Logger;
import com.ecyrd.jspwiki.log.LoggerFactory;
import com.ecyrd.jspwiki.util.FileUtil;
import com.ecyrd.jspwiki.*;
/**
* 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
implements VersioningProvider
{
private static final Logger log = LoggerFactory.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}
*/
public void initialize( WikiEngine engine, Properties properties )
throws NoRequiredPropertyException,
IOException
{
super.initialize( engine, properties );
// some additional sanity checks :
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( String page )
{
if( page == null )
{
throw new InternalWikiException("Page may NOT be null in the provider!");
}
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( String page )
throws ProviderException
{
int version = -1;
try
{
Properties props = getPageProperties( page );
for( Iterator i = props.keySet().iterator(); i.hasNext(); )
{
String key = (String)i.next();
if( key.endsWith(".author") )
{
int cutpoint = key.indexOf('.');
if( cutpoint > 0 )
{
String pageNum = key.substring(0,cutpoint);
try
{
int res = Integer.parseInt( pageNum );
if( res > version )
{
version = res;
}
}
catch( NumberFormatException e ) {} // It's okay to skip these.
}
}
}
}
catch( IOException e )
{
log.error("Unable to figure out latest version - dying...",e);
}
return version;
}
/**
* Reads page properties from the file system.
*/
private Properties getPageProperties( String page )
throws IOException
{
File propertyFile = new File( findOldPageDir(page), PROPERTYFILE );
if( propertyFile.exists() )
{
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;
}
InputStream in = null;
try
{
in = new BufferedInputStream(new FileInputStream( propertyFile ));
Properties props = new Properties();
props.load(in);
cp = new CachedProperties();
cp.m_page = page;
cp.m_lastModified = lastModified;
cp.m_props = props;
m_cachedProperties = cp; // Atomic
return props;
}
finally
{
if( in != null ) in.close();
}
}
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( String page, Properties properties )
throws IOException
{
File propertyFile = new File( findOldPageDir(page), PROPERTYFILE );
OutputStream out = null;
try
{
out = new FileOutputStream( propertyFile );
properties.store( out, " JSPWiki page properties for "+page+". DO NOT MODIFY!" );
}
finally
{
if( out != null ) out.close();
}
}
/**
* 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( String page, int requestedVersion )
throws NoSuchVersionException,
ProviderException
{
//
// Quickly check for the most common case.
//
if( requestedVersion == WikiProvider.LATEST_VERSION )
{
return -1;
}
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}
*/
public synchronized String getPageText( String page, int version )
throws ProviderException
{
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, WikiPageProvider.LATEST_VERSION );
}
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( File pagedata )
throws ProviderException
{
String result = null;
InputStream in = null;
if( pagedata.exists() )
{
if( pagedata.canRead() )
{
try
{
in = new FileInputStream( pagedata );
result = FileUtil.readContents( in, m_encoding );
}
catch( IOException e )
{
log.error("Failed to read", e);
throw new ProviderException("I/O error: "+e.getMessage());
}
finally
{
try
{
if( in != null ) in.close();
}
catch( Exception e )
{
log.error("Closing failed",e);
}
}
}
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}
*/
public synchronized void putPageText( WikiPage page, String text )
throws ProviderException
{
//
// This is a bit complicated. We'll first need to
// copy the old file to be the newest file.
//
File pageDir = findOldPageDir( page.getName() );
if( !pageDir.exists() )
{
pageDir.mkdirs();
}
int latest = findLatestVersion( page.getName() );
try
{
//
// Copy old data to safety, if one exists.
//
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;
if( oldFile != null && oldFile.exists() )
{
InputStream in = null;
OutputStream out = null;
try
{
in = new BufferedInputStream( new FileInputStream( oldFile ) );
File pageFile = new File( pageDir, Integer.toString( versionNumber )+FILE_EXT );
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++;
}
finally
{
if( out != null ) out.close();
if( in != null ) in.close();
}
}
//
// Let superclass handler writing data to a new version.
//
super.putPageText( page, text );
//
// Finally, write page version data.
//
// FIXME: No rollback available.
Properties props = getPageProperties( page.getName() );
props.setProperty( versionNumber+".author", (page.getAuthor() != null) ? page.getAuthor() : "unknown" );
String changeNote = (String) page.getAttribute(WikiPage.CHANGENOTE);
if( changeNote != null )
{
props.setProperty( versionNumber+".changenote", changeNote );
}
putPageProperties( page.getName(), props );
}
catch( IOException e )
{
log.error( "Saving failed", e );
throw new ProviderException("Could not save page text: "+e.getMessage());
}
}
/**
* {@inheritDoc}
*/
public WikiPage getPageInfo( String page, int version )
throws ProviderException
{
int latest = findLatestVersion(page);
int realVersion;
WikiPage p = null;
if( version == WikiPageProvider.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, WikiPageProvider.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;
File dir = findOldPageDir( page );
if( !dir.exists() || !dir.isDirectory() )
{
return null;
}
File file = new File( dir, version+FILE_EXT );
if( file.exists() )
{
p = m_engine.createPage( new WikiName(ContentManager.DEFAULT_SPACE, 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
{
Properties props = getPageProperties( page );
String author = props.getProperty( realVersion+".author" );
if( author != null )
{
p.setAuthor( author );
}
String changenote = props.getProperty( realVersion+".changenote" );
if( changenote != null ) p.setAttribute( WikiPage.CHANGENOTE, changenote );
}
catch( IOException e )
{
log.error( "Cannot get author for page"+page+": ", e );
}
}
return p;
}
/**
* {@inheritDoc}
*/
public boolean pageExists( String pageName, int version )
{
File dir = findOldPageDir( pageName );
if( !dir.exists() || !dir.isDirectory() )
{
return false;
}
File file = new File( dir, version+FILE_EXT );
if( file.exists() )
{
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
// FIXME: Does not get user information.
public List getVersionHistory( String page )
throws ProviderException
{
ArrayList<WikiPage> list = new ArrayList<WikiPage>();
int latest = findLatestVersion( page );
// list.add( getPageInfo(page,WikiPageProvider.LATEST_VERSION) );
for( int i = latest; i > 0; i-- )
{
WikiPage info = getPageInfo( page, i );
if( info != null )
{
list.add( info );
}
}
return list;
}
/**
* 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.
public void deletePage( String page )
throws ProviderException
{
super.deletePage( page );
File dir = findOldPageDir( page );
if( dir.exists() && dir.isDirectory() )
{
File[] files = dir.listFiles( new WikiFileFilter() );
for( int i = 0; i < files.length; i++ )
{
files[i].delete();
}
File propfile = new File( dir, PROPERTYFILE );
if( propfile.exists() )
{
propfile.delete();
}
dir.delete();
}
}
/**
* {@inheritDoc}
*/
public void deleteVersion( String page, int version )
throws ProviderException
{
File dir = findOldPageDir( page );
int latest = findLatestVersion( page );
if( version == WikiPageProvider.LATEST_VERSION ||
version == latest ||
(version == 1 && latest == -1) )
{
//
// Delete the properties
//
try
{
Properties props = getPageProperties( page );
props.remove( ((latest > 0) ? latest : 1)+".author" );
putPageProperties( page, props );
}
catch( IOException e )
{
log.error("Unable to modify page properties",e);
throw new ProviderException("Could not modify page properties");
}
// We can let the FileSystemProvider take care
// of the actual deletion
super.deleteVersion( page, WikiPageProvider.LATEST_VERSION );
//
// Copy the old file to the new location
//
latest = findLatestVersion( page );
File pageDir = findOldPageDir( page );
File previousFile = new File( pageDir, Integer.toString(latest)+FILE_EXT );
InputStream in = null;
OutputStream out = null;
try
{
if( previousFile.exists() )
{
in = new BufferedInputStream( new FileInputStream( previousFile ) );
File pageFile = findPage(page);
out = new BufferedOutputStream( new FileOutputStream( pageFile ) );
FileUtil.copyContents( in, out );
//
// We need also to set the date, since we rely on this.
//
pageFile.setLastModified( previousFile.lastModified() );
}
}
catch( IOException e )
{
log.error("Something wrong with the page directory - you may have just lost data!",e);
}
finally
{
try
{
if( in != null ) in.close();
if( out != null) out.close();
}
catch( IOException ex )
{
log.error("Closing failed",ex);
}
}
return;
}
File pageFile = new File( dir, ""+version+FILE_EXT );
if( pageFile.exists() )
{
if( !pageFile.delete() )
{
log.error("Unable to delete page.");
}
}
else
{
throw new NoSuchVersionException("Page "+page+", version="+version);
}
}
/**
* {@inheritDoc}
*/
// FIXME: This is kinda slow, we should need to do this only once.
public Collection getAllPages() throws ProviderException
{
Collection pages = super.getAllPages();
Collection<WikiPage> returnedPages = new ArrayList<WikiPage>();
for( Iterator i = pages.iterator(); i.hasNext(); )
{
WikiPage page = (WikiPage) i.next();
WikiPage info = getPageInfo( page.getName(), WikiProvider.LATEST_VERSION );
returnedPages.add( info );
}
return returnedPages;
}
/**
* {@inheritDoc}
*/
public String getProviderInfo()
{
return "";
}
/**
* {@inheritDoc}
*/
public void movePage( String from,
String to )
throws ProviderException
{
// Move the file itself
File fromFile = findPage( from );
File toFile = findPage( to );
fromFile.renameTo( toFile );
// Move any old versions
File fromOldDir = findOldPageDir( from );
File toOldDir = findOldPageDir( to );
fromOldDir.renameTo( toOldDir );
}
private static class CachedProperties
{
String m_page;
Properties m_props;
long m_lastModified;
}
}