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