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