Simple Grails Browser Detection

Print This Post Print This Post

Web Developers love it when browser vendors (*cough* Microsoft *cough*) don’t follow standards, either through incompetence, laziness, or whatever other reason. In addition to that, each browser has their own selection of specific “improvements”, additions to css styles, element types, etc.

Either way, it’s often helpful for a web application to know what browser it’s dealing with so as to specially tailor content, whether to maintain a standard look and feel, or to offer extra eye candy.

The Service

I put the browser detection code into a service for no other reason than to easily use it across controllers and taglibs. This service will not work outside of a browser request context. I start with a simple prototype service that brings in the web request and session objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.web.context.request.RequestContextHolder as RCH 
 
class WebTierService {
 
    boolean transactional = false
 
    static scope = "prototype"
 
    def getRequest()
    {
    	return RCH.currentRequestAttributes().currentRequest
    }
 
    def getSession()
    {
    	return RCH.currentRequestAttributes().session 
    }
 
}

The web tier prototype service is then extended by:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
 
class UserAgentIdentService extends WebTierService {
 
    public final static String CHROME = "chrome"
    public final static String FIREFOX = "firefox"
    public final static String SAFARI = "safari"
    public final static String OTHER = "other"
    public final static String MSIE = "msie"
    public final static String UNKNOWN = "unknown"
    public final static String BLACKBERRY = "blackberry"
    public final static String SEAMONKEY = "seamonkey"
 
    public final static int CLIENT_CHROME = 0
    public final static int CLIENT_FIREFOX = 1
    public final static int CLIENT_SAFARI = 2
    public final static int CLIENT_OTHER = 3
    public final static int CLIENT_MSIE = 4
    public final static int CLIENT_UNKNOWN = 5
    public final static int CLIENT_BLACKBERRY = 6
    public final static int CLIENT_SEAMONKEY = 7
 
    boolean transactional = false
 
 
    def getUserAgentTag()
    {
        getRequest().getHeader("user-agent")
    }
 
    def getUserAgentInfo()
    {    
 
        def userAgent = getUserAgentTag()
 
        def agentInfo = getRequest().getSession().getAttribute("myapp.service.UserAgentIdentService.agentInfo")
        if (agentInfo != null && agentInfo.agentString == userAgent) {
            return agentInfo
        } else if (agentInfo != null && agentInfo.agentString != userAgent) {
            log.warn "User agent string has changed in a single session!"
            log.warn "Previous User Agent: ${agentInfo.agentString}"
            log.warn "New User Agent: ${userAgent}"
            log.warn "Discarding existing agent info and creating new..."
        } else {
            log.debug "User agent info does not exist in session scope, creating..."
        }
 
        agentInfo = [:]
 
 
 
 
        def browserVersion
        def browserType
        def operatingSystem
 
        def platform
        def security = "unknown"
        def language = "en-US"
 
        if (userAgent == null) {
            agentInfo.browserType = UserAgentIdentService.CLIENT_UNKNOWN
            return agentInfo
        }
 
        browserType = UserAgentIdentService.CLIENT_OTHER;
 
        int pos = -1;
        if ((pos = userAgent.indexOf("Firefox")) >= 0) {
            browserType = UserAgentIdentService.CLIENT_FIREFOX;
            browserVersion = userAgent.substring(pos + 8).trim();
            if (browserVersion.indexOf(" ") > 0)
                browserVersion = browserVersion.substring(0, browserVersion.indexOf(" "));
            log.debug("Browser type: Firefox " + browserVersion);
        } 
        if ((pos = userAgent.indexOf("Chrome")) >= 0) {
            browserType = UserAgentIdentService.CLIENT_CHROME;
            browserVersion = userAgent.substring(pos + 7).trim();
            if (browserVersion.indexOf(" ") > 0)
                browserVersion = browserVersion.substring(0, browserVersion.indexOf(" "));
            log.debug("Browser type: Chrome " + browserVersion);
 
        } 
        if ((pos = userAgent.indexOf("Safari")) >= 0 && (userAgent.indexOf("Chrome") == -1)) {
            browserType = UserAgentIdentService.CLIENT_SAFARI;
            browserVersion = userAgent.substring(pos + 7).trim();
            if (browserVersion.indexOf(" ") > 0)
                browserVersion = browserVersion.substring(0, browserVersion.indexOf(" "));
            log.debug("Browser type: Safari " + browserVersion);
 
        } 
        if ((pos = userAgent.indexOf("BlackBerry")) >= 0) {
            browserType = UserAgentIdentService.CLIENT_BLACKBERRY;
            browserVersion = userAgent.substring(userAgent.indexOf("/")).trim();
            if (browserVersion.indexOf(" ") > 0)
                browserVersion = browserVersion.substring(0, browserVersion.indexOf(" "));
            log.debug("Browser type: BlackBerry " + browserVersion);
 
        } 
        if ((pos = userAgent.indexOf("SeaMonkey")) >= 0) {
            browserType = UserAgentIdentService.CLIENT_SEAMONKEY;
            browserVersion = userAgent.substring(userAgent.indexOf("/")).trim();
            if (browserVersion.indexOf(" ") > 0)
                browserVersion = browserVersion.substring(0, browserVersion.indexOf(" "));
            log.debug("Browser type: SeaMonkey " + browserVersion);
 
        } 
        if ((pos = userAgent.indexOf("MSIE")) >= 0) {
            browserType = UserAgentIdentService.CLIENT_MSIE;
            browserVersion = userAgent.substring(pos + 5).trim();
            if (browserVersion.indexOf(" ") > 0)
                browserVersion = browserVersion.substring(0, browserVersion.indexOf(" "));
            if (browserVersion.indexOf(";") > 0)
                browserVersion = browserVersion.substring(0, browserVersion.indexOf(";"));
            log.debug("Browser type: MSIE " + browserVersion);
 
        }
 
 
        if (userAgent.indexOf("(") > 0) {
            String osInfo = userAgent.substring(userAgent.indexOf("(") + 1);
            osInfo = osInfo.substring(0, osInfo.indexOf(")"));
 
            String[] infoParts = osInfo.split("; ");
            platform = infoParts[0];
            operatingSystem = infoParts[2];
 
            if (browserType != UserAgentIdentService.CLIENT_MSIE) {
                if (infoParts[1].equals("U"))
                    security = "strong";
                if (infoParts[1].equals("I"))
                    security = "weak";
                if (infoParts[1].equals("N"))
                    security = "none";
 
                language = infoParts[3];
 
            }
 
        } else {
            if (browserType == UserAgentIdentService.CLIENT_BLACKBERRY) {
                operatingSystem = "BlackBerry " + browserVersion;
            }
        }
 
        agentInfo.browserVersion = browserVersion
        agentInfo.browserType = browserType
        agentInfo.operatingSystem = operatingSystem
        agentInfo.platform = platform
        agentInfo.security = security
        agentInfo.language = language
        agentInfo.agentString = userAgent
 
 
        getRequest().getSession().setAttribute("myapp.service.UserAgentIdentService.agentInfo", agentInfo)
        return agentInfo
    }
 
 
    public boolean isChrome()
    {
        return (getUserAgentInfo().browserType == UserAgentIdentService.CLIENT_CHROME);
    }
 
