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