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

com.phloc.web.useragent.uaprofile.UAProfileDatabase Maven / Gradle / Ivy

/**
 * Copyright (C) 2006-2015 phloc systems
 * http://www.phloc.com
 * office[at]phloc[dot]com
 *
 * 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 com.phloc.web.useragent.uaprofile;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import jakarta.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.phloc.commons.CGlobal;
import com.phloc.commons.ValueEnforcer;
import com.phloc.commons.annotations.PresentForCodeCoverage;
import com.phloc.commons.annotations.ReturnsMutableCopy;
import com.phloc.commons.base64.Base64Helper;
import com.phloc.commons.callback.INonThrowingRunnableWithParameter;
import com.phloc.commons.collections.ContainerHelper;
import com.phloc.commons.regex.RegExHelper;
import com.phloc.commons.string.StringHelper;
import com.phloc.commons.string.StringParser;
import com.phloc.web.servlet.request.RequestHelper;

/**
 * Central cache for known UAProfiles.
 * 
 * @author Philip Helger
 */
@ThreadSafe
public final class UAProfileDatabase
{
  // See http://de.wikipedia.org/wiki/UAProf - references the device URL
  public static final String X_WAP_PROFILE = "X-Wap-Profile";
  public static final String PROFILE = "Profile";
  public static final String WAP_PROFILE = "Wap-Profile";
  public static final String MAN = "Man";
  public static final String OPT = "Opt";

  public static final String X_WAP_PROFILE_DIFF = "X-Wap-Profile-Diff";
  public static final String PROFILE_DIFF = "Profile-Diff";
  public static final String WAP_PROFILE_DIFF = "Wap-Profile-Diff";

  public static final int EXPECTED_MD5_DIGEST_LENGTH = 16;

  private static final String REQUEST_ATTR = UAProfileDatabase.class.getName ();
  private static final Logger s_aLogger = LoggerFactory.getLogger (UAProfileDatabase.class);

  private static final ReadWriteLock s_aRWLock = new ReentrantReadWriteLock ();
  @GuardedBy ("s_aRWLock")
  private static final Set  s_aUniqueUAProfiles = new HashSet  ();
  @GuardedBy ("s_aRWLock")
  private static INonThrowingRunnableWithParameter  s_aNewUAProfileCallback;

  @PresentForCodeCoverage
  @SuppressWarnings ("unused")
  private static final UAProfileDatabase s_aInstance = new UAProfileDatabase ();

  private UAProfileDatabase ()
  {}

  @Nullable
  public static INonThrowingRunnableWithParameter  getNewUAProfileCallback ()
  {
    s_aRWLock.readLock ().lock ();
    try
    {
      return s_aNewUAProfileCallback;
    }
    finally
    {
      s_aRWLock.readLock ().unlock ();
    }
  }

  public static void setNewUAProfileCallback (@Nullable final INonThrowingRunnableWithParameter  aCallback)
  {
    s_aRWLock.writeLock ().lock ();
    try
    {
      s_aNewUAProfileCallback = aCallback;
    }
    finally
    {
      s_aRWLock.writeLock ().unlock ();
    }
  }

  @Nullable
  private static String _getExtendedNamespaceValue (@Nonnull final String sOpt)
  {
    final Matcher aMatcher = RegExHelper.getMatcher (".+ns=(\\d+).*", sOpt);
    return aMatcher.matches () ? aMatcher.group (1) : null;
  }

  @Nonnull
  private static String _getUnifiedHeaderName (@Nonnull final String s)
  {
    return s.toLowerCase (Locale.US);
  }

  @Nullable
  private static String _getCleanedUp (@Nullable final String s)
  {
    if (StringHelper.hasNoText (s))
      return s;

    // trim string
    String sValue = s.trim ();

    // Cut surrounding quotes (if any)
    if (StringHelper.getFirstChar (sValue) == '"')
      sValue = sValue.substring (1);
    if (StringHelper.getLastChar (sValue) == '"')
      sValue = sValue.substring (0, sValue.length () - 1);
    return sValue;
  }

