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