com.maxmind.geoip2.WebServiceClient Maven / Gradle / Ivy
Show all versions of geoip2 Show documentation
package com.maxmind.geoip2;
import java.io.IOException;
import java.net.InetAddress;
import java.util.*;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.maxmind.geoip2.exception.AddressNotFoundException;
import com.maxmind.geoip2.exception.AuthenticationException;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.exception.HttpException;
import com.maxmind.geoip2.exception.InvalidRequestException;
import com.maxmind.geoip2.exception.OutOfQueriesException;
import com.maxmind.geoip2.model.CityResponse;
import com.maxmind.geoip2.model.CountryResponse;
import com.maxmind.geoip2.model.InsightsResponse;
/**
*
* This class provides a client API for all the GeoIP2 Precision web service end
* points. The end points are Country and Insights. Each end point returns a
* different set of data about an IP address, with Country returning the least
* data and Insights the most.
*
*
* Each web service end point is represented by a different model class, and
* these model classes in turn contain multiple Record classes. The record
* classes have attributes which contain data about the IP address.
*
*
* If the web service does not return a particular piece of data for an IP
* address, the associated attribute is not populated.
*
*
* The web service may not return any information for an entire record, in which
* case all of the attributes for that record class will be empty.
*
* Usage
*
* The basic API for this class is the same for all of the web service end
* points. First you create a web service object with your MaxMind
* {@code userId} and {@code licenseKey}, then you call the method corresponding
* to a specific end point, passing it the IP address you want to look up.
*
*
* If the request succeeds, the method call will return a model class for the
* end point you called. This model in turn contains multiple record classes,
* each of which represents part of the data returned by the web service.
*
*
* If the request fails, the client class throws an exception.
*
* Exceptions
*
* For details on the possible errors returned by the web service itself, see the GeoIP2 web
* service documentation.
*
*
* If the web service returns an explicit error document, this is thrown as a
* {@link InvalidRequestException}. If some other sort of transport error
* occurs, this is thrown as a {@link HttpException}. The difference is that the
* web service error includes an error message and error code delivered by the
* web service. The latter is thrown when some sort of unanticipated error
* occurs, such as the web service returning a 500 or an invalid error document.
*
*
* If the web service returns any status code besides 200, 4xx, or 5xx, this
* also becomes a {@link HttpException}.
*
*
* Finally, if the web service returns a 200 but the body is invalid, the client
* throws a {@link GeoIp2Exception}.
*
*/
public class WebServiceClient implements GeoIp2Provider {
private final String host;
private final List locales;
private final String licenseKey;
private final int connectTimeout;
private final int readTimeout;
private final HttpTransport testTransport;
private final int userId;
private WebServiceClient(Builder builder) {
this.host = builder.host;
this.locales = builder.locales;
this.licenseKey = builder.licenseKey;
this.connectTimeout = builder.connectTimeout;
this.readTimeout = builder.readTimeout;
this.testTransport = builder.testTransport;
this.userId = builder.userId;
}
/**
*
* {@code Builder} creates instances of {@code WebServiceClient}
* from values set by the methods.
*
*
* This example shows how to create a {@code WebServiceClient} object
* with the {@code Builder}:
*
*
* WebServiceClient client = new
* WebServiceClient.Builder(12,"licensekey").host
* ("geoip.maxmind.com").build();
*
*
* Only the values set in the {@code Builder} constructor are required.
*
*/
public static final class Builder {
final int userId;
final String licenseKey;
String host = "geoip.maxmind.com";
List locales = Collections.singletonList("en");
int connectTimeout = 3000;
int readTimeout = 20000;
HttpTransport testTransport;
/**
* @param userId Your MaxMind user ID.
* @param licenseKey Your MaxMind license key.
*/
public Builder(int userId, String licenseKey) {
this.userId = userId;
this.licenseKey = licenseKey;
}
/**
* @param val Timeout in milliseconds to establish a connection to the
* web service. The default is 3000 (3 seconds).
* @return Builder object
*/
public Builder connectTimeout(int val) {
this.connectTimeout = val;
return this;
}
/**
* @param val The host to use.
* @return Builder object
*/
public Builder host(String val) {
this.host = val;
return this;
}
/**
* @param val List of locale codes to use in name property from most
* preferred to least preferred.
* @return Builder object
*/
public Builder locales(List val) {
this.locales = val;
return this;
}
/**
* @param val readTimeout in milliseconds to read data from an
* established connection to the web service. The default is
* 20000 (20 seconds).
* @return Builder object
*/
public Builder readTimeout(int val) {
this.readTimeout = val;
return this;
}
/**
* @param val Transport for unit testing.
* @return Builder object
*/
Builder testTransport(HttpTransport val) {
this.testTransport = val;
return this;
}
/**
* @return an instance of {@code WebServiceClient} created from the
* fields set on this builder.
*/
public WebServiceClient build() {
return new WebServiceClient(this);
}
}
/**
* @return A Country model for the requesting IP address
* @throws GeoIp2Exception if there is an error from the web service
* @throws IOException if an IO error happens during the request
*/
public CountryResponse country() throws IOException, GeoIp2Exception {
return this.country(null);
}
@Override
public CountryResponse country(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return this.responseFor("country", ipAddress, CountryResponse.class);
}
/**
* @return A City model for the requesting IP address
* @throws GeoIp2Exception if there is an error from the web service
* @throws IOException if an IO error happens during the request
*/
public CityResponse city() throws IOException, GeoIp2Exception {
return this.city(null);
}
@Override
public CityResponse city(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return this.responseFor("city", ipAddress, CityResponse.class);
}
/**
* @return An Insights model for the requesting IP address
* @throws GeoIp2Exception if there is an error from the web service
* @throws IOException if an IO error happens during the request
*/
public InsightsResponse insights() throws IOException, GeoIp2Exception {
return this.insights(null);
}
/**
* @param ipAddress IPv4 or IPv6 address to lookup.
* @return A Insight model for the requested IP address.
* @throws GeoIp2Exception if there is an error looking up the IP
* @throws IOException if there is an IO error
*/
public InsightsResponse insights(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return this.responseFor("insights", ipAddress, InsightsResponse.class);
}
private T responseFor(String path, InetAddress ipAddress, Class cls)
throws GeoIp2Exception, IOException {
GenericUrl uri = this.createUri(path, ipAddress);
HttpResponse response = this.getResponse(uri);
Long contentLength = response.getHeaders().getContentLength();
if (contentLength == null || contentLength.intValue() <= 0) {
throw new HttpException("Received a 200 response for " + uri
+ " but there was no message body.", 200, uri.toURL());
}
String body = WebServiceClient.getSuccessBody(response, uri);
InjectableValues inject = new InjectableValues.Std().addValue(
"locales", this.locales);
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
try {
return mapper.reader(cls).with(inject).readValue(body);
} catch (IOException e) {
throw new GeoIp2Exception(
"Received a 200 response but not decode it as JSON: "
+ body);
}
}
private HttpResponse getResponse(GenericUrl uri) throws GeoIp2Exception,
IOException {
// We only use a instance variable for HttpTransport when unit testing
// as
// HttpTransport is not thread safe.
HttpTransport transport = this.testTransport == null ? new NetHttpTransport()
: this.testTransport;
HttpRequestFactory requestFactory = transport.createRequestFactory();
HttpRequest request;
try {
request = requestFactory.buildGetRequest(uri);
} catch (IOException e) {
throw new GeoIp2Exception("Error building request", e);
}
request.setConnectTimeout(this.connectTimeout);
request.setReadTimeout(this.readTimeout);
HttpHeaders headers = request.getHeaders();
headers.setAccept("application/json");
headers.setBasicAuthentication(String.valueOf(this.userId),
this.licenseKey);
headers.setUserAgent("GeoIP2 Java Client v"
+ this.getClass().getPackage().getImplementationVersion() + ";");
try {
return request.execute();
} catch (HttpResponseException e) {
int status = e.getStatusCode();
if ((status >= 400) && (status < 500)) {
WebServiceClient.handle4xxStatus(e.getContent(), status, uri);
} else if ((status >= 500) && (status < 600)) {
throw new HttpException("Received a server error (" + status
+ ") for " + uri, status, uri.toURL());
}
throw new HttpException("Received a very surprising HTTP status ("
+ status + ") for " + uri, status, uri.toURL());
}
}
private static String getSuccessBody(HttpResponse response, GenericUrl uri)
throws GeoIp2Exception {
String body;
try {
body = response.parseAsString();
} catch (IOException e) {
throw new GeoIp2Exception(
"Received a 200 response but not decode message body: "
+ e.getMessage());
}
if (response.getContentType() == null
|| !response.getContentType().contains("json")) {
throw new GeoIp2Exception("Received a 200 response for " + uri
+ " but it does not appear to be JSON:\n" + body);
}
return body;
}
private static void handle4xxStatus(String body, int status, GenericUrl uri)
throws GeoIp2Exception, HttpException {
if (body == null) {
throw new HttpException("Received a " + status + " error for "
+ uri + " with no body", status, uri.toURL());
}
try {
ObjectMapper mapper = new ObjectMapper();
Map content = mapper.readValue(body,
new TypeReference>() {
});
handleErrorWithJsonBody(content, body, status, uri);
} catch (HttpException e) {
throw e;
} catch (IOException e) {
throw new HttpException("Received a " + status + " error for "
+ uri + " but it did not include the expected JSON body: "
+ body, status, uri.toURL());
}
}
private static void handleErrorWithJsonBody(Map content,
String body, int status, GenericUrl uri) throws GeoIp2Exception,
HttpException {
String error = content.get("error");
String code = content.get("code");
if (error == null || code == null) {
throw new HttpException(
"Response contains JSON but it does not specify code or error keys: "
+ body, status, uri.toURL());
}
if (code.equals("IP_ADDRESS_NOT_FOUND")
|| code.equals("IP_ADDRESS_RESERVED")) {
throw new AddressNotFoundException(error);
} else if (code.equals("AUTHORIZATION_INVALID")
|| code.equals("LICENSE_KEY_REQUIRED")
|| code.equals("USER_ID_REQUIRED")) {
throw new AuthenticationException(error);
} else if (code.equals("OUT_OF_QUERIES")) {
throw new OutOfQueriesException(error);
}
// These should be fairly rare
throw new InvalidRequestException(error, code, uri.toURL());
}
private GenericUrl createUri(String path, InetAddress ipAddress) {
return new GenericUrl("https://" + this.host + "/geoip/v2.1/" + path
+ "/" + (ipAddress == null ? "me" : ipAddress.getHostAddress()));
}
}