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+" ");
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( " " );
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 }