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