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.  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, "&", "&amp;" );
193        s = TextUtil.replaceString( s, "<", "&lt;" );
194        s = TextUtil.replaceString( s, "]]>", "]]&gt;" );
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        StringBuilder sb = new StringBuilder();
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        StringBuilder buf = new StringBuilder();
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, "&amp;", "&" );
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}