Skip to content

Identity Map V3 #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ dependencies/
build/**
.DS_Store
*/node_modules/*
.env
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.uid2</groupId>
<artifactId>uid2-client</artifactId>
<version>4.6.0</version>
<version>4.6.4-alpha-21-SNAPSHOT</version>

<name>${project.groupId}:${project.artifactId}</name>
<description>UID2 Client</description>
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/com/uid2/client/IdentityMapV3Client.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.uid2.client;

public class IdentityMapV3Client {
/**
* @param uid2BaseUrl The <a href="https://unifiedid.com/docs/getting-started/gs-environments">UID2 Base URL</a>
* @param clientApiKey Your client API key
* @param base64SecretKey Your client secret key
*/
public IdentityMapV3Client(String uid2BaseUrl, String clientApiKey, String base64SecretKey) {
identityMapHelper = new IdentityMapV3Helper(base64SecretKey);
uid2ClientHelper = new Uid2ClientHelper(uid2BaseUrl, clientApiKey);
}

/**
* @param identityMapInput represents the input required for <a href="https://unifiedid.com/docs/endpoints/post-identity-map">/identity/map</a>
* @return an IdentityMapV3Response instance
* @throws Uid2Exception if the response did not contain a "success" status, or the response code was not 200, or there was an error communicating with the provided UID2 Base URL
*/
public IdentityMapV3Response generateIdentityMap(IdentityMapV3Input identityMapInput) {
EnvelopeV2 envelope = identityMapHelper.createEnvelopeForIdentityMapRequest(identityMapInput);

String responseString = uid2ClientHelper.makeRequest(envelope, "/v3/identity/map");
return identityMapHelper.createIdentityMapResponse(responseString, envelope, identityMapInput);
}

private final IdentityMapV3Helper identityMapHelper;
private final Uid2ClientHelper uid2ClientHelper;
}
35 changes: 35 additions & 0 deletions src/main/java/com/uid2/client/IdentityMapV3Helper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.uid2.client;

import com.google.gson.Gson;

import java.nio.charset.StandardCharsets;

public class IdentityMapV3Helper {
/**
* @param base64SecretKey your UID2 client secret
*/
public IdentityMapV3Helper(String base64SecretKey) {uid2Helper = new Uid2Helper(base64SecretKey);}

/**
* @param identityMapInput represents the input required for <a href="https://unifiedid.com/docs/endpoints/post-identity-map">/identity/map</a>
* @return an EnvelopeV2 instance to use in the POST body of <a href="https://unifiedid.com/docs/endpoints/post-identity-map">/identity/map</a>
*/
public EnvelopeV2 createEnvelopeForIdentityMapRequest(IdentityMapV3Input identityMapInput) {
byte[] jsonBytes = new Gson().toJson(identityMapInput).getBytes(StandardCharsets.UTF_8);
return uid2Helper.createEnvelopeV2(jsonBytes);
}


/**
* @param responseString the response body returned by a call to <a href="https://unifiedid.com/docs/endpoints/post-identity-map">/identity/map</a>
* @param envelope the EnvelopeV2 instance returned by {@link #createEnvelopeForIdentityMapRequest}
* @param identityMapInput the same instance that was passed to {@link #createEnvelopeForIdentityMapRequest}.
* @return an IdentityMapV3Response instance
*/
public IdentityMapV3Response createIdentityMapResponse(String responseString, EnvelopeV2 envelope, IdentityMapV3Input identityMapInput) {
String decryptedResponseString = uid2Helper.decrypt(responseString, envelope.getNonce());
return new IdentityMapV3Response(decryptedResponseString, identityMapInput);
}

private final Uid2Helper uid2Helper;
}
167 changes: 167 additions & 0 deletions src/main/java/com/uid2/client/IdentityMapV3Input.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.uid2.client;

import com.google.gson.annotations.SerializedName;

import java.util.*;

