blob: b86484bcaf401e2856ce2bd4109f40f4313454fa [file] [log] [blame]
package org.apache.velocity.tools.view;
import org.apache.velocity.exception.VelocityException;
import org.apache.velocity.shaded.commons.lang3.tuple.ImmutablePair;
import org.apache.velocity.shaded.commons.lang3.tuple.Pair;
import org.apache.velocity.tools.ClassUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UAParser
{
public UAParser()
{
}
public static class UAEntity implements Serializable
{
private String name = null;
private int majorVersion = -1;
private int minorVersion = -1;
public UAEntity(String n, String maj, String min)
{
name = n;
try
{
majorVersion = maj == null ? -1 : Integer.valueOf(maj);
minorVersion = maj == null ? -1 : Integer.valueOf(min);
}
catch (NumberFormatException nfe)
{
majorVersion = minorVersion = -1;
}
}
public String getName()
{
return name;
}
public int getMajorVersion()
{
return majorVersion;
}
public int getMinorVersion()
{
return minorVersion;
}
}
/* device, in order of growing precedence */
public enum DeviceType
{
UNKNOWN,
DESKTOP,
MOBILE,
TABLET,
TV,
ROBOT
};
private static Map<String,String> browserTranslationMap = null;
private static Map<String,String> osTranslationMap = null;
static
{
browserTranslationMap = new HashMap<String,String>();
browserTranslationMap.put("navigator","Netscape");
browserTranslationMap.put("nokia5250", "Nokia Browser");
osTranslationMap = new HashMap<String,String>();
osTranslationMap.put("android", "Android");
osTranslationMap.put("bada", "Bada");
osTranslationMap.put("bb10", "BlackBerry");
osTranslationMap.put("blackberry", "BlackBerry");
osTranslationMap.put("cros", "Chrome OS");
osTranslationMap.put("fxos", "Firefox OS");
osTranslationMap.put("hpwos", "WebOS");
osTranslationMap.put("ipad", "iOS");
osTranslationMap.put("iphone", "iOS");
osTranslationMap.put("ipod", "iOS");
osTranslationMap.put("kfthwi", "Kindle");
osTranslationMap.put("kftt", "Kindle");
osTranslationMap.put("mac os x", "OS X");
osTranslationMap.put("macos x", "OS X");
osTranslationMap.put("nokiae", "Nokia");
osTranslationMap.put("nokiax2", "Nokia");
osTranslationMap.put("remi", "Fedora");
osTranslationMap.put("rhel", "Red Hat");
osTranslationMap.put("series40", "Symbian");
osTranslationMap.put("series60", "Symbian");
osTranslationMap.put("series80", "Symbian");
osTranslationMap.put("series90", "Symbian");
osTranslationMap.put("series 40", "Symbian");
osTranslationMap.put("series 60", "Symbian");
osTranslationMap.put("series 80", "Symbian");
osTranslationMap.put("series 90", "Symbian");
osTranslationMap.put("symbianos", "Symbian");
osTranslationMap.put("symbos", "Symbian");
osTranslationMap.put("tigeros", "OS X");
osTranslationMap.put("tizen", "Tizen");
osTranslationMap.put("tt", "Android");
osTranslationMap.put("unix", "Unix");
osTranslationMap.put("unix bsd", "BSD");
osTranslationMap.put("unixware", "Unix");
osTranslationMap.put("webos", "WebOS");
osTranslationMap.put("windows nt", "Windows");
osTranslationMap.put("win98", "Windows");
}
public static class UserAgent implements Serializable
{
private DeviceType deviceType = null;
private UAEntity operatingSystem = null;
private UAEntity browser = null;
private UAEntity renderingEngine = null;
private boolean isRobot = false;
public DeviceType getDeviceType() { return deviceType; }
public UAEntity getOperatingSystem() { return operatingSystem; }
public UAEntity getBrowser() { return browser; }
public UAEntity getRenderingEngine() { return renderingEngine; }
protected void setOperatingSystem(String entity, String major, String minor)
{
if (entity.equals("Series") && major != null)
{
entity += major;
major = minor = null;
}
String alternate = osTranslationMap.get(entity.toLowerCase());
if (alternate != null) entity = alternate;
if (entity.startsWith("BlackBerry")) { entity = "BlackBerry"; }
operatingSystem = new UAEntity(entity, major, minor);
}
protected void setBrowser(String entity, String major, String minor)
{
String alternate = browserTranslationMap.get(entity.toLowerCase());
if (alternate != null) { entity = alternate; }
if ("Navigator".equals(entity)) { entity = "Netscape"; }
browser = new UAEntity(entity, major, minor);
if ("Edge".equals(entity) && renderingEngine == null) { renderingEngine = new UAEntity("EdgeHTML", major, minor); }
}
protected void setRenderingEngine(String entity, String major, String minor)
{
if (deviceType == DeviceType.ROBOT) return;
renderingEngine = new UAEntity(entity, major, minor);
}
protected void setDeviceType(DeviceType deviceType)
{
this.deviceType = deviceType;
}
}
private enum EntityType
{
BROWSER,
BROWSER_OS,
ENGINE,
FORCE_BROWSER,
FORCE_OS,
IGNORE,
MAYBE_BROWSER,
MAYBE_OS,
MAYBE_ROBOT,
MERGE,
MERGE_OR_BROWSER,
MERGE_OR_OS,
OS,
ROBOT
};
private static final String UA_KEYWORDS = "/org/apache/velocity/tools/view/ua-keywords.txt";
private static Map<String, Pair<EntityType, DeviceType>> entityMap = new HashMap<String, Pair<EntityType, DeviceType>>();
static
{
try
{
Properties properties = new Properties();
InputStream stream = ClassUtils.getResourceAsStream(UA_KEYWORDS, BrowserTool.class);
if (stream == null) { throw new IOException("could not find org.apache.velocity.tools.view.ua-keywords.txt resource"); }
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
String line = null;
int num = 1;
while ((line = reader.readLine()) != null)
{
line = line.trim();
if (line.length() == 0 || line.startsWith("#")) { ++num; continue; }
int eq = line.indexOf('=');
if (eq == -1) { throw new IOException("invalid line format in ua-keywords.txt at line " + num); }
String key = line.substring(0, eq);
String val = line.substring(eq + 1).toUpperCase();
int coma = val.indexOf(',');
DeviceType device = null;
if (coma != -1)
{
device = DeviceType.valueOf(val.substring(coma + 1));
val = val.substring(0, coma);
}
EntityType entity = val.length() > 0 ? EntityType.valueOf(val) : null;
entityMap.put(key, new ImmutablePair<EntityType, DeviceType>(entity , device));
++num;
}
}
catch(Exception e)
{
throw new VelocityException("BrowserTool: static initialization failed", e);
}
}
private static final String nonMergeSep = "(;/)";
private static Pattern versionPattern = Pattern.compile(
/* entity name */
"([a-z]+(?:(?=[;()@]|$)|(?:[0-9]+(?!\\.)[a-z]*)|(?:[!_+.\\-][a-z]+)+|(?=[/ ,\\-:0-9+!_=])))" +
/* potential version */
"(?:([/ ,\\-:+_=])?(?:v?(\\d+)(?:\\.(\\d+))?[a-z+]*)?)",
Pattern.CASE_INSENSITIVE);
private static boolean isRobotToken(String token)
{
token = token.toLowerCase();
return token.endsWith("bot") || token.endsWith("crawler") || token.endsWith("spider") || token.endsWith("agent") || token.endsWith("validator");
}
/* the big hairy parsing method */
public static UserAgent parseUserAgent(String userAgentString, Logger log)
{
UserAgent ua = null;
try
{
ua = new UserAgent();
Matcher matcher = versionPattern.matcher(userAgentString);
String merge = null;
EntityType mergeTarget = null;
boolean maybeBrowser = true;
boolean maybeOS = true;
boolean maybeRobot = false;
boolean forcedBrowser = false;
boolean forcedOS = false;
while (matcher.find())
{
String entity = matcher.group(1);
String separator = matcher.group(2);
String major = matcher.group(3);
String minor = matcher.group(4);
char next = userAgentString.length() == matcher.end(1) ? ';' : userAgentString.charAt(matcher.end(1));
if (entity != null)
{
if (merge != null)
{
String merged = merge + " " + entity;
if (mergeTarget == null)
{
entity = merged;
}
else
{
Pair<EntityType,DeviceType> pair = entityMap.get(merged.toLowerCase());
EntityType mergedType = pair == null ? null : pair.getLeft();
if (mergedType != null && (
mergeTarget == mergedType ||
mergeTarget == EntityType.BROWSER && (mergedType == EntityType.MAYBE_BROWSER || mergedType == EntityType.FORCE_BROWSER) ||
mergeTarget == EntityType.OS && (mergedType == EntityType.MAYBE_OS || mergedType == EntityType.FORCE_OS)
))
{
entity = merged;
}
else
{
/* It means the merge failed, so revert it */
switch (mergeTarget)
{
case BROWSER:
ua.setBrowser(merge, null, null);
break;
case OS:
ua.setOperatingSystem(merge, null, null);
break;
default:
throw new VelocityException("BrowserTool: unhandled case!");
}
}
}
merge = null;
mergeTarget = null;
}
Pair<EntityType, DeviceType> identity = entityMap.get(entity.toLowerCase());
EntityType entityType = null;
DeviceType deviceType = null;
if (identity == null)
{
/* try again with major version appended */
String alternateEntity = entity + separator + major;
identity = entityMap.get((entity + separator + major).toLowerCase());
if (identity != null)
{
entity = alternateEntity;
}
}
if (identity != null)
{
entityType = identity.getLeft();
deviceType = identity.getRight();
DeviceType previousDeviceType = ua.getDeviceType();
/* only overwrite device types of lower precedence */
if (deviceType != null && (previousDeviceType == null || deviceType.compareTo(previousDeviceType) > 0))
{
ua.setDeviceType(deviceType); // may be overwritten by 'robot' device type
}
}
if (entityType != null)
{
switch (entityType)
{
case BROWSER:
{
if (ua.getBrowser() == null || !forcedBrowser)
{
ua.setBrowser(entity, major, minor);
maybeBrowser = false;
}
break;
}
case BROWSER_OS:
{
ua.setBrowser(entity, major, minor);
maybeBrowser = false;
ua.setOperatingSystem(entity, major, minor);
maybeOS = false;
break;
}
case ENGINE:
{
if (!"KHTML".equals(entity) || major != null || ua.getRenderingEngine() == null)
{
ua.setRenderingEngine(entity, major, minor);
}
break;
}
case FORCE_BROWSER:
{
if (!forcedBrowser)
{
ua.setBrowser(entity, major, minor);
maybeBrowser = false;
forcedBrowser = true;
}
break;
}
case FORCE_OS:
{
if (!forcedOS)
{
ua.setOperatingSystem(entity, major, minor);
maybeOS = false;
forcedOS = true;
}
break;
}
case IGNORE:
{
break;
}
case MAYBE_BROWSER:
{
if (maybeBrowser)
{
if ("rv".equals(entity))
{
if (ua.getBrowser() != null && ua.getBrowser().getName().equals("Mozilla"))
{
entity = "Mozilla";
} else
{
entity = null;
}
} else if ("Version".equals(entity))
{
if (ua.getBrowser() != null && ua.getBrowser().getName().startsWith("Opera"))
{
entity = ua.getBrowser().getName();
} else if (ua.getBrowser() != null && ua.getBrowser().getName().equals("Mozilla"))
{
entity = "Safari";
} else
{
entity = null;
}
} else if ("Safari".equals(entity) && ua.getBrowser() != null && "Safari".equals(ua.getBrowser().getName()))
{
entity = null;
}
if (entity != null)
{
ua.setBrowser(entity, major, minor);
}
}
break;
}
case MAYBE_OS:
{
if (maybeOS)
{
ua.setOperatingSystem(entity, major, minor);
}
break;
}
case MAYBE_ROBOT:
{
maybeRobot = true;
break;
}
case MERGE:
{
if (major == null)
{
if (nonMergeSep.indexOf(next) == -1)
{
merge = merge == null ? entity : merge + " " + entity;
} else
{
if ("Mobile".equals(entity) && ua.getOperatingSystem() != null)
{
if (ua.getOperatingSystem().getName().equals("Ubuntu"))
{
ua.setOperatingSystem("Ubuntu Mobile", String.valueOf(ua.getOperatingSystem().getMajorVersion()), String.valueOf(ua.getOperatingSystem().getMinorVersion()));
} else if (ua.getOperatingSystem().getName().equals("Linux"))
{
ua.setOperatingSystem("Android", null, null);
}
}
}
}
break;
}
case MERGE_OR_BROWSER:
{
if (!forcedBrowser)
{
if (major != null || nonMergeSep.indexOf(next) != -1)
{
ua.setBrowser(entity, major, minor);
} else
{
merge = entity;
mergeTarget = EntityType.BROWSER;
}
}
break;
}
case MERGE_OR_OS:
{
if (!forcedOS)
{
if (major != null || nonMergeSep.indexOf(next) != -1)
{
ua.setOperatingSystem(entity, major, minor);
} else
{
merge = entity;
mergeTarget = EntityType.OS;
}
}
break;
}
case OS:
{
if (ua.getOperatingSystem() == null || !forcedOS)
{
ua.setOperatingSystem(entity, major, minor);
maybeOS = false;
}
break;
}
case ROBOT:
{
ua.setDeviceType(DeviceType.ROBOT);
break;
}
default:
{
throw new VelocityException("BrowserTool: unhandled case: " + entityType);
}
}
}
else
{
if (entity.startsWith("Linux") && !forcedOS)
{
ua.setOperatingSystem("Linux", null, null);
}
else if (isRobotToken(entity))
{
ua.setDeviceType(DeviceType.ROBOT);
}
else if (entity.startsWith("MID") && !entity.startsWith("MIDP") && (ua.getDeviceType() == null || DeviceType.TABLET.compareTo(ua.getDeviceType()) > 0 ))
{
ua.setDeviceType(DeviceType.TABLET);
}
else if (entity.startsWith("CoolPad") && (ua.getDeviceType() == null || DeviceType.MOBILE.compareTo(ua.getDeviceType()) > 0 ))
{
ua.setDeviceType(DeviceType.MOBILE);
}
else if (entity.startsWith("LG-") && (ua.getDeviceType() == null || DeviceType.MOBILE.compareTo(ua.getDeviceType()) > 0 ))
{
ua.setDeviceType(DeviceType.MOBILE);
}
else if (entity.startsWith("SonyEricsson"))
{
ua.setDeviceType(DeviceType.MOBILE);
}
}
}
}
if (ua.getOperatingSystem() != null && "Windows".equals(ua.getOperatingSystem().getName()) && (ua.getOperatingSystem().getMajorVersion() == 98 || ua.getOperatingSystem().getMajorVersion() == 2000))
{
if (ua.getOperatingSystem().getMajorVersion() == 98)
{
ua.setOperatingSystem("Windows 98", "4", "90");
}
else if (ua.getOperatingSystem().getMajorVersion() == 2000)
{
ua.setOperatingSystem("Windows 2000", "5", "0");
}
}
if (ua.getBrowser() == null)
{
if (ua.getDeviceType() == DeviceType.ROBOT || maybeRobot)
{
ua.setBrowser("robot", "0", "0");
ua.setDeviceType(DeviceType.ROBOT);
}
else if (ua.getOperatingSystem() != null && ua.getOperatingSystem().getName().equals("Symbian"))
{
ua.setBrowser("Nokia Browser", String.valueOf(ua.getOperatingSystem().getMajorVersion()), String.valueOf(ua.getOperatingSystem().getMinorVersion()));
}
else
{
ua.setBrowser("unknown", "0", "0");
}
}
if (ua.getOperatingSystem() == null)
{
if (ua.getDeviceType() == DeviceType.ROBOT || maybeRobot)
{
ua.setOperatingSystem("robot", "0", "0");
ua.setDeviceType(DeviceType.ROBOT);
}
else
{
ua.setOperatingSystem("unknown", "0", "0");
}
}
if (ua.getDeviceType() == null)
{
if (ua.getOperatingSystem() != null && "Android".equals(ua.getOperatingSystem().getName()))
{
ua.setDeviceType(DeviceType.MOBILE);
}
else
{
/* make Desktop the default device */
ua.setDeviceType(DeviceType.DESKTOP);
}
}
}
catch (Exception e)
{
log.error("Could not parse browser for User-Agent: {}", userAgentString, e);
ua = null;
}
return ua;
}
}