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