    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}