TableSchemaFactory.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.socialsignin.spring.data.dynamodb.marshaller.*;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;

/**
 * Factory for creating TableSchema instances based on the marshalling mode.
 *
 * <p>This factory provides a simple abstraction over TableSchema creation to support
 * both SDK_V2_NATIVE and SDK_V1_COMPATIBLE marshalling modes.
 *
 * <p><b>GraalVM Native Image Support:</b></p>
 * <p>This factory uses {@link DynamoDbTableSchemaRegistry} which provides GraalVM-compatible
 * schema generation using {@link StaticTableSchemaGenerator}. The generator uses MethodHandles
 * instead of LambdaMetafactory, making it compatible with native image compilation.
 *
 * <p><b>For SDK_V1_COMPATIBLE mode, users MUST annotate their entity fields with
 * {@code @DynamoDbConvertedBy} to specify the appropriate converter.</b> See the
 * {@link #createTableSchema(Class)} method documentation for details.
 *
 * @author Prasanna Kumar Ramachandran
 * @since 7.0.0
 * @see DynamoDbTableSchemaRegistry
 * @see StaticTableSchemaGenerator
 */
public class TableSchemaFactory {

    /**
     * Default constructor.
     */
    public TableSchemaFactory() {
    }

    /**
     * Creates a TableSchema for the given domain class based on the marshalling mode.
     *
     * <p><b>SDK_V2_NATIVE Mode (Default):</b></p>
     * <p>Uses standard AWS SDK v2 type mappings:
     * <ul>
     * <li>Date → DynamoDB Number (epoch milliseconds)</li>
     * <li>Instant → DynamoDB Number (epoch seconds with nanosecond precision)</li>
     * <li>Boolean → DynamoDB BOOL type</li>
     * </ul>
     * <p>No special annotations required beyond standard SDK v2 annotations
     * ({@code @DynamoDbBean}, {@code @DynamoDbPartitionKey}, etc.)
     *
     * <p><b>SDK_V1_COMPATIBLE Mode:</b></p>
     * <p>Enables backward compatibility with AWS SDK v1 data formats. Users MUST annotate
     * their Date, Instant, and Boolean fields with {@code @DynamoDbConvertedBy} to specify
     * the appropriate converter.
     *
     * <p><b>Required Annotations for SDK_V1_COMPATIBLE Mode:</b></p>
     * <pre>
     * import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.*;
     * import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute;
     * import org.socialsignin.spring.data.dynamodb.marshaller.*;
     *
     * {@code @DynamoDbBean}
     * public class User {
     *     {@code @DynamoDbPartitionKey}
     *     private String userId;
     *
     *     // For Date fields (SDK v1 default was ISO-8601 string):
     *     {@code @DynamoDbConvertedBy(Date2IsoAttributeConverter.class)}
     *     private Date createdDate;
     *
     *     // For Date fields with epoch milliseconds (if you used that in SDK v1):
     *     {@code @DynamoDbConvertedBy(Date2EpocheAttributeConverter.class)}
     *     private Date lastModified;
     *
     *     // For Instant fields with ISO-8601 format:
     *     {@code @DynamoDbConvertedBy(Instant2IsoAttributeConverter.class)}
     *     private Instant timestamp;
     *
     *     // For Instant fields with epoch milliseconds:
     *     {@code @DynamoDbConvertedBy(Instant2EpocheAttributeConverter.class)}
     *     private Instant eventTime;
     *
     *     // For Boolean fields (SDK v1 stored as Number "1"/"0"):
     *     {@code @DynamoDbConvertedBy(BooleanNumberAttributeConverter.class)}
     *     private Boolean active;
     *
     *     // For optimistic locking (SDK v1 @DynamoDBVersionAttribute → SDK v2 @DynamoDbVersionAttribute):
     *     {@code @DynamoDbVersionAttribute}
     *     private Long version;
     *
     *     // Getters and setters...
     * }
     * </pre>
     *
     * <p><b>Available Converters for SDK_V1_COMPATIBLE Mode:</b></p>
     * <ul>
     * <li>{@link Date2IsoAttributeConverter} - Date ↔ ISO-8601 String (e.g., "2024-01-15T10:30:00Z")</li>
     * <li>{@link Date2EpocheAttributeConverter} - Date ↔ Epoch milliseconds String (e.g., "1705318200000")</li>
     * <li>{@link Instant2IsoAttributeConverter} - Instant ↔ ISO-8601 String</li>
     * <li>{@link Instant2EpocheAttributeConverter} - Instant ↔ Epoch milliseconds String</li>
     * <li>{@link BooleanNumberAttributeConverter} - Boolean ↔ Number ("1" for true, "0" for false)</li>
     * </ul>
     *
     * <p><b>Important Notes:</b></p>
     * <ul>
     * <li>The marshalling mode affects entity persistence (save/load operations)</li>
     * <li>Query and scan operations use the mode from {@link org.socialsignin.spring.data.dynamodb.mapping.DynamoDBMappingContext}</li>
     * <li>Both modes use the same SDK v2 annotations ({@code @DynamoDbBean}, {@code @DynamoDbPartitionKey}, etc.)</li>
     * <li>SDK_V1_COMPATIBLE mode requires {@code @DynamoDbConvertedBy} annotations for Date/Instant/Boolean fields</li>
     * </ul>
     * @param <T>             The domain class type
     * @param domainClass     The domain class
     * @return A TableSchema instance configured for the specified marshalling mode
     */
    public static <T> TableSchema<T> createTableSchema(Class<T> domainClass) {
        // Use the registry which provides GraalVM-compatible schema generation
        // The registry will:
        // 1. Return a pre-registered schema if available
        // 2. Generate a StaticTableSchema using MethodHandles (GraalVM compatible)
        // 3. Fall back to TableSchema.fromBean() as a last resort (JVM only)
        return DynamoDbTableSchemaRegistry.getInstance().getTableSchema(domainClass);
    }

}