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
020package org.apache.wiki.plugin;
021
022import org.apache.commons.lang3.ClassUtils;
023import org.apache.commons.lang3.StringUtils;
024import org.apache.logging.log4j.LogManager;
025import org.apache.logging.log4j.Logger;
026import org.apache.oro.text.regex.MalformedPatternException;
027import org.apache.oro.text.regex.MatchResult;
028import org.apache.oro.text.regex.Pattern;
029import org.apache.oro.text.regex.PatternCompiler;
030import org.apache.oro.text.regex.PatternMatcher;
031import org.apache.oro.text.regex.Perl5Compiler;
032import org.apache.oro.text.regex.Perl5Matcher;
033import org.apache.wiki.InternalWikiException;
034import org.apache.wiki.ajax.WikiAjaxDispatcherServlet;
035import org.apache.wiki.ajax.WikiAjaxServlet;
036import org.apache.wiki.api.core.Context;
037import org.apache.wiki.api.core.Engine;
038import org.apache.wiki.api.exceptions.PluginException;
039import org.apache.wiki.api.plugin.InitializablePlugin;
040import org.apache.wiki.api.plugin.Plugin;
041import org.apache.wiki.modules.BaseModuleManager;
042import org.apache.wiki.modules.WikiModuleInfo;
043import org.apache.wiki.preferences.Preferences;
044import org.apache.wiki.util.ClassUtil;
045import org.apache.wiki.util.FileUtil;
046import org.apache.wiki.util.TextUtil;
047import org.apache.wiki.util.XHTML;
048import org.apache.wiki.util.XhtmlUtil;
049import org.apache.wiki.util.XmlUtil;
050import org.jdom2.Element;
051
052import jakarta.servlet.http.HttpServlet;
053import java.io.IOException;
054import java.io.PrintWriter;
055import java.io.StreamTokenizer;
056import java.io.StringReader;
057import java.io.StringWriter;
058import java.text.MessageFormat;
059import java.util.ArrayList;
060import java.util.Collection;
061import java.util.HashMap;
062import java.util.HashSet;
063import java.util.List;
064import java.util.Map;
065import java.util.NoSuchElementException;
066import java.util.Properties;
067import java.util.ResourceBundle;
068import java.util.Set;
069import java.util.StringTokenizer;
070
071/**
072 *  Manages plugin classes.  There exists a single instance of PluginManager
073 *  per each instance of Engine, that is, each JSPWiki instance.
074 *  <P>
075 *  A plugin is defined to have three parts:
076 *  <OL>
077 *    <li>The plugin class
078 *    <li>The plugin parameters
079 *    <li>The plugin body
080 *  </ol>
081 *
082 *  For example, in the following line of code:
083 *  <pre>
084 *  [{INSERT org.apache.wiki.plugin.FunnyPlugin  foo='bar'
085 *  blob='goo'
086 *
087 *  abcdefghijklmnopqrstuvw
088 *  01234567890}]
089 *  </pre>
090 *
091 *  The plugin class is "org.apache.wiki.plugin.FunnyPlugin", the
092 *  parameters are "foo" and "blob" (having values "bar" and "goo",
093 *  respectively), and the plugin body is then
094 *  "abcdefghijklmnopqrstuvw\n01234567890".   The plugin body is
095 *  accessible via a special parameter called "_body".
096 *  <p>
097 *  If the parameter "debug" is set to "true" for the plugin,
098 *  JSPWiki will output debugging information directly to the page if there
099 *  is an exception.
100 *  <P>
101 *  The class name can be shortened, and marked without the package.
102 *  For example, "FunnyPlugin" would be expanded to
103 *  "org.apache.wiki.plugin.FunnyPlugin" automatically.  It is also
104 *  possible to define other packages, by setting the
105 *  "jspwiki.plugin.searchPath" property.  See the included
106 *  jspwiki.properties file for examples.
107 *  <P>
108 *  Even though the nominal way of writing the plugin is
109 *  <pre>
110 *  [{INSERT pluginclass WHERE param1=value1...}],
111 *  </pre>
112 *  it is possible to shorten this quite a lot, by skipping the
113 *  INSERT, and WHERE words, and dropping the package name.  For
114 *  example:
115 *
116 *  <pre>
117 *  [{INSERT org.apache.wiki.plugin.Counter WHERE name='foo'}]
118 *  </pre>
119 *
120 *  is the same as
121 *  <pre>
122 *  [{Counter name='foo'}]
123 *  </pre>
124 *  <h3>Plugin property files</h3>
125 *  <p>
126 *  Since 2.3.25 you can also define a generic plugin XML properties file per
127 *  each JAR file.
128 *  <pre>
129 *  <modules>
130 *   <plugin class="org.apache.wiki.foo.TestPlugin">
131 *       <author>Janne Jalkanen</author>
132 *       <script>foo.js</script>
133 *       <stylesheet>foo.css</stylesheet>
134 *       <alias>code</alias>
135 *   </plugin>
136 *   <plugin class="org.apache.wiki.foo.TestPlugin2">
137 *       <author>Janne Jalkanen</author>
138 *   </plugin>
139 *   </modules>
140 *  </pre>
141 *  <h3>Plugin lifecycle</h3>
142 *
143 *  <p>Plugin can implement multiple interfaces to let JSPWiki know at which stages they should
144 *  be invoked:
145 *  <ul>
146 *  <li>InitializablePlugin: If your plugin implements this interface, the initialize()-method is
147 *      called once for this class
148 *      before any actual execute() methods are called.  You should use the initialize() for e.g.
149 *      precalculating things.  But notice that this method is really called only once during the
150 *      entire Engine lifetime.  The InitializablePlugin is available from 2.5.30 onwards.</li>
151 *  <li>ParserStagePlugin: If you implement this interface, the executeParse() method is called
152 *      when JSPWiki is forming the DOM tree.  You will receive an incomplete DOM tree, as well
153 *      as the regular parameters.  However, since JSPWiki caches the DOM tree to speed up later
154 *      places, which means that whatever this method returns would be irrelevant.  You can do some DOM
155 *      tree manipulation, though.  The ParserStagePlugin is available from 2.5.30 onwards.</li>
156 *  <li>Plugin: The regular kind of plugin which is executed at every rendering stage.  Each
157 *      new page load is guaranteed to invoke the plugin, unlike with the ParserStagePlugins.</li>
158 *  </ul>
159 *
160 *  @since 1.6.1
161 */
162public class DefaultPluginManager extends BaseModuleManager implements PluginManager {
163
164    private static final String PLUGIN_INSERT_PATTERN = "\\{?(INSERT)?\\s*([\\w\\._]+)[ \\t]*(WHERE)?[ \\t]*";
165    private static final Logger LOG = LogManager.getLogger( DefaultPluginManager.class );
166    private static final String DEFAULT_FORMS_PACKAGE = "org.apache.wiki.forms";
167
168    private final ArrayList< String > m_searchPath = new ArrayList<>();
169    private final ArrayList< String > m_externalJars = new ArrayList<>();
170    private final Pattern m_pluginPattern;
171    private boolean m_pluginsEnabled = true;
172
173    /** Keeps a list of all known plugin classes. */
174    private final Map< String, WikiPluginInfo > m_pluginClassMap = new HashMap<>();
175
176    /**
177     *  Create a new PluginManager.
178     *
179     *  @param engine Engine which owns this manager.
180     *  @param props Contents of a "jspwiki.properties" file.
181     */
182    public DefaultPluginManager( final Engine engine, final Properties props ) {
183        super( engine );
184        final String packageNames = props.getProperty( Engine.PROP_SEARCHPATH );
185        if ( packageNames != null ) {
186            final StringTokenizer tok = new StringTokenizer( packageNames, "," );
187            while( tok.hasMoreTokens() ) {
188                m_searchPath.add( tok.nextToken().trim() );
189            }
190        }
191
192        final String externalJars = props.getProperty( PROP_EXTERNALJARS );
193        if( externalJars != null ) {
194            final StringTokenizer tok = new StringTokenizer( externalJars, "," );
195            while( tok.hasMoreTokens() ) {
196                m_externalJars.add( tok.nextToken().trim() );
197            }
198        }
199
200        registerPlugins();
201
202        //  The default packages are always added.
203        m_searchPath.add( DEFAULT_PACKAGE );
204        m_searchPath.add( DEFAULT_FORMS_PACKAGE );
205
206        final PatternCompiler compiler = new Perl5Compiler();
207        try {
208            m_pluginPattern = compiler.compile( PLUGIN_INSERT_PATTERN );
209        } catch( final MalformedPatternException e ) {
210            LOG.fatal( "Internal error: someone messed with pluginmanager patterns.", e );
211            throw new InternalWikiException( "PluginManager patterns are broken" , e );
212        }
213    }
214
215    /** {@inheritDoc} */
216    @Override
217    public void enablePlugins( final boolean enabled ) {
218        m_pluginsEnabled = enabled;
219    }
220
221    /** {@inheritDoc} */
222    @Override
223    public boolean pluginsEnabled() {
224        return m_pluginsEnabled;
225    }
226
227    /** {@inheritDoc} */
228    @Override
229    public Pattern getPluginPattern() {
230        return m_pluginPattern;
231    }
232
233    /**
234     *  Attempts to locate a plugin class from the class path set in the property file.
235     *
236     *  @param classname Either a fully fledged class name, or just the name of the file (that is, "org.apache.wiki.plugin.Counter" or just plain "Counter").
237     *  @return A found class.
238     *  @throws ClassNotFoundException if no such class exists.
239     */
240    private Class< ? > findPluginClass( final String classname ) throws ClassNotFoundException {
241        return ClassUtil.findClass( m_searchPath, m_externalJars, classname );
242    }
243
244    /** Outputs an HTML-formatted version of a stack trace. */
245    private String stackTrace( final Map<String,String> params, final Throwable t ) {
246        final Element div = XhtmlUtil.element( XHTML.div, "Plugin execution failed, stack trace follows:" );
247        div.setAttribute( XHTML.ATTR_class, "debug" );
248
249        final StringWriter out = new StringWriter();
250        t.printStackTrace( new PrintWriter( out ) );
251        div.addContent( XhtmlUtil.element( XHTML.pre, out.toString() ) );
252        div.addContent( XhtmlUtil.element( XHTML.b, "Parameters to the plugin" ) );
253
254        final Element list = XhtmlUtil.element( XHTML.ul );
255        for( final Map.Entry< String, String > e : params.entrySet() ) {
256            final String key = e.getKey();
257            list.addContent( XhtmlUtil.element( XHTML.li, key + "'='" + e.getValue() ) );
258        }
259        div.addContent( list );
260        return XhtmlUtil.serialize( div );
261    }
262
263    /** {@inheritDoc} */
264    @Override
265    public String execute( final Context context, final String classname, final Map< String, String > params ) throws PluginException {
266        if( !m_pluginsEnabled ) {
267            return "";
268        }
269
270        final ResourceBundle rb = Preferences.getBundle( context, Plugin.CORE_PLUGINS_RESOURCEBUNDLE );
271        //see JSPWIKI-75
272        final boolean debug = TextUtil.isPositive( params.get( PARAM_DEBUG ) ) && context.hasAdminPermissions();
273        
274        try {
275            //   Create...
276            final Plugin plugin = newWikiPlugin( classname, rb );
277            if( plugin == null ) {
278                return "Plugin '" + classname + "' not compatible with this version of JSPWiki";
279            }
280
281            //  ...and launch.
282            try {
283                return plugin.execute( context, params );
284            } catch( final PluginException e ) {
285                LOG.warn(e.getMessage(), e);
286                if( debug ) {
287                    return stackTrace( params, e );
288                }
289
290                // Just pass this exception onward.
291                throw ( PluginException )e.fillInStackTrace();
292            } catch( final Throwable t ) {
293                
294                // But all others get captured here.
295                LOG.warn( "Plugin failed while executing:", t );
296                if( debug ) {
297                    return stackTrace( params, t );
298                }
299
300                throw new PluginException( rb.getString( "plugin.error.failed" ), t );
301            }
302
303        } catch( final ClassCastException e ) {
304            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.notawikiplugin" ), classname ), e );
305        }
306    }
307
308    /** {@inheritDoc} */
309    @Override
310    public Map< String, String > parseArgs( final String argstring ) throws IOException {
311        final Map< String, String > arglist = new HashMap<>();
312        //  Protection against funny users.
313        if( argstring == null ) {
314            return arglist;
315        }
316
317        arglist.put( PARAM_CMDLINE, argstring );
318        final StringReader in = new StringReader( argstring );
319        final StreamTokenizer tok = new StreamTokenizer( in );
320        tok.eolIsSignificant( true );
321
322        String param = null;
323        String value;
324        boolean potentialEmptyLine = false;
325        boolean quit = false;
326        while( !quit ) {
327            final String s;
328            final int type = tok.nextToken();
329
330            switch( type ) {
331            case StreamTokenizer.TT_EOF:
332                quit = true;
333                s = null;
334                break;
335
336            case StreamTokenizer.TT_WORD:
337                s = tok.sval;
338                potentialEmptyLine = false;
339                break;
340
341            case StreamTokenizer.TT_EOL:
342                quit = potentialEmptyLine;
343                potentialEmptyLine = true;
344                s = null;
345                break;
346
347            case StreamTokenizer.TT_NUMBER:
348                s = Integer.toString( ( int )tok.nval );
349                potentialEmptyLine = false;
350                break;
351
352            case '\'':
353                s = tok.sval;
354                break;
355
356            default:
357                s = null;
358            }
359
360            //  Assume that alternate words on the line are parameter and value, respectively.
361            if( s != null ) {
362                if( param == null ) {
363                    param = s;
364                } else {
365                    value = s;
366                    arglist.put( param, value );
367                    param = null;
368                }
369            }
370        }
371
372        //  Now, we'll check the body.
373        if( potentialEmptyLine ) {
374            final StringWriter out = new StringWriter();
375            FileUtil.copyContents( in, out );
376            final String bodyContent = out.toString();
377            if( bodyContent != null ) {
378                arglist.put( PARAM_BODY, bodyContent );
379            }
380        }
381
382        return arglist;
383    }
384
385    /** {@inheritDoc} */
386    @Override
387    public String execute( final Context context, final String commandline ) throws PluginException {
388        if( !m_pluginsEnabled ) {
389            return "";
390        }
391
392        final ResourceBundle rb = Preferences.getBundle( context, Plugin.CORE_PLUGINS_RESOURCEBUNDLE );
393        final PatternMatcher matcher = new Perl5Matcher();
394
395        try {
396            if( matcher.contains( commandline, m_pluginPattern ) ) {
397                final MatchResult res = matcher.getMatch();
398                final String plugin = res.group( 2 );
399                final int endIndex = commandline.length() - ( commandline.charAt( commandline.length() - 1 ) == '}' ? 1 : 0 );
400                final String args = commandline.substring( res.endOffset( 0 ), endIndex );
401                final Map< String, String > arglist = parseArgs( args );
402                return execute( context, plugin, arglist );
403            }
404        } catch( final NoSuchElementException e ) {
405            final String msg =  "Missing parameter in plugin definition: " + commandline;
406            LOG.warn( msg, e );
407            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.missingparameter" ), commandline ) );
408        } catch( final IOException e ) {
409            final String msg = "Zyrf.  Problems with parsing arguments: " + commandline;
410            LOG.warn( msg, e );
411            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.parsingarguments" ), commandline ) );
412        }
413
414        // FIXME: We could either return an empty string "", or the original line.  If we want unsuccessful requests
415        // to be invisible, then we should return an empty string.
416        return commandline;
417    }
418
419    /** Register a plugin. */
420    private void registerPlugin( final WikiPluginInfo pluginClass ) {
421        String name;
422
423        // Register the plugin with the className without the package-part
424        name = pluginClass.getName();
425        if( name != null ) {
426            LOG.debug( "Registering plugin [name]: " + name );
427            m_pluginClassMap.put( name, pluginClass );
428        }
429
430        // Register the plugin with a short convenient name.
431        name = pluginClass.getAlias();
432        if( name != null ) {
433            LOG.debug( "Registering plugin [shortName]: " + name );
434            m_pluginClassMap.put( name, pluginClass );
435        }
436
437        // Register the plugin with the className with the package-part
438        name = pluginClass.getClassName();
439        if( name != null ) {
440            LOG.debug( "Registering plugin [className]: " + name );
441            m_pluginClassMap.put( name, pluginClass );
442        }
443
444        pluginClass.initializePlugin( pluginClass, m_engine, m_searchPath, m_externalJars );
445    }
446
447    private void registerPlugins() {
448        // Register all plugins which have created a resource containing its properties.
449        LOG.info( "Registering plugins" );
450        final List< Element > plugins = XmlUtil.parse( PLUGIN_RESOURCE_LOCATION, "/modules/plugin" );
451
452        // Get all resources of all plugins.
453        for( final Element pluginEl : plugins ) {
454            final String className = pluginEl.getAttributeValue( "class" );
455            final WikiPluginInfo pluginInfo = WikiPluginInfo.newInstance( className, pluginEl ,m_searchPath, m_externalJars );
456            if( pluginInfo != null ) {
457                registerPlugin( pluginInfo );
458            }
459        }
460    }
461
462    /**
463     *  Contains information about a bunch of plugins.
464     */
465    // FIXME: This class needs a better interface to return all sorts of possible information from the plugin XML.  In fact, it probably
466    //  should have some sort of a superclass system.
467    public static final class WikiPluginInfo extends WikiModuleInfo {
468
469        private String    m_className;
470        private String    m_alias;
471        private String    m_ajaxAlias;
472        private Class< Plugin >  m_clazz;
473
474        private boolean m_initialized;
475
476        /**
477         *  Creates a new plugin info object which can be used to access a plugin.
478         *
479         *  @param className Either a fully qualified class name, or a "short" name which is then checked against the internal list of plugin packages.
480         *  @param el A JDOM Element containing the information about this class.
481         *  @param searchPath A List of Strings, containing different package names.
482         *  @param externalJars the list of external jars to search
483         *  @return A WikiPluginInfo object.
484         */
485        static WikiPluginInfo newInstance( final String className, final Element el, final List<String> searchPath, final List<String> externalJars ) {
486            if( className == null || className.isEmpty() ) {
487                return null;
488            }
489
490            final WikiPluginInfo info = new WikiPluginInfo( className );
491            info.initializeFromXML( el );
492            return info;
493        }
494
495        /**
496         *  Initializes a plugin, if it has not yet been initialized. If the plugin extends {@link HttpServlet} it will automatically
497         *  register it as AJAX using {@link WikiAjaxDispatcherServlet#registerServlet(String, WikiAjaxServlet)}.
498         *
499         *  @param engine The Engine
500         *  @param searchPath A List of Strings, containing different package names.
501         *  @param externalJars the list of external jars to search
502         */
503        void initializePlugin( final WikiPluginInfo info, final Engine engine , final List<String> searchPath, final List<String> externalJars) {
504            if( !m_initialized ) {
505                // This makes sure we only try once per class, even if init fails.
506                m_initialized = true;
507
508                try {
509                    final Plugin p = newPluginInstance(searchPath, externalJars);
510                    if( p instanceof InitializablePlugin ) {
511                        ( ( InitializablePlugin )p ).initialize( engine );
512                    }
513                    if( p instanceof WikiAjaxServlet ) {
514                        WikiAjaxDispatcherServlet.registerServlet( (WikiAjaxServlet) p );
515                        final String ajaxAlias = info.getAjaxAlias();
516                        if (StringUtils.isNotBlank(ajaxAlias)) {
517                            WikiAjaxDispatcherServlet.registerServlet( info.getAjaxAlias(), (WikiAjaxServlet) p );
518                        }
519                    }
520                } catch( final Exception e ) {
521                    LOG.info( "Cannot initialize plugin " + m_className, e );
522                }
523            }
524        }
525
526        /**
527         *  {@inheritDoc}
528         */
529        @Override
530        protected void initializeFromXML( final Element el ) {
531            super.initializeFromXML( el );
532            m_alias = el.getChildText( "alias" );
533            m_ajaxAlias = el.getChildText( "ajaxAlias" );
534        }
535
536        /**
537         *  Create a new WikiPluginInfo based on the Class information.
538         *
539         *  @param clazz The class to check
540         *  @return A WikiPluginInfo instance
541         */
542        static WikiPluginInfo newInstance( final Class< ? > clazz ) {
543            return new WikiPluginInfo( clazz.getName() );
544        }
545
546        private WikiPluginInfo( final String className ) {
547            super( className );
548            setClassName( className );
549        }
550
551        private void setClassName( final String fullClassName ) {
552            m_name = ClassUtils.getShortClassName( fullClassName );
553            m_className = fullClassName;
554        }
555
556        /**
557         *  Returns the full class name of this object.
558         *  @return The full class name of the object.
559         */
560        public String getClassName() {
561            return m_className;
562        }
563
564        /**
565         *  Returns the alias name for this object.
566         *  @return An alias name for the plugin.
567         */
568        public String getAlias() {
569            return m_alias;
570        }
571
572        /**
573         *  Returns the ajax alias name for this object.
574         *  @return An ajax alias name for the plugin.
575         */
576        public String getAjaxAlias() {
577            return m_ajaxAlias;
578        }
579
580        /**
581         *  Creates a new plugin instance.
582         *
583         *  @param searchPath A List of Strings, containing different package names.
584         *  @param externalJars the list of external jars to search
585         *  @return A new plugin.
586         *  @throws ClassNotFoundException If the class declared was not found.
587         *  @throws InstantiationException If the class cannot be instantiated-
588         *  @throws IllegalAccessException If the class cannot be accessed.
589         */
590
591        public Plugin newPluginInstance( final List< String > searchPath, final List< String > externalJars) throws ReflectiveOperationException {
592            if( m_clazz == null ) {
593                m_clazz = ClassUtil.findClass( searchPath, externalJars ,m_className );
594            }
595
596            return ClassUtil.buildInstance( m_clazz );
597        }
598
599        /**
600         *  Returns a text for IncludeResources.
601         *
602         *  @param type Either "script" or "stylesheet"
603         *  @return Text, or an empty string, if there is nothing to be included.
604         */
605        public String getIncludeText( final String type ) {
606            try {
607                if( "script".equals( type ) ) {
608                    return getScriptText();
609                } else if( "stylesheet".equals( type ) ) {
610                    return getStylesheetText();
611                }
612            } catch( final Exception ex ) {
613                // We want to fail gracefully here
614                return ex.getMessage();
615            }
616
617            return null;
618        }
619
620        private String getScriptText() throws IOException {
621            if( m_scriptText != null ) {
622                return m_scriptText;
623            }
624
625            if( m_scriptLocation == null ) {
626                return "";
627            }
628
629            try {
630                m_scriptText = getTextResource(m_scriptLocation);
631            } catch( final IOException ex ) {
632                // Only throw this exception once!
633                m_scriptText = "";
634                throw ex;
635            }
636
637            return m_scriptText;
638        }
639
640        private String getStylesheetText() throws IOException {
641            if( m_stylesheetText != null ) {
642                return m_stylesheetText;
643            }
644
645            if( m_stylesheetLocation == null ) {
646                return "";
647            }
648
649            try {
650                m_stylesheetText = getTextResource(m_stylesheetLocation);
651            } catch( final IOException ex ) {
652                // Only throw this exception once!
653                m_stylesheetText = "";
654                throw ex;
655            }
656
657            return m_stylesheetText;
658        }
659
660        /**
661         *  Returns a string suitable for debugging.  Don't assume that the format would stay the same.
662         *
663         *  @return Something human-readable
664         */
665        @Override
666        public String toString() {
667            return "Plugin :[name=" + m_name + "][className=" + m_className + "]";
668        }
669
670    } // WikiPluginClass
671
672    /**
673     *  {@inheritDoc}
674     */
675    @Override
676    public Collection< WikiModuleInfo > modules() {
677        return modules( m_pluginClassMap.values().iterator() );
678    }
679
680    /**
681     *  {@inheritDoc}
682     */
683    @Override
684    public WikiPluginInfo getModuleInfo( final String moduleName) {
685        return m_pluginClassMap.get(moduleName);
686    }
687
688    /**
689     * Creates a {@link Plugin}.
690     *
691     * @param pluginName plugin's classname
692     * @param rb {@link ResourceBundle} with i18ned text for exceptions.
693     * @return a {@link Plugin}.
694     * @throws PluginException if there is a problem building the {@link Plugin}.
695     */
696    @Override
697    public Plugin newWikiPlugin( final String pluginName, final ResourceBundle rb ) throws PluginException {
698        Plugin plugin = null;
699        WikiPluginInfo pluginInfo = m_pluginClassMap.get( pluginName );
700        try {
701            if( pluginInfo == null ) {
702                pluginInfo = WikiPluginInfo.newInstance( findPluginClass( pluginName ) );
703                registerPlugin( pluginInfo );
704            }
705
706            if( !checkCompatibility( pluginInfo ) ) {
707                final String msg = "Plugin '" + pluginInfo.getName() + "' not compatible with this version of JSPWiki";
708                LOG.info( msg );
709            } else {
710                plugin = pluginInfo.newPluginInstance(m_searchPath, m_externalJars);
711            }
712        } catch( final ClassNotFoundException e ) {
713            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.couldnotfind" ), pluginName ), e );
714        } catch( final InstantiationException e ) {
715            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.cannotinstantiate" ), pluginName ), e );
716        } catch( final IllegalAccessException e ) {
717            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.notallowed" ), pluginName ), e );
718        } catch( final Exception e ) {
719            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.instantationfailed" ), pluginName ), e );
720        }
721        return plugin;
722    }
723
724    @Override
725    public List<Plugin> getDiscoveredPlugins() {
726        Collection<WikiModuleInfo> pluginModules = modules();
727        Set<Plugin> plugins = new HashSet<>();
728        for(WikiModuleInfo plugin : pluginModules) {
729            try {
730                Plugin p = (Plugin) Class.forName(((WikiPluginInfo)plugin).getClassName()).getDeclaredConstructor().newInstance();
731                plugins.add(p);
732            } catch (Throwable ex) {
733                LOG.error("failed to load class " + plugin.getName(), ex);
734            }
735        }
736      
737        return new ArrayList<>(plugins);
738    }
739
740}