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