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