Skip to content

Commit

Permalink
feat: Identity API response caching
Browse files Browse the repository at this point in the history
  • Loading branch information
Mansi-mParticle committed Oct 8, 2024
1 parent 2fd32f6 commit b3b63f7
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mparticle.identity

import android.os.Handler
import android.os.Looper
import android.util.MutableBoolean
import com.mparticle.MParticle
import com.mparticle.internal.ConfigManager
Expand All @@ -16,6 +17,7 @@ import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.io.IOException
import java.lang.reflect.Method
import java.util.concurrent.CountDownLatch

class MParticleIdentityClientImplTest : BaseCleanStartedEachTest() {
Expand Down Expand Up @@ -57,6 +59,65 @@ class MParticleIdentityClientImplTest : BaseCleanStartedEachTest() {
Assert.assertTrue(called.value)
}

@Test
@Throws(Exception::class)
fun testLoginWithTwoDifferentUsers() {
// clear existing catch
clearIdentityCache()
val latch: CountDownLatch = MPLatch(2)
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({ Assert.fail("Process not complete") }, (10 * 1000).toLong())
val called = AndroidUtils.Mutable(false)
val identityRequest = IdentityApiRequest.withEmptyUser()
.email("TestEmail@mparticle6.com")
.customerId("TestUser777777")
.build()
// Login with First User
MParticle.getInstance()?.Identity()?.login(identityRequest)?.addSuccessListener {
latch.countDown()
}
// Login With second User
MParticle.getInstance()?.Identity()?.login(IdentityApiRequest.withEmptyUser().build())?.addSuccessListener {
val currentLoginRequestCount = mServer.Requests().login.size
Assert.assertEquals(2, currentLoginRequestCount)
called.value = true
latch.countDown()
}

latch.await()
Assert.assertTrue(called.value)
}

@Test
@Throws(Exception::class)
fun testLoginAndIdentitySameUser() {
// clear existing catch
clearIdentityCache()
val latch: CountDownLatch = MPLatch(2)
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({ Assert.fail("Process not complete") }, (10 * 1000).toLong())
val called = AndroidUtils.Mutable(false)
val identityRequest = IdentityApiRequest.withEmptyUser()
.email("TestEmail@mparticle6.com")
.customerId("TestUser777777")
.build()
// Login with First User
MParticle.getInstance()?.Identity()?.identify(identityRequest)?.addSuccessListener {
latch.countDown()
}
// Login With same User
MParticle.getInstance()?.Identity()?.login(identityRequest)?.addSuccessListener {
val currentLoginRequestCount = mServer.Requests().login.size
Assert.assertEquals(1, currentLoginRequestCount)
val currentIdentityRequestCount = mServer.Requests().login.size
Assert.assertEquals(1, currentIdentityRequestCount)
called.value = true
latch.countDown()
}
latch.await()
Assert.assertTrue(called.value)
}

@Test
@Throws(Exception::class)
fun testIdentifyMessage() {
Expand Down Expand Up @@ -309,6 +370,17 @@ class MParticleIdentityClientImplTest : BaseCleanStartedEachTest() {
}
MParticle.getInstance()?.Identity()?.apiClient = mApiClient
}
private fun clearIdentityCache() {
val mParticleIdentityClient = MParticleIdentityClientImpl(
mContext,
mConfigManager,
MParticle.OperatingSystem.ANDROID
)

val method: Method = MParticleIdentityClientImpl::class.java.getDeclaredMethod("clearCatch")
method.isAccessible = true
method.invoke(mParticleIdentityClient)
}

@Throws(JSONException::class)
private fun checkStaticsAndRemove(knowIdentites: JSONObject) {
Expand Down
22 changes: 22 additions & 0 deletions android-core/src/main/java/com/mparticle/identity/IdentityApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import com.mparticle.internal.MessageManager;
import com.mparticle.internal.listeners.ApiClass;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
Expand Down Expand Up @@ -348,6 +350,26 @@ private void reset() {
}
}

private String generateHash(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(input.getBytes());

// Convert byte array to hex string
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
//throw new RuntimeException("Hashing algorithm not found", e);
}
return null;

}

