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

io.dataspray.singletable.ExpressionBuilder Maven / Gradle / Ivy

Go to download

DynamoDB best practices encourages a single-table design that allows multiple record types to reside within the same table. The goal of this library is to improve the experience of Java developers and make it safer to define non-conflicting schema of each record, serializing and deserializing automatically and working with secondary indexes.

The newest version!
// SPDX-FileCopyrightText: 2019-2022 Matus Faro 
// SPDX-License-Identifier: Apache-2.0
package io.dataspray.singletable;

import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.*;
import io.dataspray.singletable.builder.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;

@Slf4j
@RequiredArgsConstructor
public abstract class ExpressionBuilder implements Mappings, UpdateExpressionBuilder,
        ConditionExpressionBuilder

, FilterExpressionBuilder

{ protected final Schema schema; private final Map nameMap = Maps.newHashMap(); private final Map valMap = Maps.newHashMap(); private final Map setUpdates = Maps.newHashMap(); private final Map removeUpdates = Maps.newHashMap(); private final Map addUpdates = Maps.newHashMap(); private final Map deleteUpdates = Maps.newHashMap(); private final List conditionExpressions = Lists.newArrayList(); private final List> builderAdjustments = Lists.newArrayList(); abstract protected P getParent(); public P builder(Consumer builder) { builderAdjustments.add(builder); return getParent(); } @Override public P conditionExpression(String expression) { conditionExpressions.add(expression); return getParent(); } @Override public P conditionExpression(MappingExpression mappingExpression) { conditionExpression(mappingExpression.getExpression(this)); return getParent(); } @Override public P filterExpression(String expression) { conditionExpression(expression); return getParent(); } @Override public P filterExpression(MappingExpression mappingExpression) { conditionExpression(mappingExpression); return getParent(); } @Override public P conditionExists() { conditionExpressions.add("attribute_exists(" + fieldMapping(schema.partitionKeyName()) + ")"); return getParent(); } @Override public P conditionNotExists() { conditionExpressions.add("attribute_not_exists(" + fieldMapping(schema.partitionKeyName()) + ")"); return getParent(); } @Override public P conditionFieldEquals(String fieldName, Object objectOther) { conditionExpressions.add(fieldMapping(fieldName) + " = " + valueMapping(fieldName, objectOther)); return getParent(); } @Override public P conditionFieldNotEquals(String fieldName, Object objectOther) { conditionExpressions.add(fieldMapping(fieldName) + " <> " + valueMapping(fieldName, objectOther)); return getParent(); } @Override public P conditionFieldExists(String fieldName) { conditionExpressions.add("attribute_exists(" + fieldMapping(fieldName) + ")"); return getParent(); } @Override public P conditionFieldNotExists(String fieldName) { conditionExpressions.add("attribute_not_exists(" + fieldMapping(fieldName) + ")"); return getParent(); } @Override public P updateExpression(String expression) { setUpdates.put(expression, expression); return getParent(); } @Override public P updateExpression(MappingExpression mappingExpression) { updateExpression(mappingExpression.getExpression(this)); return getParent(); } @Override public P upsert(T item) { upsert(item, ImmutableSet.of()); return getParent(); } @Override public P upsert(T item, ImmutableSet skipFieldNames) { schema.toAttrMap(item).forEach((key, value) -> { if (schema.partitionKeyName().equals(key) || schema.rangeKeyName().equals(key)) { return; } if (skipFieldNames.contains(key)) { return; } set(key, value); }); return getParent(); } @Override public P set(String fieldName, Object object) { checkState(!setUpdates.containsKey(fieldName)); setUpdates.put(fieldName, fieldMapping(fieldName) + " = " + valueMapping(fieldName, object)); return getParent(); } @Override public P set(ImmutableList fieldPath, Object object) { checkArgument(!fieldPath.isEmpty()); String fieldMapping = fieldMapping(fieldPath); checkState(!addUpdates.containsKey(fieldMapping)); AttributeValue value = schema.toAttrValue(object); setUpdates.put(fieldMapping, fieldMapping + " = " + constantMapping(fieldPath, value)); return getParent(); } @Override public P setIncrement(String fieldName, Number increment) { checkState(!setUpdates.containsKey(fieldName)); setUpdates.put(fieldName, String.format("%s = if_not_exists(%s, %s) + %s", fieldMapping(fieldName), fieldMapping(fieldName), constantMapping("zero", AttributeValue.fromN("0")), valueMapping(fieldName, increment))); return getParent(); } @Override public P add(String fieldName, Object object) { checkState(!addUpdates.containsKey(fieldName)); addUpdates.put(fieldName, fieldMapping(fieldName) + " " + valueMapping(fieldName, object)); return getParent(); } @Override public P add(ImmutableList fieldPath, Object object) { checkArgument(!fieldPath.isEmpty()); String fieldMapping = fieldMapping(fieldPath); checkState(!addUpdates.containsKey(fieldMapping)); AttributeValue value = schema.toAttrValue(object); addUpdates.put(fieldMapping, fieldMapping + " " + constantMapping(fieldPath, value)); return getParent(); } @Override public P remove(String fieldName) { checkState(!removeUpdates.containsKey(fieldName)); removeUpdates.put(fieldName, fieldMapping(fieldName)); return getParent(); } @Override public P remove(ImmutableList fieldPath) { checkArgument(!fieldPath.isEmpty()); String fieldMapping = fieldMapping(fieldPath); checkState(!addUpdates.containsKey(fieldMapping)); removeUpdates.put(fieldMapping, fieldMapping); return getParent(); } @Override public P delete(String fieldName, Object object) { checkState(!deleteUpdates.containsKey(fieldName)); deleteUpdates.put(fieldName, fieldMapping(fieldName) + " " + valueMapping(fieldName, object)); return getParent(); } @Override public String fieldMapping(String fieldName) { return fieldMapping(fieldName, fieldName); } @Override public String fieldMapping(ImmutableList fieldPath) { return fieldPath.stream() .map(this::fieldMapping) .collect(Collectors.joining(".")); } @Override public String fieldMapping(String fieldName, String fieldValue) { String mappedName = "#" + sanitizeFieldMapping(fieldName); while (nameMap.containsKey(mappedName) && !nameMap.get(mappedName).equals(fieldName)) { mappedName = mappedName + "_"; } nameMap.put(mappedName, fieldValue); return mappedName; } @Override public String valueMapping(String fieldName, Object object) { return constantMapping(fieldName, object); } @Override public String constantMapping(String name, Object object) { String mappedName = ":" + sanitizeFieldMapping(name); AttributeValue value = schema.toAttrValue(object); while (valMap.containsKey(mappedName) && !valMap.get(mappedName).equals(value)) { mappedName = mappedName + "_"; } valMap.put(mappedName, value); return mappedName; } @Override public String constantMapping(ImmutableList namePath, Object value) { return constantMapping(namePath.stream() .map(String::toLowerCase) .collect(Collectors.joining("X")), value); } protected Expression buildExpression() { ArrayList updates = Lists.newArrayList(); if (!setUpdates.isEmpty()) { updates.add("SET " + String.join(", ", setUpdates.values())); } if (!addUpdates.isEmpty()) { updates.add("ADD " + String.join(", ", addUpdates.values())); } if (!removeUpdates.isEmpty()) { updates.add("REMOVE " + String.join(", ", removeUpdates.values())); } if (!deleteUpdates.isEmpty()) { updates.add("DELETE " + String.join(", ", deleteUpdates.values())); } final Optional updateOpt = Optional.ofNullable(Strings.emptyToNull(String.join(" ", updates))); final Optional conditionOpt = Optional.ofNullable(Strings.emptyToNull(String.join(" AND ", conditionExpressions))); final Optional> nameImmutableMapOpt = nameMap.isEmpty() ? Optional.empty() : Optional.of(ImmutableMap.copyOf(nameMap)); final Optional> valImmutableMapOpt = valMap.isEmpty() ? Optional.empty() : Optional.of(ImmutableMap.copyOf(valMap)); final ImmutableList> immutableBuilderAdjustments = ImmutableList.copyOf(builderAdjustments); log.trace("Built dynamo expression: update {} condition {} nameMap {} valKeys {}", updateOpt, conditionOpt, nameImmutableMapOpt, valImmutableMapOpt.map(ImmutableMap::keySet)); return new Expression() { @Override public Optional updateExpression() { return updateOpt; } @Override public Optional conditionExpression() { return conditionOpt; } @Override public Optional filterExpression() { return conditionOpt; } @Override public Optional> expressionAttributeNames() { return nameImmutableMapOpt; } @Override public Optional> expressionAttributeValues() { return valImmutableMapOpt; } @Override public ImmutableList> builderAdjustments() { return immutableBuilderAdjustments; } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("updateExpression", this.updateExpression()) .add("conditionExpression", this.conditionExpression()) .add("nameMap", this.expressionAttributeNames()) .add("valMap", this.expressionAttributeValues()) .toString(); } }; } private String sanitizeFieldMapping(String fieldName) { return fieldName.replaceAll("(^[^a-z])|[^a-zA-Z0-9]", "x"); } @Override public String toString() { return buildExpression().toString(); } }