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