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        //see JSPWIKI-75
270        final boolean debug = TextUtil.isPositive( params.get( PARAM_DEBUG ) ) && context.hasAdminPermissions();
271        
272        try {
273            //   Create...
274            final Plugin plugin = newWikiPlugin( classname, rb );
275            if( plugin == null ) {
276                return "Plugin '" + classname + "' not compatible with this version of JSPWiki";
277            }
278
279            //  ...and launch.
280            try {
281                return plugin.execute( context, params );
282            } catch( final PluginException e ) {
283                LOG.warn(e.getMessage(), e);
284                if( debug ) {
285                    return stackTrace( params, e );
286                }
287
288                // Just pass this exception onward.
289                throw ( PluginException )e.fillInStackTrace();
290            } catch( final Throwable t ) {
291                
292                // But all others get captured here.
293                LOG.warn( "Plugin failed while executing:", t );
294                if( debug ) {
295                    return stackTrace( params, t );
296                }
297
298                throw new PluginException( rb.getString( "plugin.error.failed" ), t );
299            }
300
301        } catch( final ClassCastException e ) {
302            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.notawikiplugin" ), classname ), e );
303        }
304    }
305
306    /** {@inheritDoc} */
307    @Override
308    public Map< String, String > parseArgs( final String argstring ) throws IOException {
309        final Map< String, String > arglist = new HashMap<>();
310        //  Protection against funny users.
311        if( argstring == null ) {
312            return arglist;
313        }
314
315        arglist.put( PARAM_CMDLINE, argstring );
316        final StringReader in = new StringReader( argstring );
317        final StreamTokenizer tok = new StreamTokenizer( in );
318        tok.eolIsSignificant( true );
319
320        String param = null;
321        String value;
322        boolean potentialEmptyLine = false;
323        boolean quit = false;
324        while( !quit ) {
325            final String s;
326            final int type = tok.nextToken();
327
328            switch( type ) {
329            case StreamTokenizer.TT_EOF:
330                quit = true;
331                s = null;
332                break;
333
334            case StreamTokenizer.TT_WORD:
335                s = tok.sval;
336                potentialEmptyLine = false;
337                break;
338
339            case StreamTokenizer.TT_EOL:
340                quit = potentialEmptyLine;
341                potentialEmptyLine = true;
342                s = null;
343                break;
344
345            case StreamTokenizer.TT_NUMBER:
346                s = Integer.toString( ( int )tok.nval );
347                potentialEmptyLine = false;
348                break;
349
350            case '\'':
351                s = tok.sval;
352                break;
353
354            default:
355                s = null;
356            }
357
358            //  Assume that alternate words on the line are parameter and value, respectively.
359            if( s != null ) {
360                if( param == null ) {
361                    param = s;
362                } else {
363                    value = s;
364                    arglist.put( param, value );
365                    param = null;
366                }
367            }
368        }
369
370        //  Now, we'll check the body.
371        if( potentialEmptyLine ) {
372            final StringWriter out = new StringWriter();
373            FileUtil.copyContents( in, out );
374            final String bodyContent = out.toString();
375            if( bodyContent != null ) {
376                arglist.put( PARAM_BODY, bodyContent );
377            }
378        }
379
380        return arglist;
381    }
382
383    /** {@inheritDoc} */
384    @Override
385    public String execute( final Context context, final String commandline ) throws PluginException {
386        if( !m_pluginsEnabled ) {
387            return "";
388        }
389
390        final ResourceBundle rb = Preferences.getBundle( context, Plugin.CORE_PLUGINS_RESOURCEBUNDLE );
391        final PatternMatcher matcher = new Perl5Matcher();
392
393        try {
394            if( matcher.contains( commandline, m_pluginPattern ) ) {
395                final MatchResult res = matcher.getMatch();
396                final String plugin = res.group( 2 );
397                final int endIndex = commandline.length() - ( commandline.charAt( commandline.length() - 1 ) == '}' ? 1 : 0 );
398                final String args = commandline.substring( res.endOffset( 0 ), endIndex );
399                final Map< String, String > arglist = parseArgs( args );
400                return execute( context, plugin, arglist );
401            }
402        } catch( final NoSuchElementException e ) {
403            final String msg =  "Missing parameter in plugin definition: " + commandline;
404            LOG.warn( msg, e );
405            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.missingparameter" ), commandline ) );
406        } catch( final IOException e ) {
407            final String msg = "Zyrf.  Problems with parsing arguments: " + commandline;
408            LOG.warn( msg, e );
409            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.parsingarguments" ), commandline ) );
410        }
411
412        // FIXME: We could either return an empty string "", or the original line.  If we want unsuccessful requests
413        // to be invisible, then we should return an empty string.
414        return commandline;
415    }
416
417    /** Register a plugin. */
418    private void registerPlugin( final WikiPluginInfo pluginClass ) {
419        String name;
420
421        // Register the plugin with the className without the package-part
422        name = pluginClass.getName();
423        if( name != null ) {
424            LOG.debug( "Registering plugin [name]: " + name );
425            m_pluginClassMap.put( name, pluginClass );
426        }
427
428        // Register the plugin with a short convenient name.
429        name = pluginClass.getAlias();
430        if( name != null ) {
431            LOG.debug( "Registering plugin [shortName]: " + name );
432            m_pluginClassMap.put( name, pluginClass );
433        }
434
435        // Register the plugin with the className with the package-part
436        name = pluginClass.getClassName();
437        if( name != null ) {
438            LOG.debug( "Registering plugin [className]: " + name );
439            m_pluginClassMap.put( name, pluginClass );
440        }
441
442        pluginClass.initializePlugin( pluginClass, m_engine, m_searchPath, m_externalJars );
443    }
444
445    private void registerPlugins() {
446        // Register all plugins which have created a resource containing its properties.
447        LOG.info( "Registering plugins" );
448        final List< Element > plugins = XmlUtil.parse( PLUGIN_RESOURCE_LOCATION, "/modules/plugin" );
449
450        // Get all resources of all plugins.
451        for( final Element pluginEl : plugins ) {
452            final String className = pluginEl.getAttributeValue( "class" );
453            final WikiPluginInfo pluginInfo = WikiPluginInfo.newInstance( className, pluginEl ,m_searchPath, m_externalJars );
454            if( pluginInfo != null ) {
455                registerPlugin( pluginInfo );
456            }
457        }
458    }
459
460    /**
461     *  Contains information about a bunch of plugins.
462     */
463    // FIXME: This class needs a better interface to return all sorts of possible information from the plugin XML.  In fact, it probably
464    //  should have some sort of a superclass system.
465    public static final class WikiPluginInfo extends WikiModuleInfo {
466
467        private String    m_className;
468        private String    m_alias;
469        private String    m_ajaxAlias;
470        private Class< Plugin >  m_clazz;
471
472        private boolean m_initialized;
473
474        /**
475         *  Creates a new plugin info object which can be used to access a plugin.
476         *
477         *  @param className Either a fully qualified class name, or a "short" name which is then checked against the internal list of plugin packages.
478         *  @param el A JDOM Element containing the information about this class.
479         *  @param searchPath A List of Strings, containing different package names.
480         *  @param externalJars the list of external jars to search
481         *  @return A WikiPluginInfo object.
482         */
483        static WikiPluginInfo newInstance( final String className, final Element el, final List<String> searchPath, final List<String> externalJars ) {
484            if( className == null || className.isEmpty() ) {
485                return null;
486            }
487
488            final WikiPluginInfo info = new WikiPluginInfo( className );
489            info.initializeFromXML( el );
490            return info;
491        }
492
493        /**
494         *  Initializes a plugin, if it has not yet been initialized. If the plugin extends {@link HttpServlet} it will automatically
495         *  register it as AJAX using {@link WikiAjaxDispatcherServlet#registerServlet(String, WikiAjaxServlet)}.
496         *
497         *  @param engine The Engine
498         *  @param searchPath A List of Strings, containing different package names.
499         *  @param externalJars the list of external jars to search
500         */
501        void initializePlugin( final WikiPluginInfo info, final Engine engine , final List<String> searchPath, final List<String> externalJars) {
502            if( !m_initialized ) {
503                // This makes sure we only try once per class, even if init fails.
504                m_initialized = true;
505
506                try {
507                    final Plugin p = newPluginInstance(searchPath, externalJars);
508                    if( p instanceof InitializablePlugin ) {
509                        ( ( InitializablePlugin )p ).initialize( engine );
510                    }
511                    if( p instanceof WikiAjaxServlet ) {
512                        WikiAjaxDispatcherServlet.registerServlet( (WikiAjaxServlet) p );
513                        final String ajaxAlias = info.getAjaxAlias();
514                        if (StringUtils.isNotBlank(ajaxAlias)) {
515                            WikiAjaxDispatcherServlet.registerServlet( info.getAjaxAlias(), (WikiAjaxServlet) p );
516                        }
517                    }
518                } catch( final Exception e ) {
519                    LOG.info( "Cannot initialize plugin " + m_className, e );
520                }
521            }
522        }
523
524        /**
525         *  {@inheritDoc}
526         */
527        @Override
528        protected void initializeFromXML( final Element el ) {
529            super.initializeFromXML( el );
530            m_alias = el.getChildText( "alias" );
531            m_ajaxAlias = el.getChildText( "ajaxAlias" );
532        }
533
534        /**
535         *  Create a new WikiPluginInfo based on the Class information.
536         *
537         *  @param clazz The class to check
538         *  @return A WikiPluginInfo instance
539         */
540        static WikiPluginInfo newInstance( final Class< ? > clazz ) {
541            return new WikiPluginInfo( clazz.getName() );
542        }
543
544        private WikiPluginInfo( final String className ) {
545            super( className );
546            setClassName( className );
547        }
548
549        private void setClassName( final String fullClassName ) {
550            m_name = ClassUtils.getShortClassName( fullClassName );
551            m_className = fullClassName;
552        }
553
554        /**
555         *  Returns the full class name of this object.
556         *  @return The full class name of the object.
557         */
558        public String getClassName() {
559            return m_className;
560        }
561
562        /**
563         *  Returns the alias name for this object.
564         *  @return An alias name for the plugin.
565         */
566        public String getAlias() {
567            return m_alias;
568        }
569
570        /**
571         *  Returns the ajax alias name for this object.
572         *  @return An ajax alias name for the plugin.
573         */
574        public String getAjaxAlias() {
575            return m_ajaxAlias;
576        }
577
578        /**
579         *  Creates a new plugin instance.
580         *
581         *  @param searchPath A List of Strings, containing different package names.
582         *  @param externalJars the list of external jars to search
583         *  @return A new plugin.
584         *  @throws ClassNotFoundException If the class declared was not found.
585         *  @throws InstantiationException If the class cannot be instantiated-
586         *  @throws IllegalAccessException If the class cannot be accessed.
587         */
588
589        public Plugin newPluginInstance( final List< String > searchPath, final List< String > externalJars) throws ReflectiveOperationException {
590            if( m_clazz == null ) {
591                m_clazz = ClassUtil.findClass( searchPath, externalJars ,m_className );
592            }
593
594            return ClassUtil.buildInstance( m_clazz );
595        }
596
597        /**
598         *  Returns a text for IncludeResources.
599         *
600         *  @param type Either "script" or "stylesheet"
601         *  @return Text, or an empty string, if there is nothing to be included.
602         */
603        public String getIncludeText( final String type ) {
604            try {
605                if( "script".equals( type ) ) {
606                    return getScriptText();
607                } else if( "stylesheet".equals( type ) ) {
608                    return getStylesheetText();
609                }
610            } catch( final Exception ex ) {
611                // We want to fail gracefully here
612                return ex.getMessage();
613            }
614
615            return null;
616        }
617
618        private String getScriptText() throws IOException {
619            if( m_scriptText != null ) {
620                return m_scriptText;
621            }
622
623            if( m_scriptLocation == null ) {
624                return "";
625            }
626
627            try {
628                m_scriptText = getTextResource(m_scriptLocation);
629            } catch( final IOException ex ) {
630                // Only throw this exception once!
631                m_scriptText = "";
632                throw ex;
633            }
634
635            return m_scriptText;
636        }
637
638        private String getStylesheetText() throws IOException {
639            if( m_stylesheetText != null ) {
640                return m_stylesheetText;
641            }
642
643            if( m_stylesheetLocation == null ) {
644                return "";
645            }
646
647            try {
648                m_stylesheetText = getTextResource(m_stylesheetLocation);
649            } catch( final IOException ex ) {
650                // Only throw this exception once!
651                m_stylesheetText = "";
652                throw ex;
653            }
654
655            return m_stylesheetText;
656        }
657
658        /**
659         *  Returns a string suitable for debugging.  Don't assume that the format would stay the same.
660         *
661         *  @return Something human-readable
662         */
663        @Override
664        public String toString() {
665            return "Plugin :[name=" + m_name + "][className=" + m_className + "]";
666        }
667
668    } // WikiPluginClass
669
670    /**
671     *  {@inheritDoc}
672     */
673    @Override
674    public Collection< WikiModuleInfo > modules() {
675        return modules( m_pluginClassMap.values().iterator() );
676    }
677
678    /**
679     *  {@inheritDoc}
680     */
681    @Override
682    public WikiPluginInfo getModuleInfo( final String moduleName) {
683        return m_pluginClassMap.get(moduleName);
684    }
685
686    /**
687     * Creates a {@link Plugin}.
688     *
689     * @param pluginName plugin's classname
690     * @param rb {@link ResourceBundle} with i18ned text for exceptions.
691     * @return a {@link Plugin}.
692     * @throws PluginException if there is a problem building the {@link Plugin}.
693     */
694    @Override
695    public Plugin newWikiPlugin( final String pluginName, final ResourceBundle rb ) throws PluginException {
696        Plugin plugin = null;
697        WikiPluginInfo pluginInfo = m_pluginClassMap.get( pluginName );
698        try {
699            if( pluginInfo == null ) {
700                pluginInfo = WikiPluginInfo.newInstance( findPluginClass( pluginName ) );
701                registerPlugin( pluginInfo );
702            }
703
704            if( !checkCompatibility( pluginInfo ) ) {
705                final String msg = "Plugin '" + pluginInfo.getName() + "' not compatible with this version of JSPWiki";
706                LOG.info( msg );
707            } else {
708                plugin = pluginInfo.newPluginInstance(m_searchPath, m_externalJars);
709            }
710        } catch( final ClassNotFoundException e ) {
711            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.couldnotfind" ), pluginName ), e );
712        } catch( final InstantiationException e ) {
713            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.cannotinstantiate" ), pluginName ), e );
714        } catch( final IllegalAccessException e ) {
715            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.notallowed" ), pluginName ), e );
716        } catch( final Exception e ) {
717            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.instantationfailed" ), pluginName ), e );
718        }
719        return plugin;
720    }
721
722}