From 230c3801d78d81a43df1bcd7c8c628fd93fbac84 Mon Sep 17 00:00:00 2001 From: Sandra Thieme Date: Mon, 26 Feb 2018 15:56:10 +0100 Subject: [PATCH] WIP: Resolve addresses by three word addresses --- .../address/api/AddressApiController.java | 12 ++ .../iris/address/dto/AddressDtoService.java | 11 ++ .../address/dto/AddressDtoServiceImpl.java | 10 ++ .../service/AddressServiceWrapper.java | 21 +++- .../iris/address/w3w/ForwardW3wResponse.java | 77 +++++++++++++ .../iris/address/w3w/ThreeWordClient.java | 47 ++++++++ .../address/w3w/ThreeWordClientException.java | 13 +++ .../iris/address/w3w/ThreeWordMatcher.java | 17 +++ src/main/resources/application-context.xml | 12 +- .../dto/AddressDtoServiceImplUnitTest.java | 13 +++ .../AddressServiceWrapperCachingUnitTest.java | 5 +- .../AddressServiceWrapperUnitTest.java | 37 ++++++- .../address/w3w/ThreeWordClientUnitTest.java | 104 ++++++++++++++++++ .../address/w3w/ThreeWordMatcherUnitTest.java | 23 ++++ 14 files changed, 396 insertions(+), 6 deletions(-) create mode 100644 src/main/java/net/contargo/iris/address/w3w/ForwardW3wResponse.java create mode 100644 src/main/java/net/contargo/iris/address/w3w/ThreeWordClient.java create mode 100644 src/main/java/net/contargo/iris/address/w3w/ThreeWordClientException.java create mode 100644 src/main/java/net/contargo/iris/address/w3w/ThreeWordMatcher.java create mode 100644 src/test/java/net/contargo/iris/address/w3w/ThreeWordClientUnitTest.java create mode 100644 src/test/java/net/contargo/iris/address/w3w/ThreeWordMatcherUnitTest.java diff --git a/src/main/java/net/contargo/iris/address/api/AddressApiController.java b/src/main/java/net/contargo/iris/address/api/AddressApiController.java index ef0e7e51..652aa280 100644 --- a/src/main/java/net/contargo/iris/address/api/AddressApiController.java +++ b/src/main/java/net/contargo/iris/address/api/AddressApiController.java @@ -32,9 +32,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.servlet.http.HttpServletRequest; +import static net.contargo.iris.address.w3w.ThreeWordMatcher.isThreeWordAddress; + import static org.slf4j.LoggerFactory.getLogger; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; @@ -139,6 +142,15 @@ public ListOfAddressListsResponse addressesByAddressDetails(@RequestParam(requir } } + if (isThreeWordAddress(street)) { + Optional addressDto = addressDtoService.getAddressesByThreeWords(street.trim()); + + if (addressDto.isPresent()) { + addressListDtos = singletonList(new AddressListDto("Result for three words " + street, + singletonList(addressDto.get()))); + } + } + Map addressDetails = NominatimUtil.parameterMap(street, postalCode, city, country, name); if (addressListDtos.isEmpty()) { diff --git a/src/main/java/net/contargo/iris/address/dto/AddressDtoService.java b/src/main/java/net/contargo/iris/address/dto/AddressDtoService.java index 9da75540..b9e32edf 100644 --- a/src/main/java/net/contargo/iris/address/dto/AddressDtoService.java +++ b/src/main/java/net/contargo/iris/address/dto/AddressDtoService.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; /** @@ -96,4 +97,14 @@ public interface AddressDtoService { * @return a list of matching addresses */ List getAddressesByQuery(String query); + + + /** + * Returns an optional {@link AddressDto} matching the given three word address. + * + * @param threeWords a three word address + * + * @return an optional address + */ + Optional getAddressesByThreeWords(String threeWords); } diff --git a/src/main/java/net/contargo/iris/address/dto/AddressDtoServiceImpl.java b/src/main/java/net/contargo/iris/address/dto/AddressDtoServiceImpl.java index 46baa59b..2d457c48 100644 --- a/src/main/java/net/contargo/iris/address/dto/AddressDtoServiceImpl.java +++ b/src/main/java/net/contargo/iris/address/dto/AddressDtoServiceImpl.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; @@ -101,4 +102,13 @@ public List getAddressesByQuery(String query) { return addressServiceWrapper.getAddressesByQuery(query).stream().map(AddressDto::new).collect(toList()); } + + + @Override + public Optional getAddressesByThreeWords(String threeWords) { + + Optional
optionalAddress = addressServiceWrapper.getAddressByThreeWords(threeWords); + + return optionalAddress.map(AddressDto::new); + } } diff --git a/src/main/java/net/contargo/iris/address/service/AddressServiceWrapper.java b/src/main/java/net/contargo/iris/address/service/AddressServiceWrapper.java index f3e8560b..92d05541 100644 --- a/src/main/java/net/contargo/iris/address/service/AddressServiceWrapper.java +++ b/src/main/java/net/contargo/iris/address/service/AddressServiceWrapper.java @@ -7,6 +7,8 @@ import net.contargo.iris.address.staticsearch.StaticAddress; import net.contargo.iris.address.staticsearch.service.StaticAddressNotFoundException; import net.contargo.iris.address.staticsearch.service.StaticAddressService; +import net.contargo.iris.address.w3w.ThreeWordClient; +import net.contargo.iris.address.w3w.ThreeWordClientException; import net.contargo.iris.normalizer.NormalizerService; import org.apache.commons.lang.StringUtils; @@ -19,6 +21,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.regex.Pattern; import static net.contargo.iris.address.nominatim.service.AddressDetailKey.CITY; @@ -46,14 +49,16 @@ public class AddressServiceWrapper { private final StaticAddressService staticAddressService; private final AddressCache addressCache; private final NormalizerService normalizerService; + private final ThreeWordClient threeWordClient; public AddressServiceWrapper(AddressService addressService, StaticAddressService staticAddressService, - AddressCache cache, NormalizerService normalizerService) { + AddressCache cache, NormalizerService normalizerService, ThreeWordClient threeWordClient) { this.addressService = addressService; this.staticAddressService = staticAddressService; this.addressCache = cache; this.normalizerService = normalizerService; + this.threeWordClient = threeWordClient; } /** @@ -225,4 +230,18 @@ public List
getAddressesByQuery(String query) { return addresses; } + + + public Optional
getAddressByThreeWords(String threeWords) { + + try { + GeoLocation resolvedLocation = threeWordClient.resolve(threeWords); + + return Optional.of(getAddressForGeoLocation(resolvedLocation)); + } catch (ThreeWordClientException e) { + LOG.info("Cannot resolve three word address {}: {}", threeWords, e.getMessage()); + + return Optional.empty(); + } + } } diff --git a/src/main/java/net/contargo/iris/address/w3w/ForwardW3wResponse.java b/src/main/java/net/contargo/iris/address/w3w/ForwardW3wResponse.java new file mode 100644 index 00000000..0610fffd --- /dev/null +++ b/src/main/java/net/contargo/iris/address/w3w/ForwardW3wResponse.java @@ -0,0 +1,77 @@ +package net.contargo.iris.address.w3w; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import net.contargo.iris.GeoLocation; + +import java.math.BigDecimal; + + +/** + * @author Sandra Thieme - thieme@synyx.de + */ +class ForwardW3wResponse { + + private final W3wResponseGeometry geometry; + private final W3wResponseStatus status; + + @JsonCreator + ForwardW3wResponse(@JsonProperty("geometry") W3wResponseGeometry geometry, + @JsonProperty("status") W3wResponseStatus status) { + + this.geometry = geometry; + this.status = status; + } + + GeoLocation toGeolocation() { + + return new GeoLocation(geometry.lat, geometry.lon); + } + + + boolean error() { + + return status.code != null; + } + + + Integer errorCode() { + + return status.code; + } + + + String errorMessage() { + + return status.message; + } + + private static class W3wResponseGeometry { + + private final BigDecimal lat; + private final BigDecimal lon; + + @JsonCreator + private W3wResponseGeometry(@JsonProperty("lat") BigDecimal lat, + @JsonProperty("lng") BigDecimal lon) { + + this.lat = lat; + this.lon = lon; + } + } + + private static class W3wResponseStatus { + + private final Integer code; + private final String message; + + @JsonCreator + private W3wResponseStatus(@JsonProperty("code") Integer code, + @JsonProperty("message") String message) { + + this.code = code; + this.message = message; + } + } +} diff --git a/src/main/java/net/contargo/iris/address/w3w/ThreeWordClient.java b/src/main/java/net/contargo/iris/address/w3w/ThreeWordClient.java new file mode 100644 index 00000000..112780c4 --- /dev/null +++ b/src/main/java/net/contargo/iris/address/w3w/ThreeWordClient.java @@ -0,0 +1,47 @@ +package net.contargo.iris.address.w3w; + +import net.contargo.iris.GeoLocation; + +import org.springframework.http.ResponseEntity; + +import org.springframework.web.client.RestTemplate; + + +/** + * @author Sandra Thieme - thieme@synyx.de + */ +public class ThreeWordClient { + + private static final String FORWARD_URL = + "https://api.what3words.com/v2/forward?addr={w3wAddress}&key={apiKey}&lang=de"; + + private final RestTemplate restTemplate; + private final String apiKey; + + public ThreeWordClient(RestTemplate restTemplate, String apiKey) { + + this.restTemplate = restTemplate; + this.apiKey = apiKey; + } + + public GeoLocation resolve(String threeWords) { + + ResponseEntity response = restTemplate.getForEntity(FORWARD_URL, ForwardW3wResponse.class, + threeWords, apiKey); + + checkErrorStatus(response.getBody(), threeWords); + + return response.getBody().toGeolocation(); + } + + + private static void checkErrorStatus(ForwardW3wResponse response, String threeWords) { + + if (response.error()) { + Integer code = response.errorCode(); + String message = response.errorMessage(); + + throw new ThreeWordClientException(code, message, threeWords); + } + } +} diff --git a/src/main/java/net/contargo/iris/address/w3w/ThreeWordClientException.java b/src/main/java/net/contargo/iris/address/w3w/ThreeWordClientException.java new file mode 100644 index 00000000..f76d67c5 --- /dev/null +++ b/src/main/java/net/contargo/iris/address/w3w/ThreeWordClientException.java @@ -0,0 +1,13 @@ +package net.contargo.iris.address.w3w; + +/** + * @author Sandra Thieme - thieme@synyx.de + */ +public class ThreeWordClientException extends RuntimeException { + + ThreeWordClientException(Integer code, String message, String threeWords) { + + super("API of w3w returned error code " + code + " with message '" + message + + "' for three word address '" + threeWords + "'"); + } +} diff --git a/src/main/java/net/contargo/iris/address/w3w/ThreeWordMatcher.java b/src/main/java/net/contargo/iris/address/w3w/ThreeWordMatcher.java new file mode 100644 index 00000000..3b9d7891 --- /dev/null +++ b/src/main/java/net/contargo/iris/address/w3w/ThreeWordMatcher.java @@ -0,0 +1,17 @@ +package net.contargo.iris.address.w3w; + +import java.util.regex.Pattern; + + +/** + * @author Sandra Thieme - thieme@synyx.de + */ +public class ThreeWordMatcher { + + private static final Pattern THREE_WORD_PATTERN = Pattern.compile("^\\p{L}+\\.\\p{L}+\\.\\p{L}+$"); + + public static boolean isThreeWordAddress(String input) { + + return input != null && THREE_WORD_PATTERN.matcher(input.trim()).matches(); + } +} diff --git a/src/main/resources/application-context.xml b/src/main/resources/application-context.xml index 0e40976f..e200990a 100644 --- a/src/main/resources/application-context.xml +++ b/src/main/resources/application-context.xml @@ -120,8 +120,16 @@ + - + + + + + + + + @@ -401,7 +409,7 @@ - + diff --git a/src/test/java/net/contargo/iris/address/dto/AddressDtoServiceImplUnitTest.java b/src/test/java/net/contargo/iris/address/dto/AddressDtoServiceImplUnitTest.java index 3018038d..8ba26717 100644 --- a/src/test/java/net/contargo/iris/address/dto/AddressDtoServiceImplUnitTest.java +++ b/src/test/java/net/contargo/iris/address/dto/AddressDtoServiceImplUnitTest.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import static net.contargo.iris.address.nominatim.service.AddressDetailKey.CITY; import static net.contargo.iris.address.nominatim.service.AddressDetailKey.COUNTRY; @@ -192,4 +193,16 @@ public void getAddressesByQuery() { assertThat(addresses, hasSize(1)); assertThat(addresses.get(0).getDisplayName(), is("Gartenstr. 67, Karlsruhe (Südweststadt)")); } + + + @Test + public void getAddressesByThreeWords() { + + when(addressServiceWrapperMock.getAddressByThreeWords("riches.lofts.guessing")).thenReturn(Optional.of( + new Address("German Chancellery"))); + + Optional dto = sut.getAddressesByThreeWords("riches.lofts.guessing"); + + assertThat(dto.get().getDisplayName(), is("German Chancellery")); + } } diff --git a/src/test/java/net/contargo/iris/address/service/AddressServiceWrapperCachingUnitTest.java b/src/test/java/net/contargo/iris/address/service/AddressServiceWrapperCachingUnitTest.java index 46e07006..5e3dbe14 100644 --- a/src/test/java/net/contargo/iris/address/service/AddressServiceWrapperCachingUnitTest.java +++ b/src/test/java/net/contargo/iris/address/service/AddressServiceWrapperCachingUnitTest.java @@ -6,6 +6,7 @@ import net.contargo.iris.address.nominatim.service.AddressService; import net.contargo.iris.address.staticsearch.StaticAddress; import net.contargo.iris.address.staticsearch.service.StaticAddressService; +import net.contargo.iris.address.w3w.ThreeWordClient; import net.contargo.iris.normalizer.NormalizerService; import org.junit.Before; @@ -61,6 +62,8 @@ public class AddressServiceWrapperCachingUnitTest { private StaticAddressService staticAddressServiceMock; @Mock private AddressCache addressCacheMock; + @Mock + private ThreeWordClient threeWordClientMock; @Before public void setup() { @@ -69,7 +72,7 @@ public void setup() { when(normalizerServiceMock.normalize(CITY_NAME)).thenReturn(CITY_NAME_NORMALIZED); sut = new AddressServiceWrapper(addressServiceMock, staticAddressServiceMock, addressCacheMock, - normalizerServiceMock); + normalizerServiceMock, threeWordClientMock); } diff --git a/src/test/java/net/contargo/iris/address/service/AddressServiceWrapperUnitTest.java b/src/test/java/net/contargo/iris/address/service/AddressServiceWrapperUnitTest.java index 60c8d974..65f541a1 100644 --- a/src/test/java/net/contargo/iris/address/service/AddressServiceWrapperUnitTest.java +++ b/src/test/java/net/contargo/iris/address/service/AddressServiceWrapperUnitTest.java @@ -7,6 +7,8 @@ import net.contargo.iris.address.staticsearch.StaticAddress; import net.contargo.iris.address.staticsearch.service.StaticAddressNotFoundException; import net.contargo.iris.address.staticsearch.service.StaticAddressService; +import net.contargo.iris.address.w3w.ThreeWordClient; +import net.contargo.iris.address.w3w.ThreeWordClientException; import net.contargo.iris.normalizer.NormalizerService; import org.junit.Before; @@ -24,6 +26,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import static net.contargo.iris.address.nominatim.service.AddressDetailKey.CITY; import static net.contargo.iris.address.nominatim.service.AddressDetailKey.COUNTRY; @@ -81,6 +84,8 @@ public class AddressServiceWrapperUnitTest { private NormalizerService normalizerServiceMock; @Mock private AddressListFilter addressListFilterMock; + @Mock + private ThreeWordClient threeWordClientMock; private Map addressDetails; @@ -96,8 +101,8 @@ public void setUp() { when(normalizerServiceMock.normalize(CITYNAME_KARLSRUHE)).thenReturn(CITYNAME_KARLSRUHE_NORMALIZED); - sut = new AddressServiceWrapper(addressServiceMock, staticAddressServiceMock, cacheMock, - normalizerServiceMock); + sut = new AddressServiceWrapper(addressServiceMock, staticAddressServiceMock, cacheMock, normalizerServiceMock, + threeWordClientMock); } @@ -493,4 +498,32 @@ public void getAddressesByQueryWithHashkeyNotFound() { assertThat(addresses.get(0).getCity(), is("Karlsruhe")); assertThat(addresses.get(0).getPostcode(), is("76135")); } + + + @Test + public void getAddressByThreeWords() { + + GeoLocation geoLocation = new GeoLocation(52.520146, 13.36926); + when(threeWordClientMock.resolve("riches.lofts.guessing")).thenReturn(geoLocation); + + Address address = new Address("German chancellery"); + when(cacheMock.getForLocation(geoLocation)).thenReturn(address); + + Optional
resolvedAddress = sut.getAddressByThreeWords("riches.lofts.guessing"); + + assertThat(resolvedAddress.get(), is(address)); + } + + + @Test + public void unresolvableThreeWordAddress() { + + doThrow(ThreeWordClientException.class).when(threeWordClientMock).resolve("riches.lofts.guessing"); + + Optional
resolvedAddress = sut.getAddressByThreeWords("riches.lofts.guessing"); + + assertThat(resolvedAddress.isPresent(), is(false)); + + verifyZeroInteractions(cacheMock); + } } diff --git a/src/test/java/net/contargo/iris/address/w3w/ThreeWordClientUnitTest.java b/src/test/java/net/contargo/iris/address/w3w/ThreeWordClientUnitTest.java new file mode 100644 index 00000000..e452d26b --- /dev/null +++ b/src/test/java/net/contargo/iris/address/w3w/ThreeWordClientUnitTest.java @@ -0,0 +1,104 @@ +package net.contargo.iris.address.w3w; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import net.contargo.iris.GeoLocation; + +import org.junit.Before; +import org.junit.Test; + +import org.junit.runner.RunWith; + +import org.mockito.Mock; + +import org.mockito.runners.MockitoJUnitRunner; + +import org.springframework.http.ResponseEntity; + +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; + +import java.math.BigDecimal; + +import static org.hamcrest.Matchers.is; + +import static org.junit.Assert.assertThat; + +import static org.mockito.Mockito.when; + + +/** + * @author Sandra Thieme - thieme@synyx.de + */ + +@RunWith(MockitoJUnitRunner.class) +public class ThreeWordClientUnitTest { + + private static final String API_KEY = "apikey"; + + private ThreeWordClient sut; + + @Mock + private RestTemplate restTemplateMock; + + @Before + public void setUp() { + + sut = new ThreeWordClient(restTemplateMock, API_KEY); + } + + + @Test + public void resolve() throws IOException { + + when(restTemplateMock.getForEntity( + "https://api.what3words.com/v2/forward?addr={w3wAddress}&key={apiKey}&lang=de", + ForwardW3wResponse.class, "wohin.polizist.meinung", API_KEY)).thenReturn(ResponseEntity.ok( + response())); + + GeoLocation geoLocation = sut.resolve("wohin.polizist.meinung"); + + assertThat(geoLocation, is(new GeoLocation(new BigDecimal("52.520119"), new BigDecimal("13.369304")))); + } + + + private ForwardW3wResponse response() throws IOException { + + String json = "{" + + " \"thanks\": \"Thanks from all of us at index.home.raft for using a what3words API\"," + + " \"crs\": {" + + " \"type\": \"link\"," + + " \"properties\": {" + + " \"href\": \"http://spatialreference.org/ref/epsg/4326/ogcwkt/\"," + + " \"type\": \"ogcwkt\"" + + " }" + + " }," + + " \"words\": \"wohin.polizist.meinung\"," + + " \"bounds\": {" + + " \"southwest\": {" + + " \"lng\": 13.369282," + + " \"lat\": 52.520106" + + " }," + + " \"northeast\": {" + + " \"lng\": 13.369326," + + " \"lat\": 52.520133" + + " }" + + " }," + + " \"geometry\": {" + + " \"lng\": 13.369304," + + " \"lat\": 52.520119" + + " }," + + " \"language\": \"de\"," + + " \"map\": \"http://w3w.co/wohin.polizist.meinung\"," + + " \"status\": {" + + " \"reason\": \"OK\"," + + " \"status\": 200" + + " }" + + "}"; + + return new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readValue(json, ForwardW3wResponse.class); + } +} diff --git a/src/test/java/net/contargo/iris/address/w3w/ThreeWordMatcherUnitTest.java b/src/test/java/net/contargo/iris/address/w3w/ThreeWordMatcherUnitTest.java new file mode 100644 index 00000000..542a177b --- /dev/null +++ b/src/test/java/net/contargo/iris/address/w3w/ThreeWordMatcherUnitTest.java @@ -0,0 +1,23 @@ +package net.contargo.iris.address.w3w; + +import org.junit.Test; + +import static org.hamcrest.Matchers.is; + +import static org.junit.Assert.assertThat; + + +/** + * @author Sandra Thieme - thieme@synyx.de + */ +public class ThreeWordMatcherUnitTest { + + @Test + public void isThreeWordAddress() { + + assertThat(ThreeWordMatcher.isThreeWordAddress("riches.lofts.guessing"), is(true)); + assertThat(ThreeWordMatcher.isThreeWordAddress(" riches.lofts.guessing "), is(true)); + assertThat(ThreeWordMatcher.isThreeWordAddress(null), is(false)); + assertThat(ThreeWordMatcher.isThreeWordAddress("Gartenstr. 67"), is(false)); + } +}