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