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