AutoGeneratedKeyHelper.java
/*
* Copyright © 2018 spring-data-dynamodb (https://github.com/prasanna0586/spring-data-dynamodb)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.socialsignin.spring.data.dynamodb.core;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.UUID;
/**
* Helper class for auto-generating key values for entities in SDK_V1_COMPATIBLE mode.
*
* <p>This class provides backward compatibility support for SDK v1's auto-generation annotations:
* <ul>
* <li>{@code @DynamoDBAutoGeneratedKey} - Generates UUID for String fields</li>
* <li>{@code @DynamoDBAutoGeneratedTimestamp} - Generates timestamp for Date/Long fields</li>
* </ul>
*
* <p><b>Important:</b> This helper is only used when {@link MarshallingMode#SDK_V1_COMPATIBLE} is set.
* For {@link MarshallingMode#SDK_V2_NATIVE}, users should configure {@code AutoGeneratedUuidExtension}
* and use SDK v2's {@code @DynamoDbAutoGeneratedUuid} annotation.
* @since 7.0.0
*/
public class AutoGeneratedKeyHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(AutoGeneratedKeyHelper.class);
/**
* Private constructor to prevent instantiation of this utility class.
* All methods are static and should be accessed directly on the class.
*/
private AutoGeneratedKeyHelper() {
}
private static final String DYNAMODB_AUTO_GENERATED_KEY = "org.socialsignin.spring.data.dynamodb.annotation.DynamoDBAutoGeneratedKey";
private static final String DYNAMODB_AUTO_GENERATED_TIMESTAMP = "org.socialsignin.spring.data.dynamodb.annotation.DynamoDBAutoGeneratedTimestamp";
/**
* Processes an entity and auto-generates values for fields annotated with SDK v1 auto-generation annotations.
*
* <p>This method should only be called when {@link MarshallingMode#SDK_V1_COMPATIBLE} is active.
* @param entity The entity to process
* @param <T> The entity type
*/
public static <T> void processAutoGeneratedKeys(@Nullable T entity) {
if (entity == null) {
return;
}
Class<?> entityClass = entity.getClass();
// Process fields
processFields(entity, entityClass);
// Process methods (getters/setters)
processMethods(entity, entityClass);
}
private static <T> void processFields(T entity, @NonNull Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
try {
// Check for @DynamoDBAutoGeneratedKey (SDK v1)
if (hasAnnotation(field, DYNAMODB_AUTO_GENERATED_KEY)) {
processUuidField(entity, field);
}
// Check for @DynamoDBAutoGeneratedTimestamp (SDK v1)
else if (hasAnnotation(field, DYNAMODB_AUTO_GENERATED_TIMESTAMP)) {
processTimestampField(entity, field);
}
} catch (Exception e) {
LOGGER.warn("Failed to process auto-generated field: {}", field.getName(), e);
}
}
// Process parent class fields
if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) {
processFields(entity, clazz.getSuperclass());
}
}
private static <T> void processMethods(@NonNull T entity, @NonNull Class<?> clazz) {
for (Method method : clazz.getMethods()) {
try {
// Only process getters
if (!isGetter(method)) {
continue;
}
// Check for SDK v1 annotations on getter
if (hasAnnotation(method, DYNAMODB_AUTO_GENERATED_KEY)) {
processUuidMethod(entity, method);
} else if (hasAnnotation(method, DYNAMODB_AUTO_GENERATED_TIMESTAMP)) {
processTimestampMethod(entity, method);
}
} catch (Exception e) {
LOGGER.warn("Failed to process auto-generated method: {}", method.getName(), e);
}
}
}
private static <T> void processUuidField(T entity, @NonNull Field field) throws IllegalAccessException {
field.setAccessible(true);
Object currentValue = field.get(entity);
// Only generate if field is null or empty
if (currentValue == null || (currentValue instanceof String && ((String) currentValue).isEmpty())) {
if (field.getType() == String.class) {
String uuid = UUID.randomUUID().toString();
field.set(entity, uuid);
LOGGER.debug("Generated UUID for field {}: {}", field.getName(), uuid);
} else {
LOGGER.warn("Field {} has @DynamoDBAutoGeneratedKey annotation but is not of type String", field.getName());
}
}
}
private static <T> void processTimestampField(T entity, @NonNull Field field) throws IllegalAccessException {
field.setAccessible(true);
Object currentValue = field.get(entity);
// Only generate if field is null
if (currentValue == null) {
if (field.getType() == Date.class) {
field.set(entity, new Date());
LOGGER.debug("Generated Date timestamp for field {}", field.getName());
} else if (field.getType() == Long.class || field.getType() == long.class) {
field.set(entity, System.currentTimeMillis());
LOGGER.debug("Generated Long timestamp for field {}", field.getName());
} else {
LOGGER.warn("Field {} has @DynamoDBAutoGeneratedTimestamp annotation but is not of type Date or Long", field.getName());
}
}
}
private static <T> void processUuidMethod(@NonNull T entity, @NonNull Method getter) throws ReflectiveOperationException {
Object currentValue = getter.invoke(entity);
// Only generate if value is null or empty
if (currentValue == null || (currentValue instanceof String && ((String) currentValue).isEmpty())) {
if (getter.getReturnType() == String.class) {
Method setter = findSetter(entity.getClass(), getter);
if (setter != null) {
String uuid = UUID.randomUUID().toString();
setter.invoke(entity, uuid);
LOGGER.debug("Generated UUID for property {}: {}", getPropertyName(getter), uuid);
} else {
LOGGER.warn("No setter found for getter {} in processUuidMethod", getter.getName());
}
}
}
}
private static <T> void processTimestampMethod(@NonNull T entity, @NonNull Method getter) throws ReflectiveOperationException {
Object currentValue = getter.invoke(entity);
// Only generate if value is null
if (currentValue == null) {
Method setter = findSetter(entity.getClass(), getter);
if (setter != null) {
if (getter.getReturnType() == Date.class) {
setter.invoke(entity, new Date());
LOGGER.debug("Generated Date timestamp for property {}", getPropertyName(getter));
} else if (getter.getReturnType() == Long.class || getter.getReturnType() == long.class) {
setter.invoke(entity, System.currentTimeMillis());
LOGGER.debug("Generated Long timestamp for property {}", getPropertyName(getter));
}
} else {
LOGGER.warn("No setter found for getter {} in processTimestampMethod", getter.getName());
}
}
}
private static boolean isGetter(@NonNull Method method) {
String name = method.getName();
return (name.startsWith("get") || name.startsWith("is")) &&
method.getParameterCount() == 0 &&
method.getReturnType() != void.class &&
method.getReturnType() != Void.class;
}
@Nullable
private static Method findSetter(@NonNull Class<?> clazz, @NonNull Method getter) {
String propertyName = getPropertyName(getter);
String setterName = "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
try {
return clazz.getMethod(setterName, getter.getReturnType());
} catch (NoSuchMethodException e) {
return null;
}
}
@NonNull
private static String getPropertyName(@NonNull Method getter) {
String methodName = getter.getName();
if (methodName.startsWith("get") && methodName.length() > 3) {
return Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
} else if (methodName.startsWith("is") && methodName.length() > 2) {
return Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
}
return methodName;
}
private static boolean hasAnnotation(@NonNull Field field, String annotationClassName) {
return java.util.Arrays.stream(field.getAnnotations())
.anyMatch(ann -> ann.annotationType().getName().equals(annotationClassName));
}
private static boolean hasAnnotation(@NonNull Method method, String annotationClassName) {
return java.util.Arrays.stream(method.getAnnotations())
.anyMatch(ann -> ann.annotationType().getName().equals(annotationClassName));
}
}