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