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.logging.log4j.LogManager; 022import org.apache.logging.log4j.Logger; 023import org.apache.wiki.api.core.Attachment; 024import org.apache.wiki.api.core.Context; 025import org.apache.wiki.api.core.ContextEnum; 026import org.apache.wiki.api.core.Engine; 027import org.apache.wiki.api.core.Page; 028import org.apache.wiki.api.core.Session; 029import org.apache.wiki.api.providers.WikiProvider; 030import org.apache.wiki.api.spi.Wiki; 031import org.apache.wiki.auth.AuthorizationManager; 032import org.apache.wiki.auth.permissions.PagePermission; 033import org.apache.wiki.diff.DifferenceManager; 034import org.apache.wiki.pages.PageManager; 035import org.apache.wiki.pages.PageTimeComparator; 036import org.apache.wiki.render.RenderingManager; 037import org.apache.wiki.util.TextUtil; 038import org.apache.wiki.variables.VariableManager; 039 040import java.io.File; 041import java.util.Iterator; 042import java.util.List; 043import java.util.Properties; 044import java.util.Set; 045 046 047/** 048 * Default implementation for {@link RSSGenerator}. 049 * 050 * {@inheritDoc} 051 */ 052// FIXME: Limit diff and page content size. 053public class DefaultRSSGenerator implements RSSGenerator { 054 055 private static final Logger log = LogManager.getLogger( DefaultRSSGenerator.class ); 056 private final Engine m_engine; 057 058 /** The RSS file to generate. */ 059 private final String m_rssFile; 060 private String m_channelDescription = ""; 061 private String m_channelLanguage = "en-us"; 062 private boolean m_enabled = true; 063 064 private static final int MAX_CHARACTERS = Integer.MAX_VALUE-1; 065 066 /** 067 * Builds the RSS generator for a given Engine. 068 * 069 * @param engine The Engine. 070 * @param properties The properties. 071 */ 072 public DefaultRSSGenerator( final Engine engine, final Properties properties ) { 073 m_engine = engine; 074 m_channelDescription = properties.getProperty( PROP_CHANNEL_DESCRIPTION, m_channelDescription ); 075 m_channelLanguage = properties.getProperty( PROP_CHANNEL_LANGUAGE, m_channelLanguage ); 076 m_rssFile = TextUtil.getStringProperty( properties, DefaultRSSGenerator.PROP_RSSFILE, "rss.rdf" ); 077 } 078 079 /** 080 * {@inheritDoc} 081 * 082 * Start the RSS generator & generator thread 083 */ 084 @Override 085 public void initialize( final Engine engine, final Properties properties ) { 086 final File rssFile; 087 if( m_rssFile.startsWith( File.separator ) ) { // honor absolute pathnames 088 rssFile = new File( m_rssFile ); 089 } else { // relative path names are anchored from the webapp root path 090 rssFile = new File( engine.getRootPath(), m_rssFile ); 091 } 092 final int rssInterval = TextUtil.getIntegerProperty( properties, DefaultRSSGenerator.PROP_INTERVAL, 3600 ); 093 final RSSThread rssThread = new RSSThread( engine, rssFile, rssInterval ); 094 rssThread.start(); 095 } 096 097 private String getAuthor( final Page page ) { 098 String author = page.getAuthor(); 099 if( author == null ) { 100 author = "An unknown author"; 101 } 102 103 return author; 104 } 105 106 private String getAttachmentDescription( final Attachment att ) { 107 final String author = getAuthor( att ); 108 final StringBuilder sb = new StringBuilder(); 109 110 if( att.getVersion() != 1 ) { 111 sb.append( author ).append( " uploaded a new version of this attachment on " ).append( att.getLastModified() ); 112 } else { 113 sb.append( author ).append( " created this attachment on " ).append( att.getLastModified() ); 114 } 115 116 sb.append( "<br /><hr /><br />" ) 117 .append( "Parent page: <a href=\"" ) 118 .append( m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), att.getParentName(), null ) ) 119 .append( "\">" ).append( att.getParentName() ).append( "</a><br />" ) 120 .append( "Info page: <a href=\"" ) 121 .append( m_engine.getURL( ContextEnum.PAGE_INFO.getRequestContext(), att.getName(), null ) ) 122 .append( "\">" ).append( att.getName() ).append( "</a>" ); 123 124 return sb.toString(); 125 } 126 127 private String getPageDescription( final Page page ) { 128 final StringBuilder buf = new StringBuilder(); 129 final String author = getAuthor( page ); 130 final Context ctx = Wiki.context().create( m_engine, page ); 131 if( page.getVersion() > 1 ) { 132 final String diff = m_engine.getManager( DifferenceManager.class ).getDiff( ctx, 133 page.getVersion() - 1, // FIXME: Will fail when non-contiguous versions 134 page.getVersion() ); 135 136 buf.append( author ).append( " changed this page on " ).append( page.getLastModified() ).append( ":<br /><hr /><br />" ); 137 buf.append( diff ); 138 } else { 139 buf.append( author ).append( " created this page on " ).append( page.getLastModified() ).append( ":<br /><hr /><br />" ); 140 buf.append( m_engine.getManager( RenderingManager.class ).getHTML( page.getName() ) ); 141 } 142 143 return buf.toString(); 144 } 145 146 private String getEntryDescription( final Page page ) { 147 final String res; 148 if( page instanceof Attachment ) { 149 res = getAttachmentDescription( (Attachment)page ); 150 } else { 151 res = getPageDescription( page ); 152 } 153 154 return res; 155 } 156 157 // FIXME: This should probably return something more intelligent 158 private String getEntryTitle( final Page page ) { 159 return page.getName() + ", version " + page.getVersion(); 160 } 161 162 /** {@inheritDoc} */ 163 @Override 164 public String generate() { 165 final Context context = Wiki.context().create( m_engine, Wiki.contents().page( m_engine, "__DUMMY" ) ); 166 context.setRequestContext( ContextEnum.PAGE_RSS.getRequestContext() ); 167 final Feed feed = new RSS10Feed( context ); 168 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + generateFullWikiRSS( context, feed ); 169 } 170 171 /** {@inheritDoc} */ 172 @Override 173 public String generateFeed( final Context wikiContext, final List< Page > changed, final String mode, final String type ) throws IllegalArgumentException { 174 final Feed feed; 175 final String res; 176 177 if( ATOM.equals(type) ) { 178 feed = new AtomFeed( wikiContext ); 179 } else if( RSS20.equals( type ) ) { 180 feed = new RSS20Feed( wikiContext ); 181 } else { 182 feed = new RSS10Feed( wikiContext ); 183 } 184 185 feed.setMode( mode ); 186 187 if( MODE_BLOG.equals( mode ) ) { 188 res = generateBlogRSS( wikiContext, changed, feed ); 189 } else if( MODE_FULL.equals(mode) ) { 190 res = generateFullWikiRSS( wikiContext, feed ); 191 } else if( MODE_WIKI.equals(mode) ) { 192 res = generateWikiPageRSS( wikiContext, changed, feed ); 193 } else { 194 throw new IllegalArgumentException( "Invalid value for feed mode: "+mode ); 195 } 196 197 return res; 198 } 199 200 /** {@inheritDoc} */ 201 @Override 202 public synchronized boolean isEnabled() { 203 return m_enabled; 204 } 205 206 /** {@inheritDoc} */ 207 @Override 208 public synchronized void setEnabled( final boolean enabled ) { 209 m_enabled = enabled; 210 } 211 212 /** {@inheritDoc} */ 213 @Override 214 public String getRssFile() { 215 return m_rssFile; 216 } 217 218 /** {@inheritDoc} */ 219 @Override 220 public String generateFullWikiRSS( final Context wikiContext, final Feed feed ) { 221 feed.setChannelTitle( m_engine.getApplicationName() ); 222 feed.setFeedURL( m_engine.getBaseURL() ); 223 feed.setChannelLanguage( m_channelLanguage ); 224 feed.setChannelDescription( m_channelDescription ); 225 226 final Set< Page > changed = m_engine.getManager( PageManager.class ).getRecentChanges(); 227 228 final Session session = Wiki.session().guest( m_engine ); 229 int items = 0; 230 for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) { 231 final Page page = i.next(); 232 233 // Check if the anonymous user has view access to this page. 234 if( !m_engine.getManager( AuthorizationManager.class ).checkPermission(session, new PagePermission(page,PagePermission.VIEW_ACTION) ) ) { 235 // No permission, skip to the next one. 236 continue; 237 } 238 239 final String url; 240 if( page instanceof Attachment ) { 241 url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(),null ); 242 } else { 243 url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(), null ); 244 } 245 246 final Entry e = new Entry(); 247 e.setPage( page ); 248 e.setURL( url ); 249 e.setTitle( page.getName() ); 250 e.setContent( getEntryDescription(page) ); 251 e.setAuthor( getAuthor(page) ); 252 253 feed.addEntry( e ); 254 } 255 256 return feed.getString(); 257 } 258 259 /** {@inheritDoc} */ 260 @Override 261 public String generateWikiPageRSS( final Context wikiContext, final List< Page > changed, final Feed feed ) { 262 feed.setChannelTitle( m_engine.getApplicationName()+": "+wikiContext.getPage().getName() ); 263 feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) ); 264 final String language = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_LANGUAGE ); 265 266 if( language != null ) { 267 feed.setChannelLanguage( language ); 268 } else { 269 feed.setChannelLanguage( m_channelLanguage ); 270 } 271 final String channelDescription = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION ); 272 273 if( channelDescription != null ) { 274 feed.setChannelDescription( channelDescription ); 275 } 276 277 changed.sort( new PageTimeComparator() ); 278 279 int items = 0; 280 for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) { 281 final Page page = i.next(); 282 final Entry e = new Entry(); 283 e.setPage( page ); 284 String url; 285 286 if( page instanceof Attachment ) { 287 url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(), "version=" + page.getVersion() ); 288 } else { 289 url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(), "version=" + page.getVersion() ); 290 } 291 292 // Unfortunately, this is needed because the code will again go through replacement conversion 293 url = TextUtil.replaceString( url, "&", "&" ); 294 e.setURL( url ); 295 e.setTitle( getEntryTitle(page) ); 296 e.setContent( getEntryDescription(page) ); 297 e.setAuthor( getAuthor(page) ); 298 299 feed.addEntry( e ); 300 } 301 302 return feed.getString(); 303 } 304 305 306 /** {@inheritDoc} */ 307 @Override 308 public String generateBlogRSS( final Context wikiContext, final List< Page > changed, final Feed feed ) { 309 log.debug( "Generating RSS for blog, size={}", changed.size() ); 310 311 final String ctitle = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_TITLE ); 312 if( ctitle != null ) { 313 feed.setChannelTitle( ctitle ); 314 } else { 315 feed.setChannelTitle( m_engine.getApplicationName() + ":" + wikiContext.getPage().getName() ); 316 } 317 318 feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) ); 319 320 final String language = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_LANGUAGE ); 321 if( language != null ) { 322 feed.setChannelLanguage( language ); 323 } else { 324 feed.setChannelLanguage( m_channelLanguage ); 325 } 326 327 final String channelDescription = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION ); 328 if( channelDescription != null ) { 329 feed.setChannelDescription( channelDescription ); 330 } 331 332 changed.sort( new PageTimeComparator() ); 333 334 int items = 0; 335 for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) { 336 final Page page = i.next(); 337 final Entry e = new Entry(); 338 e.setPage( page ); 339 final String url; 340 341 if( page instanceof Attachment ) { 342 url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(),null ); 343 } else { 344 url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(),null ); 345 } 346 347 e.setURL( url ); 348 349 // Title 350 String pageText = m_engine.getManager( PageManager.class ).getPureText( page.getName(), WikiProvider.LATEST_VERSION ); 351 352 String title = ""; 353 final int firstLine = pageText.indexOf('\n'); 354 355 if( firstLine > 0 ) { 356 title = pageText.substring( 0, firstLine ).trim(); 357 } 358 359 if( title.isEmpty() ) { 360 title = page.getName(); 361 } 362 363 // Remove wiki formatting 364 while( title.startsWith("!") ) { 365 title = title.substring(1); 366 } 367 368 e.setTitle( title ); 369 370 // Description 371 if( firstLine > 0 ) { 372 int maxlen = pageText.length(); 373 if( maxlen > MAX_CHARACTERS ) { 374 maxlen = MAX_CHARACTERS; 375 } 376 pageText = m_engine.getManager( RenderingManager.class ).textToHTML( wikiContext, pageText.substring( firstLine + 1, maxlen ).trim() ); 377 if( maxlen == MAX_CHARACTERS ) { 378 pageText += "..."; 379 } 380 e.setContent( pageText ); 381 } else { 382 e.setContent( title ); 383 } 384 e.setAuthor( getAuthor(page) ); 385 feed.addEntry( e ); 386 } 387 388 return feed.getString(); 389 } 390 391}