From 3a66bc75e2ef097f4fd7bcfe3f646f545bfd49a5 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 9 Dec 2015 16:00:32 +0100 Subject: [PATCH] DATAREST-724 - Added configuration API for entity lookup customizations. RepositoryRestConfiguration now exposes a withCustomEntityLookup() returning an EntityLookupRegistrar that allows to define customizations as follows on Java 8: config.withCustomEntityLookup() .forRepository(UserRepository.class) .withIdMapping(User::getUsername) .withLookup(UserRepository::findByUsername); or even abbreviated to: config.withCustomEntityLookup() .forRepository(UserRepository.class, User::getUsername, UserRepository::findByUsername); The API basically takes two lambdas or method references to define the id mapping (User::getUsername in this case) as well as the lookup call based on the repository type with which the customization builder was set up in the first place (UserRepository::findByUsername in this case). --- .../config/EntityLookupConfiguration.java | 205 ++++++++++++++++++ .../core/config/EntityLookupRegistrar.java | 83 +++++++ .../config/RepositoryRestConfiguration.java | 28 +++ .../RepositoryRestMvcConfiguration.java | 13 +- 4 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupRegistrar.java diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java new file mode 100644 index 000000000..64e082b30 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java @@ -0,0 +1,205 @@ +/* + * Copyright 2015 the original author or authors. + * + * 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.springframework.data.rest.core.config; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.core.config.EntityLookupRegistrar.LookupRegistrar.Lookup; +import org.springframework.data.rest.core.support.EntityLookup; +import org.springframework.util.Assert; + +/** + * Configuration instance to implement {@link EntityLookupRegistrar}. Exposed via + * {@link RepositoryRestConfiguration#withCustomEntityLookup()}. + * + * @author Oliver Gierke + * @since 2.5 + */ +@RequiredArgsConstructor +class EntityLookupConfiguration implements EntityLookupRegistrar { + + private final List>> lookupInformation = new ArrayList>>(); + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.config.EntityLookupRegistrar#forRepository(java.lang.Class, org.springframework.core.convert.converter.Converter, org.springframework.data.rest.core.config.EntityLookupRegistrar.LookupRegistrar.Lookup) + */ + @Override + public > EntityLookupRegistrar forRepository( + Class repositoryType, Converter converter, Lookup lookup) { + + new MappingBuilder(repositoryType).withIdMapping(converter).withLookup(lookup); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.config.EntityLookupRegistrar#forRepository(java.lang.Class) + */ + @Override + public > IdMappingRegistrar forRepository( + Class type) { + return new MappingBuilder(type); + } + + /** + * Custom builder implementation to back {@link LookupRegistrar} and {@link IdMappingRegistrar}. + * + * @author Oliver Gierke + */ + @RequiredArgsConstructor + private class MappingBuilder> + implements LookupRegistrar, IdMappingRegistrar { + + private @NonNull final Class repositoryType; + private Converter idMapping; + + /** + * Creates a new {@link MappingBuilder} using the given repository type and identifier mapping. + * + * @param repositoryType must not be {@literal null}. + * @param mapping must not be {@literal null}. + */ + private MappingBuilder(Class repositoryType, Converter mapping) { + + this(repositoryType); + + Assert.notNull(mapping, "Converter must not be null!"); + + this.idMapping = mapping; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.config.EntityLookupRegistrar.LookupRegistrar#withLookup(org.springframework.data.rest.core.config.EntityLookupRegistrar.LookupRegistrar.Lookup) + */ + @Override + @SuppressWarnings("unchecked") + public EntityLookupRegistrar withLookup(Lookup lookup) { + + EntityLookupConfiguration.this.lookupInformation.add( + (LookupInformation>) new LookupInformation( + repositoryType, idMapping, lookup)); + + return EntityLookupConfiguration.this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.config.EntityLookupRegistrar.IdMappingRegistrar#withIdMapping(org.springframework.core.convert.converter.Converter) + */ + @Override + public LookupRegistrar withIdMapping(Converter idMapping) { + return new MappingBuilder(repositoryType, idMapping); + } + } + + /** + * Returns the {@link EntityLookup}s registered on this configuration. + * + * @param repositories must not be {@literal null}. + * @return + */ + public List> getEntityLookups(Repositories repositories) { + + Assert.notNull(repositories, "Repositories must not be null!"); + + List> lookups = new ArrayList>(lookupInformation.size()); + + for (LookupInformation> information : lookupInformation) { + lookups.add(new RepositoriesEntityLookup(repositories, information)); + } + + return lookups; + } + + /** + * An {@link EntityLookup} backed by a repository instance and a {@link LookupInformation}. + * + * @author Oliver Gierke + */ + private static class RepositoriesEntityLookup implements EntityLookup { + + private final LookupInformation> lookupInfo; + private final Repository repository; + private final Class domainType; + + /** + * Creates a new {@link RepositoriesEntityLookup} for the given {@link Repositories} and {@link LookupInformation}. + * + * @param repositories must not be {@literal null}. + * @param lookupInformation must not be {@literal null}. + */ + @SuppressWarnings("unchecked") + public RepositoriesEntityLookup(Repositories repositories, + LookupInformation> lookupInformation) { + + Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(lookupInformation, "LookuInformation must not be null!"); + + RepositoryInformation information = repositories.getRepositoryInformation(lookupInformation.repositoryType); + + this.repository = (Repository) repositories.getRepositoryFor(information.getDomainType()); + this.domainType = information.getDomainType(); + this.lookupInfo = lookupInformation; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.support.EntityLookup#getResourceIdentifier(java.lang.Object) + */ + @Override + public Serializable getResourceIdentifier(T entity) { + return lookupInfo.getIdentifierMapping().convert(entity); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.support.EntityLookup#lookupEntity(java.io.Serializable) + */ + @Override + public Object lookupEntity(Serializable id) { + return lookupInfo.getLookup().lookup(repository, id); + } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return domainType.isAssignableFrom(delimiter); + } + } + + @Value + private static class LookupInformation> { + + private final Class repositoryType; + private final Converter identifierMapping; + private final Lookup lookup; + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupRegistrar.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupRegistrar.java new file mode 100644 index 000000000..cea2b4a3d --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupRegistrar.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015 the original author or authors. + * + * 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.springframework.data.rest.core.config; + +import java.io.Serializable; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.repository.Repository; +import org.springframework.data.rest.core.config.EntityLookupRegistrar.LookupRegistrar.Lookup; +import org.springframework.data.rest.core.support.EntityLookup; + +/** + * Configuration interfaces to ease the configuration of custom {@link EntityLookup}s for repositories. + * + * @author Oliver Gierke + * @since 2.5 + */ +public interface EntityLookupRegistrar { + + /** + * Starts building a custom {@link EntityLookup} for the given repository type. + * + * @param type must not be {@literal null}. + * @return + */ + > IdMappingRegistrar forRepository(Class type); + + /** + * Registers an {@link EntityLookup} for the given repository type, identifier mapping and lookup operation. + * + * @param type must not be {@literal null}. + * @param identifierMapping must not be {@literal null}. + * @param lookup must not be {@literal null}. + */ + > EntityLookupRegistrar forRepository(Class type, + Converter identifierMapping, Lookup lookup); + + interface IdMappingRegistrar> { + + /** + * Registers the given {@link Converter} to map the entity to its identifying property. + * + * @param mapping must not be {@literal null}. + * @return + */ + LookupRegistrar withIdMapping(Converter mapping); + } + + interface LookupRegistrar> { + + /** + * Registers the given {@link Lookup} to obtain entity instances. + * + * @param lookup must not be {@literal null}. + */ + EntityLookupRegistrar withLookup(Lookup lookup); + + interface Lookup, ID> { + + /** + * Looks up the entity using the given {@link Repository} and identifier. + * + * @param repository + * @param identifier + * @return + */ + Object lookup(R repository, ID identifier); + } + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java index d37b19fd5..97c785dfe 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java @@ -20,8 +20,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy; import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy.RepositoryDetectionStrategies; +import org.springframework.data.rest.core.support.EntityLookup; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.util.Assert; @@ -58,6 +60,7 @@ public class RepositoryRestConfiguration { private final ProjectionDefinitionConfiguration projectionConfiguration; private final MetadataConfiguration metadataConfiguration; + private final EntityLookupConfiguration entityLookupConfiguration; private final EnumTranslationConfiguration enumTranslationConfiguration; private boolean enableEnumTranslation = false; @@ -79,6 +82,7 @@ public class RepositoryRestConfiguration { this.projectionConfiguration = projectionConfiguration; this.metadataConfiguration = metadataConfiguration; this.enumTranslationConfiguration = enumTranslationConfiguration; + this.entityLookupConfiguration = new EntityLookupConfiguration(); } /** @@ -543,4 +547,28 @@ public class RepositoryRestConfiguration { this.repositoryDetectionStrategy = repositoryDetectionStrategy == null ? RepositoryDetectionStrategies.DEFAULT : repositoryDetectionStrategy; } + + /** + * Returns the {@link EntityLookupRegistrar} to create custom {@link EntityLookup} instances registered in the + * configuration. + * + * @return the {@link EntityLookupRegistrar} to build custom {@link EntityLookup}s. + * @since 2.5 + */ + public EntityLookupRegistrar withCustomEntityLookup() { + return entityLookupConfiguration; + } + + /** + * Returns all {@link EntityLookup}s considering the customizations made to the configuration. + * + * @param repositories must not be {@literal null}. + * @return + */ + public List> getEntityLookups(Repositories repositories) { + + Assert.notNull(repositories, "Repositories must not be null!"); + + return entityLookupConfiguration.getEntityLookups(repositories); + } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index f63aef052..f4ff9f7d5 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -630,7 +630,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon public RepositoryInvokerFactory repositoryInvokerFactory() { return new UnwrappingRepositoryInvokerFactory( - new DefaultRepositoryInvokerFactory(repositories(), defaultConversionService()), lookups); + new DefaultRepositoryInvokerFactory(repositories(), defaultConversionService()), getEntityLookups()); } @Bean @@ -715,7 +715,16 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon @Bean public SelfLinkProvider selfLinkProvider() { - return new DefaultSelfLinkProvider(persistentEntities(), entityLinks(), lookups); + return new DefaultSelfLinkProvider(persistentEntities(), entityLinks(), getEntityLookups()); + } + + protected List> getEntityLookups() { + + List> lookups = new ArrayList>(); + lookups.addAll(config().getEntityLookups(repositories())); + lookups.addAll(this.lookups); + + return lookups; } protected List defaultMethodArgumentResolvers() {