Skip to content

Commit 795afc5

Browse files
committed
* Refactored code so it only exposes as little as possible
* updated pom.xml to meet sonatype requirements - updated README.md
1 parent 2370459 commit 795afc5

17 files changed

+401
-207
lines changed

README.md

+10-4
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,25 @@ The BrowsCap version currently shipped is: 6022.
1212
* platform (e.g. Android, iOS, Win7, Win10)
1313
* platformVersion (e.g. 4.2, 10 depending on what the platform is)
1414
* The fields are not configurable.
15+
* The CSV file is read in a streaming way, so it's processed line by line. This makes it more memory efficient than loading the whole into memory first.
16+
17+
## Future
18+
Possible new features we're thinking of (and are not yet present):
19+
* Make the fields configurable and let Capabilities return a Map containing the given fields
20+
* Auto-update the BrowsCap CSV or use an InputStream to use an alternative CSV file.
1521

1622
## Usage
1723
```java
24+
import com.blueconic.browscap.Capabilities;
25+
import com.blueconic.browscap.ParseException;
1826
import com.blueconic.browscap.UserAgentParser;
1927
import com.blueconic.browscap.UserAgentService;
20-
import com.blueconic.browscap.domain.Capabilities;
2128

2229
// ... class definition
2330

24-
final UserAgentService userAgentService = new UserAgentService();
25-
final UserAgentParser parser = userAgentService.loadParser();
31+
final UserAgentParser parser = new UserAgentService().loadParser(); // handle IOException and ParseException
2632

27-
// parser can be re-used
33+
// parser can be re-used for multiple lookup calls
2834
final String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36";
2935
final Capabilities capabilities = parser.parse(userAgent);
3036

pom.xml

+90
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,33 @@
66
<packaging>jar</packaging>
77
<version>1.0.0</version>
88
<name>browscap-java</name>
9+
<description>A blazingly fast and memory efficient Java client on top of the BrowsCap CSV source files.</description>
10+
11+
<licenses>
12+
<license>
13+
<name>MIT</name>
14+
<url>https://opensource.org/licenses/MIT</url>
15+
</license>
16+
</licenses>
17+
18+
<developers>
19+
<developer>
20+
<name>Paul Rütter</name>
21+
<email>[email protected]</email>
22+
<organization>BlueConic</organization>
23+
<organizationUrl>https://www.blueconic.com</organizationUrl>
24+
</developer>
25+
</developers>
26+
27+
<scm>
28+
<connection>scm:git:[email protected]:blueconic/browscap-java.git</connection>
29+
<developerConnection>scm:git:[email protected]:blueconic/browscap-java.git</developerConnection>
30+
<url>https://github.com/blueconic/browscap-java</url>
31+
</scm>
32+
933
<url>http://www.blueconic.com</url>
1034
<properties>
35+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1136
<maven.compiler.source>1.8</maven.compiler.source>
1237
<maven.compiler.target>1.8</maven.compiler.target>
1338
</properties>
@@ -24,4 +49,69 @@
2449
<scope>test</scope>
2550
</dependency>
2651
</dependencies>
52+
<build>
53+
<plugins>
54+
<plugin>
55+
<groupId>org.apache.maven.plugins</groupId>
56+
<artifactId>maven-gpg-plugin</artifactId>
57+
<version>1.6</version>
58+
<executions>
59+
<execution>
60+
<id>sign-artifacts</id>
61+
<phase>verify</phase>
62+
<goals>
63+
<goal>sign</goal>
64+
</goals>
65+
</execution>
66+
</executions>
67+
</plugin>
68+
69+
<plugin>
70+
<groupId>org.sonatype.plugins</groupId>
71+
<artifactId>nexus-staging-maven-plugin</artifactId>
72+
<version>1.6.3</version>
73+
<extensions>true</extensions>
74+
<configuration>
75+
<serverId>ossrh</serverId>
76+
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
77+
<stagingProfileId>19375019933d12</stagingProfileId>
78+
<autoReleaseAfterClose>true</autoReleaseAfterClose>
79+
</configuration>
80+
</plugin>
81+
82+
<plugin>
83+
<groupId>org.apache.maven.plugins</groupId>
84+
<artifactId>maven-source-plugin</artifactId>
85+
<version>3.0.1</version>
86+
<executions>
87+
<execution>
88+
<id>attach-sources</id>
89+
<goals>
90+
<goal>jar</goal>
91+
</goals>
92+
</execution>
93+
</executions>
94+
</plugin>
95+
96+
<plugin>
97+
<groupId>org.apache.maven.plugins</groupId>
98+
<artifactId>maven-javadoc-plugin</artifactId>
99+
<version>2.10.4</version>
100+
<executions>
101+
<execution>
102+
<id>attach-javadocs</id>
103+
<goals>
104+
<goal>jar</goal>
105+
</goals>
106+
</execution>
107+
</executions>
108+
<configuration>
109+
<additionalparam>-Xdoclint:none</additionalparam>
110+
</configuration>
111+
</plugin>
112+
113+
</plugins>
114+
115+
</build>
116+
27117
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.blueconic.browscap;
2+
3+
public interface Capabilities {
4+
String UNKNOWN_BROWSCAP_VALUE = "Unknown";
5+
6+
/**
7+
* Returns the browser value (e.g. Chrome)
8+
* @return the browser
9+
*/
10+
String getBrowser();
11+
12+
/**
13+
* Returns the browser type (e.g. Browser or Application)
14+
* @return the browser type
15+
*/
16+
String getBrowserType();
17+
18+
/**
19+
* Returns the major version of the browser (e.g. 55 in case of Chrome)
20+
* @return the browser major version
21+
*/
22+
String getBrowserMajorVersion();
23+
24+
/**
25+
* Returns the platform name (e.g. Android, iOS, Win7, Win10)
26+
* @return the platform
27+
*/
28+
String getPlatform();
29+
30+
/**
31+
* Returns the platform version (e.g. 4.2, 10 depending on what the platform is)
32+
* @return the platform version
33+
*/
34+
String getPlatformVersion();
35+
36+
/**
37+
* Returns the device type (e.g. Mobile Phone, Desktop, Tablet, Console, TV Device)
38+
* @return the device type
39+
*/
40+
String getDeviceType();
41+
42+
}

src/main/java/com/blueconic/browscap/exception/ParseException.java renamed to src/main/java/com/blueconic/browscap/ParseException.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
package com.blueconic.browscap.exception;
1+
package com.blueconic.browscap;
22

33
/**
4-
* Exception which is thrown when a regular expression cannot be parsed
4+
* Exception which is thrown when a regular expression in the BrowsCap CSV cannot be parsed
55
*/
66
public class ParseException extends Exception {
77
public ParseException(final String message) {
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,11 @@
11
package com.blueconic.browscap;
22

3-
import static java.util.Arrays.asList;
4-
5-
import java.util.ArrayList;
6-
import java.util.BitSet;
7-
import java.util.Comparator;
8-
import java.util.List;
9-
import java.util.function.Predicate;
10-
11-
import com.blueconic.browscap.domain.Capabilities;
12-
13-
/**
14-
* This class is responsible for determining the best matching useragent rule to determine the properties for a
15-
* useragent string.
16-
*/
17-
public class UserAgentParser {
18-
19-
// Common substrings to filter irrelevant rules and speed up processing
20-
static final String[] COMMON = {"-", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "profile", "player",
21-
"compatible", "android", "google", "tab", "transformer", "lenovo", "micro", "edge", "safari", "opera",
22-
"chrome", "firefox", "msie", "chromium", "cpu os ", "cpu iphone os ", "windows nt ", "mac os x ", "linux",
23-
"bsd", "windows phone", "iphone", "pad", "blackberry", "nokia", "alcatel", "ucbrowser", "mobile", "ie",
24-
"mercury", "samsung", "browser", "wow64", "silk", "lunascape", "crios", "epiphany", "konqueror", "version",
25-
"rv:", "build", "bot", "like gecko", "applewebkit", "trident", "mozilla", "windows nt 4", "windows nt 5.0",
26-
"windows nt 5.1", "windows nt 5.2", "windows nt 6.0", "windows nt 6.1", "windows nt 6.2", "windows nt 6.3",
27-
"windows nt 10.0", "android?4.0", "android?4.1", "android?4.2", "android?4.3", "android?4.4", "android?2.3",
28-
"android?5"};
29-
30-
// Common prefixes to filter irrelevant rules and speed up processing
31-
static final String[] FILTER_PREFIXES = {"mozilla/5.0", "mozilla/4"};
32-
33-
// All useragent rule ordered by size and alphabetically
34-
private final Rule[] myRules;
35-
36-
// Filters for filtering irrelevant rules and speed up processing
37-
private final Filter[] myFilters;
38-
39-
/**
40-
* Creates a new parser based on a collection of rules.
41-
* @param rules The rules, ordered by priority
42-
*/
43-
UserAgentParser(final Rule[] rules) {
44-
myRules = getOrderedRules(rules);
45-
myFilters = buildFilters();
46-
}
3+
public interface UserAgentParser {
474

485
/**
49-
* Parses a User-Agent header value.
6+
* Parses a User-Agent header value into a Capabilities object.
507
* @param userAgent The user agent
518
* @return The capabilities of the best matching rule
529
*/
53-
public Capabilities parse(final String userAgent) {
54-
55-
final SearchableString searchString = new SearchableString(userAgent.toLowerCase());
56-
57-
final BitSet includes = getIncludeRules(searchString, myFilters);
58-
59-
for (int i = includes.nextSetBit(0); i >= 0; i = includes.nextSetBit(i + 1)) {
60-
final Rule rule = myRules[i];
61-
if (rule.matches(searchString)) {
62-
return rule.getCapabilities();
63-
}
64-
}
65-
66-
return Capabilities.DEFAULT;
67-
}
68-
69-
BitSet getIncludeRules(final SearchableString searchString, final Filter[] filters) {
70-
71-
final BitSet excludes = new BitSet(myRules.length);
72-
for (final Filter filter : filters) {
73-
filter.applyExcludes(searchString, excludes);
74-
}
75-
76-
// Convert flip the excludes to determine the includes
77-
final BitSet includes = excludes;
78-
includes.flip(0, myRules.length);
79-
return includes;
80-
}
81-
82-
// Sort by size and alphabet, so the first match can be returned immediately
83-
static Rule[] getOrderedRules(final Rule[] rules) {
84-
final Comparator<Rule> c = Comparator.comparing(Rule::getSize).reversed().thenComparing(Rule::getPattern);
85-
86-
final List<Rule> orderedRules = new ArrayList<>(asList(rules));
87-
orderedRules.sort(c);
88-
return orderedRules.toArray(new Rule[0]);
89-
}
90-
91-
Filter[] buildFilters() {
92-
93-
final List<Filter> result = new ArrayList<>();
94-
95-
// Build filters for specific prefix constraints
96-
for (final String pattern : FILTER_PREFIXES) {
97-
result.add(createPrefixFilter(pattern));
98-
}
99-
100-
// Build filters for specific contains constraints
101-
for (final String pattern : COMMON) {
102-
result.add(createContainsFilter(pattern));
103-
}
104-
105-
return result.toArray(new Filter[0]);
106-
}
107-
108-
Filter createContainsFilter(final String pattern) {
109-
final Literal literal = new Literal(pattern);
110-
111-
final Predicate<SearchableString> pred = c -> c.getIndices(literal).length > 0;
112-
113-
final Predicate<Rule> matches = rule -> rule.requires(pattern);
114-
115-
return new Filter(pred, matches);
116-
}
117-
118-
Filter createPrefixFilter(final String pattern) {
119-
final Literal literal = new Literal(pattern);
120-
121-
final Predicate<SearchableString> pred = s -> s.startsWith(literal);
122-
123-
final Predicate<Rule> matches = rule -> {
124-
final Literal prefix = rule.getPrefix();
125-
return prefix != null && prefix.toString().startsWith(pattern);
126-
};
127-
128-
return new Filter(pred, matches);
129-
}
130-
131-
/**
132-
* Filter expression to can exclude a number of rules if a useragent doesn't meet it's predicate.
133-
*/
134-
class Filter {
135-
136-
private final Predicate<SearchableString> myUserAgentPredicate;
137-
private final BitSet myMask;
138-
139-
/**
140-
* Creates a the filter.
141-
* @param userAgentPredicate The predicate for matching user agents.
142-
* @param patternPredicate The corresponding predicate for matching rule
143-
*/
144-
Filter(final Predicate<SearchableString> userAgentPredicate, final Predicate<Rule> patternPredicate) {
145-
myUserAgentPredicate = userAgentPredicate;
146-
myMask = new BitSet(myRules.length);
147-
for (int i = 0; i < myRules.length; i++) {
148-
if (patternPredicate.test(myRules[i])) {
149-
myMask.set(i);
150-
}
151-
}
152-
}
153-
154-
void applyExcludes(final SearchableString userAgent, final BitSet resultExcludes) {
155-
if (!myUserAgentPredicate.test(userAgent)) {
156-
resultExcludes.or(myMask);
157-
}
158-
}
159-
}
10+
Capabilities parse(String userAgent);
16011
}

src/main/java/com/blueconic/browscap/UserAgentService.java

+10-9
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,32 @@
88
import java.util.zip.ZipEntry;
99
import java.util.zip.ZipInputStream;
1010

11-
import com.blueconic.browscap.exception.ParseException;
11+
import com.blueconic.browscap.impl.UserAgentFileParser;
1212

13+
/**
14+
* Service that manages the creation of user agent parsers. In the feature, this might be expanded so it also supports
15+
* auto-updating or use a given InputStream
16+
*/
1317
public class UserAgentService {
1418
// The version of the browscap file this bundle depends on
1519
private static final int BUNDLED_BROWSCAP_VERSION = 6022;
16-
private UserAgentParser myParser;
1720

21+
/**
22+
* Returns a parser based on the bundled BrowsCap version
23+
* @return the user agent parser
24+
*/
1825
public UserAgentParser loadParser() throws IOException, ParseException {
19-
2026
// http://browscap.org/version-number
2127
final String csvFileName = "browscap-" + BUNDLED_BROWSCAP_VERSION + ".zip";
22-
2328
try (final InputStream zipStream = getClass().getClassLoader().getResourceAsStream(csvFileName);
2429
final ZipInputStream zipIn = new ZipInputStream(zipStream)) {
25-
2630
final ZipEntry entry = zipIn.getNextEntry();
2731
if (!entry.isDirectory()) {
28-
if (myParser == null) {
29-
myParser = new UserAgentFileParser().parse(new InputStreamReader(zipIn, UTF_8));
30-
}
32+
return new UserAgentFileParser().parse(new InputStreamReader(zipIn, UTF_8));
3133
} else {
3234
throw new IOException("Unable to find BrowsCap entry: " + csvFileName);
3335
}
3436
}
3537

36-
return myParser;
3738
}
3839
}

0 commit comments

Comments
 (0)