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