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