  @Nonnull
  private static Map  _getProfileDiffData (@Nonnull final HttpServletRequest aHttpRequest,
                                                            final String sExtNSValue)
  {
    // Determine the profile diffs to use
    Enumeration  aProfileDiffs = RequestHelper.getRequestHeaders (aHttpRequest, X_WAP_PROFILE_DIFF);
    if (!aProfileDiffs.hasMoreElements ())
    {
      aProfileDiffs = RequestHelper.getRequestHeaders (aHttpRequest, PROFILE_DIFF);
      if (!aProfileDiffs.hasMoreElements ())
        aProfileDiffs = RequestHelper.getRequestHeaders (aHttpRequest, WAP_PROFILE_DIFF);
    }

    // Parse the diffs
    final Map  aProfileDiffData = new HashMap  ();
    while (aProfileDiffs.hasMoreElements ())
    {
      String sProfileDiff = aProfileDiffs.nextElement ();
      sProfileDiff = sProfileDiff.trim ();

      // Find the profile diff index (e.g. '1; aAllHeaders = RequestHelper.getRequestHeaderNames (aHttpRequest);
      while (aAllHeaders.hasMoreElements ())
      {
        final String sHeaderName = _getUnifiedHeaderName (aAllHeaders.nextElement ());
        if (sHeaderName.startsWith (sPrefix))
        {
          // We found a matching profile-diff header (e.g. "80-Profile-Diff-1")
          final int nIndex = StringParser.parseInt (sHeaderName.substring (sPrefix.length ()), CGlobal.ILLEGAL_UINT);
          if (nIndex != CGlobal.ILLEGAL_UINT)
          {
            // Handle profile diff
            String sProfileDiff = aHttpRequest.getHeader (sHeaderName);
            sProfileDiff = _getCleanedUp (sProfileDiff);
            aProfileDiffData.put (Integer.valueOf (nIndex), sProfileDiff);
          }
          else
            s_aLogger.warn ("Failed to extract numerical number from header name '" + sHeaderName + "'");
        }
      }
    }
    return aProfileDiffData;
  }

  @Nullable
  public static UAProfile getUAProfileFromRequest (@Nonnull final HttpServletRequest aHttpRequest)
  {
    ValueEnforcer.notNull (aHttpRequest, "httpRequest");

    // Determine the main profile to use
    String sExtNSValue = null;
    Enumeration  aProfiles = RequestHelper.getRequestHeaders (aHttpRequest, X_WAP_PROFILE);
    if (!aProfiles.hasMoreElements ())
    {
      aProfiles = RequestHelper.getRequestHeaders (aHttpRequest, PROFILE);
      if (!aProfiles.hasMoreElements ())
      {
        aProfiles = RequestHelper.getRequestHeaders (aHttpRequest, WAP_PROFILE);
        if (!aProfiles.hasMoreElements ())
        {
          // Check CCPP headers
          String sExt = aHttpRequest.getHeader (OPT);
          if (sExt == null)
            sExt = aHttpRequest.getHeader (MAN);
          if (sExt != null)
          {
            sExtNSValue = _getExtendedNamespaceValue (sExt);
            if (sExtNSValue != null)
            {
              aProfiles = RequestHelper.getRequestHeaders (aHttpRequest, sExtNSValue + "-Profile");
              if (!aProfiles.hasMoreElements ())
                s_aLogger.warn ("Found CCPP header namespace '" + sExtNSValue + "' but found no profile header!");
            }
            else
              s_aLogger.warn ("Failed to extract namespace value from CCPP header '" + sExt + "'");
          }
        }
      }
    }

    // Parse profile headers
    final List  aProfileData = new ArrayList  ();
    final Map  aProfileDiffDigests = new HashMap  ();
    while (aProfiles.hasMoreElements ())
    {
      String sProfile = aProfiles.nextElement ();
      sProfile = _getCleanedUp (sProfile);
      if (StringHelper.hasText (sProfile))
      {
        // Start tokenizing. Example (with stripped leading and trailing
        // quotes):
        // http://www.ex.com/hw," "1-CWccARHXxtYJE+rKkoD8ng==
        final StringTokenizer aTokenizer = new StringTokenizer (sProfile, "\",");
        while (aTokenizer.hasMoreTokens ())
        {
          final String sToken = aTokenizer.nextToken ().trim ();
          if (StringHelper.hasText (sToken))
          {
            final Matcher aMatcher = RegExHelper.getMatcher ("^(\\d+)-(.+)$", sToken);
            if (aMatcher.matches ())
            {
              // It seems to be a profile diff digest
              final String sDiffIndex = aMatcher.group (1);
              final String sDiffDigest = aMatcher.group (2);
              final int nDiffIndex = StringParser.parseInt (sDiffIndex, CGlobal.ILLEGAL_UINT);
              if (nDiffIndex != CGlobal.ILLEGAL_UINT)
              {
                if (StringHelper.hasText (sDiffDigest))
                {
                  final byte [] aDigest = Base64Helper.safeDecode (sDiffDigest);
                  if (aDigest != null)
                  {
                    // MD5 hashes have 16 bytes!
                    if (aDigest.length == EXPECTED_MD5_DIGEST_LENGTH)
                      aProfileDiffDigests.put (Integer.valueOf (nDiffIndex), aDigest);
                    else
                      s_aLogger.warn ("Decoded Base64 profile diff digest has an illegal length of " + aDigest.length);
                  }
                  else
                    s_aLogger.warn ("Failed to decode Base64 profile diff digest '" +
                                    sDiffDigest +
                                    "' from token '" +
                                    sToken +
                                    "'");
                }
                else
                  s_aLogger.warn ("Found no diff digest in token '" + sToken + "'");
              }
              else
                s_aLogger.warn ("Failed to parse profile diff index from '" + sToken + "'");
            }
            else
            {
              // Assume it is a URL
              try
              {
                new URL (sToken);
                aProfileData.add (sToken);
              }
              catch (final MalformedURLException ex)
              {
                s_aLogger.error ("Failed to convert profile token '" + sToken + "' to a URL!");
              }
            }
          }
        }
      }
    }

    if (aProfileData.isEmpty () && aProfileDiffDigests.isEmpty ())
    {
      // No UAProfile found -> no need to look for differences
      return null;
    }

    // Read diffs
    final Map  aProfileDiffData = _getProfileDiffData (aHttpRequest, sExtNSValue);

    // Merge data and digest
    final Map  aProfileDiffs = new HashMap  ();
    for (final Map.Entry  aEntry : aProfileDiffData.entrySet ())
    {
      final Integer aIndex = aEntry.getKey ();
      final byte [] aDigest = aProfileDiffDigests.get (aIndex);
      if (aDigest != null)
      {
        // Found a matching entry
        aProfileDiffs.put (aIndex, new UAProfileDiff (aEntry.getValue (), aDigest));
      }
      else
        s_aLogger.warn ("Found profile diff data but no digest for index " + aIndex);
    }

    // Consistency check
    for (final Integer aIndex : aProfileDiffDigests.keySet ())
      if (!aProfileDiffData.containsKey (aIndex))
        s_aLogger.warn ("Found profile diff digest but no data for index " + aIndex);

    if (aProfileData.isEmpty () && aProfileDiffs.isEmpty ())
    {
      // This can happen if a diff digest was found, but the diff data is
      // missing!
      return null;
    }

    // And we're done
    return new UAProfile (aProfileData, aProfileDiffs);
  }