private BaseIdentityTask makeIdentityRequest(IdentityApiRequest request, final IdentityNetworkRequestRunnable networkRequest) {
if (request == null) {
request = IdentityApiRequest.withEmptyUser().build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
import com.mparticle.internal.Logger;
import com.mparticle.internal.MPUtility;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
* Class that represents observed changes in user state, can be used as a parameter in an Identity Request.
Expand Down Expand Up @@ -211,4 +214,47 @@ public Builder userAliasHandler(@Nullable UserAliasHandler userAliasHandler) {
return this;
}
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true; // Check if the same object
if (obj == null || getClass() != obj.getClass()) return false; // Check for null and class match

IdentityApiRequest that = (IdentityApiRequest) obj; // Cast to IdentityApiRequest

// Compare all relevant fields
return Objects.equals(userIdentities, that.userIdentities) &&
Objects.equals(otherOldIdentities, that.otherOldIdentities) &&
Objects.equals(otherNewIdentities, that.otherNewIdentities) &&
Objects.equals(userAliasHandler, that.userAliasHandler) &&
Objects.equals(mpid, that.mpid);
}

@NonNull
@Override
public String toString() {
return "userIdentities"+userIdentities+" otherOldIdentities " +otherOldIdentities+" otherNewIdentities "+otherNewIdentities
+" userAliasHandler "+userAliasHandler+" mpid "+String.valueOf(mpid);
}

public String convertString(){
return "";
}

public String objectToHash() {
String input =this.toString();
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = md.digest(input.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString().substring(0, 16); // Shorten to first 16 characters
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,31 @@ public String toString() {
}
return builder.toString();
}

public static IdentityHttpResponse fromJson(@NonNull JSONObject jsonObject) throws JSONException {
int httpCode = jsonObject.optInt("http_code", 0);
return new IdentityHttpResponse(httpCode, jsonObject);
}

@NonNull
public JSONObject toJson() throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("http_code", httpCode);
jsonObject.put(MPID, mpId);
jsonObject.put(CONTEXT, context);
jsonObject.put(LOGGED_IN, loggedIn);

if (!errors.isEmpty()) {
JSONArray errorsArray = new JSONArray();
for (Error error : errors) {
JSONObject errorObject = new JSONObject();
errorObject.put(CODE, error.code);
errorObject.put(MESSAGE, error.message);
errorsArray.put(errorObject);
}
jsonObject.put(ERRORS, errorsArray);
}

return jsonObject;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.net.MalformedURLException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -65,6 +66,12 @@ public class MParticleIdentityClientImpl extends MParticleBaseClientImpl impleme

private static final String SERVICE_VERSION_1 = "/v1";
private MParticle.OperatingSystem mOperatingSystem;
public final String LOGIN_CALL = "login";
public final String IDENTIFY_CALL = "identify";
final String IDENTITY_HEADER_TIMEOUT = "X-MP-Max-Age";
static Long maxAgeTimeForIdentityCache = 0L;
static Long identityCacheTime = 0L;
HashMap<String, IdentityHttpResponse> identityCacheArray = new HashMap<>();

public MParticleIdentityClientImpl(Context context, ConfigManager configManager, MParticle.OperatingSystem operatingSystem) {
super(context, configManager);
Expand All @@ -75,18 +82,33 @@ public MParticleIdentityClientImpl(Context context, ConfigManager configManager,

public IdentityHttpResponse login(IdentityApiRequest request) throws JSONException, IOException {
JSONObject jsonObject = getStateJson(request);
IdentityHttpResponse existsResponse = checkIfExists(request, LOGIN_CALL);

if (existsResponse != null) {
return existsResponse;
}
Long maxAgeTime=0L;
Logger.verbose("Identity login request: " + jsonObject.toString());
MPConnection connection = getPostConnection(LOGIN_PATH, jsonObject.toString());
String url = connection.getURL().toString();
InternalListenerManager.getListener().onNetworkRequestStarted(SdkListener.Endpoint.IDENTITY_LOGIN, url, jsonObject, request);
connection = makeUrlRequest(Endpoint.IDENTITY, connection, jsonObject.toString(), false);
int responseCode = connection.getResponseCode();
try {
maxAgeTime = Long.valueOf(connection.getHeaderField(IDENTITY_HEADER_TIMEOUT));
maxAgeTimeForIdentityCache = maxAgeTime;
}catch (Exception e){

}
JSONObject response = MPUtility.getJsonResponse(connection);
InternalListenerManager.getListener().onNetworkRequestFinished(SdkListener.Endpoint.IDENTITY_LOGIN, url, response, responseCode);
return parseIdentityResponse(responseCode, response);
IdentityHttpResponse loginHttpResponse = parseIdentityResponse(responseCode, response);
catchRequest(request, loginHttpResponse, LOGIN_CALL, maxAgeTime);
return loginHttpResponse;
}

public IdentityHttpResponse logout(IdentityApiRequest request) throws JSONException, IOException {
clearCatch();
JSONObject jsonObject = getStateJson(request);
Logger.verbose("Identity logout request: \n" + jsonObject.toString());
MPConnection connection = getPostConnection(LOGOUT_PATH, jsonObject.toString());
Expand All @@ -100,19 +122,33 @@ public IdentityHttpResponse logout(IdentityApiRequest request) throws JSONExcept
}

public IdentityHttpResponse identify(IdentityApiRequest request) throws JSONException, IOException {
IdentityHttpResponse existsResponse = checkIfExists(request, IDENTIFY_CALL);
if (existsResponse != null) {
return existsResponse;
}
JSONObject jsonObject = getStateJson(request);
Long maxAgeTime=0L;
Logger.verbose("Identity identify request: \n" + jsonObject.toString());
MPConnection connection = getPostConnection(IDENTIFY_PATH, jsonObject.toString());
String url = connection.getURL().toString();
InternalListenerManager.getListener().onNetworkRequestStarted(SdkListener.Endpoint.IDENTITY_IDENTIFY, url, jsonObject, request);
connection = makeUrlRequest(Endpoint.IDENTITY, connection, jsonObject.toString(), false);
int responseCode = connection.getResponseCode();
try {
maxAgeTime = Long.valueOf(connection.getHeaderField(IDENTITY_HEADER_TIMEOUT));
maxAgeTimeForIdentityCache = maxAgeTime;
}catch (Exception e){

}
JSONObject response = MPUtility.getJsonResponse(connection);
InternalListenerManager.getListener().onNetworkRequestFinished(SdkListener.Endpoint.IDENTITY_IDENTIFY, url, response, responseCode);
return parseIdentityResponse(responseCode, response);
IdentityHttpResponse identityHttpResponse = parseIdentityResponse(responseCode, response);
catchRequest(request, identityHttpResponse, IDENTIFY_CALL, maxAgeTime);
return identityHttpResponse;
}

public IdentityHttpResponse modify(IdentityApiRequest request) throws JSONException, IOException {
clearCatch();
JSONObject jsonObject = getChangeJson(request);
Logger.verbose("Identity modify request: \n" + jsonObject.toString());
JSONArray identityChanges = jsonObject.optJSONArray("identity_changes");
Expand All @@ -129,6 +165,56 @@ public IdentityHttpResponse modify(IdentityApiRequest request) throws JSONExcept
return parseIdentityResponse(responseCode, response);
}

private void catchRequest(IdentityApiRequest request, IdentityHttpResponse identityHttpResponse, String callType, Long maxAgeTime) throws JSONException {
if (mConfigManager.isIdentityCacheFlagEnabled()) {
try {
if (identityCacheTime <= 0L) {
identityCacheTime = System.currentTimeMillis();
mConfigManager.saveIdentityCacheTime(identityCacheTime);
}
String key = request.objectToHash() + callType;

identityCacheArray.put(key, identityHttpResponse);
mConfigManager.saveIdentityCache(key, identityHttpResponse);
mConfigManager.saveIdentityMaxAge(maxAgeTime);
} catch (Exception e) {
Logger.error("Exception while processing Identity caching " + e);
}
}
}

private void clearCatch() {
identityCacheArray.clear();
mConfigManager.clearIdentityCatch();
}

private IdentityHttpResponse checkIfExists(IdentityApiRequest request, String callType) {
if (mConfigManager.isIdentityCacheFlagEnabled()) {
try {
String key = request.objectToHash() + callType;
if (identityCacheTime <= 0L) {
identityCacheTime = mConfigManager.getIdentityCacheTime();
}
if (maxAgeTimeForIdentityCache <= 0L) {
maxAgeTimeForIdentityCache = mConfigManager.getIdentityMaxAge();
}
if (identityCacheArray.isEmpty()) {
identityCacheArray = mConfigManager.fetchIdentityCache();
}
if ((((System.currentTimeMillis() - identityCacheTime) / 1000) <= maxAgeTimeForIdentityCache) && identityCacheArray.containsKey(key)) {
return identityCacheArray.get(key);
} else {
return null;

}
} catch (Exception e) {
Logger.error("Exception " + e);

}
}
return null;
}

private JSONObject getBaseJson() throws JSONException {
JSONObject clientSdkObject = new JSONObject();
clientSdkObject.put(PLATFORM, getOperatingSystemString());
Expand Down Expand Up @@ -281,7 +367,7 @@ private MPConnection getPostConnection(Long mpId, String endpoint, String messag
return connection;
}

private MPConnection getPostConnection(String endpoint, String message) throws IOException {
public MPConnection getPostConnection(String endpoint, String message) throws IOException {
return getPostConnection(null, endpoint, message);
}

Expand Down
Loading

0 comments on commit b3b63f7

Please sign in to comment.