All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nl.basjes.parse.useragent.UserAgent Maven / Gradle / Ivy

Go to download

A parsing an analyzing library to get information from a useragent string.

There is a newer version: 7.29.0
Show newest version
/*
 * Yet Another UserAgent Analyzer
 * Copyright (C) 2013-2024 Niels Basjes
 *
 * 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
 *
 * https://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 nl.basjes.parse.useragent;

import nl.basjes.parse.useragent.AgentField.ImmutableAgentField;
import nl.basjes.parse.useragent.AgentField.MutableAgentField;
import nl.basjes.parse.useragent.analyze.Matcher;
import nl.basjes.parse.useragent.parser.UserAgentBaseListener;
import nl.basjes.parse.useragent.utils.DefaultANTLRErrorListener;
import nl.basjes.parse.useragent.utils.springframework.util.LinkedCaseInsensitiveMap;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.dfa.DFA;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

import static org.apache.commons.text.WordUtils.capitalizeFully;

public interface UserAgent extends Serializable {

    String getUserAgentString();
    Map getHeaders();
    AgentField get(String fieldName);
    String getValue(String fieldName);
    Long getConfidence(String fieldName);
    boolean hasSyntaxError();
    boolean hasAmbiguity();
    int getAmbiguityCount();

    List getAvailableFieldNamesSorted();

    default List getCleanedAvailableFieldNamesSorted() {
        List fieldNames = new ArrayList<>();
        for (String fieldName : getAvailableFieldNamesSorted()) {
            if (!STANDARD_FIELDS.contains(fieldName)) {
                if (get(fieldName).isDefaultValue()) {
                    // Skip the "non standard" fields that do not have a relevant value.
                    continue;
                }
            }
            fieldNames.add(fieldName);
        }
        return fieldNames;
    }

    String DEVICE_CLASS                         = "DeviceClass";
    String DEVICE_NAME                          = "DeviceName";
    String DEVICE_BRAND                         = "DeviceBrand";
    String DEVICE_CPU                           = "DeviceCpu";
    String DEVICE_CPU_BITS                      = "DeviceCpuBits";
    String DEVICE_FIRMWARE_VERSION              = "DeviceFirmwareVersion";
    String DEVICE_VERSION                       = "DeviceVersion";

    String OPERATING_SYSTEM_CLASS               = "OperatingSystemClass";
    String OPERATING_SYSTEM_NAME                = "OperatingSystemName";
    String OPERATING_SYSTEM_VERSION             = "OperatingSystemVersion";
    String OPERATING_SYSTEM_VERSION_MAJOR       = "OperatingSystemVersionMajor";
    String OPERATING_SYSTEM_NAME_VERSION        = "OperatingSystemNameVersion";
    String OPERATING_SYSTEM_NAME_VERSION_MAJOR  = "OperatingSystemNameVersionMajor";
    String OPERATING_SYSTEM_VERSION_BUILD       = "OperatingSystemVersionBuild";

    String LAYOUT_ENGINE_CLASS                  = "LayoutEngineClass";
    String LAYOUT_ENGINE_NAME                   = "LayoutEngineName";
    String LAYOUT_ENGINE_VERSION                = "LayoutEngineVersion";
    String LAYOUT_ENGINE_VERSION_MAJOR          = "LayoutEngineVersionMajor";
    String LAYOUT_ENGINE_NAME_VERSION           = "LayoutEngineNameVersion";
    String LAYOUT_ENGINE_NAME_VERSION_MAJOR     = "LayoutEngineNameVersionMajor";
    String LAYOUT_ENGINE_BUILD                  = "LayoutEngineBuild";

    String AGENT_CLASS                          = "AgentClass";
    String AGENT_NAME                           = "AgentName";
    String AGENT_VERSION                        = "AgentVersion";
    String AGENT_VERSION_MAJOR                  = "AgentVersionMajor";
    String AGENT_NAME_VERSION                   = "AgentNameVersion";
    String AGENT_NAME_VERSION_MAJOR             = "AgentNameVersionMajor";
    String AGENT_BUILD                          = "AgentBuild";
    String AGENT_LANGUAGE                       = "AgentLanguage";
    String AGENT_LANGUAGE_CODE                  = "AgentLanguageCode";
    String AGENT_INFORMATION_EMAIL              = "AgentInformationEmail";
    String AGENT_INFORMATION_URL                = "AgentInformationUrl";
    String AGENT_SECURITY                       = "AgentSecurity";
    String AGENT_UUID                           = "AgentUuid";

    String WEBVIEW_APP_NAME                     = "WebviewAppName";
    String WEBVIEW_APP_VERSION                  = "WebviewAppVersion";
    String WEBVIEW_APP_VERSION_MAJOR            = "WebviewAppVersionMajor";
    String WEBVIEW_APP_NAME_VERSION             = "WebviewAppNameVersion";
    String WEBVIEW_APP_NAME_VERSION_MAJOR       = "WebviewAppNameVersionMajor";

    String FACEBOOK_CARRIER                     = "FacebookCarrier";
    String FACEBOOK_DEVICE_CLASS                = "FacebookDeviceClass";
    String FACEBOOK_DEVICE_NAME                 = "FacebookDeviceName";
    String FACEBOOK_DEVICE_VERSION              = "FacebookDeviceVersion";
    String FACEBOOK_F_B_O_P                     = "FacebookFBOP";
    String FACEBOOK_F_B_S_S                     = "FacebookFBSS";
    String FACEBOOK_OPERATING_SYSTEM_NAME       = "FacebookOperatingSystemName";
    String FACEBOOK_OPERATING_SYSTEM_VERSION    = "FacebookOperatingSystemVersion";

    String HACKER_ATTACK_VECTOR                 = "HackerAttackVector";
    String HACKER_TOOLKIT                       = "HackerToolkit";

    String KOBO_AFFILIATE                       = "KoboAffiliate";
    String KOBO_PLATFORM_ID                     = "KoboPlatformId";

    String IE_COMPATIBILITY_VERSION             = "IECompatibilityVersion";
    String IE_COMPATIBILITY_VERSION_MAJOR       = "IECompatibilityVersionMajor";
    String IE_COMPATIBILITY_NAME_VERSION        = "IECompatibilityNameVersion";
    String IE_COMPATIBILITY_NAME_VERSION_MAJOR  = "IECompatibilityNameVersionMajor";

    String SYNTAX_ERROR                         = "__SyntaxError__";
    String USERAGENT_FIELDNAME                  = "Useragent";

    // Similar to the fields we have for all client hints
    String USERAGENT_HEADER                     = "User-Agent";
    String USERAGENT_HEADER_SPEC_URL            = "https://datatracker.ietf.org/doc/html/rfc7231#section-5.5.3";
    String USERAGENT_HEADER_SPEC                = "The \"User-Agent\" header field contains information about the user agent originating the request,";
    String USERAGENT_HEADER_FIELDNAME           = "userAgent";

    String NETWORK_TYPE                         = "NetworkType";

    String UACLIENT_HINT_BRANDS                 = "UAClientHintBrands";
    String UACLIENT_HINT_ARCHITECTURE           = "UAClientHintArchitecture";
    String UACLIENT_HINT_BITNESS                = "UAClientHintBitness";
    String UACLIENT_HINT_FORMFACTOR             = "UAClientHintFormFactor";
    String UACLIENT_HINT_FULL_VERSION           = "UAClientHintFullVersion";
    String UACLIENT_HINT_FULL_VERSION_LIST      = "UAClientHintFullVersionList";
    String UACLIENT_HINT_MOBILE                 = "UAClientHintMobile";
    String UACLIENT_HINT_MODEL                  = "UAClientHintModel";
    String UACLIENT_HINT_PLATFORM               = "UAClientHintPlatform";
    String UACLIENT_HINT_PLATFORM_VERSION       = "UAClientHintPlatformVersion";
    String UACLIENT_HINT_WOW_64                 = "UAClientHintWow64";

    String REMARKABLE_PATTERN                   = "RemarkablePattern";

    String SET_ALL_FIELDS                       = "__Set_ALL_Fields__";
    String NULL_VALUE                           = "<<>>";
    String UNKNOWN_VALUE                        = "Unknown";
    String UNKNOWN_VERSION                      = "??";
    String UNKNOWN_NAME_VERSION                 = "Unknown ??";

    List STANDARD_FIELDS = Collections.unmodifiableList(Arrays.asList(
        DEVICE_CLASS,
        DEVICE_BRAND,
        DEVICE_NAME,
        OPERATING_SYSTEM_CLASS,
        OPERATING_SYSTEM_NAME,
        OPERATING_SYSTEM_VERSION,
        OPERATING_SYSTEM_VERSION_MAJOR,
        OPERATING_SYSTEM_NAME_VERSION,
        OPERATING_SYSTEM_NAME_VERSION_MAJOR,
        LAYOUT_ENGINE_CLASS,
        LAYOUT_ENGINE_NAME,
        LAYOUT_ENGINE_VERSION,
        LAYOUT_ENGINE_VERSION_MAJOR,
        LAYOUT_ENGINE_NAME_VERSION,
        LAYOUT_ENGINE_NAME_VERSION_MAJOR,
        AGENT_CLASS,
        AGENT_NAME,
        AGENT_VERSION,
        AGENT_VERSION_MAJOR,
        AGENT_NAME_VERSION,
        AGENT_NAME_VERSION_MAJOR
    ));

    // We manually sort the list of fields to ensure the output is consistent.
    // Any unspecified fieldnames will be appended to the end.
    List PRE_SORTED_FIELDS_LIST = Collections.unmodifiableList(Arrays.asList(
        DEVICE_CLASS,
        DEVICE_NAME,
        DEVICE_BRAND,
        DEVICE_CPU,
        DEVICE_CPU_BITS,
        DEVICE_FIRMWARE_VERSION,
        DEVICE_VERSION,

        OPERATING_SYSTEM_CLASS,
        OPERATING_SYSTEM_NAME,
        OPERATING_SYSTEM_VERSION,
        OPERATING_SYSTEM_VERSION_MAJOR,
        OPERATING_SYSTEM_NAME_VERSION,
        OPERATING_SYSTEM_NAME_VERSION_MAJOR,
        OPERATING_SYSTEM_VERSION_BUILD,

        LAYOUT_ENGINE_CLASS,
        LAYOUT_ENGINE_NAME,
        LAYOUT_ENGINE_VERSION,
        LAYOUT_ENGINE_VERSION_MAJOR,
        LAYOUT_ENGINE_NAME_VERSION,
        LAYOUT_ENGINE_NAME_VERSION_MAJOR,
        LAYOUT_ENGINE_BUILD,

        AGENT_CLASS,
        AGENT_NAME,
        AGENT_VERSION,
        AGENT_VERSION_MAJOR,
        AGENT_NAME_VERSION,
        AGENT_NAME_VERSION_MAJOR,
        AGENT_BUILD,
        AGENT_LANGUAGE,
        AGENT_LANGUAGE_CODE,
        AGENT_INFORMATION_EMAIL,
        AGENT_INFORMATION_URL,
        AGENT_SECURITY,
        AGENT_UUID,

        WEBVIEW_APP_NAME,
        WEBVIEW_APP_VERSION,
        WEBVIEW_APP_VERSION_MAJOR,
        WEBVIEW_APP_NAME_VERSION,
        WEBVIEW_APP_NAME_VERSION_MAJOR,

        FACEBOOK_CARRIER,
        FACEBOOK_DEVICE_CLASS,
        FACEBOOK_DEVICE_NAME,
        FACEBOOK_DEVICE_VERSION,
        FACEBOOK_F_B_O_P,
        FACEBOOK_F_B_S_S,
        FACEBOOK_OPERATING_SYSTEM_NAME,
        FACEBOOK_OPERATING_SYSTEM_VERSION,

        REMARKABLE_PATTERN,

        HACKER_ATTACK_VECTOR,
        HACKER_TOOLKIT,

        KOBO_AFFILIATE,
        KOBO_PLATFORM_ID,

        IE_COMPATIBILITY_VERSION,
        IE_COMPATIBILITY_VERSION_MAJOR,
        IE_COMPATIBILITY_NAME_VERSION,
        IE_COMPATIBILITY_NAME_VERSION_MAJOR,

        SYNTAX_ERROR
    ));

    default String escapeYaml(String input) {
        if (input == null) {
            return NULL_VALUE;
        }
        return input.replace("'", "''");
    }

    default String toYamlTestCase() {
        return toYamlTestCase(false,  getCleanedAvailableFieldNamesSorted(), null);
    }

    default String toYamlTestCase(List fieldNames) {
        return toYamlTestCase(false, fieldNames, null);
    }

    default String toYamlTestCase(boolean showConfidence) {
        return toYamlTestCase(showConfidence, getCleanedAvailableFieldNamesSorted(), null);
    }

    default String toYamlTestCase(boolean showConfidence, Map comments) {
        return toYamlTestCase(showConfidence, getCleanedAvailableFieldNamesSorted(), comments);
    }

    default String toYamlTestCase(boolean showConfidence, List fieldNames) {
        return toYamlTestCase(showConfidence, fieldNames, null);
    }

    default String toYamlTestCase(boolean showConfidence, List fieldNames, Map comments) {
        StringBuilder sb = new StringBuilder(10240);
        sb.append("\n");
        sb.append("- test:\n");
        sb.append("    input:\n");

        int maxNameLength  = fieldNames.stream().map(String::length).max(Integer::compareTo).orElse(0);
        int maxHeaderNameLength = getHeaders().keySet().stream().map(String::length).max(Integer::compareTo).orElse(0);

        maxNameLength = Math.max(30, Math.max(maxNameLength, maxHeaderNameLength)) + 6;

        sb.append("      ").append(capitalizeFully(USERAGENT_HEADER, ' ', '-'));
        for (int l = USERAGENT_HEADER.length(); l < maxNameLength; l++) {
            sb.append(' ');
        }
        sb.append(": '").append(escapeYaml(getUserAgentString())).append("'\n");
        for (Map.Entry headerEntry : getHeaders().entrySet()) {
            if (!USERAGENT_HEADER.equals(headerEntry.getKey())) {
                sb.append("      ").append(escapeYaml(capitalizeFully(headerEntry.getKey(), ' ', '-')));
                for (int l = headerEntry.getKey().length(); l < maxNameLength; l++) {
                    sb.append(' ');
                }
                sb.append(": '").append(escapeYaml(headerEntry.getValue())).append("'\n");
            }
        }

//        sb.append("      user_agent_string: '").append(escapeYaml(getUserAgentString())).append("'\n");
        sb.append("    expected:\n");

        int maxValueLength = 0;
        for (String fieldName : fieldNames) {
            String value = escapeYaml(getValue(fieldName));
            if (value != null) {
                maxValueLength = Math.max(maxValueLength, value.length());
            }
        }

        for (String fieldName : fieldNames) {
            AgentField field = get(fieldName);
            sb.append("      ").append(fieldName);
            for (int l = fieldName.length(); l < maxNameLength; l++) {
                sb.append(' ');
            }
            String value = escapeYaml(field.getValue());
            sb.append(": '").append(value).append('\'');

            if (showConfidence || comments != null) {
                int l = value.length();
                for (; l < maxValueLength + 4; l++) {
                    sb.append(' ');
                }
                sb.append("# ");
                if (showConfidence) {
                    sb.append(String.format("%8d", getConfidence(fieldName)));
                    if (field.isDefaultValue()) {
                        sb.append(" [Default]");
                    }
                }
                if (comments != null) {
                    String comment = comments.get(fieldName);
                    if (comment != null) {
                        if (!field.isDefaultValue()) {
                            sb.append("          ");
                        }
                        sb.append(" | ").append(comment);
                    }
                }
            }

            sb.append('\n');
        }

        return sb.toString();
    }

    default Map toMap() {
        List fields = new ArrayList<>();
        fields.add(USERAGENT_FIELDNAME);
        fields.addAll(getAvailableFieldNamesSorted());
        return toMap(fields);
    }

    default Map toMap(String... fieldNames) {
        return toMap(Arrays.asList(fieldNames));
    }

    default Map toMap(List fieldNames) {
        Map result = new TreeMap<>();

        for (String fieldName : fieldNames) {
            if (USERAGENT_FIELDNAME.equalsIgnoreCase(fieldName) || USERAGENT_HEADER.equalsIgnoreCase(fieldName)) {
                result.put(USERAGENT_FIELDNAME, getUserAgentString());
            } else {
                AgentField field = get(fieldName);
                result.put(fieldName, field.getValue());
            }
        }
        return result;
    }

    default String toJson() {
        List fields = new ArrayList<>();
        fields.add(USERAGENT_FIELDNAME);
        fields.addAll(getAvailableFieldNamesSorted());
        return toJson(fields);
    }

    default String toJson(String... fieldNames) {
        return toJson(Arrays.asList(fieldNames));
    }

    default String toJson(List fieldNames) {
        StringBuilder sb = new StringBuilder(10240);
        sb.append("{");

        boolean addSeparator = false;
        for (String fieldName : fieldNames) {
            if (addSeparator) {
                sb.append(',');
            } else {
                addSeparator = true;
            }
            if (USERAGENT_FIELDNAME.equals(fieldName)) {
                sb
                    .append("\"Useragent\"")
                    .append(':')
                    .append('"').append(StringEscapeUtils.escapeJson(getUserAgentString())).append('"');
            } else {
                AgentField field = get(fieldName);
                sb
                    .append('"').append(StringEscapeUtils.escapeJson(fieldName)).append('"')
                    .append(':')
                    .append('"').append(StringEscapeUtils.escapeJson(field.getValue())).append('"');
            }
        }

        sb.append("}");
        return sb.toString();
    }

    default String toXML() {
        List fields = new ArrayList<>();
        fields.add(USERAGENT_FIELDNAME);
        fields.addAll(getAvailableFieldNamesSorted());
        return toXML(fields);
    }

    default String toXML(String... fieldNames) {
        return toXML(Arrays.asList(fieldNames));
    }

    default String toXML(List fieldNames) {
        StringBuilder sb =
            new StringBuilder(10240)
                .append("");

        for (String fieldName : fieldNames) {
            if (USERAGENT_FIELDNAME.equals(fieldName)) {
                sb
                    .append("")
                    .append(StringEscapeUtils.escapeXml10(getUserAgentString()))
                    .append("");
            } else {
                AgentField field = get(fieldName);
                sb
                    .append('<').append(StringEscapeUtils.escapeXml10(fieldName)).append('>')
                    .append(StringEscapeUtils.escapeXml10(field.getValue()))
                    .append("');
            }
        }

        sb.append("");

        return sb.toString();
    }

    default String toString(String... fieldNames) {
        return toString(Arrays.asList(fieldNames));
    }

    default String toString(List fieldNames) {
        return toYamlTestCase(fieldNames);
    }

    default String toJavaTestCase() {
        return toJavaTestCase(getCleanedAvailableFieldNamesSorted());
    }

    default String toJavaTestCase(List fieldNames) {
        StringBuilder sb        = new StringBuilder();
        int    maxValueLength   = 0;
        for (String fieldName : fieldNames) {
            maxValueLength = Math.max(maxValueLength, StringEscapeUtils.escapeJava(getValue(fieldName)).length());
        }

        for (String fieldName : fieldNames) {
            if (!USERAGENT_FIELDNAME.equals(fieldName)) {
                AgentField field = get(fieldName);
                if (field != null) {
                    String value = StringEscapeUtils.escapeJava(getValue(fieldName));
                    sb.append("    assertEquals(\"").append(value).append("\", ");
                    for (int l = value.length(); l < maxValueLength + 2; l++) {
                        sb.append(' ');
                    }
                    sb.append("userAgent.getValue(\"").append(StringEscapeUtils.escapeJava(fieldName)).append("\"));");
                    sb.append('\n');
                }
            }
        }

        return sb.toString();
    }


    default String toJavaExpectationsList() {
        return toJavaExpectationsList(getCleanedAvailableFieldNamesSorted());
    }

    default String toJavaExpectationsList(List fieldNames) {
        StringBuilder sb        = new StringBuilder();
        int    maxValueLength   = 0;
        for (String fieldName : fieldNames) {
            maxValueLength = Math.max(maxValueLength, fieldName.length());
        }

        for (String fieldName : fieldNames) {
            if (!USERAGENT_FIELDNAME.equals(fieldName)) {
                AgentField field = get(fieldName);
                if (field != null) {
                    String value = StringEscapeUtils.escapeJava(getValue(fieldName));
                    sb.append("    expectations.put(\"").append(fieldName).append("\", ");
                    for (int l = fieldName.length(); l < maxValueLength + 2; l++) {
                        sb.append(' ');
                    }
                    sb.append("\"").append(value).append("\");");
                    sb.append('\n');
                }
            }
        }

        return sb.toString();
    }

    default boolean uaEquals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof UserAgent)) {
            return false;
        }
        UserAgent agent = (UserAgent) o;
        if (!Objects.equals(getHeaders(), agent.getHeaders())) {
            return false;
        }
        List fieldNamesSorted1 = getAvailableFieldNamesSorted();
        List fieldNamesSorted2 = agent.getAvailableFieldNamesSorted();
        if (!Objects.equals(fieldNamesSorted1, fieldNamesSorted2)) {
            return false;
        }
        for (String fieldName: fieldNamesSorted1) {
            if (!Objects.equals(get(fieldName), agent.get(fieldName))) {
                return false;
            }
        }
        return true;
    }

    default int uaHashCode() {
        int result = Objects.hash(getUserAgentString());
        for (String fieldName: getAvailableFieldNamesSorted()) {
            result = 31 * result + get(fieldName).hashCode();
        }
        return result;
    }

    @SuppressWarnings("this-escape")
    class MutableUserAgent extends UserAgentBaseListener implements UserAgent, Serializable, DefaultANTLRErrorListener {

        private static final Logger LOG = LogManager.getLogger(UserAgent.class);

        private static String getDefaultValueForField(String fieldName) {
            if (fieldName.equals(REMARKABLE_PATTERN)) {
                return "Nothing remarkable found";
            }
            if (fieldName.equals(SYNTAX_ERROR)) {
                return "false";
            }
            if (fieldName.contains("NameVersion")) {
                return UNKNOWN_NAME_VERSION;
            }
            if (fieldName.contains("Version")) {
                return UNKNOWN_VERSION;
            }
            return UNKNOWN_VALUE;
        }

        private Set wantedFieldNames = null;
        private boolean     hasSyntaxError;
        private boolean     hasAmbiguity;
        private int         ambiguityCount;

        public void destroy() {
            wantedFieldNames = null;
        }

        public boolean hasSyntaxError() {
            return hasSyntaxError;
        }

        public boolean hasAmbiguity() {
            return hasAmbiguity;
        }

        public int getAmbiguityCount() {
            return ambiguityCount;
        }

        @Override
        public void syntaxError(
            Recognizer recognizer,
            Object offendingSymbol,
            int line,
            int charPositionInLine,
            String msg,
            RecognitionException e) {
            if (debug) {
                LOG.error("Syntax error");
                LOG.error("Source : {}", getUserAgentString());
                LOG.error("Message: {}", msg);
            }
            hasSyntaxError = true;
            MutableAgentField syntaxError = new MutableAgentField("false");
            syntaxError.setValue("true", 1);
            allFields.put(SYNTAX_ERROR, syntaxError);
        }

        @Override
        public void reportAmbiguity(
            Parser recognizer,
            DFA dfa,
            int startIndex,
            int stopIndex,
            boolean exact,
            BitSet ambigAlts,
            ATNConfigSet configs) {
            hasAmbiguity = true;
            ambiguityCount++;
        }

        // The original input value
        private Map headers = new LinkedCaseInsensitiveMap<>();

        private boolean debug = false;

        public boolean isDebug() {
            return debug;
        }

        public void setDebug(boolean newDebug) {
            this.debug = newDebug;
        }

        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
        @Override
        public boolean equals(Object o) {
            return uaEquals(o);
        }

        @Override
        public int hashCode() {
            return uaHashCode();
        }

        private final Map allFields = new TreeMap<>();

        private void setWantedFieldNames(Collection newWantedFieldNames) {
            if (newWantedFieldNames == null) {
                wantedFieldNames = null;
            } else {
                if (!newWantedFieldNames.isEmpty()) {
                    wantedFieldNames = new LinkedHashSet<>(newWantedFieldNames);
                    for (String wantedFieldName : wantedFieldNames) {
                        set(wantedFieldName, "Nothing", -2);
                    }
                }
            }
        }

        public MutableUserAgent() {
        }

        public MutableUserAgent(ImmutableUserAgent userAgent) {
            headers = new LinkedCaseInsensitiveMap<>();
            headers.putAll(userAgent.headers);
            hasSyntaxError = userAgent.hasSyntaxError;
            hasAmbiguity = userAgent.hasAmbiguity;
            ambiguityCount = userAgent.ambiguityCount;

            for (String fieldName: userAgent.getAvailableFieldNamesSorted()) {
                allFields.put(fieldName, new MutableAgentField(userAgent.get(fieldName)));
            }

            setWantedFieldNames(userAgent.getAvailableFieldNamesSorted());
        }

        public MutableUserAgent(Collection wantedFieldNames) {
            setWantedFieldNames(wantedFieldNames);
        }

        public MutableUserAgent(String userAgentString) {
            // wantedFieldNames == null; --> Assume we want all fields.
            addHeader(USERAGENT_HEADER, userAgentString);
        }

        public MutableUserAgent(String userAgentString, Collection wantedFieldNames) {
            setWantedFieldNames(wantedFieldNames);
            addHeader(USERAGENT_HEADER, userAgentString);
        }

        public void setUserAgentString(String newUserAgentString) {
            headers.put(USERAGENT_HEADER, newUserAgentString);
            reset();
        }

        public String getUserAgentString() {
            return headers.get(USERAGENT_HEADER);
        }

        @Override
        public Map getHeaders() {
            return headers;
        }

        public void setHeaders(Map newHeaders) {
            headers.clear();
            addHeader(newHeaders);
            reset();
        }

        public void addHeader(Map newHeaders) {
            newHeaders.forEach(this::addHeader);
        }

        public void addHeader(String name, String value) {
            this.headers.put(name, value);
        }

        public void reset() {
            hasSyntaxError = false;
            hasAmbiguity = false;
            ambiguityCount = 0;

            allFields.values().forEach(MutableAgentField::reset);
        }

        public static boolean isSystemField(String fieldname) {
            switch (fieldname) {
                case SET_ALL_FIELDS:
                case SYNTAX_ERROR:
                case USERAGENT_FIELDNAME:
                    return true;
                default:
                    return fieldname.startsWith("UAClientHint");
            }
        }

        public void processSetAll() {
            MutableAgentField setAllField = allFields.get(SET_ALL_FIELDS);
            if (setAllField == null) {
                return;
            }

            String value;
            if (setAllField.isDefaultValue()) {
                value = NULL_VALUE;
            } else {
                value = setAllField.getValue();
            }

            long confidence = setAllField.confidence;
            for (Map.Entry fieldEntry : allFields.entrySet()) {
                if (!isSystemField(fieldEntry.getKey())) {
                    fieldEntry.getValue().setValue(value, confidence);
                }
            }
        }

        public void set(String attribute, String value, long confidence) {
            MutableAgentField field = allFields.get(attribute);
            if (field == null) {
                field = new MutableAgentField(getDefaultValueForField(attribute));
            }

            boolean wasEmpty = confidence == -1;
            boolean updated  = field.setValue(value, confidence);
            if (debug && !wasEmpty) {
                if (updated) {
                    LOG.info("USE  {} ({}) = {}", attribute, confidence, value);
                } else {
                    LOG.info("SKIP {} ({}) = {}", attribute, confidence, value);
                }
            }
            allFields.put(attribute, field);
        }

        public void setForced(String attribute, String value, long confidence) {
            MutableAgentField field = allFields.get(attribute);
            if (field == null) {
                field = new MutableAgentField(getDefaultValueForField(attribute));
            }

            boolean wasEmpty = confidence == -1;
            field.setValueForced(value, confidence);
            if (debug && !wasEmpty) {
                LOG.info("USE  {} ({}) = {}", attribute, confidence, value);
            }
            allFields.put(attribute, field);
        }

        // The appliedMatcher parameter is needed for development and debugging.
        public void set(MutableUserAgent newValuesUserAgent, Matcher appliedMatcher) { // NOSONAR: Unused parameter
            for (String fieldName : newValuesUserAgent.allFields.keySet()) {
                MutableAgentField field = newValuesUserAgent.allFields.get(fieldName);
                set(fieldName, field.value, field.confidence);
            }
        }

        void setImmediateForTesting(String fieldName, MutableAgentField agentField) {
            allFields.put(fieldName, agentField);
        }

        public MutableAgentField get(String fieldName) {
            if (USERAGENT_FIELDNAME.equals(fieldName)) {
                String userAgentString = getUserAgentString();
                MutableAgentField agentField = new MutableAgentField(userAgentString);
                agentField.setValue(userAgentString, 0L);
                return agentField;
            } else {
                return allFields
                    .computeIfAbsent(
                        fieldName,
                        f -> new MutableAgentField(getDefaultValueForField(fieldName)));
            }
        }

        public String getValue(String fieldName) {
            if (USERAGENT_FIELDNAME.equals(fieldName)) {
                return getUserAgentString();
            }
            AgentField field = allFields.get(fieldName);
            if (field == null) {
                return getDefaultValueForField(fieldName);
            }
            return field.getValue();
        }

        public Long getConfidence(String fieldName) {
            if (USERAGENT_FIELDNAME.equals(fieldName)) {
                return 0L;
            }
            AgentField field = allFields.get(fieldName);
            if (field == null) {
                return -1L;
            }
            return field.getConfidence();
        }

        @Override
        public List getAvailableFieldNamesSorted() {
            List fieldNames = new ArrayList<>(allFields.size() + 10);
            if (wantedFieldNames == null) {
                fieldNames.addAll(STANDARD_FIELDS);
                allFields.forEach((fieldName, field) -> {
                    if (!fieldNames.contains(fieldName)) {
                        fieldNames.add(fieldName);
                    }
                });
            } else {
                fieldNames.addAll(wantedFieldNames);
            }

            // This is not a field; this is a special operator.
            fieldNames.remove(SET_ALL_FIELDS);

            List result = PRE_SORTED_FIELDS_LIST
                .stream()
                .filter(fieldNames::remove)
                .collect(Collectors.toList());

            Collections.sort(fieldNames);
            result.addAll(fieldNames);

            // This special system field is always available and we put it at the end.
            result.remove(SYNTAX_ERROR);
            result.add(SYNTAX_ERROR);

            return result;
        }

        @Override
        public String toString() {
            return toString(getAvailableFieldNamesSorted());
        }
    }

    @SuppressWarnings("this-escape")
    class ImmutableUserAgent implements UserAgent {
        private final Map               headers;
        private final ImmutableAgentField               userAgentStringField;
        private final Map  allFields;
        private final List                      availableFieldNamesSorted;
        private List                            cleanedAvailableFieldNamesSorted;
        private final boolean                           hasSyntaxError;
        private final boolean                           hasAmbiguity;
        private final int                               ambiguityCount;

        public ImmutableUserAgent(UserAgent userAgent) {
            headers = new LinkedCaseInsensitiveMap<>();
            headers.putAll(userAgent.getHeaders());

            hasSyntaxError = userAgent.hasSyntaxError();
            hasAmbiguity = userAgent.hasAmbiguity();
            ambiguityCount = userAgent.getAmbiguityCount();

            String userAgentString = getUserAgentString();
            userAgentStringField = new ImmutableAgentField(userAgentString, 0L, false, userAgentString);

            Map preparingAllFields = new LinkedHashMap<>(userAgent.getAvailableFieldNamesSorted().size());

            for (String fieldName: userAgent.getAvailableFieldNamesSorted()) {
                preparingAllFields.put(fieldName, new ImmutableAgentField(userAgent.get(fieldName)));
            }

            allFields = Collections.unmodifiableMap(preparingAllFields);
            availableFieldNamesSorted = Collections.unmodifiableList(userAgent.getAvailableFieldNamesSorted());
        }

        @Override
        public String getUserAgentString() {
            return headers.get(USERAGENT_HEADER);
        }

        @Override
        public Map getHeaders() {
            return headers;
        }

        public AgentField get(String fieldName) {
            if (USERAGENT_FIELDNAME.equals(fieldName)) {
                return userAgentStringField;
            } else {
                ImmutableAgentField agentField = allFields.get(fieldName);
                if (agentField == null) {
                    agentField = new ImmutableAgentField(MutableUserAgent.getDefaultValueForField(fieldName),
                        -1,
                        true,
                        MutableUserAgent.getDefaultValueForField(fieldName));
                }
                return agentField;
            }
        }

        public String getValue(String fieldName) {
            if (USERAGENT_FIELDNAME.equals(fieldName)) {
                return getUserAgentString();
            }
            AgentField field = allFields.get(fieldName);
            if (field == null) {
                return MutableUserAgent.getDefaultValueForField(fieldName);
            }
            return field.getValue();
        }

        public Long getConfidence(String fieldName) {
            if (USERAGENT_FIELDNAME.equals(fieldName)) {
                return 0L;
            }
            AgentField field = allFields.get(fieldName);
            if (field == null) {
                return -1L;
            }
            return field.getConfidence();
        }

        public boolean hasSyntaxError() {
            return hasSyntaxError;
        }

        public boolean hasAmbiguity() {
            return hasAmbiguity;
        }

        public int getAmbiguityCount() {
            return ambiguityCount;
        }

        @Override
        public List getAvailableFieldNamesSorted() {
            return availableFieldNamesSorted;
        }

        @Override
        public List getCleanedAvailableFieldNamesSorted() {
            if (cleanedAvailableFieldNamesSorted == null) {
                cleanedAvailableFieldNamesSorted =
                    Collections.unmodifiableList(UserAgent.super.getCleanedAvailableFieldNamesSorted());
            }
            return cleanedAvailableFieldNamesSorted;
        }

        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
        @Override
        public boolean equals(Object o) {
            return uaEquals(o);
        }

        @Override
        public int hashCode() {
            return uaHashCode();
        }

        @Override
        public String toString() {
            return toString(getAvailableFieldNamesSorted());
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy