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 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9ab7827
Added V3 as copy of V2
aulme May 29, 2025
7eade1a
Serializing dii in the new format
aulme May 29, 2025
ec27779
Adapted V3 Identity Map clases to the new endpoint
aulme May 30, 2025
cf6168e
Ignoring .env
aulme Jun 2, 2025
3df6e35
Checking refreshFrom in IdentityMapV3 tests, should be in the future,…
aulme Jun 2, 2025
c1fb9d4
Empty constructor pulbic and with methods for one dii are available
aulme Jun 4, 2025
c3ef65d
Javadocs
aulme Jun 4, 2025
18cfc01
Addressing feedback
aulme Jun 6, 2025
ee0782a
More feedback
aulme Jun 6, 2025
7b3ae88
[CI Pipeline] Released Snapshot version: 4.6.1-alpha-17-SNAPSHOT
Jun 10, 2025
b8163ce
Using enum for unmapped operations instead of strings
aulme Jun 11, 2025
9a974ff
Address Matt's feedback
aulme Jun 11, 2025
df2c22a
Improving unit test coverage for V3 Identity Map
aulme Jun 11, 2025
162e905
[CI Pipeline] Released Snapshot version: 4.6.2-alpha-19-SNAPSHOT
Jun 12, 2025
ec0b6fd
Adapted SDK to changes in V3 API
aulme Jun 12, 2025
73ee0ab
[CI Pipeline] Released Snapshot version: 4.6.3-alpha-20-SNAPSHOT
Jun 12, 2025
80b92b9
Using Caroline's branch in build and publish for now since publishing…
aulme Jun 12, 2025
b55415a
[CI Pipeline] Released Snapshot version: 4.6.4-alpha-21-SNAPSHOT
Jun 12, 2025
da5b65b
Ok don't need it
aulme Jun 12, 2025
15ba443
Merge branch 'main' into aul-UID2-5485-identity-map-v3
aulme Jun 13, 2025
aac8510
[CI Pipeline] Released Snapshot version: 4.6.5-alpha-25-SNAPSHOT
Jun 13, 2025
361ac4e
Added mandatory email and phone fields to V3 Identity Map API
aulme Jun 13, 2025
5ff9194
[CI Pipeline] Released Snapshot version: 4.6.6-alpha-27-SNAPSHOT
Jun 13, 2025
127f219
[CI Pipeline] Released Snapshot version: 4.6.7-alpha-28-SNAPSHOT
Jun 13, 2025
e66f6ce
Fixed request to new unrwrapped version for V3 Identity Map
aulme Jun 13, 2025
db70dee
[CI Pipeline] Released Snapshot version: 4.6.8-alpha-29-SNAPSHOT
Jun 13, 2025
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.8-alpha-29-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;
}
162 changes: 162 additions & 0 deletions src/main/java/com/uid2/client/IdentityMapV3Input.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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<String> hashedEmails = new ArrayList<>();

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

// We never send unhashed emails or phone numbers in the SDK, but they are required fields in the API request
@SerializedName("email")
private List<String> emails = Collections.unmodifiableList(new ArrayList<>());
@SerializedName("phone")
private List<String> phones = Collections.unmodifiableList(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(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(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(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(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);
case "phone_hash": return hashedPhones.get(i);
}
throw new Uid2Exception("Unexpected identity type: " + identityType);
}
}
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