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