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