This is a plugin to use MaxMind GeoIP2 Database to find out about geolocation of some particular IP address without constant calls to remote web services. This plugin supports:
- Async concurrent interactions with DatabaseReader under the hood
- Scheduled renewals of MaxMind database
- Handles ETag and Last-Modified headers at cache to minimize impact onto outer service (where DB file is stored).
Plugin is built on top of Akka and is a set of actors where you can send message to ad get a response. It uses Akka Streams and Play! standalone ws to download and process DB file.
The first step is include the the dependency in your build.sbt
file:
scalaVersion := "2.12.4"
libraryDependencies += "com.github.sovaalexandr" %% "maxmind-geoip2-async-guice" % "1.0.0"
Module will be added automatically.
This plugins offers the following configurations:
geolocation {
geoip2db {
instances = ${akka.actor.deployment."/geolocation-supervisor/geolocation".nr-of-instances} // How much IPs do you want to locate at a time
dbUrl = "http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz" // URL where to go for new DB file.
dbFile = ${java.io.tmpdir}"/GeoLite2-City.mmdb" // Place where to store DB Files. Directory must be writeable.
download {
dispatcherName = akka.actor.default-dispatcher // Dispatcher which will execute all actions related to file download.
headers {
validFor: Inf // Plugin also stores ETag and Last-Modified headers at cache. This setting is FiniteDuration of that validity
}
}
compression {
type = GZIP // Usually GeoLite DB files are GZip-compressed. This setting'll decompress a file downstream.
// Accepted options: GZIP, DEFLATE, NONE
chunkSize = 64kB //${akka.stream.scaladsl.Compression.MaxBytesPerChunkDefault} - size of chunk to decompress. Set to AkkaStreams default.
}
databaseFileProviderPersistenceId = "maxmind.geoip2db.database" // PersistenceId for DatabaseFileProvider actor.
}
}
More in detail here: reference.conf
You can see the full Playframework integration example at samples dir. This is an example of Play Scala controller injected with Guice:
class GeoScala @Inject()(cc: ControllerComponents, @Named("geolocation") geolocation: ActorRef, geoTemplate: Geo, errorTemplate: Error)(implicit ec: ExecutionContext) extends AbstractController(cc) {
def getGeolocation(address: String): Action[AnyContent] = Action.async {
implicit val timeout: Timeout = Timeout(5.seconds)
Try(InetAddress.getByName(address)) match {
case Success(inet) => (geolocation ? City(inet)).map({
case r: CityResponse => Ok(geoTemplate(address, r, "Got this one:"))
case AddressNotFound => NotFound(errorTemplate.render("MaxMind don't know where is it"))
case Idle => PreconditionFailed(errorTemplate.render("Geolocation is warming-up. Maybe after restart or there is a file renewal. Try a bit later."))
}).recover({case e:Throwable => BadRequest(errorTemplate.render(e.getMessage))})
case Failure(e) => Futures.successful(BadRequest(errorTemplate.render(e.getMessage)))
}
}
}
And similar example of Play Java controller injected with Guice:
public class GeoJava extends Controller
{
private final ActorRef geolocation;
private final views.html.Geo geoTemplate;
private final views.html.Error errorTemplate;
@Inject
public GeoJava(@Named("geolocation") ActorRef geolocation, views.html.Geo geoTemplate, views.html.Error errorTemplate)
{
this.geolocation = geolocation;
this.geoTemplate = geoTemplate;
this.errorTemplate = errorTemplate;
}
public CompletionStage<Result> getGeolocation(String address) {
try {
return ask(geolocation, City.apply(InetAddress.getByName(address)), 5000L)
.thenApply(location -> toResult(location, address))
.exceptionally(e -> badRequest(errorTemplate.render(e.getMessage())));
} catch (UnknownHostException e) {
return CompletableFuture.completedFuture(badRequest(errorTemplate.render(e.getMessage())));
}
}
// ask returns Future[Object] so use this workaround to restrict a return type.
private Result toResult(Object response, String address) {
if (response instanceof CityResponse) {
return ok(geoTemplate.render(address, (CityResponse)response, "Got this one:"));
} if (response instanceof AddressNotFound$) {
return notFound(errorTemplate.render("MaxMind don't know where is it"));
} if (response instanceof Idle$) {
return internalServerError(errorTemplate.render("Geolocation is warming-up. Maybe after restart or there is a file renewal. Try a bit later."));
}
return internalServerError(errorTemplate.render("Something wierd have returned: "+response.getClass().toString()));
}
}
Feedback, suggestions, bug reports, code quality reviews and contributions are extremely appreciated. See details at CONTRIBUTING.