The BrowserDatabase Class
So far we can represent a section of the browscap.ini file. Our next need is to read and parse the file
itself, construct a number of BrowserPattern objects as necessary, and search and weigh them to
match a particular browser's USER-AGENT string. This is the task of our BrowserDatabase class,
which represents the database of web clients loaded from the file:
package com.wrox.browser;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Iterator;
class BrowserDatabase {
We naturally need to store our BrowserPatterns. Often we just need to be able to iterate through
them, but when we have a pattern with a parent property we will need to be able to look up a
BrowserPattern by name. In this case we will store them in a HashMap, with the USER-AGENT
pattern as the key. We can easily obtain a Collection containing just the BrowserPatterns by
calling HashMap's values() method. We also keep a reference to the default browser, since we will
often require access to it:
private HashMap browserMap = new HashMap();
private BrowserPattern defaultBrowserPattern;
The BrowserDatabase constructor is where the fun begins. In an effort to reduce coupling between
BrowserDatabase and the servlet environment, the constructor takes as a parameter an
InputStream that must point to the browscap.ini data. This means that the BrowserDatabase
class itself is agnostic as to where the browser data comes from. It may well be loaded from a file, but
could also be loaded across the network, or even generated dynamically:
protected BrowserDatabase(InputStream is) throws BrowserException {
We read the data one line at a time, constructing BrowserPattern instances as required. When we
read a line there are three possibilities:
-
It is a comment or blank line
-
It starts a new section, being enclosed within [ and ] characters
-
It specifies a property name-value pair for the current browser
To keep track of this, we keep a reference to the current BrowserPattern as we go through the file:
BrowserPattern currentBrowser = null;
We start by opening the browscap.ini file for reading:
BufferedReader reader =
new BufferedReader(new InputStreamReader(is));
try {
String line, trimLine;
while ((line = reader.readLine()) != null) {
trimLine = line.trim();
if ((trimLine.equals("")) || (trimLine.startsWith(";"))) {
continue;
}
If we are starting a new section, we create a new BrowserPattern and keep it as the current pattern:
if ((trimLine.startsWith("[")) && (trimLine.endsWith("]"))) {
String browserLine =
trimLine.substring(1, trimLine.length()-1);
currentBrowser = new BrowserPattern(browserLine);
The new BrowserPattern can then be stashed away in our HashMap. We use the USER-AGENT
string (browserLine) as the key since that is the term we want to be able to use to look
BrowserPatterns up:
browserMap.put(browserLine, currentBrowser);
continue;
}
A browser property line is treated by separating it into the name and value parts, and storing that in the
current BrowserPattern. We take care to convert all property names to lower case, and to check for
property values starting with a # character (denoting an integer value):
if (currentBrowser != null) {
int equalsSign = trimLine.indexOf('=');
if (equalsSign != -1) {
String name = trimLine.substring(0,equalsSign)
.toLowerCase();
String value = trimLine.substring(equalsSign+1,
trimLine.length());
if (value.charAt(0) == '#') {
value = value.substring(1);
}
currentBrowser.addProperty(name, value);
}
}
}
Finally, we must remember to close the input data:
reader.close();
is.close();
} catch (IOException e) {
throw new BrowserException
("Problem reading browser capabilities file");
}
and retrieve our reference to the default browser settings:
defaultBrowserPattern = (BrowserPattern)
browserMap.get("Default Browser Capability Settings");
if (defaultBrowserPattern == null) {
throw new BrowserException("No default browser settings found");
}
}
With our database constructed, we can search it. This functionality is split into two methods:
-
performSearch()
Takes a USER-AGENT string and performs the actual search through the database, returning
the BrowserPattern that best matches it.
-
lookup()
Returns a BrowserInfo object representing that browser, suitable for being made available
to the rest of a web application. If a parent property is specified, its properties will be copied
into the BrowserInfo object.
performSearch() implements the rules for matching a browser USER-AGENT string against the
patterns specified in browscap.ini: first check for an exact match; if there is none, return the pattern
that has the smallest number of characters matching wildcards:
private BrowserPattern performSearch(String userAgent) {
We start by iterating through the BrowserPatterns, checking for an exact match. If we find one, we
simply return it:
Iterator entries = browserMap.values().iterator();
while (entries.hasNext()) {
BrowserPattern currentEntry = (BrowserPattern) entries.next();
if (currentEntry.matchExact(userAgent)) {
return currentEntry;
}
}
If there was no exact match, we must instead look for the best match involving wildcards:
entries = browserMap.values().iterator();
BrowserPattern bestPattern = null;
int bestScore = BrowserPattern.NO_MATCH;
while (entries.hasNext()) {
BrowserPattern currentEntry = (BrowserPattern) entries.next();
If a particular BrowserPattern doesn't have any wildcards, there's obviously no need to try:
if (!currentEntry.usesRE()) {
continue;
}
Otherwise, we check how good a match it was and compare with the previous known best match:
int currentScore = currentEntry.matchRE(userAgent);
if (currentScore < bestScore) {
bestScore = currentScore;
bestPattern = currentEntry;
}
}
return bestPattern;
}
Finally, the lookup() method is used to convert an USER-AGENT into a BrowserInfo object. This is
the method that our filter will call when a request comes in. Starting with the default browser properties,
we first check to see whether it has a parent property and if so add the parent's properties, then add
the properties of the BrowserPattern that matches best:
protected BrowserInfo lookup(String userAgent) {
Load the default browser properties:
BrowserInfo theBrowser =
new BrowserInfo(defaultBrowserPattern.getProperties());
We need to check just in case someone fails to give us a USER-AGENT at all in that case, we simply
return the default settings:
if (userAgent == null) {
return theBrowser;
}
For completeness, we store the USER-AGENT string in the BrowserInfo object:
theBrowser.addProperty("useragent", userAgent);
Next, we invoke performSearch() to try to match the USER-AGENT:
BrowserPattern thePattern = performSearch(userAgent);
If we didn't find anything we just return the defaults:
if (thePattern == null) {
return theBrowser;
}
If we have a parent entry, add its properties:
String parentName = thePattern.getProperty("parent");
if (parentName != null) {
BrowserPattern parentEntry =
(BrowserPattern) browserMap.get(parentName);
theBrowser.addProperties(parentEntry.getProperties());
}
Finally, we add our specific properties to the BrowserInfo object and return it:
theBrowser.addProperties(thePattern.getProperties());
return theBrowser;
}
}
New on the Java Boutique:
New Review:
Time Management Made Easy with the Quartz Enterprise Job Scheduler
Why not just use the Java timer API? This open source scheduling
API boasts simplicity, ease-of-integration, a well-rounded feature
set, and it's free!
New Applet:
Reverse Complement
Reverse Complement is a simple applet that converts DNA or RNA
sequences into three useful formats.
Elsewhere on internet.com:
WebDeveloper Java
Lots of Java information on webdeveloper.com
WDVL Java
Thorough Java resource at the Web Developer's Virtual Library.
ScriptSearch Java
Hundreds of free Java code files to download.
jGuru: Your View of the Java Universe
Customizable portal with online training, FAQs, regular news updates, and tutorials.
|