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