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.WikiContext;
040import org.apache.wiki.WikiEngine;
041import org.apache.wiki.WikiPage;
042import org.apache.wiki.WikiProvider;
043import org.apache.wiki.api.exceptions.PluginException;
044import org.apache.wiki.api.exceptions.ProviderException;
045import org.apache.wiki.api.plugin.ParserStagePlugin;
046import org.apache.wiki.api.plugin.WikiPlugin;
047import org.apache.wiki.auth.AuthorizationManager;
048import org.apache.wiki.auth.permissions.PagePermission;
049import org.apache.wiki.pages.PageManager;
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    public String execute( WikiContext context, Map<String, String> params )
163        throws PluginException
164    {
165        Calendar   startTime;
166        Calendar   stopTime;
167        int        numDays = DEFAULT_DAYS;
168        WikiEngine engine = context.getEngine();
169        AuthorizationManager mgr = engine.getAuthorizationManager();
170
171        //
172        //  Parse parameters.
173        //
174        String  days;
175        DateFormat entryFormat;
176        String  startDay = null;
177        boolean hasComments = false;
178        int     maxEntries;
179        String  weblogName;
180
181        if( (weblogName = params.get(PARAM_PAGE)) == null )
182        {
183            weblogName = context.getPage().getName();
184        }
185
186        if( (days = context.getHttpParameter( "weblog."+PARAM_DAYS )) == null )
187        {
188            days = params.get( PARAM_DAYS );
189        }
190
191        if( ( params.get(PARAM_ENTRYFORMAT)) == null )
192        {
193            entryFormat = Preferences.getDateFormat( context, TimeFormat.DATETIME );
194        }
195        else
196        {
197            entryFormat = new SimpleDateFormat( params.get(PARAM_ENTRYFORMAT) );
198        }
199
200        if( days != null )
201        {
202            if( days.equalsIgnoreCase("all") )
203            {
204                numDays = Integer.MAX_VALUE;
205            }
206            else
207            {
208                numDays = TextUtil.parseIntParameter( days, DEFAULT_DAYS );
209            }
210        }
211
212
213        if( (startDay = params.get(PARAM_STARTDATE)) == null )
214        {
215            startDay = context.getHttpParameter( "weblog."+PARAM_STARTDATE );
216        }
217
218        if( TextUtil.isPositive( params.get(PARAM_ALLOWCOMMENTS) ) )
219        {
220            hasComments = true;
221        }
222
223        maxEntries = TextUtil.parseIntParameter( params.get(PARAM_MAXENTRIES),
224                                                 Integer.MAX_VALUE );
225
226        //
227        //  Determine the date range which to include.
228        //
229
230        startTime = Calendar.getInstance();
231        stopTime  = Calendar.getInstance();
232
233        if( startDay != null )
234        {
235            SimpleDateFormat fmt = new SimpleDateFormat( DEFAULT_DATEFORMAT );
236            try
237            {
238                Date d = fmt.parse( startDay );
239                startTime.setTime( d );
240                stopTime.setTime( d );
241            }
242            catch( ParseException e )
243            {
244                return "Illegal time format: "+startDay;
245            }
246        }
247
248        //
249        //  Mark this to be a weblog
250        //
251
252        context.getPage().setAttribute(ATTR_ISWEBLOG, "true");
253
254        //
255        //  We make a wild guess here that nobody can do millisecond
256        //  accuracy here.
257        //
258        startTime.add( Calendar.DAY_OF_MONTH, -numDays );
259        startTime.set( Calendar.HOUR, 0 );
260        startTime.set( Calendar.MINUTE, 0 );
261        startTime.set( Calendar.SECOND, 0 );
262        stopTime.set( Calendar.HOUR, 23 );
263        stopTime.set( Calendar.MINUTE, 59 );
264        stopTime.set( Calendar.SECOND, 59 );
265
266        StringBuilder sb = new StringBuilder();
267
268        try
269        {
270            List<WikiPage> blogEntries = findBlogEntries( engine,
271                                                          weblogName,
272                                                          startTime.getTime(),
273                                                          stopTime.getTime() );
274
275            Collections.sort( blogEntries, new PageDateComparator() );
276
277            sb.append("<div class=\"weblog\">\n");
278
279            for( Iterator< WikiPage > i = blogEntries.iterator(); i.hasNext() && maxEntries-- > 0 ; )
280            {
281                WikiPage p = i.next();
282
283                if( mgr.checkPermission( context.getWikiSession(),
284                                         new PagePermission(p, PagePermission.VIEW_ACTION) ) )
285                {
286                    addEntryHTML(context, entryFormat, hasComments, sb, p);
287                }
288            }
289
290            sb.append("</div>\n");
291        }
292        catch( ProviderException e )
293        {
294            log.error( "Could not locate blog entries", e );
295            throw new PluginException( "Could not locate blog entries: "+e.getMessage() );
296        }
297
298        return sb.toString();
299    }
300
301    /**
302     *  Generates HTML for an entry.
303     *
304     *  @param context
305     *  @param entryFormat
306     *  @param hasComments  True, if comments are enabled.
307     *  @param buffer       The buffer to which we add.
308     *  @param entry
309     *  @throws ProviderException
310     */
311    private void addEntryHTML(WikiContext context, DateFormat entryFormat, boolean hasComments, StringBuilder buffer, WikiPage entry)
312        throws ProviderException
313    {
314        WikiEngine engine = context.getEngine();
315        ResourceBundle rb = Preferences.getBundle(context, WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE);
316
317        buffer.append("<div class=\"weblogentry\">\n");
318
319        //
320        //  Heading
321        //
322        buffer.append("<div class=\"weblogentryheading\">\n");
323
324        Date entryDate = entry.getLastModified();
325        buffer.append( entryFormat.format(entryDate) );
326
327        buffer.append("</div>\n");
328
329        //
330        //  Append the text of the latest version.  Reset the
331        //  context to that page.
332        //
333
334        WikiContext entryCtx = (WikiContext) context.clone();
335        entryCtx.setPage( entry );
336
337        String html = engine.getHTML( entryCtx, engine.getPage(entry.getName()) );
338
339        // Extract the first h1/h2/h3 as title, and replace with null
340        buffer.append("<div class=\"weblogentrytitle\">\n");
341        Matcher matcher = HEADINGPATTERN.matcher( html );
342        if ( matcher.find() )
343        {
344            String title = matcher.group(2);
345            html = matcher.replaceFirst("");
346            buffer.append( title );
347        }
348        else
349        {
350            buffer.append( entry.getName() );
351        }
352        buffer.append("</div>\n");
353
354        buffer.append("<div class=\"weblogentrybody\">\n");
355        buffer.append( html );
356        buffer.append("</div>\n");
357
358        //
359        //  Append footer
360        //
361        buffer.append("<div class=\"weblogentryfooter\">\n");
362
363        String author = entry.getAuthor();
364
365        if( author != null )
366        {
367            if( engine.pageExists(author) )
368            {
369                author = "<a href=\""+entryCtx.getURL( WikiContext.VIEW, author )+"\">"+engine.beautifyTitle(author)+"</a>";
370            }
371        }
372        else
373        {
374            author = "AnonymousCoward";
375        }
376
377        buffer.append( MessageFormat.format( rb.getString("weblogentryplugin.postedby"), author));
378        buffer.append( "<a href=\""+entryCtx.getURL(WikiContext.VIEW, entry.getName())+"\">"+rb.getString("weblogentryplugin.permalink")+"</a>" );
379        String commentPageName = TextUtil.replaceString( entry.getName(),
380                                                         "blogentry",
381                                                         "comments" );
382
383        if( hasComments )
384        {
385            int numComments = guessNumberOfComments( engine, commentPageName );
386
387            //
388            //  We add the number of comments to the URL so that
389            //  the user's browsers would realize that the page
390            //  has changed.
391            //
392            buffer.append( "&nbsp;&nbsp;" );
393
394            String addcomment = rb.getString("weblogentryplugin.addcomment");
395
396            buffer.append( "<a href=\""+
397                       entryCtx.getURL(WikiContext.COMMENT,
398                                       commentPageName,
399                                       "nc="+numComments)+
400                       "\">"+
401                       MessageFormat.format(addcomment, numComments)
402                       +"</a>" );
403        }
404
405        buffer.append("</div>\n");
406
407        //
408        //  Done, close
409        //
410        buffer.append("</div>\n");
411    }
412
413    private int guessNumberOfComments( WikiEngine engine, String commentpage )
414        throws ProviderException
415    {
416        String pagedata = engine.getPureText( commentpage, WikiProvider.LATEST_VERSION );
417
418        if( pagedata == null || pagedata.trim().length() == 0 )
419        {
420            return 0;
421        }
422
423        return TextUtil.countSections( pagedata );
424    }
425
426    /**
427     *  Attempts to locate all pages that correspond to the
428     *  blog entry pattern.  Will only consider the days on the dates; not the hours and minutes.
429     *
430     *  @param engine WikiEngine which is used to get the pages
431     *  @param baseName The basename (e.g. "Main" if you want "Main_blogentry_xxxx")
432     *  @param start The date which is the first to be considered
433     *  @param end   The end date which is the last to be considered
434     *  @return a list of pages with their FIRST revisions.
435     *  @throws ProviderException If something goes wrong
436     */
437    public List< WikiPage > findBlogEntries( WikiEngine engine, String baseName, Date start, Date end )
438        throws ProviderException
439    {
440        PageManager mgr = engine.getPageManager();
441        Set< String > allPages = engine.getReferenceManager().findCreated();
442
443        ArrayList<WikiPage> result = new ArrayList<WikiPage>();
444
445        baseName = makeEntryPage( baseName );
446
447        for( Iterator< String > i = allPages.iterator(); i.hasNext(); )
448        {
449            String pageName = i.next();
450
451            if( pageName.startsWith( baseName ) )
452            {
453                try
454                {
455                    WikiPage firstVersion = mgr.getPageInfo( pageName, 1 );
456                    Date d = firstVersion.getLastModified();
457
458                    if( d.after(start) && d.before(end) )
459                    {
460                        result.add( firstVersion );
461                    }
462                }
463                catch( Exception e )
464                {
465                    log.debug("Page name :"+pageName+" was suspected as a blog entry but it isn't because of parsing errors",e);
466                }
467            }
468        }
469
470        return result;
471    }
472
473    /**
474     *  Reverse comparison.
475     */
476    private static class PageDateComparator implements Comparator<WikiPage>
477    {
478        public int compare( WikiPage page1, WikiPage page2 )
479        {
480            if( page1 == null || page2 == null )
481            {
482                return 0;
483            }
484
485            return page2.getLastModified().compareTo( page1.getLastModified() );
486        }
487    }
488
489    /**
490     *  Mark us as being a real weblog.
491     *  {@inheritDoc}
492     */
493    public void executeParser(PluginContent element, WikiContext context, Map<String, String> params)
494    {
495        context.getPage().setAttribute( ATTR_ISWEBLOG, "true" );
496    }
497}