package org.rakam; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Ints; import org.apache.avro.Schema; import org.apache.avro.generic.GenericData; import org.rakam.collection.Event; import org.rakam.collection.FieldType; import org.rakam.collection.SchemaField; import org.rakam.analysis.metadata.Metastore; import org.rakam.util.AvroUtil; import java.time.Instant; import java.time.LocalDate; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import static java.lang.String.format; import static org.rakam.collection.FieldType.*; public class EventBuilder { private final Metastore metastore; private final String project; private final Map<String, List<SchemaField>> fieldCache = new ConcurrentHashMap<>(); public EventBuilder(String project, Metastore metastore) { this.project = project; this.metastore = metastore; } public Event createEvent(String collection, Map<String, Object> properties) { List<SchemaField> cache = fieldCache.get(collection); List<SchemaField> fields; List<SchemaField> generatedSchema = generateSchema(properties); if (cache == null || !generatedSchema.stream().allMatch(f -> cache.contains(f))) { fields = metastore.getOrCreateCollectionFieldList(project, collection, ImmutableSet.copyOf(generatedSchema)); fieldCache.put(collection, fields); } else { fields = cache; } try { GenericData.Record record = new GenericData.Record(AvroUtil.convertAvroSchema(fields)); properties.forEach((key, value) -> record.put(key, cast(value, record.getSchema().getField(key).schema().getTypes().get(1).getType()))); return new Event(project, collection, Event.EventContext.empty(), fields, record); } catch (Exception e) { throw Throwables.propagate(e); } } public void cleanCache() { fieldCache.clear(); } private Object cast(Object value, Schema.Type type) { if(value instanceof Instant) { if(type != Schema.Type.LONG) { throw new IllegalStateException(); } return ((Instant) value).toEpochMilli(); } if(value instanceof LocalDate) { if(type != Schema.Type.INT) { throw new IllegalStateException(); } return Ints.checkedCast(((LocalDate) value).toEpochDay()); } if(type == Schema.Type.DOUBLE) { return ((Number) value).doubleValue(); } if(type == Schema.Type.INT) { return ((Number) value).intValue(); } if(type == Schema.Type.LONG) { return ((Number) value).longValue(); } if(type == Schema.Type.STRING) { return value.toString(); } return value; } private List<SchemaField> generateSchema(Map<String, Object> properties) { return properties.entrySet().stream() .map(entry -> new SchemaField(entry.getKey(), getType(entry.getValue()))) .collect(Collectors.toList()); } public static FieldType getType(Object value) { if(value instanceof String) { return STRING; } if(value instanceof Long) { return LONG; } if(value instanceof Integer) { return INTEGER; } if(value instanceof Double) { return DOUBLE; } if(value instanceof Boolean) { return BOOLEAN; } if(value instanceof Map) { Iterator<Map.Entry> iterator = ((Map) value).entrySet().iterator(); if(!iterator.hasNext()) { throw new UnsupportedOperationException("empty map"); } Map.Entry next = iterator.next(); if(!(next.getKey() instanceof String)) { throw new UnsupportedOperationException("the key of map value must be string"); } return getType(next.getValue()).convertToMapValueType(); } if(value instanceof List) { Iterator iterator = ((List) value).iterator(); if(!iterator.hasNext()) { throw new UnsupportedOperationException("empty map"); } return getType(iterator.next()).convertToArrayType(); } if(value instanceof Instant) { return TIMESTAMP; } if(value instanceof LocalDate) { return DATE; } throw new IllegalArgumentException(format("Undefined type: %s", value.getClass())); } }