    public boolean isFirefox()
    {
        return (getUserAgentInfo().browserType == UserAgentIdentService.CLIENT_FIREFOX);
    }
 
    public boolean isMsie()
    {
        return (getUserAgentInfo().browserType == UserAgentIdentService.CLIENT_MSIE);
    }
 
    public boolean isOther()
    {
        return (getUserAgentInfo().browserType == UserAgentIdentService.CLIENT_OTHER);
    }
 
    public boolean isSafari()
    {
        return (getUserAgentInfo().browserType == UserAgentIdentService.CLIENT_SAFARI);
    }
 
    public boolean isBlackberry()
    {
        return (getUserAgentInfo().browserType == UserAgentIdentService.CLIENT_BLACKBERRY);
    }
 
    public boolean isSeamonkey()
    { 
        return (getUserAgentInfo().browserType == UserAgentIdentService.CLIENT_SEAMONKEY);
    }
 
 
    public String getBrowserVersion()
    {
        return getUserAgentInfo().browserVersion;
    }
 
    public String getOperatingSystem()
    {
        return getUserAgentInfo().operatingSystem;
    }
 
    public String getPlatform()
    {
        return getUserAgentInfo().platform;
    }
 
    public String getSecurity()
    {
        return getUserAgentInfo().security;
    }
 
    public String getLanguage()
    {
        return getUserAgentInfo().language;
    }
 
    public String getBrowserType()
    {
        switch (getUserAgentInfo().browserType) {
        case CLIENT_FIREFOX:
            return FIREFOX;
        case CLIENT_CHROME:
            return CHROME;
        case CLIENT_SAFARI:
            return SAFARI;
        case CLIENT_SEAMONKEY:
            return SEAMONKEY;
        case CLIENT_MSIE:
            return MSIE;
        case CLIENT_BLACKBERRY:
            return BLACKBERRY;
        case CLIENT_OTHER:
        case CLIENT_UNKNOWN:
        default:
            return OTHER;
        }
    }
}

What’s going on in this service, and I left some extras in, is the detection and validation of the remote browser as well as the storing of the browser information in the user’s session. I decided to store the info in the session so I don’t need to keep detecting the browser on each request (but I do make sure it hasn’t changed!).

Not all browsers are represented here as I targeted those I saw the most often on my application. Adding additional browsers (i.e. Opera, Safari on Android or the iPhone, etc…) is relatively simple. Also, this app does not receive search engine spider traffic, so it has not been tested against those types of agent strings.

Most of the blocks of the detection function tends to differ slightly due to subtle variations in individual agent strings. For example, I found it necessary to dance around Safari and Chrome as they both share very similar strings (they share a rendering engine in WebKit). Internet Explorer user agent strings are a mess and differ greatly from all others (surprised?).

The TagLib

Implementing a taglib is pretty straight forward:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class GlobalTagLib {
 
    static namespace = "wthr"
    def userAgentIdentService
     def isMsie = { attrs, body ->
        if (userAgentIdentService.isMsie()) {
            out << body()
        }
    }
 
    def isFirefox = { attrs, body ->
        if (userAgentIdentService.isFirefox()) {
            out << body()
        }
    }
 
    def isChrome = { attrs, body ->
        if (userAgentIdentService.isChrome()) {
            out << body()
        }
    }
 
 
    def isBlackberry = { attrs, body ->
        if (userAgentIdentService.isBlackberry()) {
            out << body()
        }    
    }
}

The example simply exposes some of the boolean is* functions of the service as GSP tags allowing content contained in the elements to only be sent to the intended browsers:

1
2
3
4
5
6
7
8
9
10
11
12
    <wthr:isMsie>
        You are using Internet Explorer
    </wthr:isMsie>
    <wthr:isFirefox>
        You are using Firefox
    </wthr:isFirefox>
    <wthr:isChrome>
        You are using Google Chrome
    </wthr:isChrome>
    <wthr:isBlackberry>
        You are using the Blackberry browser
    </wthr:isBlackberry>

Comments are closed.