DynamoDBRepositoryFactory.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.repository.support;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.socialsignin.spring.data.dynamodb.core.DynamoDBOperations;
import org.socialsignin.spring.data.dynamodb.repository.DynamoDBCrudRepository;
import org.socialsignin.spring.data.dynamodb.repository.query.DynamoDBQueryLookupStrategy;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.Version;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import software.amazon.awssdk.core.util.VersionInfo;

import java.util.Optional;
import java.util.StringTokenizer;

import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT;

/**
 * Repository factory for creating DynamoDB repository instances with support for entity information,
 * query lookup strategies, and repository base classes.
 * @author Prasanna Kumar Ramachandran
 */
public class DynamoDBRepositoryFactory extends RepositoryFactorySupport {
    private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBRepositoryFactory.class);

    static {
        final String DEVELOPMENT = "DEVELOPMENT";

        // SDK v2: Get version from VersionInfo utility class
        String awsSdkVersion = VersionInfo.SDK_VERSION;
        String springDataVersion = Version.class.getPackage().getImplementationVersion();

        String thisSpecVersion = DynamoDBRepositoryFactory.class.getPackage().getSpecificationVersion();
        String thisImplVersion = DynamoDBRepositoryFactory.class.getPackage().getImplementationVersion();
        if (thisImplVersion == null || thisSpecVersion == null) {
            thisSpecVersion = DEVELOPMENT;
            thisImplVersion = DEVELOPMENT;
        }

        LOGGER.info("Spring Data DynamoDB Version: {} ({})", thisImplVersion, thisSpecVersion);
        LOGGER.info("Spring Data Version:          {}", springDataVersion);
        LOGGER.info("AWS SDK Version:              {}", awsSdkVersion);
        LOGGER.info("Java Version:                 {} - {} {}", System.getProperty("java.version"),
                System.getProperty("java.vm.name"), System.getProperty("java.vm.version"));
        LOGGER.info("Platform Details:             {} {}", System.getProperty("os.name"),
                System.getProperty("os.version"));

        if (!DEVELOPMENT.equals(thisImplVersion) && !isCompatible(springDataVersion, thisSpecVersion)) {
            LOGGER.warn("This Spring Data DynamoDB implementation might not be compatible with the available Spring Data classes on the classpath!{}NoDefClassFoundExceptions or similar might occur!", System.lineSeparator());
        }
    }

    /**
     * Checks if the specification version is compatible with the implementation version.
     * @param spec the specification version
     * @param impl the implementation version
     * @return true if compatible, false otherwise
     */
    protected static boolean isCompatible(@Nullable String spec, @Nullable String impl) {
        if (spec == null && impl == null) {
            return false;
        } else if (spec == null) {
            spec = "";
        } else if (impl == null) {
            impl = "";
        }
        StringTokenizer specTokenizer = new StringTokenizer(spec, ".");
        StringTokenizer implTokenizer = new StringTokenizer(impl, ".");

        String specMajor = specTokenizer.hasMoreTokens() ? specTokenizer.nextToken() : "0";
        String specMinor = specTokenizer.hasMoreTokens() ? specTokenizer.nextToken() : "0";

        String implMajor = implTokenizer.hasMoreTokens() ? implTokenizer.nextToken() : "0";
        String implMinor = implTokenizer.hasMoreTokens() ? implTokenizer.nextToken() : "0";

        return specMajor.equals(implMajor) && specMinor.equals(implMinor);
    }

    private final DynamoDBOperations dynamoDBOperations;

    /**
     * Creates a new DynamoDB repository factory.
     * @param dynamoDBOperations the DynamoDB operations instance
     */
    public DynamoDBRepositoryFactory(DynamoDBOperations dynamoDBOperations) {
        this.dynamoDBOperations = dynamoDBOperations;
    }

    @NonNull
    @Override
    public <T, ID> DynamoDBEntityInformation<T, ID> getEntityInformation(@NonNull final Class<T> domainClass) {

        final DynamoDBEntityMetadataSupport<T, ID> metadata = new DynamoDBEntityMetadataSupport<>(domainClass,
                this.dynamoDBOperations);
        return metadata.getEntityInformation();
    }

    @NonNull
    @Override
    protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
                                                                   @NonNull ValueExpressionDelegate valueExpressionDelegate) {
        return Optional.of(DynamoDBQueryLookupStrategy.create(dynamoDBOperations, key));
    }

    /**
     * Callback to create a {@link DynamoDBCrudRepository} instance with the given {@link RepositoryMetadata}.
     * @param <T> Type of the Entity
     * @param <ID> Type of the Hash (Primary) Key
     * @param metadata Metadata of the entity
     * @see #getTargetRepository(RepositoryInformation)
     * @return the created {@link DynamoDBCrudRepository} instance
     */
    @NonNull
    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected <T, ID> DynamoDBCrudRepository<?, ?> getDynamoDBRepository(@NonNull RepositoryMetadata metadata) {
        return new SimpleDynamoDBPagingAndSortingRepository(getEntityInformation(metadata.getDomainType()),
                dynamoDBOperations, getEnableScanPermissions(metadata));
    }

    /**
     * Gets the scan permissions for the given repository metadata.
     * @param metadata the repository metadata
     * @return the enable scan permissions
     */
    @NonNull
    protected EnableScanPermissions getEnableScanPermissions(@NonNull RepositoryMetadata metadata) {
        return new EnableScanAnnotationPermissions(metadata.getRepositoryInterface());
    }

    @NonNull
    @Override
    protected Class<?> getRepositoryBaseClass(@NonNull RepositoryMetadata metadata) {
        if (isQueryDslRepository(metadata.getRepositoryInterface())) {
            throw new IllegalArgumentException("QueryDsl Support has not been implemented yet.");
        }
        return SimpleDynamoDBPagingAndSortingRepository.class;
    }

    private static boolean isQueryDslRepository(@NonNull Class<?> repositoryInterface) {
        return QUERY_DSL_PRESENT && QuerydslPredicateExecutor.class.isAssignableFrom(repositoryInterface);
    }

    @NonNull
    @Override
    protected Object getTargetRepository(@NonNull RepositoryInformation metadata) {
        return getDynamoDBRepository(metadata);
    }

}