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.rss;
021:
022: import java.util.*;
023:
024: import org.apache.log4j.Logger;
025:
026: import com.ecyrd.jspwiki.*;
027: import com.ecyrd.jspwiki.attachment.Attachment;
028: import com.ecyrd.jspwiki.auth.permissions.PagePermission;
029: import com.ecyrd.jspwiki.providers.ProviderException;
030:
031: /**
032: * Generates an RSS feed from the recent changes.
033: * <P>
034: * We use the 1.0 spec, including the wiki-specific extensions. Wiki extensions
035: * have been defined in <A HREF="http://usemod.com/cgi-bin/mb.pl?ModWiki">UseMod:ModWiki</A>.
036: *
037: * @author Janne Jalkanen
038: * @since 1.7.5.
039: */
040: // FIXME: Limit diff and page content size.
041: public class RSSGenerator {
042: static Logger log = Logger.getLogger(RSSGenerator.class);
043: private WikiEngine m_engine;
044:
045: private String m_channelDescription = "";
046: private String m_channelLanguage = "en-us";
047: private boolean m_enabled = true;
048:
049: public static final String RSS10 = "rss10";
050: public static final String RSS20 = "rss20";
051: public static final String ATOM = "atom";
052:
053: public static final String MODE_BLOG = "blog";
054: public static final String MODE_WIKI = "wiki";
055: public static final String MODE_FULL = "full";
056:
057: /**
058: * Defines the property name for the RSS channel description. Default value for the
059: * channel description is an empty string.
060: * @since 1.7.6.
061: */
062: public static final String PROP_CHANNEL_DESCRIPTION = "jspwiki.rss.channelDescription";
063:
064: /**
065: * Defines the property name for the RSS channel language. Default value for the
066: * language is "en-us".
067: * @since 1.7.6.
068: */
069: public static final String PROP_CHANNEL_LANGUAGE = "jspwiki.rss.channelLanguage";
070:
071: public static final String PROP_CHANNEL_TITLE = "jspwiki.rss.channelTitle";
072:
073: /**
074: * Defines the property name for the RSS generator main switch.
075: * @since 1.7.6.
076: */
077: public static final String PROP_GENERATE_RSS = "jspwiki.rss.generate";
078:
079: /**
080: * Defines the property name for the RSS file that the wiki should generate.
081: * @since 1.7.6.
082: */
083: public static final String PROP_RSSFILE = "jspwiki.rss.fileName";
084:
085: public static final String PROP_RSSAUTHOR = "jspwiki.rss.author";
086: public static final String PROP_RSSAUTHOREMAIL = "jspwiki.rss.author.email";
087:
088: /**
089: * Defines the property name for the RSS generation interval in seconds.
090: * @since 1.7.6.
091: */
092: public static final String PROP_INTERVAL = "jspwiki.rss.interval";
093:
094: public static final String PROP_RSS_AUTHOR = "jspwiki.rss.author";
095: public static final String PROP_RSS_AUTHOREMAIL = "jspwiki.rss.author.email";
096: public static final String PROP_RSS_COPYRIGHT = "jspwiki.rss.copyright";
097:
098: private static final int MAX_CHARACTERS = Integer.MAX_VALUE - 1;
099:
100: /**
101: * Initialize the RSS generator.
102: */
103: public RSSGenerator(WikiEngine engine, Properties properties)
104: throws NoRequiredPropertyException {
105: m_engine = engine;
106:
107: // FIXME: This assumes a bit too much.
108: if (engine.getBaseURL() == null
109: || engine.getBaseURL().length() == 0) {
110: throw new NoRequiredPropertyException(
111: "RSS requires jspwiki.baseURL to be set!",
112: WikiEngine.PROP_BASEURL);
113: }
114:
115: m_channelDescription = properties.getProperty(
116: PROP_CHANNEL_DESCRIPTION, m_channelDescription);
117: m_channelLanguage = properties.getProperty(
118: PROP_CHANNEL_LANGUAGE, m_channelLanguage);
119: }
120:
121: /**
122: * Does the required formatting and entity replacement for XML.
123: */
124: public static String format(String s) {
125: s = TextUtil.replaceString(s, "&", "&");
126: s = TextUtil.replaceString(s, "<", "<");
127: s = TextUtil.replaceString(s, "]]>", "]]>");
128:
129: return s.trim();
130: }
131:
132: private String getAuthor(WikiPage page) {
133: String author = page.getAuthor();
134:
135: if (author == null)
136: author = "An unknown author";
137:
138: return author;
139: }
140:
141: private String getAttachmentDescription(Attachment att) {
142: String author = getAuthor(att);
143: StringBuffer sb = new StringBuffer();
144:
145: if (att.getVersion() != 1) {
146: sb.append(author
147: + " uploaded a new version of this attachment on "
148: + att.getLastModified());
149: } else {
150: sb.append(author + " created this attachment on "
151: + att.getLastModified());
152: }
153:
154: sb.append("<br /><hr /><br />");
155: sb.append("Parent page: <a href=\""
156: + m_engine.getURL(WikiContext.VIEW,
157: att.getParentName(), null, true) + "\">"
158: + att.getParentName() + "</a><br />");
159: sb.append("Info page: <a href=\""
160: + m_engine.getURL(WikiContext.INFO, att.getName(),
161: null, true) + "\">" + att.getName() + "</a>");
162:
163: return sb.toString();
164: }
165:
166: private String getPageDescription(WikiPage page) {
167: StringBuffer buf = new StringBuffer();
168: String author = getAuthor(page);
169:
170: WikiContext ctx = new WikiContext(m_engine, page);
171: if (page.getVersion() > 1) {
172: String diff = m_engine.getDiff(ctx, page.getVersion() - 1, // FIXME: Will fail when non-contiguous versions
173: page.getVersion());
174:
175: buf.append(author + " changed this page on "
176: + page.getLastModified() + ":<br /><hr /><br />");
177: buf.append(diff);
178: } else {
179: buf.append(author + " created this page on "
180: + page.getLastModified() + ":<br /><hr /><br />");
181: buf.append(m_engine.getHTML(page.getName()));
182: }
183:
184: return buf.toString();
185: }
186:
187: private String getEntryDescription(WikiPage page) {
188: String res;
189:
190: if (page instanceof Attachment) {
191: res = getAttachmentDescription((Attachment) page);
192: } else {
193: res = getPageDescription(page);
194: }
195:
196: return res;
197: }
198:
199: // FIXME: This should probably return something more intelligent
200: private String getEntryTitle(WikiPage page) {
201: return page.getName() + ", version " + page.getVersion();
202: }
203:
204: /**
205: * Generates the RSS resource. You probably want to output this
206: * result into a file or something, or serve as output from a servlet.
207: */
208: public String generate() {
209: WikiContext context = new WikiContext(m_engine, new WikiPage(
210: m_engine, "__DUMMY"));
211: context.setRequestContext(WikiContext.RSS);
212: Feed feed = new RSS10Feed(context);
213:
214: String result = generateFullWikiRSS(context, feed);
215:
216: result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
217: + result;
218:
219: return result;
220: }
221:
222: /**
223: * Returns the content type of this RSS feed.
224: * @since 2.3.15
225: * @param mode the RSS mode: {@link #RSS10}, {@link #RSS20} or {@link #ATOM}.
226: * @return the content type
227: */
228: public static String getContentType(String mode) {
229: if (mode.equals(RSS10) || mode.equals(RSS20)) {
230: return "application/rss+xml";
231: } else if (mode.equals(ATOM)) {
232: return "application/atom+xml";
233: }
234:
235: return "application/octet-stream"; // Unknown type
236: }
237:
238: /**
239: * Generates a feed based on a context and list of changes.
240: * @param wikiContext The WikiContext
241: * @param changed A list of Entry objects
242: * @param mode The mode (wiki/blog)
243: * @param type The type (RSS10, RSS20, ATOM). Default is RSS 1.0
244: * @return Fully formed XML.
245: *
246: * @throws ProviderException If the underlying provider failed.
247: * @throws IllegalArgumentException If an illegal mode is given.
248: */
249: public String generateFeed(WikiContext wikiContext, List changed,
250: String mode, String type) throws ProviderException {
251: Feed feed = null;
252: String res = null;
253:
254: if (ATOM.equals(type)) {
255: feed = new AtomFeed(wikiContext);
256: } else if (RSS20.equals(type)) {
257: feed = new RSS20Feed(wikiContext);
258: } else {
259: feed = new RSS10Feed(wikiContext);
260: }
261:
262: feed.setMode(mode);
263:
264: if (MODE_BLOG.equals(mode)) {
265: res = generateBlogRSS(wikiContext, changed, feed);
266: } else if (MODE_FULL.equals(mode)) {
267: res = generateFullWikiRSS(wikiContext, feed);
268: } else if (MODE_WIKI.equals(mode)) {
269: res = generateWikiPageRSS(wikiContext, changed, feed);
270: } else {
271: throw new IllegalArgumentException(
272: "Invalid value for feed mode: " + mode);
273: }
274:
275: return res;
276: }
277:
278: /**
279: * Returns <code>true</code> if RSS generation is enabled.
280: * @return whether RSS generation is currently enabled
281: */
282: public boolean isEnabled() {
283: return m_enabled;
284: }
285:
286: /**
287: * Turns RSS generation on or off. This setting is used to set
288: * the "enabled" flag only for use by callers, and does not
289: * actually affect whether the {@link #generate()} or
290: * {@link #generateFeed(WikiContext, List, String, String)}
291: * methods output anything.
292: * @param enabled whether RSS generation is considered enabled.
293: */
294: public synchronized void setEnabled(boolean enabled) {
295: m_enabled = enabled;
296: }
297:
298: /**
299: * Generates an RSS feed for the entire wiki. Each item should be an instance of the RSSItem class.
300: */
301: protected String generateFullWikiRSS(WikiContext wikiContext,
302: Feed feed) {
303: feed.setChannelTitle(m_engine.getApplicationName());
304: feed.setFeedURL(m_engine.getBaseURL());
305: feed.setChannelLanguage(m_channelLanguage);
306: feed.setChannelDescription(m_channelDescription);
307:
308: Collection changed = m_engine.getRecentChanges();
309:
310: WikiSession session = WikiSession.guestSession(m_engine);
311: int items = 0;
312: for (Iterator i = changed.iterator(); i.hasNext() && items < 15; items++) {
313: WikiPage page = (WikiPage) i.next();
314:
315: //
316: // Check if the anonymous user has view access to this page.
317: //
318:
319: if (!m_engine.getAuthorizationManager()
320: .checkPermission(
321: session,
322: new PagePermission(page,
323: PagePermission.VIEW_ACTION))) {
324: // No permission, skip to the next one.
325: continue;
326: }
327:
328: Entry e = new Entry();
329:
330: e.setPage(page);
331:
332: String url;
333:
334: if (page instanceof Attachment) {
335: url = m_engine.getURL(WikiContext.ATTACH, page
336: .getName(), null, true);
337: } else {
338: url = m_engine.getURL(WikiContext.VIEW, page.getName(),
339: null, true);
340: }
341:
342: e.setURL(url);
343: e.setTitle(page.getName());
344: e.setContent(getEntryDescription(page));
345: e.setAuthor(getAuthor(page));
346:
347: feed.addEntry(e);
348: }
349:
350: return feed.getString();
351: }
352:
353: /**
354: * Create RSS/Atom as if this page was a wikipage (in contrast to Blog mode).
355: *
356: * @param wikiContext
357: * @param changed
358: * @param feed
359: * @return
360: */
361: protected String generateWikiPageRSS(WikiContext wikiContext,
362: List changed, Feed feed) {
363: feed.setChannelTitle(m_engine.getApplicationName() + ": "
364: + wikiContext.getPage().getName());
365: feed.setFeedURL(wikiContext.getViewURL(wikiContext.getPage()
366: .getName()));
367: String language = m_engine.getVariable(wikiContext,
368: PROP_CHANNEL_LANGUAGE);
369:
370: if (language != null)
371: feed.setChannelLanguage(language);
372: else
373: feed.setChannelLanguage(m_channelLanguage);
374:
375: String channelDescription = m_engine.getVariable(wikiContext,
376: PROP_CHANNEL_DESCRIPTION);
377:
378: if (channelDescription != null) {
379: feed.setChannelDescription(channelDescription);
380: }
381:
382: Collections.sort(changed, new PageTimeComparator());
383:
384: int items = 0;
385: for (Iterator i = changed.iterator(); i.hasNext() && items < 15; items++) {
386: WikiPage page = (WikiPage) i.next();
387:
388: Entry e = new Entry();
389:
390: e.setPage(page);
391:
392: String url;
393:
394: if (page instanceof Attachment) {
395: url = m_engine.getURL(WikiContext.ATTACH, page
396: .getName(), "version=" + page.getVersion(),
397: true);
398: } else {
399: url = m_engine.getURL(WikiContext.VIEW, page.getName(),
400: "version=" + page.getVersion(), true);
401: }
402:
403: // Unfortunately, this is needed because the code will again go through
404: // replacement conversion
405:
406: url = TextUtil.replaceString(url, "&", "&");
407:
408: e.setURL(url);
409: e.setTitle(getEntryTitle(page));
410: e.setContent(getEntryDescription(page));
411: e.setAuthor(getAuthor(page));
412:
413: feed.addEntry(e);
414: }
415:
416: return feed.getString();
417: }
418:
419: /**
420: * Creates RSS from modifications as if this page was a blog (using the WeblogPlugin).
421: *
422: * @param wikiContext The WikiContext, as usual.
423: * @param changed A list of the changed pages.
424: * @param feed A valid Feed object. The feed will be used to create the RSS/Atom, depending
425: * on which kind of an object you want to put in it.
426: * @return A String of valid RSS or Atom.
427: * @throws ProviderException If reading of pages was not possible.
428: */
429: protected String generateBlogRSS(WikiContext wikiContext,
430: List changed, Feed feed) throws ProviderException {
431: if (log.isDebugEnabled())
432: log
433: .debug("Generating RSS for blog, size="
434: + changed.size());
435:
436: String ctitle = m_engine.getVariable(wikiContext,
437: PROP_CHANNEL_TITLE);
438:
439: if (ctitle != null)
440: feed.setChannelTitle(ctitle);
441: else
442: feed.setChannelTitle(m_engine.getApplicationName() + ":"
443: + wikiContext.getPage().getName());
444:
445: feed.setFeedURL(wikiContext.getViewURL(wikiContext.getPage()
446: .getName()));
447:
448: String language = m_engine.getVariable(wikiContext,
449: PROP_CHANNEL_LANGUAGE);
450:
451: if (language != null)
452: feed.setChannelLanguage(language);
453: else
454: feed.setChannelLanguage(m_channelLanguage);
455:
456: String channelDescription = m_engine.getVariable(wikiContext,
457: PROP_CHANNEL_DESCRIPTION);
458:
459: if (channelDescription != null) {
460: feed.setChannelDescription(channelDescription);
461: }
462:
463: Collections.sort(changed, new PageTimeComparator());
464:
465: int items = 0;
466: for (Iterator i = changed.iterator(); i.hasNext() && items < 15; items++) {
467: WikiPage page = (WikiPage) i.next();
468:
469: Entry e = new Entry();
470:
471: e.setPage(page);
472:
473: String url;
474:
475: if (page instanceof Attachment) {
476: url = m_engine.getURL(WikiContext.ATTACH, page
477: .getName(), null, true);
478: } else {
479: url = m_engine.getURL(WikiContext.VIEW, page.getName(),
480: null, true);
481: }
482:
483: e.setURL(url);
484:
485: //
486: // Title
487: //
488:
489: String pageText = m_engine.getPureText(page.getName(),
490: WikiProvider.LATEST_VERSION);
491:
492: String title = "";
493: int firstLine = pageText.indexOf('\n');
494:
495: if (firstLine > 0) {
496: title = pageText.substring(0, firstLine).trim();
497: }
498:
499: if (title.length() == 0)
500: title = page.getName();
501:
502: // Remove wiki formatting
503: while (title.startsWith("!"))
504: title = title.substring(1);
505:
506: e.setTitle(title);
507:
508: //
509: // Description
510: //
511:
512: if (firstLine > 0) {
513: int maxlen = pageText.length();
514: if (maxlen > MAX_CHARACTERS)
515: maxlen = MAX_CHARACTERS;
516:
517: if (maxlen > 0) {
518: pageText = m_engine.textToHTML(wikiContext,
519: pageText.substring(firstLine + 1, maxlen)
520: .trim());
521:
522: if (maxlen == MAX_CHARACTERS)
523: pageText += "...";
524:
525: e.setContent(pageText);
526: } else {
527: e.setContent(title);
528: }
529: } else {
530: e.setContent(title);
531: }
532:
533: e.setAuthor(getAuthor(page));
534:
535: feed.addEntry(e);
536: }
537:
538: return feed.getString();
539: }
540:
541: }
|