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