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.File; 022 import java.io.FileInputStream; 023 import java.io.FileNotFoundException; 024 import java.io.FileOutputStream; 025 import java.io.FilenameFilter; 026 import java.io.IOException; 027 import java.io.InputStream; 028 import java.io.OutputStreamWriter; 029 import java.io.PrintWriter; 030 import java.io.UnsupportedEncodingException; 031 import java.util.ArrayList; 032 import java.util.Collection; 033 import java.util.Date; 034 import java.util.List; 035 import java.util.Properties; 036 import java.util.TreeSet; 037 038 import org.apache.log4j.Logger; 039 import org.apache.wiki.InternalWikiException; 040 import org.apache.wiki.WikiEngine; 041 import org.apache.wiki.WikiPage; 042 import org.apache.wiki.WikiProvider; 043 import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 044 import org.apache.wiki.api.exceptions.ProviderException; 045 import org.apache.wiki.search.QueryItem; 046 import org.apache.wiki.search.SearchMatcher; 047 import org.apache.wiki.search.SearchResult; 048 import org.apache.wiki.search.SearchResultComparator; 049 import org.apache.wiki.util.FileUtil; 050 import org.apache.wiki.util.TextUtil; 051 052 /** 053 * Provides a simple directory based repository for Wiki pages. 054 * <P> 055 * All files have ".txt" appended to make life easier for those 056 * who insist on using Windows or other software which makes assumptions 057 * on the files contents based on its name. 058 * <p> 059 * This class functions as a superclass to all file based providers. 060 * 061 * @since 2.1.21. 062 * 063 */ 064 public abstract class AbstractFileProvider 065 implements WikiPageProvider 066 { 067 private static final Logger log = Logger.getLogger(AbstractFileProvider.class); 068 private String m_pageDirectory = "/tmp/"; 069 070 protected String m_encoding; 071 072 protected WikiEngine m_engine; 073 074 /** 075 * Name of the property that defines where page directories are. 076 */ 077 public static final String PROP_PAGEDIR = "jspwiki.fileSystemProvider.pageDir"; 078 079 /** 080 * All files should have this extension to be recognized as JSPWiki files. 081 * We default to .txt, because that is probably easiest for Windows users, 082 * and guarantees correct handling. 083 */ 084 public static final String FILE_EXT = ".txt"; 085 086 /** The default encoding. */ 087 public static final String DEFAULT_ENCODING = "ISO-8859-1"; 088 089 private boolean m_windowsHackNeeded = false; 090 091 /** 092 * {@inheritDoc} 093 * @throws FileNotFoundException If the specified page directory does not exist. 094 * @throws IOException In case the specified page directory is a file, not a directory. 095 */ 096 public void initialize( WikiEngine engine, Properties properties ) 097 throws NoRequiredPropertyException, 098 IOException, FileNotFoundException 099 { 100 log.debug("Initing FileSystemProvider"); 101 m_pageDirectory = TextUtil.getCanonicalFilePathProperty(properties, PROP_PAGEDIR, 102 System.getProperty("user.home") + File.separator + "jspwiki-files"); 103 104 File f = new File(m_pageDirectory); 105 106 if( !f.exists() ) 107 { 108 if( !f.mkdirs() ) 109 { 110 throw new IOException( "Failed to create page directory " + f.getAbsolutePath() + " , please check property " 111 + PROP_PAGEDIR ); 112 } 113 } 114 else 115 { 116 if( !f.isDirectory() ) 117 { 118 throw new IOException( "Page directory is not a directory: " + f.getAbsolutePath() ); 119 } 120 if( !f.canWrite() ) 121 { 122 throw new IOException( "Page directory is not writable: " + f.getAbsolutePath() ); 123 } 124 } 125 126 m_engine = engine; 127 128 m_encoding = properties.getProperty( WikiEngine.PROP_ENCODING, 129 DEFAULT_ENCODING ); 130 131 String os = System.getProperty( "os.name" ).toLowerCase(); 132 133 if( os.startsWith("windows") || os.equals("nt") ) 134 { 135 m_windowsHackNeeded = true; 136 } 137 138 log.info( "Wikipages are read from '" + m_pageDirectory + "'" ); 139 } 140 141 142 String getPageDirectory() 143 { 144 return m_pageDirectory; 145 } 146 147 private static final String[] WINDOWS_DEVICE_NAMES = 148 { 149 "con", "prn", "nul", "aux", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", 150 "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9" 151 }; 152 153 /** 154 * This makes sure that the queried page name 155 * is still readable by the file system. For example, all XML entities 156 * and slashes are encoded with the percent notation. 157 * 158 * @param pagename The name to mangle 159 * @return The mangled name. 160 */ 161 protected String mangleName( String pagename ) 162 { 163 pagename = TextUtil.urlEncode( pagename, m_encoding ); 164 165 pagename = TextUtil.replaceString( pagename, "/", "%2F" ); 166 167 // 168 // Names which start with a dot must be escaped to prevent problems. 169 // Since we use URL encoding, this is invisible in our unescaping. 170 // 171 if( pagename.startsWith( "." ) ) 172 { 173 pagename = "%2E" + pagename.substring( 1 ); 174 } 175 176 if( m_windowsHackNeeded ) 177 { 178 String pn = pagename.toLowerCase(); 179 for( int i = 0; i < WINDOWS_DEVICE_NAMES.length; i++ ) 180 { 181 if( WINDOWS_DEVICE_NAMES[i].equals(pn) ) 182 { 183 pagename = "$$$" + pagename; 184 } 185 } 186 } 187 188 return pagename; 189 } 190 191 /** 192 * This makes the reverse of mangleName. 193 * 194 * @param filename The filename to unmangle 195 * @return The unmangled name. 196 */ 197 protected String unmangleName( String filename ) 198 { 199 // The exception should never happen. 200 try 201 { 202 if( m_windowsHackNeeded && filename.startsWith( "$$$") && filename.length() > 3 ) 203 { 204 filename = filename.substring(3); 205 } 206 207 return TextUtil.urlDecode( filename, m_encoding ); 208 } 209 catch( UnsupportedEncodingException e ) 210 { 211 throw new InternalWikiException("Faulty encoding; should never happen"); 212 } 213 } 214 215 /** 216 * Finds a Wiki page from the page repository. 217 * 218 * @param page The name of the page. 219 * @return A File to the page. May be null. 220 */ 221 protected File findPage( String page ) 222 { 223 return new File( m_pageDirectory, mangleName(page)+FILE_EXT ); 224 } 225 226 /** 227 * {@inheritDoc} 228 */ 229 public boolean pageExists( String page ) 230 { 231 File pagefile = findPage( page ); 232 233 return pagefile.exists(); 234 } 235 236 /** 237 * {@inheritDoc} 238 */ 239 public boolean pageExists( String page, int version ) 240 { 241 return pageExists (page); 242 } 243 244 /** 245 * This implementation just returns the current version, as filesystem 246 * does not provide versioning information for now. 247 * 248 * @param page {@inheritDoc} 249 * @param version {@inheritDoc} 250 * @throws {@inheritDoc} 251 */ 252 public String getPageText( String page, int version ) 253 throws ProviderException 254 { 255 return getPageText( page ); 256 } 257 258 /** 259 * Read the text directly from the correct file. 260 */ 261 private String getPageText( String page ) 262 { 263 String result = null; 264 InputStream in = null; 265 266 File pagedata = findPage( page ); 267 268 if( pagedata.exists() ) 269 { 270 if( pagedata.canRead() ) 271 { 272 try 273 { 274 in = new FileInputStream( pagedata ); 275 result = FileUtil.readContents( in, m_encoding ); 276 } 277 catch( IOException e ) 278 { 279 log.error("Failed to read", e); 280 } 281 finally 282 { 283 try 284 { 285 if( in != null ) in.close(); 286 } 287 catch( Exception e ) 288 { 289 log.fatal("Closing failed",e); 290 } 291 } 292 } 293 else 294 { 295 log.warn("Failed to read page '"+page+"' from '"+pagedata.getAbsolutePath()+"', possibly a permissions problem"); 296 } 297 } 298 else 299 { 300 // This is okay. 301 log.info("New page '"+page+"'"); 302 } 303 304 return result; 305 } 306 307 /** 308 * {@inheritDoc} 309 */ 310 public void putPageText( WikiPage page, String text ) 311 throws ProviderException 312 { 313 File file = findPage( page.getName() ); 314 PrintWriter out = null; 315 316 try 317 { 318 out = new PrintWriter(new OutputStreamWriter( new FileOutputStream( file ), 319 m_encoding )); 320 321 out.print( text ); 322 } 323 catch( IOException e ) 324 { 325 log.error( "Saving failed" ); 326 } 327 finally 328 { 329 if( out != null ) out.close(); 330 } 331 } 332 333 /** 334 * {@inheritDoc} 335 */ 336 public Collection getAllPages() 337 throws ProviderException 338 { 339 log.debug("Getting all pages..."); 340 341 ArrayList<WikiPage> set = new ArrayList<WikiPage>(); 342 343 File wikipagedir = new File( m_pageDirectory ); 344 345 File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() ); 346 347 if( wikipages == null ) 348 { 349 log.error("Wikipages directory '" + m_pageDirectory + "' does not exist! Please check " + PROP_PAGEDIR + " in jspwiki.properties."); 350 throw new InternalWikiException("Page directory does not exist"); 351 } 352 353 for( int i = 0; i < wikipages.length; i++ ) 354 { 355 String wikiname = wikipages[i].getName(); 356 int cutpoint = wikiname.lastIndexOf( FILE_EXT ); 357 358 WikiPage page = getPageInfo( unmangleName(wikiname.substring(0,cutpoint)), 359 WikiPageProvider.LATEST_VERSION ); 360 if( page == null ) 361 { 362 // This should not really happen. 363 // FIXME: Should we throw an exception here? 364 log.error("Page "+wikiname+" was found in directory listing, but could not be located individually."); 365 continue; 366 } 367 368 set.add( page ); 369 } 370 371 return set; 372 } 373 374 /** 375 * Does not work. 376 * 377 * @param date {@inheritDoc} 378 * @return {@inheritDoc} 379 */ 380 public Collection getAllChangedSince( Date date ) 381 { 382 return new ArrayList(); // FIXME 383 } 384 385 /** 386 * {@inheritDoc} 387 */ 388 public int getPageCount() 389 { 390 File wikipagedir = new File( m_pageDirectory ); 391 392 File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() ); 393 394 return wikipages.length; 395 } 396 397 /** 398 * Iterates through all WikiPages, matches them against the given query, 399 * and returns a Collection of SearchResult objects. 400 * 401 * @param query {@inheritDoc} 402 * @return {@inheritDoc} 403 */ 404 public Collection findPages( QueryItem[] query ) 405 { 406 File wikipagedir = new File( m_pageDirectory ); 407 TreeSet<SearchResult> res = new TreeSet<SearchResult>( new SearchResultComparator() ); 408 SearchMatcher matcher = new SearchMatcher( m_engine, query ); 409 410 File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() ); 411 412 for( int i = 0; i < wikipages.length; i++ ) 413 { 414 FileInputStream input = null; 415 416 // log.debug("Searching page "+wikipages[i].getPath() ); 417 418 String filename = wikipages[i].getName(); 419 int cutpoint = filename.lastIndexOf( FILE_EXT ); 420 String wikiname = filename.substring( 0, cutpoint ); 421 422 wikiname = unmangleName( wikiname ); 423 424 try 425 { 426 input = new FileInputStream( wikipages[i] ); 427 String pagetext = FileUtil.readContents( input, m_encoding ); 428 SearchResult comparison = matcher.matchPageContent( wikiname, pagetext ); 429 if( comparison != null ) 430 { 431 res.add( comparison ); 432 } 433 } 434 catch( IOException e ) 435 { 436 log.error( "Failed to read " + filename, e ); 437 } 438 finally 439 { 440 try 441 { 442 if( input != null ) input.close(); 443 } 444 catch( IOException e ) {} // It's fine to fail silently. 445 } 446 } 447 448 return res; 449 } 450 451 /** 452 * Always returns the latest version, since FileSystemProvider 453 * does not support versioning. 454 * 455 * @param page {@inheritDoc} 456 * @param version {@inheritDoc} 457 * @return {@inheritDoc} 458 * @throws {@inheritDoc} 459 */ 460 public WikiPage getPageInfo( String page, int version ) 461 throws ProviderException 462 { 463 File file = findPage( page ); 464 465 if( !file.exists() ) 466 { 467 return null; 468 } 469 470 WikiPage p = new WikiPage( m_engine, page ); 471 p.setLastModified( new Date(file.lastModified()) ); 472 473 return p; 474 } 475 476 /** 477 * The FileSystemProvider provides only one version. 478 * 479 * @param page {@inheritDoc} 480 * @throws {@inheritDoc} 481 * @return {@inheritDoc} 482 */ 483 public List getVersionHistory( String page ) 484 throws ProviderException 485 { 486 ArrayList<WikiPage> list = new ArrayList<WikiPage>(); 487 488 list.add( getPageInfo( page, WikiPageProvider.LATEST_VERSION ) ); 489 490 return list; 491 } 492 493 /** 494 * {@inheritDoc} 495 */ 496 public String getProviderInfo() 497 { 498 return ""; 499 } 500 501 /** 502 * {@inheritDoc} 503 */ 504 public void deleteVersion( String pageName, int version ) 505 throws ProviderException 506 { 507 if( version == WikiProvider.LATEST_VERSION ) 508 { 509 File f = findPage( pageName ); 510 511 f.delete(); 512 } 513 } 514 515 /** 516 * {@inheritDoc} 517 */ 518 public void deletePage( String pageName ) 519 throws ProviderException 520 { 521 File f = findPage( pageName ); 522 523 f.delete(); 524 } 525 526 /** 527 * A simple filter which filters only those filenames which correspond to the 528 * file extension used. 529 */ 530 public static class WikiFileFilter 531 implements FilenameFilter 532 { 533 /** 534 * {@inheritDoc} 535 */ 536 public boolean accept( File dir, String name ) 537 { 538 return name.endsWith( FILE_EXT ); 539 } 540 } 541 }