001/* 
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.  
018 */
019package org.apache.wiki.rss;
020
021import org.apache.log4j.Logger;
022import org.apache.wiki.api.core.Attachment;
023import org.apache.wiki.api.core.Context;
024import org.apache.wiki.api.core.ContextEnum;
025import org.apache.wiki.api.core.Engine;
026import org.apache.wiki.api.core.Page;
027import org.apache.wiki.api.core.Session;
028import org.apache.wiki.api.providers.WikiProvider;
029import org.apache.wiki.api.spi.Wiki;
030import org.apache.wiki.auth.AuthorizationManager;
031import org.apache.wiki.auth.permissions.PagePermission;
032import org.apache.wiki.diff.DifferenceManager;
033import org.apache.wiki.pages.PageManager;
034import org.apache.wiki.pages.PageTimeComparator;
035import org.apache.wiki.render.RenderingManager;
036import org.apache.wiki.util.TextUtil;
037import org.apache.wiki.variables.VariableManager;
038
039import java.io.File;
040import java.util.Iterator;
041import java.util.List;
042import java.util.Properties;
043import java.util.Set;
044
045
046/**
047 * Default implementation for {@link RSSGenerator}.
048 *
049 * {@inheritDoc}
050 */
051// FIXME: Limit diff and page content size.
052public class DefaultRSSGenerator implements RSSGenerator {
053
054    private static final Logger log = Logger.getLogger( DefaultRSSGenerator.class );
055    private Engine m_engine;
056
057    /** The RSS file to generate. */
058    private String m_rssFile;
059    private String m_channelDescription = "";
060    private String m_channelLanguage = "en-us";
061    private boolean m_enabled = true;
062
063    private static final int MAX_CHARACTERS = Integer.MAX_VALUE-1;
064
065    /**
066     *  Builds the RSS generator for a given Engine.
067     *
068     *  @param engine The Engine.
069     *  @param properties The properties.
070     */
071    public DefaultRSSGenerator( final Engine engine, final Properties properties ) {
072        m_engine = engine;
073        m_channelDescription = properties.getProperty( PROP_CHANNEL_DESCRIPTION, m_channelDescription );
074        m_channelLanguage = properties.getProperty( PROP_CHANNEL_LANGUAGE, m_channelLanguage );
075        m_rssFile = TextUtil.getStringProperty( properties, DefaultRSSGenerator.PROP_RSSFILE, "rss.rdf" );
076    }
077
078    /**
079     * {@inheritDoc}
080     *
081     * Start the RSS generator & generator thread
082     */
083    @Override
084    public void initialize( final Engine engine, final Properties properties ) {
085        final File rssFile;
086        if( m_rssFile.startsWith( File.separator ) ) { // honor absolute pathnames
087            rssFile = new File( m_rssFile );
088        } else { // relative path names are anchored from the webapp root path
089            rssFile = new File( engine.getRootPath(), m_rssFile );
090        }
091        final int rssInterval = TextUtil.getIntegerProperty( properties, DefaultRSSGenerator.PROP_INTERVAL, 3600 );
092        final RSSThread rssThread = new RSSThread( engine, rssFile, rssInterval );
093        rssThread.start();
094    }
095
096    private String getAuthor( final Page page ) {
097        String author = page.getAuthor();
098        if( author == null ) {
099            author = "An unknown author";
100        }
101
102        return author;
103    }
104
105    private String getAttachmentDescription( final Attachment att ) {
106        final String author = getAuthor( att );
107        final StringBuilder sb = new StringBuilder();
108
109        if( att.getVersion() != 1 ) {
110            sb.append( author ).append( " uploaded a new version of this attachment on " ).append( att.getLastModified() );
111        } else {
112            sb.append( author ).append( " created this attachment on " ).append( att.getLastModified() );
113        }
114
115        sb.append( "<br /><hr /><br />" )
116          .append( "Parent page: <a href=\"" )
117          .append( m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), att.getParentName(), null ) )
118          .append( "\">" ).append( att.getParentName() ).append( "</a><br />" )
119          .append( "Info page: <a href=\"" )
120          .append( m_engine.getURL( ContextEnum.PAGE_INFO.getRequestContext(), att.getName(), null ) )
121          .append( "\">" ).append( att.getName() ).append( "</a>" );
122
123        return sb.toString();
124    }
125
126    private String getPageDescription( final Page page ) {
127        final StringBuilder buf = new StringBuilder();
128        final String author = getAuthor( page );
129        final Context ctx = Wiki.context().create( m_engine, page );
130        if( page.getVersion() > 1 ) {
131            final String diff = m_engine.getManager( DifferenceManager.class ).getDiff( ctx,
132                                                                page.getVersion() - 1, // FIXME: Will fail when non-contiguous versions
133                                                                         page.getVersion() );
134
135            buf.append( author ).append( " changed this page on " ).append( page.getLastModified() ).append( ":<br /><hr /><br />" );
136            buf.append( diff );
137        } else {
138            buf.append( author ).append( " created this page on " ).append( page.getLastModified() ).append( ":<br /><hr /><br />" );
139            buf.append( m_engine.getManager( RenderingManager.class ).getHTML( page.getName() ) );
140        }
141
142        return buf.toString();
143    }
144
145    private String getEntryDescription( final Page page ) {
146        final String res;
147        if( page instanceof Attachment ) {
148            res = getAttachmentDescription( (Attachment)page );
149        } else {
150            res = getPageDescription( page );
151        }
152
153        return res;
154    }
155
156    // FIXME: This should probably return something more intelligent
157    private String getEntryTitle( final Page page ) {
158        return page.getName() + ", version " + page.getVersion();
159    }
160
161    /** {@inheritDoc} */
162    @Override
163    public String generate() {
164        final Context context = Wiki.context().create( m_engine, Wiki.contents().page( m_engine, "__DUMMY" ) );
165        context.setRequestContext( ContextEnum.PAGE_RSS.getRequestContext() );
166        final Feed feed = new RSS10Feed( context );
167        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + generateFullWikiRSS( context, feed );
168    }
169
170    /** {@inheritDoc} */
171    @Override
172    public String generateFeed( final Context wikiContext, final List< Page > changed, final String mode, final String type ) throws IllegalArgumentException {
173        final Feed feed;
174        final String res;
175
176        if( ATOM.equals(type) ) {
177            feed = new AtomFeed( wikiContext );
178        } else if( RSS20.equals( type ) ) {
179            feed = new RSS20Feed( wikiContext );
180        } else {
181            feed = new RSS10Feed( wikiContext );
182        }
183
184        feed.setMode( mode );
185
186        if( MODE_BLOG.equals( mode ) ) {
187            res = generateBlogRSS( wikiContext, changed, feed );
188        } else if( MODE_FULL.equals(mode) ) {
189            res = generateFullWikiRSS( wikiContext, feed );
190        } else if( MODE_WIKI.equals(mode) ) {
191            res = generateWikiPageRSS( wikiContext, changed, feed );
192        } else {
193            throw new IllegalArgumentException( "Invalid value for feed mode: "+mode );
194        }
195
196        return res;
197    }
198
199    /** {@inheritDoc} */
200    @Override
201    public synchronized boolean isEnabled() {
202        return m_enabled;
203    }
204
205    /** {@inheritDoc} */
206    @Override
207    public synchronized void setEnabled( final boolean enabled ) {
208        m_enabled = enabled;
209    }
210
211    /** {@inheritDoc} */
212    @Override
213    public String getRssFile() {
214        return m_rssFile;
215    }
216
217    /** {@inheritDoc} */
218    @Override
219    public String generateFullWikiRSS( final Context wikiContext, final Feed feed ) {
220        feed.setChannelTitle( m_engine.getApplicationName() );
221        feed.setFeedURL( m_engine.getBaseURL() );
222        feed.setChannelLanguage( m_channelLanguage );
223        feed.setChannelDescription( m_channelDescription );
224
225        final Set< Page > changed = m_engine.getManager( PageManager.class ).getRecentChanges();
226
227        final Session session = Wiki.session().guest( m_engine );
228        int items = 0;
229        for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) {
230            final Page page = i.next();
231
232            //  Check if the anonymous user has view access to this page.
233            if( !m_engine.getManager( AuthorizationManager.class ).checkPermission(session, new PagePermission(page,PagePermission.VIEW_ACTION) ) ) {
234                // No permission, skip to the next one.
235                continue;
236            }
237
238            final String url;
239            if( page instanceof Attachment ) {
240                url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(),null );
241            } else {
242                url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(), null );
243            }
244
245            final Entry e = new Entry();
246            e.setPage( page );
247            e.setURL( url );
248            e.setTitle( page.getName() );
249            e.setContent( getEntryDescription(page) );
250            e.setAuthor( getAuthor(page) );
251
252            feed.addEntry( e );
253        }
254
255        return feed.getString();
256    }
257
258    /** {@inheritDoc} */
259    @Override
260    public String generateWikiPageRSS( final Context wikiContext, final List< Page > changed, final Feed feed ) {
261        feed.setChannelTitle( m_engine.getApplicationName()+": "+wikiContext.getPage().getName() );
262        feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) );
263        final String language = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_LANGUAGE );
264
265        if( language != null ) {
266            feed.setChannelLanguage( language );
267        } else {
268            feed.setChannelLanguage( m_channelLanguage );
269        }
270        final String channelDescription = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION );
271
272        if( channelDescription != null ) {
273            feed.setChannelDescription( channelDescription );
274        }
275
276        changed.sort( new PageTimeComparator() );
277
278        int items = 0;
279        for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) {
280            final Page page = i.next();
281            final Entry e = new Entry();
282            e.setPage( page );
283            String url;
284
285            if( page instanceof Attachment ) {
286                url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(), "version=" + page.getVersion() );
287            } else {
288                url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(), "version=" + page.getVersion() );
289            }
290
291            // Unfortunately, this is needed because the code will again go through replacement conversion
292            url = TextUtil.replaceString( url, "&amp;", "&" );
293            e.setURL( url );
294            e.setTitle( getEntryTitle(page) );
295            e.setContent( getEntryDescription(page) );
296            e.setAuthor( getAuthor(page) );
297
298            feed.addEntry( e );
299        }
300
301        return feed.getString();
302    }
303
304
305    /** {@inheritDoc} */
306    @Override
307    public String generateBlogRSS( final Context wikiContext, final List< Page > changed, final Feed feed ) {
308        if( log.isDebugEnabled() ) {
309            log.debug( "Generating RSS for blog, size=" + changed.size() );
310        }
311
312        final String ctitle = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_TITLE );
313        if( ctitle != null ) {
314            feed.setChannelTitle( ctitle );
315        } else {
316            feed.setChannelTitle( m_engine.getApplicationName() + ":" + wikiContext.getPage().getName() );
317        }
318
319        feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) );
320
321        final String language = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_LANGUAGE );
322        if( language != null ) {
323            feed.setChannelLanguage( language );
324        } else {
325            feed.setChannelLanguage( m_channelLanguage );
326        }
327
328        final String channelDescription = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION );
329        if( channelDescription != null ) {
330            feed.setChannelDescription( channelDescription );
331        }
332
333        changed.sort( new PageTimeComparator() );
334
335        int items = 0;
336        for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) {
337            final Page page = i.next();
338            final Entry e = new Entry();
339            e.setPage( page );
340            final String url;
341
342            if( page instanceof Attachment ) {
343                url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(),null );
344            } else {
345                url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(),null );
346            }
347
348            e.setURL( url );
349
350            //  Title
351            String pageText = m_engine.getManager( PageManager.class ).getPureText( page.getName(), WikiProvider.LATEST_VERSION );
352
353            String title = "";
354            final int firstLine = pageText.indexOf('\n');
355
356            if( firstLine > 0 ) {
357                title = pageText.substring( 0, firstLine ).trim();
358            }
359
360            if( title.length() == 0 ) {
361                title = page.getName();
362            }
363
364            // Remove wiki formatting
365            while( title.startsWith("!") ) {
366                title = title.substring(1);
367            }
368
369            e.setTitle( title );
370
371            //  Description
372            if( firstLine > 0 ) {
373                int maxlen = pageText.length();
374                if( maxlen > MAX_CHARACTERS ) {
375                    maxlen = MAX_CHARACTERS;
376                }
377                pageText = m_engine.getManager( RenderingManager.class ).textToHTML( wikiContext, pageText.substring( firstLine + 1, maxlen ).trim() );
378                if( maxlen == MAX_CHARACTERS ) {
379                    pageText += "...";
380                }
381                e.setContent( pageText );
382            } else {
383                e.setContent( title );
384            }
385            e.setAuthor( getAuthor(page) );
386            feed.addEntry( e );
387        }
388
389        return feed.getString();
390    }
391
392}