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