diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java index e86cf16c5..a1c439556 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java @@ -16,6 +16,7 @@ package org.springframework.data.rest.core; import java.net.URI; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -23,9 +24,9 @@ import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.DomainClassConverter; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; /** @@ -36,45 +37,43 @@ import org.springframework.util.Assert; */ public class UriToEntityConverter implements ConditionalGenericConverter { + private static final TypeDescriptor URI_TYPE = TypeDescriptor.valueOf(URI.class); private static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class); - private final Repositories repositories; + private final PersistentEntities entities; private final DomainClassConverter domainClassConverter; private final Set convertiblePairs; - private ResourceMappings mappings; - /** - * Creates a new {@link UriToEntityConverter} using the given {@link Repositories} and {@link DomainClassConverter}. + * Creates a new {@link UriToEntityConverter} using the given {@link PersistentEntities} and + * {@link DomainClassConverter}. * - * @param repositories must not be {@literal null}. + * @param entities must not be {@literal null}. * @param domainClassConverter must not be {@literal null}. */ - public UriToEntityConverter(Repositories repositories, DomainClassConverter domainClassConverter) { + public UriToEntityConverter(PersistentEntities entities, DomainClassConverter domainClassConverter) { - Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(entities, "PersistentEntities must not be null!"); Assert.notNull(domainClassConverter, "DomainClassConverter must not be null!"); - this.repositories = repositories; - this.domainClassConverter = domainClassConverter; - this.convertiblePairs = new HashSet(); + Set convertiblePairs = new HashSet(); - for (Class domainType : repositories) { - convertiblePairs.add(new ConvertiblePair(URI.class, domainType)); + for (TypeInformation domainType : entities.getManagedTypes()) { + convertiblePairs.add(new ConvertiblePair(URI.class, domainType.getType())); } + + this.convertiblePairs = Collections.unmodifiableSet(convertiblePairs); + this.entities = entities; + this.domainClassConverter = domainClassConverter; } - /* + /* * (non-Javadoc) * @see org.springframework.core.convert.converter.ConditionalConverter#matches(org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) */ @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { - - mappings.exportsMappingFor(targetType.getType()); - - return URI.class.isAssignableFrom(sourceType.getType()) - && repositories.getPersistentEntity(targetType.getType()) != null; + return domainClassConverter.matches(URI_TYPE, targetType); } /* @@ -93,9 +92,9 @@ public class UriToEntityConverter implements ConditionalGenericConverter { @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - PersistentEntity entity = repositories.getPersistentEntity(targetType.getType()); + PersistentEntity entity = entities.getPersistentEntity(targetType.getType()); - if (entity == null || !domainClassConverter.matches(STRING_TYPE, targetType)) { + if (entity == null) { throw new ConversionFailedException(sourceType, targetType, source, new IllegalArgumentException( "No PersistentEntity information available for " + targetType.getType())); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MappingResourceMetadata.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MappingResourceMetadata.java new file mode 100644 index 000000000..632eb145b --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MappingResourceMetadata.java @@ -0,0 +1,103 @@ +/* + * Copyright 2014 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.mapping; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; + +/** + * {@link ResourceMetadata} based on a {@link PersistentEntity}. + * + * @author Oliver Gierke + * @since 2.1 + */ +public class MappingResourceMetadata extends TypeBasedCollectionResourceMapping implements ResourceMetadata { + + private final PersistentEntity entity; + private final Map, ResourceMapping> propertyMappings; + + /** + * Creates a new {@link MappingResourceMetadata} for the given {@link PersistentEntity}. + * + * @param entity must not be {@literal null}. + */ + public MappingResourceMetadata(PersistentEntity entity) { + + super(entity.getType()); + + this.entity = entity; + this.propertyMappings = new HashMap, ResourceMapping>(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMetadata#getDomainType() + */ + @Override + public Class getDomainType() { + return entity.getType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMetadata#isManagedResource(org.springframework.data.mapping.PersistentProperty) + */ + @Override + public boolean isManagedResource(PersistentProperty property) { + return property.isAssociation(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMetadata#isExported(org.springframework.data.mapping.PersistentProperty) + */ + @Override + public boolean isExported(PersistentProperty property) { + return getMappingFor(property).isExported(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMetadata#getMappingFor(org.springframework.data.mapping.PersistentProperty) + */ + @Override + public ResourceMapping getMappingFor(PersistentProperty property) { + + ResourceMapping propertyMapping = propertyMappings.get(property); + + if (propertyMapping != null) { + return propertyMapping; + } + + propertyMapping = new RepositoryResourceMappings.PersistentPropertyResourceMapping(property, this, this); + propertyMappings.put(property, propertyMapping); + + return propertyMapping; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMetadata#getSearchResourceMappings() + */ + @Override + public SearchResourceMappings getSearchResourceMappings() { + return new SearchResourceMappings(Collections. emptyList()); + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java index db6440f26..c9ac344f9 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java @@ -32,7 +32,7 @@ class RepositoryAwareResourceInformation implements ResourceMetadata { private final Repositories repositories; private final CollectionResourceMapping mapping; - private final ResourceMappings provider; + private final RepositoryResourceMappings provider; private final RepositoryMetadata repositoryInterface; /** @@ -45,7 +45,7 @@ class RepositoryAwareResourceInformation implements ResourceMetadata { * @param repositoryMetadata must not be {@literal null}. */ public RepositoryAwareResourceInformation(Repositories repositories, CollectionResourceMapping mapping, - ResourceMappings provider, RepositoryMetadata repositoryMetadata) { + RepositoryResourceMappings provider, RepositoryMetadata repositoryMetadata) { Assert.notNull(repositories, "Repositories must not be null!"); Assert.notNull(mapping, "CollectionResourceMapping must not be null!"); diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java new file mode 100644 index 000000000..9a0cc31ce --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java @@ -0,0 +1,339 @@ +/* + * Copyright 2013-2014 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.mapping; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.core.Path; +import org.springframework.data.rest.core.annotation.Description; +import org.springframework.data.rest.core.annotation.RestResource; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.data.rest.core.support.RepositoriesUtils; +import org.springframework.hateoas.RelProvider; +import org.springframework.hateoas.core.EvoInflectorRelProvider; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Central abstraction obtain {@link ResourceMetadata} and {@link ResourceMapping} instances for domain types and + * repositories. + * + * @author Oliver Gierke + */ +public class RepositoryResourceMappings implements ResourceMappings { + + private final Repositories repositories; + private final RelProvider relProvider; + + private final Map, ResourceMetadata> cache = new HashMap, ResourceMetadata>(); + private final Map, SearchResourceMappings> searchCache = new HashMap, SearchResourceMappings>(); + private final Map, ResourceMapping> propertyCache = new HashMap, ResourceMapping>(); + + /** + * Creates a new {@link RepositoryResourceMappings} using the given {@link RepositoryRestConfiguration} and + * {@link Repositories} . + * + * @param config + * @param repositories + */ + public RepositoryResourceMappings(RepositoryRestConfiguration config, Repositories repositories) { + this(config, repositories, new EvoInflectorRelProvider()); + } + + /** + * Creates a new {@link RepositoryResourceMappings} from the given {@link RepositoryRestConfiguration}, + * {@link Repositories} and {@link RelProvider}. + * + * @param config must not be {@literal null}. + * @param repositories must not be {@literal null}. + * @param relProvider must not be {@literal null}. + */ + public RepositoryResourceMappings(RepositoryRestConfiguration config, Repositories repositories, + RelProvider relProvider) { + + Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(relProvider, "RelProvider must not be null!"); + + this.repositories = repositories; + this.relProvider = relProvider; + + this.populateCache(repositories); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMappings#getMappingFor(java.lang.Class) + */ + @Override + public ResourceMetadata getMappingFor(Class type) { + + Assert.notNull(type, "Type must not be null!"); + return cache.get(type); + } + + private final void populateCache(Repositories repositories) { + + for (Class type : repositories) { + + RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(type); + Class repositoryInterface = repositoryInformation.getRepositoryInterface(); + + CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(repositoryInformation, relProvider); + + RepositoryAwareResourceInformation information = new RepositoryAwareResourceInformation(repositories, mapping, + this, repositoryInformation); + + cache.put(repositoryInterface, information); + + if (!cache.containsKey(type) || information.isPrimary()) { + cache.put(type, information); + } + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMappings#getSearchResourceMappings(java.lang.Class) + */ + @Override + public SearchResourceMappings getSearchResourceMappings(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + if (searchCache.containsKey(type)) { + return searchCache.get(type); + } + + Class domainType = RepositoriesUtils.getDomainType(type); + + if (searchCache.containsKey(domainType)) { + return searchCache.get(domainType); + } + + RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(domainType); + List mappings = new ArrayList(); + ResourceMetadata resourceMapping = getMappingFor(domainType); + + if (resourceMapping.isExported()) { + for (Method queryMethod : repositoryInformation.getQueryMethods()) { + RepositoryMethodResourceMapping methodMapping = new RepositoryMethodResourceMapping(queryMethod, + resourceMapping); + if (methodMapping.isExported()) { + mappings.add(methodMapping); + } + } + } + + SearchResourceMappings searchResourceMappings = new SearchResourceMappings(mappings); + searchCache.put(type, searchResourceMappings); + searchCache.put(domainType, searchResourceMappings); + return searchResourceMappings; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMappings#exportsMappingFor(java.lang.Class) + */ + @Override + public boolean exportsMappingFor(Class type) { + + if (!hasMappingFor(type)) { + return false; + } + + ResourceMetadata metadata = getMappingFor(type); + return metadata.isExported(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMappings#exportsTopLevelResourceFor(java.lang.String) + */ + @Override + public boolean exportsTopLevelResourceFor(String path) { + + Assert.hasText(path); + + for (ResourceMetadata metadata : cache.values()) { + if (metadata.getPath().matches(path)) { + return metadata.isExported(); + } + } + + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMappings#hasMappingFor(java.lang.Class) + */ + @Override + public boolean hasMappingFor(Class type) { + + if (cache.containsKey(type)) { + return true; + } + + if (repositories.hasRepositoryFor(type)) { + return true; + } + + if (RepositoriesUtils.isRepositoryInterface(type) && hasMappingFor(RepositoriesUtils.getDomainType(type))) { + return true; + } + + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMetadataProvider#getMappingFor(org.springframework.data.mapping.PersistentProperty) + */ + ResourceMapping getMappingFor(PersistentProperty property) { + + ResourceMapping propertyMapping = propertyCache.get(property); + + if (propertyMapping != null) { + return propertyMapping; + } + + ResourceMetadata propertyTypeMapping = getMappingFor(property.getActualType()); + ResourceMetadata ownerTypeMapping = getMappingFor(property.getOwner().getType()); + propertyMapping = new PersistentPropertyResourceMapping(property, propertyTypeMapping, ownerTypeMapping); + + propertyCache.put(property, propertyMapping); + + return propertyMapping; + } + + public boolean isMapped(PersistentProperty property) { + + ResourceMapping metadata = getMappingFor(property); + return metadata != null && metadata.isExported(); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return cache.values().iterator(); + } + + /** + * Special resource mapping for {@link PersistentProperty} instances. + * + * @author Oliver Gierke + */ + static class PersistentPropertyResourceMapping implements ResourceMapping { + + private final PersistentProperty property; + private final ResourceMapping typeMapping; + private final CollectionResourceMapping ownerTypeMapping; + private final RestResource annotation; + private final Description description; + + /** + * Creates a new {@link PersistentPropertyResourceMapping}. + * + * @param property must not be {@literal null}. + * @param exported whether the property is exported or not. + */ + public PersistentPropertyResourceMapping(PersistentProperty property, ResourceMapping typeMapping, + CollectionResourceMapping ownerTypeMapping) { + + Assert.notNull(property, "PersistentProperty must not be null!"); + + this.property = property; + this.typeMapping = typeMapping; + this.ownerTypeMapping = ownerTypeMapping; + this.annotation = property.isAssociation() ? property.findAnnotation(RestResource.class) : null; + this.description = property.findAnnotation(Description.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#getPath() + */ + @Override + public Path getPath() { + return annotation != null && StringUtils.hasText(annotation.path()) ? new Path(annotation.path()) : new Path( + property.getName()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#getRel() + */ + @Override + public String getRel() { + return annotation != null && StringUtils.hasText(annotation.rel()) ? annotation.rel() : property.getName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#isExported() + */ + @Override + public boolean isExported() { + + if (typeMapping == null) { + return false; + } + + return !typeMapping.isExported() ? false : annotation == null ? true : annotation.exported(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#isPagingResource() + */ + @Override + public boolean isPagingResource() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#getDescription() + */ + @Override + public ResourceDescription getDescription() { + + ResourceDescription fallback = SimpleResourceDescription.defaultFor(property, + ownerTypeMapping.getItemResourceRel()); + + if (description != null) { + return new AnnotationBasedResourceDescription(description, fallback); + } + + if (annotation != null) { + return new AnnotationBasedResourceDescription(annotation.description(), fallback); + } + + return fallback; + } + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java index 2e85dff85..05e38b4f0 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2014 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. @@ -15,70 +15,11 @@ */ package org.springframework.data.rest.core.mapping; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.rest.core.Path; -import org.springframework.data.rest.core.annotation.Description; -import org.springframework.data.rest.core.annotation.RestResource; -import org.springframework.data.rest.core.config.RepositoryRestConfiguration; -import org.springframework.data.rest.core.support.RepositoriesUtils; -import org.springframework.hateoas.RelProvider; -import org.springframework.hateoas.core.EvoInflectorRelProvider; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** - * Central abstraction obtain {@link ResourceMetadata} and {@link ResourceMapping} instances for domain types and - * repositories. - * * @author Oliver Gierke */ -public class ResourceMappings implements Iterable { - - private final Repositories repositories; - private final RelProvider relProvider; - - private final Map, ResourceMetadata> cache = new HashMap, ResourceMetadata>(); - private final Map, SearchResourceMappings> searchCache = new HashMap, SearchResourceMappings>(); - private final Map, ResourceMapping> propertyCache = new HashMap, ResourceMapping>(); - - /** - * Creates a new {@link ResourceMappings} using the given {@link RepositoryRestConfiguration} and {@link Repositories} - * . - * - * @param config - * @param repositories - */ - public ResourceMappings(RepositoryRestConfiguration config, Repositories repositories) { - this(config, repositories, new EvoInflectorRelProvider()); - } - - /** - * Creates a new {@link ResourceMappings} from the given {@link RepositoryRestConfiguration}, {@link Repositories} and - * {@link RelProvider}. - * - * @param config must not be {@literal null}. - * @param repositories must not be {@literal null}. - * @param relProvider must not be {@literal null}. - */ - public ResourceMappings(RepositoryRestConfiguration config, Repositories repositories, RelProvider relProvider) { - - Assert.notNull(repositories, "Repositories must not be null!"); - Assert.notNull(relProvider, "RelProvider must not be null!"); - - this.repositories = repositories; - this.relProvider = relProvider; - - this.populateCache(repositories); - } +public interface ResourceMappings extends Iterable { /** * Returns a {@link ResourceMetadata} for the given type if available. @@ -86,31 +27,7 @@ public class ResourceMappings implements Iterable { * @param type must not be {@literal null}. * @return */ - public ResourceMetadata getMappingFor(Class type) { - - Assert.notNull(type, "Type must not be null!"); - return cache.get(type); - } - - private final void populateCache(Repositories repositories) { - - for (Class type : repositories) { - - RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(type); - Class repositoryInterface = repositoryInformation.getRepositoryInterface(); - - CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(repositoryInformation, relProvider); - - RepositoryAwareResourceInformation information = new RepositoryAwareResourceInformation(repositories, mapping, - this, repositoryInformation); - - cache.put(repositoryInterface, information); - - if (!cache.containsKey(type) || information.isPrimary()) { - cache.put(type, information); - } - } - } + ResourceMetadata getMappingFor(Class type); /** * Returns the {@link ResourceMapping}s for the search resources of the given type. @@ -118,39 +35,7 @@ public class ResourceMappings implements Iterable { * @param type must not be {@literal null}. * @return */ - public SearchResourceMappings getSearchResourceMappings(Class type) { - - Assert.notNull(type, "Type must not be null!"); - - if (searchCache.containsKey(type)) { - return searchCache.get(type); - } - - Class domainType = RepositoriesUtils.getDomainType(type); - - if (searchCache.containsKey(domainType)) { - return searchCache.get(domainType); - } - - RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(domainType); - List mappings = new ArrayList(); - ResourceMetadata resourceMapping = getMappingFor(domainType); - - if (resourceMapping.isExported()) { - for (Method queryMethod : repositoryInformation.getQueryMethods()) { - RepositoryMethodResourceMapping methodMapping = new RepositoryMethodResourceMapping(queryMethod, - resourceMapping); - if (methodMapping.isExported()) { - mappings.add(methodMapping); - } - } - } - - SearchResourceMappings searchResourceMappings = new SearchResourceMappings(mappings); - searchCache.put(type, searchResourceMappings); - searchCache.put(domainType, searchResourceMappings); - return searchResourceMappings; - } + SearchResourceMappings getSearchResourceMappings(Class type); /** * Returns whether we have a {@link ResourceMapping} for the given type and it is exported. @@ -158,15 +43,7 @@ public class ResourceMappings implements Iterable { * @param type * @return */ - public boolean exportsMappingFor(Class type) { - - if (!hasMappingFor(type)) { - return false; - } - - ResourceMetadata metadata = getMappingFor(type); - return metadata.isExported(); - } + boolean exportsMappingFor(Class type); /** * Returns whether we export a top-level resource for the given path. @@ -174,18 +51,7 @@ public class ResourceMappings implements Iterable { * @param path must not be {@literal null} or empty. * @return */ - public boolean exportsTopLevelResourceFor(String path) { - - Assert.hasText(path); - - for (ResourceMetadata metadata : cache.values()) { - if (metadata.getPath().matches(path)) { - return metadata.isExported(); - } - } - - return false; - } + boolean exportsTopLevelResourceFor(String path); /** * Returns whether we have a {@link ResourceMapping} for the given type. @@ -193,155 +59,5 @@ public class ResourceMappings implements Iterable { * @param type must not be {@literal null}. * @return */ - public boolean hasMappingFor(Class type) { - - if (cache.containsKey(type)) { - return true; - } - - if (repositories.hasRepositoryFor(type)) { - return true; - } - - if (RepositoriesUtils.isRepositoryInterface(type) && hasMappingFor(RepositoriesUtils.getDomainType(type))) { - return true; - } - - return false; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.core.mapping.ResourceMetadataProvider#getMappingFor(org.springframework.data.mapping.PersistentProperty) - */ - ResourceMapping getMappingFor(PersistentProperty property) { - - ResourceMapping propertyMapping = propertyCache.get(property); - - if (propertyMapping != null) { - return propertyMapping; - } - - ResourceMetadata propertyTypeMapping = getMappingFor(property.getActualType()); - ResourceMetadata ownerTypeMapping = getMappingFor(property.getOwner().getType()); - propertyMapping = new PersistentPropertyResourceMapping(property, propertyTypeMapping, ownerTypeMapping); - - propertyCache.put(property, propertyMapping); - - return propertyMapping; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.core.mapping.ResourceMetadataProvider#hasMappingFor(org.springframework.data.mapping.PersistentProperty) - */ - public boolean isMapped(PersistentProperty property) { - - ResourceMapping metadata = getMappingFor(property); - return metadata != null && metadata.isExported(); - } - - /* - * (non-Javadoc) - * @see java.lang.Iterable#iterator() - */ - @Override - public Iterator iterator() { - return cache.values().iterator(); - } - - /** - * Special resource mapping for {@link PersistentProperty} instances. - * - * @author Oliver Gierke - */ - static class PersistentPropertyResourceMapping implements ResourceMapping { - - private final PersistentProperty property; - private final ResourceMapping typeMapping; - private final CollectionResourceMapping ownerTypeMapping; - private final RestResource annotation; - private final Description description; - - /** - * Creates a new {@link PersistentPropertyResourceMapping}. - * - * @param property must not be {@literal null}. - * @param exported whether the property is exported or not. - */ - public PersistentPropertyResourceMapping(PersistentProperty property, ResourceMapping typeMapping, - CollectionResourceMapping ownerTypeMapping) { - - Assert.notNull(property, "PersistentProperty must not be null!"); - - this.property = property; - this.typeMapping = typeMapping; - this.ownerTypeMapping = ownerTypeMapping; - this.annotation = property.isAssociation() ? property.findAnnotation(RestResource.class) : null; - this.description = property.findAnnotation(Description.class); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.core.mapping.ResourceMapping#getPath() - */ - @Override - public Path getPath() { - return annotation != null && StringUtils.hasText(annotation.path()) ? new Path(annotation.path()) : new Path( - property.getName()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.core.mapping.ResourceMapping#getRel() - */ - @Override - public String getRel() { - return annotation != null && StringUtils.hasText(annotation.rel()) ? annotation.rel() : property.getName(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.core.mapping.ResourceMapping#isExported() - */ - @Override - public boolean isExported() { - - if (typeMapping == null) { - return false; - } - - return !typeMapping.isExported() ? false : annotation == null ? true : annotation.exported(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.core.mapping.ResourceMapping#isPagingResource() - */ - @Override - public boolean isPagingResource() { - return false; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.core.mapping.ResourceMapping#getDescription() - */ - @Override - public ResourceDescription getDescription() { - - ResourceDescription fallback = SimpleResourceDescription.defaultFor(property, - ownerTypeMapping.getItemResourceRel()); - - if (description != null) { - return new AnnotationBasedResourceDescription(description, fallback); - } - - if (annotation != null) { - return new AnnotationBasedResourceDescription(annotation.description(), fallback); - } - - return fallback; - } - } + boolean hasMappingFor(Class type); } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java index 24e3c1674..0ebf74435 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java @@ -15,11 +15,16 @@ */ package org.springframework.data.rest.core; +import java.util.Collections; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; @@ -37,11 +42,12 @@ import org.springframework.format.support.DefaultFormattingConversionService; @Import({ JpaRepositoryConfig.class }) public class RepositoryTestsConfig { - @Autowired private ApplicationContext appCtx; + @Autowired private ApplicationContext context; + @Autowired(required = false) List> mappingContexts = Collections.emptyList(); @Bean public Repositories repositories() { - return new Repositories(appCtx); + return new Repositories(context); } @SuppressWarnings("deprecation") @@ -70,8 +76,13 @@ public class RepositoryTestsConfig { return new DomainClassConverter(defaultConversionService()); } + @Bean + public PersistentEntities persistentEntities() { + return new PersistentEntities(mappingContexts); + } + @Bean public UriToEntityConverter uriToEntityConverter() { - return new UriToEntityConverter(repositories(), domainClassConverter()); + return new UriToEntityConverter(persistentEntities(), domainClassConverter()); } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/PersistentPropertyResourceMappingUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/PersistentPropertyResourceMappingUnitTests.java index 00a4f2c36..d98269fd9 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/PersistentPropertyResourceMappingUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/PersistentPropertyResourceMappingUnitTests.java @@ -30,7 +30,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.core.annotation.Description; import org.springframework.data.rest.core.annotation.RestResource; -import org.springframework.data.rest.core.mapping.ResourceMappings.PersistentPropertyResourceMapping; +import org.springframework.data.rest.core.mapping.RepositoryResourceMappings.PersistentPropertyResourceMapping; /** * Unit tests for {@link PersistentPropertyResourceMapping}. diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ResourceMappingsIntegrationTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ResourceMappingsIntegrationTests.java index d48aeba4f..f668646a5 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ResourceMappingsIntegrationTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ResourceMappingsIntegrationTests.java @@ -58,7 +58,7 @@ public class ResourceMappingsIntegrationTests { public void setUp() { Repositories repositories = new Repositories(factory); - this.mappings = new ResourceMappings(new RepositoryRestConfiguration(), repositories); + this.mappings = new RepositoryResourceMappings(new RepositoryRestConfiguration(), repositories); } @Test 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 1bc4444ea..386ebbefe 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 @@ -37,6 +37,8 @@ import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.env.Environment; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.UriToEntityConverter; @@ -47,6 +49,7 @@ import org.springframework.data.rest.core.event.AnnotatedHandlerBeanPostProcesso import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener; import org.springframework.data.rest.core.invoke.DefaultRepositoryInvokerFactory; import org.springframework.data.rest.core.invoke.RepositoryInvokerFactory; +import org.springframework.data.rest.core.mapping.RepositoryResourceMappings; import org.springframework.data.rest.core.mapping.ResourceDescription; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.projection.ProxyProjectionFactory; @@ -132,6 +135,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon @Autowired Environment environment; @Autowired(required = false) List> resourceProcessors = Collections.emptyList(); + @Autowired(required = false) List> mappingContexts = Collections.emptyList(); @Autowired(required = false) RelProvider relProvider; @Autowired(required = false) CurieProvider curieProvider; @@ -145,6 +149,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return new RepositoryRelProvider(resourceMappings); } + @Bean + public PersistentEntities persistentEntities() { + return new PersistentEntities(mappingContexts); + } + @Bean @Qualifier public DefaultFormattingConversionService defaultConversionService() { @@ -162,7 +171,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon @Bean public UriToEntityConverter uriToEntityConverter() { - return new UriToEntityConverter(repositories(), domainClassConverter()); + return new UriToEntityConverter(persistentEntities(), domainClassConverter()); } /** @@ -294,7 +303,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() { - return new PersistentEntityToJsonSchemaConverter(repositories(), resourceMappings(), + return new PersistentEntityToJsonSchemaConverter(persistentEntities(), resourceMappings(), resourceDescriptionMessageSourceAccessor(), entityLinks()); } @@ -433,7 +442,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon Repositories repositories = repositories(); RepositoryRestConfiguration config = config(); - return new ResourceMappings(config, repositories); + return new RepositoryResourceMappings(config, repositories); } /** @@ -443,7 +452,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public Module persistentEntityJackson2Module() { - return new PersistentEntityJackson2Module(resourceMappings(), repositories(), config(), uriToEntityConverter()); + return new PersistentEntityJackson2Module(resourceMappings(), persistentEntities(), config(), + uriToEntityConverter()); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index 44d596178..d66b48fc5 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -25,18 +25,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.SimpleAssociationHandler; +import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.core.UriToEntityConverter; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; -import org.springframework.data.rest.core.mapping.ResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMappings; -import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.PersistentEntityResource; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; +import org.springframework.data.rest.webmvc.mapping.LinkCollectingAssociationHandler; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.util.Assert; @@ -84,46 +83,25 @@ public class PersistentEntityJackson2Module extends SimpleModule { * , {@link RepositoryRestConfiguration} and {@link UriToEntityConverter}. * * @param mappings must not be {@literal null}. - * @param repositories must not be {@literal null}. + * @param entities must not be {@literal null}. * @param config must not be {@literal null}. * @param converter must not be {@literal null}. */ - public PersistentEntityJackson2Module(ResourceMappings mappings, Repositories repositories, + public PersistentEntityJackson2Module(ResourceMappings mappings, PersistentEntities entities, RepositoryRestConfiguration config, UriToEntityConverter converter) { super(new Version(2, 0, 0, null, "org.springframework.data.rest", "jackson-module")); Assert.notNull(mappings, "ResourceMappings must not be null!"); - Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(entities, "Repositories must not be null!"); Assert.notNull(config, "RepositoryRestConfiguration must not be null!"); Assert.notNull(converter, "UriToEntityConverter must not be null!"); - addSerializer(new PersistentEntityResourceSerializer(mappings)); - setSerializerModifier(new AssociationOmittingSerializerModifier(repositories, mappings, config)); - setDeserializerModifier(new AssociationUriResolvingDeserializerModifier(repositories, converter, mappings)); - } + AssociationLinks associationLinks = new AssociationLinks(mappings); - public static boolean maybeAddAssociationLink(Path path, ResourceMappings mappings, - PersistentProperty persistentProperty, List links) { - - Assert.isTrue(persistentProperty.isAssociation(), "PersistentProperty must be an association!"); - ResourceMetadata ownerMetadata = mappings.getMappingFor(persistentProperty.getOwner().getType()); - - if (!ownerMetadata.isManagedResource(persistentProperty)) { - return false; - } - - ResourceMapping propertyMapping = ownerMetadata.getMappingFor(persistentProperty); - - if (propertyMapping.isExported()) { - - links.add(new Link(path.slash(propertyMapping.getPath()).toString(), propertyMapping.getRel())); - // This is an association. We added a Link. - return true; - } - - // This is not an association. No Link was added. - return false; + addSerializer(new PersistentEntityResourceSerializer(entities, associationLinks)); + setSerializerModifier(new AssociationOmittingSerializerModifier(entities, associationLinks, config)); + setDeserializerModifier(new AssociationUriResolvingDeserializerModifier(entities, converter, associationLinks)); } /** @@ -134,21 +112,26 @@ public class PersistentEntityJackson2Module extends SimpleModule { */ private static class PersistentEntityResourceSerializer extends StdSerializer> { - private final ResourceMappings mappings; + private final PersistentEntities entities; + private final AssociationLinks associationLinks; /** - * Creates a new {@link PersistentEntityResourceSerializer} using the given {@link ResourceMappings}. + * Creates a new {@link PersistentEntityResourceSerializer} using the given {@link PersistentEntities} and + * {@link AssociationLinks}. * - * @param mappings must not be {@literal null}. + * @param entities must not be {@literal null}. + * @param links must not be {@literal null}. */ @SuppressWarnings({ "unchecked", "rawtypes" }) - private PersistentEntityResourceSerializer(ResourceMappings mappings) { + private PersistentEntityResourceSerializer(PersistentEntities entities, AssociationLinks links) { super((Class) PersistentEntityResource.class); - Assert.notNull(mappings, "ResourceMappings must not be null!"); + Assert.notNull(entities, "PersistentEntities must not be null!"); + Assert.notNull(links, "AssociationLinks must not be null!"); - this.mappings = mappings; + this.associationLinks = links; + this.entities = entities; } /* @@ -169,23 +152,14 @@ public class PersistentEntityJackson2Module extends SimpleModule { throw new JsonGenerationException(String.format("No self link found resource %s!", resource)); } - final List links = new ArrayList(); + Path basePath = new Path(id.expand().getHref()); + LinkCollectingAssociationHandler associationHandler = new LinkCollectingAssociationHandler(entities, basePath, + associationLinks); + resource.getPersistentEntity().doWithAssociations(associationHandler); + + List links = new ArrayList(); links.addAll(resource.getLinks()); - - // Add associations as links - resource.getPersistentEntity().doWithAssociations(new SimpleAssociationHandler() { - - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association) - */ - @Override - public void doWithAssociation(Association> association) { - - PersistentProperty property = association.getInverse(); - maybeAddAssociationLink(new Path(id.expand().getHref()), mappings, property, links); - } - }); + links.addAll(associationHandler.getLinks()); Resource resourceToRender = new Resource(resource.getContent(), links); provider.defaultSerializeValue(resourceToRender, jgen); @@ -199,24 +173,28 @@ public class PersistentEntityJackson2Module extends SimpleModule { */ private static class AssociationOmittingSerializerModifier extends BeanSerializerModifier { - private final Repositories repositories; - private final ResourceMappings mappings; + private final PersistentEntities entities; private final RepositoryRestConfiguration configuration; + private final AssociationLinks associationLinks; /** - * Creates a new {@link AssociationOmittingSerializerModifier} for the given {@link Repositories}, - * {@link ResourceMappings} and {@link RepositoryRestConfiguration}. + * Creates a new {@link AssociationOmittingSerializerModifier} for the given {@link PersistentEntities}, + * {@link AssociationLinks} and {@link RepositoryRestConfiguration}. * - * @param repositories must not be {@literal null}. - * @param mappings must not be {@literal null}. + * @param entities must not be {@literal null}. + * @param associationLinks must not be {@literal null}. * @param configuration must not be {@literal null}. */ - private AssociationOmittingSerializerModifier(Repositories repositories, ResourceMappings mappings, + private AssociationOmittingSerializerModifier(PersistentEntities entities, AssociationLinks associationLinks, RepositoryRestConfiguration configuration) { - this.repositories = repositories; - this.mappings = mappings; + Assert.notNull(entities, "PersistentEntities must not be null!"); + Assert.notNull(associationLinks, "AssociationLinks must not be null!"); + Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!"); + + this.entities = entities; this.configuration = configuration; + this.associationLinks = associationLinks; } /* @@ -227,30 +205,30 @@ public class PersistentEntityJackson2Module extends SimpleModule { public BeanSerializerBuilder updateBuilder(SerializationConfig config, BeanDescription beanDesc, BeanSerializerBuilder builder) { - PersistentEntity entity = repositories.getPersistentEntity(beanDesc.getBeanClass()); + PersistentEntity entity = entities.getPersistentEntity(beanDesc.getBeanClass()); if (entity == null) { return builder; } List result = new ArrayList(); - ResourceMetadata resourceMetadata = mappings.getMappingFor(entity.getType()); for (BeanPropertyWriter writer : builder.getProperties()) { + // Skip exported associations PersistentProperty persistentProperty = entity.getPersistentProperty(writer.getName()); - if (persistentProperty != null) { + if (persistentProperty == null) { + continue; + } - // Skip exported associations - if (persistentProperty.isAssociation() && resourceMetadata.isExported(persistentProperty)) { - continue; - } + if (associationLinks.isLinkableAssociation(persistentProperty)) { + continue; + } - // Skip ids unless explicitly configured to expose - if (persistentProperty.isIdProperty() && !configuration.isIdExposedFor(entity.getType())) { - continue; - } + // Skip ids unless explicitly configured to expose + if (persistentProperty.isIdProperty() && !configuration.isIdExposedFor(entity.getType())) { + continue; } result.add(writer); @@ -269,30 +247,30 @@ public class PersistentEntityJackson2Module extends SimpleModule { * * @author Oliver Gierke */ - private static class AssociationUriResolvingDeserializerModifier extends BeanDeserializerModifier { + public static class AssociationUriResolvingDeserializerModifier extends BeanDeserializerModifier { private final UriToEntityConverter converter; - private final Repositories repositories; - private final ResourceMappings mappings; + private final PersistentEntities repositories; + private final AssociationLinks associationLinks; /** * Creates a new {@link AssociationUriResolvingDeserializerModifier} using the given {@link Repositories}, - * {@link UriToEntityConverter} and {@link ResourceMappings}. + * {@link UriToEntityConverter} and {@link AssociationLinks}. * * @param repositories must not be {@literal null}. * @param converter must not be {@literal null}. * @param mappings must not be {@literal null}. */ - public AssociationUriResolvingDeserializerModifier(Repositories repositories, UriToEntityConverter converter, - ResourceMappings mappings) { + public AssociationUriResolvingDeserializerModifier(PersistentEntities repositories, UriToEntityConverter converter, + AssociationLinks associationLinks) { Assert.notNull(repositories, "Repositories must not be null!"); Assert.notNull(converter, "UriToEntityConverter must not be null!"); - Assert.notNull(mappings, "ResourceMappings must not be null!"); + Assert.notNull(associationLinks, "AssociationLinks must not be null!"); this.repositories = repositories; this.converter = converter; - this.mappings = mappings; + this.associationLinks = associationLinks; } /* @@ -305,7 +283,6 @@ public class PersistentEntityJackson2Module extends SimpleModule { Iterator properties = builder.getProperties(); PersistentEntity entity = repositories.getPersistentEntity(beanDesc.getBeanClass()); - ResourceMetadata metadata = mappings.getMappingFor(beanDesc.getBeanClass()); if (entity == null) { return builder; @@ -315,9 +292,8 @@ public class PersistentEntityJackson2Module extends SimpleModule { SettableBeanProperty property = properties.next(); PersistentProperty persistentProperty = entity.getPersistentProperty(property.getName()); - ResourceMapping propertyMapping = metadata.getMappingFor(persistentProperty); - if (!persistentProperty.isAssociation() || !propertyMapping.isExported()) { + if (!associationLinks.isLinkableAssociation(persistentProperty)) { continue; } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java index e05c7d5af..8f3aeea79 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java @@ -15,12 +15,9 @@ */ package org.springframework.data.rest.webmvc.json; -import static org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.*; import static org.springframework.util.StringUtils.*; -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Set; import javax.validation.constraints.NotNull; @@ -28,12 +25,10 @@ import javax.validation.constraints.NotNull; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.SimpleAssociationHandler; import org.springframework.data.mapping.SimplePropertyHandler; -import org.springframework.data.repository.support.Repositories; +import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.core.mapping.ResourceDescription; import org.springframework.data.rest.core.mapping.ResourceMapping; @@ -41,6 +36,9 @@ import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.json.JsonSchema.ArrayProperty; import org.springframework.data.rest.webmvc.json.JsonSchema.Property; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; +import org.springframework.data.rest.webmvc.mapping.LinkCollectingAssociationHandler; +import org.springframework.data.util.TypeInformation; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; import org.springframework.util.Assert; @@ -56,31 +54,33 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric private final Set convertiblePairs = new HashSet(); private final ResourceMappings mappings; - private final Repositories repositories; + private final PersistentEntities repositories; private final MessageSourceAccessor accessor; private final EntityLinks entityLinks; /** - * Creates a new {@link PersistentEntityToJsonSchemaConverter} for the given {@link Repositories} and + * Creates a new {@link PersistentEntityToJsonSchemaConverter} for the given {@link PersistentEntities} and * {@link ResourceMappings}. * - * @param repositories must not be {@literal null}. + * @param entities must not be {@literal null}. * @param mappings must not be {@literal null}. * @param accessor */ - public PersistentEntityToJsonSchemaConverter(Repositories repositories, ResourceMappings mappings, + public PersistentEntityToJsonSchemaConverter(PersistentEntities entities, ResourceMappings mappings, MessageSourceAccessor accessor, EntityLinks entityLinks) { - Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(entities, "PersistentEntities must not be null!"); Assert.notNull(mappings, "ResourceMappings must not be null!"); + Assert.notNull(accessor, "MessageSourceAccessor must not be null!"); + Assert.notNull(entityLinks, "EntityLinks must not be null!"); - this.repositories = repositories; + this.repositories = entities; this.mappings = mappings; this.accessor = accessor; this.entityLinks = entityLinks; - for (Class domainType : repositories) { - convertiblePairs.add(new ConvertiblePair(domainType, JsonSchema.class)); + for (TypeInformation domainType : entities.getManagedTypes()) { + convertiblePairs.add(new ConvertiblePair(domainType.getType(), JsonSchema.class)); } } @@ -145,29 +145,13 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric } }); - final List links = new ArrayList(); + Link link = entityLinks.linkToCollectionResource(persistentEntity.getType()).expand(); - persistentEntity.doWithAssociations(new SimpleAssociationHandler() { + LinkCollectingAssociationHandler associationHandler = new LinkCollectingAssociationHandler(repositories, new Path( + link.getHref()), new AssociationLinks(mappings)); + persistentEntity.doWithAssociations(associationHandler); - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.AssociationHandler#doWithAssociation(org.springframework.data.mapping.Association) - */ - @Override - public void doWithAssociation(Association> association) { - - PersistentProperty persistentProperty = association.getInverse(); - - if (!metadata.isExported(persistentProperty)) { - return; - } - - Link link = entityLinks.linkToCollectionResource(persistentEntity.getType()); - maybeAddAssociationLink(new Path(link.getHref()), mappings, persistentProperty, links); - } - }); - - jsonSchema.add(links); + jsonSchema.add(associationHandler.getLinks()); return jsonSchema; } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/AssociationLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/AssociationLinks.java new file mode 100644 index 000000000..df7e5ab79 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/AssociationLinks.java @@ -0,0 +1,102 @@ +/* + * Copyright 2014 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.webmvc.mapping; + +import java.util.Collections; +import java.util.List; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.rest.core.Path; +import org.springframework.data.rest.core.mapping.ResourceMapping; +import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.hateoas.Link; +import org.springframework.util.Assert; + +/** + * A value object to for {@link Link}s representing an association. + * + * @author Oliver Gierke + * @since 2.1 + */ +public class AssociationLinks { + + private final ResourceMappings mappings; + private final PropertyMappings propertyMappings; + + /** + * Creates a new {@link AssociationLinks} using the given {@link ResourceMappings}. + * + * @param mappings must not be {@literal null}. + */ + public AssociationLinks(ResourceMappings mappings) { + + Assert.notNull(mappings, "ResourceMappings must not be null!"); + + this.propertyMappings = new PropertyMappings(mappings); + this.mappings = mappings; + } + + /** + * Returns the links to render for the given {@link Association}. + * + * @param association must not be {@literal null}. + * @param path must not be {@literal null}. + * @return + */ + public List getLinksFor(Association> association, Path path) { + + Assert.notNull(association, "Association must not be null!"); + Assert.notNull(path, "Base path must not be null!"); + + PersistentProperty property = association.getInverse(); + + if (isLinkableAssociation(property)) { + + ResourceMapping propertyMapping = propertyMappings.getMappingFor(property); + + String href = path.slash(propertyMapping.getPath()).toString(); + String rel = propertyMapping.getRel(); + + return Collections.singletonList(new Link(href, rel)); + } + + return Collections.emptyList(); + } + + /** + * Returns whether the given property is an association that is linkable. + * + * @param property can be {@literal null}. + * @return + */ + public boolean isLinkableAssociation(PersistentProperty property) { + + if (property == null || !property.isAssociation()) { + return false; + } + + ResourceMetadata metadata = mappings.getMappingFor(property.getOwner().getType()); + + if (metadata != null && !metadata.isExported(property)) { + return false; + } + + metadata = mappings.getMappingFor(property.getActualType()); + return metadata == null ? false : metadata.isExported(); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollectingAssociationHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollectingAssociationHandler.java new file mode 100644 index 000000000..63f2bdd33 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollectingAssociationHandler.java @@ -0,0 +1,104 @@ +/* + * Copyright 2014 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.webmvc.mapping; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.SimpleAssociationHandler; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.rest.core.Path; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; +import org.springframework.util.Assert; + +/** + * {@link SimpleAssociationHandler} that will collect {@link Link}s for all linkable associations. + * + * @author Oliver Gierke + * @since 2.1 + */ +public class LinkCollectingAssociationHandler implements SimpleAssociationHandler { + + private static final String AMBIGUOUS_ASSOCIATIONS = "Detected multiple association links with same relation type! Disambiguate association %s using @RestResource!"; + + private final PersistentEntities entities; + private final AssociationLinks associationLinks; + private final Path basePath; + + private final List links; + + /** + * Creates a new {@link LinkCollectingAssociationHandler} for the given {@link PersistentEntities}, {@link Path} and + * {@link AssociationLinks}. + * + * @param entities must not be {@literal null}. + * @param path must not be {@literal null}. + * @param associationLinks must not be {@literal null}. + */ + public LinkCollectingAssociationHandler(PersistentEntities entities, Path path, AssociationLinks associationLinks) { + + Assert.notNull(entities, "PersistentEntities must not be null!"); + Assert.notNull(path, "Path must not be null!"); + Assert.notNull(associationLinks, "AssociationLinks must not be null!"); + + this.entities = entities; + this.associationLinks = associationLinks; + this.basePath = path; + + this.links = new ArrayList(); + } + + /** + * Returns the links collected after the {@link Association} has been traversed. + * + * @return the links + */ + public List getLinks() { + return links; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association) + */ + @Override + public void doWithAssociation(final Association> association) { + + PersistentProperty property = association.getInverse(); + + if (associationLinks.isLinkableAssociation(property)) { + + Links existingLinks = new Links(links); + + for (Link link : associationLinks.getLinksFor(association, basePath)) { + if (existingLinks.hasLink(link.getRel())) { + throw new MappingException(String.format(AMBIGUOUS_ASSOCIATIONS, property.toString())); + } else { + links.add(link); + } + } + + } else { + PersistentEntity associationEntity = entities.getPersistentEntity(property.getActualType()); + associationEntity.doWithAssociations(this); + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/PropertyMappings.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/PropertyMappings.java new file mode 100644 index 000000000..cd9ba5313 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/PropertyMappings.java @@ -0,0 +1,82 @@ +/* + * Copyright 2014 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.webmvc.mapping; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.rest.core.mapping.MappingResourceMetadata; +import org.springframework.data.rest.core.mapping.ResourceMapping; +import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.util.Assert; + +/** + * Value object for {@link ResourceMapping}s for {@link PersistentProperty} instances. + * + * @author Oliver Gierke + * @see 2.1 + */ +public class PropertyMappings { + + private final ResourceMappings resourceMappings; + private final Map, ResourceMetadata> resourceMetadata; + + /** + * Creates a new {@link PropertyMappings} instance for the given {@link ResourceMappings}. + * + * @param resourceMappings + */ + public PropertyMappings(ResourceMappings resourceMappings) { + + Assert.notNull(resourceMappings, "ResourceMappings must not be null!"); + + this.resourceMappings = resourceMappings; + this.resourceMetadata = new HashMap, ResourceMetadata>(); + } + + /** + * Returns the {@link ResourceMapping} for the given {@link PersistentProperty}. + * + * @param property can be {@literal null}. + * @return + */ + public ResourceMapping getMappingFor(PersistentProperty property) { + + Assert.notNull(property, "PersistentProperty must not be null!"); + + ResourceMetadata metadata = resourceMetadata.get(property); + + if (metadata != null) { + return metadata.getMappingFor(property); + } + + metadata = resourceMappings.getMappingFor(property.getOwner().getType()); + + if (metadata != null) { + return cacheAndReturn(metadata, property); + } + + return cacheAndReturn(new MappingResourceMetadata(property.getOwner()), property); + } + + private ResourceMapping cacheAndReturn(ResourceMetadata metadata, PersistentProperty property) { + + resourceMetadata.put(property, metadata); + return metadata.getMappingFor(property); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AssociationLinksUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AssociationLinksUnitTests.java new file mode 100644 index 000000000..05a5678e3 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AssociationLinksUnitTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2014 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.webmvc; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.annotation.Reference; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.rest.core.Path; +import org.springframework.data.rest.core.annotation.RestResource; +import org.springframework.data.rest.core.mapping.MappingResourceMetadata; +import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; +import org.springframework.hateoas.Link; + +/** + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class AssociationLinksUnitTests { + + AssociationLinks links; + + @Mock ResourceMappings mappings; + + MongoMappingContext mappingContext; + MongoPersistentEntity entity; + ResourceMetadata sampleResourceMetadata; + + @Before + public void setUp() { + + this.links = new AssociationLinks(mappings); + + this.mappingContext = new MongoMappingContext(); + this.entity = mappingContext.getPersistentEntity(Sample.class); + this.sampleResourceMetadata = new MappingResourceMetadata(entity); + + when(mappings.getMappingFor(Sample.class)).thenReturn(sampleResourceMetadata); + } + + /** + * @see DATAREST-262 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullMappings() { + new AssociationLinks(null); + } + + /** + * @see DATAREST-262 + */ + @Test + public void considersNullPropertyUnlinkable() { + assertThat(links.isLinkableAssociation(null), is(false)); + } + + /** + * @see DATAREST-262 + */ + @Test + public void consideredExportedPropertyLinkable() { + assertThat(links.isLinkableAssociation(exposeProperty("property")), is(true)); + } + + /** + * @see DATAREST-262 + */ + @Test + public void consideredHiddenPropertyUnlinkable() { + assertThat(links.isLinkableAssociation(exposeProperty("hiddenProperty")), is(false)); + } + + /** + * @see DATAREST-262 + */ + @Test + public void considersUnexportedPropertyUnlinkable() { + + MongoPersistentProperty property = entity.getPersistentProperty("unexportedProperty"); + assertThat(links.isLinkableAssociation(property), is(false)); + } + + /** + * @see DATAREST-262 + */ + @Test + public void createsLinkToAssociationProperty() { + + PersistentProperty property = exposeProperty("property"); + List associationLinks = links.getLinksFor(property.getAssociation(), new Path("/base")); + + assertThat(associationLinks, hasSize(1)); + assertThat(associationLinks, hasItem(new Link("/base/property", "property"))); + } + + /** + * @see DATAREST-262 + */ + @Test + public void doesNotCreateLinksForHiddenProperty() { + + PersistentProperty property = exposeProperty("hiddenProperty"); + assertThat(links.getLinksFor(property.getAssociation(), new Path("/sample")), hasSize(0)); + } + + /** + * @see DATAREST-262 + */ + @Test + public void doesNotCreateLinksForUnexportedProperty() { + + PersistentProperty property = entity.getPersistentProperty("unexportedProperty"); + assertThat(links.getLinksFor(property.getAssociation(), new Path("/sample")), hasSize(0)); + } + + private PersistentProperty exposeProperty(String name) { + + MongoPersistentProperty property = entity.getPersistentProperty(name); + ResourceMetadata metadata = new MappingResourceMetadata(mappingContext.getPersistentEntity(property)); + + when(mappings.getMappingFor(property.getActualType())).thenReturn(metadata); + + return property; + } + + public static class Sample { + + @Reference Property property; + @RestResource(exported = false) @Reference Property hiddenProperty; + @Reference UnexportedProperty unexportedProperty; + } + + public static class Property { + + } + + public static class UnexportedProperty { + + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/gemfire/GemfireRepositoryConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/gemfire/GemfireRepositoryConfig.java index 76395bd22..cc2d542f4 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/gemfire/GemfireRepositoryConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/gemfire/GemfireRepositoryConfig.java @@ -15,9 +15,13 @@ */ package org.springframework.data.rest.webmvc.gemfire; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; +import org.springframework.data.gemfire.mapping.GemfireMappingContext; +import org.springframework.data.gemfire.mapping.Region; import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories; +import org.springframework.data.util.AnnotatedTypeScanner; /** * Spring JavaConfig configuration class to setup a Spring container and infrastructure components. @@ -30,4 +34,19 @@ import org.springframework.data.gemfire.repository.config.EnableGemfireRepositor @EnableGemfireRepositories public class GemfireRepositoryConfig { + /** + * TODO: Remove, once Spring Data Gemfire exposes a mapping context. + */ + @Bean + @SuppressWarnings("unchecked") + public GemfireMappingContext gemfireMappingContext() { + + AnnotatedTypeScanner scanner = new AnnotatedTypeScanner(Region.class); + + GemfireMappingContext context = new GemfireMappingContext(); + context.setInitialEntitySet(scanner.findTypes(GemfireRepositoryConfig.class.getPackage().getName())); + context.initialize(); + + return context; + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java new file mode 100644 index 000000000..35aea2b35 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2014 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.webmvc.jpa; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import javax.persistence.Embeddable; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.validation.constraints.NotNull; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.data.jpa.mapping.JpaPersistentEntity; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.rest.webmvc.PersistentEntityResource; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; + +/** + * Integration tests for DATAREST-262, checking serialization and deserialization of associations within embeddables. + * + * @author Oliver Gierke + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class DataRest262Tests { + + @Configuration + @Import({ RepositoryRestMvcConfiguration.class, JpaInfrastructureConfig.class }) + @EnableJpaRepositories(considerNestedRepositories = true) + static class Config { + + } + + @Autowired ApplicationContext beanFactory; + @Autowired JpaMetamodelMappingContext mappingContext; + @Autowired AirportRepository repository; + @Autowired @Qualifier("halObjectMapper") ObjectMapper mapper; + + @Before + public void setUp() { + mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY); + } + + /** + * @see DATAREST-262 + */ + @Test + public void deserializesNestedAssociation() throws Exception { + + Airport airport = repository.save(new Airport()); + String payload = "{\"orgOrDstFlightPart\":{\"airport\":\"/api/airports/" + airport.id + "\"}}"; + + AircraftMovement result = mapper.readValue(payload, AircraftMovement.class); + assertThat(result.orgOrDstFlightPart.airport.id, is(airport.id)); + } + + /** + * @see DATAREST-262 + */ + @Test + public void serializesLinksToNestedAssociations() throws Exception { + + Airport first = new Airport(); + first.id = 1L; + + Airport second = new Airport(); + second.id = 2L; + + FlightPart part = new FlightPart(); + part.airport = second; + + AircraftMovement movement = new AircraftMovement(); + movement.id = 3L; + movement.originOrDestinationAirport = first; + movement.orgOrDstFlightPart = part; + + JpaPersistentEntity persistentEntity = mappingContext.getPersistentEntity(AircraftMovement.class); + + Resource resource = PersistentEntityResource.wrap(persistentEntity, movement, new Link( + "/api/airports/" + movement.id)); + + String result = mapper.writeValueAsString(resource); + + assertThat(JsonPath.read(result, "$_links.self"), is(notNullValue())); + assertThat(JsonPath.read(result, "$_links.airport"), is(notNullValue())); + assertThat(JsonPath.read(result, "$_links.originOrDestinationAirport"), is(notNullValue())); + } + + public interface AircraftMovementRepository extends CrudRepository { + + } + + public interface AirportRepository extends CrudRepository { + + } + + @Entity(name = "aircraftmovement") + public static class AircraftMovement { + + @Id @GeneratedValue Long id; + @ManyToOne Airport originOrDestinationAirport; + @Embedded @NotNull FlightPart orgOrDstFlightPart; + } + + @Embeddable + public static class FlightPart { + @ManyToOne Airport airport; + } + + @Entity(name = "airport") + public static class Airport { + @Id @GeneratedValue Long id; + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaInfrastructureConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaInfrastructureConfig.java new file mode 100644 index 000000000..3181b94e0 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaInfrastructureConfig.java @@ -0,0 +1,63 @@ +/* + * Copyright 2014 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.webmvc.jpa; + +import javax.sql.DataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * @author Oliver Gierke + */ +@Configuration +public class JpaInfrastructureConfig { + + @Bean + public DataSource dataSource() { + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setDatabase(Database.HSQL); + vendorAdapter.setGenerateDdl(true); + + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setJpaVendorAdapter(vendorAdapter); + factory.setPackagesToScan(getClass().getPackage().getName()); + factory.setPersistenceUnitName("spring-data-rest-webmvc"); + factory.setDataSource(dataSource()); + factory.afterPropertiesSet(); + + return factory; + } + + @Bean + public PlatformTransactionManager transactionManager() { + return new JpaTransactionManager(); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java index d5cf50ccd..ecca733c8 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java @@ -15,19 +15,9 @@ */ package org.springframework.data.rest.webmvc.jpa; -import javax.sql.DataSource; - import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.Database; -import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; -import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; /** @@ -35,36 +25,12 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; * @author Oliver Gierke */ @Configuration -@ComponentScan @EnableJpaRepositories @EnableTransactionManagement -public class JpaRepositoryConfig { +public class JpaRepositoryConfig extends JpaInfrastructureConfig { @Bean - public DataSource dataSource() { - EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); - return builder.setType(EmbeddedDatabaseType.HSQL).build(); - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - - HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); - vendorAdapter.setDatabase(Database.HSQL); - vendorAdapter.setGenerateDdl(true); - - LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); - factory.setJpaVendorAdapter(vendorAdapter); - factory.setPackagesToScan(getClass().getPackage().getName()); - factory.setPersistenceUnitName("spring-data-rest-webmvc"); - factory.setDataSource(dataSource()); - factory.afterPropertiesSet(); - - return factory; - } - - @Bean - public PlatformTransactionManager transactionManager() { - return new JpaTransactionManager(); + public TestDataPopulator testDataPopulator() { + return new TestDataPopulator(); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java index c35fa2b23..15750d0da 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java @@ -3,28 +3,16 @@ package org.springframework.data.rest.webmvc.jpa; import java.util.Arrays; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; /** * @author Jon Brisbin */ -@Component public class TestDataPopulator { - private final PersonRepository people; - private final OrderRepository orders; - private final AuthorRepository authorRepository; - private final BookRepository books; - - @Autowired - public TestDataPopulator(PersonRepository people, OrderRepository orders, AuthorRepository authors, - BookRepository books) { - - this.people = people; - this.orders = orders; - this.authorRepository = authors; - this.books = books; - } + @Autowired private PersonRepository people; + @Autowired private OrderRepository orders; + @Autowired private AuthorRepository authorRepository; + @Autowired private BookRepository books; public void populateRepositories() { diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java index 12461432a..4b3dd2f43 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java @@ -49,6 +49,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.util.UriTemplate; import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; /** * Integration tests for entity (de)serialization. @@ -187,10 +188,9 @@ public class PersistentEntitySerializationTests { PagedResources> persistentEntityResource = new PagedResources>( Arrays.asList(userResource), new PageMetadata(1, 0, 10)); - assertThat( - mapper.writeValueAsString(persistentEntityResource), - is("{\"_embedded\":{\"users\":[{\"address\":{\"street\":\"Street\"},\"_links\":{\"self\":{\"href\":\"/users/1\"}}}]}," - + "\"page\":{\"size\":1,\"totalElements\":10,\"totalPages\":10,\"number\":0}}")); + String result = mapper.writeValueAsString(persistentEntityResource); + + assertThat(JsonPath.read(result, "$_embedded.users[*].address"), is(notNullValue())); } /** @@ -213,9 +213,8 @@ public class PersistentEntitySerializationTests { PagedResources> persistentEntityResource = new PagedResources>( Arrays.asList(orderResource), new PageMetadata(1, 0, 10)); - assertThat(mapper.writeValueAsString(persistentEntityResource), - is("{\"_embedded\":{\"orders\":[{\"lineItems\":[{\"name\":\"first\"},{\"name\":\"second\"}],\"price\":2.5" - + ",\"_links\":{\"self\":{\"href\":\"/orders/1\"},\"creator\":{\"href\":\"/orders/1/creator\"}}}]},\"" - + "page\":{\"size\":1,\"totalElements\":10,\"totalPages\":10,\"number\":0}}")); + String result = mapper.writeValueAsString(persistentEntityResource); + + assertThat(JsonPath.read(result, "$_embedded.orders[*].lineItems"), is(notNullValue())); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java index 63886333f..7f2c17b39 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java @@ -16,17 +16,21 @@ package org.springframework.data.rest.webmvc.json; import java.net.URI; +import java.util.Collections; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.UriToEntityConverter; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; -import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.RepositoryResourceMappings; import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; import org.springframework.data.rest.webmvc.jpa.Person; import org.springframework.data.rest.webmvc.jpa.PersonRepository; @@ -51,7 +55,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; @SuppressWarnings("deprecation") public class RepositoryTestsConfig { - @Autowired private ApplicationContext appCtx; + @Autowired ApplicationContext appCtx; + @Autowired(required = false) List> mappingContexts = Collections.emptyList(); @Bean public Repositories repositories() { @@ -87,15 +92,20 @@ public class RepositoryTestsConfig { return new DomainClassConverter(defaultConversionService()); } + @Bean + public PersistentEntities persistentEntities() { + return new PersistentEntities(mappingContexts); + } + @Bean public UriToEntityConverter uriToEntityConverter() { - return new UriToEntityConverter(repositories(), domainClassConverter()); + return new UriToEntityConverter(persistentEntities(), domainClassConverter()); } @Bean public Module persistentEntityModule() { - return new PersistentEntityJackson2Module(new ResourceMappings(config(), repositories()), repositories(), config(), - uriToEntityConverter()); + return new PersistentEntityJackson2Module(new RepositoryResourceMappings(config(), repositories()), + persistentEntities(), config(), uriToEntityConverter()); } @Bean diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoDbRepositoryConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoDbRepositoryConfig.java index b4743134f..0330be696 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoDbRepositoryConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoDbRepositoryConfig.java @@ -15,15 +15,11 @@ */ package org.springframework.data.rest.webmvc.mongodb; -import java.net.UnknownHostException; - -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.mongodb.MongoDbFactory; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.SimpleMongoDbFactory; +import org.springframework.data.mongodb.config.AbstractMongoConfiguration; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; +import com.mongodb.Mongo; import com.mongodb.MongoClient; /** @@ -31,15 +27,32 @@ import com.mongodb.MongoClient; */ @Configuration @EnableMongoRepositories -public class MongoDbRepositoryConfig { +public class MongoDbRepositoryConfig extends AbstractMongoConfiguration { - @Bean - public MongoDbFactory mongoDbFactory() throws UnknownHostException { - return new SimpleMongoDbFactory(new MongoClient("localhost"), "spring-data-rest-example"); + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.config.AbstractMongoConfiguration#getDatabaseName() + */ + @Override + protected String getDatabaseName() { + return "spring-data-rest-sample"; } - @Bean - public MongoTemplate mongoTemplate() throws UnknownHostException { - return new MongoTemplate(mongoDbFactory()); + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.config.AbstractMongoConfiguration#getMappingBasePackage() + */ + @Override + protected String getMappingBasePackage() { + return ""; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.config.AbstractMongoConfiguration#mongo() + */ + @Override + public Mongo mongo() throws Exception { + return new MongoClient(); } }