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