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