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