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