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