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.log4j.Logger;
022import org.apache.wiki.api.core.Context;
023import org.apache.wiki.i18n.InternationalizationManager;
024import org.apache.wiki.modules.ModuleManager;
025import org.apache.wiki.preferences.Preferences;
026import org.apache.wiki.util.ClassUtil;
027
028import javax.servlet.jsp.PageContext;
029import javax.servlet.jsp.jstl.fmt.LocaleSupport;
030import java.util.Enumeration;
031import java.util.HashMap;
032import java.util.LinkedHashMap;
033import java.util.List;
034import java.util.Locale;
035import java.util.Map;
036import java.util.ResourceBundle;
037import java.util.Set;
038import java.util.TimeZone;
039import java.util.Vector;
040
041
042/**
043 *  This class takes care of managing JSPWiki templates.  This class also provides the ResourceRequest mechanism.
044 *
045 *  @since 2.1.62
046 */
047public interface TemplateManager extends ModuleManager {
048
049    String SKIN_DIRECTORY = "skins";
050
051    /** Requests a JavaScript function to be called during window.onload. Value is {@value}. */
052    String RESOURCE_JSFUNCTION = "jsfunction";
053
054    /** Requests a JavaScript associative array with all localized strings. */
055    String RESOURCE_JSLOCALIZEDSTRINGS = "jslocalizedstrings";
056
057    /** Requests a stylesheet to be inserted. Value is {@value}. */
058    String RESOURCE_STYLESHEET = "stylesheet";
059
060    /** Requests a script to be loaded. Value is {@value}. */
061    String RESOURCE_SCRIPT = "script";
062
063    /** Requests inlined CSS. Value is {@value}. */
064    String RESOURCE_INLINECSS = "inlinecss";
065
066    /** The default directory for the properties. Value is {@value}. */
067    String DIRECTORY = "templates";
068
069    /** The name of the default template. Value is {@value}. */
070    String DEFAULT_TEMPLATE = "default";
071
072    /** Name of the file that contains the properties. */
073    String PROPERTYFILE = "template.properties";
074
075    /** Location of I18N Resource bundles, and path prefix and suffixes */
076    String I18NRESOURCE_PREFIX = "templates/default_";
077
078    String I18NRESOURCE_SUFFIX = ".properties";
079
080    /** The default (en) RESOURCE name and id. */
081    String I18NRESOURCE_EN = "templates/default.properties";
082    String I18NRESOURCE_EN_ID = "en";
083
084    /** I18N string to mark the default locale */
085    String I18NDEFAULT_LOCALE = "prefs.user.language.default";
086
087    /** I18N string to mark the server timezone */
088    String I18NSERVER_TIMEZONE = "prefs.user.timezone.server";
089
090    /** Prefix of the default timeformat properties. */
091    String TIMEFORMATPROPERTIES = "jspwiki.defaultprefs.timeformat.";
092
093    /** The name under which the resource includes map is stored in the  WikiContext. */
094    String RESOURCE_INCLUDES = "jspwiki.resourceincludes";
095
096    /** Requests a HTTP header. Value is {@value}. */
097    String RESOURCE_HTTPHEADER = "httpheader";
098
099    /**
100     *  Check the existence of a template.
101     */
102    boolean templateExists( String templateName );
103
104    /**
105     *  An utility method for finding a JSP page.  It searches only under either current context or by the absolute name.
106     *
107     *  @param pageContext the JSP PageContext
108     *  @param name The name of the JSP page to look for (e.g "Wiki.jsp")
109     *  @return The context path to the resource
110     */
111    String findJSP( PageContext pageContext, String name );
112
113    /**
114     *  Attempts to locate a resource under the given template.  If that template does not exist, or the page does not exist under that
115     *  template, will attempt to locate a similarly named file under the default template.
116     *  <p>
117     *  Even though the name suggests only JSP files can be located, but in fact this method can find also other resources than JSP files.
118     *
119     *  @param pageContext The JSP PageContext
120     *  @param template From which template we should seek initially?
121     *  @param name Which resource are we looking for (e.g. "ViewTemplate.jsp")
122     *  @return path to the JSP page; null, if it was not found.
123     */
124    String findJSP( PageContext pageContext, String template, String name );
125
126    /**
127     *  Attempts to locate a resource under the given template.  This matches the functionality findJSP(), but uses the WikiContext as
128     *  the argument.  If there is no servlet context (i.e. this is embedded), will just simply return a best-guess.
129     *  <p>
130     *  This method is typically used to locate any resource, including JSP pages, images, scripts, etc.
131     *
132     *  @since 2.6
133     *  @param ctx the wiki context
134     *  @param template the name of the template to use
135     *  @param name the name of the resource to fine
136     *  @return the path to the resource
137     */
138    String findResource( Context ctx, String template, String name );
139
140    /**
141     *   Lists the skins available under this template.  Returns an empty Set, if there are no extra skins available.  Note that
142     *   this method does not check whether there is anything actually in the directories, it just lists them.  This may change
143     *   in the future.
144     *
145     *   @param pageContext the JSP PageContext
146     *   @param template The template to search
147     *   @return Set of Strings with the skin names.
148     *   @since 2.3.26
149     */
150    Set< String > listSkins( PageContext pageContext, String template );
151
152    /**
153     * List all installed i18n language properties by classpath searching for files like :
154     *    templates/default_*.properties
155     *    templates/default.properties
156     *
157     * @param pageContext page context
158     * @return map of installed Languages
159     * @since 2.7.x
160     */
161    default Map< String, String > listLanguages( final PageContext pageContext ) {
162        final Map< String, String > resultMap = new LinkedHashMap<>();
163        final String clientLanguage = pageContext.getRequest().getLocale().toString();
164        final List< String > entries = ClassUtil.classpathEntriesUnder( DIRECTORY );
165        for( String name : entries ) {
166            if ( name.equals( I18NRESOURCE_EN ) || (name.startsWith( I18NRESOURCE_PREFIX ) && name.endsWith( I18NRESOURCE_SUFFIX ) ) ) {
167                if( name.equals( I18NRESOURCE_EN ) ) {
168                    name = I18NRESOURCE_EN_ID;
169                } else {
170                    name = name.substring( I18NRESOURCE_PREFIX.length(), name.lastIndexOf( I18NRESOURCE_SUFFIX ) );
171                }
172                final Locale locale = new Locale( name.substring( 0, 2 ), !name.contains( "_" ) ? "" : name.substring( 3, 5 ) );
173                String defaultLanguage = "";
174                if( clientLanguage.startsWith( name ) ) {
175                    defaultLanguage = LocaleSupport.getLocalizedMessage( pageContext, I18NDEFAULT_LOCALE );
176                }
177                resultMap.put( name, locale.getDisplayName( locale ) + " " + defaultLanguage );
178            }
179        }
180
181        return resultMap;
182    }
183
184
185    /**
186     * List all available timeformats, read from the jspwiki.properties
187     *
188     * @param pageContext page context
189     * @return map of TimeFormats
190     * @since 2.7.x
191     */
192    Map< String, String > listTimeFormats( final PageContext pageContext );
193
194    /**
195     * List all timezones, with special marker for server timezone
196     *
197     * @param pageContext page context
198     * @return map of TimeZones
199     * @since 2.7.x
200     */
201    default Map< String, String > listTimeZones( final PageContext pageContext ) {
202        final Map< String, String > resultMap = new LinkedHashMap<>();
203        final String[][] tzs = {
204                          { "GMT-12", "Enitwetok, Kwajalien" },
205                          { "GMT-11", "Nome, Midway Island, Samoa" },
206                          { "GMT-10", "Hawaii" },
207                          { "GMT-9", "Alaska" },
208                          { "GMT-8", "Pacific Time" },
209                          { "GMT-7", "Mountain Time" },
210                          { "GMT-6", "Central Time, Mexico City" },
211                          { "GMT-5", "Eastern Time, Bogota, Lima, Quito" },
212                          { "GMT-4", "Atlantic Time, Caracas, La Paz" },
213                          { "GMT-3:30", "Newfoundland" },
214                          { "GMT-3", "Brazil, Buenos Aires, Georgetown, Falkland Is." },
215                          { "GMT-2", "Mid-Atlantic, Ascention Is., St Helena" },
216                          { "GMT-1", "Azores, Cape Verde Islands" },
217                          { "GMT", "Casablanca, Dublin, Edinburgh, London, Lisbon, Monrovia" },
218                          { "GMT+1", "Berlin, Brussels, Copenhagen, Madrid, Paris, Rome" },
219                          { "GMT+2", "Helsinki, Athens, Kaliningrad, South Africa, Warsaw" },
220                          { "GMT+3", "Baghdad, Riyadh, Moscow, Nairobi" },
221                          { "GMT+3:30", "Tehran" },
222                          { "GMT+4", "Adu Dhabi, Baku, Muscat, Tbilisi" },
223                          { "GMT+4:30", "Kabul" },
224                          { "GMT+5", "Islamabad, Karachi, Tashkent" },
225                          { "GMT+5:30", "Bombay, Calcutta, Madras, New Delhi" },
226                          { "GMT+6", "Almaty, Colomba, Dhakra" },
227                          { "GMT+7", "Bangkok, Hanoi, Jakarta" },
228                          { "GMT+8", "Beijing, Hong Kong, Perth, Singapore, Taipei" },
229                          { "GMT+9", "Osaka, Sapporo, Seoul, Tokyo, Yakutsk" },
230                          { "GMT+9:30", "Adelaide, Darwin" },
231                          { "GMT+10", "Melbourne, Papua New Guinea, Sydney, Vladivostok" },
232                          { "GMT+11", "Magadan, New Caledonia, Solomon Islands" },
233                          { "GMT+12", "Auckland, Wellington, Fiji, Marshall Island" } };
234
235        final TimeZone servertz = TimeZone.getDefault();
236        for( final String[] strings : tzs ) {
237            String tzID = strings[ 0 ];
238            final TimeZone tz = TimeZone.getTimeZone( tzID );
239            String serverTimeZone = "";
240            if( servertz.getRawOffset() == tz.getRawOffset() ) {
241                serverTimeZone = LocaleSupport.getLocalizedMessage( pageContext, I18NSERVER_TIMEZONE );
242                tzID = servertz.getID();
243            }
244
245            resultMap.put( tzID, "(" + strings[ 0 ] + ") " + strings[ 1 ] + " " + serverTimeZone );
246        }
247
248        return resultMap;
249    }
250
251    /**
252     *  Returns the include resources marker for a given type.  This is in a
253     *  HTML or Javascript comment format.
254     *
255     *  @param context the wiki context
256     *  @param type the marker
257     *  @return the generated marker comment
258     */
259    static String getMarker( final Context context, final String type ) {
260        if( type.equals( RESOURCE_JSLOCALIZEDSTRINGS ) ) {
261            return getJSLocalizedStrings( context );
262        } else if( type.equals( RESOURCE_JSFUNCTION ) ) {
263            return "/* INCLUDERESOURCES ("+type+") */";
264        }
265        return "<!-- INCLUDERESOURCES ("+type+") -->";
266    }
267
268    /**
269     *  Extract all i18n strings in the javascript domain. (javascript.*) Returns a javascript snippet which defines the LocalizedStings array.
270     *
271     *  @param context the {@link Context}
272     *  @return Javascript snippet which defines the LocalizedStrings array
273     *  @since 2.5.108
274     */
275    static String getJSLocalizedStrings( final Context context ) {
276        final StringBuilder sb = new StringBuilder();
277        sb.append( "var LocalizedStrings = {\n");
278        final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.DEF_TEMPLATE );
279        boolean first = true;
280
281        for( final Enumeration< String > en = rb.getKeys(); en.hasMoreElements(); ) {
282            final String key = en.nextElement();
283            if( key.startsWith("javascript") ) {
284                if( first ) {
285                    first = false;
286                } else {
287                    sb.append( ",\n" );
288                }
289                sb.append( "\"" ).append( key ).append( "\":\"" ).append( rb.getString( key ) ).append( "\"" );
290            }
291        }
292        sb.append("\n};\n");
293
294        return( sb.toString() );
295    }
296
297    /**
298     *  Adds a resource request to the current request context. The content will be added at the resource-type marker
299     *  (see IncludeResourcesTag) in WikiJSPFilter.
300     *  <p>
301     *  The resources can be of different types.  For RESOURCE_SCRIPT and RESOURCE_STYLESHEET this is an URI path to the resource
302     *  (a script file or an external stylesheet) that needs to be included.  For RESOURCE_INLINECSS the resource should be something
303     *  that can be added between &lt;style>&lt;/style> in the header file (commonheader.jsp).  For RESOURCE_JSFUNCTION it is the name
304     *  of the Javascript function that should be run at page load.
305     *  <p>
306     *  The IncludeResourceTag inserts code in the template files, which is then filled by the WikiFilter after the request has been
307     *  rendered but not yet sent to the recipient.
308     *  <p>
309     *  Note that ALL resource requests get rendered, so this method does not check if the request already exists in the resources.
310     *  Therefore, if you have a plugin which makes a new resource request every time, you'll end up with multiple resource requests
311     *  rendered.  It's thus a good idea to make this request only once during the page life cycle.
312     *
313     *  @param ctx The current wiki context
314     *  @param type What kind of a request should be added?
315     *  @param resource The resource to add.
316     */
317    static void addResourceRequest( final Context ctx, final String type, final String resource ) {
318        HashMap< String, Vector< String > > resourcemap = ctx.getVariable( RESOURCE_INCLUDES );
319        if( resourcemap == null ) {
320            resourcemap = new HashMap<>();
321        }
322
323        Vector< String > resources = resourcemap.get( type );
324        if( resources == null ) {
325            resources = new Vector<>();
326        }
327
328        String resourceString = null;
329        switch( type ) {
330        case RESOURCE_SCRIPT:
331            resourceString = "<script type='text/javascript' src='" + resource + "'></script>";
332            break;
333        case RESOURCE_STYLESHEET:
334            resourceString = "<link rel='stylesheet' type='text/css' href='" + resource + "' />";
335            break;
336        case RESOURCE_INLINECSS:
337            resourceString = "<style type='text/css'>\n" + resource + "\n</style>\n";
338            break;
339        case RESOURCE_JSFUNCTION:
340        case RESOURCE_HTTPHEADER:
341            resourceString = resource;
342            break;
343        }
344
345        if( resourceString != null ) {
346            resources.add( resourceString );
347        }
348
349        Logger.getLogger( TemplateManager.class ).debug( "Request to add a resource: " + resourceString );
350
351        resourcemap.put( type, resources );
352        ctx.setVariable( RESOURCE_INCLUDES, resourcemap );
353    }
354
355    /**
356     *  Returns resource requests for a particular type.  If there are no resources, returns an empty array.
357     *
358     *  @param ctx WikiContext
359     *  @param type The resource request type
360     *  @return a String array for the resource requests
361     */
362    static String[] getResourceRequests( final Context ctx, final String type ) {
363        final HashMap< String, Vector< String > > hm = ctx.getVariable( RESOURCE_INCLUDES );
364        if( hm == null ) {
365            return new String[0];
366        }
367
368        final Vector<String> resources = hm.get( type );
369        if( resources == null ){
370            return new String[0];
371        }
372
373        final String[] res = new String[resources.size()];
374        return resources.toArray( res );
375    }
376
377    /**
378     *  Returns all those types that have been requested so far.
379     *
380     * @param ctx the wiki context
381     * @return the array of types requested
382     */
383    static String[] getResourceTypes( final Context ctx ) {
384        String[] res = new String[0];
385        if( ctx != null ) {
386            final HashMap< String, String > hm = ctx.getVariable( RESOURCE_INCLUDES );
387            if( hm != null ) {
388                final Set< String > keys = hm.keySet();
389                res = keys.toArray( res );
390            }
391        }
392
393        return res;
394    }
395
396}