All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.embulk.config.TaskSerDe Maven / Gradle / Ivy

package org.embulk.config;

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.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.Deserializers;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

class TaskSerDe {
    public static class TaskSerializer extends JsonSerializer {
        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 objects = h.getObjects();
                    jgen.writeStartObject();
                    for (Map.Entry 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 extends JsonDeserializer {
        private final ObjectMapper nestedObjectMapper;

        @Deprecated  // https://github.com/embulk/embulk/issues/1304
        private final ModelManager model;

        private final Class iface;
        private final Multimap mappings;
        private final List injects;

        @SuppressWarnings("deprecation")  // https://github.com/embulk/embulk/issues/1304
        public TaskDeserializer(ObjectMapper nestedObjectMapper, ModelManager model, Class iface) {
            this.nestedObjectMapper = nestedObjectMapper;
            this.model = model;
            this.iface = iface;
            this.mappings = getterMappings(iface);
            this.injects = injectEntries(iface);
        }

        protected Multimap getterMappings(Class iface) {
            ImmutableMultimap.Builder builder = ImmutableMultimap.builder();
            for (Map.Entry getter : TaskInvocationHandler.fieldGetters(iface).entries()) {
                Method getterMethod = getter.getValue();
                String fieldName = getter.getKey();

                if (getterMethod.getAnnotation(ConfigInject.class) != null) {
                    // InjectEntry
                    continue;
                }

                Type fieldType = getterMethod.getGenericReturnType();

                final Optional jsonKey = getJsonKey(getterMethod, fieldName);
                if (!jsonKey.isPresent()) {
                    // skip this field
                    continue;
                }
                final Optional defaultJsonString = getDefaultJsonString(getterMethod);
                builder.put(jsonKey.get(), new FieldEntry(fieldName, fieldType, defaultJsonString));
            }
            return builder.build();
        }

        protected List injectEntries(Class iface) {
            ImmutableList.Builder builder = ImmutableList.builder();
            for (Map.Entry getter : TaskInvocationHandler.fieldGetters(iface).entries()) {
                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 getJsonKey(final Method getterMethod, final String fieldName) {
            return Optional.of(fieldName);
        }

        protected Optional getDefaultJsonString(final Method getterMethod) {
            return Optional.empty();
        }

        @Override
        @SuppressWarnings("unchecked")
        public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
            Map objects = new ConcurrentHashMap();
            HashMultimap unusedMappings = HashMultimap.create(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
                final Collection fields = mappings.get(key);
                if (fields.isEmpty()) {
                    jp.skipChildren();
                } else {
                    final JsonNode children = nestedObjectMapper.readValue(jp, JsonNode.class);
                    for (final FieldEntry field : fields) {
                        final Object value = nestedObjectMapper.convertValue(children, new GenericTypeReference(field.getType()));
                        if (value == null) {
                            throw new JsonMappingException("Setting null to a task field is not allowed. Use Optional to represent null.");
                        }
                        objects.put(field.getName(), value);
                        if (!unusedMappings.remove(key, field)) {
                            throw new JsonMappingException(String.format(
                                    "FATAL: Expected to be a bug in Embulk. Mapping \"%s: (%s) %s\" might have already been processed, or not in %s.",
                                    key,
                                    field.getType().toString(),
                                    field.getName(),
                                    this.iface.toString()));
                        }
                    }
                }
            }

            // set default values
            for (Map.Entry unused : unusedMappings.entries()) {
                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 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 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 defaultJsonString;

            public FieldEntry(String name, Type type, Optional defaultJsonString) {
                this.name = name;
                this.type = type;
                this.defaultJsonString = defaultJsonString;
            }

            public String getName() {
                return name;
            }

            public Type getType() {
                return type;
            }

            public Optional 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 extends TaskDeserializer {
        @SuppressWarnings("deprecation")  // https://github.com/embulk/embulk/issues/1304
        public ConfigTaskDeserializer(ObjectMapper nestedObjectMapper, ModelManager model, Class iface) {
            super(nestedObjectMapper, model, iface);
        }

        @Override
        protected Optional getJsonKey(final Method getterMethod, final String fieldName) {
            final Config a = getterMethod.getAnnotation(Config.class);
            if (a != null) {
                return Optional.of(a.value());
            } else {
                return Optional.empty();  // skip this field
            }
        }

        @Override
        protected Optional getDefaultJsonString(final Method getterMethod) {
            final 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;

        @Deprecated  // https://github.com/embulk/embulk/issues/1304
        protected final ModelManager model;

        @SuppressWarnings("deprecation")  // https://github.com/embulk/embulk/issues/1304
        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 {
        @SuppressWarnings("deprecation")  // https://github.com/embulk/embulk/issues/1304
        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);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy