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