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