Skip to content

Commit b136aed

Browse files
Merge pull request #23 from googleads/new_address_fields
feat(formatter): add formatting methods for new GA4 address info fields
2 parents b15171a + 0c37a0f commit b136aed

3 files changed

Lines changed: 217 additions & 1 deletion

File tree

‎data-manager-util/build.gradle‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ plugins {
2121

2222
group = 'com.google.api-ads'
2323
description = 'Utilities for working with the Data Manager API'
24-
version = '0.3.0'
24+
version = '0.4.0'
2525

2626
sourceCompatibility = 1.8
2727
targetCompatibility = 1.8

‎data-manager-util/src/main/java/com/google/ads/datamanager/util/UserDataFormatter.java‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ public class UserDataFormatter {
9090
/** Pattern that matches all uppercase characters. */
9191
private static final Pattern ALL_UPPERCASE_CHARS_PATTERN = Pattern.compile("^[A-Z]+$");
9292

93+
/** Pattern that matches all non-alphanumeric characters, except spaces. */
94+
private static final Pattern SYMBOL_PATTERN = Pattern.compile("[^\\p{L}\\p{N}\\s]");
95+
9396
private UserDataFormatter(MessageDigest sha256Digest) {
9497
this.sha256Digest = sha256Digest;
9598
}
@@ -267,6 +270,55 @@ public String formatPostalCode(String postalCode) {
267270
return postalCode;
268271
}
269272

273+
/**
274+
* Returns the normalized and formatted location string.
275+
*
276+
* @param value the string to format
277+
* @param label the label used for error messages
278+
* @throws IllegalArgumentException if {@code value} is invalid. Examples of an invalid value
279+
* include a {@code null}, blank, or empty string.
280+
*/
281+
private String formatLocationString(String value, String label) {
282+
Preconditions.checkArgument(value != null, "Null %s", label);
283+
value = value.trim().toLowerCase(LOCALE);
284+
value = SYMBOL_PATTERN.matcher(value).replaceAll("");
285+
Preconditions.checkArgument(!value.isEmpty(), "Empty or blank %s", label);
286+
return value;
287+
}
288+
289+
/**
290+
* Returns the provided address line, normalized and formatted.
291+
*
292+
* @param addressLine the address line to format
293+
* @throws IllegalArgumentException if {@code addressLine} is invalid. Examples of an invalid value
294+
* include a {@code null}, blank, or empty string.
295+
*/
296+
public String formatAddressLine(String addressLine) {
297+
return formatLocationString(addressLine, "address line");
298+
}
299+
300+
/**
301+
* Returns the provided city, normalized and formatted.
302+
*
303+
* @param city the city to format
304+
* @throws IllegalArgumentException if {@code city} is invalid. Examples of an invalid value
305+
* include a {@code null}, blank, or empty string.
306+
*/
307+
public String formatCity(String city) {
308+
return formatLocationString(city, "city");
309+
}
310+
311+
/**
312+
* Returns the provided administrative area, normalized and formatted.
313+
*
314+
* @param administrativeArea the administrative area to format
315+
* @throws IllegalArgumentException if {@code administrativeArea} is invalid. Examples of an invalid value
316+
* include a {@code null}, blank, or empty string.
317+
*/
318+
public String formatAdministrativeArea(String administrativeArea) {
319+
return formatLocationString(administrativeArea, "administrative area");
320+
}
321+
270322
/**
271323
* Returns the SHA-256 hash of the provided string.
272324
*
@@ -474,6 +526,75 @@ public String processPostalCode(String postalCode) {
474526
return formatPostalCode(postalCode);
475527
}
476528

529+
/**
530+
* Formats the address line, hashes, and encodes using the specified encoding.
531+
*
532+
* <p>This is a convenience method that combines {@link #formatAddressLine(String)}, {@link
533+
* #hashString(String)}, and either {@link #hexEncode(byte[])} or {@link #base64Encode(byte[])}
534+
* into a single call.
535+
*
536+
* @return the address line, formatted, hashed, and encoded for the {@code AddressInfo.address_line}
537+
* field in the API.
538+
* @throws IllegalArgumentException if the address line is invalid
539+
*/
540+
public String processAddressLine(String addressLine, Encoding encoding) {
541+
return hashAndEncode(formatAddressLine(addressLine), encoding);
542+
}
543+
544+
/**
545+
* Formats the address line, hashes, base 64-encodes, encrypts, and encodes using the specified
546+
* encoding.
547+
*
548+
* <p>This is a convenience method that combines {@link #formatAddressLine(String)}, {@link
549+
* #hashString(String)}, {@link #base64Encode(byte[])}, {@link Encrypter#encrypt(String)}, and
550+
* either {@link #hexEncode(byte[])} or {@link #base64Encode(byte[])} into a single call.
551+
*
552+
* @return the address line, formatted, hashed, encrypted, and encoded for the {@code
553+
* AddressInfo.address_line} field in the API.
554+
* @throws IllegalArgumentException if the address line is invalid
555+
* @throws NullPointerException if {@code encrypter} is null
556+
*/
557+
public String processAddressLine(String addressLine, Encoding encoding, Encrypter encrypter) {
558+
Preconditions.checkNotNull(encrypter, "Null encrypter");
559+
return hashEncodeAndEncrypt(formatAddressLine(addressLine), encoding, encrypter);
560+
}
561+
562+
/**
563+
* Processes the city.
564+
*
565+
* <p>This is a convenience method that simply calls {@link #formatCity(String)}. This
566+
* method exists for consistency so that all data types have a {@code process...} method.
567+
*
568+
* <p>Doesn't require an {@link Encoding} since cities shouldn't be encoded or hashed.
569+
*
570+
* <p>There is no overloaded counterpart that takes an {@link Encrypter} since cities
571+
* shouldn't be encrypted.
572+
*
573+
* @return the city, formatted for the {@code AddressInfo.city} field in the API.
574+
* @throws IllegalArgumentException if the city is invalid
575+
*/
576+
public String processCity(String city) {
577+
return formatCity(city);
578+
}
579+
580+
/**
581+
* Processes the administrative area.
582+
*
583+
* <p>This is a convenience method that simply calls {@link #formatAdministrativeArea(String)}. This
584+
* method exists for consistency so that all data types have a {@code process...} method.
585+
*
586+
* <p>Doesn't require an {@link Encoding} since administrative areas shouldn't be encoded or hashed.
587+
*
588+
* <p>There is no overloaded counterpart that takes an {@link Encrypter} since administrative areas
589+
* shouldn't be encrypted.
590+
*
591+
* @return the administrative area, formatted for the {@code AddressInfo.administrative_area} field in the API.
592+
* @throws IllegalArgumentException if the administrative area is invalid
593+
*/
594+
public String processAdministrativeArea(String administrativeArea) {
595+
return formatAdministrativeArea(administrativeArea);
596+
}
597+
477598
/**
478599
* Hashes the string and then encodes using the specified encoding.
479600
*

‎data-manager-util/src/test/java/com/google/ads/datamanager/util/UserDataFormatterTest.java‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,72 @@ public void testFormatPostalCode_invalidInputs() {
238238
() -> formatter.formatPostalCode(" "));
239239
}
240240

241+
@Test
242+
public void testFormatAddressLine_validInputs() {
243+
assertEquals("1800 amphibious blvd", formatter.formatAddressLine(" 1800 Amphibious Blvd. "));
244+
}
245+
246+
@Test
247+
public void testFormatAddressLine_invalidInputs() {
248+
assertThrows(
249+
"Null address line",
250+
IllegalArgumentException.class,
251+
() -> formatter.formatAddressLine(null));
252+
assertThrows(
253+
"Empty address line",
254+
IllegalArgumentException.class,
255+
() -> formatter.formatAddressLine(""));
256+
assertThrows(
257+
"Blank address line",
258+
IllegalArgumentException.class,
259+
() -> formatter.formatAddressLine(" "));
260+
}
261+
262+
@Test
263+
public void testFormatCity_validInputs() {
264+
assertEquals("mountain view", formatter.formatCity(" Mountain View "));
265+
assertEquals("mountain view", formatter.formatCity("Mountain View,"));
266+
}
267+
268+
@Test
269+
public void testFormatCity_invalidInputs() {
270+
assertThrows(
271+
"Null city",
272+
IllegalArgumentException.class,
273+
() -> formatter.formatCity(null));
274+
assertThrows(
275+
"Empty city",
276+
IllegalArgumentException.class,
277+
() -> formatter.formatCity(""));
278+
assertThrows(
279+
"Blank city",
280+
IllegalArgumentException.class,
281+
() -> formatter.formatCity(" "));
282+
}
283+
284+
@Test
285+
public void testFormatAdministrativeArea_validInputs() {
286+
assertEquals("ca", formatter.formatAdministrativeArea(" CA "));
287+
assertEquals("california", formatter.formatAdministrativeArea(" California "));
288+
assertEquals("ca", formatter.formatAdministrativeArea("CA,"));
289+
}
290+
291+
@Test
292+
public void testFormatAdministrativeArea_invalidInputs() {
293+
assertThrows(
294+
"Null administrative area",
295+
IllegalArgumentException.class,
296+
() -> formatter.formatAdministrativeArea(null));
297+
assertThrows(
298+
"Empty administrative area",
299+
IllegalArgumentException.class,
300+
() -> formatter.formatAdministrativeArea(""));
301+
assertThrows(
302+
"Blank administrative area",
303+
IllegalArgumentException.class,
304+
() -> formatter.formatAdministrativeArea(" "));
305+
}
306+
241307
@Test
242308
public void testHashString_validInputs() {
243309
Function<String, String> hashAndEncode = s -> hexEncoder.encode(formatter.hashString(s));
@@ -247,6 +313,9 @@ public void testHashString_validInputs() {
247313
assertEquals(
248314
"FB4F73A6EC5FDB7077D564CDD22C3554B43CE49168550C3B12C547B78C517B30",
249315
hashAndEncode.apply("+18005550100"));
316+
assertEquals(
317+
"FF75E73A0E768CC1FA28A64FAEBBCECCB562D7C05F2FFCDD8D100ABAD73E4579",
318+
hashAndEncode.apply("1800 amphibious blvd"));
250319
}
251320

252321
@Test
@@ -377,4 +446,30 @@ public void testProcessPostalCode_validInputs() {
377446
assertEquals("1229-076", formatter.formatPostalCode("1229-076"));
378447
assertEquals("1229-076", formatter.formatPostalCode(" 1229-076 "));
379448
}
449+
450+
@Test
451+
public void testProcessAddressLine_validInputs_hexEncoding() {
452+
final String encodedHash = "FF75E73A0E768CC1FA28A64FAEBBCECCB562D7C05F2FFCDD8D100ABAD73E4579";
453+
assertEquals(
454+
encodedHash,
455+
formatter.processAddressLine(" 1800 Amphibious Blvd. ", Encoding.HEX));
456+
}
457+
458+
@Test
459+
public void testProcessAddressLine_validInputs_base64Encoding() {
460+
final String encodedHash = "/3XnOg52jMH6KKZPrrvOzLVi18BfL/zdjRAKutc+RXk=";
461+
assertEquals(
462+
encodedHash,
463+
formatter.processAddressLine(" 1800 Amphibious Blvd. ", Encoding.BASE64));
464+
}
465+
466+
@Test
467+
public void testProcessCity_validInputs() {
468+
assertEquals("mountain view", formatter.processCity(" Mountain View "));
469+
}
470+
471+
@Test
472+
public void testProcessAdministrativeArea_validInputs() {
473+
assertEquals("ca", formatter.processAdministrativeArea(" CA "));
474+
}
380475
}

0 commit comments

Comments
 (0)