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

com.scientiamobile.wurfl.wmclient.WmClient Maven / Gradle / Ivy

There is a newer version: 2.1.6
Show newest version
/**
 * Copyright 2018 Scientiamobile Inc.
 * 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.scientiamobile.wurfl.wmclient;

import com.google.gson.Gson;
import com.scientiamobile.wurfl.wmclient.Model.Request;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;

import static com.scientiamobile.wurfl.wmclient.Model.newRequest;

/**
 * Main class for Java WM client. Performs requests to WURFL Microservice server and handles response.
* Author(s): Andrea Castello * Date: 19/07/2017. */ public class WmClient { private final static String DEVICE_ID_CACHE_TYPE = "dId-cache"; private final static String USERAGENT_CACHE_TYPE = "ua-cache"; private static final String CLIENT_TOKEN_HEADER = "X-Md5-Signature"; private String scheme; private String host; private String port; private String baseURI; // These are the lists of all static or virtual that can be returned by the running wm server private String[] staticCaps; private String[] virtualCaps; // Requested are used in the lookup requests, accessible via the SetRequested[...] methods private String[] requestedStaticCaps; private String[] requestedVirtualCaps; private String[] importantHeaders; // Internal caches private LRUCache devIDCache; // Maps device ID -> JSONDeviceData private LRUCache uaCache; // Maps concat headers (mainly UA) -> JSONDeviceData // Time of last WURFL.xml file load on server private String ltime; // Token sent to server for API permission check private String clientToken; // Stores the result of time consuming call getAllMakeModel public Model.JSONMakeModel[] makeModels = new Model.JSONMakeModel[0]; // Lock object used for MakeModel safety private final Object mkMdLock = new Object(); // internal http client private CloseableHttpClient _internalClient; private WmClient(String scheme, String host, String port, String baseURI) throws WmException { this.scheme = scheme; this.host = host; this.port = port; this.baseURI = baseURI; if (StringUtils.isEmpty(scheme)) { throw new WmException("WM client scheme cannot be empty"); } if (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) { PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); // Increase max total connection to 200 cm.setMaxTotal(200); _internalClient = HttpClients.custom().setConnectionManager(cm).build(); } else { throw new WmException("Invalid connection scheme specified: [" + scheme + " ]"); } } private String createUrl(String path) { String bpath = scheme + "://" + host + ":" + port + "/"; if (StringUtils.isNotEmpty(baseURI)) { bpath += baseURI + "/"; } return bpath + "/" + path; } /** * Creates an instance of a WURFL Microservice client * @param scheme protocol scheme * @param host host of the WM server * @param port port of the WM server * @param baseURI any base URI which must be added after the host (NOT including the endpoints, which are handled by the client). * This may be useful, for example, with thrird parties VMs (like docker or AWS). Leave empty or null if not needed. * @return The instance of the WM client * @throws WmException In case a connection error occurs */ public static WmClient create(String scheme, String host, String port, String baseURI) throws WmException { try { WmClient client = new WmClient(scheme, host, port, baseURI); client.createMD5Token(); // Test server connection and save important headers taken using getInfo function Model.JSONInfoData info = client.getInfo(); if (!checkData(info)) { throw new WmException("Error getting data from WM server : client not authorized"); } client.importantHeaders = info.getImportantHeaders(); client.staticCaps = info.getStaticCaps(); client.virtualCaps = info.getVirtualCaps(); Arrays.sort(client.staticCaps); Arrays.sort(client.virtualCaps); client.ltime = info.ltime; return client; } catch (Exception e) { throw new WmException("Unable to create wm client: " + e.getMessage()); } } private static boolean checkData(Model.JSONInfoData info) { // If these are empty there's something wrong return StringUtils.isNotEmpty(info.getWmVersion()) && StringUtils.isNotEmpty(info.getWurflApiVersion()) && StringUtils.isNotEmpty(info.getWurflInfo()) && (ArrayUtils.isNotEmpty(info.getStaticCaps()) || ArrayUtils.isNotEmpty(info.getVirtualCaps())); } /** * @return A JSONInfoData instance holding the capabilities exposed from WM server, the headers used for device detection, WURFL file and API version * @throws WmException If server cannot send data or incomplete data are sent */ public Model.JSONInfoData getInfo() throws WmException { try { final HttpGet req = new HttpGet(createUrl("/v2/getinfo/json")); req.addHeader(CLIENT_TOKEN_HEADER, this.clientToken); Class type = Model.JSONInfoData.class; Model.JSONInfoData info = _internalClient.execute(req, new WmDataHandler(type)); // Check if cache must be cleared clearCachesIfNeeded(info.ltime); return info; } catch (Exception e) { throw new WmException("Unable to get information from WM server :" + e.getMessage(), e); } } /** * @return An array of JSONMakeModel structures, holding brand, model and marketing name of all devices handled by WM server. * @throws WmException In case a connection error occurs or malformed data are sent */ public Model.JSONMakeModel[] getAllMakeModel() throws WmException { // If makeModel cache has values we return them synchronized (mkMdLock) { if (this.makeModels != null && this.makeModels.length > 0) { return this.makeModels; } } // No cache found, let's do a server lookup try { final HttpGet req = new HttpGet(createUrl("/v2/alldevices/json")); req.addHeader(CLIENT_TOKEN_HEADER, this.clientToken); Class type = Model.JSONMakeModel[].class; Model.JSONMakeModel[] localMakeModels = _internalClient.execute(req, new WmDataHandler(type)); synchronized (mkMdLock) { if (this.makeModels == null || this.makeModels.length == 0) { this.makeModels = localMakeModels; } } return localMakeModels; } catch (IOException e) { throw new WmException("An error occurred gettin all devices " + e.getMessage(), e); } } /** * Performs a device detection against a user agent header * @param useragent a user agent header * @return An object containing the device capabilities * @throws WmException In case any error occurs during device detection */ public Model.JSONDeviceData lookupUseragent(String useragent) throws WmException { Map headers = new HashMap(); headers.put("User-Agent", useragent); Request request = newRequest(headers, this.requestedStaticCaps, this.requestedVirtualCaps, null); return internalRequest("/v2/lookupuseragent/json", request, USERAGENT_CACHE_TYPE); } /** * Returns the device matching the given WURFL ID * @param wurflId a WURFL device identifier * @return An object containing the device capabilities * @throws WmException In case any error occurs */ public Model.JSONDeviceData lookupDeviceId(String wurflId) throws WmException { Request request = newRequest(null, this.requestedStaticCaps, this.requestedVirtualCaps, wurflId); return internalRequest("/v2/lookupdeviceid/json", request, DEVICE_ID_CACHE_TYPE); } /** * Performs a device detection using an HTTP request object, as passed from Java Web applications * @param httpRequest an instance of HTTPServletRequest * @return An object containing the device capabilities * @throws WmException In case any error occurs during device detection */ public Model.JSONDeviceData lookupRequest(HttpServletRequest httpRequest) throws WmException { if (httpRequest == null) { throw new WmException("HttpServletRequest cannot be null"); } Map reqHeaders = new HashMap(); for (String hname : importantHeaders) { String hval = httpRequest.getHeader(hname); if (!StringUtils.isEmpty(hval)) { reqHeaders.put(hname, hval); } } Model.JSONDeviceData device = internalRequest("/v2/lookuprequest/json", newRequest(reqHeaders, this.requestedStaticCaps, this.requestedVirtualCaps, null), USERAGENT_CACHE_TYPE); return device; } public void setRequestedStaticCapabilities(String[] capsList) { if (capsList == null) { this.requestedStaticCaps = null; this.clearCaches(); return; } List stCaps = new ArrayList(); for (String name : capsList) { if (hasStaticCapability(name)) { stCaps.add(name); } } this.requestedStaticCaps = stCaps.toArray(new String[stCaps.size()]); clearCaches(); } public void setRequestedVirtualCapabilities(String[] vcapsList) { if (vcapsList == null) { this.requestedVirtualCaps = null; this.clearCaches(); return; } List vCaps = new ArrayList(); for (String name : vcapsList) { if (hasVirtualCapability(name)) { vCaps.add(name); } } this.requestedVirtualCaps = vCaps.toArray(new String[vCaps.size()]); clearCaches(); } /** * * @param capName capability name * @return true if the given static capability is handled by this client, false otherwise */ public boolean hasStaticCapability(String capName) { return ArrayUtils.contains(this.staticCaps, capName); } /** * @param capName capability name * @return true if the given virtual capability is handled by this client, false otherwise */ public boolean hasVirtualCapability(String capName) { return ArrayUtils.contains(this.virtualCaps, capName); } public void setRequestedCapabilities(String[] capsList) { if (capsList == null) { this.requestedStaticCaps = null; this.requestedVirtualCaps = null; this.clearCaches(); return; } List capNames = new ArrayList(); List vcapNames = new ArrayList(); for (String name : capsList) { if (hasStaticCapability(name)) { capNames.add(name); } else if (hasVirtualCapability(name)) { vcapNames.add(name); } } if (CollectionUtils.isNotEmpty(capNames)) { this.requestedStaticCaps = capNames.toArray(new String[capNames.size()]); } if (CollectionUtils.isNotEmpty(vcapNames)) { this.requestedVirtualCaps = vcapNames.toArray(new String[vcapNames.size()]); } clearCaches(); } /** * Deallocates all resources used by client. All subsequent usage of client will result in a WmException (you need to create the client again * with a call to WmClient.create(). * @throws WmException In case of closing connection errors. */ public void destroyConnection() throws WmException { try { clearCaches(); uaCache = null; devIDCache = null; makeModels = null; _internalClient.close(); } catch (IOException e) { throw new WmException("Unable to close client: " + e.getMessage(), e); } } /** * @return All static capabilities handled by this client */ public String[] getStaticCaps() { return staticCaps; } /** * @return All the virtual capabilities handled by this client */ public String[] getVirtualCaps() { return virtualCaps; } /** * @return list all HTTP headers used for device detection by this client */ public String[] getImportantHeaders() { return importantHeaders; } private Model.JSONDeviceData internalRequest(String path, Request request, String cacheType) throws WmException { Model.JSONDeviceData device; String cacheKey = null; if (DEVICE_ID_CACHE_TYPE.equals(cacheType)) { cacheKey = request.getWurflId(); } else if (USERAGENT_CACHE_TYPE.equals(cacheType)) { cacheKey = this.getUserAgentCacheKey(request.getLookupHeaders(), cacheType); } // First, do a cache lookup if (StringUtils.isNotEmpty(cacheType) && !StringUtils.isNotEmpty(cacheKey)) { if (cacheType.equals(DEVICE_ID_CACHE_TYPE) && devIDCache != null) { device = devIDCache.getEntry(request.getWurflId()); if (device != null) { return device; } } else if (cacheType.equals(USERAGENT_CACHE_TYPE) && uaCache != null) { device = uaCache.getEntry(cacheKey); if (device != null) { return device; } } } // No device found in cache, let's try a server lookup Gson gson = new Gson(); StringEntity requestEntity = new StringEntity( gson.toJson(request), ContentType.APPLICATION_JSON); HttpPost postMethod = new HttpPost(createUrl(path)); postMethod.addHeader(CLIENT_TOKEN_HEADER, this.clientToken); postMethod.setEntity(requestEntity); Class type = Model.JSONDeviceData.class; try { device = _internalClient.execute(postMethod, new WmDataHandler(type)); if (StringUtils.isNotEmpty(device.error)) { throw new WmException("Unable to complete request to WM server: " + device.error); } // Check if caches must be cleared before adding a new device clearCachesIfNeeded(device.ltime); if (cacheType != null) { if (cacheType.equals(USERAGENT_CACHE_TYPE) && devIDCache != null && !cacheKey.equals("")) { safePutDevice(uaCache, cacheKey, device); } else if (cacheType.equals(DEVICE_ID_CACHE_TYPE) && uaCache != null && !cacheKey.equals("")) { safePutDevice(devIDCache, cacheKey, device); } } return device; } catch (Exception e) { throw new WmException("Unable to complete request to WM server: " + e.getMessage(), e); } } /** * Sets the client cache size * @param uaMaxEntries maximum cache dimension */ public void setCacheSize(int uaMaxEntries) { this.uaCache = new LRUCache(uaMaxEntries); this.devIDCache = new LRUCache(); // this has the default cache size } /** * @return This client API version */ public String getApiVersion() { return "1.1.0.0"; } private void clearCaches() { if (uaCache != null) { uaCache.clear(); } if (devIDCache != null) { devIDCache.clear(); } makeModels = new Model.JSONMakeModel[0]; } private void clearCachesIfNeeded(String ltime) { if (ltime != null && !ltime.equals(this.ltime)) { this.ltime = ltime; clearCaches(); } } private String getUserAgentCacheKey(Map headers, String cacheType) throws WmException { String key = ""; if (headers == null && USERAGENT_CACHE_TYPE.equals(cacheType)) { throw new WmException("No User-Agent provided"); } // Using important headers array preserves header name order for (String h : importantHeaders) { String hval = headers.get(h); if (hval != null) { key += hval; } } return key; } private void safePutDevice(LRUCache cache, String key, Model.JSONDeviceData device) { if (cache != null) { cache.putEntry(key, device); } } public int[] getActualCacheSizes() { int[] csize = new int[2]; if (devIDCache != null) { csize[0] = devIDCache.size(); } if (uaCache != null) { csize[1] = uaCache.size(); } return csize; } private void createMD5Token() throws WmException { try { MessageDigest md = MessageDigest.getInstance("MD5"); Date now = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); String strDate = sdf.format(now); md.update(strDate.getBytes("UTF-8"), 0, strDate.length()); String s = new BigInteger(1, md.digest()).toString(16); StringBuilder b = new StringBuilder(s); b.replace(11, 12, "0"); b.replace(17, 18, "4"); this.clientToken = b.toString(); } catch (Exception e) { throw new WmException("An error occurred generating MD5 token", e); } } } class WmDataHandler implements ResponseHandler { private Class type; public WmDataHandler(Class type) { this.type = type; } @Override public T handleResponse(HttpResponse res) throws IOException { Gson gson = new Gson(); int status = res.getStatusLine().getStatusCode(); String json; if (status >= 200 && status < 300) { HttpEntity entity = res.getEntity(); json = entity != null ? EntityUtils.toString(entity) : null; T result = gson.fromJson(json, type); return result; } else { throw new ClientProtocolException("Unexpected response status: " + status); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy