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