001: /*
002: * Copyright 2000,2005 wingS development team.
003: *
004: * This file is part of wingS (http://wingsframework.org).
005: *
006: * wingS is free software; you can redistribute it and/or modify
007: * it under the terms of the GNU Lesser General Public License
008: * as published by the Free Software Foundation; either version 2.1
009: * of the License, or (at your option) any later version.
010: *
011: * Please see COPYING for the complete licence.
012: */
013: package org.wings.session;
014:
015: import java.io.FileReader;
016: import java.io.LineNumberReader;
017: import java.io.Serializable;
018: import java.util.Locale;
019: import java.util.StringTokenizer;
020: import java.util.regex.Pattern;
021: import java.util.regex.Matcher;
022:
023: /**
024: * Detect the browser from the user-agent string passed in the HTTP header.
025: *
026: * @author <a href="mailto:andre@lison.de">Andre Lison</a>
027: */
028: public class Browser implements Serializable {
029: private String agent;
030: private int majorVersion;
031: private double minorVersion;
032: private String release;
033: private String os;
034: private OSType osType = OSType.UNKNOWN;
035: private String osVersion;
036: private String browserName;
037: private Locale browserLocale;
038: private BrowserType browserType = BrowserType.UNKNOWN;
039:
040: /**
041: * Create a new browser object and start scanning for
042: * browser, os and client language in given string.
043: *
044: * @param agent the "User-Agent" string from the request.
045: */
046: public Browser(String agent) {
047: this .agent = agent;
048: detect();
049: }
050:
051: /**
052: * Get the browser browserName (Mozilla, MSIE, Opera etc.).
053: */
054: public String getBrowserName() {
055: return browserName;
056: }
057:
058: /**
059: * Gets the classification of the browser, this can be either GECKO, IE, KONQUEROR, MOZILLA, OPERA or UNKNOWN.
060: *
061: * @return A classification of the browser {@link BrowserType}
062: */
063: public BrowserType getBrowserType() {
064: return browserType;
065: }
066:
067: /**
068: * Get the browser major version.
069: * <br>f.e. the major version for <i>Netscape 6.2</i> is <i>6</i>.
070: *
071: * @return the major version or <i>0</i> if not found
072: */
073: public int getMajorVersion() {
074: return majorVersion;
075: }
076:
077: /**
078: * Get the minor version. This is the number after the
079: * dot in the version string.
080: * <br>f.e. the minor version for <i>Netscape 6.01</i> is <i>0.01</i>.
081: *
082: * @return the minor version if found, <i>0</i> otherwise
083: */
084: public double getMinorVersion() {
085: return minorVersion;
086: }
087:
088: /**
089: * Get additional information about browser version.
090: * <br>f.e. the release for <i>MSIE 6.1b</i> is <i>b</i>.
091: *
092: * @return the release or <i>null</i>, if not available.
093: */
094: public String getRelease() {
095: return release;
096: }
097:
098: /**
099: * Get the operating system string provided by the browser. {@link OSType}
100: *
101: * @return the os browserName or <i>null</i>, if not available.
102: */
103: public String getOs() {
104: return os;
105: }
106:
107: /**
108: * Get the operating system version.
109: *
110: * @return the os version or <i>null</i>, if not available.
111: */
112: public String getOsVersion() {
113: return osVersion;
114: }
115:
116: /**
117: * Get the operating system type.
118: *
119: * @return A valid {@link OSType}
120: */
121: public OSType getOsType() {
122: return osType;
123: }
124:
125: /**
126: * Get the browser/client locale.
127: *
128: * @return the found locale or the default server locale
129: * specified by {@link Locale#getDefault} if not found.
130: */
131: public Locale getClientLocale() {
132: return browserLocale;
133: }
134:
135: /* regexps are not threadsafe, we have to create them. */
136: private transient Pattern RE_START;
137: private transient Pattern RE_MSIE;
138: private transient Pattern RE_MSIE_WIN_LANG_OS;
139: private transient Pattern RE_MSIE_MAC_LANG_OS;
140: private transient Pattern RE_NS_LANG_OS;
141: private transient Pattern RE_NS_X11_LANG_OS;
142: private transient Pattern RE_NS6_LANG_OS;
143: private transient Pattern RE_LANG;
144: private transient Pattern RE_LANG2;
145: private transient Pattern RE_OPERA;
146: private transient Pattern RE_OPERA8X_CLOAKED;
147: private transient Pattern RE_OPERA_LANG_OS;
148: private transient Pattern RE_KONQUEROR_OS;
149: private transient Pattern RE_GALEON_OS;
150: private transient Pattern RE_GECKO_ENGINE;
151:
152: /**
153: * Lazily create RE as they are not serializable. Will be null after session restore.
154: */
155: private void createREs() {
156: if (RE_START == null) {
157: RE_START = Pattern
158: .compile("^([a-zA-Z0-9_\\-]+)(/([0-9])\\.([0-9]+))?");
159: RE_MSIE = Pattern
160: .compile("MSIE\\s([0-9])\\.([0-9]+)([a-z])?");
161: RE_MSIE_WIN_LANG_OS = Pattern
162: .compile("[wW]in(dows)?\\s([A-Z0-9]+)\\s?([0-9]\\.[0-9])?(;\\s[UIN])?(;\\s(\\w{2}[_-]?\\w{0,2}))");
163: RE_MSIE_MAC_LANG_OS = Pattern.compile("Mac_PowerPC");
164: RE_NS_LANG_OS = Pattern
165: .compile("\\[([a-z-]+)\\][\\sa-zA-Z0-9-]*\\(([a-zA-Z\\-]+)/?([0-9]*\\s?[.a-zA-Z0-9\\s]*);");
166: RE_NS_X11_LANG_OS = Pattern
167: .compile("\\(X11;\\s[UIN];\\s([a-zA-Z-]+)\\s([0-9\\.]+)[^\\);]+\\)");
168: RE_NS6_LANG_OS = Pattern
169: .compile("\\(([a-zA-Z0-9]+);\\s[a-zA-Z]+;\\s([a-zA-Z0-9_]+)(\\s([a-zA-Z0-9]+))?;\\s([_a-zA-Z-]+);");
170: RE_LANG = Pattern.compile("\\[([_a-zA-Z-]+)\\]");
171: /* Search for any xx_XX or xx-XX */
172: RE_LANG2 = Pattern.compile("\\b(\\D{2}[_-]\\D{2})\\b");
173: RE_OPERA = Pattern
174: .compile("((;\\s)|\\()([a-zA-Z0-9\\-]+)[\\s]+([a-zA-Z0-9\\.]+)([^;\\)]*)(;\\s\\w+)?\\)\\sOpera\\s([0-9]+)\\.([0-9]+)[\\s]+\\[([_a-zA-Z-]+)\\]");
175: RE_OPERA8X_CLOAKED = Pattern
176: .compile("((;\\s)|\\()([a-zA-Z0-9\\-]+)[\\s]+([a-zA-Z0-9\\.\\s]+)([^;\\)]*)(;\\s[UIN])?;\\s([_a-zA-Z-]+)\\)\\sOpera\\s([0-9]+)\\.([0-9]+)");
177: RE_OPERA_LANG_OS = Pattern
178: .compile("\\((\\w+;\\s)?([a-zA-Z0-9\\-]+)\\s([\\w\\.\\s]+)(;\\s[UIN])?(;\\s(\\w{2}[-_]?\\w{0,2}))");
179: RE_KONQUEROR_OS = Pattern
180: .compile("Konqueror/([rc0-9\\.-]+);\\s([a-zA-Z0-9\\-]+)");
181: RE_GALEON_OS = Pattern
182: .compile("\\(([a-zA-Z0-9]+);\\s[UIN];\\sGaleon;\\s([0-9]+)\\.([0-9]+);");
183: RE_GECKO_ENGINE = Pattern
184: .compile("Gecko/[0-9]*(\\s([a-zA-Z]+)+[0-9]*/([0-9]+)\\.([0-9]+)([a-zA-Z0-9]*))?");
185: }
186: }
187:
188: /**
189: * That does all the work.
190: */
191: protected void detect() {
192: if (agent == null || agent.length() == 0) {
193: return;
194: }
195: String mav, miv, lang = null;
196:
197: createREs();
198:
199: final Matcher RE_START_MATCHER = RE_START.matcher(agent);
200: if (RE_START_MATCHER.find()) {
201: browserName = RE_START_MATCHER.group(1);
202: mav = RE_START_MATCHER.group(3);
203: miv = RE_START_MATCHER.group(4);
204:
205: /* RE_MSIE hides itself behind Mozilla or different browserName,
206: good idea, congratulation Bill !
207: */
208: final Matcher RE_MSIE_MATCHER = RE_MSIE.matcher(agent);
209: final Matcher RE_MSIE_WIN_LANG_OS_MATCHER = RE_MSIE_WIN_LANG_OS
210: .matcher(agent);
211: if (RE_MSIE_MATCHER.find()) {
212: browserName = "MSIE";
213: browserType = BrowserType.IE;
214: mav = RE_MSIE_MATCHER.group(1);
215: miv = RE_MSIE_MATCHER.group(2);
216: release = RE_MSIE_MATCHER.group(3);
217:
218: if (RE_MSIE_WIN_LANG_OS_MATCHER.find()) {
219: osType = OSType.WINDOWS;
220: os = "Windows";
221: osVersion = RE_MSIE_WIN_LANG_OS_MATCHER.group(2)
222: + (RE_MSIE_WIN_LANG_OS_MATCHER.group(3) == null ? ""
223: : " "
224: + RE_MSIE_WIN_LANG_OS_MATCHER
225: .group(3));
226: } else {
227: final Matcher RE_MSIE_MAC_LANG_MATCHER = RE_MSIE_MAC_LANG_OS
228: .matcher(agent);
229: if (RE_MSIE_MAC_LANG_MATCHER.find()) {
230: os = "MacOS";
231: osType = OSType.MACOS;
232: }
233: }
234: }
235: /* Mozilla has two different id's; one up to version 4
236: and a second for version >= 5
237: */
238: else if (browserName.equals("Mozilla")
239: || browserName == null) {
240: browserName = "Mozilla";
241: browserType = BrowserType.MOZILLA;
242:
243: /* old mozilla */
244: final Matcher RE_NS_LANG_OS_MATCHER = RE_NS_LANG_OS
245: .matcher(agent);
246: if (RE_NS_LANG_OS_MATCHER.find()) {
247: lang = RE_NS_LANG_OS_MATCHER.group(1);
248: os = RE_NS_LANG_OS_MATCHER.group(2);
249: osVersion = RE_NS_LANG_OS_MATCHER.group(3);
250:
251: if (os.equals("X")) {
252: final Matcher RE_NS_X11_LANG_OS_MATCHER = RE_NS_X11_LANG_OS
253: .matcher(agent);
254: if (RE_NS_X11_LANG_OS_MATCHER.find()) {
255: os = RE_NS_X11_LANG_OS_MATCHER.group(1);
256: osVersion = RE_NS_X11_LANG_OS_MATCHER
257: .group(2);
258: osType = OSType.UNIX;
259: }
260: }
261: }
262: /* NS5, NS6 Galeon etc. */
263: else {
264: final Matcher RE_GALEON_OS_MATCHER = RE_GALEON_OS
265: .matcher(agent);
266: if (RE_GALEON_OS_MATCHER.find()) {
267: browserName = "Galeon";
268: browserType = BrowserType.GECKO;
269: os = RE_GALEON_OS_MATCHER.group(1);
270: if (os.equals("X11")) {
271: os = "Unix";
272: osType = OSType.UNIX;
273: }
274: mav = RE_GALEON_OS_MATCHER.group(2);
275: miv = RE_GALEON_OS_MATCHER.group(3);
276: } else {
277: final Matcher RE_NS6_LANG_OS_MATCHER = RE_NS6_LANG_OS
278: .matcher(agent);
279: if (RE_NS6_LANG_OS_MATCHER.find()) {
280: os = RE_NS6_LANG_OS_MATCHER.group(2);
281: lang = RE_NS6_LANG_OS_MATCHER.group(5);
282: }
283: /* realy seldom but is there */
284: else {
285: if (RE_MSIE_WIN_LANG_OS_MATCHER.find()) {
286: os = "Windows";
287: osType = OSType.WINDOWS;
288: osVersion = RE_MSIE_WIN_LANG_OS_MATCHER
289: .group(2)
290: + (RE_MSIE_WIN_LANG_OS_MATCHER
291: .group(3) == null ? ""
292: : " "
293: + RE_MSIE_WIN_LANG_OS_MATCHER
294: .group(3));
295: }
296: /* Konqueror */
297: else {
298: final Matcher RE_KONQUEROR_OS_MATCHER = RE_KONQUEROR_OS
299: .matcher(agent);
300: if (RE_KONQUEROR_OS_MATCHER.find()) {
301: browserName = "Konqueror";
302: browserType = BrowserType.KONQUEROR;
303: StringTokenizer strtok = new StringTokenizer(
304: RE_KONQUEROR_OS_MATCHER
305: .group(1), ".");
306: mav = strtok.nextToken();
307: if (strtok.hasMoreTokens()) {
308: miv = strtok.nextToken();
309: }
310: if (strtok.hasMoreTokens()) {
311: release = strtok.nextToken();
312: }
313: os = RE_KONQUEROR_OS_MATCHER
314: .group(2);
315: }
316: /* f*ck, what's that ??? */
317: else {
318: browserName = "Mozilla";
319: browserType = BrowserType.MOZILLA;
320: }
321: }
322: }
323: }
324: }
325:
326: /* reformat browser os */
327: if (os != null
328: && os.startsWith("Win")
329: && (osVersion == null || osVersion.length() == 0)) {
330: osVersion = os.substring(3, os.length());
331: os = "Windows";
332: osType = OSType.WINDOWS;
333: }
334: /* just any windows */
335: if (os != null && os.equals("Win")) {
336: os = "Windows";
337: osType = OSType.WINDOWS;
338: }
339: }
340: /* Opera identified as opera, that's easy! */
341: else if (browserName.equals("Opera")) {
342: browserType = BrowserType.OPERA;
343: if (RE_MSIE_WIN_LANG_OS_MATCHER.find()) {
344: os = "Windows";
345: osType = OSType.WINDOWS;
346: osVersion = RE_MSIE_WIN_LANG_OS_MATCHER.group(2)
347: + (RE_MSIE_WIN_LANG_OS_MATCHER.group(3) == null ? ""
348: : " "
349: + RE_MSIE_WIN_LANG_OS_MATCHER
350: .group(3));
351: if (RE_MSIE_WIN_LANG_OS_MATCHER.group(6) != null)
352: lang = RE_MSIE_WIN_LANG_OS_MATCHER.group(6);
353: } else {
354: final Matcher RE_OPERA_LANG_OS_MATCHER = RE_OPERA_LANG_OS
355: .matcher(agent);
356: if (RE_OPERA_LANG_OS_MATCHER.find()) {
357: os = RE_OPERA_LANG_OS_MATCHER.group(2);
358: osVersion = RE_OPERA_LANG_OS_MATCHER.group(3);
359: lang = RE_OPERA_LANG_OS_MATCHER.group(6);
360: }
361: }
362: }
363:
364: /* Opera identified as something else (Mozilla, IE ...) */
365: final Matcher RE_OPERA_MATCHER = RE_OPERA.matcher(agent);
366: if (RE_OPERA_MATCHER.find()) {
367: browserName = "Opera";
368: browserType = BrowserType.OPERA;
369: os = RE_OPERA_MATCHER.group(3);
370: osVersion = RE_OPERA_MATCHER.group(4);
371: mav = RE_OPERA_MATCHER.group(7);
372: miv = RE_OPERA_MATCHER.group(8);
373: lang = RE_OPERA_MATCHER.group(9);
374: }
375:
376: /* Opera 8.xx identified as something else (Mozilla, IE ...) */
377: final Matcher RE_OPERA8X_CLOAKED_MATCHER = RE_OPERA8X_CLOAKED
378: .matcher(agent);
379: if (RE_OPERA8X_CLOAKED_MATCHER.find()) {
380: browserName = "Opera";
381: browserType = BrowserType.OPERA;
382: os = RE_OPERA8X_CLOAKED_MATCHER.group(3);
383: osVersion = RE_OPERA8X_CLOAKED_MATCHER.group(4);
384: mav = RE_OPERA8X_CLOAKED_MATCHER.group(8);
385: miv = RE_OPERA8X_CLOAKED_MATCHER.group(9);
386: lang = RE_OPERA8X_CLOAKED_MATCHER.group(7);
387: }
388:
389: /* detect gecko */
390: final Matcher RE_GECKO_ENGINE_MATCHER = RE_GECKO_ENGINE
391: .matcher(agent);
392: if (RE_GECKO_ENGINE_MATCHER.find()) {
393: browserType = BrowserType.GECKO;
394: if (RE_GECKO_ENGINE_MATCHER.group(2) != null) {
395: browserName = RE_GECKO_ENGINE_MATCHER.group(2);
396: }
397: if (RE_GECKO_ENGINE_MATCHER.group(3) != null) {
398: mav = RE_GECKO_ENGINE_MATCHER.group(3);
399: }
400: if (RE_GECKO_ENGINE_MATCHER.group(4) != null) {
401: miv = RE_GECKO_ENGINE_MATCHER.group(4);
402: }
403: if (RE_GECKO_ENGINE_MATCHER.group(5) != null) {
404: release = RE_GECKO_ENGINE_MATCHER.group(5);
405: }
406: }
407:
408: /* try to find language in uncommon places if not detected before */
409: if (lang == null) {
410: final Matcher RE_LANG_MATCHER = RE_LANG.matcher(agent);
411: if (RE_LANG_MATCHER.find()) {
412: lang = RE_LANG_MATCHER.group(1);
413: }
414: }
415: if (lang == null) {
416: final Matcher RE_LANG2_MATCHER = RE_LANG2
417: .matcher(agent);
418: if (RE_LANG2_MATCHER.find()) {
419: lang = RE_LANG2_MATCHER.group(1);
420: }
421: }
422:
423: try {
424: majorVersion = Integer.parseInt(mav);
425: } catch (NumberFormatException ex) {
426: majorVersion = 0;
427: }
428:
429: try {
430: minorVersion = Double.parseDouble("0." + miv);
431: } catch (NumberFormatException ex) {
432: minorVersion = 0f;
433: }
434:
435: if (lang == null) {
436: browserLocale = Locale.getDefault();
437: } else {
438: /* Mozilla does that, maybe any other browser too ? */
439: lang = lang.replace('-', '_');
440:
441: /* test for country extension */
442: StringTokenizer strtok = new StringTokenizer(lang, "_");
443: String l = strtok.nextToken();
444: if (strtok.hasMoreElements()) {
445: browserLocale = new Locale(l, strtok.nextToken());
446: } else {
447: browserLocale = new Locale(l, "");
448: }
449: }
450:
451: if (osType == OSType.UNKNOWN && os != null) {
452: if (os.equals("Windows")) {
453: osType = OSType.WINDOWS;
454: } else if (os.equals("MacOS")) {
455: osType = OSType.MACOS;
456: } else if (os.equals("Linux") || os.equals("AIX")
457: || os.equals("SunOS") || os.equals("HP-UX")
458: || os.equals("Solaris") || os.equals("BSD")) {
459: osType = OSType.UNIX;
460: } else if (os.equals("os")) {
461: osType = OSType.IBMOS;
462: }
463: }
464: }
465: }
466:
467: /**
468: * just for testing ...
469: */
470: public static void main(String[] args) {
471: try {
472: if (args.length != 1) {
473: System.err.println("Usage: java "
474: + Browser.class.getName() + " <agents file>");
475: return;
476: }
477: FileReader fi = new FileReader(args[0]);
478: LineNumberReader lnr = new LineNumberReader(fi);
479: String line;
480: while ((line = lnr.readLine()) != null) {
481: System.out.println(line);
482: System.out.println("\t" + new Browser(line).toString());
483: }
484: fi.close();
485: } catch (Exception ex) {
486: ex.printStackTrace();
487: }
488: }
489:
490: /**
491: * Get a full human readable representation of the browser.
492: */
493: public String toString() {
494: return browserName + " v" + (majorVersion + minorVersion)
495: + (release == null ? "" : "-" + release)
496: + " browsertype:[" + browserType + "], locale:"
497: + browserLocale + ", ostype:" + osType.getName()
498: + ": os:" + os + " osv:" + osVersion;
499: }
500:
501: }
|