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 */ 019package org.apache.wiki.rss; 020 021import java.util.Collection; 022import java.util.Collections; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Properties; 026 027import org.apache.log4j.Logger; 028import org.apache.wiki.WikiContext; 029import org.apache.wiki.WikiEngine; 030import org.apache.wiki.WikiPage; 031import org.apache.wiki.WikiProvider; 032import org.apache.wiki.WikiSession; 033import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 034import org.apache.wiki.api.exceptions.ProviderException; 035import org.apache.wiki.attachment.Attachment; 036import org.apache.wiki.auth.permissions.PagePermission; 037import org.apache.wiki.util.TextUtil; 038import 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. 057public 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. 159 * 160 * @param engine The WikiEngine. 161 * @param properties The properties. 162 * @throws NoRequiredPropertyException If something is missing from the given property set. 163 */ 164 public RSSGenerator( WikiEngine engine, Properties properties ) 165 throws NoRequiredPropertyException 166 { 167 m_engine = engine; 168 169 m_channelDescription = properties.getProperty( PROP_CHANNEL_DESCRIPTION, 170 m_channelDescription ); 171 m_channelLanguage = properties.getProperty( PROP_CHANNEL_LANGUAGE, 172 m_channelLanguage ); 173 } 174 175 /** 176 * Does the required formatting and entity replacement for XML. 177 * 178 * @param s String to format. 179 * @return A formatted string. 180 */ 181 // FIXME: Replicates Feed.format(). 182 public static String format( String s ) 183 { 184 s = TextUtil.replaceString( s, "&", "&" ); 185 s = TextUtil.replaceString( s, "<", "<" ); 186 s = TextUtil.replaceString( s, "]]>", "]]>" ); 187 188 return s.trim(); 189 } 190 191 private String getAuthor( WikiPage page ) 192 { 193 String author = page.getAuthor(); 194 195 if( author == null ) author = "An unknown author"; 196 197 return author; 198 } 199 200 private String getAttachmentDescription( Attachment att ) 201 { 202 String author = getAuthor(att); 203 StringBuilder sb = new StringBuilder(); 204 205 if( att.getVersion() != 1 ) 206 { 207 sb.append(author+" uploaded a new version of this attachment on "+att.getLastModified() ); 208 } 209 else 210 { 211 sb.append(author+" created this attachment on "+att.getLastModified() ); 212 } 213 214 sb.append("<br /><hr /><br />"); 215 sb.append( "Parent page: <a href=\""+ 216 m_engine.getURL( WikiContext.VIEW, att.getParentName(), null, true ) + 217 "\">"+att.getParentName()+"</a><br />" ); 218 sb.append( "Info page: <a href=\""+ 219 m_engine.getURL( WikiContext.INFO, att.getName(), null, true ) + 220 "\">"+att.getName()+"</a>" ); 221 222 return sb.toString(); 223 } 224 225 private String getPageDescription( WikiPage page ) 226 { 227 StringBuilder buf = new StringBuilder(); 228 String author = getAuthor(page); 229 230 WikiContext ctx = new WikiContext( m_engine, page ); 231 if( page.getVersion() > 1 ) 232 { 233 String diff = m_engine.getDiff( ctx, 234 page.getVersion()-1, // FIXME: Will fail when non-contiguous versions 235 page.getVersion() ); 236 237 buf.append(author+" changed this page on "+page.getLastModified()+":<br /><hr /><br />" ); 238 buf.append(diff); 239 } 240 else 241 { 242 buf.append(author+" created this page on "+page.getLastModified()+":<br /><hr /><br />" ); 243 buf.append(m_engine.getHTML( page.getName() )); 244 } 245 246 return buf.toString(); 247 } 248 249 private String getEntryDescription( WikiPage page ) 250 { 251 String res; 252 253 if( page instanceof Attachment ) 254 { 255 res = getAttachmentDescription( (Attachment)page ); 256 } 257 else 258 { 259 res = getPageDescription( page ); 260 } 261 262 return res; 263 } 264 265 // FIXME: This should probably return something more intelligent 266 private String getEntryTitle( WikiPage page ) 267 { 268 return page.getName()+", version "+page.getVersion(); 269 } 270 271 /** 272 * Generates the RSS resource. You probably want to output this 273 * result into a file or something, or serve as output from a servlet. 274 * 275 * @return A RSS 1.0 feed in the "full" mode. 276 */ 277 public String generate() 278 { 279 WikiContext context = new WikiContext( m_engine,new WikiPage( m_engine, "__DUMMY" ) ); 280 context.setRequestContext( WikiContext.RSS ); 281 Feed feed = new RSS10Feed( context ); 282 283 String result = generateFullWikiRSS( context, feed ); 284 285 result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + result; 286 287 return result; 288 } 289 290 /** 291 * Returns the content type of this RSS feed. 292 * @since 2.3.15 293 * @param mode the RSS mode: {@link #RSS10}, {@link #RSS20} or {@link #ATOM}. 294 * @return the content type 295 */ 296 public static String getContentType( String mode ) 297 { 298 if( mode.equals( RSS10 )||mode.equals(RSS20) ) 299 { 300 return "application/rss+xml"; 301 } 302 else if( mode.equals(ATOM) ) 303 { 304 return "application/atom+xml"; 305 } 306 307 return "application/octet-stream"; // Unknown type 308 } 309 310 /** 311 * Generates a feed based on a context and list of changes. 312 * @param wikiContext The WikiContext 313 * @param changed A list of Entry objects 314 * @param mode The mode (wiki/blog) 315 * @param type The type (RSS10, RSS20, ATOM). Default is RSS 1.0 316 * @return Fully formed XML. 317 * 318 * @throws ProviderException If the underlying provider failed. 319 * @throws IllegalArgumentException If an illegal mode is given. 320 */ 321 public String generateFeed( WikiContext wikiContext, List changed, String mode, String type ) 322 throws ProviderException, IllegalArgumentException 323 { 324 Feed feed = null; 325 String res = null; 326 327 if( ATOM.equals(type) ) 328 { 329 feed = new AtomFeed( wikiContext ); 330 } 331 else if( RSS20.equals( type ) ) 332 { 333 feed = new RSS20Feed( wikiContext ); 334 } 335 else 336 { 337 feed = new RSS10Feed( wikiContext ); 338 } 339 340 feed.setMode( mode ); 341 342 if( MODE_BLOG.equals( mode ) ) 343 { 344 res = generateBlogRSS( wikiContext, changed, feed ); 345 } 346 else if( MODE_FULL.equals(mode) ) 347 { 348 res = generateFullWikiRSS( wikiContext, feed ); 349 } 350 else if( MODE_WIKI.equals(mode) ) 351 { 352 res = generateWikiPageRSS( wikiContext, changed, feed ); 353 } 354 else 355 { 356 throw new IllegalArgumentException( "Invalid value for feed mode: "+mode ); 357 } 358 359 return res; 360 } 361 362 /** 363 * Returns <code>true</code> if RSS generation is enabled. 364 * @return whether RSS generation is currently enabled 365 */ 366 public boolean isEnabled() 367 { 368 return m_enabled; 369 } 370 371 /** 372 * Turns RSS generation on or off. This setting is used to set 373 * the "enabled" flag only for use by callers, and does not 374 * actually affect whether the {@link #generate()} or 375 * {@link #generateFeed(WikiContext, List, String, String)} 376 * methods output anything. 377 * @param enabled whether RSS generation is considered enabled. 378 */ 379 public synchronized void setEnabled( boolean enabled ) 380 { 381 m_enabled = enabled; 382 } 383 384 /** 385 * Generates an RSS feed for the entire wiki. Each item should be an instance of the RSSItem class. 386 * 387 * @param wikiContext A WikiContext 388 * @param feed A Feed to generate the feed to. 389 * @return feed.getString(). 390 */ 391 protected String generateFullWikiRSS( WikiContext wikiContext, Feed feed ) 392 { 393 feed.setChannelTitle( m_engine.getApplicationName() ); 394 feed.setFeedURL( m_engine.getBaseURL() ); 395 feed.setChannelLanguage( m_channelLanguage ); 396 feed.setChannelDescription( m_channelDescription ); 397 398 Collection changed = m_engine.getRecentChanges(); 399 400 WikiSession session = WikiSession.guestSession( m_engine ); 401 int items = 0; 402 for( Iterator i = changed.iterator(); i.hasNext() && items < 15; items++ ) 403 { 404 WikiPage page = (WikiPage) i.next(); 405 406 // 407 // Check if the anonymous user has view access to this page. 408 // 409 410 if( !m_engine.getAuthorizationManager().checkPermission(session, 411 new PagePermission(page,PagePermission.VIEW_ACTION) ) ) 412 { 413 // No permission, skip to the next one. 414 continue; 415 } 416 417 Entry e = new Entry(); 418 419 e.setPage( page ); 420 421 String url; 422 423 if( page instanceof Attachment ) 424 { 425 url = m_engine.getURL( WikiContext.ATTACH, 426 page.getName(), 427 null, 428 true ); 429 } 430 else 431 { 432 url = m_engine.getURL( WikiContext.VIEW, 433 page.getName(), 434 null, 435 true ); 436 } 437 438 e.setURL( url ); 439 e.setTitle( page.getName() ); 440 e.setContent( getEntryDescription(page) ); 441 e.setAuthor( getAuthor(page) ); 442 443 feed.addEntry( e ); 444 } 445 446 return feed.getString(); 447 } 448 449 /** 450 * Create RSS/Atom as if this page was a wikipage (in contrast to Blog mode). 451 * 452 * @param wikiContext The WikiContext 453 * @param changed A List of changed WikiPages. 454 * @param feed A Feed object to fill. 455 * @return the RSS representation of the wiki context 456 */ 457 @SuppressWarnings("unchecked") 458 protected String generateWikiPageRSS( WikiContext wikiContext, List changed, Feed feed ) 459 { 460 feed.setChannelTitle( m_engine.getApplicationName()+": "+wikiContext.getPage().getName() ); 461 feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) ); 462 String language = m_engine.getVariable( wikiContext, PROP_CHANNEL_LANGUAGE ); 463 464 if( language != null ) 465 feed.setChannelLanguage( language ); 466 else 467 feed.setChannelLanguage( m_channelLanguage ); 468 469 String channelDescription = m_engine.getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION ); 470 471 if( channelDescription != null ) 472 { 473 feed.setChannelDescription( channelDescription ); 474 } 475 476 Collections.sort( changed, new PageTimeComparator() ); 477 478 int items = 0; 479 for( Iterator i = changed.iterator(); i.hasNext() && items < 15; items++ ) 480 { 481 WikiPage page = (WikiPage) i.next(); 482 483 Entry e = new Entry(); 484 485 e.setPage( page ); 486 487 String url; 488 489 if( page instanceof Attachment ) 490 { 491 url = m_engine.getURL( WikiContext.ATTACH, 492 page.getName(), 493 "version="+page.getVersion(), 494 true ); 495 } 496 else 497 { 498 url = m_engine.getURL( WikiContext.VIEW, 499 page.getName(), 500 "version="+page.getVersion(), 501 true ); 502 } 503 504 // Unfortunately, this is needed because the code will again go through 505 // replacement conversion 506 507 url = TextUtil.replaceString( url, "&", "&" ); 508 509 e.setURL( url ); 510 e.setTitle( getEntryTitle(page) ); 511 e.setContent( getEntryDescription(page) ); 512 e.setAuthor( getAuthor(page) ); 513 514 feed.addEntry( e ); 515 } 516 517 return feed.getString(); 518 } 519 520 521 /** 522 * Creates RSS from modifications as if this page was a blog (using the WeblogPlugin). 523 * 524 * @param wikiContext The WikiContext, as usual. 525 * @param changed A list of the changed pages. 526 * @param feed A valid Feed object. The feed will be used to create the RSS/Atom, depending 527 * on which kind of an object you want to put in it. 528 * @return A String of valid RSS or Atom. 529 * @throws ProviderException If reading of pages was not possible. 530 */ 531 @SuppressWarnings("unchecked") 532 protected String generateBlogRSS( WikiContext wikiContext, List changed, Feed feed ) 533 throws ProviderException 534 { 535 if( log.isDebugEnabled() ) log.debug("Generating RSS for blog, size="+changed.size()); 536 537 String ctitle = m_engine.getVariable( wikiContext, PROP_CHANNEL_TITLE ); 538 539 if( ctitle != null ) 540 feed.setChannelTitle( ctitle ); 541 else 542 feed.setChannelTitle( m_engine.getApplicationName()+":"+wikiContext.getPage().getName() ); 543 544 feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) ); 545 546 String language = m_engine.getVariable( wikiContext, PROP_CHANNEL_LANGUAGE ); 547 548 if( language != null ) 549 feed.setChannelLanguage( language ); 550 else 551 feed.setChannelLanguage( m_channelLanguage ); 552 553 String channelDescription = m_engine.getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION ); 554 555 if( channelDescription != null ) 556 { 557 feed.setChannelDescription( channelDescription ); 558 } 559 560 Collections.sort( changed, new PageTimeComparator() ); 561 562 int items = 0; 563 for( Iterator i = changed.iterator(); i.hasNext() && items < 15; items++ ) 564 { 565 WikiPage page = (WikiPage) i.next(); 566 567 Entry e = new Entry(); 568 569 e.setPage( page ); 570 571 String url; 572 573 if( page instanceof Attachment ) 574 { 575 url = m_engine.getURL( WikiContext.ATTACH, 576 page.getName(), 577 null, 578 true ); 579 } 580 else 581 { 582 url = m_engine.getURL( WikiContext.VIEW, 583 page.getName(), 584 null, 585 true ); 586 } 587 588 e.setURL( url ); 589 590 // 591 // Title 592 // 593 594 String pageText = m_engine.getPureText(page.getName(), WikiProvider.LATEST_VERSION ); 595 596 String title = ""; 597 int firstLine = pageText.indexOf('\n'); 598 599 if( firstLine > 0 ) 600 { 601 title = pageText.substring( 0, firstLine ).trim(); 602 } 603 604 if( title.length() == 0 ) title = page.getName(); 605 606 // Remove wiki formatting 607 while( title.startsWith("!") ) title = title.substring(1); 608 609 e.setTitle( title ); 610 611 // 612 // Description 613 // 614 615 if( firstLine > 0 ) 616 { 617 int maxlen = pageText.length(); 618 if( maxlen > MAX_CHARACTERS ) maxlen = MAX_CHARACTERS; 619 620 if( maxlen > 0 ) 621 { 622 pageText = m_engine.textToHTML( wikiContext, 623 pageText.substring( firstLine+1, 624 maxlen ).trim() ); 625 626 if( maxlen == MAX_CHARACTERS ) pageText += "..."; 627 628 e.setContent( pageText ); 629 } 630 else 631 { 632 e.setContent( title ); 633 } 634 } 635 else 636 { 637 e.setContent( title ); 638 } 639 640 e.setAuthor( getAuthor(page) ); 641 642 feed.addEntry( e ); 643 } 644 645 return feed.getString(); 646 } 647 648}