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 = 0;
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                        if( log.isDebugEnabled() ) {
289                            log.debug( "lastModified Date of page " + pageName + " : " + m_dateLastModified );
290                        }
291                        if( lastModPage.after( m_dateLastModified ) ) {
292                            m_dateLastModified = lastModPage;
293                        }
294                    }
295
296                }
297            }
298        }
299
300        return result;
301    }
302
303    /**
304     *  Filters and sorts a collection according to the include and exclude parameters.
305     *
306     *  @param c The collection to filter.
307     *  @return A filtered and sorted collection.
308     */
309    protected List< String > filterAndSortCollection( final Collection< String > c ) {
310        final List< String > result = filterCollection( c );
311        result.sort( m_sorter );
312        return result;
313    }
314
315    /**
316     *  Makes WikiText from a Collection.
317     *
318     *  @param links Collection to make into WikiText.
319     *  @param separator Separator string to use.
320     *  @param numItems How many items to show.
321     *  @return The WikiText
322     */
323    protected String wikitizeCollection( final Collection< String > links, final String separator, final int numItems ) {
324        if( links == null || links.isEmpty() ) {
325            return "";
326        }
327
328        final StringBuilder output = new StringBuilder();
329
330        final Iterator< String > it = links.iterator();
331        int count = 0;
332
333        //  The output will be B Item[1] A S B Item[2] A S B Item[3] A
334        while( it.hasNext() && ( (count < numItems) || ( numItems == ALL_ITEMS ) ) ) {
335            final String value = it.next();
336            if( count > 0 ) {
337                output.append( m_after );
338                output.append( separator );
339            }
340
341            output.append( m_before );
342
343            // Make a Wiki markup link. See TranslatorReader.
344            output.append( "[" )
345                  .append( m_engine.getManager( RenderingManager.class ).beautifyTitle( value ) )
346                  .append( "|" )
347                  .append( value )
348                  .append( "]" );
349            count++;
350        }
351
352        //  Output final item - if there have been none, no "after" is printed
353        if( count > 0 ) {
354            output.append( m_after );
355        }
356
357        return output.toString();
358    }
359
360    /**
361     *  Makes HTML with common parameters.
362     *
363     *  @param context The WikiContext
364     *  @param wikitext The wikitext to render
365     *  @return HTML
366     *  @since 1.6.4
367     */
368    protected String makeHTML( final Context context, final String wikitext ) {
369        String result = "";
370
371        final RenderingManager mgr = m_engine.getManager( RenderingManager.class );
372
373        try {
374            final MarkupParser parser = mgr.getParser( context, wikitext );
375            parser.addLinkTransmutator( new CutMutator( m_maxwidth ) );
376            parser.enableImageInlining( false );
377
378            final WikiDocument doc = parser.parse();
379            result = mgr.getHTML( context, doc );
380        } catch( final IOException e ) {
381            log.error("Failed to convert page data to HTML", e);
382        }
383
384        return result;
385    }
386
387    protected String applyColumnsStyle( final String result ) {
388        if( items > 1 ) {
389            return "<div style=\"columns:" + items + ";" +
390                                "-moz-columns:" + items + ";" +
391                                "-webkit-columns:" + items + ";" + "\">"
392                    + result + "</div>";
393        }
394        return result;
395    }
396
397    /**
398     *  A simple class that just cuts a String to a maximum
399     *  length, adding three dots after the cutpoint.
400     */
401    private static class CutMutator implements StringTransmutator {
402
403        private final int m_length;
404
405        public CutMutator( final int length ) {
406            m_length = length;
407        }
408
409        @Override
410        public String mutate( final Context context, final String text ) {
411            if( text.length() > m_length ) {
412                return text.substring( 0, m_length ) + "...";
413            }
414
415            return text;
416        }
417    }
418
419    /**
420     * Helper method to initialize the comparator for this page.
421     */
422    private void initSorter( final Context context, final Map< String, String > params ) {
423        final String order = params.get( PARAM_SORTORDER );
424        if( order == null || order.isEmpty() ) {
425            // Use the configured comparator
426            m_sorter = context.getEngine().getManager( PageManager.class ).getPageSorter();
427        } else if( order.equalsIgnoreCase( PARAM_SORTORDER_JAVA ) ) {
428            // use Java "natural" ordering
429            m_sorter = new PageSorter( JavaNaturalComparator.DEFAULT_JAVA_COMPARATOR );
430        } else if( order.equalsIgnoreCase( PARAM_SORTORDER_LOCALE ) ) {
431            // use this locale's ordering
432            m_sorter = new PageSorter( LocaleComparator.DEFAULT_LOCALE_COMPARATOR );
433        } else if( order.equalsIgnoreCase( PARAM_SORTORDER_HUMAN ) ) {
434            // use human ordering
435            m_sorter = new PageSorter( HumanComparator.DEFAULT_HUMAN_COMPARATOR );
436        } else {
437            try {
438                final Collator collator = new RuleBasedCollator( order );
439                collator.setStrength( Collator.PRIMARY );
440                m_sorter = new PageSorter( new CollatorComparator( collator ) );
441            } catch( final ParseException pe ) {
442                log.info( "Failed to parse requested collator - using default ordering", pe );
443                m_sorter = context.getEngine().getManager( PageManager.class ).getPageSorter();
444            }
445        }
446    }
447
448}