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.Collections; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Properties; 025import java.util.Set; 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.pages.PageTimeComparator; 038import org.apache.wiki.util.TextUtil; 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< WikiPage > 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 Set< WikiPage > changed = m_engine.getRecentChanges(); 399 400 WikiSession session = WikiSession.guestSession( m_engine ); 401 int items = 0; 402 for( Iterator< WikiPage > i = changed.iterator(); i.hasNext() && items < 15; items++ ) 403 { 404 WikiPage page = 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, new PagePermission(page,PagePermission.VIEW_ACTION) ) ) 411 { 412 // No permission, skip to the next one. 413 continue; 414 } 415 416 Entry e = new Entry(); 417 418 e.setPage( page ); 419 420 String url; 421 422 if( page instanceof Attachment ) 423 { 424 url = m_engine.getURL( WikiContext.ATTACH, 425 page.getName(), 426 null, 427 true ); 428 } 429 else 430 { 431 url = m_engine.getURL( WikiContext.VIEW, 432 page.getName(), 433 null, 434 true ); 435 } 436 437 e.setURL( url ); 438 e.setTitle( page.getName() ); 439 e.setContent( getEntryDescription(page) ); 440 e.setAuthor( getAuthor(page) ); 441 442 feed.addEntry( e ); 443 } 444 445 return feed.getString(); 446 } 447 448 /** 449 * Create RSS/Atom as if this page was a wikipage (in contrast to Blog mode). 450 * 451 * @param wikiContext The WikiContext 452 * @param changed A List of changed WikiPages. 453 * @param feed A Feed object to fill. 454 * @return the RSS representation of the wiki context 455 */ 456 protected String generateWikiPageRSS( WikiContext wikiContext, List< WikiPage > changed, Feed feed ) 457 { 458 feed.setChannelTitle( m_engine.getApplicationName()+": "+wikiContext.getPage().getName() ); 459 feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) ); 460 String language = m_engine.getVariable( wikiContext, PROP_CHANNEL_LANGUAGE ); 461 462 if( language != null ) 463 feed.setChannelLanguage( language ); 464 else 465 feed.setChannelLanguage( m_channelLanguage ); 466 467 String channelDescription = m_engine.getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION ); 468 469 if( channelDescription != null ) 470 { 471 feed.setChannelDescription( channelDescription ); 472 } 473 474 Collections.sort( changed, new PageTimeComparator() ); 475 476 int items = 0; 477 for( Iterator< WikiPage > i = changed.iterator(); i.hasNext() && items < 15; items++ ) 478 { 479 WikiPage page = i.next(); 480 481 Entry e = new Entry(); 482 483 e.setPage( page ); 484 485 String url; 486 487 if( page instanceof Attachment ) 488 { 489 url = m_engine.getURL( WikiContext.ATTACH, 490 page.getName(), 491 "version="+page.getVersion(), 492 true ); 493 } 494 else 495 { 496 url = m_engine.getURL( WikiContext.VIEW, 497 page.getName(), 498 "version="+page.getVersion(), 499 true ); 500 } 501 502 // Unfortunately, this is needed because the code will again go through 503 // replacement conversion 504 505 url = TextUtil.replaceString( url, "&", "&" ); 506 507 e.setURL( url ); 508 e.setTitle( getEntryTitle(page) ); 509 e.setContent( getEntryDescription(page) ); 510 e.setAuthor( getAuthor(page) ); 511 512 feed.addEntry( e ); 513 } 514 515 return feed.getString(); 516 } 517 518 519 /** 520 * Creates RSS from modifications as if this page was a blog (using the WeblogPlugin). 521 * 522 * @param wikiContext The WikiContext, as usual. 523 * @param changed A list of the changed pages. 524 * @param feed A valid Feed object. The feed will be used to create the RSS/Atom, depending 525 * on which kind of an object you want to put in it. 526 * @return A String of valid RSS or Atom. 527 * @throws ProviderException If reading of pages was not possible. 528 */ 529 protected String generateBlogRSS( WikiContext wikiContext, List< WikiPage > changed, Feed feed ) 530 throws ProviderException 531 { 532 if( log.isDebugEnabled() ) log.debug("Generating RSS for blog, size="+changed.size()); 533 534 String ctitle = m_engine.getVariable( wikiContext, PROP_CHANNEL_TITLE ); 535 536 if( ctitle != null ) 537 feed.setChannelTitle( ctitle ); 538 else 539 feed.setChannelTitle( m_engine.getApplicationName()+":"+wikiContext.getPage().getName() ); 540 541 feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) ); 542 543 String language = m_engine.getVariable( wikiContext, PROP_CHANNEL_LANGUAGE ); 544 545 if( language != null ) 546 feed.setChannelLanguage( language ); 547 else 548 feed.setChannelLanguage( m_channelLanguage ); 549 550 String channelDescription = m_engine.getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION ); 551 552 if( channelDescription != null ) 553 { 554 feed.setChannelDescription( channelDescription ); 555 } 556 557 Collections.sort( changed, new PageTimeComparator() ); 558 559 int items = 0; 560 for( Iterator< WikiPage > i = changed.iterator(); i.hasNext() && items < 15; items++ ) 561 { 562 WikiPage page = i.next(); 563 564 Entry e = new Entry(); 565 566 e.setPage( page ); 567 568 String url; 569 570 if( page instanceof Attachment ) 571 { 572 url = m_engine.getURL( WikiContext.ATTACH, 573 page.getName(), 574 null, 575 true ); 576 } 577 else 578 { 579 url = m_engine.getURL( WikiContext.VIEW, 580 page.getName(), 581 null, 582 true ); 583 } 584 585 e.setURL( url ); 586 587 // 588 // Title 589 // 590 591 String pageText = m_engine.getPureText(page.getName(), WikiProvider.LATEST_VERSION ); 592 593 String title = ""; 594 int firstLine = pageText.indexOf('\n'); 595 596 if( firstLine > 0 ) 597 { 598 title = pageText.substring( 0, firstLine ).trim(); 599 } 600 601 if( title.length() == 0 ) title = page.getName(); 602 603 // Remove wiki formatting 604 while( title.startsWith("!") ) title = title.substring(1); 605 606 e.setTitle( title ); 607 608 // 609 // Description 610 // 611 612 if( firstLine > 0 ) 613 { 614 int maxlen = pageText.length(); 615 if( maxlen > MAX_CHARACTERS ) maxlen = MAX_CHARACTERS; 616 617 if( maxlen > 0 ) 618 { 619 pageText = m_engine.textToHTML( wikiContext, 620 pageText.substring( firstLine+1, 621 maxlen ).trim() ); 622 623 if( maxlen == MAX_CHARACTERS ) pageText += "..."; 624 625 e.setContent( pageText ); 626 } 627 else 628 { 629 e.setContent( title ); 630 } 631 } 632 else 633 { 634 e.setContent( title ); 635 } 636 637 e.setAuthor( getAuthor(page) ); 638 639 feed.addEntry( e ); 640 } 641 642 return feed.getString(); 643 } 644 645}