From c84135a0a2d8cc5e25c4801432cbe037a1efaf8b Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Mon, 7 Jun 2021 15:05:53 -0400
Subject: [PATCH] Create an endpoint for creating country lists #15

---
 .../geoip/client/helper/CountryHelper.java    | 48 +++++++++++++++++++
 .../model/{Country.java => CountryCSV.java}   |  4 +-
 .../geoip/client/model/ISOCountry.java        | 33 +++++++++++++
 .../client/resources/CountryResource.java     | 19 ++++++++
 .../service/impl/CSVNetworkService.java       |  6 +--
 .../client/resources/CountryResourceTest.java | 48 +++++++++++++++++--
 6 files changed, 148 insertions(+), 10 deletions(-)
 create mode 100644 src/main/java/org/eclipsefoundation/geoip/client/helper/CountryHelper.java
 rename src/main/java/org/eclipsefoundation/geoip/client/model/{Country.java => CountryCSV.java} (95%)
 create mode 100644 src/main/java/org/eclipsefoundation/geoip/client/model/ISOCountry.java

diff --git a/src/main/java/org/eclipsefoundation/geoip/client/helper/CountryHelper.java b/src/main/java/org/eclipsefoundation/geoip/client/helper/CountryHelper.java
new file mode 100644
index 0000000..731edc7
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/geoip/client/helper/CountryHelper.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2021 Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * Author: Martin Lowe <martin.lowe@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.geoip.client.helper;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.eclipsefoundation.geoip.client.model.ISOCountry;
+
+/**
+ * Helper to retrieve and cache countries and country codes in a light manner.
+ */
+public class CountryHelper {
+    private static final Map<String, List<ISOCountry>> countryListCache = new HashMap<>();
+
+    public static List<ISOCountry> getCountries(Locale l) {
+        // Default to EN locale if not specified
+        Locale actualLocale = l != null ? l : Locale.ENGLISH;
+        // get the cached data if it exists
+        List<ISOCountry> countries = countryListCache.get(actualLocale.getLanguage());
+        if (countries == null) {
+            // generate cached data
+            String[] isoCountries = Locale.getISOCountries();
+            countries = new ArrayList<>(isoCountries.length);
+            for (String isoCountry : isoCountries) {
+                Locale c = new Locale("", isoCountry);
+                countries.add(new ISOCountry(c.getDisplayCountry(actualLocale), c.getCountry()));
+            }
+            countryListCache.put(actualLocale.getLanguage(), countries);
+        }
+        return new ArrayList<>(countries);
+    }
+
+    private CountryHelper() {
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/geoip/client/model/Country.java b/src/main/java/org/eclipsefoundation/geoip/client/model/CountryCSV.java
similarity index 95%
rename from src/main/java/org/eclipsefoundation/geoip/client/model/Country.java
rename to src/main/java/org/eclipsefoundation/geoip/client/model/CountryCSV.java
index 135ad49..27d1251 100644
--- a/src/main/java/org/eclipsefoundation/geoip/client/model/Country.java
+++ b/src/main/java/org/eclipsefoundation/geoip/client/model/CountryCSV.java
@@ -17,13 +17,13 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
  *
  */
 @RegisterForReflection
-public class Country {
+public class CountryCSV {
 	@CsvBindByName(column = "geoname_id")
 	private String id;
 	@CsvBindByName(column = "country_iso_code")
 	private String countryIsoCode;
 
-	public Country() {
+	public CountryCSV() {
 	}
 
 	/**
diff --git a/src/main/java/org/eclipsefoundation/geoip/client/model/ISOCountry.java b/src/main/java/org/eclipsefoundation/geoip/client/model/ISOCountry.java
new file mode 100644
index 0000000..21bf4fb
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/geoip/client/model/ISOCountry.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2021 Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * Author: Martin Lowe <martin.lowe@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.geoip.client.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class ISOCountry {
+    private String name;
+    @JsonProperty("iso_code")
+    private String isoCode;
+
+    public ISOCountry(String name, String isoCode) {
+        this.name = name;
+        this.isoCode = isoCode;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public String getIsoCode() {
+        return this.isoCode;
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/geoip/client/resources/CountryResource.java b/src/main/java/org/eclipsefoundation/geoip/client/resources/CountryResource.java
index f56b996..d9df323 100644
--- a/src/main/java/org/eclipsefoundation/geoip/client/resources/CountryResource.java
+++ b/src/main/java/org/eclipsefoundation/geoip/client/resources/CountryResource.java
@@ -7,18 +7,22 @@
 package org.eclipsefoundation.geoip.client.resources;
 
 import java.io.IOException;
+import java.util.Locale;
+import java.util.MissingResourceException;
 
 import javax.inject.Inject;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriInfo;
 
+import org.eclipsefoundation.geoip.client.helper.CountryHelper;
 import org.eclipsefoundation.geoip.client.helper.InetAddressHelper;
 import org.eclipsefoundation.geoip.client.model.Error;
 import org.eclipsefoundation.geoip.client.service.GeoIPService;
@@ -59,4 +63,19 @@ public class CountryResource {
 			throw new RuntimeException("Error while converting country record to JSON", e);
 		}
 	}
+
+	@GET
+	@Path("/all")
+	public Response getAll(@QueryParam("locale") String locale) {
+		Locale l = new Locale(locale != null ? locale : "en");
+		return Response.ok(CountryHelper.getCountries(isValid(l) ? l : null)).build();
+	}
+
+	private boolean isValid(Locale locale) {
+		try {
+			return locale.getISO3Language() != null && locale.getISO3Country() != null;
+		} catch (MissingResourceException e) {
+			return false;
+		}
+	}
 }
diff --git a/src/main/java/org/eclipsefoundation/geoip/client/service/impl/CSVNetworkService.java b/src/main/java/org/eclipsefoundation/geoip/client/service/impl/CSVNetworkService.java
index 39fd863..cae92e8 100644
--- a/src/main/java/org/eclipsefoundation/geoip/client/service/impl/CSVNetworkService.java
+++ b/src/main/java/org/eclipsefoundation/geoip/client/service/impl/CSVNetworkService.java
@@ -20,7 +20,7 @@ import javax.annotation.PostConstruct;
 import javax.enterprise.context.ApplicationScoped;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.eclipsefoundation.geoip.client.model.Country;
+import org.eclipsefoundation.geoip.client.model.CountryCSV;
 import org.eclipsefoundation.geoip.client.model.IPVersion;
 import org.eclipsefoundation.geoip.client.model.SubnetRange;
 import org.eclipsefoundation.geoip.client.service.NetworkService;
@@ -73,9 +73,9 @@ public class CSVNetworkService implements NetworkService {
 	private void loadCountries(String filePath, Map<String, String> container) {
 		try (FileReader reader = new FileReader(filePath)) {
 			// read in all of the country lines as country objects
-			List<Country> countries = new CsvToBeanBuilder<Country>(reader).withType(Country.class).build().parse();
+			List<CountryCSV> countries = new CsvToBeanBuilder<CountryCSV>(reader).withType(CountryCSV.class).build().parse();
 			// add each of the countries to the container map, mapping ID to the ISO code
-			for (Country c : countries) {
+			for (CountryCSV c : countries) {
 				container.put(c.getId(), c.getCountryIsoCode().toLowerCase());
 			}
 		} catch (FileNotFoundException e) {
diff --git a/src/test/java/org/eclipsefoundation/geoip/client/resources/CountryResourceTest.java b/src/test/java/org/eclipsefoundation/geoip/client/resources/CountryResourceTest.java
index 70b0593..98b1769 100644
--- a/src/test/java/org/eclipsefoundation/geoip/client/resources/CountryResourceTest.java
+++ b/src/test/java/org/eclipsefoundation/geoip/client/resources/CountryResourceTest.java
@@ -8,9 +8,17 @@ package org.eclipsefoundation.geoip.client.resources;
 
 import static io.restassured.RestAssured.given;
 
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Stream;
+
+import org.eclipsefoundation.geoip.client.model.ISOCountry;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.response.Response;
 
 /**
  * Test the listing resource endpoint, using fake data points to test solely the
@@ -25,11 +33,11 @@ public class CountryResourceTest {
 	private static final String VALID_IPV4_ADDRESS = "72.137.192.0";
 	// Google IE server address
 	private static final String VALID_IPV6_ADDRESS = "2a00:1450:400a:804::2004";
-	
+
 	@Test
 	public void testCityIPEndpoint() {
-		given().when().get("/countries/"+VALID_IPV4_ADDRESS).then().statusCode(200);
-		given().when().get("/countries/"+VALID_IPV6_ADDRESS).then().statusCode(200);
+		given().when().get("/countries/" + VALID_IPV4_ADDRESS).then().statusCode(200);
+		given().when().get("/countries/" + VALID_IPV6_ADDRESS).then().statusCode(200);
 	}
 
 	@Test
@@ -41,17 +49,47 @@ public class CountryResourceTest {
 		given().when().get("/countries/1.0.300.0").then().statusCode(400);
 		given().when().get("/countries/1.0.0.300").then().statusCode(400);
 		given().when().get("/countries/sample").then().statusCode(400);
-		given().when().get("/countries/"+VALID_IPV4_ADDRESS+":8080").then().statusCode(400);
+		given().when().get("/countries/" + VALID_IPV4_ADDRESS + ":8080").then().statusCode(400);
 		// seems to be an issue with Google Guava code, only gets detected by MaxMind
 		given().when().get("/countries/0.1.1.1").then().statusCode(500);
 		// loopback + unspecified address
 		given().when().get("/countries/127.0.0.1").then().statusCode(400);
 		given().when().get("/countries/0.0.0.0").then().statusCode(400);
-		
+
 		// IPv6 tests
 		given().when().get("/countries/bad:ip:add::res").then().statusCode(400);
 		// loopback + unspecified address
 		given().when().get("/countries/::").then().statusCode(400);
 		given().when().get("/countries/::1").then().statusCode(400);
 	}
+
+	@Test
+	void countryList_base() {
+		Response r = given().when().get("/countries/all");
+		r.then().statusCode(200);
+		// assert that United states exists as English by default
+		Assertions.assertTrue(Stream.of(r.as(ISOCountry[].class))
+				.anyMatch(c -> Locale.US.getDisplayCountry().equalsIgnoreCase(c.getName())
+						&& "us".equalsIgnoreCase(c.getIsoCode())));
+	}
+
+	@Test
+	void countryList_localized() {
+		Response r = given().when().get("/countries/all?locale=fr");
+		r.then().statusCode(200);
+		// assert that United states exists as French value
+		Assertions.assertTrue(Stream.of(r.as(ISOCountry[].class))
+				.anyMatch(c -> Locale.US.getDisplayCountry(Locale.FRENCH).equalsIgnoreCase(c.getName())
+						&& "us".equalsIgnoreCase(c.getIsoCode())));
+	}
+
+	@Test
+	void countryList_badLocale() {
+		Response r = given().when().get("/countries/all?locale=foobar");
+		r.then().statusCode(200);
+		// assert that English value exists as fallback when bad locale passed
+		Assertions.assertTrue(Stream.of(r.as(ISOCountry[].class))
+				.anyMatch(c -> Locale.US.getDisplayCountry().equalsIgnoreCase(c.getName())
+						&& "us".equalsIgnoreCase(c.getIsoCode())));
+	}
 }
-- 
GitLab