001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2005 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.File;
023: import java.io.BufferedReader;
024: import java.io.InputStreamReader;
025: import java.io.InputStream;
026: import java.io.IOException;
027: import java.util.Properties;
028: import java.util.ArrayList;
029: import java.util.List;
030: import java.util.Date;
031: import java.util.Iterator;
032: import java.text.SimpleDateFormat;
033: import java.text.ParseException;
034: import org.apache.log4j.Logger;
035: import org.apache.oro.text.regex.*;
036:
037: import com.ecyrd.jspwiki.*;
038:
039: /**
040: * This class implements a simple RCS file provider. NOTE: You MUST
041: * have the RCS package installed for this to work. They must also
042: * be in your path...
043: *
044: * <P>
045: * The RCS file provider extends from the FileSystemProvider, which
046: * means that it provides the pages in the same way. The only difference
047: * is that it implements the version history commands, and also in each
048: * checkin it writes the page to the RCS repository as well.
049: * <p>
050: * If you decide to dabble with the default commands, please make sure
051: * that you do not check the default archive suffix ",v". File deletion
052: * depends on it.
053: *
054: * @author Janne Jalkanen
055: */
056: // FIXME: Not all commands read their format from the property file yet.
057: public class RCSFileProvider extends AbstractFileProvider {
058: private String m_checkinCommand = "ci -m\"author=%u;changenote=%c\" -l -t-none %s";
059: private String m_checkoutCommand = "co -l %s";
060: private String m_logCommand = "rlog -zLT -r %s";
061: private String m_fullLogCommand = "rlog -zLT %s";
062: private String m_checkoutVersionCommand = "co -p -r1.%v %s";
063: private String m_deleteVersionCommand = "rcs -o1.%v %s";
064:
065: private static final Logger log = Logger
066: .getLogger(RCSFileProvider.class);
067:
068: public static final String PROP_CHECKIN = "jspwiki.rcsFileProvider.checkinCommand";
069: public static final String PROP_CHECKOUT = "jspwiki.rcsFileProvider.checkoutCommand";
070: public static final String PROP_LOG = "jspwiki.rcsFileProvider.logCommand";
071: public static final String PROP_FULLLOG = "jspwiki.rcsFileProvider.fullLogCommand";
072: public static final String PROP_CHECKOUTVERSION = "jspwiki.rcsFileProvider.checkoutVersionCommand";
073:
074: private static final String PATTERN_DATE = "^date:\\s*(.*\\d);";
075: private static final String PATTERN_AUTHOR = "^\"?author=([\\w\\.\\s\\+\\.\\%]*)\"?";
076: private static final String PATTERN_CHANGENOTE = ";changenote=([\\w\\.\\s\\+\\.\\%]*)\"?";
077: private static final String PATTERN_REVISION = "^revision \\d+\\.(\\d+)";
078:
079: private static final String RCSFMT_DATE = "yyyy-MM-dd HH:mm:ss";
080: private static final String RCSFMT_DATE_UTC = "yyyy/MM/dd HH:mm:ss";
081:
082: // Date format parsers, placed here to save on object creation
083: private SimpleDateFormat m_rcsdatefmt = new SimpleDateFormat(
084: RCSFMT_DATE);
085: private SimpleDateFormat m_rcsdatefmt_utc = new SimpleDateFormat(
086: RCSFMT_DATE_UTC);
087:
088: public void initialize(WikiEngine engine, Properties props)
089: throws NoRequiredPropertyException, IOException {
090: log.debug("Initing RCS");
091: super .initialize(engine, props);
092:
093: m_checkinCommand = props.getProperty(PROP_CHECKIN,
094: m_checkinCommand);
095: m_checkoutCommand = props.getProperty(PROP_CHECKOUT,
096: m_checkoutCommand);
097: m_logCommand = props.getProperty(PROP_LOG, m_logCommand);
098: m_fullLogCommand = props.getProperty(PROP_FULLLOG,
099: m_fullLogCommand);
100: m_checkoutVersionCommand = props.getProperty(
101: PROP_CHECKOUTVERSION, m_checkoutVersionCommand);
102:
103: File rcsdir = new File(getPageDirectory(), "RCS");
104:
105: if (!rcsdir.exists()) {
106: rcsdir.mkdirs();
107: }
108:
109: log.debug("checkin=" + m_checkinCommand);
110: log.debug("checkout=" + m_checkoutCommand);
111: log.debug("log=" + m_logCommand);
112: log.debug("fulllog=" + m_fullLogCommand);
113: log.debug("checkoutversion=" + m_checkoutVersionCommand);
114: }
115:
116: // NB: This is a very slow method.
117:
118: public WikiPage getPageInfo(String page, int version)
119: throws ProviderException {
120: PatternMatcher matcher = new Perl5Matcher();
121: PatternCompiler compiler = new Perl5Compiler();
122: BufferedReader stdout = null;
123:
124: WikiPage info = super .getPageInfo(page, version);
125:
126: if (info == null)
127: return null;
128:
129: try {
130: String cmd = m_fullLogCommand;
131:
132: cmd = TextUtil.replaceString(cmd, "%s", mangleName(page)
133: + FILE_EXT);
134:
135: Process process = Runtime.getRuntime().exec(cmd, null,
136: new File(getPageDirectory()));
137:
138: // FIXME: Should this use encoding as well?
139: stdout = new BufferedReader(new InputStreamReader(process
140: .getInputStream()));
141:
142: String line;
143: Pattern headpattern = compiler.compile(PATTERN_REVISION);
144: // This complicated pattern is required, since on Linux RCS adds
145: // quotation marks, but on Windows, it does not.
146: Pattern userpattern = compiler.compile(PATTERN_AUTHOR);
147: Pattern datepattern = compiler.compile(PATTERN_DATE);
148: Pattern notepattern = compiler.compile(PATTERN_CHANGENOTE);
149:
150: boolean found = false;
151:
152: while ((line = stdout.readLine()) != null) {
153: if (matcher.contains(line, headpattern)) {
154: MatchResult result = matcher.getMatch();
155:
156: try {
157: int vernum = Integer.parseInt(result.group(1));
158:
159: if (vernum == version
160: || version == WikiPageProvider.LATEST_VERSION) {
161: info.setVersion(vernum);
162: found = true;
163: }
164: } catch (NumberFormatException e) {
165: log
166: .info(
167: "Failed to parse version number from RCS log: ",
168: e);
169: // Just continue reading through
170: }
171: } else if (matcher.contains(line, datepattern) && found) {
172: MatchResult result = matcher.getMatch();
173: Date d = parseDate(result.group(1));
174:
175: if (d != null) {
176: info.setLastModified(d);
177: } else {
178: log
179: .info("WikiPage "
180: + info.getName()
181: + " has null modification date for version "
182: + version);
183: }
184: } else if (found && line.startsWith("----")) {
185: // End of line sign from RCS
186: break;
187: }
188:
189: if (found && matcher.contains(line, userpattern)) {
190: MatchResult result = matcher.getMatch();
191: info.setAuthor(TextUtil.urlDecodeUTF8(result
192: .group(1)));
193: }
194:
195: if (found && matcher.contains(line, notepattern)) {
196: MatchResult result = matcher.getMatch();
197:
198: info.setAttribute(WikiPage.CHANGENOTE, TextUtil
199: .urlDecodeUTF8(result.group(1)));
200: }
201: }
202:
203: //
204: // Especially with certain versions of RCS on Windows,
205: // process.waitFor() hangs unless you read all of the
206: // standard output. So we make sure it's all emptied.
207: //
208:
209: while ((line = stdout.readLine()) != null) {
210: }
211:
212: process.waitFor();
213:
214: // we must close all by exec(..) opened streams: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692
215: process.getInputStream().close();
216: process.getOutputStream().close();
217: process.getErrorStream().close();
218:
219: } catch (Exception e) {
220: // This also occurs when 'info' was null.
221: log.warn("Failed to read RCS info", e);
222: } finally {
223: try {
224: if (stdout != null)
225: stdout.close();
226: } catch (IOException e) {
227: }
228: }
229:
230: return info;
231: }
232:
233: public String getPageText(String page, int version)
234: throws ProviderException {
235: String result = null;
236: InputStream stdout = null;
237: BufferedReader stderr = null;
238: Process process = null;
239:
240: // Let parent handle latest fetches, since the FileSystemProvider
241: // can do the file reading just as well.
242:
243: if (version == WikiPageProvider.LATEST_VERSION)
244: return super .getPageText(page, version);
245:
246: log.debug("Fetching specific version " + version + " of page "
247: + page);
248:
249: try {
250: PatternMatcher matcher = new Perl5Matcher();
251: PatternCompiler compiler = new Perl5Compiler();
252: int checkedOutVersion = -1;
253: String line;
254: String cmd = m_checkoutVersionCommand;
255:
256: cmd = TextUtil.replaceString(cmd, "%s", mangleName(page)
257: + FILE_EXT);
258: cmd = TextUtil.replaceString(cmd, "%v", Integer
259: .toString(version));
260:
261: log.debug("Command = '" + cmd + "'");
262:
263: process = Runtime.getRuntime().exec(cmd, null,
264: new File(getPageDirectory()));
265: stdout = process.getInputStream();
266: result = FileUtil.readContents(stdout, m_encoding);
267:
268: stderr = new BufferedReader(new InputStreamReader(process
269: .getErrorStream()));
270:
271: Pattern headpattern = compiler.compile(PATTERN_REVISION);
272:
273: while ((line = stderr.readLine()) != null) {
274: if (matcher.contains(line, headpattern)) {
275: MatchResult mr = matcher.getMatch();
276: checkedOutVersion = Integer.parseInt(mr.group(1));
277: }
278: }
279:
280: process.waitFor();
281:
282: int exitVal = process.exitValue();
283:
284: log.debug("Done, returned = " + exitVal);
285:
286: //
287: // If fetching failed, assume that this is because of the user
288: // has just migrated from FileSystemProvider, and check
289: // if he's getting version 1. Else he might be trying to find
290: // a version that has been deleted.
291: //
292: if (exitVal != 0 || checkedOutVersion == -1) {
293: if (version == 1) {
294: result = super .getPageText(page,
295: WikiProvider.LATEST_VERSION);
296: } else {
297: throw new NoSuchVersionException("Page: " + page
298: + ", version=" + version);
299: }
300: } else {
301: //
302: // Check which version we actually got out!
303: //
304:
305: if (checkedOutVersion != version) {
306: throw new NoSuchVersionException("Page: " + page
307: + ", version=" + version);
308: }
309: }
310:
311: } catch (MalformedPatternException e) {
312: throw new InternalWikiException(
313: "Malformed pattern in RCSFileProvider!");
314: } catch (InterruptedException e) {
315: // This is fine, we'll just log it.
316: log
317: .info("RCS process was interrupted, we'll just return whatever we found.");
318: } catch (IOException e) {
319: log.error("RCS checkout failed", e);
320: } finally {
321: try {
322: // we must close all by exec(..) opened streams: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692
323: if (stdout != null)
324: stdout.close();
325: if (stderr != null)
326: stderr.close();
327: if (process != null)
328: process.getInputStream().close();
329: } catch (Exception e) {
330: log.error("Unable to close streams!");
331: }
332: }
333:
334: return result;
335: }
336:
337: /**
338: * Puts the page into RCS and makes sure there is a fresh copy in
339: * the directory as well.
340: */
341: public void putPageText(WikiPage page, String text)
342: throws ProviderException {
343: Process process = null;
344: String pagename = page.getName();
345: // Writes it in the dir.
346: super .putPageText(page, text);
347:
348: log.debug("Checking in text...");
349:
350: try {
351: String cmd = m_checkinCommand;
352:
353: String author = page.getAuthor();
354: if (author == null)
355: author = "unknown";
356:
357: String changenote = (String) page
358: .getAttribute(WikiPage.CHANGENOTE);
359: if (changenote == null)
360: changenote = "";
361:
362: cmd = TextUtil.replaceString(cmd, "%s",
363: mangleName(pagename) + FILE_EXT);
364: cmd = TextUtil.replaceString(cmd, "%u", TextUtil
365: .urlEncodeUTF8(author));
366: cmd = TextUtil.replaceString(cmd, "%c", TextUtil
367: .urlEncodeUTF8(changenote));
368: log.debug("Command = '" + cmd + "'");
369:
370: process = Runtime.getRuntime().exec(cmd, null,
371: new File(getPageDirectory()));
372:
373: process.waitFor();
374:
375: //
376: // Collect possible error output
377: //
378: BufferedReader error = null;
379: String elines = "";
380: error = new BufferedReader(new InputStreamReader(process
381: .getErrorStream()));
382: String line = null;
383: while ((line = error.readLine()) != null) {
384: elines = elines + line + "\n";
385: }
386:
387: log.debug("Done, returned = " + process.exitValue());
388: log.debug(elines);
389: if (process.exitValue() != 0) {
390: throw new ProviderException(cmd + "\n"
391: + "Done, returned = " + process.exitValue()
392: + "\n" + elines);
393: }
394: } catch (Exception e) {
395: log.error("RCS checkin failed", e);
396: ProviderException pe = new ProviderException(
397: "RCS checkin failed");
398: pe.initCause(e);
399: throw pe;
400: } finally {
401: // we must close all by exec(..) opened streams: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692
402: if (process != null) {
403: try {
404: if (process.getOutputStream() != null)
405: process.getOutputStream().close();
406: } catch (Exception e) {
407: }
408: try {
409: if (process.getInputStream() != null)
410: process.getInputStream().close();
411: } catch (Exception e) {
412: }
413: try {
414: if (process.getErrorStream() != null)
415: process.getErrorStream().close();
416: } catch (Exception e) {
417: }
418: }
419: }
420: }
421:
422: // FIXME: Put the rcs date formats into properties as well.
423: public List getVersionHistory(String page) {
424: PatternMatcher matcher = new Perl5Matcher();
425: PatternCompiler compiler = new Perl5Compiler();
426: BufferedReader stdout = null;
427:
428: log.debug("Getting RCS version history");
429:
430: ArrayList list = new ArrayList();
431:
432: try {
433: Pattern revpattern = compiler.compile(PATTERN_REVISION);
434: Pattern datepattern = compiler.compile(PATTERN_DATE);
435: // This complicated pattern is required, since on Linux RCS adds
436: // quotation marks, but on Windows, it does not.
437: Pattern userpattern = compiler.compile(PATTERN_AUTHOR);
438:
439: Pattern notepattern = compiler.compile(PATTERN_CHANGENOTE);
440:
441: String cmd = TextUtil.replaceString(m_fullLogCommand, "%s",
442: mangleName(page) + FILE_EXT);
443:
444: Process process = Runtime.getRuntime().exec(cmd, null,
445: new File(getPageDirectory()));
446:
447: // FIXME: Should this use encoding as well?
448: stdout = new BufferedReader(new InputStreamReader(process
449: .getInputStream()));
450:
451: String line;
452:
453: WikiPage info = null;
454:
455: while ((line = stdout.readLine()) != null) {
456: if (matcher.contains(line, revpattern)) {
457: info = new WikiPage(m_engine, page);
458:
459: MatchResult result = matcher.getMatch();
460:
461: int vernum = Integer.parseInt(result.group(1));
462: info.setVersion(vernum);
463:
464: list.add(info);
465: }
466:
467: if (matcher.contains(line, datepattern)) {
468: MatchResult result = matcher.getMatch();
469:
470: Date d = parseDate(result.group(1));
471:
472: info.setLastModified(d);
473: }
474:
475: if (matcher.contains(line, userpattern)) {
476: MatchResult result = matcher.getMatch();
477:
478: info.setAuthor(TextUtil.urlDecodeUTF8(result
479: .group(1)));
480: }
481:
482: if (matcher.contains(line, notepattern)) {
483: MatchResult result = matcher.getMatch();
484:
485: info.setAttribute(WikiPage.CHANGENOTE, TextUtil
486: .urlDecodeUTF8(result.group(1)));
487: }
488: }
489:
490: process.waitFor();
491:
492: // we must close all by exec(..) opened streams: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692
493: process.getInputStream().close();
494: process.getOutputStream().close();
495: process.getErrorStream().close();
496:
497: //
498: // FIXME: This is very slow
499: //
500: for (Iterator i = list.iterator(); i.hasNext();) {
501: WikiPage p = (WikiPage) i.next();
502:
503: String content = getPageText(p.getName(), p
504: .getVersion());
505:
506: p.setSize(content.length());
507: }
508: } catch (Exception e) {
509: log.error("RCS log failed", e);
510: } finally {
511: try {
512: if (stdout != null)
513: stdout.close();
514: } catch (IOException e) {
515: }
516: }
517:
518: return list;
519: }
520:
521: /**
522: * Removes the page file and the RCS archive from the repository.
523: * This method assumes that the page archive ends with ",v".
524: */
525: public void deletePage(String page) throws ProviderException {
526: log.debug("Deleting page " + page);
527: super .deletePage(page);
528:
529: File rcsdir = new File(getPageDirectory(), "RCS");
530:
531: if (rcsdir.exists() && rcsdir.isDirectory()) {
532: File rcsfile = new File(rcsdir, mangleName(page) + FILE_EXT
533: + ",v");
534:
535: if (rcsfile.exists()) {
536: if (rcsfile.delete() == false) {
537: log.warn("Deletion of RCS file "
538: + rcsfile.getAbsolutePath() + " failed!");
539: }
540: } else {
541: log.info("RCS file does not exist for page: " + page);
542: }
543: } else {
544: log.info("No RCS directory at " + rcsdir.getAbsolutePath());
545: }
546: }
547:
548: public void deleteVersion(String page, int version) {
549: String line = "<rcs not run>";
550: BufferedReader stderr = null;
551: boolean success = false;
552: String cmd = m_deleteVersionCommand;
553:
554: log.debug("Deleting version " + version + " of page " + page);
555:
556: cmd = TextUtil.replaceString(cmd, "%s", mangleName(page)
557: + FILE_EXT);
558: cmd = TextUtil.replaceString(cmd, "%v", Integer
559: .toString(version));
560:
561: log.debug("Running command " + cmd);
562: Process process = null;
563:
564: try {
565: process = Runtime.getRuntime().exec(cmd, null,
566: new File(getPageDirectory()));
567:
568: //
569: // 'rcs' command outputs to stderr methinks.
570: //
571:
572: // FIXME: Should this use encoding as well?
573:
574: stderr = new BufferedReader(new InputStreamReader(process
575: .getErrorStream()));
576:
577: while ((line = stderr.readLine()) != null) {
578: log.debug("LINE=" + line);
579: if (line.equals("done")) {
580: success = true;
581: }
582: }
583: } catch (IOException e) {
584: log.error("Page deletion failed: ", e);
585: } finally {
586: try {
587: if (stderr != null)
588: stderr.close();
589: if (process != null) {
590: process.getInputStream().close();
591: process.getOutputStream().close();
592: }
593: } catch (IOException e) {
594: log
595: .error("Cannot close streams for process while deleting page version.");
596: }
597: }
598:
599: if (!success) {
600: log
601: .error("Version deletion failed. Last info from RCS is: "
602: + line);
603: }
604: }
605:
606: /**
607: * util method to parse a date string in Local and UTC formats. This method is synchronized
608: * because SimpleDateFormat is not thread-safe.
609: *
610: * @see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4228335
611: */
612: private synchronized Date parseDate(String str) {
613: Date d = null;
614:
615: try {
616: d = m_rcsdatefmt.parse(str);
617: return d;
618: } catch (ParseException pe) {
619: }
620:
621: try {
622: d = m_rcsdatefmt_utc.parse(str);
623: return d;
624: } catch (ParseException pe) {
625: }
626:
627: return d;
628: }
629:
630: public void movePage(String from, String to)
631: throws ProviderException {
632: // XXX: Error checking could be better throughout this method.
633: File fromFile = findPage(from);
634: File toFile = findPage(to);
635:
636: fromFile.renameTo(toFile);
637:
638: String fromRCSName = "RCS/" + mangleName(from) + FILE_EXT
639: + ",v";
640: String toRCSName = "RCS/" + mangleName(to) + FILE_EXT + ",v";
641:
642: File fromRCSFile = new File(getPageDirectory(), fromRCSName);
643: File toRCSFile = new File(getPageDirectory(), toRCSName);
644:
645: fromRCSFile.renameTo(toRCSFile);
646: }
647: }
|