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.preferences; 020 021import com.google.gson.Gson; 022import org.apache.commons.lang3.LocaleUtils; 023import org.apache.commons.lang3.StringUtils; 024import org.apache.logging.log4j.LogManager; 025import org.apache.logging.log4j.Logger; 026import org.apache.wiki.InternalWikiException; 027import org.apache.wiki.api.core.Context; 028import org.apache.wiki.i18n.InternationalizationManager; 029import org.apache.wiki.util.HttpUtil; 030import org.apache.wiki.util.PropertyReader; 031import org.apache.wiki.util.TextUtil; 032 033import javax.servlet.http.HttpServletRequest; 034import javax.servlet.jsp.PageContext; 035import java.text.DateFormat; 036import java.text.SimpleDateFormat; 037import java.util.Date; 038import java.util.HashMap; 039import java.util.Locale; 040import java.util.Map; 041import java.util.MissingResourceException; 042import java.util.Properties; 043import java.util.ResourceBundle; 044import java.util.TimeZone; 045 046 047/** 048 * Represents an object which is used to store user preferences. 049 */ 050public class Preferences extends HashMap< String,String > { 051 052 private static final long serialVersionUID = 1L; 053 054 /** 055 * The name under which a Preferences object is stored in the HttpSession. Its value is {@value}. 056 */ 057 public static final String SESSIONPREFS = "prefs"; 058 059 public static final String COOKIE_USER_PREFS_NAME = "JSPWikiUserPrefs"; 060 061 private static final Logger LOG = LogManager.getLogger( Preferences.class ); 062 063 /** 064 * This is an utility method which is called to make sure that the 065 * JSP pages do have proper access to any user preferences. It should be 066 * called from the commonheader.jsp. 067 * <p> 068 * This method reads user cookie preferences and mixes them up with any 069 * default preferences (and in the future, any user-specific preferences) 070 * and puts them all in the session, so that they do not have to be rewritten 071 * again. 072 * <p> 073 * This method will remember if the user has already changed his prefs. 074 * 075 * @param pageContext The JSP PageContext. 076 */ 077 public static void setupPreferences( final PageContext pageContext ) { 078 //HttpSession session = pageContext.getSession(); 079 //if( session.getAttribute( SESSIONPREFS ) == null ) 080 //{ 081 reloadPreferences( pageContext ); 082 //} 083 } 084 085 /** 086 * Reloads the preferences from the PageContext into the WikiContext. 087 * 088 * @param pageContext The page context. 089 */ 090 // FIXME: The way that date preferences are chosen is currently a bit wacky: it all gets saved to the cookie based on the browser state 091 // with which the user happened to first arrive to the site with. This, unfortunately, means that even if the user changes e.g. 092 // language preferences (like in a web cafe), the old preferences still remain in a site cookie. 093 public static void reloadPreferences( final PageContext pageContext ) { 094 final Preferences prefs = new Preferences(); 095 final Properties props = PropertyReader.loadWebAppProps( pageContext.getServletContext() ); 096 final Context ctx = Context.findContext( pageContext ); 097 final String dateFormat = ctx.getEngine().getManager( InternationalizationManager.class ) 098 .get( InternationalizationManager.CORE_BUNDLE, getLocale( ctx ), "common.datetimeformat" ); 099 100 prefs.put("SkinName", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.skinname", "PlainVanilla" ) ); 101 prefs.put("DateFormat", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.dateformat", dateFormat ) ); 102 prefs.put("TimeZone", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.timezone", TimeZone.getDefault().getID() ) ); 103 prefs.put("Orientation", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.orientation", "fav-left" ) ); 104 prefs.put("Sidebar", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.sidebar", "active" ) ); 105 prefs.put("Layout", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.layout", "fluid" ) ); 106 prefs.put("Language", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.language", getLocale( ctx ).toString() ) ); 107 prefs.put("SectionEditing", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.sectionediting", "true" ) ); 108 prefs.put("Appearance", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.appearance", "true" ) ); 109 110 //editor cookies 111 prefs.put("autosuggest", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.autosuggest", "true" ) ); 112 prefs.put("tabcompletion", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.tabcompletion", "true" ) ); 113 prefs.put("smartpairs", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.smartpairs", "false" ) ); 114 prefs.put("livepreview", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.livepreview", "true" ) ); 115 prefs.put("previewcolumn", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.previewcolumn", "true" ) ); 116 117 118 // FIXME: editormanager reads jspwiki.editor -- which of both properties should continue 119 prefs.put("editor", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.editor", "plain" ) ); 120 parseJSONPreferences( (HttpServletRequest) pageContext.getRequest(), prefs ); 121 pageContext.getSession().setAttribute( SESSIONPREFS, prefs ); 122 } 123 124 125 /** 126 * Parses new-style preferences stored as JSON objects and stores them in the session. Everything in the cookie is stored. 127 * 128 * @param request 129 * @param prefs The default hashmap of preferences 130 */ 131 private static void parseJSONPreferences( final HttpServletRequest request, final Preferences prefs ) { 132 final String prefVal = TextUtil.urlDecodeUTF8( HttpUtil.retrieveCookieValue( request, COOKIE_USER_PREFS_NAME ) ); 133 if( prefVal != null ) { 134 // Convert prefVal JSON to a generic hashmap 135 @SuppressWarnings( "unchecked" ) final Map< String, String > map = new Gson().fromJson( prefVal, Map.class ); 136 for( String key : map.keySet() ) { 137 key = TextUtil.replaceEntities( key ); 138 // Sometimes this is not a String as it comes from the Cookie set by Javascript 139 final Object value = map.get( key ); 140 if( value != null ) { 141 prefs.put( key, value.toString() ); 142 } 143 } 144 } 145 } 146 147 /** 148 * Returns a preference value programmatically. 149 * FIXME 150 * 151 * @param wikiContext 152 * @param name 153 * @return the preference value 154 */ 155 public static String getPreference( final Context wikiContext, final String name ) { 156 final HttpServletRequest request = wikiContext.getHttpRequest(); 157 if ( request == null ) { 158 return null; 159 } 160 161 final Preferences prefs = (Preferences)request.getSession().getAttribute( SESSIONPREFS ); 162 if( prefs != null ) { 163 return prefs.get( name ); 164 } 165 166 return null; 167 } 168 169 /** 170 * Returns a preference value programmatically. 171 * FIXME 172 * 173 * @param pageContext 174 * @param name 175 * @return the preference value 176 */ 177 public static String getPreference( final PageContext pageContext, final String name ) { 178 final Preferences prefs = ( Preferences )pageContext.getSession().getAttribute( SESSIONPREFS ); 179 if( prefs != null ) { 180 return prefs.get( name ); 181 } 182 183 return null; 184 } 185 186 /** 187 * Get Locale according to user-preference settings or the user browser locale 188 * 189 * @param context The context to examine. 190 * @return a Locale object. 191 * @since 2.8 192 */ 193 public static Locale getLocale( final Context context ) { 194 Locale loc = null; 195 196 final String langSetting = getPreference( context, "Language" ); 197 198 // parse language and construct valid Locale object 199 if( langSetting != null ) { 200 String language = ""; 201 String country = ""; 202 String variant = ""; 203 204 final String[] res = StringUtils.split( langSetting, "-_" ); 205 final int resLength = res.length; 206 if( resLength > 2 ) { 207 variant = res[ 2 ]; 208 } 209 if( resLength > 1 ) { 210 country = res[ 1 ]; 211 } 212 if( resLength > 0 ) { 213 language = res[ 0 ]; 214 loc = new Locale( language, country, variant ); 215 } 216 } 217 218 // see if default locale is set server side 219 if( loc == null ) { 220 final String locale = context.getEngine().getWikiProperties().getProperty( "jspwiki.preferences.default-locale" ); 221 try { 222 loc = LocaleUtils.toLocale( locale ); 223 } catch( final IllegalArgumentException iae ) { 224 LOG.error( iae.getMessage() ); 225 } 226 } 227 228 // otherwise try to find out the browser's preferred language setting, or use the JVM's default 229 if( loc == null ) { 230 final HttpServletRequest request = context.getHttpRequest(); 231 loc = ( request != null ) ? request.getLocale() : Locale.getDefault(); 232 } 233 234 LOG.debug( "using locale " + loc.toString() ); 235 return loc; 236 } 237 238 /** 239 * Locates the i18n ResourceBundle given. This method interprets the request locale, and uses that to figure out which language the 240 * user wants. 241 * 242 * @param context {@link Context} holding the user's locale 243 * @param bundle The name of the bundle you are looking for. 244 * @return A localized string (or from the default language, if not found) 245 * @throws MissingResourceException If the bundle cannot be found 246 * @see org.apache.wiki.i18n.InternationalizationManager 247 */ 248 public static ResourceBundle getBundle( final Context context, final String bundle ) throws MissingResourceException { 249 final Locale loc = getLocale( context ); 250 final InternationalizationManager i18n = context.getEngine().getManager( InternationalizationManager.class ); 251 return i18n.getBundle( bundle, loc ); 252 } 253 254 /** 255 * Get SimpleTimeFormat according to user browser locale and preferred time formats. If not found, it will revert to whichever format 256 * is set for the default. 257 * 258 * @param context WikiContext to use for rendering. 259 * @param tf Which version of the dateformat you are looking for? 260 * @return A SimpleTimeFormat object which you can use to render 261 * @since 2.8 262 */ 263 public static SimpleDateFormat getDateFormat( final Context context, final TimeFormat tf ) { 264 final InternationalizationManager imgr = context.getEngine().getManager( InternationalizationManager.class ); 265 final Locale clientLocale = getLocale( context ); 266 final String prefTimeZone = getPreference( context, "TimeZone" ); 267 String prefDateFormat; 268 269 LOG.debug("Checking for preferences..."); 270 switch( tf ) { 271 case DATETIME: 272 prefDateFormat = getPreference( context, "DateFormat" ); 273 LOG.debug("Preferences fmt = "+prefDateFormat); 274 if( prefDateFormat == null ) { 275 prefDateFormat = imgr.get( InternationalizationManager.CORE_BUNDLE, clientLocale,"common.datetimeformat" ); 276 LOG.debug("Using locale-format = "+prefDateFormat); 277 } 278 break; 279 280 case TIME: 281 prefDateFormat = imgr.get( "common.timeformat" ); 282 break; 283 284 case DATE: 285 prefDateFormat = imgr.get( "common.dateformat" ); 286 break; 287 288 default: 289 throw new InternalWikiException( "Got a TimeFormat for which we have no value!" ); 290 } 291 292 try { 293 final SimpleDateFormat fmt = new SimpleDateFormat( prefDateFormat, clientLocale ); 294 if( prefTimeZone != null ) { 295 final TimeZone tz = TimeZone.getTimeZone( prefTimeZone ); 296 // TimeZone tz = TimeZone.getDefault(); 297 // tz.setRawOffset(Integer.parseInt(prefTimeZone)); 298 fmt.setTimeZone( tz ); 299 } 300 301 return fmt; 302 } catch( final Exception e ) { 303 return null; 304 } 305 } 306 307 /** 308 * A simple helper function to render a date based on the user preferences. This is useful for example for all plugins. 309 * 310 * @param context The context which is used to get the preferences 311 * @param date The date to render. 312 * @param tf In which format the date should be rendered. 313 * @return A ready-rendered date. 314 * @since 2.8 315 */ 316 public static String renderDate( final Context context, final Date date, final TimeFormat tf ) { 317 final DateFormat df = getDateFormat( context, tf ); 318 return df.format( date ); 319 } 320 321 /** 322 * Is used to choose between the different date formats that JSPWiki supports. 323 * <ul> 324 * <li>TIME: A time format, without date</li> 325 * <li>DATE: A date format, without a time</li> 326 * <li>DATETIME: A date format, with a time</li> 327 * </ul> 328 * 329 * @since 2.8 330 */ 331 public enum TimeFormat { 332 /** A time format, no date. */ 333 TIME, 334 335 /** A date format, no time. */ 336 DATE, 337 338 /** A date+time format. */ 339 DATETIME 340 } 341 342}