package org.rakam.collection.mapper.geoip.maxmind.ip2location;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import io.airlift.log.Logger;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.cookie.Cookie;
import org.apache.avro.generic.GenericRecord;
import org.rakam.Mapper;
import org.rakam.collection.Event;
import org.rakam.collection.FieldDependencyBuilder;
import org.rakam.collection.FieldType;
import org.rakam.collection.SchemaField;
import org.rakam.plugin.EventMapper;
import org.rakam.plugin.SyncEventMapper;
import org.rakam.plugin.user.ISingleUserBatchOperation;
import org.rakam.plugin.user.UserPropertyMapper;
import org.rakam.util.MapProxyGenericRecord;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.stream.Collectors;
import static org.rakam.collection.FieldType.STRING;
import static org.rakam.collection.mapper.geoip.maxmind.ip2location.IP2LocationGeoIPModule.downloadOrGetFile;
@Mapper(name = "IP2Location Event mapper", description = "Looks up geolocation data from _ip field using IP2Location and attaches geo-related attributed")
public class IP2LocationGeoIPEventMapper
implements SyncEventMapper, UserPropertyMapper
{
private static final Logger LOGGER = Logger.get(IP2LocationGeoIPEventMapper.class);
private final static List<String> CITY_DATABASE_ATTRIBUTES = ImmutableList
.of("city", "region", "country_code", "latitude", "longitude");
private final IPReader lookup;
public IP2LocationGeoIPEventMapper(GeoIPModuleConfig config)
throws IOException
{
Preconditions.checkNotNull(config, "config is null");
lookup = getReader(config.getDatabaseUrl());
}
private IPReader getReader(String url)
{
try {
FileInputStream cityDatabase = new FileInputStream(downloadOrGetFile(url));
return IPReader.build(cityDatabase);
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
@Override
public List<Cookie> map(Event event, RequestParams extraProperties, InetAddress sourceAddress, HttpHeaders responseHeaders)
{
Object ip = event.properties().get("_ip");
InetAddress addr;
if ((ip instanceof String)) {
try {
// it may be slow because java performs reverse hostname lookup.
addr = Inet4Address.getByName((String) ip);
}
catch (UnknownHostException e) {
return null;
}
}
else if (Boolean.TRUE == ip) {
addr = sourceAddress;
}
else {
if (lookup != null) {
// Cloudflare country code header (Only works when the request passed through CF servers)
String countryCode = extraProperties.headers().get("HTTP_CF_IPCOUNTRY");
if (countryCode != null) {
event.properties().put("_country_code", countryCode);
}
}
return null;
}
setGeoFields(addr, event.properties());
return null;
}
@Override
public List<Cookie> map(String project, List<? extends ISingleUserBatchOperation> user, RequestParams requestParams, InetAddress sourceAddress)
{
for (ISingleUserBatchOperation data : user) {
if (data.getSetProperties() != null) {
mapInternal(project, data.getSetProperties(), sourceAddress);
}
if (data.getSetPropertiesOnce() != null) {
mapInternal(project, data.getSetPropertiesOnce(), sourceAddress);
}
}
return null;
}
public void mapInternal(String project, ObjectNode data, InetAddress sourceAddress)
{
Object ip = data.get("_ip");
if (ip == null) {
return;
}
if ((ip instanceof String)) {
try {
// it may be slow because java performs reverse hostname lookup.
sourceAddress = Inet4Address.getByName((String) ip);
}
catch (UnknownHostException e) {
return;
}
}
GenericRecord record = new MapProxyGenericRecord(data);
setGeoFields(sourceAddress, record);
}
@Override
public void addFieldDependency(FieldDependencyBuilder builder)
{
List<SchemaField> fields = CITY_DATABASE_ATTRIBUTES.stream()
.map(attr -> new SchemaField("_" + attr, getType(attr)))
.collect(Collectors.toList());
builder.addFields("_ip", fields);
}
private static FieldType getType(String attr)
{
switch (attr) {
case "country_code":
case "region":
case "city":
case "timezone":
return STRING;
case "latitude":
case "longitude":
return FieldType.DOUBLE;
default:
throw new IllegalStateException();
}
}
private void setGeoFields(InetAddress address, GenericRecord properties)
{
GeoLocation city = lookup.lookup(address);
properties.put("_country_code", city.country);
properties.put("_region", city.stateProv);
properties.put("_city", city.city);
properties.put("_latitude", city.coordination.latitude);
properties.put("_longitude", city.coordination.longitude);
}
}