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