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}