VersionParser.java

  1. /*******************************************************************************
  2.  * Copyright 2012 André Rouél
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *   http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  ******************************************************************************/
  16. package net.sf.uadetector;

  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.List;
  20. import java.util.regex.Matcher;
  21. import java.util.regex.Pattern;

  22. import javax.annotation.Nonnull;

  23. import net.sf.qualitycheck.Check;

  24. /**
  25.  * This class is used to detect version information within <i>User-Agent</i> strings.
  26.  *
  27.  * @author André Rouél
  28.  */
  29. final class VersionParser {

  30.     /**
  31.      * Index number of the group in a matching {@link Pattern} which contains the extension/suffix of a version string
  32.      */
  33.     private static final int EXTENSION_INDEX = 5;

  34.     /**
  35.      * Index number of the group in a matching {@link Pattern} which contains the first/major number of a version string
  36.      */
  37.     private static final int MAJOR_INDEX = 1;

  38.     /**
  39.      * Regular expression to analyze a version number separated by a dot
  40.      */
  41.     private static final Pattern VERSIONNUMBER = Pattern.compile("((\\d+)((\\.\\d+)+)?)");

  42.     /**
  43.      * Regular expression to analyze a version number separated by a dot with suffix
  44.      */
  45.     private static final Pattern VERSIONNUMBER_WITH_SUFFIX = Pattern.compile(VERSIONNUMBER.pattern() + "((\\s|\\-|\\.|\\[|\\]|\\w+)+)?");

  46.     /**
  47.      * Regular expression to analyze segments of a version string, consisting of prefix, numeric groups and suffix
  48.      */
  49.     private static final Pattern VERSIONSTRING = Pattern.compile("^" + VERSIONNUMBER_WITH_SUFFIX.pattern());

  50.     /**
  51.      * This method try to determine the version number of the operating system <i>Android</i> more accurately.
  52.      *
  53.      * @param userAgent
  54.      *            user agent string
  55.      * @return more accurately identified version number or {@code null}
  56.      */
  57.     static VersionNumber identifyAndroidVersion(@Nonnull final String userAgent) {
  58.         VersionNumber version = VersionNumber.UNKNOWN;
  59.         final List<Pattern> patterns = new ArrayList<Pattern>();
  60.         patterns.add(Pattern.compile("Android\\s?((\\d+)((\\.\\d+)+)?(\\-(\\w|\\d)+)?);"));
  61.         patterns.add(Pattern.compile("Android\\-((\\d+)((\\.\\d+)+)?(\\-(\\w|\\d)+)?);"));
  62.         for (final Pattern pattern : patterns) {
  63.             final Matcher m = pattern.matcher(userAgent);
  64.             if (m.find()) {
  65.                 version = parseFirstVersionNumber(m.group(MAJOR_INDEX));
  66.                 break;
  67.             }
  68.         }
  69.         return version;
  70.     }

  71.     /**
  72.      * This method try to determine the version number of the operating system <i>Bada</i> more accurately.
  73.      *
  74.      * @param userAgent
  75.      *            user agent string
  76.      * @return more accurately identified version number or {@code null}
  77.      */
  78.     static VersionNumber identifyBadaVersion(final String userAgent) {
  79.         VersionNumber version = VersionNumber.UNKNOWN;
  80.         final Pattern pattern = Pattern.compile("Bada/((\\d+)((\\.\\d+)+)?)");
  81.         final Matcher m = pattern.matcher(userAgent);
  82.         if (m.find()) {
  83.             version = parseFirstVersionNumber(m.group(MAJOR_INDEX));
  84.         }
  85.         return version;
  86.     }

  87.     /**
  88.      * This method try to determine the version number of an operating system of a <i>BSD</i> platform more accurately.
  89.      *
  90.      * @param userAgent
  91.      *            user agent string
  92.      * @return more accurately identified version number or {@code null}
  93.      */
  94.     static VersionNumber identifyBSDVersion(final String userAgent) {
  95.         VersionNumber version = VersionNumber.UNKNOWN;
  96.         final Pattern pattern = Pattern.compile("\\w+bsd\\s?((\\d+)((\\.\\d+)+)?((\\-|_)[\\w\\d\\-]+)?)", Pattern.CASE_INSENSITIVE);
  97.         final Matcher m = pattern.matcher(userAgent);
  98.         if (m.find()) {
  99.             version = parseFirstVersionNumber(m.group(MAJOR_INDEX));
  100.         }
  101.         return version;
  102.     }

  103.     /**
  104.      * This method try to determine the version number of the operating system <i>iOS</i> more accurately.
  105.      *
  106.      * @param userAgent
  107.      *            user agent string
  108.      * @return more accurately identified version number or {@code null}
  109.      */
  110.     static VersionNumber identifyIOSVersion(final String userAgent) {
  111.         VersionNumber version = VersionNumber.UNKNOWN;
  112.         final List<Pattern> patterns = new ArrayList<Pattern>();
  113.         patterns.add(Pattern.compile("iPhone OS\\s?((\\d+)((\\_\\d+)+)?) like Mac OS X"));
  114.         patterns.add(Pattern.compile("CPU OS\\s?((\\d+)((\\_\\d+)+)?) like Mac OS X"));
  115.         patterns.add(Pattern.compile("iPhone OS\\s?((\\d+)((\\.\\d+)+)?);"));
  116.         for (final Pattern pattern : patterns) {
  117.             final Matcher m = pattern.matcher(userAgent);
  118.             if (m.find()) {
  119.                 version = parseFirstVersionNumber(m.group(MAJOR_INDEX).replaceAll("_", "."));
  120.                 break;
  121.             }
  122.         }
  123.         return version;
  124.     }

  125.     /**
  126.      * This method try to determine the version number of the running <i>JVM</i> more accurately.
  127.      *
  128.      * @param userAgent
  129.      *            user agent string
  130.      * @return more accurately identified version number or {@code null}
  131.      */
  132.     static VersionNumber identifyJavaVersion(final String userAgent) {
  133.         VersionNumber version = VersionNumber.UNKNOWN;
  134.         final List<Pattern> patterns = new ArrayList<Pattern>();
  135.         patterns.add(Pattern.compile("Java/((\\d+)((\\.\\d+)+)?((\\-|_)[\\w\\d\\-]+)?)"));
  136.         patterns.add(Pattern.compile("Java((\\d+)((\\.\\d+)+)?((\\-|_)[\\w\\d\\-]+)?)"));
  137.         for (final Pattern pattern : patterns) {
  138.             final Matcher m = pattern.matcher(userAgent);
  139.             if (m.find()) {
  140.                 version = parseFirstVersionNumber(m.group(MAJOR_INDEX));
  141.                 break;
  142.             }
  143.         }
  144.         return version;
  145.     }

  146.     /**
  147.      * This method try to determine the version number of the operating system <i>OS X</i> more accurately.
  148.      *
  149.      * @param userAgent
  150.      *            user agent string
  151.      * @return more accurately identified version number or {@code null}
  152.      */
  153.     static VersionNumber identifyOSXVersion(final String userAgent) {
  154.         VersionNumber version = VersionNumber.UNKNOWN;
  155.         final List<Pattern> patterns = new ArrayList<Pattern>();
  156.         patterns.add(Pattern.compile("Mac OS X\\s?((\\d+)((\\.\\d+)+)?);"));
  157.         patterns.add(Pattern.compile("Mac OS X\\s?((\\d+)((\\_\\d+)+)?);"));
  158.         patterns.add(Pattern.compile("Mac OS X\\s?((\\d+)((\\_\\d+)+)?)\\)"));
  159.         for (final Pattern pattern : patterns) {
  160.             final Matcher m = pattern.matcher(userAgent);
  161.             if (m.find()) {
  162.                 version = parseFirstVersionNumber(m.group(MAJOR_INDEX).replaceAll("_", "."));
  163.                 break;
  164.             }
  165.         }
  166.         return version;
  167.     }

  168.     /**
  169.      * This method try to determine the version number of the operating system <i>Symbian</i> more accurately.
  170.      *
  171.      * @param userAgent
  172.      *            user agent string
  173.      * @return more accurately identified version number or {@code null}
  174.      */
  175.     static VersionNumber identifySymbianVersion(final String userAgent) {
  176.         VersionNumber version = VersionNumber.UNKNOWN;
  177.         final Pattern pattern = Pattern.compile("SymbianOS/((\\d+)((\\.\\d+)+)?s?)");
  178.         final Matcher m = pattern.matcher(userAgent);
  179.         if (m.find()) {
  180.             version = parseFirstVersionNumber(m.group(MAJOR_INDEX));
  181.         }
  182.         return version;
  183.     }

  184.     /**
  185.      * This method try to determine the version number of the operating system <i>webOS</i> more accurately.
  186.      *
  187.      * @param userAgent
  188.      *            user agent string
  189.      * @return more accurately identified version number or {@code null}
  190.      */
  191.     static VersionNumber identifyWebOSVersion(final String userAgent) {
  192.         VersionNumber version = VersionNumber.UNKNOWN;
  193.         final List<Pattern> patterns = new ArrayList<Pattern>();
  194.         patterns.add(Pattern.compile("hpwOS/((\\d+)((\\.\\d+)+)?);"));
  195.         patterns.add(Pattern.compile("webOS/((\\d+)((\\.\\d+)+)?);"));
  196.         for (final Pattern pattern : patterns) {
  197.             final Matcher m = pattern.matcher(userAgent);
  198.             if (m.find()) {
  199.                 version = parseFirstVersionNumber(m.group(MAJOR_INDEX));
  200.                 break;
  201.             }
  202.         }
  203.         return version;
  204.     }

  205.     /**
  206.      * This method try to determine the version number of the operating system <i>Windows</i> more accurately.
  207.      *
  208.      * @param userAgent
  209.      *            user agent string
  210.      * @return more accurately identified version number or {@code null}
  211.      */
  212.     static VersionNumber identifyWindowsVersion(final String userAgent) {
  213.         VersionNumber version = VersionNumber.UNKNOWN;
  214.         final List<Pattern> patterns = new ArrayList<Pattern>();
  215.         patterns.add(Pattern.compile("Windows NT\\s?((\\d+)((\\.\\d+)+)?)"));
  216.         patterns.add(Pattern.compile("Windows Phone OS ((\\d+)((\\.\\d+)+)?)"));
  217.         patterns.add(Pattern.compile("Windows CE ((\\d+)((\\.\\d+)+)?)"));
  218.         patterns.add(Pattern.compile("Windows 2000\\s?((\\d+)((\\.\\d+)+)?)"));
  219.         patterns.add(Pattern.compile("Windows XP\\s?((\\d+)((\\.\\d+)+)?)"));
  220.         patterns.add(Pattern.compile("Windows 7\\s?((\\d+)((\\.\\d+)+)?)"));
  221.         patterns.add(Pattern.compile("Win 9x ((\\d+)((\\.\\d+)+)?)"));
  222.         patterns.add(Pattern.compile("Windows ((\\d+)((\\.\\d+)+)?)"));
  223.         patterns.add(Pattern.compile("WebTV/((\\d+)((\\.\\d+)+)?)"));
  224.         for (final Pattern pattern : patterns) {
  225.             final Matcher m = pattern.matcher(userAgent);
  226.             if (m.find()) {
  227.                 version = parseFirstVersionNumber(m.group(MAJOR_INDEX));
  228.                 break;
  229.             }
  230.         }
  231.         return version;
  232.     }

  233.     /**
  234.      * Interprets a string with version information. The first occurrence of a version number in the string will be
  235.      * searched and processed.
  236.      *
  237.      * @param text
  238.      *            string with version information
  239.      * @return an object of {@code VersionNumber}, never {@code null}
  240.      */
  241.     static VersionNumber parseFirstVersionNumber(@Nonnull final String text) {
  242.         Check.notNull(text, "text");

  243.         final Matcher matcher = VERSIONNUMBER_WITH_SUFFIX.matcher(text);
  244.         String[] split = null;
  245.         String ext = null;
  246.         if (matcher.find()) {
  247.             split = matcher.group(MAJOR_INDEX).split("\\.");
  248.             ext = matcher.group(EXTENSION_INDEX);
  249.         }

  250.         final String extension = ext == null ? VersionNumber.EMPTY_EXTENSION : trimRight(ext);

  251.         return split == null ? VersionNumber.UNKNOWN : new VersionNumber(Arrays.asList(split), extension);
  252.     }

  253.     /**
  254.      * Interprets a string with version information. The last version number in the string will be searched and
  255.      * processed.
  256.      *
  257.      * @param text
  258.      *            string with version information
  259.      * @return an object of {@code VersionNumber}, never {@code null}
  260.      */
  261.     public static VersionNumber parseLastVersionNumber(@Nonnull final String text) {
  262.         Check.notNull(text, "text");

  263.         final Matcher matcher = VERSIONNUMBER_WITH_SUFFIX.matcher(text);
  264.         String[] split = null;
  265.         String ext = null;
  266.         while (matcher.find()) {
  267.             split = matcher.group(MAJOR_INDEX).split("\\.");
  268.             ext = matcher.group(EXTENSION_INDEX);
  269.         }

  270.         final String extension = ext == null ? VersionNumber.EMPTY_EXTENSION : trimRight(ext);

  271.         return split == null ? VersionNumber.UNKNOWN : new VersionNumber(Arrays.asList(split), extension);
  272.     }

  273.     /**
  274.      * Try to determine the version number of the operating system by parsing the user agent string.
  275.      *
  276.      *
  277.      * @param family
  278.      *            family of the operating system
  279.      * @param userAgent
  280.      *            user agent string
  281.      * @return extracted version number
  282.      */
  283.     public static VersionNumber parseOperatingSystemVersion(@Nonnull final OperatingSystemFamily family, @Nonnull final String userAgent) {
  284.         Check.notNull(family, "family");
  285.         Check.notNull(userAgent, "userAgent");

  286.         final VersionNumber v;
  287.         if (OperatingSystemFamily.ANDROID == family) {
  288.             v = identifyAndroidVersion(userAgent);
  289.         } else if (OperatingSystemFamily.BADA == family) {
  290.             v = identifyBadaVersion(userAgent);
  291.         } else if (OperatingSystemFamily.BSD == family) {
  292.             v = identifyBSDVersion(userAgent);
  293.         } else if (OperatingSystemFamily.IOS == family) {
  294.             v = identifyIOSVersion(userAgent);
  295.         } else if (OperatingSystemFamily.JVM == family) {
  296.             v = identifyJavaVersion(userAgent);
  297.         } else if (OperatingSystemFamily.OS_X == family) {
  298.             v = identifyOSXVersion(userAgent);
  299.         } else if (OperatingSystemFamily.SYMBIAN == family) {
  300.             v = identifySymbianVersion(userAgent);
  301.         } else if (OperatingSystemFamily.WEBOS == family) {
  302.             v = identifyWebOSVersion(userAgent);
  303.         } else if (OperatingSystemFamily.WINDOWS == family) {
  304.             v = identifyWindowsVersion(userAgent);
  305.         } else {
  306.             v = VersionNumber.UNKNOWN;
  307.         }
  308.         return v;
  309.     }

  310.     /**
  311.      * Interprets a string with version information. The first found group will be taken and processed.
  312.      *
  313.      * @param version
  314.      *            version as string
  315.      * @return an object of {@code VersionNumber}, never {@code null}
  316.      */
  317.     public static VersionNumber parseVersion(@Nonnull final String version) {
  318.         Check.notNull(version, "version");

  319.         VersionNumber result = new VersionNumber(new ArrayList<String>(0), version);
  320.         final Matcher matcher = VERSIONSTRING.matcher(version);
  321.         if (matcher.find()) {
  322.             final List<String> groups = Arrays.asList(matcher.group(MAJOR_INDEX).split("\\."));
  323.             final String extension = matcher.group(EXTENSION_INDEX) == null ? VersionNumber.EMPTY_EXTENSION : trimRight(matcher
  324.                     .group(EXTENSION_INDEX));
  325.             result = new VersionNumber(groups, extension);
  326.         }

  327.         return result;
  328.     }

  329.     /**
  330.      * Trims the whitespace at the end of the given string.
  331.      *
  332.      * @param text
  333.      *            string to trim
  334.      * @return trimmed string
  335.      */
  336.     private static String trimRight(@Nonnull final String text) {
  337.         return text.replaceAll("\\s+$", "");
  338.     }

  339.     /**
  340.      * <strong>Attention:</strong> This class is not intended to create objects from it.
  341.      */
  342.     private VersionParser() {
  343.         // This class is not intended to create objects from it.
  344.     }

  345. }