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