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.io.IOException; 022import java.text.Collator; 023import java.text.ParseException; 024import java.text.RuleBasedCollator; 025import java.text.SimpleDateFormat; 026import java.util.ArrayList; 027import java.util.Collection; 028import java.util.Date; 029import java.util.Iterator; 030import java.util.Map; 031 032import org.apache.commons.lang.StringUtils; 033import org.apache.log4j.Logger; 034import org.apache.oro.text.GlobCompiler; 035import org.apache.oro.text.regex.MalformedPatternException; 036import org.apache.oro.text.regex.Pattern; 037import org.apache.oro.text.regex.PatternCompiler; 038import org.apache.oro.text.regex.PatternMatcher; 039import org.apache.oro.text.regex.Perl5Matcher; 040import org.apache.wiki.PageSorter; 041import org.apache.wiki.StringTransmutator; 042import org.apache.wiki.WikiContext; 043import org.apache.wiki.WikiEngine; 044import org.apache.wiki.WikiPage; 045import org.apache.wiki.api.exceptions.PluginException; 046import org.apache.wiki.api.plugin.WikiPlugin; 047import org.apache.wiki.parser.MarkupParser; 048import org.apache.wiki.parser.WikiDocument; 049import org.apache.wiki.preferences.Preferences; 050import org.apache.wiki.preferences.Preferences.TimeFormat; 051import org.apache.wiki.render.RenderingManager; 052import org.apache.wiki.util.TextUtil; 053import org.apache.wiki.util.comparators.CollatorComparator; 054import org.apache.wiki.util.comparators.HumanComparator; 055import org.apache.wiki.util.comparators.JavaNaturalComparator; 056import org.apache.wiki.util.comparators.LocaleComparator; 057 058/** 059 * This is a base class for all plugins using referral things. 060 * 061 * <p>Parameters (also valid for all subclasses of this class) : </p> 062 * <ul> 063 * <li><b>maxwidth</b> - maximum width of generated links</li> 064 * <li><b>separator</b> - separator between generated links (wikitext)</li> 065 * <li><b>after</b> - output after the link</li> 066 * <li><b>before</b> - output before the link</li> 067 * <li><b>exclude</b> - a regular expression of pages to exclude from the list. </li> 068 * <li><b>include</b> - a regular expression of pages to include in the list. </li> 069 * <li><b>show</b> - value is either "pages" (default) or "count". When "count" is specified, shows only the count 070 * of pages which match. (since 2.8)</li> 071 * <li><b>showLastModified</b> - When show=count, shows also the last modified date. (since 2.8)</li> 072 * <li><b>sortOrder</b> - specifies the sort order for the resulting list. Options are 073 * 'human', 'java', 'locale' or a <code>RuleBasedCollator</code> rule string. (since 2.8.3)</li> 074 * </ul> 075 * 076 */ 077public abstract class AbstractReferralPlugin implements WikiPlugin 078{ 079 private static Logger log = Logger.getLogger( AbstractReferralPlugin.class ); 080 081 /** Magic value for rendering all items. */ 082 public static final int ALL_ITEMS = -1; 083 084 /** Parameter name for setting the maximum width. Value is <tt>{@value}</tt>. */ 085 public static final String PARAM_MAXWIDTH = "maxwidth"; 086 087 /** Parameter name for the separator string. Value is <tt>{@value}</tt>. */ 088 public static final String PARAM_SEPARATOR = "separator"; 089 090 /** Parameter name for the output after the link. Value is <tt>{@value}</tt>. */ 091 public static final String PARAM_AFTER = "after"; 092 093 /** Parameter name for the output before the link. Value is <tt>{@value}</tt>. */ 094 public static final String PARAM_BEFORE = "before"; 095 096 /** Parameter name for setting the list of excluded patterns. Value is <tt>{@value}</tt>. */ 097 public static final String PARAM_EXCLUDE = "exclude"; 098 099 /** Parameter name for setting the list of included patterns. Value is <tt>{@value}</tt>. */ 100 public static final String PARAM_INCLUDE = "include"; 101 102 /** Parameter name for the show parameter. Value is <tt>{@value}</tt>. */ 103 public static final String PARAM_SHOW = "show"; 104 105 /** Parameter name for setting show to "pages". Value is <tt>{@value}</tt>. */ 106 public static final String PARAM_SHOW_VALUE_PAGES = "pages"; 107 108 /** Parameter name for setting show to "count". Value is <tt>{@value}</tt>. */ 109 public static final String PARAM_SHOW_VALUE_COUNT = "count"; 110 111 /** Parameter name for showing the last modification count. Value is <tt>{@value}</tt>. */ 112 public static final String PARAM_LASTMODIFIED = "showLastModified"; 113 114 /** Parameter name for specifying the sort order. Value is <tt>{@value}</tt>. */ 115 protected static final String PARAM_SORTORDER = "sortOrder"; 116 protected static final String PARAM_SORTORDER_HUMAN = "human"; 117 protected static final String PARAM_SORTORDER_JAVA = "java"; 118 protected static final String PARAM_SORTORDER_LOCALE = "locale"; 119 120 protected int m_maxwidth = Integer.MAX_VALUE; 121 protected String m_before = ""; // null not blank 122 protected String m_separator = ""; // null not blank 123 protected String m_after = "\\\\"; 124 125 protected Pattern[] m_exclude; 126 protected Pattern[] m_include; 127 protected PageSorter m_sorter; 128 129 protected String m_show = "pages"; 130 protected boolean m_lastModified=false; 131 // the last modified date of the page that has been last modified: 132 protected Date m_dateLastModified = new Date(0); 133 protected SimpleDateFormat m_dateFormat; 134 135 protected WikiEngine m_engine; 136 137 /** 138 * @param context the wiki context 139 * @param params parameters for initializing the plugin 140 * @throws PluginException if any of the plugin parameters are malformed 141 */ 142 // FIXME: The compiled pattern strings should really be cached somehow. 143 public void initialize( WikiContext context, Map<String, String> params ) 144 throws PluginException 145 { 146 m_dateFormat = Preferences.getDateFormat( context, TimeFormat.DATETIME ); 147 m_engine = context.getEngine(); 148 m_maxwidth = TextUtil.parseIntParameter( params.get( PARAM_MAXWIDTH ), Integer.MAX_VALUE ); 149 if( m_maxwidth < 0 ) m_maxwidth = 0; 150 151 String s = params.get( PARAM_SEPARATOR ); 152 153 if( s != null ) 154 { 155 m_separator = s; 156 // pre-2.1.145 there was a separator at the end of the list 157 // if they set the parameters, we use the new format of 158 // before Item1 after separator before Item2 after separator before Item3 after 159 m_after = ""; 160 } 161 162 s = params.get( PARAM_BEFORE ); 163 164 if( s != null ) 165 { 166 m_before = s; 167 } 168 169 s = params.get( PARAM_AFTER ); 170 171 if( s != null ) 172 { 173 m_after = s; 174 } 175 176 s = params.get( PARAM_EXCLUDE ); 177 178 if( s != null ) 179 { 180 try 181 { 182 PatternCompiler pc = new GlobCompiler(); 183 184 String[] ptrns = StringUtils.split( s, "," ); 185 186 m_exclude = new Pattern[ptrns.length]; 187 188 for( int i = 0; i < ptrns.length; i++ ) 189 { 190 m_exclude[i] = pc.compile( ptrns[i] ); 191 } 192 } 193 catch( MalformedPatternException e ) 194 { 195 throw new PluginException("Exclude-parameter has a malformed pattern: "+e.getMessage()); 196 } 197 } 198 199 // TODO: Cut-n-paste, refactor 200 s = params.get( PARAM_INCLUDE ); 201 202 if( s != null ) 203 { 204 try 205 { 206 PatternCompiler pc = new GlobCompiler(); 207 208 String[] ptrns = StringUtils.split( s, "," ); 209 210 m_include = new Pattern[ptrns.length]; 211 212 for( int i = 0; i < ptrns.length; i++ ) 213 { 214 m_include[i] = pc.compile( ptrns[i] ); 215 } 216 } 217 catch( MalformedPatternException e ) 218 { 219 throw new PluginException("Include-parameter has a malformed pattern: "+e.getMessage()); 220 } 221 } 222 223 // log.debug( "Requested maximum width is "+m_maxwidth ); 224 s = params.get(PARAM_SHOW); 225 226 if( s != null ) 227 { 228 if( s.equalsIgnoreCase( "count" ) ) 229 { 230 m_show = "count"; 231 } 232 } 233 234 s = params.get( PARAM_LASTMODIFIED ); 235 236 if( s != null ) 237 { 238 if( s.equalsIgnoreCase( "true" ) ) 239 { 240 if( m_show.equals( "count" ) ) 241 { 242 m_lastModified = true; 243 } 244 else 245 { 246 throw new PluginException( "showLastModified=true is only valid if show=count is also specified" ); 247 } 248 } 249 } 250 251 initSorter( context, params ); 252 } 253 254 /** 255 * Filters a collection according to the include and exclude parameters. 256 * 257 * @param c The collection to filter. 258 * @return A filtered collection. 259 */ 260 protected Collection filterCollection( Collection c ) 261 { 262 ArrayList<Object> result = new ArrayList<Object>(); 263 264 PatternMatcher pm = new Perl5Matcher(); 265 266 for( Iterator i = c.iterator(); i.hasNext(); ) 267 { 268 String pageName = null; 269 Object objectje = i.next(); 270 if( objectje instanceof WikiPage ) 271 { 272 pageName = ((WikiPage) objectje).getName(); 273 } 274 else 275 { 276 pageName = (String) objectje; 277 } 278 279 // 280 // If include parameter exists, then by default we include only those 281 // pages in it (excluding the ones in the exclude pattern list). 282 // 283 // include='*' means the same as no include. 284 // 285 boolean includeThis = m_include == null; 286 287 if( m_include != null ) 288 { 289 for( int j = 0; j < m_include.length; j++ ) 290 { 291 if( pm.matches( pageName, m_include[j] ) ) 292 { 293 includeThis = true; 294 break; 295 } 296 } 297 } 298 299 if( m_exclude != null ) 300 { 301 for( int j = 0; j < m_exclude.length; j++ ) 302 { 303 if( pm.matches( pageName, m_exclude[j] ) ) 304 { 305 includeThis = false; 306 break; // The inner loop, continue on the next item 307 } 308 } 309 } 310 311 if( includeThis ) 312 { 313 if( objectje instanceof WikiPage ) 314 { 315 result.add( objectje ); 316 } 317 else 318 { 319 result.add( pageName ); 320 } 321 // 322 // if we want to show the last modified date of the most recently change page, we keep a "high watermark" here: 323 WikiPage page = null; 324 if( m_lastModified ) 325 { 326 page = m_engine.getPage( pageName ); 327 if( page != null ) 328 { 329 Date lastModPage = page.getLastModified(); 330 if( log.isDebugEnabled() ) 331 { 332 log.debug( "lastModified Date of page " + pageName + " : " + m_dateLastModified ); 333 } 334 if( lastModPage.after( m_dateLastModified ) ) 335 { 336 m_dateLastModified = lastModPage; 337 } 338 } 339 340 } 341 } 342 } 343 344 return result; 345 } 346 347 /** 348 * Filters and sorts a collection according to the include and exclude parameters. 349 * 350 * @param c The collection to filter. 351 * @return A filtered and sorted collection. 352 */ 353 @SuppressWarnings( "unchecked" ) 354 protected Collection filterAndSortCollection( Collection c ) 355 { 356 ArrayList<Object> result = (ArrayList<Object>)filterCollection( c ); 357 m_sorter.sortPages( result ); 358 return result; 359 } 360 361 /** 362 * Makes WikiText from a Collection. 363 * 364 * @param links Collection to make into WikiText. 365 * @param separator Separator string to use. 366 * @param numItems How many items to show. 367 * @return The WikiText 368 */ 369 protected String wikitizeCollection( Collection links, String separator, int numItems ) 370 { 371 if( links == null || links.isEmpty() ) 372 return ""; 373 374 StringBuilder output = new StringBuilder(); 375 376 Iterator it = links.iterator(); 377 int count = 0; 378 379 // 380 // The output will be B Item[1] A S B Item[2] A S B Item[3] A 381 // 382 while( it.hasNext() && ( (count < numItems) || ( numItems == ALL_ITEMS ) ) ) 383 { 384 String value = (String)it.next(); 385 386 if( count > 0 ) 387 { 388 output.append( m_after ); 389 output.append( m_separator ); 390 } 391 392 output.append( m_before ); 393 394 // Make a Wiki markup link. See TranslatorReader. 395 output.append( "[" + m_engine.beautifyTitle(value) + "|" + value + "]" ); 396 count++; 397 } 398 399 // 400 // Output final item - if there have been none, no "after" is printed 401 // 402 if( count > 0 ) output.append( m_after ); 403 404 return output.toString(); 405 } 406 407 /** 408 * Makes HTML with common parameters. 409 * 410 * @param context The WikiContext 411 * @param wikitext The wikitext to render 412 * @return HTML 413 * @since 1.6.4 414 */ 415 protected String makeHTML( WikiContext context, String wikitext ) 416 { 417 String result = ""; 418 419 RenderingManager mgr = m_engine.getRenderingManager(); 420 421 try 422 { 423 MarkupParser parser = mgr.getParser(context, wikitext); 424 425 parser.addLinkTransmutator( new CutMutator(m_maxwidth) ); 426 parser.enableImageInlining( false ); 427 428 WikiDocument doc = parser.parse(); 429 430 result = mgr.getHTML( context, doc ); 431 } 432 catch( IOException e ) 433 { 434 log.error("Failed to convert page data to HTML", e); 435 } 436 437 return result; 438 } 439 440 /** 441 * A simple class that just cuts a String to a maximum 442 * length, adding three dots after the cutpoint. 443 */ 444 private static class CutMutator implements StringTransmutator 445 { 446 private int m_length; 447 448 public CutMutator( int length ) 449 { 450 m_length = length; 451 } 452 453 public String mutate( WikiContext context, String text ) 454 { 455 if( text.length() > m_length ) 456 { 457 return text.substring( 0, m_length ) + "..."; 458 } 459 460 return text; 461 } 462 } 463 464 /** 465 * Helper method to initialize the comparator for this page. 466 */ 467 private void initSorter( WikiContext context, Map<String, String> params ) 468 { 469 String order = params.get( PARAM_SORTORDER ); 470 if( order == null || order.length() == 0 ) 471 { 472 // Use the configured comparator 473 m_sorter = context.getEngine().getPageSorter(); 474 } 475 else if( order.equalsIgnoreCase( PARAM_SORTORDER_JAVA ) ) 476 { 477 // use Java "natural" ordering 478 m_sorter = new PageSorter( JavaNaturalComparator.DEFAULT_JAVA_COMPARATOR ); 479 } 480 else if( order.equalsIgnoreCase( PARAM_SORTORDER_LOCALE ) ) 481 { 482 // use this locale's ordering 483 m_sorter = new PageSorter( LocaleComparator.DEFAULT_LOCALE_COMPARATOR ); 484 } 485 else if( order.equalsIgnoreCase( PARAM_SORTORDER_HUMAN ) ) 486 { 487 // use human ordering 488 m_sorter = new PageSorter( HumanComparator.DEFAULT_HUMAN_COMPARATOR ); 489 } 490 else 491 try 492 { 493 Collator collator = new RuleBasedCollator( order ); 494 collator.setStrength( Collator.PRIMARY ); 495 m_sorter = new PageSorter( new CollatorComparator( collator ) ); 496 } 497 catch( ParseException pe ) 498 { 499 log.info( "Failed to parse requested collator - using default ordering", pe ); 500 m_sorter = context.getEngine().getPageSorter(); 501 } 502 } 503}