    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}