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, "&", "&amp;" );
149        s = TextUtil.replaceString( s, "<", "&lt;" );
150        s = TextUtil.replaceString( s, "]]>", "]]&gt;" );
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, "&amp;", "&" );
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}