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