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