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     */
019    package org.apache.wiki.plugin;
020    
021    import java.io.IOException;
022    import java.text.Collator;
023    import java.text.ParseException;
024    import java.text.RuleBasedCollator;
025    import java.text.SimpleDateFormat;
026    import java.util.ArrayList;
027    import java.util.Collection;
028    import java.util.Date;
029    import java.util.Iterator;
030    import java.util.Map;
031    
032    import org.apache.commons.lang.StringUtils;
033    import org.apache.log4j.Logger;
034    import org.apache.oro.text.GlobCompiler;
035    import org.apache.oro.text.regex.MalformedPatternException;
036    import org.apache.oro.text.regex.Pattern;
037    import org.apache.oro.text.regex.PatternCompiler;
038    import org.apache.oro.text.regex.PatternMatcher;
039    import org.apache.oro.text.regex.Perl5Matcher;
040    import org.apache.wiki.PageSorter;
041    import org.apache.wiki.StringTransmutator;
042    import org.apache.wiki.WikiContext;
043    import org.apache.wiki.WikiEngine;
044    import org.apache.wiki.WikiPage;
045    import org.apache.wiki.api.exceptions.PluginException;
046    import org.apache.wiki.api.plugin.WikiPlugin;
047    import org.apache.wiki.parser.MarkupParser;
048    import org.apache.wiki.parser.WikiDocument;
049    import org.apache.wiki.preferences.Preferences;
050    import org.apache.wiki.preferences.Preferences.TimeFormat;
051    import org.apache.wiki.render.RenderingManager;
052    import org.apache.wiki.util.TextUtil;
053    import org.apache.wiki.util.comparators.CollatorComparator;
054    import org.apache.wiki.util.comparators.HumanComparator;
055    import org.apache.wiki.util.comparators.JavaNaturalComparator;
056    import 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     */
077    public 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            StringBuffer output = new StringBuffer();
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    }