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