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.hd.org/. */
335 private static final long serialVersionUID = -200996533296387845L;
336 }