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     */
019    package org.apache.wiki.rss;
020    
021    import java.util.Collection;
022    import java.util.Collections;
023    import java.util.Iterator;
024    import java.util.List;
025    import java.util.Properties;
026    
027    import org.apache.log4j.Logger;
028    import org.apache.wiki.WikiContext;
029    import org.apache.wiki.WikiEngine;
030    import org.apache.wiki.WikiPage;
031    import org.apache.wiki.WikiProvider;
032    import org.apache.wiki.WikiSession;
033    import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
034    import org.apache.wiki.api.exceptions.ProviderException;
035    import org.apache.wiki.attachment.Attachment;
036    import org.apache.wiki.auth.permissions.PagePermission;
037    import org.apache.wiki.util.TextUtil;
038    import 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.
057    public 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            StringBuffer sb = new StringBuffer();
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            StringBuffer buf = new StringBuffer();
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    }