001    /*
002    Copyright (c) 1996-2012, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    
030    package org.hd.d.pg2k.webSvr.util;
031    
032    import java.io.Serializable;
033    import java.net.MalformedURLException;
034    import java.net.URL;
035    import java.util.Locale;
036    import java.util.Map;
037    import java.util.SortedMap;
038    import java.util.TreeMap;
039    
040    import javax.servlet.http.HttpServletRequest;
041    import javax.servlet.http.HttpSession;
042    
043    import org.hd.d.pg2k.svrCore.CoreConsts;
044    import org.hd.d.pg2k.svrCore.HostUtils;
045    import org.hd.d.pg2k.svrCore.I18NTools;
046    import org.hd.d.pg2k.svrCore.Rnd;
047    import org.hd.d.pg2k.webSvr.exhibit.DataSourceBean;
048    
049    import ORG.hd.d.IsDebug;
050    
051    /**JavaBean to hold explicit session variables such as user-selected-locale.
052     * Error tolerant on being set (should be able to be done by a filter using reflection)
053     * and can spit out a set of non-default values to adjust or pass to another server instance.
054     * <p>
055     * Names of all (visible) properties start "sessionVar" to avoid collision with other
056     * HTML (GET/POST) parameter users.
057     * <p>
058     * Not thread-safe; access to an instance in a session should be synchronised on the session object.
059     */
060    public final class SessionVarBean implements Serializable, Cloneable
061        {
062        /**HTTP tag/parameter we use to indicate that session variable should be set to defaults.
063         * The parameter with this name can be given a random value as a "cache buster".
064         * <p>
065         * When this is set, session variables should be set to all defaults and then
066         * set to whatever other actual values are present,
067         * meaning that only changes from defaults need be sent.
068         * <p>
069         * This parameter is set to a fixed value for spiders to avoid presenting them
070         * with a spurious vast URI space to index.
071         */
072        public static final String KEY_sessionVar_CLEAR = "sessionVar";
073    
074        /**Immutable random token (shared with any clones/copies to save time) for cache-busting in URLs. */
075        private final int rndToken = Rnd.fastRnd.nextInt();
076    
077        /**Lite UI; if true we should run a "lite" UI for users on slow modems, etc, else full UI.
078         * Full UI (flag off) is the default.
079         */
080        private boolean sessionVarLiteUI;
081        public static final String KEY_sessionVarLiteUI = "sessionVarLiteUI";
082        public boolean isSessionVarLiteUI() { return(sessionVarLiteUI); }
083        public SessionVarBean setSessionVarLiteUI(final boolean sessionVarLiteUI)
084            { this.sessionVarLiteUI = sessionVarLiteUI; return(this); }
085    
086        /**Session locale: if non-null this overrides any locale information from elsewhere.
087         * No override (null) is the default.
088         * <p>
089         * Note that this field can only be set to Locales with a valid language code
090         * (ie two-letter lowercase)
091         * and an optional valid country code (ie two-letter uppercase) and no variant
092         * and that are found in our full supported-locales list.
093         * Note thae we do this validation since the Locale object does not (SHAME)!
094         * <p>
095         * We <em>ignore</em> attempts to set to an unsupported/unsafe/broken locale.
096         * <p>
097         * We rely on immutability of the Locale object here.
098         */
099        private Locale sessionVarLocale;
100        public static final String KEY_sessionVarLocale = "sessionVarLocale";
101        public Locale getSessionVarLocale() { return(sessionVarLocale); }
102        public SessionVarBean setSessionVarLocale(final Locale sessionVarLocale)
103            {
104            if((sessionVarLocale != null) && !I18NTools.LOCALES.contains(sessionVarLocale))
105                { return(this); /* Refuse/ignore the invalid "set" request. */ }
106            this.sessionVarLocale = sessionVarLocale;
107            return(this);
108            }
109    
110    
111        /**Make an independent clone of this object. */
112        @Override
113        public Object clone()
114            throws CloneNotSupportedException
115            {
116            // No shared-mutable state, so basic Object.clone() does the trick.
117            return(super.clone());
118            }
119    
120        /**Make an independent clone of this object, returning the correct type; never null.
121         * Typically used to get a modified version of the current state
122         * to form a URL to set the new value.
123         *
124         * @return  a non-null independent clone/copy of this instance
125         */
126        public SessionVarBean copy()
127            {
128            // No shared-mutable state, so basic Object.clone() does the trick.
129            try { return((SessionVarBean) clone()); }
130            // Should never throw an exception.
131            catch(final CloneNotSupportedException e) { throw new Error(e); }
132            }
133    
134    
135        /**Update session vars from request, saving result back to session if need be.
136         * This should be called early enough (before output is committed)
137         * that a session can be created if need be.
138         * <p>
139         * This should probably only be routinely applied to GET operations
140         * where it cannot accidentally gobble data such as in a file-upload POST.
141         * <p>
142         * If a "clear" tag is present then we ignore extant values and start from defaults.
143         */
144        public static void updateSessionVarsFromRequest(final HttpServletRequest request)
145            {
146            final String clParam = request.getParameter(KEY_sessionVar_CLEAR);
147            final boolean clearVars = (clParam != null) && !"".equals(clParam);
148            final SessionVarBean vars = clearVars ?
149                    new SessionVarBean() /* All defaults. */ :
150                    getExtantSessionVars(request, true) /* Adjust extant values. */ ;
151    
152            // Set the individual values.
153            final String liteUI = request.getParameter(KEY_sessionVarLiteUI);
154            if((liteUI != null) && !"".equals(liteUI)) { vars.setSessionVarLiteUI(Boolean.parseBoolean(liteUI)); }
155            final String locale = request.getParameter(KEY_sessionVarLocale);
156            // We will accept any explicitly-supported locales of form ll or ll_CC.
157            // We will NOT accept random/broken locale values for security/robustness.
158            if((locale != null) && !"".equals(locale) &&
159                            ((locale.length() == 2) || (locale.length() == 5)))
160                {
161                final Locale ul = new Locale(locale.substring(0, 2),
162                                             (locale.length() < 5) ? "" : locale.substring(3, 5));
163                if(I18NTools.LOCALES.contains(ul))
164                    { vars.setSessionVarLocale(ul); }
165                }
166    
167            // If any values are non-default and we don't have a session yet, then save.
168            // If we do have a session then save regardless (or if we can detect a change).
169            // This avoids us creating a session just to store default values
170            // which is unnecessary and may be intrusive or irritate the user.
171            if((request.getSession(false) != null) || !vars.getNonDefaultValues().isEmpty())
172                {
173                // Create session if need be...
174                final HttpSession session = request.getSession(true); // Force session creation.
175                synchronized(session)
176                    { session.setAttribute(ATTR_NAME_SVB, vars); }
177                }
178            }
179    
180        /**Generate new URL to set the desired state; never null.
181         * This generates a URL based on the current one
182         * with GET parameters added to set session values to those in this instance.
183         * <p>
184         * For brevity/efficiency of result,
185         * this returns the shortest unambiguous relative or full URL
186         * that can be used as an href in the current page to get to the desired page
187         * with the desired session values set.
188         * <p>
189         * This may drop any query component of the original URL.
190         * <p>
191         * This may drop or preserve any fragment/ref component of the original URL.
192         * <p>
193         * This involves resetting all implicit session values to default and then
194         * explicitly setting all non-default values with query parameters.
195         * <p>
196         * This also adds a "cache-busting" random component to help ensure that
197         * the browser or a proxy does not just replay an old value from cache.
198         * This is given a fixed value if the request appears to be from a spider
199         * to avoid creating a huge virtual page space.
200         * <p>
201         * This can optionally force a diversion to an optimal mirror for the current site,
202         * helping to ensure that the user's browser is correctly "pinned" to a server
203         * and thus will not "lose" the state
204         * (and will probably give better performance for the user and be cheaper to serve too).
205         *
206         * @return relative or full URL based on the current request; never null
207         */
208        public String makeSessionVarSetURL(final HttpServletRequest request,
209                                           final boolean attemptToPinToMirror,
210                                           final DataSourceBean vars)
211            {
212            if((request == null) || (vars == null))
213                { throw new IllegalArgumentException(); }
214    
215            final URL origURL;
216            try { origURL = new URL(request.getRequestURL().toString()); }
217            catch(final MalformedURLException e) { throw new IllegalArgumentException(e); }
218    
219            final int initialCapacity = 256; // Resulting URLs can be BIG!
220            final StringBuilder sb = new StringBuilder(initialCapacity);
221    
222            // Insert mirror host name if requested to pin to (any) mirror
223            // and on a normalised (non-master, non-mirror) URL
224            // and on a standard port (etc) so that this appears to be a "normal" access.
225            // Don't do this if the "generic" host is selected
226            // or the user is apparently already on the right mirror.
227            // All this reduces the size of the HTML generated as much as possible.
228            if(attemptToPinToMirror &&
229                    !HostUtils.isMirrorName(origURL.getHost()) &&
230                    !HostUtils.isMasterName(origURL.getHost()) &&
231                    (origURL.getPort() == -1))
232                {
233                final String selectedMirror = MirrorSelectionUtils.chooseMirrorHostForLowLatency(request, vars);
234                if(!CoreConsts.MAIN_DATA_HOST.equals(selectedMirror) &&
235                   !origURL.getHost().equals(selectedMirror))
236                    {
237                    sb.append(origURL.getProtocol()).append("://");
238                    sb.append(selectedMirror);
239                    // Only put in full path when creating an absolute/full URL.
240                    sb.append(origURL.getFile());
241                    }
242                }
243    
244            // Now append the query string,
245            // starting by resetting all var values to defaults and "cache busting",
246            // and then setting all non-default values.
247            sb.append('?').append(KEY_sessionVar_CLEAR).append('=');
248            if(WebUtils.requestProbablyFromSpider(request))
249                { sb.append("spider"); /* Fixed token to avoid trapping spider in huge page space. */ }
250            else
251                { sb.append(Integer.toString(rndToken >>> 1, Character.MAX_RADIX)); /* Random (non-negative high-radix integer) token to "cache bust". */ }
252    
253            final Map<String,String> vals = getNonDefaultValues();
254            for(final String key : vals.keySet())
255                {
256                sb.append('&').append(key).append('=');
257                // Assume value is non-null and does not require URL encoding for now.
258                final String value = vals.get(key);
259                sb.append(value);
260                }
261    
262            // Append any fragment component.
263            final String ref = origURL.getRef();
264            if((ref != null) && (ref.length() > 0)) { sb.append('#').append(ref); }
265    
266    if(IsDebug.isDebug && (sb.length() > initialCapacity)) { System.err.println("[WARNING: makeSessionVarSetURL result needed copy/resize: "+(sb.length())+" chars]"); }
267    
268            return(sb.toString());
269            }
270    
271    
272        /**Get (copy of) any existing value from the HTTP session; null if no session values and create is false.
273         * This is suitable to then override with user-specified values as required.
274         * <p>
275         * Note that this returns a private copy of any value held in the session.
276         *
277         * @param request  HTTP request object; never null
278         * @param create  if true and session or session variable is missing
279         *                then create and return a new all-default value,
280         *     else if false and session or session variable is missing then return null
281         */
282        public static SessionVarBean getExtantSessionVars(final HttpServletRequest request,
283                                                          final boolean create)
284            {
285            if(request == null)
286                { throw new IllegalArgumentException(); }
287    
288            final HttpSession s = request.getSession(false);
289    
290            // If we have a session AND the session attr exists AND is the right type
291            // then return a copy of it.
292            if(s != null)
293                {
294                final Object attr;
295                synchronized(s) { attr = s.getAttribute(ATTR_NAME_SVB); }
296                if(attr instanceof SessionVarBean)
297                    {
298                    final SessionVarBean svb = (SessionVarBean) attr;
299                    if(svb != null)
300                        { return(svb.copy()); }
301                    }
302                }
303    
304            // Could not find attr and not creating
305            // so return null.
306            if(!create) { return(null); }
307    
308            // Return new all-defaults instance.
309            return(new SessionVarBean());
310            }
311    
312        /**Get sorted map of attribute names to values for all non-default values; never null.
313         * Values are given as an HTML/HTTP-safe String.
314         * <p>
315         * If the result is not empty then at least one value is non-default
316         * and we should probably save this in the session (creating a session if need be).
317         * <p>
318         * The map is sorted to so that it is easier to dump keys into HTML in sorted order
319         * and thus help compression if we are doing any.
320         *
321         * @return non-null sorted map from attribute name to String representation of value
322         */
323        public SortedMap<String,String> getNonDefaultValues()
324            {
325            final SortedMap<String,String> result = new TreeMap<String, String>();
326            if(sessionVarLiteUI) { result.put(KEY_sessionVarLiteUI, "true"); }
327            if(sessionVarLocale != null) { result.put(KEY_sessionVarLocale, String.valueOf(sessionVarLocale)); }
328            return(result);
329            }
330    
331        /**Name of SessionVarBean instance in HttpSession. */
332        public static final String ATTR_NAME_SVB = "org.hd.d.pg2k.SessionVarBean";
333    
334        /**Unique Serialisation class ID generated by http://random&#46;hd&#46;org/. */
335        private static final long serialVersionUID = -200996533296387845L;
336        }