package org.embulk.config; import java.lang.reflect.Proxy; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableMap; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.deser.Deserializers; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.JsonMappingException; class TaskSerDe { public static class TaskSerializer extends JsonSerializer<Task> { private final ObjectMapper nestedObjectMapper; public TaskSerializer(ObjectMapper nestedObjectMapper) { this.nestedObjectMapper = nestedObjectMapper; } @Override public void serialize(Task value, JsonGenerator jgen, SerializerProvider provider) throws IOException { if (value instanceof Proxy) { Object handler = Proxy.getInvocationHandler(value); if (handler instanceof TaskInvocationHandler) { TaskInvocationHandler h = (TaskInvocationHandler) handler; Map<String, Object> objects = h.getObjects(); jgen.writeStartObject(); for (Map.Entry<String, Object> pair : objects.entrySet()) { if (h.getInjectedFields().contains(pair.getKey())) { continue; } jgen.writeFieldName(pair.getKey()); nestedObjectMapper.writeValue(jgen, pair.getValue()); } jgen.writeEndObject(); return; } } // TODO exception class & message throw new UnsupportedOperationException("Serializing Task is not supported"); } } public static class TaskDeserializer <T> extends JsonDeserializer<T> { private final ObjectMapper nestedObjectMapper; private final ModelManager model; private final Class<?> iface; private final Map<String, FieldEntry> mappings; private final List<InjectEntry> injects; public TaskDeserializer(ObjectMapper nestedObjectMapper, ModelManager model, Class<T> iface) { this.nestedObjectMapper = nestedObjectMapper; this.model = model; this.iface = iface; this.mappings = getterMappings(iface); this.injects = injectEntries(iface); } protected Map<String, FieldEntry> getterMappings(Class<?> iface) { ImmutableMap.Builder<String, FieldEntry> builder = ImmutableMap.builder(); for (Map.Entry<String, Method> getter : TaskInvocationHandler.fieldGetters(iface).entrySet()) { Method getterMethod = getter.getValue(); String fieldName = getter.getKey(); if (getterMethod.getAnnotation(ConfigInject.class) != null) { // InjectEntry continue; } Type fieldType = getterMethod.getGenericReturnType(); Optional<String> jsonKey = getJsonKey(getterMethod, fieldName); if (!jsonKey.isPresent()) { // skip this field continue; } Optional<String> defaultJsonString = getDefaultJsonString(getterMethod); builder.put(jsonKey.get(), new FieldEntry(fieldName, fieldType, defaultJsonString)); } return builder.build(); } protected List<InjectEntry> injectEntries(Class<?> iface) { ImmutableList.Builder<InjectEntry> builder = ImmutableList.builder(); for (Map.Entry<String, Method> getter : TaskInvocationHandler.fieldGetters(iface).entrySet()) { Method getterMethod = getter.getValue(); String fieldName = getter.getKey(); ConfigInject inject = getterMethod.getAnnotation(ConfigInject.class); if (inject != null) { // InjectEntry builder.add(new InjectEntry(fieldName, getterMethod.getReturnType())); } } return builder.build(); } protected Optional<String> getJsonKey(Method getterMethod, String fieldName) { return Optional.of(fieldName); } protected Optional<String> getDefaultJsonString(Method getterMethod) { return Optional.absent(); } @Override @SuppressWarnings("unchecked") public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { Map<String, Object> objects = new ConcurrentHashMap<String, Object>(); HashMap<String, FieldEntry> unusedMappings = new HashMap<>(mappings); String key; JsonToken current = jp.getCurrentToken(); if (current == JsonToken.START_OBJECT) { current = jp.nextToken(); key = jp.getCurrentName(); } else { key = jp.nextFieldName(); } for (; key != null; key = jp.nextFieldName()) { JsonToken t = jp.nextToken(); // to get to value FieldEntry field = mappings.get(key); if (field == null) { jp.skipChildren(); } else { Object value = nestedObjectMapper.readValue(jp, new GenericTypeReference(field.getType())); if (value == null) { throw new JsonMappingException("Setting null to a task field is not allowed. Use Optional<T> (com.google.common.base.Optional) to represent null."); } objects.put(field.getName(), value); unusedMappings.remove(key); } } // set default values for (Map.Entry<String, FieldEntry> unused : unusedMappings.entrySet()) { FieldEntry field = unused.getValue(); if (field.getDefaultJsonString().isPresent()) { Object value = nestedObjectMapper.readValue(field.getDefaultJsonString().get(), new GenericTypeReference(field.getType())); if (value == null) { throw new JsonMappingException("Setting null to a task field is not allowed. Use Optional<T> (com.google.common.base.Optional) to represent null."); } objects.put(field.getName(), value); } else { // required field throw new JsonMappingException("Field '"+unused.getKey()+"' is required but not set", jp.getCurrentLocation()); } } // inject ImmutableSet.Builder<String> injectedFields = ImmutableSet.builder(); for (InjectEntry inject : injects) { objects.put(inject.getName(), model.getInjectedInstance(inject.getType())); injectedFields.add(inject.getName()); } return (T) Proxy.newProxyInstance( iface.getClassLoader(), new Class<?>[] { iface }, new TaskInvocationHandler(model, iface, objects, injectedFields.build())); } private static class FieldEntry { private final String name; private final Type type; private final Optional<String> defaultJsonString; public FieldEntry(String name, Type type, Optional<String> defaultJsonString) { this.name = name; this.type = type; this.defaultJsonString = defaultJsonString; } public String getName() { return name; } public Type getType() { return type; } public Optional<String> getDefaultJsonString() { return defaultJsonString; } } private static class InjectEntry { private final String name; private Class<?> type; public InjectEntry(String name, Class<?> type) { this.name = name; this.type = type; } public String getName() { return name; } public Class<?> getType() { return type; } } } public static class TaskSerializerModule extends SimpleModule { public TaskSerializerModule(ObjectMapper nestedObjectMapper) { super(); addSerializer(Task.class, new TaskSerializer(nestedObjectMapper)); } } public static class ConfigTaskDeserializer <T> extends TaskDeserializer<T> { public ConfigTaskDeserializer(ObjectMapper nestedObjectMapper, ModelManager model, Class<T> iface) { super(nestedObjectMapper, model, iface); } @Override protected Optional<String> getJsonKey(Method getterMethod, String fieldName) { Config a = getterMethod.getAnnotation(Config.class); if (a != null) { return Optional.of(a.value()); } else { return Optional.absent(); // skip this field } } @Override public Optional<String> getDefaultJsonString(Method getterMethod) { ConfigDefault a = getterMethod.getAnnotation(ConfigDefault.class); if (a != null && !a.value().isEmpty()) { return Optional.of(a.value()); } return super.getDefaultJsonString(getterMethod); } } public static class TaskDeserializerModule extends Module // can't use just SimpleModule, due to generic types { protected final ObjectMapper nestedObjectMapper; protected final ModelManager model; public TaskDeserializerModule(ObjectMapper nestedObjectMapper, ModelManager model) { this.nestedObjectMapper = nestedObjectMapper; this.model = model; } @Override public String getModuleName() { return "embulk.config.TaskSerDe"; } @Override public Version version() { return Version.unknownVersion(); } @Override public void setupModule(SetupContext context) { context.addDeserializers(new Deserializers.Base() { @Override public JsonDeserializer<?> findBeanDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException { Class<?> raw = type.getRawClass(); if (Task.class.isAssignableFrom(raw)) { return newTaskDeserializer(raw); } return super.findBeanDeserializer(type, config, beanDesc); } }); } @SuppressWarnings("unchecked") protected JsonDeserializer<?> newTaskDeserializer(Class<?> raw) { return new TaskDeserializer(nestedObjectMapper, model, raw); } } public static class ConfigTaskDeserializerModule extends TaskDeserializerModule { public ConfigTaskDeserializerModule(ObjectMapper nestedObjectMapper, ModelManager model) { super(nestedObjectMapper, model); } @Override public String getModuleName() { return "embulk.config.ConfigTaskSerDe"; } @Override @SuppressWarnings("unchecked") protected JsonDeserializer<?> newTaskDeserializer(Class<?> raw) { return new ConfigTaskDeserializer(nestedObjectMapper, model, raw); } } }