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