public class IdentityMapV3Input {
/**
* @param emails a list of normalized or unnormalized email addresses
* @return a IdentityMapV3Input instance, to be used in {@link IdentityMapV3Helper#createEnvelopeForIdentityMapRequest}
*/
public static IdentityMapV3Input fromEmails(List<String> emails) {
return new IdentityMapV3Input().withEmails(emails);
}

/**
* @param hashedEmails a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#email-address-normalization">normalized</a> and <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#email-address-hash-encoding">hashed</a> email address
* @return an IdentityMapV3Input instance
*/
public static IdentityMapV3Input fromHashedEmails(List<String> hashedEmails) {
return new IdentityMapV3Input().withHashedEmails(hashedEmails);
}

/**
* @param phones a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-normalization">normalized</a> phone number
* @return an IdentityMapV3Input instance
*/
public static IdentityMapV3Input fromPhones(List<String> phones) {
return new IdentityMapV3Input().withPhones(phones);
}

/**
* @param hashedPhones a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-normalization">normalized</a> and <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-hash-encoding">hashed</a> phone number
* @return an IdentityMapV3Input instance
*/
public static IdentityMapV3Input fromHashedPhones(List<String> hashedPhones) {
return new IdentityMapV3Input().withHashedPhones(hashedPhones);
}

// Transient as this should not be part of the serialized JSON payload we send to UID2 Operator
private transient final Map<String, List<String>> hashedDiiToRawDii = new HashMap<>();

@SerializedName("email_hash")
private final List<Identity> hashedEmails = new ArrayList<>();

@SerializedName("phone_hash")
private final List<Identity> hashedPhones = new ArrayList<>();

public IdentityMapV3Input() {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want/need a public ctor?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from memory, might be to help consumers of the SDK to write their own tests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also for cases where clients have a mix of different DII types, which we now support.
You could do IdentityMapV3Input.fromPhones(...).withEmails(...) if you have those cleanly separated. If you have a mix of records you can do something like:

input = new IdentityMapV3Input()
for dii in diis:
  if dii is email:
    input.addEmail(dii)
  if dii is phone:
    input.addPhone(dii)


/**
* @param hashedEmails a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#email-address-normalization">normalized</a> and <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#email-address-hash-encoding">hashed</a> email address
* @return this IdentityMapV3Input instance
*/
public IdentityMapV3Input withHashedEmails(List<String> hashedEmails) {
for (String hashedEmail : hashedEmails) {
withHashedEmail(hashedEmail);
}
return this;
}

/**
* @param hashedEmail a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#email-address-normalization">normalized</a> and <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#email-address-hash-encoding">hashed</a> email address
* @return this IdentityMapV3Input instance
*/
public IdentityMapV3Input withHashedEmail(String hashedEmail) {
this.hashedEmails.add(new Identity(hashedEmail));
addToDiiMappings(hashedEmail, hashedEmail);
return this;
}

/**
* @param hashedPhones a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-normalization">normalized</a> and <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-hash-encoding">hashed</a> phone number
* @return this IdentityMapV3Input instance
*/
public IdentityMapV3Input withHashedPhones(List<String> hashedPhones) {
for (String hashedPhone : hashedPhones) {
withHashedPhone(hashedPhone);
}
return this;
}

/**
* @param hashedPhone a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-normalization">normalized</a> and <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-hash-encoding">hashed</a> phone number
* @return this IdentityMapV3Input instance
*/
public IdentityMapV3Input withHashedPhone(String hashedPhone) {
this.hashedPhones.add(new Identity(hashedPhone));
addToDiiMappings(hashedPhone, hashedPhone);
return this;
}

/**
* @param emails a list of normalized or unnormalized email addresses
* @return this IdentityMapV3Input instance
*/
public IdentityMapV3Input withEmails(List<String> emails) {
for (String email : emails) {
withEmail(email);
}
return this;
}

/**
* @param email a normalized or unnormalized email address
* @return this IdentityMapV3Input instance
*/
public IdentityMapV3Input withEmail(String email) {
String hashedEmail = InputUtil.normalizeAndHashEmail(email);
this.hashedEmails.add(new Identity(hashedEmail));
addToDiiMappings(hashedEmail, email);
return this;
}

/**
* @param phones a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-normalization">normalized</a> phone number
* @return this IdentityMapV3Input instance
*/
public IdentityMapV3Input withPhones(List<String> phones) {
for (String phone : phones) {
withPhone(phone);
}
return this;
}

/**
* @param phone a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-normalization">normalized</a> phone number
* @return this IdentityMapV3Input instance
*/
public IdentityMapV3Input withPhone(String phone) {
if (!InputUtil.isPhoneNumberNormalized(phone)) {
throw new IllegalArgumentException("phone number is not normalized: " + phone);
}

String hashedPhone = InputUtil.getBase64EncodedHash(phone);
this.hashedPhones.add(new Identity(hashedPhone));
addToDiiMappings(hashedPhone, phone);
return this;

}

List<String> getInputDiis(String identityType, int i) {
return hashedDiiToRawDii.get(getHashedDii(identityType, i));
}

private void addToDiiMappings(String hashedDii, String rawDii) {
hashedDiiToRawDii.computeIfAbsent(hashedDii, k -> new ArrayList<>()).add(rawDii);
}

private String getHashedDii(String identityType, int i) {
switch (identityType) {
case "email_hash": return hashedEmails.get(i).identity;
case "phone_hash": return hashedPhones.get(i).identity;
}
throw new Uid2Exception("Unexpected identity type: " + identityType);
}


private static class Identity {
@SerializedName("i")
private final String identity;

public Identity(String value) {
this.identity = value;
}
}
}
128 changes: 128 additions & 0 deletions src/main/java/com/uid2/client/IdentityMapV3Response.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.uid2.client;

import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;

import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class IdentityMapV3Response {
IdentityMapV3Response(String response, IdentityMapV3Input identityMapInput) {
ApiResponse apiResponse = new Gson().fromJson(response, ApiResponse.class);
status = apiResponse.status;

if (!isSuccess()) {
throw new Uid2Exception("Got unexpected identity map status: " + status);
}

populateIdentities(apiResponse.body, identityMapInput);
}

private void populateIdentities(Map<String, List<ApiIdentity>> apiResponse, IdentityMapV3Input identityMapInput) {
for (Map.Entry<String, List<ApiIdentity>> identitiesForType : apiResponse.entrySet()) {
populateIdentitiesForType(identityMapInput, identitiesForType.getKey(), identitiesForType.getValue());
}
}

private void populateIdentitiesForType(IdentityMapV3Input identityMapInput, String identityType, List<ApiIdentity> identities) {
for (int i = 0; i < identities.size(); i++) {
ApiIdentity apiIdentity = identities.get(i);
List<String> inputDiis = identityMapInput.getInputDiis(identityType, i);
for (String inputDii : inputDiis) {
if (apiIdentity.error == null) {
mappedIdentities.put(inputDii, new MappedIdentity(apiIdentity));
} else {
unmappedIdentities.put(inputDii, new UnmappedIdentity(apiIdentity.error));
}
}
}
}

public boolean isSuccess() {
return "success".equals(status);
}

public static class ApiResponse {
@SerializedName("status")
public String status;

@SerializedName("body")
public Map<String, List<ApiIdentity>> body;
}

public static class ApiIdentity {
@SerializedName("u")
public String currentUid;

@SerializedName("p")
public String previousUid;

@SerializedName("r")
public Long refreshFromSeconds;

@SerializedName("e")
public String error;
}

public static class MappedIdentity {
public MappedIdentity(String currentUid, String previousUid, Instant refreshFrom) {
this.currentUid = currentUid;
this.previousUid = previousUid;
this.refreshFrom = refreshFrom;
}

public MappedIdentity(ApiIdentity apiIdentity) {
this(apiIdentity.currentUid, apiIdentity.previousUid, Instant.ofEpochSecond(apiIdentity.refreshFromSeconds));
}

private final String currentUid;
private final String previousUid;
private final Instant refreshFrom;

public String getCurrentRawUid() {
return currentUid;
}

public String getPreviousRawUid() {
return previousUid;
}

public Instant getRefreshFrom() {
return refreshFrom;
}
}

public static class UnmappedIdentity {
public UnmappedIdentity(String reason)
{
this.reason = UnmappedIdentityReason.fromString(reason);
this.rawReason = reason;
}

public UnmappedIdentityReason getReason() {
return reason;
}

public String getRawReason() {
return rawReason;
}

private final UnmappedIdentityReason reason;

private final String rawReason;
}

public HashMap<String, MappedIdentity> getMappedIdentities() {
return new HashMap<>(mappedIdentities);
}

public HashMap<String, UnmappedIdentity> getUnmappedIdentities() {
return new HashMap<>(unmappedIdentities);
}

private final String status;
private final HashMap<String, MappedIdentity> mappedIdentities = new HashMap<>();
private final HashMap<String, UnmappedIdentity> unmappedIdentities = new HashMap<>();
}
19 changes: 19 additions & 0 deletions src/main/java/com/uid2/client/UnmappedIdentityReason.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.uid2.client;


public enum UnmappedIdentityReason {
OPTOUT,
INVALID_IDENTIFIER,
UNKNOWN;

public static UnmappedIdentityReason fromString(String reason) {
if (reason.equals("optout")) {
return OPTOUT;
}
if (reason.equals("invalid identifier")) {
return INVALID_IDENTIFIER;
}

return UNKNOWN;
}
}
Loading