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 }