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}