autonomouslogic/dynamo-mapper

View on GitHub
src/main/java/com/autonomouslogic/dynamomapper/codec/DynamoEncoder.java

Summary

Maintainability
A
45 mins
Test Coverage
package com.autonomouslogic.dynamomapper.codec;

import com.autonomouslogic.dynamomapper.util.ReflectionUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.dynamodb.model.AttributeAction;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate;

@RequiredArgsConstructor
public class DynamoEncoder {
    @NonNull
    private final ObjectMapper objectMapper;

    @NonNull
    private final ReflectionUtil reflectionUtil;

    /**
     * Encodes a POJO into DynamoDB values.
     */
    public Map<String, AttributeValue> encode(@NonNull Object obj) throws IOException {
        var json = objectMapper.valueToTree(obj);
        if (json.isObject()) {
            return encodeObject(json);
        } else {
            throw new IllegalArgumentException(
                    String.format("Can only encode objects, %s supplied", json.getNodeType()));
        }
    }

    private Map<String, AttributeValue> encodeObject(@NonNull JsonNode node) throws IOException {
        var map = new HashMap<String, AttributeValue>();
        var fieldsIterator = node.fields();
        while (fieldsIterator.hasNext()) {
            var field = fieldsIterator.next();
            map.put(field.getKey(), encodeValue(field.getValue()));
        }
        return map;
    }

    public AttributeValue encodeValue(JsonNode node) throws IOException {
        if (node == null || node.isNull()) {
            return AttributeValue.builder().nul(true).build();
        }
        if (node.isBinary()) {
            return AttributeValue.builder()
                    .b(SdkBytes.fromByteArrayUnsafe(node.binaryValue()))
                    .build();
        }
        if (node.isBoolean()) {
            return AttributeValue.builder().bool(node.booleanValue()).build();
        }
        if (node.isTextual()) {
            return AttributeValue.builder().s(node.textValue()).build();
        }
        if (node.isNumber()) {
            return AttributeValue.builder().n(node.numberValue().toString()).build();
        }
        if (node.isObject()) {
            var obj = encodeObject(node);
            return AttributeValue.builder().m(obj).build();
        }
        if (node.isArray()) {
            return encodeList(node);
        }
        throw new IllegalArgumentException(String.format("Unsupported node type: %s", node.getNodeType()));
    }

    private AttributeValue encodeList(@NonNull JsonNode list) throws IOException {
        var values = new ArrayList<AttributeValue>();
        for (var entry : list) {
            values.add(encodeValue(entry));
        }
        return AttributeValue.builder().l(values).build();
    }

    public <T> Map<String, AttributeValue> encodeKeyValue(@NonNull T keyObject) throws IOException {
        var encoded = encode(keyObject);
        var primaryKeys = reflectionUtil.resolvePrimaryKeyFields(keyObject.getClass());
        var keyValues = new HashMap<String, AttributeValue>();
        for (String field : primaryKeys) {
            keyValues.put(field, encoded.get(field));
        }
        return keyValues;
    }

    public <T> Map<String, AttributeValue> encodeKeyValue(@NonNull Object primaryKey, @NonNull Class<T> clazz)
            throws IOException {
        var primaryKeys = reflectionUtil.resolvePrimaryKeyFields(clazz);
        if (primaryKeys.isEmpty()) {
            throw new IllegalArgumentException(String.format("No primary key defined on %s", clazz.getSimpleName()));
        }
        if (primaryKeys.size() > 1) {
            throw new IllegalArgumentException(
                    String.format("Multiple primary keys defined on %s", clazz.getSimpleName()));
        }
        var json = objectMapper.valueToTree(primaryKey);
        var primaryKeyValue = encodeValue(json);
        return Map.of(primaryKeys.get(0), primaryKeyValue);
    }

    public Map<String, AttributeValueUpdate> encodeUpdates(@NonNull Object obj) throws IOException {
        var encoded = encode(obj);
        var key = encodeKeyValue(obj);
        for (String k : key.keySet()) {
            encoded.remove(k);
        }
        var updates = new HashMap<String, AttributeValueUpdate>();
        encoded.forEach((k, val) -> updates.put(
                k,
                AttributeValueUpdate.builder()
                        .value(val)
                        .action(AttributeAction.PUT)
                        .build()));
        return updates;
    }
}