/******************************************************************************* * Copyright 2012 André Rouél * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package net.sf.uadetector; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; import net.sf.qualitycheck.Check; /** * This class is used to detect version information within <i>User-Agent</i> strings. * * @author André Rouél */ final class VersionParser { /** * Index number of the group in a matching {@link Pattern} which contains the extension/suffix of a version string */ private static final int EXTENSION_INDEX = 5; /** * Index number of the group in a matching {@link Pattern} which contains the first/major number of a version string */ private static final int MAJOR_INDEX = 1; /** * Regular expression to analyze a version number separated by a dot */ private static final Pattern VERSIONNUMBER = Pattern.compile("((\\d+)((\\.\\d+)+)?)"); /** * Regular expression to analyze a version number separated by a dot with suffix */ private static final Pattern VERSIONNUMBER_WITH_SUFFIX = Pattern.compile(VERSIONNUMBER.pattern() + "((\\s|\\-|\\.|\\[|\\]|\\w+)+)?"); /** * Regular expression to analyze segments of a version string, consisting of prefix, numeric groups and suffix */ private static final Pattern VERSIONSTRING = Pattern.compile("^" + VERSIONNUMBER_WITH_SUFFIX.pattern()); /** * This method try to determine the version number of the operating system <i>Android</i> more accurately. * * @param userAgent * user agent string * @return more accurately identified version number or {@code null} */ static VersionNumber identifyAndroidVersion(@Nonnull final String userAgent) { VersionNumber version = VersionNumber.UNKNOWN; final List<Pattern> patterns = new ArrayList<Pattern>(); patterns.add(Pattern.compile("Android\\s?((\\d+)((\\.\\d+)+)?(\\-(\\w|\\d)+)?);")); patterns.add(Pattern.compile("Android\\-((\\d+)((\\.\\d+)+)?(\\-(\\w|\\d)+)?);")); for (final Pattern pattern : patterns) { final Matcher m = pattern.matcher(userAgent); if (m.find()) { version = parseFirstVersionNumber(m.group(MAJOR_INDEX)); break; } } return version; } /** * This method try to determine the version number of the operating system <i>Bada</i> more accurately. * * @param userAgent * user agent string * @return more accurately identified version number or {@code null} */ static VersionNumber identifyBadaVersion(final String userAgent) { VersionNumber version = VersionNumber.UNKNOWN; final Pattern pattern = Pattern.compile("Bada/((\\d+)((\\.\\d+)+)?)"); final Matcher m = pattern.matcher(userAgent); if (m.find()) { version = parseFirstVersionNumber(m.group(MAJOR_INDEX)); } return version; } /** * This method try to determine the version number of an operating system of a <i>BSD</i> platform more accurately. * * @param userAgent * user agent string * @return more accurately identified version number or {@code null} */ static VersionNumber identifyBSDVersion(final String userAgent) { VersionNumber version = VersionNumber.UNKNOWN; final Pattern pattern = Pattern.compile("\\w+bsd\\s?((\\d+)((\\.\\d+)+)?((\\-|_)[\\w\\d\\-]+)?)", Pattern.CASE_INSENSITIVE); final Matcher m = pattern.matcher(userAgent); if (m.find()) { version = parseFirstVersionNumber(m.group(MAJOR_INDEX)); } return version; } /** * This method try to determine the version number of the operating system <i>iOS</i> more accurately. * * @param userAgent * user agent string * @return more accurately identified version number or {@code null} */ static VersionNumber identifyIOSVersion(final String userAgent) { VersionNumber version = VersionNumber.UNKNOWN; final List<Pattern> patterns = new ArrayList<Pattern>(); patterns.add(Pattern.compile("iPhone OS\\s?((\\d+)((\\_\\d+)+)?) like Mac OS X")); patterns.add(Pattern.compile("CPU OS\\s?((\\d+)((\\_\\d+)+)?) like Mac OS X")); patterns.add(Pattern.compile("iPhone OS\\s?((\\d+)((\\.\\d+)+)?);")); for (final Pattern pattern : patterns) { final Matcher m = pattern.matcher(userAgent); if (m.find()) { version = parseFirstVersionNumber(m.group(MAJOR_INDEX).replaceAll("_", ".")); break; } } return version; } /** * This method try to determine the version number of the running <i>JVM</i> more accurately. * * @param userAgent * user agent string * @return more accurately identified version number or {@code null} */ static VersionNumber identifyJavaVersion(final String userAgent) { VersionNumber version = VersionNumber.UNKNOWN; final List<Pattern> patterns = new ArrayList<Pattern>(); patterns.add(Pattern.compile("Java/((\\d+)((\\.\\d+)+)?((\\-|_)[\\w\\d\\-]+)?)")); patterns.add(Pattern.compile("Java((\\d+)((\\.\\d+)+)?((\\-|_)[\\w\\d\\-]+)?)")); for (final Pattern pattern : patterns) { final Matcher m = pattern.matcher(userAgent); if (m.find()) { version = parseFirstVersionNumber(m.group(MAJOR_INDEX)); break; } } return version; } /** * This method try to determine the version number of the operating system <i>OS X</i> more accurately. * * @param userAgent * user agent string * @return more accurately identified version number or {@code null} */ static VersionNumber identifyOSXVersion(final String userAgent) { VersionNumber version = VersionNumber.UNKNOWN; final List<Pattern> patterns = new ArrayList<Pattern>(); patterns.add(Pattern.compile("Mac OS X\\s?((\\d+)((\\.\\d+)+)?);")); patterns.add(Pattern.compile("Mac OS X\\s?((\\d+)((\\_\\d+)+)?);")); patterns.add(Pattern.compile("Mac OS X\\s?((\\d+)((\\_\\d+)+)?)\\)")); for (final Pattern pattern : patterns) { final Matcher m = pattern.matcher(userAgent); if (m.find()) { version = parseFirstVersionNumber(m.group(MAJOR_INDEX).replaceAll("_", ".")); break; } } return version; } /** * This method try to determine the version number of the operating system <i>Symbian</i> more accurately. * * @param userAgent * user agent string * @return more accurately identified version number or {@code null} */ static VersionNumber identifySymbianVersion(final String userAgent) { VersionNumber version = VersionNumber.UNKNOWN; final Pattern pattern = Pattern.compile("SymbianOS/((\\d+)((\\.\\d+)+)?s?)"); final Matcher m = pattern.matcher(userAgent); if (m.find()) { version = parseFirstVersionNumber(m.group(MAJOR_INDEX)); } return version; } /** * This method try to determine the version number of the operating system <i>webOS</i> more accurately. * * @param userAgent * user agent string * @return more accurately identified version number or {@code null} */ static VersionNumber identifyWebOSVersion(final String userAgent) { VersionNumber version = VersionNumber.UNKNOWN; final List<Pattern> patterns = new ArrayList<Pattern>(); patterns.add(Pattern.compile("hpwOS/((\\d+)((\\.\\d+)+)?);")); patterns.add(Pattern.compile("webOS/((\\d+)((\\.\\d+)+)?);")); for (final Pattern pattern : patterns) { final Matcher m = pattern.matcher(userAgent); if (m.find()) { version = parseFirstVersionNumber(m.group(MAJOR_INDEX)); break; } } return version; } /** * This method try to determine the version number of the operating system <i>Windows</i> more accurately. * * @param userAgent * user agent string * @return more accurately identified version number or {@code null} */ static VersionNumber identifyWindowsVersion(final String userAgent) { VersionNumber version = VersionNumber.UNKNOWN; final List<Pattern> patterns = new ArrayList<Pattern>(); patterns.add(Pattern.compile("Windows NT\\s?((\\d+)((\\.\\d+)+)?)")); patterns.add(Pattern.compile("Windows Phone OS ((\\d+)((\\.\\d+)+)?)")); patterns.add(Pattern.compile("Windows CE ((\\d+)((\\.\\d+)+)?)")); patterns.add(Pattern.compile("Windows 2000\\s?((\\d+)((\\.\\d+)+)?)")); patterns.add(Pattern.compile("Windows XP\\s?((\\d+)((\\.\\d+)+)?)")); patterns.add(Pattern.compile("Windows 7\\s?((\\d+)((\\.\\d+)+)?)")); patterns.add(Pattern.compile("Win 9x ((\\d+)((\\.\\d+)+)?)")); patterns.add(Pattern.compile("Windows ((\\d+)((\\.\\d+)+)?)")); patterns.add(Pattern.compile("WebTV/((\\d+)((\\.\\d+)+)?)")); for (final Pattern pattern : patterns) { final Matcher m = pattern.matcher(userAgent); if (m.find()) { version = parseFirstVersionNumber(m.group(MAJOR_INDEX)); break; } } return version; } /** * Interprets a string with version information. The first occurrence of a version number in the string will be * searched and processed. * * @param text * string with version information * @return an object of {@code VersionNumber}, never {@code null} */ static VersionNumber parseFirstVersionNumber(@Nonnull final String text) { Check.notNull(text, "text"); final Matcher matcher = VERSIONNUMBER_WITH_SUFFIX.matcher(text); String[] split = null; String ext = null; if (matcher.find()) { split = matcher.group(MAJOR_INDEX).split("\\."); ext = matcher.group(EXTENSION_INDEX); } final String extension = ext == null ? VersionNumber.EMPTY_EXTENSION : trimRight(ext); return split == null ? VersionNumber.UNKNOWN : new VersionNumber(Arrays.asList(split), extension); } /** * Interprets a string with version information. The last version number in the string will be searched and * processed. * * @param text * string with version information * @return an object of {@code VersionNumber}, never {@code null} */ public static VersionNumber parseLastVersionNumber(@Nonnull final String text) { Check.notNull(text, "text"); final Matcher matcher = VERSIONNUMBER_WITH_SUFFIX.matcher(text); String[] split = null; String ext = null; while (matcher.find()) { split = matcher.group(MAJOR_INDEX).split("\\."); ext = matcher.group(EXTENSION_INDEX); } final String extension = ext == null ? VersionNumber.EMPTY_EXTENSION : trimRight(ext); return split == null ? VersionNumber.UNKNOWN : new VersionNumber(Arrays.asList(split), extension); } /** * Try to determine the version number of the operating system by parsing the user agent string. * * * @param family * family of the operating system * @param userAgent * user agent string * @return extracted version number */ public static VersionNumber parseOperatingSystemVersion(@Nonnull final OperatingSystemFamily family, @Nonnull final String userAgent) { Check.notNull(family, "family"); Check.notNull(userAgent, "userAgent"); final VersionNumber v; if (OperatingSystemFamily.ANDROID == family) { v = identifyAndroidVersion(userAgent); } else if (OperatingSystemFamily.BADA == family) { v = identifyBadaVersion(userAgent); } else if (OperatingSystemFamily.BSD == family) { v = identifyBSDVersion(userAgent); } else if (OperatingSystemFamily.IOS == family) { v = identifyIOSVersion(userAgent); } else if (OperatingSystemFamily.JVM == family) { v = identifyJavaVersion(userAgent); } else if (OperatingSystemFamily.OS_X == family) { v = identifyOSXVersion(userAgent); } else if (OperatingSystemFamily.SYMBIAN == family) { v = identifySymbianVersion(userAgent); } else if (OperatingSystemFamily.WEBOS == family) { v = identifyWebOSVersion(userAgent); } else if (OperatingSystemFamily.WINDOWS == family) { v = identifyWindowsVersion(userAgent); } else { v = VersionNumber.UNKNOWN; } return v; } /** * Interprets a string with version information. The first found group will be taken and processed. * * @param version * version as string * @return an object of {@code VersionNumber}, never {@code null} */ public static VersionNumber parseVersion(@Nonnull final String version) { Check.notNull(version, "version"); VersionNumber result = new VersionNumber(new ArrayList<String>(0), version); final Matcher matcher = VERSIONSTRING.matcher(version); if (matcher.find()) { final List<String> groups = Arrays.asList(matcher.group(MAJOR_INDEX).split("\\.")); final String extension = matcher.group(EXTENSION_INDEX) == null ? VersionNumber.EMPTY_EXTENSION : trimRight(matcher .group(EXTENSION_INDEX)); result = new VersionNumber(groups, extension); } return result; } /** * Trims the whitespace at the end of the given string. * * @param text * string to trim * @return trimmed string */ private static String trimRight(@Nonnull final String text) { return text.replaceAll("\\s+$", ""); } /** * <strong>Attention:</strong> This class is not intended to create objects from it. */ private VersionParser() { // This class is not intended to create objects from it. } }