  /**
   * Get the user agent object from the given HTTP request.
   * 
   * @param aHttpRequest
   *        The HTTP request to extract the information from.
   * @return A non-null user agent object.
   */
  @Nonnull
  public static UAProfile getUAProfile (@Nonnull final HttpServletRequest aHttpRequest)
  {
    ValueEnforcer.notNull (aHttpRequest, "HttpRequest");

    UAProfile aUAProfile = (UAProfile) aHttpRequest.getAttribute (REQUEST_ATTR);
    if (aUAProfile == null)
    {
      // Extract HTTP header from request
      aUAProfile = getUAProfileFromRequest (aHttpRequest);
      if (aUAProfile == null)
        aUAProfile = UAProfile.EMPTY;

      aHttpRequest.setAttribute (REQUEST_ATTR, aUAProfile);
      if (aUAProfile.isSet ())
      {
        s_aRWLock.writeLock ().lock ();
        try
        {
          if (s_aUniqueUAProfiles.add (aUAProfile))
          {
            if (s_aLogger.isDebugEnabled ())
              s_aLogger.debug ("Found UA-Profile info: " + aUAProfile.toString ());

            if (s_aNewUAProfileCallback != null)
              s_aNewUAProfileCallback.run (aUAProfile);
          }
        }
        finally
        {
          s_aRWLock.writeLock ().unlock ();
        }
      }
    }
    return aUAProfile;
  }

  @Nonnull
  @ReturnsMutableCopy
  public static Set  getUniqueUAProfiles ()
  {
    s_aRWLock.readLock ().lock ();
    try
    {
      return ContainerHelper.newSet (s_aUniqueUAProfiles);
    }
    finally
    {
      s_aRWLock.readLock ().unlock ();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy