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