    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.logging.log4j.LogManager;
    022import org.apache.logging.log4j.Logger;
    023import org.apache.oro.text.regex.MalformedPatternException;
    024import org.apache.oro.text.regex.Pattern;
    025import org.apache.oro.text.regex.PatternCompiler;
    026import org.apache.oro.text.regex.PatternMatcher;
    027import org.apache.oro.text.regex.Perl5Compiler;
    028import org.apache.oro.text.regex.Perl5Matcher;
    029import org.apache.wiki.api.core.Context;
    030import org.apache.wiki.api.core.ContextEnum;
    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.references.ReferenceManager;
    037import org.apache.wiki.util.TextUtil;
    038
    039import java.util.ArrayList;
    040import java.util.Collection;
    041import java.util.HashSet;
    042import java.util.Locale;
    043import java.util.Map;
    044import java.util.ResourceBundle;
    045
    046
    047/**
    048 *  Displays the pages referring to the current page.
    049 *
    050 *  <p>Parameters</p>
    051 *  <ul>
    052 *    <li><b>name</b> - Name of the root page. Default name of calling page
    053 *    <li><b>type</b> - local|externalattachment
    054 *    <li><b>depth</b> - How many levels of pages to be parsed.
    055 *    <li><b>include</b> - Include only these pages. (eg. include='UC.*|BP.*' )
    056 *    <li><b>exclude</b> - Exclude with this pattern. (eg. exclude='LeftMenu' )
    057 *    <li><b>format</b> -  full|compact, FULL now expands all levels correctly
    058 *  </ul>
    059 *
    060 */
    061public class ReferredPagesPlugin implements Plugin {
    062
    063    private static final Logger LOG = LogManager.getLogger( ReferredPagesPlugin.class );
    064    private Engine m_engine;
    065    private int m_depth;
    066    private final HashSet< String > m_exists  = new HashSet<>();
    067    private final StringBuffer m_result  = new StringBuffer( 1024 );
    068    private final PatternMatcher m_matcher = new Perl5Matcher();
    069    private Pattern m_includePattern;
    070    private Pattern m_excludePattern;
    071    private int items;
    072    private boolean m_formatCompact = true;
    073    private boolean m_formatSort;
    074
    075    /** The parameter name for the root page to start from.  Value is <tt>{@value}</tt>. */
    076    public static final String PARAM_ROOT = "page";
    077
    078    /** The parameter name for the depth.  Value is <tt>{@value}</tt>. */
    079    public static final String PARAM_DEPTH = "depth";
    080
    081    /** The parameter name for the type of the references.  Value is <tt>{@value}</tt>. */
    082    public static final String PARAM_TYPE = "type";
    083
    084    /** The parameter name for the included pages.  Value is <tt>{@value}</tt>. */
    085    public static final String PARAM_INCLUDE = "include";
    086
    087    /** The parameter name for the excluded pages.  Value is <tt>{@value}</tt>. */
    088    public static final String PARAM_EXCLUDE = "exclude";
    089
    090    /** The parameter name for the format.  Value is <tt>{@value}</tt>. */
    091    public static final String PARAM_FORMAT = "format";
    092
    093    /** 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. */
    094    public static final String PARAM_COLUMNS = "columns";
    095
    096    /** The minimum depth. Value is <tt>{@value}</tt>. */
    097    public static final int MIN_DEPTH = 1;
    098
    099    /** The maximum depth. Value is <tt>{@value}</tt>. */
    100    public static final int MAX_DEPTH = 8;
    101
    102    @Override
    103    public String getDisplayName(Locale locale) {
    104        final ResourceBundle rb = ResourceBundle.getBundle(PluginManager.PLUGIN_I18N_RESOURCE, locale);
    105        return rb.getString(this.getClass().getSimpleName());
    106    }
    107
    108    @Override
    109    public String getSnipExample() {
    110        return "ReferredPagesPlugin page='{pagename}' type='local|external|attachment' depth='1..8' include='regexp' exclude='regexp'";
    111    }
    112
    113    /**
    114     *  {@inheritDoc}
    115     */
    116    @Override
    117    public String execute( final Context context, final Map< String, String > params ) throws PluginException {
    118        m_engine = context.getEngine();
    119        final Page page = context.getPage();
    120        if( page == null ) {
    121            return "";
    122        }
    123
    124        // parse parameters
    125        String rootname = params.get( PARAM_ROOT );
    126        if( rootname == null ) {
    127            rootname = page.getName() ;
    128        }
    129
    130        String format = params.get( PARAM_FORMAT );
    131        if( format == null) {
    132            format = "";
    133        }
    134        if( format.contains( "full" ) ) {
    135            m_formatCompact = false ;
    136        }
    137        if( format.contains( "sort" ) ) {
    138            m_formatSort = true  ;
    139        }
    140
    141        m_depth = TextUtil.parseIntParameter( params.get( PARAM_DEPTH ), MIN_DEPTH );
    142        if( m_depth > MAX_DEPTH ) {
    143            m_depth = MAX_DEPTH;
    144        }
    145
    146        String includePattern = params.get(PARAM_INCLUDE);
    147        if( includePattern == null ) {
    148            includePattern = ".*";
    149        }
    150
    151        String excludePattern = params.get(PARAM_EXCLUDE);
    152        if( excludePattern == null ) {
    153            excludePattern = "^$";
    154        }
    155
    156        final String columns = params.get( PARAM_COLUMNS );
    157        if( columns != null ) {
    158            items = TextUtil.parseIntParameter( columns, 0 );
    159        }
    160
    161        LOG.debug( "Fetching referred pages for "+ rootname +
    162                   " with a depth of "+ m_depth +
    163                   " with include pattern of "+ includePattern +
    164                   " with exclude pattern of "+ excludePattern +
    165                   " with " + columns + " items" );
    166
    167        //
    168        // do the actual work
    169        //
    170        final String href  = context.getViewURL( rootname );
    171        final String title = "ReferredPagesPlugin: depth[" + m_depth +
    172                             "] include[" + includePattern + "] exclude[" + excludePattern +
    173                             "] format[" + ( m_formatCompact ? "compact" : "full" ) +
    174                             ( m_formatSort ? " sort" : "" ) + "]";
    175
    176        if( items > 1 ) {
    177            m_result.append( "<div class=\"ReferredPagesPlugin\" style=\"" )
    178                    .append( "columns:" ).append( columns ).append( ";" )
    179                    .append( "moz-columns:" ).append( columns ).append( ";" )
    180                    .append( "webkit-columns:" ).append( columns ).append( ";" )
    181                    .append( "\">\n" );
    182        } else {
    183            m_result.append( "<div class=\"ReferredPagesPlugin\">\n" );
    184        }
    185        m_result.append( "<a class=\"wikipage\" href=\"" )
    186                .append( href ).append( "\" title=\"" )
    187                .append( TextUtil.replaceEntities( title ) )
    188                .append( "\">" )
    189                .append( TextUtil.replaceEntities( rootname ) )
    190                .append( "</a>\n" );
    191        m_exists.add( rootname );
    192
    193        // pre compile all needed patterns
    194        // glob compiler :  * is 0..n instance of any char  -- more convenient as input
    195        // perl5 compiler : .* is 0..n instances of any char -- more powerful
    196        //PatternCompiler g_compiler = new GlobCompiler();
    197        final PatternCompiler compiler = new Perl5Compiler();
    198
    199        try {
    200            m_includePattern = compiler.compile( includePattern );
    201            m_excludePattern = compiler.compile( excludePattern );
    202        } catch( final MalformedPatternException e ) {
    203            if( m_includePattern == null ) {
    204                throw new PluginException( "Illegal include pattern detected." );
    205            } else if( m_excludePattern == null ) {
    206                throw new PluginException( "Illegal exclude pattern detected." );
    207            } else {
    208                throw new PluginException( "Illegal internal pattern detected." );
    209            }
    210        }
    211
    212        // go get all referred links
    213        getReferredPages(context,rootname, 0);
    214
    215        // close and finish
    216        m_result.append ("</div>\n" ) ;
    217
    218        return m_result.toString() ;
    219    }
    220
    221    /**
    222     * Retrieves a list of all referred pages. Is called recursively depending on the depth parameter.
    223     */
    224    private void getReferredPages( final Context context, final String pagename, int depth ) {
    225        if( depth >= m_depth ) {
    226            return;  // end of recursion
    227        }
    228        if( pagename == null ) {
    229            return;
    230        }
    231        if( !m_engine.getManager( PageManager.class ).wikiPageExists(pagename) ) {
    232            return;
    233        }
    234
    235        final ReferenceManager mgr = m_engine.getManager( ReferenceManager.class );
    236        final Collection< String > allPages = mgr.findRefersTo( pagename );
    237        handleLinks( context, allPages, ++depth, pagename );
    238    }
    239
    240    private void handleLinks( final Context context, final Collection<String> links, final int depth, final String pagename ) {
    241        boolean isUL = false;
    242        final HashSet< String > localLinkSet = new HashSet<>();  // needed to skip multiple links to the same page
    243        localLinkSet.add( pagename );
    244
    245        final ArrayList< String > allLinks = new ArrayList<>();
    246
    247        if( links != null )
    248            allLinks.addAll( links );
    249
    250        if( m_formatSort ) context.getEngine().getManager( PageManager.class ).getPageSorter().sort( allLinks );
    251
    252        for( final String link : allLinks ) {
    253            if( localLinkSet.contains( link ) ) {
    254                continue; // skip multiple links to the same page
    255            }
    256            localLinkSet.add( link );
    257
    258            if( !m_engine.getManager( PageManager.class ).wikiPageExists( link ) ) {
    259                continue; // hide links to non-existing pages
    260            }
    261            if(  m_matcher.matches( link , m_excludePattern ) ) {
    262                continue;
    263            }
    264            if( !m_matcher.matches( link , m_includePattern ) ) {
    265                continue;
    266            }
    267
    268            if( m_exists.contains( link ) ) {
    269                if( !m_formatCompact ) {
    270                    if( !isUL ) {
    271                        isUL = true;
    272                        m_result.append("<ul>\n");
    273                    }
    274
    275                    //See https://www.w3.org/wiki/HTML_lists  for proper nesting of UL and LI
    276                    m_result.append( "<li> " ).append( TextUtil.replaceEntities( link ) ).append( "\n" );
    277                    getReferredPages( context, link, depth );  // added recursive call - on general request
    278                    m_result.append( "\n</li>\n" );
    279                }
    280            } else {
    281                if( !isUL ) {
    282                    isUL = true;
    283                    m_result.append("<ul>\n");
    284                }
    285
    286                final String href = context.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), link );
    287                m_result.append( "<li><a class=\"wikipage\" href=\"" ).append( href ).append( "\">" ).append( TextUtil.replaceEntities( link ) ).append( "</a>\n" );
    288                m_exists.add( link );
    289                getReferredPages( context, link, depth );
    290                m_result.append( "\n</li>\n" );
    291            }
    292        }
    293
    294        if( isUL ) {
    295            m_result.append("</ul>\n");
    296        }
    297    }
    298
    299}