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}