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 <style></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}