package org.rakam.module.website;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericRecord;
import org.rakam.Mapper;
import org.rakam.analysis.ConfigManager;
import org.rakam.analysis.InternalConfig;
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.AvroUtil;
import javax.inject.Inject;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.List;
import java.util.UUID;
import static org.rakam.collection.FieldType.STRING;
@Mapper(name = "User Id Event mapper", description = "")
public class UserIdEventMapper
implements SyncEventMapper, UserPropertyMapper
{
private final LoadingCache<String, FieldType> userTypeCache;
DistributedIdGenerator idGenerator;
@Inject
public UserIdEventMapper(ConfigManager configManager)
{
idGenerator = new DistributedIdGenerator();
userTypeCache = CacheBuilder.newBuilder().build(new CacheLoader<String, FieldType>()
{
@Override
public FieldType load(String key)
throws Exception
{
return configManager.setConfigOnce(key, InternalConfig.USER_TYPE.name(), STRING);
}
});
}
@Override
public List<Cookie> map(Event event, RequestParams requestParams, InetAddress sourceAddress, HttpHeaders responseHeaders)
{
GenericRecord properties = event.properties();
if (properties.get("_user") == null) {
Schema.Field user = event.properties().getSchema().getField("_user");
if (user == null) {
return null;
}
Schema.Type type = user.schema().getTypes().get(1).getType();
Object anonymousUser = requestParams.cookies().stream()
.filter(e -> e.name().equals("_anonymous_user")).findAny()
.map(e -> cast(type, e.value())).orElse(generate(type));
properties.put("_user", anonymousUser);
DefaultCookie cookie = new DefaultCookie("_anonymous_user", String.valueOf(anonymousUser));
cookie.setPath("/");
return ImmutableList.of(cookie);
}
return null;
}
private Object generate(Schema.Type type)
{
switch (type) {
case STRING:
return UUID.randomUUID().toString();
case LONG:
return idGenerator.generateId();
case INT:
return (int) idGenerator.generateId();
default:
return null;
}
}
private Object cast(Schema.Type type, String value)
{
switch (type) {
case STRING:
return value;
case LONG:
try {
return Long.parseLong(value);
}
catch (NumberFormatException e) {
return null;
}
case INT:
try {
return Integer.parseInt(value);
}
catch (NumberFormatException e) {
return null;
}
default:
return null;
}
}
@Override
public List<Cookie> map(String project, List<? extends ISingleUserBatchOperation> user, RequestParams requestParams, InetAddress sourceAddress)
{
// if (user.id == null) {
// FieldType fieldType = userTypeCache.getUnchecked(project);
// Schema field = AvroUtil.generateAvroSchema(fieldType);
// Schema.Type type = field.getTypes().get(1).getType();
// Object anonymousUser = requestParams.cookies().stream()
// .filter(e -> e.name().equals("_anonymous_user")).findAny()
// .map(e -> cast(type, e.value())).orElse(generate(type));
//
// user.setId(anonymousUser);
// return ImmutableList.of(new DefaultCookie("_anonymous_user", String.valueOf(anonymousUser)));
// }
//
return null;
}
/**
* This class implements an ID generator.
* <p>
* The ID is a signed 64 bit long composed of:
* <p>
* sign - 1 bit
* timestamp - 41 bits (millisecond precision with a custom epoch allowing for 69 years)
* host id - 10 bits (allowing for 1024 hosts)
* sequence - 12 bits (allowing for 4096 IDs per millisecond)
* <p>
* There is a check that catches sequence rollover within the current millisecond.
*
* @author Maxim Khodanovich
*/
public static class DistributedIdGenerator
{
private static final long START_EPOCH = 1464307172048L;
private static final long SEQUENCE_BITS = 12L;
private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);
private static final long HOST_ID_BITS = 10L;
private static final long HOST_ID_MAX = -1L ^ (-1L << HOST_ID_BITS);
private static final long HOST_ID_SHIFT = SEQUENCE_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + HOST_ID_BITS;
private final long hostId;
private volatile long lastTimestamp = -1L;
private volatile long sequence = 0L;
public DistributedIdGenerator()
{
hostId = getHostId();
if (hostId < 0 || hostId > HOST_ID_MAX) {
throw new IllegalStateException("Invalid host ID: " + hostId);
}
}
public long generateId()
throws IllegalStateException
{
long timestamp = System.currentTimeMillis();
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
}
else {
sequence = 0;
}
lastTimestamp = timestamp;
return ((timestamp - START_EPOCH) << TIMESTAMP_SHIFT) | (hostId << HOST_ID_SHIFT) | sequence;
}
private long nextTimestamp(long lastTimestamp)
{
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
private long getHostId()
throws IllegalStateException
{
try {
NetworkInterface iface = NetworkInterface.getByInetAddress(getHostAddress());
byte[] mac = iface.getHardwareAddress();
return ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
}
catch (IOException e) {
throw new IllegalStateException("Failed to get host ID", e);
}
}
private InetAddress getHostAddress()
throws IOException
{
InetAddress address = null;
// Iterate all the network interfaces
for (Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements(); ) {
NetworkInterface iface = ifaces.nextElement();
// Iterate all the addresses assigned to the network interface
for (Enumeration<InetAddress> addrs = iface.getInetAddresses(); addrs.hasMoreElements(); ) {
InetAddress addr = addrs.nextElement();
if (!addr.isLoopbackAddress()) {
if (addr.isSiteLocalAddress()) {
// Found a non-loopback site-local address
return addr;
}
if (address == null) {
// Found the first non-loopback, non-site-local address
address = addr;
}
}
}
}
if (address != null) {
// Return the first non-loopback, non-site-local address
return address;
}
// Return the local host address (may be the loopback address)
return InetAddress.getLocalHost();
}
}
}