package com.maxmind.geoip2;
import com.maxmind.db.DatabaseRecord;
import com.maxmind.db.Metadata;
import com.maxmind.db.Network;
import com.maxmind.db.NoCache;
import com.maxmind.db.NodeCache;
import com.maxmind.db.Reader;
import com.maxmind.db.Reader.FileMode;
import com.maxmind.geoip2.exception.AddressNotFoundException;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.AnonymousIpResponse;
import com.maxmind.geoip2.model.AsnResponse;
import com.maxmind.geoip2.model.CityResponse;
import com.maxmind.geoip2.model.ConnectionTypeResponse;
import com.maxmind.geoip2.model.CountryResponse;
import com.maxmind.geoip2.model.DomainResponse;
import com.maxmind.geoip2.model.EnterpriseResponse;
import com.maxmind.geoip2.model.IpRiskResponse;
import com.maxmind.geoip2.model.IspResponse;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
/**
*
* The class {@code DatabaseReader} provides a reader for the GeoIP2 database
* format.
*
* Usage
*
* To use the database API, you must create a new {@code DatabaseReader} using
* the {@code DatabaseReader.Builder}. You must provide the {@code Builder}
* constructor either an {@code InputStream} or {@code File} for your GeoIP2
* database. You may also specify the {@code fileMode} and the {@code locales}
* fallback order using the methods on the {@code Builder} object.
*
*
* After you have created the {@code DatabaseReader}, you may then call one of
* the appropriate methods, e.g., {@code city} or {@code tryCity}, for your
* database. These methods take the IP address to be looked up. The methods
* with the "try" prefix return an {@code Optional} object, which will be
* empty if the value is not present in the database. The method without the
* prefix will throw an {@code AddressNotFoundException} if the address is
* not in the database. If you are looking up many IPs that are not contained
* in the database, the "try" method will be slightly faster as they do not
* need to construct and throw an exception. These methods otherwise behave
* the same.
*
*
* If the lookup succeeds, the method call will return a response class for
* the GeoIP2 lookup. The class in turn contains multiple record classes,
* each of which represents part of the data returned by the database.
*
*
* We recommend reusing the {@code DatabaseReader} object rather than creating
* a new one for each lookup. The creation of this object is relatively
* expensive as it must read in metadata for the file. It is safe to share the
* object across threads.
*
* Caching
*
* The database API supports pluggable caching (by default, no caching is
* performed). A simple implementation is provided by
* {@code com.maxmind.db.CHMCache}. Using this cache, lookup performance is
* significantly improved at the cost of a small (~2MB) memory overhead.
*
*/
public class DatabaseReader implements DatabaseProvider, Closeable {
private final Reader reader;
private final List locales;
private final int databaseType;
private enum DatabaseType {
ANONYMOUS_IP,
ASN,
CITY,
CONNECTION_TYPE,
COUNTRY,
DOMAIN,
ENTERPRISE,
IP_RISK,
ISP;
final int type;
DatabaseType() {
type = 1 << this.ordinal();
}
}
private DatabaseReader(Builder builder) throws IOException {
if (builder.stream != null) {
this.reader = new Reader(builder.stream, builder.cache);
} else if (builder.database != null) {
this.reader = new Reader(builder.database, builder.mode, builder.cache);
} else {
// This should never happen. If it does, review the Builder class
// constructors for errors.
throw new IllegalArgumentException(
"Unsupported Builder configuration: expected either File or URL");
}
this.locales = builder.locales;
databaseType = getDatabaseType();
}
private int getDatabaseType() {
String databaseType = this.getMetadata().getDatabaseType();
int type = 0;
if (databaseType.contains("GeoIP2-Anonymous-IP")) {
type |= DatabaseType.ANONYMOUS_IP.type;
}
if (databaseType.contains("GeoIP2-IP-Risk")) {
type |= DatabaseType.IP_RISK.type;
}
if (databaseType.contains("GeoLite2-ASN")) {
type |= DatabaseType.ASN.type;
}
if (databaseType.contains("City")) {
type |= DatabaseType.CITY.type | DatabaseType.COUNTRY.type;
}
if (databaseType.contains("GeoIP2-Connection-Type")) {
type |= DatabaseType.CONNECTION_TYPE.type;
}
if (databaseType.contains("Country")) {
type |= DatabaseType.COUNTRY.type;
}
if (databaseType.contains("GeoIP2-Domain")) {
type |= DatabaseType.DOMAIN.type;
}
if (databaseType.contains("Enterprise")) {
type |=
DatabaseType.ENTERPRISE.type | DatabaseType.CITY.type | DatabaseType.COUNTRY.type;
}
if (databaseType.contains("GeoIP2-ISP")) {
type |= DatabaseType.ISP.type;
}
if (type == 0) {
// XXX - exception type
throw new UnsupportedOperationException(
"Invalid attempt to open an unknown database type: " + databaseType);
}
return type;
}
/**
*
* Constructs a Builder for the {@code DatabaseReader}. The file passed to
* it must be a valid GeoIP2 database file.
*
*
* {@code Builder} creates instances of {@code DatabaseReader}
* from values set by the methods.
*
*
* Only the values set in the {@code Builder} constructor are required.
*
*/
public static final class Builder {
final File database;
final InputStream stream;
List locales = Collections.singletonList("en");
FileMode mode = FileMode.MEMORY_MAPPED;
NodeCache cache = NoCache.getInstance();
/**
* @param stream the stream containing the GeoIP2 database to use.
*/
public Builder(InputStream stream) {
this.stream = stream;
this.database = null;
}
/**
* @param database the GeoIP2 database file to use.
*/
public Builder(File database) {
this.database = database;
this.stream = null;
}
/**
* @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 cache backing cache instance
* @return Builder object
*/
public Builder withCache(NodeCache cache) {
this.cache = cache;
return this;
}
/**
* @param val The file mode used to open the GeoIP2 database
* @return Builder object
* @throws java.lang.IllegalArgumentException if you initialized the Builder with a URL,
* which uses {@link FileMode#MEMORY}, but you
* provided a different FileMode to this method.
*/
public Builder fileMode(FileMode val) {
if (this.stream != null && FileMode.MEMORY != val) {
throw new IllegalArgumentException(
"Only FileMode.MEMORY is supported when using an InputStream.");
}
this.mode = val;
return this;
}
/**
* @return an instance of {@code DatabaseReader} created from the
* fields set on this builder.
* @throws IOException if there is an error reading the database
*/
public DatabaseReader build() throws IOException {
return new DatabaseReader(this);
}
}
static final class LookupResult {
final T model;
final String ipAddress;
final Network network;
LookupResult(T model, String ipAddress, Network network) {
this.model = model;
this.ipAddress = ipAddress;
this.network = network;
}
T getModel() {
return this.model;
}
String getIpAddress() {
return this.ipAddress;
}
Network getNetwork() {
return this.network;
}
}
/**
* @param ipAddress IPv4 or IPv6 address to lookup.
* @param cls The class to deserialize to.
* @param expectedType The expected database type.
* @return A {@code LookupResult} object with the data for the IP address
* @throws IOException if there is an error opening or reading from the file.
*/
private LookupResult get(InetAddress ipAddress, Class cls,
DatabaseType expectedType)
throws IOException {
if ((databaseType & expectedType.type) == 0) {
String caller = Thread.currentThread().getStackTrace()[3]
.getMethodName();
throw new UnsupportedOperationException(
"Invalid attempt to open a " + getMetadata().getDatabaseType()
+ " database using the " + caller + " method");
}
DatabaseRecord record = reader.getRecord(ipAddress, cls);
T o = record.getData();
return new LookupResult<>(o, ipAddress.getHostAddress(), record.getNetwork());
}
/**
*
* Closes the database.
*
*
* If you are using {@code FileMode.MEMORY_MAPPED}, this will
* not unmap the underlying file due to a limitation in Java's
* {@code MappedByteBuffer}. It will however set the reference to
* the buffer to {@code null}, allowing the garbage collector to
* collect it.
*
*
* @throws IOException if an I/O error occurs.
*/
@Override
public void close() throws IOException {
this.reader.close();
}
@Override
public CountryResponse country(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
Optional r = getCountry(ipAddress);
if (r.isEmpty()) {
throw new AddressNotFoundException("The address "
+ ipAddress.getHostAddress() + " is not in the database.");
}
return r.get();
}
@Override
public Optional tryCountry(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return getCountry(ipAddress);
}
private Optional getCountry(
InetAddress ipAddress
) throws IOException, GeoIp2Exception {
LookupResult result = this.get(
ipAddress,
CountryResponse.class,
DatabaseType.COUNTRY
);
CountryResponse response = result.getModel();
if (response == null) {
return Optional.empty();
}
return Optional.of(
new CountryResponse(
response,
result.getIpAddress(),
result.getNetwork(),
locales
)
);
}
@Override
public CityResponse city(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
Optional r = getCity(ipAddress);
if (r.isEmpty()) {
throw new AddressNotFoundException("The address "
+ ipAddress.getHostAddress() + " is not in the database.");
}
return r.get();
}
@Override
public Optional tryCity(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return getCity(ipAddress);
}
private Optional getCity(
InetAddress ipAddress
) throws IOException, GeoIp2Exception {
LookupResult result = this.get(
ipAddress,
CityResponse.class,
DatabaseType.CITY
);
CityResponse response = result.getModel();
if (response == null) {
return Optional.empty();
}
return Optional.of(
new CityResponse(
response,
result.getIpAddress(),
result.getNetwork(),
locales
)
);
}
/**
* Look up an IP address in a GeoIP2 Anonymous IP.
*
* @param ipAddress IPv4 or IPv6 address to lookup.
* @return a AnonymousIpResponse for the requested IP address.
* @throws GeoIp2Exception if there is an error looking up the IP
* @throws IOException if there is an IO error
*/
@Override
public AnonymousIpResponse anonymousIp(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
Optional r = getAnonymousIp(ipAddress);
if (r.isEmpty()) {
throw new AddressNotFoundException("The address "
+ ipAddress.getHostAddress() + " is not in the database.");
}
return r.get();
}
@Override
public Optional tryAnonymousIp(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return getAnonymousIp(ipAddress);
}
private Optional getAnonymousIp(
InetAddress ipAddress
) throws IOException, GeoIp2Exception {
LookupResult result = this.get(
ipAddress,
AnonymousIpResponse.class,
DatabaseType.ANONYMOUS_IP
);
AnonymousIpResponse response = result.getModel();
if (response == null) {
return Optional.empty();
}
return Optional.of(
new AnonymousIpResponse(
response,
result.getIpAddress(),
result.getNetwork()
)
);
}
/**
* Look up an IP address in a GeoIP2 IP Risk database.
*
* @param ipAddress IPv4 or IPv6 address to lookup.
* @return a IPRiskResponse for the requested IP address.
* @throws GeoIp2Exception if there is an error looking up the IP
* @throws IOException if there is an IO error
* @deprecated This database has been discontinued.
*/
@Deprecated
@Override
public IpRiskResponse ipRisk(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
Optional r = getIpRisk(ipAddress);
if (r.isEmpty()) {
throw new AddressNotFoundException("The address "
+ ipAddress.getHostAddress() + " is not in the database.");
}
return r.get();
}
@Deprecated
@Override
public Optional tryIpRisk(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return getIpRisk(ipAddress);
}
@Deprecated
private Optional getIpRisk(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
LookupResult result = this.get(
ipAddress,
IpRiskResponse.class,
DatabaseType.IP_RISK
);
IpRiskResponse response = result.getModel();
if (response == null) {
return Optional.empty();
}
return Optional.of(
new IpRiskResponse(
response,
result.getIpAddress(),
result.getNetwork()
)
);
}
/**
* Look up an IP address in a GeoLite2 ASN database.
*
* @param ipAddress IPv4 or IPv6 address to lookup.
* @return an AsnResponse for the requested IP address.
* @throws GeoIp2Exception if there is an error looking up the IP
* @throws IOException if there is an IO error
*/
@Override
public AsnResponse asn(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
Optional r = getAsn(ipAddress);
if (r.isEmpty()) {
throw new AddressNotFoundException("The address "
+ ipAddress.getHostAddress() + " is not in the database.");
}
return r.get();
}
@Override
public Optional tryAsn(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return getAsn(ipAddress);
}
private Optional getAsn(InetAddress ipAddress)
throws IOException, GeoIp2Exception {
LookupResult result = this.get(
ipAddress,
AsnResponse.class,
DatabaseType.ASN
);
AsnResponse response = result.getModel();
if (response == null) {
return Optional.empty();
}
return Optional.of(
new AsnResponse(
response,
result.getIpAddress(),
result.getNetwork()
)
);
}
/**
* Look up an IP address in a GeoIP2 Connection Type database.
*
* @param ipAddress IPv4 or IPv6 address to lookup.
* @return a ConnectTypeResponse for the requested IP address.
* @throws GeoIp2Exception if there is an error looking up the IP
* @throws IOException if there is an IO error
*/
@Override
public ConnectionTypeResponse connectionType(InetAddress ipAddress)
throws IOException, GeoIp2Exception {
Optional r = getConnectionType(ipAddress);
if (r.isEmpty()) {
throw new AddressNotFoundException("The address "
+ ipAddress.getHostAddress() + " is not in the database.");
}
return r.get();
}
@Override
public Optional tryConnectionType(InetAddress ipAddress)
throws IOException, GeoIp2Exception {
return getConnectionType(ipAddress);
}
private Optional getConnectionType(
InetAddress ipAddress
) throws IOException, GeoIp2Exception {
LookupResult result = this.get(
ipAddress,
ConnectionTypeResponse.class,
DatabaseType.CONNECTION_TYPE
);
ConnectionTypeResponse response = result.getModel();
if (response == null) {
return Optional.empty();
}
return Optional.of(
new ConnectionTypeResponse(
response,
result.getIpAddress(),
result.getNetwork()
)
);
}
/**
* Look up an IP address in a GeoIP2 Domain database.
*
* @param ipAddress IPv4 or IPv6 address to lookup.
* @return a DomainResponse for the requested IP address.
* @throws GeoIp2Exception if there is an error looking up the IP
* @throws IOException if there is an IO error
*/
@Override
public DomainResponse domain(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
Optional r = getDomain(ipAddress);
if (r.isEmpty()) {
throw new AddressNotFoundException("The address "
+ ipAddress.getHostAddress() + " is not in the database.");
}
return r.get();
}
@Override
public Optional tryDomain(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return getDomain(ipAddress);
}
private Optional getDomain(
InetAddress ipAddress
) throws IOException, GeoIp2Exception {
LookupResult result = this.get(
ipAddress,
DomainResponse.class,
DatabaseType.DOMAIN
);
DomainResponse response = result.getModel();
if (response == null) {
return Optional.empty();
}
return Optional.of(
new DomainResponse(
response,
result.getIpAddress(),
result.getNetwork()
)
);
}
/**
* Look up an IP address in a GeoIP2 Enterprise database.
*
* @param ipAddress IPv4 or IPv6 address to lookup.
* @return an EnterpriseResponse for the requested IP address.
* @throws GeoIp2Exception if there is an error looking up the IP
* @throws IOException if there is an IO error
*/
@Override
public EnterpriseResponse enterprise(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
Optional r = getEnterprise(ipAddress);
if (r.isEmpty()) {
throw new AddressNotFoundException("The address "
+ ipAddress.getHostAddress() + " is not in the database.");
}
return r.get();
}
@Override
public Optional tryEnterprise(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return getEnterprise(ipAddress);
}
private Optional getEnterprise(
InetAddress ipAddress
) throws IOException, GeoIp2Exception {
LookupResult result = this.get(
ipAddress,
EnterpriseResponse.class,
DatabaseType.ENTERPRISE
);
EnterpriseResponse response = result.getModel();
if (response == null) {
return Optional.empty();
}
return Optional.of(
new EnterpriseResponse(
response,
result.getIpAddress(),
result.getNetwork(),
locales
)
);
}
/**
* Look up an IP address in a GeoIP2 ISP database.
*
* @param ipAddress IPv4 or IPv6 address to lookup.
* @return an IspResponse for the requested IP address.
* @throws GeoIp2Exception if there is an error looking up the IP
* @throws IOException if there is an IO error
*/
@Override
public IspResponse isp(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
Optional r = getIsp(ipAddress);
if (r.isEmpty()) {
throw new AddressNotFoundException("The address "
+ ipAddress.getHostAddress() + " is not in the database.");
}
return r.get();
}
@Override
public Optional tryIsp(InetAddress ipAddress) throws IOException,
GeoIp2Exception {
return getIsp(ipAddress);
}
private Optional getIsp(
InetAddress ipAddress
) throws IOException, GeoIp2Exception {
LookupResult result = this.get(
ipAddress,
IspResponse.class,
DatabaseType.ISP
);
IspResponse response = result.getModel();
if (response == null) {
return Optional.empty();
}
return Optional.of(
new IspResponse(
response,
result.getIpAddress(),
result.getNetwork()
)
);
}
/**
* @return the metadata for the open MaxMind DB file.
*/
public Metadata getMetadata() {
return this.reader.getMetadata();
}
}