fiftyone.mobile.detection.Match Maven / Gradle / Ivy
/* *********************************************************************
* This Source Code Form is copyright of 51Degrees Mobile Experts Limited.
* Copyright © 2017 51Degrees Mobile Experts Limited, 5 Charlotte Close,
* Caversham, Reading, Berkshire, United Kingdom RG4 7BY
*
* This Source Code Form is the subject of the following patents and patent
* applications, owned by 51Degrees Mobile Experts Limited of 5 Charlotte
* Close, Caversham, Reading, Berkshire, United Kingdom RG4 7BY:
* European Patent No. 2871816;
* European Patent Application No. 17184134.9;
* United States Patent Nos. 9,332,086 and 9,350,823; and
* United States Patent Application No. 15/686,066.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0.
*
* If a copy of the MPL was not distributed with this file, You can obtain
* one at http://mozilla.org/MPL/2.0/.
*
* This Source Code Form is "Incompatible With Secondary Licenses", as
* defined by the Mozilla Public License, v. 2.0.
* ********************************************************************* */
package fiftyone.mobile.detection;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import fiftyone.mobile.detection.entities.Node;
import fiftyone.mobile.detection.entities.Profile;
import fiftyone.mobile.detection.entities.Property;
import fiftyone.mobile.detection.entities.Signature;
import fiftyone.mobile.detection.entities.Value;
import fiftyone.mobile.detection.entities.Values;
import fiftyone.properties.DetectionConstants;
import fiftyone.properties.MatchMethods;
import java.nio.ByteBuffer;
/**
* Contains detection results with all the information relevant to the matched
* device.
*
* Access {@link Property} values using the
* {@link #getValues(java.lang.String)} method for property names and
* {@link #getValues(fiftyone.mobile.detection.entities.Property)} for property
* objects. For example: {@code match.getValues("IsMobile");}
*
* All values are returned as strings unless you request a specific format:
* {@code match.getValues("ScreenPixelsWidth").toDouble();}
*
* If you want a more general device information like the rank you should use
* {@link #getSignature()} method which returns a {@link Signature} object this
* device relates to.
*
* Match also provides various metrics properties: device Id, method used,
* difference and rank.
*
* - Device id consists of four
* {@link fiftyone.mobile.detection.entities.Component components} where each
* component is the Id of the {@link Profile} matched by the detector.
* Access like:
* {@code match.getDeviceId();}
*
- Detection method refers to the algorithm used for this match. Use like:
* {@code match.getMethod();}
*
For more information on the detection methods see:
*
* how Pattern device detection works.
*
- Difference indicates the level of confidence in the current detection
* results. Used in conjunction with the detection method and only makes sense
* if "Numeric", "Closest" or "Nearest" algorithm was used. The higher the
* number the lower the confidence.
*
- Rank provides information on the level of popularity of the User-Agent
* used to create this match. The lower the rank the more popular the
* User-Agent is. Popularity is determined by 51Degrees based on our internal
* usage statistics.
*
*
* Keeping the data file up to date improves the overall quality of detection
* as the 51Degrees data team adds (on average) 200 new devices to our database
* each week. With Premium and Enterprise data files can benefit from the
*
* automatic data updates as well as a wider range of properties and more
* devices.
*
* This object should not be created manually in the external code. Use one of
* the match methods in the {@link Provider} class to obtain a match object
* with data members initialised.
*
* For more information see https://51degrees.com/Support/Documentation/Java
*/
public class Match {
/**
* Instance of the provider used to create the match.
*/
final Provider provider;
/**
* Current working state of the matching process.
*/
final MatchState state;
/**
* A string that contains the cookie header for the request.
*/
public String cookie;
/**
* Map of property names to dynamic values populated from the cookie.
*/
private Map propertyValueOverridesCookies;
/**
* Sets the result of the match explicitly.
* @param value of the match result to set.
*/
void setResult(MatchResult value) {
matchResult = value;
}
MatchResult getResult() {
return matchResult;
}
private MatchResult matchResult;
/**
* @return {@link Dataset} used to create the match.
*/
public Dataset getDataSet() {
return provider.dataSet;
}
/**
* @return target User-Agent string used for detection
*/
public String getTargetUserAgent() {
return getResult().getTargetUserAgent();
}
/**
* @return the elapsed time for the match.
*/
public long getElapsed() {
return getResult().getElapsed();
}
/**
* Returns a {@link Signature} that best fits the provided User-Agent string.
* A signature can be used to retrieve {@link Profile profiles} and rank.
*
* @return {@link Signature} with best match to the User-Agent provided.
* @throws java.io.IOException
*/
public Signature getSignature() throws IOException {
return getResult().getSignature();
}
/**
* Returns the detection method used to obtain this object. Method used
* reflects the confidence of the detector in the accuracy of the current
* match.
*
* - "Exact" means the detector is confident the results are accurate.
*
- "None" means the User-Agent provided is fake.
*
- "Numeric", "Closest" and "Nearest" will always return a result but
* the {@link #getDifference()} should be used to assess the accuracy of
* the detection. The higher the number the less confident the detector is.
*
*
* With Premium or Enterprise data files you will see more "Exact"
* detections as the number of device combinations available in these files
* is significantly larger than in the "Lite" data file.
*
* Compare data options
*
* For more information on detection methods see:
*
* how Pattern device detection works
*
* @return {@link MatchMethods} used to obtain match.
*/
public MatchMethods getMethod() {
return getResult().getMethod();
}
/**
* @return number of closest signatures returned for evaluation.
*/
public int getClosestSignaturesCount() {
return getResult().getClosestSignaturesCount();
}
/**
* @return integer representing number of signatures compared against
* the User-Agent if closest match method was used.
*/
public int getSignaturesCompared() {
return getResult().getSignaturesCompared();
}
/**
* @return integer representing number of signatures read during detection
*/
public int getSignaturesRead() {
return getResult().getSignaturesRead();
}
/**
* @return integer representing number of root node checked
*/
public int getRootNodesEvaluated() {
return getResult().getRootNodesEvaluated();
}
/**
* @return integer representing the number of nodes checked
*/
public int getNodesEvaluated() {
return getResult().getNodesEvaluated();
}
/**
* @return the number of nodes found by the match.
* @throws java.io.IOException
*/
public int getNodesFound() throws IOException {
return getResult().getNodes().length;
}
/**
* @return integer representing number of strings read for the match
*/
public int getStringsRead() {
return getResult().getStringsRead();
}
/**
* Array of {@link Profile profiles} associated with the device that was
* found. Profiles can then be used to retrieve {@link Property properties}
* and {@link Value values}.
*
* @return array of {@link Profile profiles} associated with the device
* that was found.
* @throws IOException if there was a problem accessing data file.
*/
public Profile[] getProfiles() throws IOException {
return overriddenProfiles == null ?
getResult().getProfiles() :
getOverriddenProfiles();
}
/**
* Array of profiles associated with the device that may have been
* overridden for this instance of match.
* This property is needed to ensure that other references to the instance
* of MatchResult are not altered when overriding profiles.
*
* @return profiles set specifically for this match.
* @throws IOException if there was a problem accessing data file.
*/
@SuppressWarnings("DoubleCheckedLocking")
Profile[] getOverriddenProfiles() throws IOException {
Profile[] result = overriddenProfiles;
if (result == null && getSignature() != null) {
synchronized (this) {
result = overriddenProfiles;
if (result == null) {
result = new Profile[getResult().getProfiles().length];
System.arraycopy(
getResult().getProfiles(), 0,
result, 0, result.length);
overriddenProfiles = result;
}
}
}
return result;
}
@SuppressWarnings("VolatileArrayField")
private volatile Profile[] overriddenProfiles;
/**
* The numeric difference between the target User-Agent and the match.
* Numeric sub strings of the same length are compared based on the numeric
* value. Other character differences are compared based on the difference
* in ASCII values of the two characters at the same positions.
*
* @return numeric difference.
*/
public int getDifference() {
int score = getResult().getLowestScore();
return score >= 0 ? score : 0;
}
/**
* The unique id of the device represented by the match. Id is composed of
* several {@link Profile profiles} separated by hyphen symbol. One profile
* is chosen per each {@link fiftyone.mobile.detection.entities.Component
* component}.
*
* Device Id can be stored for future use and the relevant
* {@link Property properties} and {@link Value values} restored using the
* {@link Provider#matchForDeviceId(java.lang.String)} method.
*
* @return string representing unique id of device
* @throws IOException if there was a problem accessing data file.
*/
public String getDeviceId() throws IOException {
String result;
if (getSignature() != null) {
result = getSignature().getDeviceId();
}
else {
String[] profileIds = new String[getProfiles().length];
for (int i = 0; i < profileIds.length; i++) {
profileIds[i] = Integer.toString(getProfiles()[i].profileId);
}
result = Utilities.joinString(DetectionConstants.PROFILE_SEPARATOR,
profileIds);
}
return result;
}
/**
* Id of the device represented as an array of bytes. Unlike the String Id
* this Id only contains integers and no hyphen separators.
*
* To obtain the unique profile IDs wrap the byte array in a ByteBuffer and
* use {@code getInt()} repeatedly.
*
* To obtain a {@link Match} with the corresponding
* {@link Property properties} and {@link Value values} use the
* {@link Provider#matchForDeviceId(byte[])}.
*
* @return Profile Id represented as byte array. Note that Id separators
* such as "-" are not part of the byte array, only the integer IDs are.
* @throws IOException if there was a problem accessing the data file.
*/
public byte[] getDeviceIdAsByteArray() throws IOException {
// Allocate enough bytes to store Id for every profile.
// Integer.SIZE divided by 8 as the size is in bits.
byte[] result = new byte[(getProfiles().length * Integer.SIZE / 8)];
ByteBuffer bb = ByteBuffer.wrap(result);
for (Profile tempProfile : getProfiles()) {
bb.putInt(tempProfile.profileId);
}
return result;
}
/**
* @return User-Agent of the matching device with irrelevant characters
* removed.
* @throws java.io.IOException
*/
public String getUserAgent() throws IOException {
return getSignature() != null ? getSignature().toString() : null;
}
/**
* This method is not memory efficient and should be avoided as the Match
* class now exposes an getValues methods keyed on property name.
*
* @return the results of the match as a sorted list of property names
* and values.
* @throws IOException if there was a problem accessing data file.
* @deprecated use getValues methods
*/
@Deprecated
public Map getResults() throws IOException {
Map results = new HashMap();
// Add the properties and values first.
for (Property property : getDataSet().getProperties()) {
Values values = getValues(property);
List strings = new ArrayList();
for (Value value : values.getAll()) {
if (value.getProperty() == property) {
strings.add(value.getName());
}
}
results.put(
property.getName(),
strings.toArray(new String[strings.size()]));
}
results.put(DetectionConstants.DIFFERENCE_PROPERTY,
new String[]{Integer.toString(getDifference())});
results.put(DetectionConstants.NODES,
new String[]{toString()});
// Add any other derived values.
results.put(DetectionConstants.DEVICEID,
new String[]{getDeviceId()});
return results;
}
/**
* Gets the {@link Values} associated with the property name using the
* profiles found by the match. If matched profiles don't contain a value
* then the default profiles for each of the components are also checked.
*
* If a value is provided via the property value override functionality
* then this will be returned in place of the static value from the
* dataset.
*
* @param property The property whose values are required.
* @return Array of the values associated with the property, or null if the
* property does not exist.
* @throws IOException if there was a problem accessing data file.
*/
public Values getValues(Property property) throws IOException {
Values value = null;
if (property != null) {
// Create a dynamic values instance for this value which does
// not reference a static value index in the dataset.
if (this.cookie != null) {
String cookieValue = getPropertyValueOverridesCookies().get(
property.getName());
if (cookieValue != null) {
// Create a dynamic values instance for this value which
// does not reference a static value index in the dataset.
value = new Values(property, new Value[] { new Value(
this.getDataSet(),
property,
cookieValue) });
}
}
if (value == null) {
// Get the property value from the profile returned
// from the match.
for (Profile profile : getProfiles()) {
if (profile.getComponent().getComponentId()
== property.getComponent().getComponentId()) {
value = profile.getValues(property);
break;
}
}
// If the value has not been found use the default profile.
if (value == null) {
value = property.getComponent().
getDefaultProfile().
getValues(property);
}
}
}
return value;
}
/**
* Gets the {@link Values} associated with the property name using the
* profiles found by the match. If matched profiles don't contain a value
* then the default profiles for each of the components are also checked.
*
* @param propertyName The property name whose values are required.
* @return Array of the values associated with the property, or null if the
* property does not exist.
* @throws IOException if there was a problem accessing data file.
*/
public Values getValues(String propertyName) throws IOException {
return getValues(getDataSet().get(propertyName));
}
@SuppressWarnings("DoubleCheckedLocking")
private Map getPropertyValueOverridesCookies()
{
if (propertyValueOverridesCookies == null) {
synchronized(this) {
if (propertyValueOverridesCookies == null) {
Map tempMap = new HashMap();
try {
String prefix = DetectionConstants.
PROPERTY_VALUE_OVERRIDE_COOKIE_PREFIX;
String[] pairs = cookie.split(";");
for (String pair : pairs) {
pair = pair.trim();
if (pair.startsWith(prefix)) {
int equalsIndex = pair.indexOf("=");
if (equalsIndex > prefix.length()) {
String key = pair.substring(
prefix.length(),
equalsIndex);
String value = pair.substring(equalsIndex + 1);
tempMap.put(key, value);
}
}
}
}
catch (Exception ex) {
// There is no opportunity to throw an exception as the
// method is core and is not essential to processing.
// When a logger is added to core an entry should be
// written to the log file.
}
propertyValueOverridesCookies = tempMap;
}
}
}
return propertyValueOverridesCookies;
}
/**
* Constructs a new detection match ready to be used.
*
* @param provider data set to be used for this match
*/
Match(Provider provider) {
this.provider = provider;
this.state = new MatchState(this);
matchResult = state;
}
/**
* Constructs a new detection match ready to be used to identify the
* profiles associated with the target User-Agent.
*
* @param dataSet data set to be used for this match
* @param targetUserAgent User-Agent to identify
* @throws UnsupportedEncodingException indicates an Unsupported Encoding
* exception occurred
*/
Match(Provider provider, String targetUserAgent)
throws UnsupportedEncodingException {
this(provider);
this.state.init(targetUserAgent);
}
/**
* Resets the match instance ready for further matching.
*/
void reset() {
this.state.reset();
this.overriddenProfiles = null;
this.cookie = null;
this.propertyValueOverridesCookies = null;
}
/**
* Override the profiles found by the match with the profileId provided.
*
* @param profileId The ID of the profile to replace the existing component
* @throws IOException indicates an I/O exception occurred
*/
public void updateProfile(int profileId) throws IOException {
// Find the new profile from the data set.
Profile newProfile = getDataSet().findProfile(profileId);
if (newProfile != null) {
// Loop through the profiles found so far and replace the
// profile for the same component with the new one.
for (int i = 0; i < getOverriddenProfiles().length; i++) {
// Compare by component Id incase the stream data source is
// used and we have different instances of the same component
// being used.
if (getOverriddenProfiles()[i].getComponent().getComponentId()
== newProfile.getComponent().getComponentId()) {
getOverriddenProfiles()[i] = newProfile;
break;
}
}
}
}
/**
* A string representation of the nodes found from the target User-Agent.
*
* @return a string representation of the match.
*/
@Override
public String toString() {
if (state.getNodesList() != null && state.getNodes().length > 0) {
try {
byte[] value = new byte[getTargetUserAgent().length()];
for (Node node : state.getNodes()) {
node.addCharacters(value);
}
for (int i = 0; i < value.length; i++) {
if (value[i] == 0) {
value[i] = (byte) '_';
}
}
return new String(value, "US-ASCII");
} catch (IOException e) {
return super.toString();
}
}
return super.toString();
}
}