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