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 java.io.IOException;
023import java.io.PrintWriter;
024import java.io.StreamTokenizer;
025import java.io.StringReader;
026import java.io.StringWriter;
027import java.text.MessageFormat;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.HashMap;
031import java.util.Iterator;
032import java.util.List;
033import java.util.Map;
034import java.util.NoSuchElementException;
035import java.util.Properties;
036import java.util.ResourceBundle;
037import java.util.StringTokenizer;
038
039import javax.servlet.http.HttpServlet;
040
041import org.apache.commons.lang.ClassUtils;
042import org.apache.commons.lang.StringUtils;
043import org.apache.log4j.Logger;
044import org.apache.oro.text.regex.MalformedPatternException;
045import org.apache.oro.text.regex.MatchResult;
046import org.apache.oro.text.regex.Pattern;
047import org.apache.oro.text.regex.PatternCompiler;
048import org.apache.oro.text.regex.PatternMatcher;
049import org.apache.oro.text.regex.Perl5Compiler;
050import org.apache.oro.text.regex.Perl5Matcher;
051import org.apache.wiki.InternalWikiException;
052import org.apache.wiki.WikiContext;
053import org.apache.wiki.WikiEngine;
054import org.apache.wiki.ajax.WikiAjaxDispatcherServlet;
055import org.apache.wiki.ajax.WikiAjaxServlet;
056import org.apache.wiki.api.engine.PluginManager;
057import org.apache.wiki.api.exceptions.PluginException;
058import org.apache.wiki.api.plugin.InitializablePlugin;
059import org.apache.wiki.api.plugin.WikiPlugin;
060import org.apache.wiki.modules.ModuleManager;
061import org.apache.wiki.modules.WikiModuleInfo;
062import org.apache.wiki.preferences.Preferences;
063import org.apache.wiki.util.ClassUtil;
064import org.apache.wiki.util.FileUtil;
065import org.apache.wiki.util.TextUtil;
066import org.apache.wiki.util.XHTML;
067import org.apache.wiki.util.XhtmlUtil;
068import org.apache.wiki.util.XmlUtil;
069import org.jdom2.Element;
070
071/**
072 *  Manages plugin classes.  There exists a single instance of PluginManager
073 *  per each instance of WikiEngine, 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 WikiEngine 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>WikiPlugin: 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 ModuleManager implements PluginManager {
163
164    private static final String PLUGIN_INSERT_PATTERN = "\\{?(INSERT)?\\s*([\\w\\._]+)[ \\t]*(WHERE)?[ \\t]*";
165
166    private static Logger log = Logger.getLogger( DefaultPluginManager.class );
167
168    private static final String DEFAULT_FORMS_PACKAGE = "org.apache.wiki.forms";
169
170    private ArrayList<String> m_searchPath = new ArrayList<String>();
171
172    private ArrayList<String> m_externalJars = new ArrayList<String>();
173
174    private Pattern m_pluginPattern;
175
176    private boolean m_pluginsEnabled = true;
177
178    /**
179     *  Keeps a list of all known plugin classes.
180     */
181    private Map<String, WikiPluginInfo> m_pluginClassMap = new HashMap<String, WikiPluginInfo>();
182
183    /**
184     *  Create a new PluginManager.
185     *
186     *  @param engine WikiEngine which owns this manager.
187     *  @param props Contents of a "jspwiki.properties" file.
188     */
189    public DefaultPluginManager( WikiEngine engine, Properties props ) {
190        super( engine );
191        String packageNames = props.getProperty( PROP_SEARCHPATH );
192
193        if ( packageNames != null ) {
194            StringTokenizer tok = new StringTokenizer( packageNames, "," );
195
196            while( tok.hasMoreTokens() ) {
197                m_searchPath.add( tok.nextToken().trim() );
198            }
199        }
200
201        String externalJars = props.getProperty( PROP_EXTERNALJARS );
202
203        if( externalJars != null ) {
204            StringTokenizer tok = new StringTokenizer( externalJars, "," );
205
206            while( tok.hasMoreTokens() ) {
207                m_externalJars.add( tok.nextToken().trim() );
208            }
209        }
210
211        registerPlugins();
212
213        //
214        //  The default packages are always added.
215        //
216        m_searchPath.add( DEFAULT_PACKAGE );
217        m_searchPath.add( DEFAULT_FORMS_PACKAGE );
218
219        PatternCompiler compiler = new Perl5Compiler();
220
221        try {
222            m_pluginPattern = compiler.compile( PLUGIN_INSERT_PATTERN );
223        } catch( MalformedPatternException e ) {
224            log.fatal( "Internal error: someone messed with pluginmanager patterns.", e );
225            throw new InternalWikiException( "PluginManager patterns are broken" , e);
226        }
227
228    }
229
230    /**
231     * {@inheritDoc}
232     */
233    public void enablePlugins( boolean enabled ) {
234        m_pluginsEnabled = enabled;
235    }
236
237    /**
238     * {@inheritDoc}
239     */
240    public boolean pluginsEnabled() {
241        return m_pluginsEnabled;
242    }
243
244    /**
245     * {@inheritDoc}
246     */
247    public Pattern getPluginPattern() {
248        return m_pluginPattern;
249    }
250
251    /**
252     * {@inheritDoc}
253     */
254    public String getPluginSearchPath() {
255        return TextUtil.getStringProperty( m_engine.getWikiProperties(), PROP_SEARCHPATH, null );
256    }
257
258    /**
259     *  Attempts to locate a plugin class from the class path set in the property file.
260     *
261     *  @param classname Either a fully fledged class name, or just the name of the file (that is,
262     *  "org.apache.wiki.plugin.Counter" or just plain "Counter").
263     *
264     *  @return A found class.
265     *
266     *  @throws ClassNotFoundException if no such class exists.
267     */
268    private Class< ? > findPluginClass( String classname ) throws ClassNotFoundException {
269        return ClassUtil.findClass( m_searchPath, m_externalJars, classname );
270    }
271
272    /**
273     *  Outputs a HTML-formatted version of a stack trace.
274     */
275    private String stackTrace( Map<String,String> params, Throwable t )
276    {
277        Element div = XhtmlUtil.element(XHTML.div,"Plugin execution failed, stack trace follows:");
278        div.setAttribute(XHTML.ATTR_class,"debug");
279
280
281        StringWriter out = new StringWriter();
282        t.printStackTrace(new PrintWriter(out));
283        div.addContent(XhtmlUtil.element(XHTML.pre,out.toString()));
284        div.addContent(XhtmlUtil.element(XHTML.b,"Parameters to the plugin"));
285
286        Element list = XhtmlUtil.element(XHTML.ul);
287
288        for( Iterator<Map.Entry<String,String>> i = params.entrySet().iterator(); i.hasNext(); ) {
289            Map.Entry<String,String> e = i.next();
290            String key = e.getKey();
291            list.addContent(XhtmlUtil.element(XHTML.li,key + "'='" + e.getValue()));
292        }
293
294        div.addContent(list);
295
296        return XhtmlUtil.serialize(div);
297    }
298
299    /**
300     *  Executes a plugin class in the given context.
301     *  <P>Used to be private, but is public since 1.9.21.
302     *
303     *  @param context The current WikiContext.
304     *  @param classname The name of the class.  Can also be a
305     *  shortened version without the package name, since the class name is searched from the
306     *  package search path.
307     *
308     *  @param params A parsed map of key-value pairs.
309     *
310     *  @return Whatever the plugin returns.
311     *
312     *  @throws PluginException If the plugin execution failed for
313     *  some reason.
314     *
315     *  @since 2.0
316     */
317    public String execute( WikiContext context, String classname, Map< String, String > params ) throws PluginException {
318        if( !m_pluginsEnabled ) {
319            return "";
320        }
321
322        ResourceBundle rb = Preferences.getBundle( context, WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE );
323        boolean debug = TextUtil.isPositive( params.get( PARAM_DEBUG ) );
324        try {
325            //
326            //   Create...
327            //
328            WikiPlugin plugin = newWikiPlugin( classname, rb );
329            if( plugin == null ) {
330                return "Plugin '" + classname + "' not compatible with this version of JSPWiki";
331            }
332
333            //
334            //  ...and launch.
335            //
336            try {
337                return plugin.execute( context, params );
338            } catch( PluginException e ) {
339                if( debug ) {
340                    return stackTrace( params, e );
341                }
342
343                // Just pass this exception onward.
344                throw ( PluginException )e.fillInStackTrace();
345            } catch( Throwable t ) {
346                // But all others get captured here.
347                log.info( "Plugin failed while executing:", t );
348                if( debug ) {
349                    return stackTrace( params, t );
350                }
351
352                throw new PluginException( rb.getString( "plugin.error.failed" ), t );
353            }
354
355        } catch( ClassCastException e ) {
356            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.notawikiplugin" ), classname ), e );
357        }
358    }
359
360    /**
361     *  Parses plugin arguments.  Handles quotes and all other kewl stuff.
362     *
363     *  <h3>Special parameters</h3>
364     *  The plugin body is put into a special parameter defined by {@link #PARAM_BODY};
365     *  the plugin's command line into a parameter defined by {@link #PARAM_CMDLINE};
366     *  and the bounds of the plugin within the wiki page text by a parameter defined
367     *  by {@link #PARAM_BOUNDS}, whose value is stored as a two-element int[] array,
368     *  i.e., <tt>[start,end]</tt>.
369     *
370     * @param argstring The argument string to the plugin.  This is
371     *  typically a list of key-value pairs, using "'" to escape
372     *  spaces in strings, followed by an empty line and then the
373     *  plugin body.  In case the parameter is null, will return an
374     *  empty parameter list.
375     *
376     * @return A parsed list of parameters.
377     *
378     * @throws IOException If the parsing fails.
379     */
380    public Map< String, String > parseArgs( String argstring ) throws IOException {
381        Map< String, String > arglist = new HashMap< String, String >();
382
383        //
384        //  Protection against funny users.
385        //
386        if( argstring == null ) return arglist;
387
388        arglist.put( PARAM_CMDLINE, argstring );
389
390        StringReader    in      = new StringReader(argstring);
391        StreamTokenizer tok     = new StreamTokenizer(in);
392        int             type;
393
394
395        String param = null;
396        String value = null;
397
398        tok.eolIsSignificant( true );
399
400        boolean potentialEmptyLine = false;
401        boolean quit               = false;
402
403        while( !quit ) {
404            String s;
405            type = tok.nextToken();
406
407            switch( type ) {
408              case StreamTokenizer.TT_EOF:
409                quit = true;
410                s = null;
411                break;
412
413              case StreamTokenizer.TT_WORD:
414                s = tok.sval;
415                potentialEmptyLine = false;
416                break;
417
418              case StreamTokenizer.TT_EOL:
419                quit = potentialEmptyLine;
420                potentialEmptyLine = true;
421                s = null;
422                break;
423
424              case StreamTokenizer.TT_NUMBER:
425                s = Integer.toString( (int) tok.nval );
426                potentialEmptyLine = false;
427                break;
428
429              case '\'':
430                s = tok.sval;
431                break;
432
433              default:
434                s = null;
435            }
436
437            //
438            //  Assume that alternate words on the line are
439            //  parameter and value, respectively.
440            //
441            if( s != null ) {
442                if( param == null ) {
443                    param = s;
444                } else {
445                    value = s;
446
447                    arglist.put( param, value );
448
449                    // log.debug("ARG: "+param+"="+value);
450                    param = null;
451                }
452            }
453        }
454
455        //
456        //  Now, we'll check the body.
457        //
458        if( potentialEmptyLine ) {
459            StringWriter out = new StringWriter();
460            FileUtil.copyContents( in, out );
461
462            String bodyContent = out.toString();
463
464            if( bodyContent != null ) {
465                arglist.put( PARAM_BODY, bodyContent );
466            }
467        }
468
469        return arglist;
470    }
471
472    /**
473     *  Parses a plugin.  Plugin commands are of the form:
474     *  [{INSERT myplugin WHERE param1=value1, param2=value2}]
475     *  myplugin may either be a class name or a plugin alias.
476     *  <P>
477     *  This is the main entry point that is used.
478     *
479     *  @param context The current WikiContext.
480     *  @param commandline The full command line, including plugin name, parameters and body.
481     *
482     *  @return HTML as returned by the plugin, or possibly an error message.
483     *
484     *  @throws PluginException From the plugin itself, it propagates, waah!
485     */
486    public String execute( WikiContext context, String commandline ) throws PluginException {
487        if( !m_pluginsEnabled ) {
488            return "";
489        }
490
491        ResourceBundle rb = Preferences.getBundle( context, WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE );
492        PatternMatcher matcher = new Perl5Matcher();
493
494        try {
495            if( matcher.contains( commandline, m_pluginPattern ) ) {
496                MatchResult res = matcher.getMatch();
497
498                String plugin   = res.group(2);
499                String args     = commandline.substring(res.endOffset(0),
500                                                        commandline.length() -
501                                                        (commandline.charAt(commandline.length()-1) == '}' ? 1 : 0 ) );
502                Map<String, String> arglist  = parseArgs( args );
503
504                return execute( context, plugin, arglist );
505            }
506        } catch( NoSuchElementException e ) {
507            String msg =  "Missing parameter in plugin definition: "+commandline;
508            log.warn( msg, e );
509            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.missingparameter" ), commandline ) );
510        } catch( IOException e ) {
511            String msg = "Zyrf.  Problems with parsing arguments: "+commandline;
512            log.warn( msg, e );
513            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.parsingarguments" ), commandline ) );
514        }
515
516        // FIXME: We could either return an empty string "", or
517        // the original line.  If we want unsuccessful requests
518        // to be invisible, then we should return an empty string.
519        return commandline;
520    }
521
522    /**
523     *  Register a plugin.
524     */
525    private void registerPlugin( WikiPluginInfo pluginClass ) {
526        String name;
527
528        // Registrar the plugin with the className without the package-part
529        name = pluginClass.getName();
530        if( name != null ) {
531            log.debug( "Registering plugin [name]: " + name );
532            m_pluginClassMap.put( name, pluginClass );
533        }
534
535        // Registrar the plugin with a short convenient name.
536        name = pluginClass.getAlias();
537        if( name != null ) {
538            log.debug( "Registering plugin [shortName]: " + name );
539            m_pluginClassMap.put( name, pluginClass );
540        }
541
542        // Registrar the plugin with the className with the package-part
543        name = pluginClass.getClassName();
544        if( name != null ) {
545            log.debug( "Registering plugin [className]: " + name );
546            m_pluginClassMap.put( name, pluginClass );
547        }
548
549        pluginClass.initializePlugin( pluginClass, m_engine , m_searchPath, m_externalJars);
550    }
551
552    private void registerPlugins() {
553        log.info( "Registering plugins" );
554        List< Element > plugins = XmlUtil.parse( PLUGIN_RESOURCE_LOCATION, "/modules/plugin" );
555
556        //
557        // Register all plugins which have created a resource containing its properties.
558        //
559        // Get all resources of all plugins.
560        //
561        for( Element pluginEl : plugins ) {
562            String className = pluginEl.getAttributeValue( "class" );
563            WikiPluginInfo pluginInfo = WikiPluginInfo.newInstance( className, pluginEl ,m_searchPath, m_externalJars);
564
565            if( pluginInfo != null ) {
566                registerPlugin( pluginInfo );
567            }
568        }
569    }
570
571    /**
572     *  Contains information about a bunch of plugins.
573     *
574     *
575     */
576    // FIXME: This class needs a better interface to return all sorts of possible
577    //        information from the plugin XML.  In fact, it probably should have
578    //        some sort of a superclass system.
579    public static final class WikiPluginInfo extends WikiModuleInfo {
580
581        private String    m_className;
582        private String    m_alias;
583        private String    m_ajaxAlias;
584        private Class<?>  m_clazz;
585
586        private boolean m_initialized = false;
587
588        /**
589         *  Creates a new plugin info object which can be used to access a plugin.
590         *
591         *  @param className Either a fully qualified class name, or a "short" name which is then
592         *                   checked against the internal list of plugin packages.
593         *  @param el A JDOM Element containing the information about this class.
594         *  @param searchPath A List of Strings, containing different package names.
595         *  @param externalJars the list of external jars to search
596         *  @return A WikiPluginInfo object.
597         */
598        protected static WikiPluginInfo newInstance( String className, Element el, List<String> searchPath, List<String> externalJars ) {
599            if( className == null || className.length() == 0 ) return null;
600
601            WikiPluginInfo info = new WikiPluginInfo( className );
602            info.initializeFromXML( el );
603            return info;
604        }
605
606        /**
607         *  Initializes a plugin, if it has not yet been initialized.
608         *  If the plugin extends {@link HttpServlet} it will automatically
609         *  register it as AJAX using {@link WikiAjaxDispatcherServlet#registerServlet(String, WikiAjaxServlet)}.
610         *
611         *  @param engine The WikiEngine
612         *  @param searchPath A List of Strings, containing different package names.
613         *  @param externalJars the list of external jars to search
614         */
615        protected void initializePlugin( WikiPluginInfo info, WikiEngine engine , List<String> searchPath, List<String> externalJars) {
616            if( !m_initialized ) {
617                // This makes sure we only try once per class, even if init fails.
618                m_initialized = true;
619
620                try {
621                    WikiPlugin p = newPluginInstance(searchPath, externalJars);
622                    if( p instanceof InitializablePlugin ) {
623                        ( ( InitializablePlugin )p ).initialize( engine );
624                    }
625                    if( p instanceof WikiAjaxServlet ) {
626                        WikiAjaxDispatcherServlet.registerServlet( (WikiAjaxServlet) p );
627                        String ajaxAlias = info.getAjaxAlias();
628                        if (StringUtils.isNotBlank(ajaxAlias)) {
629                            WikiAjaxDispatcherServlet.registerServlet( info.getAjaxAlias(), (WikiAjaxServlet) p );
630                        }
631                    }
632                } catch( Exception e ) {
633                    log.info( "Cannot initialize plugin " + m_className, e );
634                }
635            }
636        }
637
638        /**
639         *  {@inheritDoc}
640         */
641        @Override
642        protected void initializeFromXML( Element el ) {
643            super.initializeFromXML( el );
644            m_alias = el.getChildText( "alias" );
645            m_ajaxAlias = el.getChildText( "ajaxAlias" );
646        }
647
648        /**
649         *  Create a new WikiPluginInfo based on the Class information.
650         *
651         *  @param clazz The class to check
652         *  @return A WikiPluginInfo instance
653         */
654        protected static WikiPluginInfo newInstance( Class< ? > clazz ) {
655            return new WikiPluginInfo( clazz.getName() );
656        }
657
658        private WikiPluginInfo( String className ) {
659            super( className );
660            setClassName( className );
661        }
662
663        private void setClassName( String fullClassName ) {
664            m_name = ClassUtils.getShortClassName( fullClassName );
665            m_className = fullClassName;
666        }
667
668        /**
669         *  Returns the full class name of this object.
670         *  @return The full class name of the object.
671         */
672        public String getClassName() {
673            return m_className;
674        }
675
676        /**
677         *  Returns the alias name for this object.
678         *  @return An alias name for the plugin.
679         */
680        public String getAlias() {
681            return m_alias;
682        }
683
684        /**
685         *  Returns the ajax alias name for this object.
686         *  @return An ajax alias name for the plugin.
687         */
688        public String getAjaxAlias() {
689            return m_ajaxAlias;
690        }
691
692        /**
693         *  Creates a new plugin instance.
694         *
695         *  @param searchPath A List of Strings, containing different package names.
696         *  @param externalJars the list of external jars to search
697
698         *  @return A new plugin.
699         *  @throws ClassNotFoundException If the class declared was not found.
700         *  @throws InstantiationException If the class cannot be instantiated-
701         *  @throws IllegalAccessException If the class cannot be accessed.
702         */
703
704        public WikiPlugin newPluginInstance(List<String> searchPath, List<String> externalJars) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
705            if( m_clazz == null ) {
706                m_clazz = ClassUtil.findClass(searchPath, externalJars ,m_className);
707            }
708
709            return (WikiPlugin) m_clazz.newInstance();
710        }
711
712        /**
713         *  Returns a text for IncludeResources.
714         *
715         *  @param type Either "script" or "stylesheet"
716         *  @return Text, or an empty string, if there is nothing to be included.
717         */
718        public String getIncludeText( String type ) {
719            try {
720                if( "script".equals( type ) ) {
721                    return getScriptText();
722                } else if( "stylesheet".equals( type ) ) {
723                    return getStylesheetText();
724                }
725            } catch( Exception ex ) {
726                // We want to fail gracefully here
727                return ex.getMessage();
728            }
729
730            return null;
731        }
732
733        private String getScriptText() throws IOException {
734            if( m_scriptText != null ) {
735                return m_scriptText;
736            }
737
738            if( m_scriptLocation == null ) {
739                return "";
740            }
741
742            try {
743                m_scriptText = getTextResource(m_scriptLocation);
744            } catch( IOException ex ) {
745                // Only throw this exception once!
746                m_scriptText = "";
747                throw ex;
748            }
749
750            return m_scriptText;
751        }
752
753        private String getStylesheetText() throws IOException {
754            if( m_stylesheetText != null ) {
755                return m_stylesheetText;
756            }
757
758            if( m_stylesheetLocation == null ) {
759                return "";
760            }
761
762            try {
763                m_stylesheetText = getTextResource(m_stylesheetLocation);
764            } catch( IOException ex ) {
765                // Only throw this exception once!
766                m_stylesheetText = "";
767                throw ex;
768            }
769
770            return m_stylesheetText;
771        }
772
773        /**
774         *  Returns a string suitable for debugging.  Don't assume that the format would stay the same.
775         *
776         *  @return Something human-readable
777         */
778        public String toString() {
779            return "Plugin :[name=" + m_name + "][className=" + m_className + "]";
780        }
781    } // WikiPluginClass
782
783    /**
784     *  {@inheritDoc}
785     */
786    @Override
787    public Collection< WikiModuleInfo > modules() {
788        return modules( m_pluginClassMap.values().iterator() );
789    }
790
791    /**
792     *  {@inheritDoc}
793     */
794    @Override
795    public WikiPluginInfo getModuleInfo(String moduleName) {
796        return m_pluginClassMap.get(moduleName);
797    }
798
799    /**
800     * Creates a {@link WikiPlugin}.
801     *
802     * @param pluginName plugin's classname
803     * @param rb {@link ResourceBundle} with i18ned text for exceptions.
804     * @return a {@link WikiPlugin}.
805     * @throws PluginException if there is a problem building the {@link WikiPlugin}.
806     */
807    public WikiPlugin newWikiPlugin( String pluginName, ResourceBundle rb ) throws PluginException {
808        WikiPlugin plugin = null;
809        WikiPluginInfo pluginInfo = m_pluginClassMap.get( pluginName );
810        try {
811            if( pluginInfo == null ) {
812                pluginInfo = WikiPluginInfo.newInstance( findPluginClass( pluginName ) );
813                registerPlugin( pluginInfo );
814            }
815
816            if( !checkCompatibility( pluginInfo ) ) {
817                String msg = "Plugin '" + pluginInfo.getName() + "' not compatible with this version of JSPWiki";
818                log.info( msg );
819            } else {
820                plugin = pluginInfo.newPluginInstance(m_searchPath, m_externalJars);
821            }
822        } catch( ClassNotFoundException e ) {
823            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.couldnotfind" ), pluginName ), e );
824        } catch( InstantiationException e ) {
825            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.cannotinstantiate" ), pluginName ), e );
826        } catch( IllegalAccessException e ) {
827            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.notallowed" ), pluginName ), e );
828        } catch( Exception e ) {
829            throw new PluginException( MessageFormat.format( rb.getString( "plugin.error.instantationfailed" ), pluginName ), e );
830        }
831        return plugin;
832    }
833
834}