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        //editor cookies
118        prefs.put("autosuggest", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.autosuggest", "true" ) );
119        prefs.put("tabcompletion", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.tabcompletion", "true" ) );
120        prefs.put("smartpairs", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.smartpairs", "false" ) );
121        prefs.put("livepreview", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.livepreview", "true" ) );
122        prefs.put("previewcolumn", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.previewcolumn", "true" ) );
123
124
125        // FIXME: editormanager reads jspwiki.editor -- which of both properties should continue
126        prefs.put("editor", TextUtil.getStringProperty( props, "jspwiki.defaultprefs.template.editor", "plain" ) );
127
128        parseJSONPreferences( (HttpServletRequest) pageContext.getRequest(), prefs );
129
130        pageContext.getSession().setAttribute( SESSIONPREFS, prefs );
131    }
132
133
134    /**
135     *  Parses new-style preferences stored as JSON objects and stores them
136     *  in the session.  Everything in the cookie is stored.
137     *
138     *  @param request
139     *  @param prefs The default hashmap of preferences
140     *
141     */
142    private static void parseJSONPreferences( HttpServletRequest request, Preferences prefs ) {
143        String prefVal = TextUtil.urlDecodeUTF8( HttpUtil.retrieveCookieValue( request, "JSPWikiUserPrefs" ) );
144
145        if( prefVal != null ) {
146            // Convert prefVal JSON to a generic hashmap
147            @SuppressWarnings("unchecked")
148            Map<String,String> map = new Gson().fromJson(prefVal, Map.class );
149
150            for (String key : map.keySet()) {
151                key = TextUtil.replaceEntities( key );
152                // Sometimes this is not a String as it comes from the Cookie set by Javascript
153                Object value = map.get(key);
154                if (value != null) {
155                    prefs.put( key, value.toString() );
156                }
157            }
158        }
159    }
160
161    /**
162     *  Returns a preference value programmatically.
163     *  FIXME
164     *
165     *  @param wikiContext
166     *  @param name
167     *  @return the preference value
168     */
169    public static String getPreference( WikiContext wikiContext, String name ) {
170        HttpServletRequest request = wikiContext.getHttpRequest();
171        if ( request == null ) return null;
172
173        Preferences prefs = (Preferences)request.getSession().getAttribute( SESSIONPREFS );
174
175        if( prefs != null ) {
176            return prefs.get( name );
177        }
178
179        return null;
180    }
181
182    /**
183     *  Returns a preference value programmatically.
184     *  FIXME
185     *
186     *  @param pageContext
187     *  @param name
188     *  @return the preference value
189     */
190    public static String getPreference( PageContext pageContext, String name )
191    {
192        Preferences prefs = (Preferences)pageContext.getSession().getAttribute( SESSIONPREFS );
193
194        if( prefs != null )
195            return prefs.get( name );
196
197        return null;
198    }
199
200
201    /**
202     * Get Locale according to user-preference settings or the user browser locale
203     *
204     * @param context The context to examine.
205     * @return a Locale object.
206     * @since 2.8
207     */
208    public static Locale getLocale( WikiContext context ) {
209        Locale loc = null;
210
211        String langSetting = getPreference( context, "Language" );
212
213        // parse language and construct valid Locale object
214        if( langSetting != null ) {
215            String language = "";
216            String country  = "";
217            String variant  = "";
218
219            String[] res = StringUtils.split( langSetting, "-_" );
220
221            if( res.length > 2 ) variant = res[2];
222            if( res.length > 1 ) country = res[1];
223
224            if( res.length > 0 ) {
225                language = res[0];
226
227                loc = new Locale( language, country, variant );
228            }
229        }
230
231        // see if default locale is set server side
232        if( loc == null ) {
233            String locale = context.getEngine().getWikiProperties().getProperty( "jspwiki.preferences.default-locale" );
234            try {
235                loc = LocaleUtils.toLocale( locale );
236            } catch( IllegalArgumentException iae ) {
237                log.error( iae.getMessage() );
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            HttpServletRequest request = context.getHttpRequest();
244            loc = ( request != null ) ? request.getLocale() : Locale.getDefault();
245        }
246
247        log.debug( "using locale "+loc.toString() );
248        return loc;
249    }
250
251    /**
252     *  Locates the i18n ResourceBundle given.  This method interprets
253     *  the request locale, and uses that to figure out which language the
254     *  user wants.
255     *  @see org.apache.wiki.i18n.InternationalizationManager
256     *  @param context {@link WikiContext} holding the user's locale
257     *  @param bundle  The name of the bundle you are looking for.
258     *  @return A localized string (or from the default language, if not found)
259     *  @throws MissingResourceException If the bundle cannot be found
260     */
261    public static ResourceBundle getBundle( WikiContext context, String bundle )
262        throws MissingResourceException
263    {
264        Locale loc = getLocale( context );
265        InternationalizationManager i18n = context.getEngine().getInternationalizationManager();
266        return i18n.getBundle( bundle, loc );
267    }
268
269    /**
270     *  Get SimpleTimeFormat according to user browser locale and preferred time
271     *  formats. If not found, it will revert to whichever format is set for the
272     *  default
273     *
274     *  @param context WikiContext to use for rendering.
275     *  @param tf Which version of the dateformat you are looking for?
276     *  @return A SimpleTimeFormat object which you can use to render
277     *  @since 2.8
278     */
279    public static SimpleDateFormat getDateFormat( WikiContext context, TimeFormat tf )
280    {
281        InternationalizationManager imgr = context.getEngine().getInternationalizationManager();
282        Locale clientLocale = getLocale( context );
283        String prefTimeZone = getPreference( context, "TimeZone" );
284        String prefDateFormat;
285
286        log.debug("Checking for preferences...");
287
288        switch( tf )
289        {
290            case DATETIME:
291                prefDateFormat = getPreference( context, "DateFormat" );
292                log.debug("Preferences fmt = "+prefDateFormat);
293                if( prefDateFormat == null )
294                {
295                    prefDateFormat = imgr.get( InternationalizationManager.CORE_BUNDLE,
296                                               clientLocale,
297                                               "common.datetimeformat" );
298                    log.debug("Using locale-format = "+prefDateFormat);
299                }
300                break;
301
302            case TIME:
303                prefDateFormat = imgr.get( "common.timeformat" );
304                break;
305
306            case DATE:
307                prefDateFormat = imgr.get( "common.dateformat" );
308                break;
309
310            default:
311                throw new InternalWikiException( "Got a TimeFormat for which we have no value!" );
312        }
313
314        try
315        {
316            SimpleDateFormat fmt = new SimpleDateFormat( prefDateFormat, clientLocale );
317
318            if( prefTimeZone != null )
319            {
320                TimeZone tz = TimeZone.getTimeZone( prefTimeZone );
321                // TimeZone tz = TimeZone.getDefault();
322                // tz.setRawOffset(Integer.parseInt(prefTimeZone));
323
324                fmt.setTimeZone( tz );
325            }
326
327            return fmt;
328        }
329        catch( Exception e )
330        {
331            return null;
332        }
333    }
334
335    /**
336     *  A simple helper function to render a date based on the user preferences.
337     *  This is useful for example for all plugins.
338     *
339     *  @param context  The context which is used to get the preferences
340     *  @param date     The date to render.
341     *  @param tf       In which format the date should be rendered.
342     *  @return A ready-rendered date.
343     *  @since 2.8
344     */
345    public static String renderDate( WikiContext context, Date date, TimeFormat tf )
346    {
347        DateFormat df = getDateFormat( context, tf );
348
349        return df.format( date );
350    }
351
352    /**
353     *  Is used to choose between the different date formats that JSPWiki supports.
354     *  <ul>
355     *   <li>TIME: A time format, without  date</li>
356     *   <li>DATE: A date format, without a time</li>
357     *   <li>DATETIME: A date format, with a time</li>
358     *  </ul>
359     *
360     *  @since 2.8
361     */
362    public enum TimeFormat
363    {
364        /** A time format, no date. */
365        TIME,
366
367        /** A date format, no time. */
368        DATE,
369
370        /** A date+time format. */
371        DATETIME
372    }
373}