| /* |
| 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 java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.FilenameFilter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.Enumeration; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.TreeSet; |
| |
| import org.apache.commons.io.IOUtils; |
| import org.apache.commons.lang.StringUtils; |
| import org.apache.log4j.Logger; |
| import org.apache.wiki.InternalWikiException; |
| import org.apache.wiki.WikiEngine; |
| import org.apache.wiki.WikiPage; |
| import org.apache.wiki.WikiProvider; |
| import org.apache.wiki.api.exceptions.NoRequiredPropertyException; |
| import org.apache.wiki.api.exceptions.ProviderException; |
| import org.apache.wiki.search.QueryItem; |
| import org.apache.wiki.search.SearchMatcher; |
| import org.apache.wiki.search.SearchResult; |
| import org.apache.wiki.search.SearchResultComparator; |
| import org.apache.wiki.util.FileUtil; |
| import org.apache.wiki.util.TextUtil; |
| |
| |
| /** |
| * Provides a simple directory based repository for Wiki pages. |
| * <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. |
| * <p> |
| * This class functions as a superclass to all file based providers. |
| * |
| * @since 2.1.21. |
| * |
| */ |
| public abstract class AbstractFileProvider |
| implements WikiPageProvider |
| { |
| private static final Logger log = Logger.getLogger(AbstractFileProvider.class); |
| private String m_pageDirectory = "/tmp/"; |
| |
| protected String m_encoding; |
| |
| protected WikiEngine m_engine; |
| |
| public static final String PROP_CUSTOMPROP_MAXLIMIT = "custom.pageproperty.max.allowed"; |
| public static final String PROP_CUSTOMPROP_MAXKEYLENGTH = "custom.pageproperty.key.length"; |
| public static final String PROP_CUSTOMPROP_MAXVALUELENGTH = "custom.pageproperty.value.length"; |
| |
| public static final int DEFAULT_MAX_PROPLIMIT = 200; |
| public static final int DEFAULT_MAX_PROPKEYLENGTH = 255; |
| public static final int DEFAULT_MAX_PROPVALUELENGTH = 4096; |
| |
| /** |
| * This parameter limits the number of custom page properties allowed on a page |
| */ |
| public static int MAX_PROPLIMIT = DEFAULT_MAX_PROPLIMIT; |
| /** |
| * This number limits the length of a custom page property key length |
| * The default value here designed with future JDBC providers in mind. |
| */ |
| public static int MAX_PROPKEYLENGTH = DEFAULT_MAX_PROPKEYLENGTH; |
| /** |
| * This number limits the length of a custom page property value length |
| * The default value here designed with future JDBC providers in mind. |
| */ |
| public static int MAX_PROPVALUELENGTH = DEFAULT_MAX_PROPVALUELENGTH; |
| |
| /** |
| * Name of the property that defines where page directories are. |
| */ |
| public static final String PROP_PAGEDIR = "jspwiki.fileSystemProvider.pageDir"; |
| |
| /** |
| * All files should have this extension to be recognized as JSPWiki files. |
| * We default to .txt, because that is probably easiest for Windows users, |
| * and guarantees correct handling. |
| */ |
| public static final String FILE_EXT = ".txt"; |
| |
| /** The default encoding. */ |
| public static final String DEFAULT_ENCODING = "ISO-8859-1"; |
| |
| private boolean m_windowsHackNeeded = false; |
| |
| /** |
| * {@inheritDoc} |
| * @throws FileNotFoundException If the specified page directory does not exist. |
| * @throws IOException In case the specified page directory is a file, not a directory. |
| */ |
| public void initialize( WikiEngine engine, Properties properties ) |
| throws NoRequiredPropertyException, |
| IOException, FileNotFoundException |
| { |
| log.debug("Initing FileSystemProvider"); |
| m_pageDirectory = TextUtil.getCanonicalFilePathProperty(properties, PROP_PAGEDIR, |
| System.getProperty("user.home") + File.separator + "jspwiki-files"); |
| |
| File f = new File(m_pageDirectory); |
| |
| if( !f.exists() ) |
| { |
| if( !f.mkdirs() ) |
| { |
| throw new IOException( "Failed to create page directory " + f.getAbsolutePath() + " , please check property " |
| + PROP_PAGEDIR ); |
| } |
| } |
| else |
| { |
| if( !f.isDirectory() ) |
| { |
| throw new IOException( "Page directory is not a directory: " + f.getAbsolutePath() ); |
| } |
| if( !f.canWrite() ) |
| { |
| throw new IOException( "Page directory is not writable: " + f.getAbsolutePath() ); |
| } |
| } |
| |
| m_engine = engine; |
| |
| m_encoding = properties.getProperty( WikiEngine.PROP_ENCODING, DEFAULT_ENCODING ); |
| |
| String os = System.getProperty( "os.name" ).toLowerCase(); |
| |
| if( os.startsWith("windows") || os.equals("nt") ) |
| { |
| m_windowsHackNeeded = true; |
| } |
| |
| if (properties != null) { |
| MAX_PROPLIMIT = TextUtil.getIntegerProperty(properties,PROP_CUSTOMPROP_MAXLIMIT,DEFAULT_MAX_PROPLIMIT); |
| MAX_PROPKEYLENGTH = TextUtil.getIntegerProperty(properties,PROP_CUSTOMPROP_MAXKEYLENGTH,DEFAULT_MAX_PROPKEYLENGTH); |
| MAX_PROPVALUELENGTH = TextUtil.getIntegerProperty(properties,PROP_CUSTOMPROP_MAXVALUELENGTH,DEFAULT_MAX_PROPVALUELENGTH); |
| } |
| |
| log.info( "Wikipages are read from '" + m_pageDirectory + "'" ); |
| } |
| |
| |
| String getPageDirectory() |
| { |
| return m_pageDirectory; |
| } |
| |
| private static final String[] WINDOWS_DEVICE_NAMES = |
| { |
| "con", "prn", "nul", "aux", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", |
| "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9" |
| }; |
| |
| /** |
| * This makes sure that the queried page name |
| * is still readable by the file system. For example, all XML entities |
| * and slashes are encoded with the percent notation. |
| * |
| * @param pagename The name to mangle |
| * @return The mangled name. |
| */ |
| protected String mangleName( String pagename ) |
| { |
| pagename = TextUtil.urlEncode( pagename, m_encoding ); |
| |
| pagename = TextUtil.replaceString( pagename, "/", "%2F" ); |
| |
| // |
| // Names which start with a dot must be escaped to prevent problems. |
| // Since we use URL encoding, this is invisible in our unescaping. |
| // |
| if( pagename.startsWith( "." ) ) |
| { |
| pagename = "%2E" + pagename.substring( 1 ); |
| } |
| |
| if( m_windowsHackNeeded ) |
| { |
| String pn = pagename.toLowerCase(); |
| for( int i = 0; i < WINDOWS_DEVICE_NAMES.length; i++ ) |
| { |
| if( WINDOWS_DEVICE_NAMES[i].equals(pn) ) |
| { |
| pagename = "$$$" + pagename; |
| } |
| } |
| } |
| |
| return pagename; |
| } |
| |
| /** |
| * This makes the reverse of mangleName. |
| * |
| * @param filename The filename to unmangle |
| * @return The unmangled name. |
| */ |
| protected String unmangleName( String filename ) |
| { |
| // The exception should never happen. |
| try |
| { |
| if( m_windowsHackNeeded && filename.startsWith( "$$$") && filename.length() > 3 ) |
| { |
| filename = filename.substring(3); |
| } |
| |
| return TextUtil.urlDecode( filename, m_encoding ); |
| } |
| catch( UnsupportedEncodingException e ) |
| { |
| throw new InternalWikiException("Faulty encoding; should never happen", e); |
| } |
| } |
| |
| /** |
| * Finds a Wiki page from the page repository. |
| * |
| * @param page The name of the page. |
| * @return A File to the page. May be null. |
| */ |
| protected File findPage( String page ) |
| { |
| return new File( m_pageDirectory, mangleName(page)+FILE_EXT ); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public boolean pageExists( String page ) |
| { |
| File pagefile = findPage( page ); |
| |
| return pagefile.exists(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public boolean pageExists( String page, int version ) |
| { |
| return pageExists (page); |
| } |
| |
| /** |
| * This implementation just returns the current version, as filesystem |
| * does not provide versioning information for now. |
| * |
| * @param page {@inheritDoc} |
| * @param version {@inheritDoc} |
| * @throws {@inheritDoc} |
| */ |
| public String getPageText( String page, int version ) |
| throws ProviderException |
| { |
| return getPageText( page ); |
| } |
| |
| /** |
| * Read the text directly from the correct file. |
| */ |
| private String getPageText( String page ) |
| { |
| String result = null; |
| InputStream in = null; |
| |
| File pagedata = findPage( page ); |
| |
| 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); |
| } |
| finally |
| { |
| IOUtils.closeQuietly( in ); |
| } |
| } |
| else |
| { |
| log.warn("Failed to read page '"+page+"' from '"+pagedata.getAbsolutePath()+"', possibly a permissions problem"); |
| } |
| } |
| else |
| { |
| // This is okay. |
| log.info("New page '"+page+"'"); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void putPageText( WikiPage page, String text ) |
| throws ProviderException |
| { |
| File file = findPage( page.getName() ); |
| PrintWriter out = null; |
| |
| try |
| { |
| out = new PrintWriter(new OutputStreamWriter( new FileOutputStream( file ), |
| m_encoding )); |
| |
| out.print( text ); |
| } |
| catch( IOException e ) |
| { |
| log.error( "Saving failed", e ); |
| } |
| finally |
| { |
| IOUtils.closeQuietly( out ); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public Collection getAllPages() |
| throws ProviderException |
| { |
| log.debug("Getting all pages..."); |
| |
| ArrayList<WikiPage> set = new ArrayList<WikiPage>(); |
| |
| File wikipagedir = new File( m_pageDirectory ); |
| |
| File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() ); |
| |
| if( wikipages == null ) |
| { |
| log.error("Wikipages directory '" + m_pageDirectory + "' does not exist! Please check " + PROP_PAGEDIR + " in jspwiki.properties."); |
| throw new InternalWikiException("Page directory does not exist"); |
| } |
| |
| for( int i = 0; i < wikipages.length; i++ ) |
| { |
| String wikiname = wikipages[i].getName(); |
| int cutpoint = wikiname.lastIndexOf( FILE_EXT ); |
| |
| WikiPage page = getPageInfo( unmangleName(wikiname.substring(0,cutpoint)), |
| WikiPageProvider.LATEST_VERSION ); |
| if( page == null ) |
| { |
| // This should not really happen. |
| // FIXME: Should we throw an exception here? |
| log.error("Page "+wikiname+" was found in directory listing, but could not be located individually."); |
| continue; |
| } |
| |
| set.add( page ); |
| } |
| |
| return set; |
| } |
| |
| /** |
| * Does not work. |
| * |
| * @param date {@inheritDoc} |
| * @return {@inheritDoc} |
| */ |
| public Collection getAllChangedSince( Date date ) |
| { |
| return new ArrayList(); // FIXME |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public int getPageCount() |
| { |
| File wikipagedir = new File( m_pageDirectory ); |
| |
| File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() ); |
| |
| return wikipages.length; |
| } |
| |
| /** |
| * Iterates through all WikiPages, matches them against the given query, |
| * and returns a Collection of SearchResult objects. |
| * |
| * @param query {@inheritDoc} |
| * @return {@inheritDoc} |
| */ |
| public Collection findPages( QueryItem[] query ) |
| { |
| File wikipagedir = new File( m_pageDirectory ); |
| TreeSet<SearchResult> res = new TreeSet<SearchResult>( new SearchResultComparator() ); |
| SearchMatcher matcher = new SearchMatcher( m_engine, query ); |
| |
| File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() ); |
| |
| for( int i = 0; i < wikipages.length; i++ ) |
| { |
| FileInputStream input = null; |
| |
| // log.debug("Searching page "+wikipages[i].getPath() ); |
| |
| String filename = wikipages[i].getName(); |
| int cutpoint = filename.lastIndexOf( FILE_EXT ); |
| String wikiname = filename.substring( 0, cutpoint ); |
| |
| wikiname = unmangleName( wikiname ); |
| |
| try |
| { |
| input = new FileInputStream( wikipages[i] ); |
| String pagetext = FileUtil.readContents( input, m_encoding ); |
| SearchResult comparison = matcher.matchPageContent( wikiname, pagetext ); |
| if( comparison != null ) |
| { |
| res.add( comparison ); |
| } |
| } |
| catch( IOException e ) |
| { |
| log.error( "Failed to read " + filename, e ); |
| } |
| finally |
| { |
| IOUtils.closeQuietly( input ); |
| } |
| } |
| |
| return res; |
| } |
| |
| /** |
| * Always returns the latest version, since FileSystemProvider |
| * does not support versioning. |
| * |
| * @param page {@inheritDoc} |
| * @param version {@inheritDoc} |
| * @return {@inheritDoc} |
| * @throws {@inheritDoc} |
| */ |
| public WikiPage getPageInfo( String page, int version ) |
| throws ProviderException |
| { |
| File file = findPage( page ); |
| |
| if( !file.exists() ) |
| { |
| return null; |
| } |
| |
| WikiPage p = new WikiPage( m_engine, page ); |
| p.setLastModified( new Date(file.lastModified()) ); |
| |
| return p; |
| } |
| |
| /** |
| * The FileSystemProvider provides only one version. |
| * |
| * @param page {@inheritDoc} |
| * @throws {@inheritDoc} |
| * @return {@inheritDoc} |
| */ |
| public List getVersionHistory( String page ) |
| throws ProviderException |
| { |
| ArrayList<WikiPage> list = new ArrayList<WikiPage>(); |
| |
| list.add( getPageInfo( page, WikiPageProvider.LATEST_VERSION ) ); |
| |
| return list; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public String getProviderInfo() |
| { |
| return ""; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void deleteVersion( String pageName, int version ) |
| throws ProviderException |
| { |
| if( version == WikiProvider.LATEST_VERSION ) |
| { |
| File f = findPage( pageName ); |
| |
| f.delete(); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void deletePage( String pageName ) |
| throws ProviderException |
| { |
| File f = findPage( pageName ); |
| |
| f.delete(); |
| } |
| |
| /** |
| * Set the custom properties provided into the given page. |
| * |
| * @since 2.10.2 |
| */ |
| protected void setCustomProperties(WikiPage page, Properties properties) { |
| Enumeration propertyNames = properties.propertyNames(); |
| while (propertyNames.hasMoreElements()) { |
| String key = (String) propertyNames.nextElement(); |
| if (!key.equals(WikiPage.AUTHOR) && !key.equals(WikiPage.CHANGENOTE) && !key.equals(WikiPage.VIEWCOUNT)) { |
| page.setAttribute(key, properties.get(key)); |
| } |
| } |
| } |
| |
| /** |
| * Get custom properties using {@link this.addCustomPageProperties}, validate them using {@link this.validateCustomPageProperties} |
| * and add them to default properties provided |
| * |
| * @since 2.10.2 |
| */ |
| protected void getCustomProperties(WikiPage page, Properties defaultProperties) throws IOException { |
| Properties customPageProperties = addCustomProperties(page,defaultProperties); |
| validateCustomPageProperties(customPageProperties); |
| defaultProperties.putAll(customPageProperties); |
| } |
| |
| /** |
| * By default all page attributes that start with "@" are returned as custom properties. |
| * This can be overwritten by custom FileSystemProviders to save additional properties. |
| * CustomPageProperties are validated by {@link this.validateCustomPageProperties} |
| * |
| * @since 2.10.2 |
| * @param page the current page |
| * @param props the default properties of this page |
| * @return default implementation returns empty Properties. |
| */ |
| protected Properties addCustomProperties(WikiPage page, Properties props) { |
| Properties customProperties = new Properties(); |
| if (page != null) { |
| Map<String,Object> atts = page.getAttributes(); |
| for (String key : atts.keySet()) { |
| Object value = atts.get(key); |
| if (key.startsWith("@") && value != null) { |
| customProperties.put(key,value.toString()); |
| } |
| } |
| |
| } |
| return customProperties; |
| } |
| |
| /** |
| * Default validation, validates that key and value is ASCII <code>StringUtils.isAsciiPrintable()</code> and within lengths set up in jspwiki-custom.properties. |
| * This can be overwritten by custom FileSystemProviders to validate additional properties |
| * See https://issues.apache.org/jira/browse/JSPWIKI-856 |
| * @since 2.10.2 |
| * @param customProperties the custom page properties being added |
| */ |
| protected void validateCustomPageProperties(Properties customProperties) throws IOException { |
| // Default validation rules |
| if (customProperties != null && !customProperties.isEmpty()) { |
| if (customProperties.size()>MAX_PROPLIMIT) { |
| throw new IOException("Too many custom properties. You are adding "+customProperties.size()+", but max limit is "+MAX_PROPLIMIT); |
| } |
| Enumeration propertyNames = customProperties.propertyNames(); |
| while (propertyNames.hasMoreElements()) { |
| String key = (String) propertyNames.nextElement(); |
| String value = (String)customProperties.get(key); |
| if (key != null) { |
| if (key.length()>MAX_PROPKEYLENGTH) { |
| throw new IOException("Custom property key "+key+" is too long. Max allowed length is "+MAX_PROPKEYLENGTH); |
| } |
| if (!StringUtils.isAsciiPrintable(key)) { |
| throw new IOException("Custom property key "+key+" is not simple ASCII!"); |
| } |
| } |
| if (value != null) { |
| if (value.length()>MAX_PROPVALUELENGTH) { |
| throw new IOException("Custom property key "+key+" has value that is too long. Value="+value+". Max allowed length is "+MAX_PROPVALUELENGTH); |
| } |
| if (!StringUtils.isAsciiPrintable(value)) { |
| throw new IOException("Custom property key "+key+" has value that is not simple ASCII! Value="+value); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * A simple filter which filters only those filenames which correspond to the |
| * file extension used. |
| */ |
| public static class WikiFileFilter |
| implements FilenameFilter |
| { |
| /** |
| * {@inheritDoc} |
| */ |
| public boolean accept( File dir, String name ) |
| { |
| return name.endsWith( FILE_EXT ); |
| } |
| } |
| } |