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 }