001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
005:
006: This program is free software; you can redistribute it and/or modify
007: it under the terms of the GNU Lesser General Public License as published by
008: the Free Software Foundation; either version 2.1 of the License, or
009: (at your option) any later version.
010:
011: This program is distributed in the hope that it will be useful,
012: but WITHOUT ANY WARRANTY; without even the implied warranty of
013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: GNU Lesser General Public License for more details.
015:
016: You should have received a copy of the GNU Lesser General Public License
017: along with this program; if not, write to the Free Software
018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: */
020: package com.ecyrd.jspwiki.providers;
021:
022: import java.io.*;
023: import java.util.Collection;
024: import java.util.Iterator;
025: import java.util.Properties;
026: import java.util.Date;
027: import java.util.ArrayList;
028: import java.util.List;
029: import org.apache.log4j.Logger;
030:
031: import com.ecyrd.jspwiki.*;
032:
033: /**
034: * Provides a simple directory based repository for Wiki pages.
035: * Pages are held in a directory structure:
036: * <PRE>
037: * Main.txt
038: * Foobar.txt
039: * OLD/
040: * Main/
041: * 1.txt
042: * 2.txt
043: * page.properties
044: * Foobar/
045: * page.properties
046: * </PRE>
047: *
048: * In this case, "Main" has three versions, and "Foobar" just one version.
049: * <P>
050: * The properties file contains the necessary metainformation (such as author)
051: * information of the page. DO NOT MESS WITH IT!
052: *
053: * <P>
054: * All files have ".txt" appended to make life easier for those
055: * who insist on using Windows or other software which makes assumptions
056: * on the files contents based on its name.
057: *
058: * @author Janne Jalkanen
059: */
060: public class VersioningFileProvider extends AbstractFileProvider
061: implements VersioningProvider {
062: private static final Logger log = Logger
063: .getLogger(VersioningFileProvider.class);
064:
065: public static final String PAGEDIR = "OLD";
066: public static final String PROPERTYFILE = "page.properties";
067:
068: private CachedProperties m_cachedProperties;
069:
070: public void initialize(WikiEngine engine, Properties properties)
071: throws NoRequiredPropertyException, IOException {
072: super .initialize(engine, properties);
073: }
074:
075: /**
076: * Returns the directory where the old versions of the pages
077: * are being kept.
078: */
079: private File findOldPageDir(String page) {
080: if (page == null) {
081: throw new InternalWikiException(
082: "Page may NOT be null in the provider!");
083: }
084:
085: File oldpages = new File(getPageDirectory(), PAGEDIR);
086:
087: return new File(oldpages, mangleName(page));
088: }
089:
090: /**
091: * Goes through the repository and decides which version is
092: * the newest one in that directory.
093: *
094: * @return Latest version number in the repository, or -1, if
095: * there is no page in the repository.
096: */
097:
098: // FIXME: This is relatively slow.
099: /*
100: private int findLatestVersion( String page )
101: {
102: File pageDir = findOldPageDir( page );
103:
104: String[] pages = pageDir.list( new WikiFileFilter() );
105:
106: if( pages == null )
107: {
108: return -1; // No such thing found.
109: }
110:
111: int version = -1;
112:
113: for( int i = 0; i < pages.length; i++ )
114: {
115: int cutpoint = pages[i].indexOf( '.' );
116: if( cutpoint > 0 )
117: {
118: String pageNum = pages[i].substring( 0, cutpoint );
119:
120: try
121: {
122: int res = Integer.parseInt( pageNum );
123:
124: if( res > version )
125: {
126: version = res;
127: }
128: }
129: catch( NumberFormatException e ) {} // It's okay to skip these.
130: }
131: }
132:
133: return version;
134: }
135: */
136: private int findLatestVersion(String page) throws ProviderException {
137: int version = -1;
138:
139: try {
140: Properties props = getPageProperties(page);
141:
142: for (Iterator i = props.keySet().iterator(); i.hasNext();) {
143: String key = (String) i.next();
144:
145: if (key.endsWith(".author")) {
146: int cutpoint = key.indexOf('.');
147: if (cutpoint > 0) {
148: String pageNum = key.substring(0, cutpoint);
149:
150: try {
151: int res = Integer.parseInt(pageNum);
152:
153: if (res > version) {
154: version = res;
155: }
156: } catch (NumberFormatException e) {
157: } // It's okay to skip these.
158: }
159: }
160: }
161: } catch (IOException e) {
162: log.error("Unable to figure out latest version - dying...",
163: e);
164: }
165:
166: return version;
167: }
168:
169: /**
170: * Reads page properties from the file system.
171: */
172: private Properties getPageProperties(String page)
173: throws IOException {
174: File propertyFile = new File(findOldPageDir(page), PROPERTYFILE);
175:
176: if (propertyFile.exists()) {
177: long lastModified = propertyFile.lastModified();
178:
179: //
180: // The profiler showed that when calling the history of a page the propertyfile
181: // was read just as much times as there were versions of that file. The loading
182: // of a propertyfile is a cpu-intensive jobs. So now hold on to the last propertyfile
183: // read because the next method will with a high probability ask for the same propertyfile.
184: // The time it took to show a historypage with 267 versions dropped with 300%.
185: //
186:
187: CachedProperties cp = m_cachedProperties;
188:
189: if (cp != null && cp.m_page.equals(page)
190: && cp.m_lastModified == lastModified) {
191: return cp.m_props;
192: }
193:
194: InputStream in = null;
195:
196: try {
197: in = new BufferedInputStream(new FileInputStream(
198: propertyFile));
199:
200: Properties props = new Properties();
201:
202: props.load(in);
203:
204: cp = new CachedProperties();
205: cp.m_page = page;
206: cp.m_lastModified = lastModified;
207: cp.m_props = props;
208:
209: m_cachedProperties = cp; // Atomic
210:
211: return props;
212: } finally {
213: if (in != null)
214: in.close();
215: }
216: }
217:
218: return new Properties(); // Returns an empty object
219: }
220:
221: /**
222: * Writes the page properties back to the file system.
223: * Note that it WILL overwrite any previous properties.
224: */
225: private void putPageProperties(String page, Properties properties)
226: throws IOException {
227: File propertyFile = new File(findOldPageDir(page), PROPERTYFILE);
228: OutputStream out = null;
229:
230: try {
231: out = new FileOutputStream(propertyFile);
232:
233: properties.store(out, " JSPWiki page properties for "
234: + page + ". DO NOT MODIFY!");
235: } finally {
236: if (out != null)
237: out.close();
238: }
239: }
240:
241: /**
242: * Figures out the real version number of the page and also checks
243: * for its existence.
244: *
245: * @throws NoSuchVersionException if there is no such version.
246: */
247: private int realVersion(String page, int requestedVersion)
248: throws NoSuchVersionException, ProviderException {
249: //
250: // Quickly check for the most common case.
251: //
252: if (requestedVersion == WikiProvider.LATEST_VERSION) {
253: return -1;
254: }
255:
256: int latest = findLatestVersion(page);
257:
258: if (requestedVersion == latest
259: || (requestedVersion == 1 && latest == -1)) {
260: return -1;
261: } else if (requestedVersion <= 0 || requestedVersion > latest) {
262: throw new NoSuchVersionException("Requested version "
263: + requestedVersion + ", but latest is " + latest);
264: }
265:
266: return requestedVersion;
267: }
268:
269: public synchronized String getPageText(String page, int version)
270: throws ProviderException {
271: File dir = findOldPageDir(page);
272:
273: version = realVersion(page, version);
274: if (version == -1) {
275: // We can let the FileSystemProvider take care
276: // of these requests.
277: return super .getPageText(page,
278: WikiPageProvider.LATEST_VERSION);
279: }
280:
281: File pageFile = new File(dir, "" + version + FILE_EXT);
282:
283: if (!pageFile.exists())
284: throw new NoSuchVersionException("Version " + version
285: + "does not exist.");
286:
287: return readFile(pageFile);
288: }
289:
290: // FIXME: Should this really be here?
291: private String readFile(File pagedata) throws ProviderException {
292: String result = null;
293: InputStream in = null;
294:
295: if (pagedata.exists()) {
296: if (pagedata.canRead()) {
297: try {
298: in = new FileInputStream(pagedata);
299: result = FileUtil.readContents(in, m_encoding);
300: } catch (IOException e) {
301: log.error("Failed to read", e);
302: throw new ProviderException("I/O error: "
303: + e.getMessage());
304: } finally {
305: try {
306: if (in != null)
307: in.close();
308: } catch (Exception e) {
309: log.fatal("Closing failed", e);
310: }
311: }
312: } else {
313: log.warn("Failed to read page from '"
314: + pagedata.getAbsolutePath()
315: + "', possibly a permissions problem");
316: throw new ProviderException(
317: "I cannot read the requested page.");
318: }
319: } else {
320: // This is okay.
321: // FIXME: is it?
322: log.info("New page");
323: }
324:
325: return result;
326: }
327:
328: // FIXME: This method has no rollback whatsoever.
329:
330: /*
331: This is how the page directory should look like:
332:
333: version pagedir olddir
334: none empty empty
335: 1 Main.txt (1) empty
336: 2 Main.txt (2) 1.txt
337: 3 Main.txt (3) 1.txt, 2.txt
338: */
339: public synchronized void putPageText(WikiPage page, String text)
340: throws ProviderException {
341: //
342: // This is a bit complicated. We'll first need to
343: // copy the old file to be the newest file.
344: //
345:
346: File pageDir = findOldPageDir(page.getName());
347:
348: if (!pageDir.exists()) {
349: pageDir.mkdirs();
350: }
351:
352: int latest = findLatestVersion(page.getName());
353:
354: try {
355: //
356: // Copy old data to safety, if one exists.
357: //
358:
359: File oldFile = findPage(page.getName());
360:
361: // Figure out which version should the old page be?
362: // Numbers should always start at 1.
363: // "most recent" = -1 ==> 1
364: // "first" = 1 ==> 2
365:
366: int versionNumber = (latest > 0) ? latest : 1;
367:
368: if (oldFile != null && oldFile.exists()) {
369: InputStream in = null;
370: OutputStream out = null;
371:
372: try {
373: in = new BufferedInputStream(new FileInputStream(
374: oldFile));
375: File pageFile = new File(pageDir, Integer
376: .toString(versionNumber)
377: + FILE_EXT);
378: out = new BufferedOutputStream(
379: new FileOutputStream(pageFile));
380:
381: FileUtil.copyContents(in, out);
382:
383: //
384: // We need also to set the date, since we rely on this.
385: //
386: pageFile.setLastModified(oldFile.lastModified());
387:
388: //
389: // Kludge to make the property code to work properly.
390: //
391: versionNumber++;
392: } finally {
393: if (out != null)
394: out.close();
395: if (in != null)
396: in.close();
397: }
398: }
399:
400: //
401: // Let superclass handler writing data to a new version.
402: //
403:
404: super .putPageText(page, text);
405:
406: //
407: // Finally, write page version data.
408: //
409:
410: // FIXME: No rollback available.
411: Properties props = getPageProperties(page.getName());
412:
413: props.setProperty(versionNumber + ".author", (page
414: .getAuthor() != null) ? page.getAuthor()
415: : "unknown");
416:
417: String changeNote = (String) page
418: .getAttribute(WikiPage.CHANGENOTE);
419: if (changeNote != null) {
420: props.setProperty(versionNumber + ".changenote",
421: changeNote);
422: }
423:
424: putPageProperties(page.getName(), props);
425: } catch (IOException e) {
426: log.error("Saving failed", e);
427: throw new ProviderException("Could not save page text: "
428: + e.getMessage());
429: }
430: }
431:
432: public WikiPage getPageInfo(String page, int version)
433: throws ProviderException {
434: int latest = findLatestVersion(page);
435: int realVersion;
436:
437: WikiPage p = null;
438:
439: if (version == WikiPageProvider.LATEST_VERSION
440: || version == latest || (version == 1 && latest == -1)) {
441: //
442: // Yes, we need to talk to the top level directory
443: // to get this version.
444: //
445: // I am listening to Press Play On Tape's guitar version of
446: // the good old C64 "Wizardry" -tune at this moment.
447: // Oh, the memories...
448: //
449: realVersion = (latest >= 0) ? latest : 1;
450:
451: p = super
452: .getPageInfo(page, WikiPageProvider.LATEST_VERSION);
453:
454: if (p != null) {
455: p.setVersion(realVersion);
456: }
457: } else {
458: //
459: // The file is not the most recent, so we'll need to
460: // find it from the deep trenches of the "OLD" directory
461: // structure.
462: //
463: realVersion = version;
464: File dir = findOldPageDir(page);
465:
466: if (!dir.exists() || !dir.isDirectory()) {
467: return null;
468: }
469:
470: File file = new File(dir, version + FILE_EXT);
471:
472: if (file.exists()) {
473: p = new WikiPage(m_engine, page);
474:
475: p.setLastModified(new Date(file.lastModified()));
476: p.setVersion(version);
477: }
478: }
479:
480: //
481: // Get author and other metadata information
482: // (Modification date has already been set.)
483: //
484: if (p != null) {
485: try {
486: Properties props = getPageProperties(page);
487: String author = props.getProperty(realVersion
488: + ".author");
489: if (author != null) {
490: p.setAuthor(author);
491: }
492:
493: String changenote = props.getProperty(realVersion
494: + ".changenote");
495: if (changenote != null)
496: p.setAttribute(WikiPage.CHANGENOTE, changenote);
497:
498: } catch (IOException e) {
499: log
500: .error("Cannot get author for page" + page
501: + ": ", e);
502: }
503: }
504:
505: return p;
506: }
507:
508: public boolean pageExists(String pageName, int version) {
509: File dir = findOldPageDir(pageName);
510:
511: if (!dir.exists() || !dir.isDirectory()) {
512: return false;
513: }
514:
515: File file = new File(dir, version + FILE_EXT);
516:
517: if (file.exists()) {
518: return true;
519: }
520:
521: return false;
522: }
523:
524: /**
525: * FIXME: Does not get user information.
526: */
527: public List getVersionHistory(String page) throws ProviderException {
528: ArrayList list = new ArrayList();
529:
530: int latest = findLatestVersion(page);
531:
532: // list.add( getPageInfo(page,WikiPageProvider.LATEST_VERSION) );
533:
534: for (int i = latest; i > 0; i--) {
535: WikiPage info = getPageInfo(page, i);
536:
537: if (info != null) {
538: list.add(info);
539: }
540: }
541:
542: return list;
543: }
544:
545: /**
546: * Removes the relevant page directory under "OLD" -directory as well,
547: * but does not remove any extra subdirectories from it. It will only
548: * touch those files that it thinks to be WikiPages.
549: */
550: // FIXME: Should log errors.
551: public void deletePage(String page) throws ProviderException {
552: super .deletePage(page);
553:
554: File dir = findOldPageDir(page);
555:
556: if (dir.exists() && dir.isDirectory()) {
557: File[] files = dir.listFiles(new WikiFileFilter());
558:
559: for (int i = 0; i < files.length; i++) {
560: files[i].delete();
561: }
562:
563: File propfile = new File(dir, PROPERTYFILE);
564:
565: if (propfile.exists()) {
566: propfile.delete();
567: }
568:
569: dir.delete();
570: }
571: }
572:
573: public void deleteVersion(String page, int version)
574: throws ProviderException {
575: File dir = findOldPageDir(page);
576:
577: int latest = findLatestVersion(page);
578:
579: if (version == WikiPageProvider.LATEST_VERSION
580: || version == latest || (version == 1 && latest == -1)) {
581: //
582: // Delete the properties
583: //
584: try {
585: Properties props = getPageProperties(page);
586: props.remove(((latest > 0) ? latest : 1) + ".author");
587: putPageProperties(page, props);
588: } catch (IOException e) {
589: log.error("Unable to modify page properties", e);
590: throw new ProviderException(
591: "Could not modify page properties");
592: }
593:
594: // We can let the FileSystemProvider take care
595: // of the actual deletion
596: super .deleteVersion(page, WikiPageProvider.LATEST_VERSION);
597:
598: //
599: // Copy the old file to the new location
600: //
601: latest = findLatestVersion(page);
602:
603: File pageDir = findOldPageDir(page);
604: File previousFile = new File(pageDir, Integer
605: .toString(latest)
606: + FILE_EXT);
607:
608: InputStream in = null;
609: OutputStream out = null;
610:
611: try {
612: if (previousFile.exists()) {
613: in = new BufferedInputStream(new FileInputStream(
614: previousFile));
615: File pageFile = findPage(page);
616: out = new BufferedOutputStream(
617: new FileOutputStream(pageFile));
618:
619: FileUtil.copyContents(in, out);
620:
621: //
622: // We need also to set the date, since we rely on this.
623: //
624: pageFile.setLastModified(previousFile
625: .lastModified());
626: }
627: } catch (IOException e) {
628: log
629: .fatal(
630: "Something wrong with the page directory - you may have just lost data!",
631: e);
632: } finally {
633: try {
634: if (in != null)
635: in.close();
636: if (out != null)
637: out.close();
638: } catch (IOException ex) {
639: log.error("Closing failed", ex);
640: }
641: }
642:
643: return;
644: }
645:
646: File pageFile = new File(dir, "" + version + FILE_EXT);
647:
648: if (pageFile.exists()) {
649: if (!pageFile.delete()) {
650: log.error("Unable to delete page.");
651: }
652: } else {
653: throw new NoSuchVersionException("Page " + page
654: + ", version=" + version);
655: }
656: }
657:
658: // FIXME: This is kinda slow, we should need to do this only once.
659: public Collection getAllPages() throws ProviderException {
660: Collection pages = super .getAllPages();
661: Collection returnedPages = new ArrayList();
662:
663: for (Iterator i = pages.iterator(); i.hasNext();) {
664: WikiPage page = (WikiPage) i.next();
665:
666: WikiPage info = getPageInfo(page.getName(),
667: WikiProvider.LATEST_VERSION);
668:
669: returnedPages.add(info);
670: }
671:
672: return returnedPages;
673: }
674:
675: public String getProviderInfo() {
676: return "";
677: }
678:
679: public void movePage(String from, String to)
680: throws ProviderException {
681: // Move the file itself
682: File fromFile = findPage(from);
683: File toFile = findPage(to);
684:
685: fromFile.renameTo(toFile);
686:
687: // Move any old versions
688: File fromOldDir = findOldPageDir(from);
689: File toOldDir = findOldPageDir(to);
690:
691: fromOldDir.renameTo(toOldDir);
692: }
693:
694: private static class CachedProperties {
695: String m_page;
696: Properties m_props;
697: long m_lastModified;
698: }
699: }
|