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.plugin;
020
021import java.text.DateFormat;
022import java.text.MessageFormat;
023import java.text.ParseException;
024import java.text.SimpleDateFormat;
025import java.util.ArrayList;
026import java.util.Calendar;
027import java.util.Collections;
028import java.util.Comparator;
029import java.util.Date;
030import java.util.Iterator;
031import java.util.List;
032import java.util.Map;
033import java.util.ResourceBundle;
034import java.util.Set;
035import java.util.regex.Matcher;
036import java.util.regex.Pattern;
037
038import org.apache.log4j.Logger;
039import org.apache.wiki.PageManager;
040import org.apache.wiki.WikiContext;
041import org.apache.wiki.WikiEngine;
042import org.apache.wiki.WikiPage;
043import org.apache.wiki.WikiProvider;
044import org.apache.wiki.api.exceptions.PluginException;
045import org.apache.wiki.api.exceptions.ProviderException;
046import org.apache.wiki.api.plugin.ParserStagePlugin;
047import org.apache.wiki.api.plugin.WikiPlugin;
048import org.apache.wiki.auth.AuthorizationManager;
049import org.apache.wiki.auth.permissions.PagePermission;
050import org.apache.wiki.parser.PluginContent;
051import org.apache.wiki.preferences.Preferences;
052import org.apache.wiki.preferences.Preferences.TimeFormat;
053import org.apache.wiki.util.TextUtil;
054
055/**
056 *  <p>Builds a simple weblog.
057 *  The pageformat can use the following params:</p>
058 *  <p>%p - Page name</p>
059 *  <p>Parameters:</p>
060 *  <ul>
061 *    <li><b>page</b> - which page is used to do the blog; default is the current page.</li>
062 *    <li><b>entryFormat</b> - how to display the date on pages, using the J2SE SimpleDateFormat
063 *       syntax. Defaults to the current locale's DateFormat.LONG format
064 *       for the date, and current locale's DateFormat.SHORT for the time.
065 *       Thus, for the US locale this will print dates similar to
066 *       this: September 4, 2005 11:54 PM</li>
067 *    <li><b>days</b> - how many days the weblog aggregator should show.  If set to
068 *      "all", shows all pages.</li>
069 *    <li><b>pageformat</b> - What the entry pages should look like.</li>
070 *    <li><b>startDate</b> - Date when to start.  Format is "ddMMyy."</li>
071 *    <li><b>maxEntries</b> - How many entries to show at most.</li>
072 *  </ul>
073 *  <p>The "days" and "startDate" can also be sent in HTTP parameters,
074 *  and the names are "weblog.days" and "weblog.startDate", respectively.</p>
075 *  <p>The weblog plugin also adds an attribute to each page it is on:
076 *  "weblogplugin.isweblog" is set to "true".  This can be used to quickly
077 *  peruse pages which have weblogs.</p>
078 *  @since 1.9.21
079 */
080
081// FIXME: Add "entries" param as an alternative to "days".
082// FIXME: Entries arrive in wrong order.
083
084public class WeblogPlugin
085    implements WikiPlugin, ParserStagePlugin
086{
087    private static Logger     log = Logger.getLogger(WeblogPlugin.class);
088    private static final Pattern HEADINGPATTERN;
089
090    /** How many days are considered by default.  Default value is {@value} */
091    private static final int     DEFAULT_DAYS = 7;
092    private static final String  DEFAULT_PAGEFORMAT = "%p_blogentry_";
093
094    /** The default date format used in the blog entry page names. */
095    public static final String   DEFAULT_DATEFORMAT = "ddMMyy";
096
097    /** Parameter name for the startDate.  Value is <tt>{@value}</tt>. */
098    public static final String  PARAM_STARTDATE    = "startDate";
099    /** Parameter name for the entryFormat.  Value is <tt>{@value}</tt>. */
100    public static final String  PARAM_ENTRYFORMAT  = "entryFormat";
101    /** Parameter name for the days.  Value is <tt>{@value}</tt>. */
102    public static final String  PARAM_DAYS         = "days";
103    /** Parameter name for the allowComments.  Value is <tt>{@value}</tt>. */
104    public static final String  PARAM_ALLOWCOMMENTS = "allowComments";
105    /** Parameter name for the maxEntries.  Value is <tt>{@value}</tt>. */
106    public static final String  PARAM_MAXENTRIES   = "maxEntries";
107    /** Parameter name for the page.  Value is <tt>{@value}</tt>. */
108    public static final String  PARAM_PAGE         = "page";
109
110    /** The attribute which is stashed to the WikiPage attributes to check if a page
111     *  is a weblog or not. You may check for its presence.
112     */
113    public static final String  ATTR_ISWEBLOG      = "weblogplugin.isweblog";
114
115    static
116    {
117        // This is a pretty ugly, brute-force regex. But it will do for now...
118        HEADINGPATTERN = Pattern.compile("(<h[1-4][^>]*>)(.*)(</h[1-4]>)", Pattern.CASE_INSENSITIVE);
119    }
120
121    /**
122     *  Create an entry name based on the blogname, a date, and an entry number.
123     *
124     *  @param pageName Name of the blog
125     *  @param date The date (in ddMMyy format)
126     *  @param entryNum The entry number.
127     *  @return A formatted page name.
128     */
129    public static String makeEntryPage( String pageName,
130                                        String date,
131                                        String entryNum )
132    {
133        return TextUtil.replaceString(DEFAULT_PAGEFORMAT,"%p",pageName)+date+"_"+entryNum;
134    }
135
136    /**
137     *  Return just the basename for entires without date and entry numebr.
138     *
139     *  @param pageName The name of the blog.
140     *  @return A formatted name.
141     */
142    public static String makeEntryPage( String pageName )
143    {
144        return TextUtil.replaceString(DEFAULT_PAGEFORMAT,"%p",pageName);
145    }
146
147    /**
148     *  Returns the entry page without the entry number.
149     *
150     *  @param pageName Blog name.
151     *  @param date The date.
152     *  @return A base name for the blog entries.
153     */
154    public static String makeEntryPage( String pageName, String date )
155    {
156        return TextUtil.replaceString(DEFAULT_PAGEFORMAT,"%p",pageName)+date;
157    }
158
159    /**
160     *  {@inheritDoc}
161     */
162    @SuppressWarnings("unchecked")
163    public String execute( WikiContext context, Map<String, String> params )
164        throws PluginException
165    {
166        Calendar   startTime;
167        Calendar   stopTime;
168        int        numDays = DEFAULT_DAYS;
169        WikiEngine engine = context.getEngine();
170        AuthorizationManager mgr = engine.getAuthorizationManager();
171
172        //
173        //  Parse parameters.
174        //
175        String  days;
176        DateFormat entryFormat;
177        String  startDay = null;
178        boolean hasComments = false;
179        int     maxEntries;
180        String  weblogName;
181
182        if( (weblogName = params.get(PARAM_PAGE)) == null )
183        {
184            weblogName = context.getPage().getName();
185        }
186
187        if( (days = context.getHttpParameter( "weblog."+PARAM_DAYS )) == null )
188        {
189            days = params.get( PARAM_DAYS );
190        }
191
192        if( ( params.get(PARAM_ENTRYFORMAT)) == null )
193        {
194            entryFormat = Preferences.getDateFormat( context, TimeFormat.DATETIME );
195        }
196        else
197        {
198            entryFormat = new SimpleDateFormat( params.get(PARAM_ENTRYFORMAT) );
199        }
200
201        if( days != null )
202        {
203            if( days.equalsIgnoreCase("all") )
204            {
205                numDays = Integer.MAX_VALUE;
206            }
207            else
208            {
209                numDays = TextUtil.parseIntParameter( days, DEFAULT_DAYS );
210            }
211        }
212
213
214        if( (startDay = params.get(PARAM_STARTDATE)) == null )
215        {
216            startDay = context.getHttpParameter( "weblog."+PARAM_STARTDATE );
217        }
218
219        if( TextUtil.isPositive( params.get(PARAM_ALLOWCOMMENTS) ) )
220        {
221            hasComments = true;
222        }
223
224        maxEntries = TextUtil.parseIntParameter( params.get(PARAM_MAXENTRIES),
225                                                 Integer.MAX_VALUE );
226
227        //
228        //  Determine the date range which to include.
229        //
230
231        startTime = Calendar.getInstance();
232        stopTime  = Calendar.getInstance();
233
234        if( startDay != null )
235        {
236            SimpleDateFormat fmt = new SimpleDateFormat( DEFAULT_DATEFORMAT );
237            try
238            {
239                Date d = fmt.parse( startDay );
240                startTime.setTime( d );
241                stopTime.setTime( d );
242            }
243            catch( ParseException e )
244            {
245                return "Illegal time format: "+startDay;
246            }
247        }
248
249        //
250        //  Mark this to be a weblog
251        //
252
253        context.getPage().setAttribute(ATTR_ISWEBLOG, "true");
254
255        //
256        //  We make a wild guess here that nobody can do millisecond
257        //  accuracy here.
258        //
259        startTime.add( Calendar.DAY_OF_MONTH, -numDays );
260        startTime.set( Calendar.HOUR, 0 );
261        startTime.set( Calendar.MINUTE, 0 );
262        startTime.set( Calendar.SECOND, 0 );
263        stopTime.set( Calendar.HOUR, 23 );
264        stopTime.set( Calendar.MINUTE, 59 );
265        stopTime.set( Calendar.SECOND, 59 );
266
267        StringBuilder sb = new StringBuilder();
268
269        try
270        {
271            List<WikiPage> blogEntries = findBlogEntries( engine,
272                                                          weblogName,
273                                                          startTime.getTime(),
274                                                          stopTime.getTime() );
275
276            Collections.sort( blogEntries, new PageDateComparator() );
277
278            sb.append("<div class=\"weblog\">\n");
279
280            for( Iterator< WikiPage > i = blogEntries.iterator(); i.hasNext() && maxEntries-- > 0 ; )
281            {
282                WikiPage p = i.next();
283
284                if( mgr.checkPermission( context.getWikiSession(),
285                                         new PagePermission(p, PagePermission.VIEW_ACTION) ) )
286                {
287                    addEntryHTML(context, entryFormat, hasComments, sb, p);
288                }
289            }
290
291            sb.append("</div>\n");
292        }
293        catch( ProviderException e )
294        {
295            log.error( "Could not locate blog entries", e );
296            throw new PluginException( "Could not locate blog entries: "+e.getMessage() );
297        }
298
299        return sb.toString();
300    }
301
302    /**
303     *  Generates HTML for an entry.
304     *
305     *  @param context
306     *  @param entryFormat
307     *  @param hasComments  True, if comments are enabled.
308     *  @param buffer       The buffer to which we add.
309     *  @param entry
310     *  @throws ProviderException
311     */
312    private void addEntryHTML(WikiContext context, DateFormat entryFormat, boolean hasComments, StringBuilder buffer, WikiPage entry)
313        throws ProviderException
314    {
315        WikiEngine engine = context.getEngine();
316        ResourceBundle rb = Preferences.getBundle(context, WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE);
317
318        buffer.append("<div class=\"weblogentry\">\n");
319
320        //
321        //  Heading
322        //
323        buffer.append("<div class=\"weblogentryheading\">\n");
324
325        Date entryDate = entry.getLastModified();
326        buffer.append( entryFormat.format(entryDate) );
327
328        buffer.append("</div>\n");
329
330        //
331        //  Append the text of the latest version.  Reset the
332        //  context to that page.
333        //
334
335        WikiContext entryCtx = (WikiContext) context.clone();
336        entryCtx.setPage( entry );
337
338        String html = engine.getHTML( entryCtx, engine.getPage(entry.getName()) );
339
340        // Extract the first h1/h2/h3 as title, and replace with null
341        buffer.append("<div class=\"weblogentrytitle\">\n");
342        Matcher matcher = HEADINGPATTERN.matcher( html );
343        if ( matcher.find() )
344        {
345            String title = matcher.group(2);
346            html = matcher.replaceFirst("");
347            buffer.append( title );
348        }
349        else
350        {
351            buffer.append( entry.getName() );
352        }
353        buffer.append("</div>\n");
354
355        buffer.append("<div class=\"weblogentrybody\">\n");
356        buffer.append( html );
357        buffer.append("</div>\n");
358
359        //
360        //  Append footer
361        //
362        buffer.append("<div class=\"weblogentryfooter\">\n");
363
364        String author = entry.getAuthor();
365
366        if( author != null )
367        {
368            if( engine.pageExists(author) )
369            {
370                author = "<a href=\""+entryCtx.getURL( WikiContext.VIEW, author )+"\">"+engine.beautifyTitle(author)+"</a>";
371            }
372        }
373        else
374        {
375            author = "AnonymousCoward";
376        }
377
378        buffer.append( MessageFormat.format( rb.getString("weblogentryplugin.postedby"), author));
379        buffer.append( "<a href=\""+entryCtx.getURL(WikiContext.VIEW, entry.getName())+"\">"+rb.getString("weblogentryplugin.permalink")+"</a>" );
380        String commentPageName = TextUtil.replaceString( entry.getName(),
381                                                         "blogentry",
382                                                         "comments" );
383
384        if( hasComments )
385        {
386            int numComments = guessNumberOfComments( engine, commentPageName );
387
388            //
389            //  We add the number of comments to the URL so that
390            //  the user's browsers would realize that the page
391            //  has changed.
392            //
393            buffer.append( "&nbsp;&nbsp;" );
394
395            String addcomment = rb.getString("weblogentryplugin.addcomment");
396
397            buffer.append( "<a href=\""+
398                       entryCtx.getURL(WikiContext.COMMENT,
399                                       commentPageName,
400                                       "nc="+numComments)+
401                       "\">"+
402                       MessageFormat.format(addcomment, numComments)
403                       +"</a>" );
404        }
405
406        buffer.append("</div>\n");
407
408        //
409        //  Done, close
410        //
411        buffer.append("</div>\n");
412    }
413
414    private int guessNumberOfComments( WikiEngine engine, String commentpage )
415        throws ProviderException
416    {
417        String pagedata = engine.getPureText( commentpage, WikiProvider.LATEST_VERSION );
418
419        if( pagedata == null || pagedata.trim().length() == 0 )
420        {
421            return 0;
422        }
423
424        return TextUtil.countSections( pagedata );
425    }
426
427    /**
428     *  Attempts to locate all pages that correspond to the
429     *  blog entry pattern.  Will only consider the days on the dates; not the hours and minutes.
430     *
431     *  @param engine WikiEngine which is used to get the pages
432     *  @param baseName The basename (e.g. "Main" if you want "Main_blogentry_xxxx")
433     *  @param start The date which is the first to be considered
434     *  @param end   The end date which is the last to be considered
435     *  @return a list of pages with their FIRST revisions.
436     *  @throws ProviderException If something goes wrong
437     */
438    public List findBlogEntries( WikiEngine engine,
439                                 String baseName, Date start, Date end )
440        throws ProviderException
441    {
442        PageManager mgr = engine.getPageManager();
443        Set allPages = engine.getReferenceManager().findCreated();
444
445        ArrayList<WikiPage> result = new ArrayList<WikiPage>();
446
447        baseName = makeEntryPage( baseName );
448
449        for( Iterator i = allPages.iterator(); i.hasNext(); )
450        {
451            String pageName = (String)i.next();
452
453            if( pageName.startsWith( baseName ) )
454            {
455                try
456                {
457                    WikiPage firstVersion = mgr.getPageInfo( pageName, 1 );
458                    Date d = firstVersion.getLastModified();
459
460                    if( d.after(start) && d.before(end) )
461                    {
462                        result.add( firstVersion );
463                    }
464                }
465                catch( Exception e )
466                {
467                    log.debug("Page name :"+pageName+" was suspected as a blog entry but it isn't because of parsing errors",e);
468                }
469            }
470        }
471
472        return result;
473    }
474
475    /**
476     *  Reverse comparison.
477     */
478    private static class PageDateComparator implements Comparator<WikiPage>
479    {
480        public int compare( WikiPage page1, WikiPage page2 )
481        {
482            if( page1 == null || page2 == null )
483            {
484                return 0;
485            }
486
487            return page2.getLastModified().compareTo( page1.getLastModified() );
488        }
489    }
490
491    /**
492     *  Mark us as being a real weblog.
493     *  {@inheritDoc}
494     */
495    public void executeParser(PluginContent element, WikiContext context, Map<String, String> params)
496    {
497        context.getPage().setAttribute( ATTR_ISWEBLOG, "true" );
498    }
499}