Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.targomo.client.api.request.GeocodingRequest Maven / Gradle / Ivy
Go to download
Java client library for easy usage of Targomo web services.
package com.targomo.client.api.request;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.targomo.client.api.Address;
import com.targomo.client.api.exception.TargomoClientException;
import com.targomo.client.api.exception.TargomoClientRuntimeException;
import com.targomo.client.api.request.esri.ESRIAuthenticationDetails;
import com.targomo.client.api.response.GeocodingResponse;
import com.targomo.client.api.response.esri.AuthenticationResponse;
import com.targomo.client.api.util.POJOUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.ServiceUnavailableException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.function.UnaryOperator;
/**
* The {@link GeocodingRequest} uses the ESRI webservice to request a coordinate candidates for one or more addresses.
*
* Multiple requests can be made with one {@link GeocodingRequest} object.
*
* @see Documentation
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class GeocodingRequest implements GetRequest {
/**
* Special Geocoding request options that can be set at creation of the request.
* For more details please see REST service documentation.
*
* @see Documentation
*/
public enum Option {
/**
* Limits the candidates returned by the findAddressCandidates operation to the specified country or countries,
* e.g. "sourceCountry=FRA,DEU,ESP"
* @see Country Codes
*/
SOURCE_COUNTRY("sourceCountry"),
/**
* A set of bounding box coordinates that limit the search area to a specific region, e.g. "-104,35.6,-94.32,41"
*/
SEARCH_EXTENT("searchExtent"),
/**
* Defines an origin point location that is used to sort geocoding candidates based on their proximity to the location.
*/
CLOSE_LOCATION("location"),
/**
* The spatial reference of the x/y coordinates returned by a geocode request, e.g. "outSR=102100"
* @see PCS
* @see GCS
*/
SPATIAL_REFERENCE("outSR"),
//The list of fields to be returned in the response. So far only default coordinates are returned -
//https://developers.arcgis.com/rest/geocode/api-reference/geocoding-service-output.htm#ESRI_SECTION1_42D7D3D0231241E9B656C01438209440
//OUTPUT_FIELDS("outFields"),
/**
* If more geocode candidates are supposed to be returned; by default only the one best fitting candidate is returned
*/
MAX_LOCATIONS("maxLocations"),
/**
* Specifies whether the results of the operation will be persisted, if true an authentication is required and
* the operation execution will be charged to the account
*/
FOR_STORAGE("forStorage");
private String name;
Option(String repName){
this.name = repName;
}
}
//ESRI Service constants
private static final String REST_URI = "https://geocode.arcgis.com";
private static final String PATH_SINGLE_ADDRESS = "arcgis/rest/services/World/GeocodeServer/findAddressCandidates";
private static final String URI_AUTHENTICATION = "https://www.arcgis.com";
private static final String PATH_AUTHENTICATION = "sharing/oauth2/token";
private static final Integer ESRI_ERROR_INVALID_TOKEN = 498;
private static final int DEFAULT_REQUEST_TIMEOUT_IN_MS = 60000;
//Class constants
private static final ObjectMapper JSON_PARSER = new ObjectMapper();
private static final Logger LOGGER = LoggerFactory.getLogger(GeocodingRequest.class);
//"Immutable" Object values
private final Client client;
private final ESRIAuthenticationDetails authenticationDetails;
private final Map requestOptions;
private final int requestTimeoutInMs;
//currently valid access token
private String currentAccessToken = null;
private final Object authSynchObject = new Object();
/**
* Creation of a default geo coding request without ESRI credentials (for batch requests slower than with credentials)
*
*
* Note1: The client's lifecycle is not handled here. Please make sure to properly close the client when not
* needed anymore.
* Note2: For the parallel batch requests it may be required to set a certain connection pool size when client is
* created. (e.g. this was necessary for a JBoss client but not for the Jersey client)
*
*
* @param client specified Client implementation to be used, e.g. Jersey or jBoss client
*/
public GeocodingRequest(Client client){
this(client, new EnumMap<>(Option.class));
}
/**
* Use a custom client implementation for the geo coding request with non-default request parameters and no
* ESRI credentials)
*
*
* Note1: The client's lifecycle is not handled here. Please make sure to properly close the client when not
* needed anymore.
* Note2: For the parallel batch requests it may be required to set a certain connection pool size when client is
* created. (e.g. this was necessary for a JBoss client but not for the Jersey client)
*
*
* @param client specified Client implementation to be used, e.g. Jersey or jBoss client
* @param extraOptions see {@link Option} for possibilities - null pointer and empty strings will be ignored
*/
public GeocodingRequest(Client client, Map extraOptions){
this(client, null, extraOptions, DEFAULT_REQUEST_TIMEOUT_IN_MS);
}
/**
* Creation of a default geo coding request with ESRI credentials.
*
*
* Note1: The client's lifecycle is not handled here. Please make sure to properly close the client when not
* needed anymore.
* Note2: For the parallel batch requests it may be required to set a certain connection pool size when client is
* created. (e.g. this was necessary for a JBoss client but not for the Jersey client)
*
*
* @param client specified Client implementation to be used, e.g. Jersey or jBoss client
* @param authenticationDetails the authentication details with which accessToken will be retrieved
*/
public GeocodingRequest(Client client, ESRIAuthenticationDetails authenticationDetails){
this(client, authenticationDetails, new EnumMap<>(Option.class), DEFAULT_REQUEST_TIMEOUT_IN_MS);
}
/**
* Creation of a default geo coding request with customizable timeout.
*
*
* Note1: The client's lifecycle is not handled here. Please make sure to properly close the client when not
* needed anymore.
* Note2: For the parallel batch requests it may be required to set a certain connection pool size when client is
* created. (e.g. this was necessary for a JBoss client but not for the Jersey client)
*
*
* @param client specified Client implementation to be used, e.g. Jersey or jBoss client
* @param requestTimeOutInMs the timeout before an request fails with {@link TargomoClientException}
*/
public GeocodingRequest(Client client, int requestTimeOutInMs){
this(client, null, new EnumMap<>(Option.class), requestTimeOutInMs);
}
/**
* Use a custom client implementation for the geo coding request with non-default request parameters and
* ESRI credentials).
*
*
* Note1: The client's lifecycle is not handled here. Please make sure to properly close the client when not
* needed anymore.
* Note2: For the parallel batch requests it may be required to set a certain connection pool size when client is
* created. (e.g. this was necessary for a JBoss client but not for the Jersey client)
*
*
* @param client specified Client implementation to be used, e.g. Jersey or jBoss client
* @param authenticationDetails the authentication details with which accessToken will be retrieved
* @param extraOptions see {@link Option} for possibilities - null pointer and empty strings will be ignored
* @param requestTimeOutInMs the timeout before an request fails with {@link TargomoClientException}
*/
@JsonCreator
public GeocodingRequest(@JsonProperty("client") Client client, @JsonProperty("authenticationDetails") ESRIAuthenticationDetails authenticationDetails,
@JsonProperty("extraOptions") Map extraOptions,@JsonProperty("requestTimeOutInMs") int requestTimeOutInMs){
this.client = client;
this.requestOptions = extraOptions;
this.authenticationDetails = authenticationDetails;
this.requestTimeoutInMs = requestTimeOutInMs;
//validation
if("true".equals(extraOptions.get(Option.FOR_STORAGE))) {
Objects.requireNonNull(this.authenticationDetails,
"client authorization is required for option " + Option.FOR_STORAGE.name + "=true");
}
if(this.requestTimeoutInMs<1)
throw new IllegalArgumentException("requestTimeOutInMs must be greater than 0 but was " + this.requestTimeoutInMs);
}
/**
* Invokes a simple request to geocode one address. The address is given in a single {@link String}.
*
* @param singleLineAddress e.g. "Chausseestr. 101, 10115 Berlin"
* @return the parsed REST service response
* @throws TargomoClientException when error occurs during request. This does not query a Targomo Service.
* @throws ProcessingException when connection error occurs
*/
@Override
public GeocodingResponse get(String singleLineAddress) throws TargomoClientException, ProcessingException {
return get( webTarget -> webTarget.queryParam("singleLine", singleLineAddress));
}
/**
* Invokes a simple request to geocode one address. The address is given in an {@link Address}.
* Empty or null fields will be ignored by the request.
*
* @param address e.g. new Address("Chausseestr. 101","","Berlin","10115",null);
* @return the parsed REST service response
* @throws TargomoClientException when error occurs during request. This does not query a Targomo Service.
* @throws ProcessingException when connection error occurs
*/
public GeocodingResponse get(Address address) throws TargomoClientException, ProcessingException {
return get( webTarget ->
conditionalQueryParam("address", address.street,
conditionalQueryParam("address2", address.streetDetails,
conditionalQueryParam("city", address.city,
conditionalQueryParam("postal", address.postalCode,
conditionalQueryParam("countryCode", address.country, webTarget))))));
}
/**
* Private method to only add a query paramter when the value is not null and not an empty string.
*/
private WebTarget conditionalQueryParam(String key, String value, WebTarget webTarget) {
if( value == null || value.isEmpty() )
return webTarget;
else
return webTarget.queryParam(key,value);
}
/**
* Private Method facilitating a single geocoding request, i.e. creating web target, requesting the result, and
* validating/parsing the result. Also includes authentication and the usage of access tokens if this request
* was created with user credentials.
*
* @param queryPrep function to prepare the query
* @return the resulting {@link GeocodingResponse}
* @throws TargomoClientException when error occurs during request. This does not query a Targomo Service.
* @throws ProcessingException when connection error occurs
*/
private GeocodingResponse get(UnaryOperator queryPrep)
throws TargomoClientException, ProcessingException {
//prepare statement
WebTarget target = queryPrep.apply(client.target(REST_URI)
.path(PATH_SINGLE_ADDRESS)
.queryParam("f", "json"));
if(!requestOptions.containsKey(Option.MAX_LOCATIONS) )
target = target.queryParam(Option.MAX_LOCATIONS.name, 1);
for(Map.Entry entry : requestOptions.entrySet())
target = conditionalQueryParam(entry.getKey().name, entry.getValue(), target);
boolean tokenIsInvalid = this.currentAccessToken == null;
WebTarget finalTarget;
do {
//add token if authorization available
if (authenticationDetails != null) {
if ( tokenIsInvalid )
authenticateWithAccountAndRetrieveValidToken(); //throws Exception if unsuccessful
finalTarget = target.queryParam("token", this.currentAccessToken);
} else
finalTarget = target;
//execute request
LOGGER.debug("Executing geocoding request to URI: {}", finalTarget.getUri());
WebTarget immutableTarget = finalTarget;
Response response = null;
try {
ExecutorService singleThread = Executors.newSingleThreadExecutor();
response = singleThread.submit( () -> immutableTarget.request().buildGet().invoke() )
.get(requestTimeoutInMs, TimeUnit.MILLISECONDS);
shutdownServiceExecutor(singleThread);
GeocodingResponse reqResponse = validateGeocodingResponse(response);
if (reqResponse.wasErrorResponse() &&
reqResponse.getError().getCode().equals(ESRI_ERROR_INVALID_TOKEN)) {
if( tokenIsInvalid ) // do not loop a second time - instead throw an error
throw new TargomoClientException( Thread.currentThread() + "Freshly retrieved token already " +
"invalid - should never happen: \n" + POJOUtil.prettyPrintPOJO(reqResponse.getError()));
tokenIsInvalid = true;
} else
return reqResponse; //successful request with valid response (can also be an error unrelated to the token validity)
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new TargomoClientException( "Error occurred during Geocoding an address", e);
} finally {
if(response != null)
response.close();
}
} while (true);
// The above while-loop is looping at least once and a maximum of two times - there are three possible outcomes:
// (1) The authentication (either executed in the first or second loop iteration) produced an error -> TargomoClientException
// (2) An Token invalid error occurred AFTER a token was retrieved through authentication -> TargomoClientException
// (3) The geocoding request finished "successfully" without the above two errors:
// (3a) Token there and still valid -> valid response
// (3b) Token not there -> authentication -> request -> valid response
// (3c) Token there but invalid -> 2. iteration of loop -> (3b)
// Note that a valid response can also be an error caused by a different request problem
}
/**
* Private method to make sure a valid access token is set for the next request
*
* @return true if successful
* @throws TargomoClientException if unsuccessful. This does not query a Targomo Service.
*/
private boolean authenticateWithAccountAndRetrieveValidToken() throws TargomoClientException {
this.currentAccessToken = null;
synchronized (this.authSynchObject) {
//make sure only one authorization is carried out
if (this.currentAccessToken == null)
this.currentAccessToken = retrieveNewTokenViaAuthentication();
}
return true;
}
/**
* Caries out the authentication against the ESRI service interface - uses authenticationDetails to retrieve a
* valid token (which can expire after a while)
*
* @return the access token
* @throws TargomoClientException if unsuccessful - otherwise returns the access token. This does not query a Targomo Service.
*/
private String retrieveNewTokenViaAuthentication() throws TargomoClientException {
WebTarget target = client
.target(URI_AUTHENTICATION)
.path(PATH_AUTHENTICATION)
.queryParam("grant_type", "client_credentials")
.queryParam("f", "json")
.queryParam("client_id", this.authenticationDetails.getClientID())
.queryParam("client_secret", this.authenticationDetails.getClientSecret())
.queryParam("expiration", this.authenticationDetails.getTokenExpirationInMinutes());
LOGGER.debug("Have to redo authentication for ESRI user with client id: {}",
this.authenticationDetails.getClientID());
Response response = null;
AuthenticationResponse auth;
try{
ExecutorService singleThread = Executors.newSingleThreadExecutor();
response = singleThread.submit( () -> target.request().buildGet().invoke() )
.get(requestTimeoutInMs, TimeUnit.MILLISECONDS);
shutdownServiceExecutor(singleThread);
auth = validateAuthenticationResponse(response);
if (auth.wasErrorResponse())
throw new TargomoClientException("Error occurred during authentication with ESRI Service - \nRequest: \n" +
target.getUri() + "\nResponse: \n" + POJOUtil.prettyPrintPOJO(auth.getError()));
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new TargomoClientException( "Error occurred during authentication at ESRI for Geocoding", e);
} finally {
if(response != null)
response.close();
}
return auth.getAccessToken();
}
/**
* Facilitating a parallel batch request for geocoding multiple addresses given as single String. It uses an
* {@link ExecutorService} with a specified thread pool size. These threads are used to request single geocoding
* results in parallel. Due to temporal unavailability of the service the request may be repeated a number
* of times before failing.
*
* @see GeocodingRequest#get(String)
*
* @param parallelThreads greater than or equal to 1 (==1 means sequential processing) - be careful to not create too many Threads
* @param triesBeforeFail greater than or equal to 1
* @param addresses not null and with at least on element
* @return the resulting individual responses - same order as input addresses
* @throws TargomoClientException when error occurs during request. This does not query a Targomo Service.
* @throws ProcessingException when connection error occurs
*/
public GeocodingResponse[] getBatchParallel( final int parallelThreads, final int triesBeforeFail,
final String... addresses)
throws TargomoClientException, ProcessingException {
return getBatchParallel( this::get, parallelThreads, triesBeforeFail, addresses);
}
/**
* Facilitating a parallel batch request for geocoding multiple {@link Address Addresses}. It uses an
* {@link ExecutorService} with a specified thread pool size. These threads are used to request single geocoding
* results in parallel. Due to temporal unavailability of the service the request may be repeated a number
* of times before failing.
*
* @see GeocodingRequest#get(Address)
*
* @param parallelThreads greater than or equal to 1 (==1 means sequential processing) - be careful to not create too many Threads
* @param triesBeforeFail greater than or equal to 1
* @param addresses not null and with at least on element
* @return the resulting individual responses - same order as input addresses
* @throws TargomoClientException when error occurs during request. This does not query a Targomo Service.
* @throws ProcessingException when connection error occurs
*/
public GeocodingResponse[] getBatchParallel( final int parallelThreads, final int triesBeforeFail,
final Address... addresses)
throws TargomoClientException, ProcessingException {
return getBatchParallel( this::get, parallelThreads, triesBeforeFail, addresses);
}
/**
* Private Method facilitating a parallel batch request for geocoding multiple addresses. It uses an
* {@link ExecutorService} with a specified thread pool size. These threads are used to request single geocoding
* results in parallel. Due to temporal unavailability of the service the request may be repeated a number
* of times before failing.
*
* @see GeocodingRequest#get(Function)
*
* @param singleRequest
* @param parallelThreads greater than or equal to 1 (==1 means sequential processing) - be careful to not create too many Threads
* @param triesBeforeFail greater than or equal to 1
* @param addresses not null and with at least on element
* @param address type (String or {@link Address})
* @return the resulting individual responses - same order as input addresses
* @throws TargomoClientException when error occurs during request. This does not query a Targomo Service.
* @throws ProcessingException when connection error occurs
*/
private GeocodingResponse[] getBatchParallel(
final GetRequest singleRequest,
final int parallelThreads, final int triesBeforeFail, final A[] addresses)
throws TargomoClientException, ProcessingException {
if(parallelThreads<1)
throw new TargomoClientRuntimeException("The number of specified threads has to be equal or greater than one.");
if(triesBeforeFail<1)
throw new TargomoClientRuntimeException("The number of specified tries has to be equal or greater than one.");
if(addresses == null || addresses.length == 0)
throw new TargomoClientRuntimeException("The addresses array has to be not null and contain at least one element.");
final ExecutorService executor = Executors.newFixedThreadPool(parallelThreads);
final List> requests = new ArrayList<>();
for (A singleAddress : addresses) {
requests.add( () -> { //Adding individual Callables to be executed in available parallel Threads
// will terminate after the n-th try
for( int numberOfTries = 0; numberOfTries < triesBeforeFail; numberOfTries ++) {
try {
return singleRequest.get(singleAddress);
// special case since the service is sometimes unavailable when too many parallel requests are processed
} catch (ServiceUnavailableException e) { /* do nothing, i.e. ignore the exception and try again */ }
}
throw new ServiceUnavailableException("Even after " + triesBeforeFail + " tries the service was still " +
"unavailable. Try reducing the thread number or increasing the number of tries.");
});
}
try {
//Execution of the requests
List> results = executor.invokeAll(requests);
//at finish shutdown of the parallel executor
shutdownServiceExecutor(executor);
//Collecting results
GeocodingResponse[] resultArray = new GeocodingResponse[addresses.length];
int i = 0;
for(Future result : results)
resultArray[i++] = result.get();
return resultArray;
} catch(InterruptedException | ExecutionException e) {
throw new TargomoClientException("Parallel Execution Failed! Cause: ", e);
}
}
/**
* Oracle's proposed routine to savely shutdown the {@link ExecutorService}.
* @param executor
* @throws InterruptedException
*/
private void shutdownServiceExecutor(ExecutorService executor) throws InterruptedException {
executor.shutdown();
try {
if (!executor.awaitTermination(800, TimeUnit.MILLISECONDS))
executor.shutdownNow();
} catch (InterruptedException e) {
executor.shutdownNow();
throw e;
}
}
/**
* Facilitating a sequential batch request for geocoding multiple addresses each given as single String.
*
* @see GeocodingRequest#get(String)
*
* @param addresses not null and with at least on element
* @return the resulting individual responses - same order as input addresses
* @throws TargomoClientException when error occurs during request. This does not query a Targomo Service.
* @throws ProcessingException when connection error occurs
*/
public GeocodingResponse[] getBatchSequential(String... addresses) throws TargomoClientException, ProcessingException {
return getBatchSequential( this::get, addresses);
}
/**
* Facilitating a sequential batch request for geocoding multiple {@link Address Addresses}.
*
* @see GeocodingRequest#get(Address)
*
* @param addresses not null and with at least on element
* @return the resulting individual responses - same order as input addresses
* @throws TargomoClientException when error occurs during request. This does not query a Targomo Service.
* @throws ProcessingException when connection error occurs
*/
public GeocodingResponse[] getBatchSequential(Address... addresses) throws TargomoClientException, ProcessingException {
return getBatchSequential( this::get, addresses);
}
public String getCurrentAccessToken() {
return currentAccessToken;
}
/**
* Private helper method to sequentially batch request for geocoding multiple {@link Address Addresses}.
*
* @see GeocodingRequest#get(Address)
*
* @param addresses not null and with at least on element
* @return the resulting individual responses - same order as input addresses
* @throws TargomoClientException when error occurs during request. This does not query a Targomo Service.
* @throws ProcessingException when connection error occurs
*/
private GeocodingResponse[] getBatchSequential(final GetRequest singleRequest,
A[] addresses) throws TargomoClientException, ProcessingException {
if(addresses == null || addresses.length == 0)
throw new TargomoClientException("The addresses array has to be not null and contain at least one element.");
GeocodingResponse[] batchResults = new GeocodingResponse[addresses.length];
for( int i = 0; i {
try {
return JSON_PARSER.readValue(jsonString, AuthenticationResponse.class);
} catch (IOException e) {
throw new TargomoClientRuntimeException(e.getMessage(), e);
}
});
}
/**
* Private method to help validate and parse a request response.
*
* @param response object to be validated
* @param parser the parser that is executed if no errors occurred
* @param the return type of the response, e.g. {@link AuthenticationResponse} or {@link GeocodingResponse}
* @return interpreted/parsed response of type T
* @throws TargomoClientException when an unexpected error occurs during request. This does not query a Targomo Service.
*/
private T validateResponse(final Response response, final Function parser) throws TargomoClientException {
// compare the HTTP status codes, NOT the route 360 code
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
// parse the results
String jsonString = response.readEntity(String.class);
return parser.apply(jsonString);
} else if(response.getStatus() == Response.Status.SERVICE_UNAVAILABLE.getStatusCode() )
throw new ServiceUnavailableException(); // Some clients (e.g. jBoss) return SERVICE_UNAVAILABLE while others will wait
else
throw new TargomoClientException("Request failed with response: \n" + response.readEntity(String.class));
}
}