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 */
019package org.apache.wiki.ui;
020
021import org.apache.commons.lang3.StringUtils;
022import org.apache.log4j.Logger;
023import org.apache.wiki.InternalWikiException;
024import org.apache.wiki.WikiContext;
025import org.apache.wiki.WikiEngine;
026import org.apache.wiki.i18n.InternationalizationManager;
027import org.apache.wiki.modules.ModuleManager;
028import org.apache.wiki.modules.WikiModuleInfo;
029import org.apache.wiki.preferences.Preferences;
030import org.apache.wiki.preferences.Preferences.TimeFormat;
031import org.apache.wiki.util.ClassUtil;
032
033import javax.servlet.ServletContext;
034import javax.servlet.http.HttpServletRequest;
035import javax.servlet.jsp.PageContext;
036import javax.servlet.jsp.jstl.fmt.LocaleSupport;
037import java.io.IOException;
038import java.io.InputStream;
039import java.text.SimpleDateFormat;
040import java.util.ArrayList;
041import java.util.Collection;
042import java.util.Collections;
043import java.util.Date;
044import java.util.Enumeration;
045import java.util.HashMap;
046import java.util.LinkedHashMap;
047import java.util.List;
048import java.util.Locale;
049import java.util.Map;
050import java.util.Properties;
051import java.util.ResourceBundle;
052import java.util.Set;
053import java.util.TimeZone;
054import java.util.TreeSet;
055import java.util.Vector;
056
057
058/**
059 *  This class takes care of managing JSPWiki templates.  This class also provides
060 *  the ResourceRequest mechanism.
061 *
062 *  @since 2.1.62
063 */
064public class TemplateManager extends ModuleManager {
065
066    private static final String SKIN_DIRECTORY = "skins";
067
068    /**
069     * Requests a JavaScript function to be called during window.onload. Value is {@value}.
070     */
071    public static final String RESOURCE_JSFUNCTION = "jsfunction";
072
073    /**
074     * Requests a JavaScript associative array with all localized strings.
075     */
076    public static final String RESOURCE_JSLOCALIZEDSTRINGS = "jslocalizedstrings";
077
078    /**
079     * Requests a stylesheet to be inserted. Value is {@value}.
080     */
081    public static final String RESOURCE_STYLESHEET = "stylesheet";
082
083    /**
084     * Requests a script to be loaded. Value is {@value}.
085     */
086    public static final String RESOURCE_SCRIPT = "script";
087
088    /**
089     * Requests inlined CSS. Value is {@value}.
090     */
091    public static final String RESOURCE_INLINECSS = "inlinecss";
092
093    /** The default directory for the properties. Value is {@value}. */
094    public static final String DIRECTORY = "templates";
095
096    /** The name of the default template. Value is {@value}. */
097    public static final String DEFAULT_TEMPLATE = "default";
098
099    /** Name of the file that contains the properties. */
100
101    public static final String PROPERTYFILE = "template.properties";
102
103    /** Location of I18N Resource bundles, and path prefix and suffixes */
104
105    public static final String I18NRESOURCE_PREFIX = "templates/default_";
106
107    public static final String I18NRESOURCE_SUFFIX = ".properties";
108
109    /** The default (en) RESOURCE name and id. */
110
111    public static final String I18NRESOURCE_EN = "templates/default.properties";
112    public static final String I18NRESOURCE_EN_ID = "en";
113
114    /** I18N string to mark the default locale */
115
116    public static final String I18NDEFAULT_LOCALE = "prefs.user.language.default";
117
118    /** I18N string to mark the server timezone */
119
120    public static final String I18NSERVER_TIMEZONE = "prefs.user.timezone.server";
121
122    /** Prefix of the default timeformat properties. */
123
124    public static final String TIMEFORMATPROPERTIES = "jspwiki.defaultprefs.timeformat.";
125
126    /**
127     * The name under which the resource includes map is stored in the
128     * WikiContext.
129     */
130    public static final String RESOURCE_INCLUDES = "jspwiki.resourceincludes";
131
132    // private Cache m_propertyCache;
133
134    protected static final Logger log = Logger.getLogger(TemplateManager.class);
135
136    /** Requests a HTTP header. Value is {@value}. */
137    public static final String RESOURCE_HTTPHEADER = "httpheader";
138
139    /**
140     *  Creates a new TemplateManager.  There is typically one manager per engine.
141     *
142     *  @param engine The owning engine.
143     *  @param properties The property list used to initialize this.
144     */
145    public TemplateManager( WikiEngine engine, Properties properties )
146    {
147        super(engine);
148
149        //
150        //  Uses the unlimited cache.
151        //
152        // m_propertyCache = new Cache( true, false );
153    }
154
155    /**
156     *  Check the existence of a template.
157     */
158    // FIXME: Does not work yet
159    public boolean templateExists( String templateName ) {
160        ServletContext context = m_engine.getServletContext();
161        try( final InputStream in = context.getResourceAsStream( getPath( templateName ) + "ViewTemplate.jsp" ) ) {
162            if( in != null ) {
163                return true;
164            }
165        } catch( IOException e ) {
166            log.error( e.getMessage(), e );
167        }
168        return false;
169    }
170
171    /**
172     *  Tries to locate a given resource from the template directory. If the
173     *  given resource is not found under the current name, returns the
174     *  path to the corresponding one in the default template.
175     *
176     *  @param sContext The servlet context
177     *  @param name The name of the resource
178     *  @return The name of the resource which was found.
179     */
180    private static String findResource( final ServletContext sContext, final String name ) {
181        String resourceName = name;
182        try( final InputStream is = sContext.getResourceAsStream( resourceName ) ) {
183            if( is == null ) {
184                final String defname = makeFullJSPName( DEFAULT_TEMPLATE, removeTemplatePart( resourceName ) );
185                try( final InputStream iis = sContext.getResourceAsStream( defname ) ) {
186                    resourceName = iis != null ? defname : null;
187                }
188            }
189        } catch( final IOException e ) {
190            log.error( "unable to open " + name + " as resource stream", e );
191        }
192        return resourceName;
193    }
194
195    /**
196     *  Attempts to find a resource from the given template, and if it's not found
197     *  attempts to locate it from the default template.
198     * @param sContext
199     * @param template
200     * @param name
201     * @return the Resource for the given template and name.
202     */
203    private static String findResource( ServletContext sContext, String template, String name )
204    {
205        if( name.charAt(0) == '/' )
206        {
207            // This is already a full path
208            return findResource( sContext, name );
209        }
210
211        String fullname = makeFullJSPName( template, name );
212
213        return findResource( sContext, fullname );
214    }
215
216    /**
217     *  An utility method for finding a JSP page.  It searches only under
218     *  either current context or by the absolute name.
219     *
220     *  @param pageContext the JSP PageContext
221     *  @param name The name of the JSP page to look for (e.g "Wiki.jsp")
222     *  @return The context path to the resource
223     */
224    public String findJSP( PageContext pageContext, String name )
225    {
226        ServletContext sContext = pageContext.getServletContext();
227
228        return findResource( sContext, name );
229    }
230
231    /**
232     *  Removes the template part of a name.
233     */
234    private static String removeTemplatePart( String name )
235    {
236        int idx = 0;
237        if( name.startsWith( "/" ) ) idx = 1;
238
239        idx = name.indexOf('/', idx);
240        if( idx != -1 )
241        {
242            idx = name.indexOf('/', idx+1); // Find second "/"
243
244            if( idx != -1 )
245            {
246                name = name.substring( idx+1 );
247            }
248        }
249
250        if( log.isDebugEnabled() ) log.debug( "Final name = "+name );
251        return name;
252    }
253
254    /**
255     *  Returns the full name (/templates/foo/bar) for name=bar, template=foo.
256     *
257     * @param template The name of the template.
258     * @param name The name of the resource.
259     * @return The full name for a template.
260     */
261    private static String makeFullJSPName( String template, String name )
262    {
263        return "/"+DIRECTORY+"/"+template+"/"+name;
264    }
265
266    /**
267     *  Attempts to locate a resource under the given template.  If that template
268     *  does not exist, or the page does not exist under that template, will
269     *  attempt to locate a similarly named file under the default template.
270     *  <p>
271     *  Even though the name suggests only JSP files can be located, but in fact
272     *  this method can find also other resources than JSP files.
273     *
274     *  @param pageContext The JSP PageContext
275     *  @param template From which template we should seek initially?
276     *  @param name Which resource are we looking for (e.g. "ViewTemplate.jsp")
277     *  @return path to the JSP page; null, if it was not found.
278     */
279    public String findJSP( PageContext pageContext, String template, String name )
280    {
281        if( name == null || template == null )
282        {
283            log.fatal("findJSP() was asked to find a null template or name ("+template+","+name+")."+
284                      " JSP page '"+
285                      ((HttpServletRequest)pageContext.getRequest()).getRequestURI()+"'");
286            throw new InternalWikiException("Illegal arguments to findJSP(); please check logs.");
287        }
288
289        return findResource( pageContext.getServletContext(), template, name );
290    }
291
292    /**
293     *  Attempts to locate a resource under the given template.  This matches the
294     *  functionality findJSP(), but uses the WikiContext as the argument.  If there
295     *  is no servlet context (i.e. this is embedded), will just simply return
296     *  a best-guess.
297     *  <p>
298     *  This method is typically used to locate any resource, including JSP pages, images,
299     *  scripts, etc.
300     *
301     *  @since 2.6
302     *  @param ctx the wiki context
303     *  @param template the name of the template to use
304     *  @param name the name of the resource to fine
305     *  @return the path to the resource
306     */
307    public String findResource( WikiContext ctx, String template, String name )
308    {
309        if( m_engine.getServletContext() != null )
310        {
311            return findResource( m_engine.getServletContext(), template, name );
312        }
313
314        return getPath(template)+"/"+name;
315    }
316
317    /**
318     *  Returns a property, as defined in the template.  The evaluation
319     *  is lazy, i.e. the properties are not loaded until the template is
320     *  actually used for the first time.
321     */
322    /*
323    public String getTemplateProperty( WikiContext context, String key )
324    {
325        String template = context.getTemplate();
326
327        try
328        {
329            Properties props = (Properties)m_propertyCache.getFromCache( template, -1 );
330
331            if( props == null )
332            {
333                try
334                {
335                    props = getTemplateProperties( template );
336
337                    m_propertyCache.putInCache( template, props );
338                }
339                catch( IOException e )
340                {
341                    log.warn("IO Exception while reading template properties",e);
342
343                    return null;
344                }
345            }
346
347            return props.getProperty( key );
348        }
349        catch( NeedsRefreshException ex )
350        {
351            // FIXME
352            return null;
353        }
354    }
355*/
356    /**
357     *  Returns an absolute path to a given template.
358     */
359    private static String getPath( String template )
360    {
361        return "/"+DIRECTORY+"/"+template+"/";
362    }
363
364    /**
365     *   Lists the skins available under this template.  Returns an
366     *   empty Set, if there are no extra skins available.  Note that
367     *   this method does not check whether there is anything actually
368     *   in the directories, it just lists them.  This may change
369     *   in the future.
370     *
371     *   @param pageContext the JSP PageContext
372     *   @param template The template to search
373     *   @return Set of Strings with the skin names.
374     *   @since 2.3.26
375     */
376    public Set< String > listSkins( PageContext pageContext, String template )
377    {
378        String place = makeFullJSPName( template, SKIN_DIRECTORY );
379
380        ServletContext sContext = pageContext.getServletContext();
381
382        Set<String> skinSet = sContext.getResourcePaths( place );
383        TreeSet<String> resultSet = new TreeSet<>();
384
385        if( log.isDebugEnabled() ) log.debug( "Listings skins from "+place );
386
387        if( skinSet != null )
388        {
389            String[] skins = {};
390
391            skins = skinSet.toArray(skins);
392
393            for (int i = 0; i < skins.length; i++)
394            {
395                String[] s = StringUtils.split(skins[i], "/");
396
397                if (s.length > 2 && skins[i].endsWith("/"))
398                {
399                    String skinName = s[s.length - 1];
400                    resultSet.add(skinName);
401                    if (log.isDebugEnabled())
402                        log.debug("...adding skin '" + skinName + "'");
403                }
404            }
405        }
406
407        return resultSet;
408    }
409
410    /**
411     * List all installed i18n language properties by classpath searching for files like :
412     *    templates/default_*.properties
413     *    templates/default.properties
414     *
415     * @param pageContext
416     * @return map of installed Languages
417     * @since 2.7.x
418     */
419    public Map< String, String > listLanguages(PageContext pageContext)
420    {
421        Map< String, String > resultMap = new LinkedHashMap<>();
422        String clientLanguage = ((HttpServletRequest) pageContext.getRequest()).getLocale().toString();
423
424        List< String > entries = ClassUtil.classpathEntriesUnder( DIRECTORY );
425        for( String name : entries ) {
426            if ( name.equals( I18NRESOURCE_EN ) ||
427                    (name.startsWith( I18NRESOURCE_PREFIX ) && name.endsWith( I18NRESOURCE_SUFFIX ) ) )
428            {
429                if (name.equals( I18NRESOURCE_EN )) {
430                    name = I18NRESOURCE_EN_ID;
431                }    else {
432                    name = name.substring(I18NRESOURCE_PREFIX.length(), name.lastIndexOf(I18NRESOURCE_SUFFIX));
433                }
434                Locale locale = new Locale(name.substring(0, 2), ((name.indexOf("_") == -1) ? "" : name.substring(3, 5)));
435                String defaultLanguage = "";
436                if (clientLanguage.startsWith(name))
437                {
438                    defaultLanguage = LocaleSupport.getLocalizedMessage(pageContext, I18NDEFAULT_LOCALE);
439                }
440                resultMap.put(name, locale.getDisplayName(locale) + " " + defaultLanguage);
441            }
442        }
443
444        return resultMap;
445    }
446
447
448    /**
449     * List all available timeformats, read from the jspwiki.properties
450     *
451     * @param pageContext
452     * @return map of TimeFormats
453     * @since 2.7.x
454     */
455    public Map< String, String > listTimeFormats(PageContext pageContext)
456    {
457        WikiContext context = WikiContext.findContext( pageContext );
458        Properties props = m_engine.getWikiProperties();
459        ArrayList<String> tfArr = new ArrayList<>(40);
460        LinkedHashMap<String,String> resultMap = new LinkedHashMap<>();
461
462        /* filter timeformat properties */
463        for (Enumeration< ? > e = props.propertyNames(); e.hasMoreElements();)
464        {
465            String name = (String) e.nextElement();
466
467            if (name.startsWith(TIMEFORMATPROPERTIES))
468            {
469                tfArr.add(name);
470            }
471        }
472
473        /* fetch actual formats */
474        if (tfArr.size() == 0)  {/* no props found - make sure some default formats are avail */
475            tfArr.add("dd-MMM-yy");
476            tfArr.add("d-MMM-yyyy");
477            tfArr.add("EEE, dd-MMM-yyyy, zzzz");
478        } else {
479            Collections.sort(tfArr);
480
481            for (int i = 0; i < tfArr.size(); i++) {
482                tfArr.set(i, props.getProperty(tfArr.get(i)));
483            }
484        }
485
486        String prefTimeZone = Preferences.getPreference( context, "TimeZone" );
487        //TimeZone tz = TimeZone.getDefault();
488        TimeZone tz = TimeZone.getTimeZone(prefTimeZone);
489        /*try
490        {
491            tz.setRawOffset(Integer.parseInt(prefTimeZone));
492        }
493        catch (Exception e)
494        {
495        }*/
496
497        Date d = new Date(); // current date
498        try
499        {
500            // dummy format pattern
501            SimpleDateFormat fmt = Preferences.getDateFormat( context, TimeFormat.DATETIME );
502            fmt.setTimeZone(tz);
503
504            for (int i = 0; i < tfArr.size(); i++)
505            {
506                try
507                {
508                    String f = tfArr.get(i);
509                    fmt.applyPattern(f);
510
511                    resultMap.put(f, fmt.format(d));
512                }
513                catch (IllegalArgumentException e)
514                {
515                } // skip parameter
516            }
517        }
518        catch (IllegalArgumentException e)
519        {
520        } // skip parameter
521
522        return resultMap;
523    }
524
525    /**
526     * List all timezones, with special marker for server timezone
527     *
528     * @param pageContext
529     * @return map of TimeZones
530     * @since 2.7.x
531     */
532    public Map< String, String > listTimeZones(PageContext pageContext)
533    {
534        Map<String,String> resultMap = new LinkedHashMap<>();
535
536        String[][] tzs = { { "GMT-12", "Enitwetok, Kwajalien" },
537                          { "GMT-11", "Nome, Midway Island, Samoa" },
538                          { "GMT-10", "Hawaii" },
539                          { "GMT-9", "Alaska" },
540                          { "GMT-8", "Pacific Time" },
541                          { "GMT-7", "Mountain Time" },
542                          { "GMT-6", "Central Time, Mexico City" },
543                          { "GMT-5", "Eastern Time, Bogota, Lima, Quito" },
544                          { "GMT-4", "Atlantic Time, Caracas, La Paz" },
545                          { "GMT-3:30", "Newfoundland" },
546                          { "GMT-3", "Brazil, Buenos Aires, Georgetown, Falkland Is." },
547                          { "GMT-2", "Mid-Atlantic, Ascention Is., St Helena" },
548                          { "GMT-1", "Azores, Cape Verde Islands" },
549                          { "GMT", "Casablanca, Dublin, Edinburgh, London, Lisbon, Monrovia" },
550                          { "GMT+1", "Berlin, Brussels, Copenhagen, Madrid, Paris, Rome" },
551                          { "GMT+2", "Helsinki, Athens, Kaliningrad, South Africa, Warsaw" },
552                          { "GMT+3", "Baghdad, Riyadh, Moscow, Nairobi" },
553                          { "GMT+3:30", "Tehran" },
554                          { "GMT+4", "Adu Dhabi, Baku, Muscat, Tbilisi" },
555                          { "GMT+4:30", "Kabul" },
556                          { "GMT+5", "Islamabad, Karachi, Tashkent" },
557                          { "GMT+5:30", "Bombay, Calcutta, Madras, New Delhi" },
558                          { "GMT+6", "Almaty, Colomba, Dhakra" },
559                          { "GMT+7", "Bangkok, Hanoi, Jakarta" },
560                          { "GMT+8", "Beijing, Hong Kong, Perth, Singapore, Taipei" },
561                          { "GMT+9", "Osaka, Sapporo, Seoul, Tokyo, Yakutsk" },
562                          { "GMT+9:30", "Adelaide, Darwin" },
563                          { "GMT+10", "Melbourne, Papua New Guinea, Sydney, Vladivostok" },
564                          { "GMT+11", "Magadan, New Caledonia, Solomon Islands" },
565                          { "GMT+12", "Auckland, Wellington, Fiji, Marshall Island" } };
566
567        java.util.TimeZone servertz = java.util.TimeZone.getDefault();
568
569        for( int i = 0; i < tzs.length; i++ )
570        {
571            String tzID = tzs[i][0];
572            java.util.TimeZone tz = java.util.TimeZone.getTimeZone(tzID);
573
574            String serverTimeZone = "";
575
576            if( servertz.getRawOffset() == tz.getRawOffset() )
577            {
578                serverTimeZone = LocaleSupport.getLocalizedMessage(pageContext, I18NSERVER_TIMEZONE);
579                tzID = servertz.getID();
580            }
581
582            resultMap.put(tzID, "(" + tzs[i][0] + ") "+tzs[i][1] + " " + serverTimeZone);
583        }
584
585        return resultMap;
586    }
587
588    /**
589     *  Always returns a valid property map.
590     */
591    /*
592    private Properties getTemplateProperties( String templateName )
593        throws IOException
594    {
595        Properties p = new Properties();
596
597        ServletContext context = m_engine.getServletContext();
598
599        InputStream propertyStream = context.getResourceAsStream(getPath(templateName)+PROPERTYFILE);
600
601        if( propertyStream != null )
602        {
603            p.load( propertyStream );
604
605            propertyStream.close();
606        }
607        else
608        {
609            log.debug("Template '"+templateName+"' does not have a propertyfile '"+PROPERTYFILE+"'.");
610        }
611
612        return p;
613    }
614*/
615    /**
616     *  Returns the include resources marker for a given type.  This is in a
617     *  HTML or Javascript comment format.
618     *
619     *  @param context the wiki context
620     *  @param type the marker
621     *  @return the generated marker comment
622     */
623    public static String getMarker(WikiContext context, String type )
624    {
625        if( type.equals(RESOURCE_JSLOCALIZEDSTRINGS) )
626        {
627            return getJSLocalizedStrings( context );
628        }
629        else if( type.equals(RESOURCE_JSFUNCTION) )
630        {
631            return "/* INCLUDERESOURCES ("+type+") */";
632        }
633        return "<!-- INCLUDERESOURCES ("+type+") -->";
634    }
635
636    /**
637     *  Extract all i18n strings in the javascript domain. (javascript.*)
638     *  Returns a javascript snippet which defines the LocalizedStings array.
639     *
640     *  @param context the {@link WikiContext}
641     *  @return Javascript snippet which defines the LocalizedStrings array
642     *  @since 2.5.108
643     */
644    private static String getJSLocalizedStrings( WikiContext context )
645    {
646        StringBuilder sb = new StringBuilder();
647
648        sb.append( "var LocalizedStrings = {\n");
649
650        ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.DEF_TEMPLATE );
651
652        boolean first = true;
653
654        for( Enumeration< String > en = rb.getKeys(); en.hasMoreElements(); )
655        {
656            String key = en.nextElement();
657
658            if( key.startsWith("javascript") )
659            {
660                if( first )
661                {
662                    first = false;
663                }
664                else
665                {
666                    sb.append( ",\n" );
667                }
668                sb.append( "\""+key+"\":\""+rb.getString(key)+"\"" );
669            }
670        }
671        sb.append("\n};\n");
672
673        return( sb.toString() );
674    }
675
676    /**
677     *  Adds a resource request to the current request context.
678     *  The content will be added at the resource-type marker
679     *  (see IncludeResourcesTag) in WikiJSPFilter.
680     *  <p>
681     *  The resources can be of different types.  For RESOURCE_SCRIPT and RESOURCE_STYLESHEET
682     *  this is an URI path to the resource (a script file or an external stylesheet)
683     *  that needs to be included.  For RESOURCE_INLINECSS
684     *  the resource should be something that can be added between &lt;style>&lt;/style> in the
685     *  header file (commonheader.jsp).  For RESOURCE_JSFUNCTION it is the name of the Javascript
686     *  function that should be run at page load.
687     *  <p>
688     *  The IncludeResourceTag inserts code in the template files, which is then filled
689     *  by the WikiFilter after the request has been rendered but not yet sent to the recipient.
690     *  <p>
691     *  Note that ALL resource requests get rendered, so this method does not check if
692     *  the request already exists in the resources.  Therefore, if you have a plugin which
693     *  makes a new resource request every time, you'll end up with multiple resource requests
694     *  rendered.  It's thus a good idea to make this request only once during the page
695     *  life cycle.
696     *
697     *  @param ctx The current wiki context
698     *  @param type What kind of a request should be added?
699     *  @param resource The resource to add.
700     */
701    @SuppressWarnings("unchecked")
702    public static void addResourceRequest( WikiContext ctx, String type, String resource )
703    {
704        HashMap<String,Vector<String>> resourcemap = (HashMap<String,Vector<String>>) ctx.getVariable( RESOURCE_INCLUDES );
705
706        if( resourcemap == null )
707        {
708            resourcemap = new HashMap<>();
709        }
710
711        Vector<String> resources = resourcemap.get( type );
712
713        if( resources == null )
714        {
715            resources = new Vector<>();
716        }
717
718        String resourceString = null;
719
720        if( type.equals(RESOURCE_SCRIPT) )
721        {
722            resourceString = "<script type='text/javascript' src='"+resource+"'></script>";
723        }
724        else if( type.equals(RESOURCE_STYLESHEET) )
725        {
726            resourceString = "<link rel='stylesheet' type='text/css' href='"+resource+"' />";
727        }
728        else if( type.equals(RESOURCE_INLINECSS) )
729        {
730            resourceString = "<style type='text/css'>\n"+resource+"\n</style>\n";
731        }
732        else if( type.equals(RESOURCE_JSFUNCTION) )
733        {
734            resourceString = resource;
735        }
736        else if( type.equals(RESOURCE_HTTPHEADER) )
737        {
738            resourceString = resource;
739        }
740
741        if( resourceString != null )
742        {
743            resources.add( resourceString );
744        }
745
746        log.debug("Request to add a resource: "+resourceString);
747
748        resourcemap.put( type, resources );
749        ctx.setVariable( RESOURCE_INCLUDES, resourcemap );
750    }
751
752    /**
753     *  Returns resource requests for a particular type.  If there are no resources,
754     *  returns an empty array.
755     *
756     *  @param ctx WikiContext
757     *  @param type The resource request type
758     *  @return a String array for the resource requests
759     */
760
761    @SuppressWarnings("unchecked")
762    public static String[] getResourceRequests( WikiContext ctx, String type )
763    {
764        HashMap<String,Vector<String>> hm = (HashMap<String,Vector<String>>) ctx.getVariable( RESOURCE_INCLUDES );
765
766        if( hm == null ) return new String[0];
767
768        Vector<String> resources = hm.get( type );
769
770        if( resources == null ) return new String[0];
771
772        String[] res = new String[resources.size()];
773
774        return resources.toArray( res );
775    }
776
777    /**
778     *  Returns all those types that have been requested so far.
779     *
780     * @param ctx the wiki context
781     * @return the array of types requested
782     */
783    @SuppressWarnings("unchecked")
784    public static String[] getResourceTypes( WikiContext ctx )
785    {
786        String[] res = new String[0];
787
788        if( ctx != null )
789        {
790            HashMap<String,String> hm = (HashMap<String,String>) ctx.getVariable( RESOURCE_INCLUDES );
791
792            if( hm != null )
793            {
794                Set<String> keys = hm.keySet();
795
796                res = keys.toArray( res );
797            }
798        }
799
800        return res;
801    }
802
803    /**
804     *  Returns an empty collection, since at the moment the TemplateManager
805     *  does not manage any modules.
806     *
807     *  @return {@inheritDoc}
808     */
809    @Override
810    public Collection< WikiModuleInfo > modules()
811    {
812        return new ArrayList<>();
813    }
814
815    /**
816     *  Returns null!
817     *  {@inheritDoc}
818     */
819    @Override
820    public WikiModuleInfo getModuleInfo(String moduleName) {
821        return null;
822    }
823}