DATAREST-93 - Further fixes in repository mappings.

Simplified new RepositoryMappings infrastructure. Integrated EvoInflectionRelProvider to build collection resource rels. Bumped version number to 2.0 as we're going to break backwards compatibility with the next release to straighten out the rel construction and mapping.
This commit is contained in:
Oliver Gierke
2013-06-18 19:17:56 +02:00
parent 5c61632ec2
commit c5a592bd9d
26 changed files with 598 additions and 576 deletions

View File

@@ -25,6 +25,7 @@ ext {
hibernateVersion = "4.2.0.Final"
hibernateValidatorVersion = "4.3.1.Final"
hsqldbVersion = "2.2.9"
evoVersion="1.0.1"
// Supporting libraries
cglibVersion = "2.2.2"
@@ -182,6 +183,9 @@ project("spring-data-rest-repository") {
// JSR 303 Validation
compile("javax.validation:validation-api:1.0.0.GA", optional)
// Evo Inflector
runtime "org.atteo:evo-inflector:${evoVersion}"
// Testing
testCompile "org.hsqldb:hsqldb:$hsqldbVersion"
testCompile "org.hibernate:hibernate-entitymanager:$hibernateVersion"

View File

@@ -1 +1 @@
version = 1.1.0.BUILD-SNAPSHOT
version = 2.0.0.BUILD-SNAPSHOT

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2013 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;
import org.springframework.util.StringUtils;
/**
* Simple value object to build up (URI) paths. Allows easy concatenation of {@link String}s and will take care of
* removal of whitespace and reducing slashes to single ones.
*
* @author Oliver Gierke
*/
public class Path {
private static final String SLASH = "/";
private final String path;
/**
* Creates a new {@link Path} from the given {@link String}.
*
* @param path
*/
public Path(String path) {
this(path, true);
}
/**
* Creates a new {@link Path} from the given string and potentially bypasses the cleanup.
*
* @param path
* @param cleanUp
*/
private Path(String path, boolean cleanUp) {
this.path = cleanUp ? cleanUp(path) : path;
}
/**
* Appends the given {@link String} to the current {@link Path}.
*
* @param path
* @return
*/
public Path slash(String path) {
return new Path(this.path + cleanUp(path), false);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return path.hashCode();
}
private static String cleanUp(String path) {
if (!StringUtils.hasText(path)) {
return "";
}
String trimmed = path.trim().replaceAll(" ", "");
trimmed = SLASH + trimmed.substring(getFirstNoneSlashIndex(trimmed));
while (trimmed.endsWith("/")) {
trimmed = trimmed.substring(0, trimmed.length() - 1);
}
return trimmed;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return path;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Path)) {
return false;
}
Path that = (Path) obj;
return this.path.equals(that.path);
}
private static int getFirstNoneSlashIndex(String input) {
for (int i = 0; i < input.length(); i++) {
if (input.charAt(i) != '/') {
return i;
}
}
return input.length();
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2013 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;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.data.rest.core.Path;
/**
* @author Oliver Gierke
*/
public class PathUnitTests {
@Test
public void combinesSimplePaths() {
Path builder = new Path("foo").slash("bar");
assertThat(builder.toString(), is("/foo/bar"));
}
@Test
public void removesLeadingAndTrailingSlashes() {
Path builder = new Path("foo/").slash("/bar").slash("//foobar///");
assertThat(builder.toString(), is("/foo/bar/foobar"));
}
@Test
public void removesWhitespace() {
Path builder = new Path("foo/ ").slash("/ b a r").slash(" //foobar/// ");
assertThat(builder.toString(), is("/foo/bar/foobar"));
}
}

View File

@@ -1,114 +0,0 @@
/*
* Copyright 2013 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.repository.mapping;
import org.springframework.util.Assert;
class CollectionResourceMappingBuilder implements InternalMappingBuilder {
private final CollectionResourceMapping mapping;
public CollectionResourceMappingBuilder(CollectionResourceMapping mapping) {
this.mapping = mapping;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#withCollectionRel(java.lang.String)
*/
public CollectionResourceMappingBuilder withCollectionRel(String rel) {
SimpleCollectionResourceMapping newMapping = new SimpleCollectionResourceMapping(rel != null ? rel
: mapping.getRel(), mapping.getSingleResourceRel(), mapping.getPath(), mapping.isExported());
return new CollectionResourceMappingBuilder(newMapping);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#withSingleRel(java.lang.String)
*/
@Override
public CollectionResourceMappingBuilder withSingleRel(String rel) {
SimpleCollectionResourceMapping newMapping = new SimpleCollectionResourceMapping(mapping.getRel(),
rel != null ? rel : mapping.getSingleResourceRel(), mapping.getPath(), mapping.isExported());
return new CollectionResourceMappingBuilder(newMapping);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#withPath(java.lang.String)
*/
@Override
public CollectionResourceMappingBuilder withPath(String path) {
SimpleCollectionResourceMapping newMapping = new SimpleCollectionResourceMapping(mapping.getRel(),
mapping.getSingleResourceRel(), path != null ? path : mapping.getPath(), mapping.isExported());
return new CollectionResourceMappingBuilder(newMapping);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#withExposed(java.lang.Boolean)
*/
@Override
public CollectionResourceMappingBuilder withExposed(Boolean exported) {
SimpleCollectionResourceMapping newMapping = new SimpleCollectionResourceMapping(mapping.getRel(),
mapping.getSingleResourceRel(), mapping.getPath(), exported != null ? exported : mapping.isExported());
return new CollectionResourceMappingBuilder(newMapping);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#merge(org.springframework.data.rest.repository.mapping.CollectionResourceMapping)
*/
@Override
public InternalMappingBuilder merge(CollectionResourceMapping mapping) {
if (mapping == null) {
return this;
}
return withCollectionRel(mapping.getRel()). //
withSingleRel(mapping.getSingleResourceRel()). //
withPath(mapping.getPath()). //
withExposed(mapping.isExported());
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#getMapping()
*/
@Override
public CollectionResourceMapping getMapping() {
Assert.hasText(mapping.getRel(), "Rel must not be null or empty!");
return mapping;
}
/**
* @return the exported
*/
public Boolean isExported() {
return mapping.isExported();
}
}

View File

@@ -1,26 +0,0 @@
/*
* Copyright 2013 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.repository.mapping;
/**
* @author Oliver Gierke
*/
interface InternalMappingBuilder extends MappingBuilder {
InternalMappingBuilder merge(CollectionResourceMapping mapping);
CollectionResourceMapping getMapping();
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2013 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.repository.mapping;
/**
* SPI to allow users to register
*
* @author Oliver Gierke
*/
public interface MappingBuilder {
MappingBuilder withCollectionRel(String rel);
MappingBuilder withSingleRel(String rel);
MappingBuilder withPath(String path);
MappingBuilder withExposed(Boolean exposed);
}

View File

@@ -15,11 +15,14 @@
*/
package org.springframework.data.rest.repository.mapping;
import java.util.Map;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.AnnotationUtils;
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.util.Assert;
/**
@@ -29,7 +32,7 @@ public class RepositoryAwareResourceInformation implements ResourceMetadata {
private final Repositories repositories;
private final CollectionResourceMapping mapping;
private final ResourceMetadataProvider provider;
private final ResourceMappings provider;
private final RepositoryInformation repositoryInterface;
/**
@@ -38,7 +41,7 @@ public class RepositoryAwareResourceInformation implements ResourceMetadata {
* @param provider must not be {@literal null}.
*/
public RepositoryAwareResourceInformation(Repositories repositories, CollectionResourceMapping mapping,
ResourceMetadataProvider provider, RepositoryInformation repositoryInterface) {
ResourceMappings provider, RepositoryInformation repositoryInterface) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(mapping, "ResourceMapping must not be null!");
@@ -113,7 +116,17 @@ public class RepositoryAwareResourceInformation implements ResourceMetadata {
* @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getPath()
*/
@Override
public String getPath() {
public Path getPath() {
return mapping.getPath();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.ResourceMetadata#getSearchResourceMappings()
*/
@Override
public Map<String, ResourceMapping> getSearchResourceMappings() {
return provider.getSearchResourceMappings(repositoryInterface.getRepositoryInterface());
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2013 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.repository.mapping;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.data.rest.repository.support.RepositoriesUtils;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link CollectionResourceMapping} to be built from repository interfaces. Will inspect {@link RestResource}
* annotations on the repository interface but fall back to the mapping information of the managed domain type for
* defaults.
*
* @author Oliver Gierke
*/
public class RepositoryCollectionResourceMapping implements CollectionResourceMapping {
private final RestResource annotation;
private final CollectionResourceMapping domainTypeMapping;
public RepositoryCollectionResourceMapping(Class<?> repositoryType) {
this(repositoryType, new EvoInflectorRelProvider());
}
/**
* Creates a new {@link RepositoryCollectionResourceMapping} for the given repository using the given
* {@link RelProvider}.
*
* @param repositoryType must not be {@literal null}.
* @param relProvider must not be {@literal null}.
*/
public RepositoryCollectionResourceMapping(Class<?> repositoryType, RelProvider relProvider) {
Assert.isTrue(RepositoriesUtils.isRepositoryInterface(repositoryType), "Given type is not a repository!");
Assert.notNull(relProvider, "RelProvider must not be null!");
this.annotation = AnnotationUtils.findAnnotation(repositoryType, RestResource.class);
this.domainTypeMapping = new TypeBasedCollectionResourceMapping(RepositoriesUtils.getDomainType(repositoryType),
relProvider);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.ResourceMapping#getPath()
*/
@Override
public Path getPath() {
return annotation == null || !StringUtils.hasText(annotation.path()) ? domainTypeMapping.getPath()
: new Path(annotation.path());
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.ResourceMapping#getRel()
*/
@Override
public String getRel() {
return annotation == null || !StringUtils.hasText(annotation.rel()) ? domainTypeMapping.getRel() : annotation.rel();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getSingleResourceRel()
*/
@Override
public String getSingleResourceRel() {
return domainTypeMapping.getSingleResourceRel();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.ResourceMapping#isExported()
*/
@Override
public Boolean isExported() {
return annotation == null ? domainTypeMapping.isExported() : annotation.exported();
}
}

View File

@@ -18,7 +18,9 @@ package org.springframework.data.rest.repository.mapping;
import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.util.StringUtils;
/**
* A {@link RepositoryMethodResourceMapping} created from a {@link Method}.
@@ -29,20 +31,24 @@ public class RepositoryMethodResourceMapping implements ResourceMapping {
private final boolean isExported;
private final String rel;
private final String path;
private final Path path;
/**
* Creates a new {@link RepositoryMethodResourceMapping} for the given {@link Method}.
*
* @param method must not be {@literal null}.
*/
public RepositoryMethodResourceMapping(Method method) {
public RepositoryMethodResourceMapping(Method method, ResourceMapping resourceMapping) {
RestResource annotation = AnnotationUtils.findAnnotation(method, RestResource.class);
this.isExported = annotation != null ? annotation.exported() : true;
this.rel = annotation != null ? annotation.rel() : method.getName();
this.path = annotation != null ? annotation.path() : method.getName();
Path resourcePath = resourceMapping.getPath();
String toAppend = annotation == null || !StringUtils.hasText(annotation.path()) ? method.getName() : annotation
.path();
this.path = resourcePath.slash(toAppend);
}
/*
@@ -68,7 +74,7 @@ public class RepositoryMethodResourceMapping implements ResourceMapping {
* @see org.springframework.data.rest.repository.mapping.ResourceMapping#getPath()
*/
@Override
public String getPath() {
public Path getPath() {
return path;
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.rest.repository.mapping;
import org.springframework.data.rest.core.Path;
/**
* Mapping information for components to be exported as REST resources.
*
@@ -22,24 +24,6 @@ package org.springframework.data.rest.repository.mapping;
*/
public interface ResourceMapping {
public static ResourceMapping NO_MAPPING = new ResourceMapping() {
@Override
public Boolean isExported() {
return false;
}
@Override
public String getRel() {
return null;
}
@Override
public String getPath() {
return null;
}
};
/**
* Returns whether the component shall be exported at all.
*
@@ -59,5 +43,5 @@ public interface ResourceMapping {
*
* @return will never be {@literal null}.
*/
String getPath();
Path getPath();
}

View File

@@ -1,147 +0,0 @@
/*
* Copyright 2013 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.repository.mapping;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.data.rest.repository.support.RepositoriesUtils;
import org.springframework.hateoas.RelProvider;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* @author Oliver Gierke
*/
public class ResourceMappingFactory {
private final RelProvider relProvider;
public ResourceMappingFactory(RelProvider relProvider) {
this.relProvider = relProvider;
}
public CollectionResourceMapping getMappingForType(Class<?> type) {
return getMappingForType(type, new CollectionResourceMapping[0]);
}
public CollectionResourceMapping getMappingForType(Class<?> type, CollectionResourceMapping... manualMapping) {
Class<?> typeToInspect = getTypeToInspect(type);
InternalMappingBuilder mapping = getBaseMetadata(typeToInspect). //
merge(discoverConfig(typeToInspect));
if (type != typeToInspect) {
mapping = mapping.merge(discoverConfig(type));
}
for (CollectionResourceMapping externalMapping : manualMapping) {
mapping = mapping.merge(externalMapping);
}
return mapping.getMapping();
}
private static Class<?> getTypeToInspect(Class<?> type) {
if (!RepositoriesUtils.isRepositoryInterface(type)) {
return type;
}
return RepositoriesUtils.getDomainType(type);
}
private InternalMappingBuilder getBaseMetadata(Class<?> domainType) {
String path = StringUtils.uncapitalize(domainType.getSimpleName());
String defaultCollectionRel = relProvider.getCollectionResourceRelFor(domainType);
String defaultSingleRel = relProvider.getSingleResourceRelFor(domainType);
CollectionResourceMapping mapping = new SimpleCollectionResourceMapping(defaultCollectionRel, defaultSingleRel,
path, true);
return new CollectionResourceMappingBuilder(mapping);
}
private static final CollectionResourceMapping discoverConfig(Class<?> type) {
RestResource resource = AnnotationUtils.findAnnotation(type, RestResource.class);
if (resource == null) {
return null;
}
return new AnnotationResourceMapping(resource);
}
/**
* {@link CollectionResourceMapping} based on an {@link RestResource} annotation.
*
* @author Oliver Gierke
*/
private static class AnnotationResourceMapping implements CollectionResourceMapping {
private final RestResource annotation;
/**
* Creates a new {@link AnnotationResourceMapping} for the given {@link RestResource}.
*
* @param annotation must not be {@literal null}.
*/
public AnnotationResourceMapping(RestResource annotation) {
Assert.notNull(annotation, "Annotation must not be null!");
this.annotation = annotation;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#isExported()
*/
@Override
public Boolean isExported() {
return annotation.exported();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getCollectionRel()
*/
@Override
public String getRel() {
return StringUtils.hasText(annotation.rel()) ? annotation.rel() : null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getSingleResourceRel()
*/
@Override
public String getSingleResourceRel() {
String rel = getRel();
return rel == null ? null : String.format("%s.%s", rel, rel);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getPath()
*/
@Override
public String getPath() {
return StringUtils.hasText(annotation.path()) ? annotation.path() : null;
}
}
}

View File

@@ -35,12 +35,11 @@ import org.springframework.util.Assert;
*
* @author Oliver Gierke
*/
@SuppressWarnings("deprecation")
public class ResourceMappings implements ResourceMetadataProvider, Iterable<ResourceMetadata> {
public class ResourceMappings implements Iterable<ResourceMetadata> {
private final RepositoryRestConfiguration config;
private final ResourceMappingFactory factory;
private final Repositories repositories;
private final RelProvider relProvider;
private final Map<Class<?>, ResourceMetadata> cache = new HashMap<Class<?>, ResourceMetadata>();
private final Map<Class<?>, Map<String, ResourceMapping>> searchCache = new HashMap<Class<?>, Map<String, ResourceMapping>>();
@@ -66,13 +65,11 @@ public class ResourceMappings implements ResourceMetadataProvider, Iterable<Reso
*/
public ResourceMappings(RepositoryRestConfiguration config, Repositories repositories, RelProvider relProvider) {
Assert.notNull(config, "RepositoryRestConfiguration must not be null!");
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(relProvider, "RelProvider must not be null!");
this.config = config;
this.repositories = repositories;
this.factory = new ResourceMappingFactory(relProvider);
this.relProvider = relProvider;
this.populateCache(repositories);
}
@@ -96,8 +93,7 @@ public class ResourceMappings implements ResourceMetadataProvider, Iterable<Reso
RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(type);
Class<?> repositoryInterface = repositoryInformation.getRepositoryInterface();
CollectionResourceMapping mapping = factory.getMappingForType(repositoryInterface, fromConfig(type),
fromConfig(repositoryInterface));
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(repositoryInterface, relProvider);
RepositoryAwareResourceInformation information = new RepositoryAwareResourceInformation(repositories, mapping,
this, repositoryInformation);
@@ -125,9 +121,11 @@ public class ResourceMappings implements ResourceMetadataProvider, Iterable<Reso
Class<?> domainType = RepositoriesUtils.getDomainType(type);
RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(domainType);
Map<String, ResourceMapping> mappings = new HashMap<String, ResourceMapping>();
ResourceMetadata repositoryMapping = getMappingFor(repositoryInformation.getRepositoryInterface());
for (Method queryMethod : repositoryInformation.getQueryMethods()) {
mappings.put(queryMethod.getName(), new RepositoryMethodResourceMapping(queryMethod));
mappings.put(queryMethod.getName(), new RepositoryMethodResourceMapping(queryMethod,
repositoryMapping));
}
searchCache.put(type, mappings);
@@ -177,8 +175,7 @@ public class ResourceMappings implements ResourceMetadataProvider, Iterable<Reso
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.ResourceMetadataProvider#getMappingFor(org.springframework.data.mapping.PersistentProperty)
*/
@Override
public ResourceMapping getMappingFor(PersistentProperty<?> property) {
ResourceMapping getMappingFor(PersistentProperty<?> property) {
return getMappingFor(property.getActualType());
}
@@ -186,7 +183,6 @@ public class ResourceMappings implements ResourceMetadataProvider, Iterable<Reso
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.ResourceMetadataProvider#hasMappingFor(org.springframework.data.mapping.PersistentProperty)
*/
@Override
public boolean isMapped(PersistentProperty<?> property) {
ResourceMapping metadata = getMappingFor(property);
@@ -201,15 +197,4 @@ public class ResourceMappings implements ResourceMetadataProvider, Iterable<Reso
public Iterator<ResourceMetadata> iterator() {
return cache.values().iterator();
}
private CollectionResourceMapping fromConfig(Class<?> domainType) {
org.springframework.data.rest.config.ResourceMapping mapping = config.getResourceMappingForDomainType(domainType);
if (mapping == null) {
return null;
}
return new SimpleCollectionResourceMapping(mapping.getRel(), null, mapping.getPath(), mapping.isExported());
}
}

View File

@@ -15,12 +15,20 @@
*/
package org.springframework.data.rest.repository.mapping;
import java.util.Map;
import org.springframework.data.mapping.PersistentProperty;
/**
* @author Oliver Gierke
*/
public interface ResourceMetadata extends CollectionResourceMapping, ResourceMetadataProvider {
public interface ResourceMetadata extends CollectionResourceMapping {
boolean isManaged(PersistentProperty<?> property);
boolean isMapped(PersistentProperty<?> property);
ResourceMapping getMappingFor(PersistentProperty<?> property);
Map<String, ResourceMapping> getSearchResourceMappings();
}

View File

@@ -1,28 +0,0 @@
/*
* Copyright 2013 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.repository.mapping;
import org.springframework.data.mapping.PersistentProperty;
/**
* @author Oliver Gierke
*/
public interface ResourceMetadataProvider {
boolean isMapped(PersistentProperty<?> property);
ResourceMapping getMappingFor(PersistentProperty<?> property);
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright 2013 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.repository.mapping;
public class SimpleCollectionResourceMapping implements CollectionResourceMapping {
private final String collectionRel;
private final String singleRel;
private final String path;
private final Boolean exported;
public SimpleCollectionResourceMapping(String relsAndPath) {
this(relsAndPath, relsAndPath, relsAndPath, true);
}
public SimpleCollectionResourceMapping(String collectionRel, String singleRel, String path, Boolean exported) {
this.collectionRel = collectionRel;
this.singleRel = singleRel;
this.path = path;
this.exported = exported;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getCollectionRel()
*/
public String getRel() {
return collectionRel;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getSingleResourceRel()
*/
public String getSingleResourceRel() {
return singleRel;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getPath()
*/
@Override
public String getPath() {
return path;
}
/**
* @return the exported
*/
public Boolean isExported() {
return exported;
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2013 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.repository.mapping;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link CollectionResourceMapping} based on a type. Will derive default relation types and pathes from the type but
* inspect it for {@link RestResource} annotations for customization.
*
* @author Oliver Gierke
*/
public class TypeBasedCollectionResourceMapping implements CollectionResourceMapping {
private final Class<?> type;
private final RestResource annotation;
private final RelProvider relProvider;
/**
* Creates a new {@link TypeBasedCollectionResourceMapping} using the given type.
*
* @param type must not be {@literal null}.
*/
public TypeBasedCollectionResourceMapping(Class<?> type) {
this(type, new EvoInflectorRelProvider());
}
/**
* Creates a new {@link TypeBasedCollectionResourceMapping} using the given type and {@link RelProvider}.
*
* @param type must not be {@literal null}.
* @param relProvider must not be {@literal null}.
*/
public TypeBasedCollectionResourceMapping(Class<?> type, RelProvider relProvider) {
Assert.notNull(type, "Type must not be null!");
Assert.notNull(relProvider, "RelProvider must not be null!");
this.type = type;
this.relProvider = relProvider;
this.annotation = AnnotationUtils.findAnnotation(type, RestResource.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.ResourceMapping#getPath()
*/
@Override
public Path getPath() {
String path = annotation == null ? null : annotation.path().trim();
path = StringUtils.hasText(path) ? path : StringUtils.uncapitalize(type.getSimpleName());
return new Path(path);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.ResourceMapping#isExported()
*/
@Override
public Boolean isExported() {
return annotation == null ? true : annotation.exported();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.ResourceMapping#getRel()
*/
@Override
public String getRel() {
if (annotation == null || !StringUtils.hasText(annotation.rel())) {
return relProvider.getCollectionResourceRelFor(type);
}
return annotation.rel();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getSingleResourceRel()
*/
@Override
public String getSingleResourceRel() {
return relProvider.getSingleResourceRelFor(type);
}
}

View File

@@ -25,13 +25,15 @@ import org.junit.Test;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.data.rest.repository.domain.jpa.AnnotatedPersonRepository;
import org.springframework.data.rest.repository.domain.jpa.Person;
import org.springframework.data.rest.repository.domain.jpa.PlainPersonRepository;
import org.springframework.data.rest.repository.mapping.RepositoryCollectionResourceMapping;
import org.springframework.data.rest.repository.mapping.ResourceMapping;
import org.springframework.data.rest.repository.mapping.ResourceMappingFactory;
import org.springframework.data.rest.repository.support.SimpleRelProvider;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
/**
* Ensure the {@link ResourceMapping} components convey the correct information.
@@ -41,25 +43,26 @@ import org.springframework.data.rest.repository.support.SimpleRelProvider;
@SuppressWarnings("deprecation")
public class ResourceMappingUnitTests {
ResourceMappingFactory factory = new ResourceMappingFactory(new SimpleRelProvider());
RelProvider relProvider = new EvoInflectorRelProvider();
@Test
public void shouldDetectDefaultRelAndPath() throws Exception {
ResourceMapping newMapping = factory.getMappingForType(PlainPersonRepository.class);
ResourceMapping newMapping = new RepositoryCollectionResourceMapping(PlainPersonRepository.class, relProvider);
assertThat(newMapping.getRel(), is("person"));
assertThat(newMapping.getPath(), is("person"));
assertThat(newMapping.getRel(), is("persons"));
assertThat(newMapping.getPath(), is(new Path("person")));
assertThat(newMapping.isExported(), is(true));
}
@Test
public void shouldDetectAnnotatedRelAndPath() throws Exception {
ResourceMapping newMapping = factory.getMappingForType(AnnotatedPersonRepository.class);
ResourceMapping newMapping = new RepositoryCollectionResourceMapping(AnnotatedPersonRepository.class, relProvider);
assertThat(newMapping.getRel(), is("people"));
assertThat(newMapping.getPath(), is("person"));
assertThat(newMapping.getPath(), is(new Path("person")));
assertThat(newMapping.isExported(), is(false));
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2013 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.repository.mapping;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.data.rest.repository.support.SimpleRelProvider;
/**
* @author Oliver Gierke
*/
public class RepositoryAwareResourceMappingFactoryUnitTests {
ResourceMappingFactory factory = new ResourceMappingFactory(new SimpleRelProvider());
@Test
public void foo() {
CollectionResourceMapping mapping = factory.getMappingForType(Person.class);
assertThat(mapping.getPath(), is("person"));
assertThat(mapping.getRel(), is("person"));
assertThat(mapping.getSingleResourceRel(), is("person.person"));
}
@Test
public void honorsAnnotatedMapping() {
CollectionResourceMapping mapping = factory.getMappingForType(AnnotatedPerson.class);
assertThat(mapping.getPath(), is("bar"));
assertThat(mapping.getRel(), is("foo"));
assertThat(mapping.getSingleResourceRel(), is("foo.foo"));
assertThat(mapping.isExported(), is(false));
}
static class Person {
}
@RestResource(path = "bar", rel = "foo", exported = false)
static class AnnotatedPerson {
}
}

View File

@@ -20,70 +20,59 @@ import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.data.repository.Repository;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.data.rest.repository.support.SimpleRelProvider;
/**
* Unit tests for {@link RepositoryCollectionResourceMapping}.
*
* @author Oliver Gierke
*/
public class ResourceMappingFactoryUnitTests {
ResourceMappingFactory factory = new ResourceMappingFactory(new SimpleRelProvider());
public class RepositoryCollectionResourceMappingUnitTests {
@Test
public void foo() {
public void buildsDefaultMappingForRepository() {
CollectionResourceMapping mapping = factory.getMappingForType(Person.class);
assertThat(mapping.getPath(), is("person"));
assertThat(mapping.getRel(), is("person"));
assertThat(mapping.getSingleResourceRel(), is("person.person"));
}
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(PersonRepository.class);
@Test
public void honorsAnnotatedMapping() {
CollectionResourceMapping mapping = factory.getMappingForType(AnnotatedPerson.class);
assertThat(mapping.getPath(), is("bar"));
assertThat(mapping.getRel(), is("foo"));
assertThat(mapping.getSingleResourceRel(), is("foo.foo"));
assertThat(mapping.isExported(), is(false));
assertThat(mapping.getPath(), is(new Path("person")));
assertThat(mapping.getRel(), is("persons"));
assertThat(mapping.getSingleResourceRel(), is("person"));
assertThat(mapping.isExported(), is(true));
}
@Test
public void honorsAnnotatedsMapping() {
CollectionResourceMapping mapping = factory.getMappingForType(PersonRepository.class);
assertThat(mapping.getPath(), is("bar"));
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(AnnotatedPersonRepository.class);
assertThat(mapping.getPath(), is(new Path("bar")));
assertThat(mapping.getRel(), is("foo"));
assertThat(mapping.getSingleResourceRel(), is("foo.foo"));
assertThat(mapping.getSingleResourceRel(), is("annotatedPerson"));
assertThat(mapping.isExported(), is(false));
}
@Test
public void repositoryAnnotationTrumpsDomainTypeMapping() {
CollectionResourceMapping mapping = factory.getMappingForType(AnnotatedAnnotatedPersonRepository.class);
assertThat(mapping.getPath(), is("trumpsAll"));
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(
AnnotatedAnnotatedPersonRepository.class);
assertThat(mapping.getPath(), is(new Path("/trumpsAll")));
assertThat(mapping.getRel(), is("foo"));
assertThat(mapping.getSingleResourceRel(), is("foo.foo"));
assertThat(mapping.getSingleResourceRel(), is("annotatedPerson"));
assertThat(mapping.isExported(), is(true));
}
static class Person {
}
static class Person {}
@RestResource(path = "bar", rel = "foo", exported = false)
static class AnnotatedPerson {
static class AnnotatedPerson {}
}
interface PersonRepository extends Repository<Person, Long> {}
interface PersonRepository extends Repository<AnnotatedPerson, Long> {
}
interface AnnotatedPersonRepository extends Repository<AnnotatedPerson, Long> {}
@RestResource(path = "trumpsAll")
interface AnnotatedAnnotatedPersonRepository extends Repository<AnnotatedPerson, Long> {
}
interface AnnotatedAnnotatedPersonRepository extends Repository<AnnotatedPerson, Long> {}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2013 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.repository.mapping;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.lang.reflect.Method;
import org.junit.Test;
import org.springframework.data.repository.Repository;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.repository.annotation.RestResource;
/**
* @author Oliver Gierke
*/
public class RepositoryMethodResourceMappingUnitTests {
RepositoryCollectionResourceMapping resourceMapping = new RepositoryCollectionResourceMapping(
PersonRepository.class);
@Test
public void foo() throws Exception {
Method method = PersonRepository.class.getMethod("findByLastname", String.class);
ResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping);
assertThat(mapping.getPath(), is(new Path("person/findByLastname")));
}
@Test
public void usesConfiguredNameWithLeadingSlash() throws Exception {
Method method = PersonRepository.class.getMethod("findByFirstname", String.class);
ResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping);
assertThat(mapping.getPath(), is(new Path("person/bar")));
}
static class Person {}
interface PersonRepository extends Repository<Person, Long> {
Iterable<Person> findByLastname(String lastname);
@RestResource(path = "/bar")
Iterable<Person> findByFirstname(String firstname);
@RestResource(path = "foo")
Iterable<Person> findByEmailAddress(String email);
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2013 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.repository.mapping;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.repository.annotation.RestResource;
/**
* Unit tests for {@link TypeBasedCollectionResourceMapping}.
*
* @author Oliver Gierke
*/
public class TypeBasedCollectionResourceMappingUnitTest {
@Test
public void defaultsMappingsByType() {
CollectionResourceMapping mapping = new TypeBasedCollectionResourceMapping(Sample.class);
assertThat(mapping.getPath(), is(new Path("sample")));
assertThat(mapping.getRel(), is("samples"));
assertThat(mapping.getSingleResourceRel(), is("sample"));
assertThat(mapping.isExported(), is(true));
}
@Test
public void usesCustomizedRel() {
CollectionResourceMapping mapping = new TypeBasedCollectionResourceMapping(CustomizedSample.class);
assertThat(mapping.getPath(), is(new Path("customizedSample")));
assertThat(mapping.getRel(), is("myRel"));
assertThat(mapping.getSingleResourceRel(), is("customizedSample"));
assertThat(mapping.isExported(), is(true));
}
class Sample {
}
@RestResource(rel = "myRel")
class CustomizedSample {
}
}

View File

@@ -43,7 +43,7 @@ public class RepositoryLinkBuilder extends LinkBuilderSupport<RepositoryLinkBuil
UriComponentsBuilder builder = baseUri != null ? UriComponentsBuilder.fromUri(baseUri) : ControllerLinkBuilder
.linkTo(RepositoryController.class).toUriComponentsBuilder();
return builder.path(metadata.getPath());
return builder.path(metadata.getPath().toString());
}
@Override

View File

@@ -127,7 +127,8 @@ public abstract class AbstractWebIntegrationTests {
@Override
public void match(MvcResult result) throws Exception {
String s = result.getResponse().getContentAsString();
assertThat(links.findLinkWithRel(rel, s), notNullValue());
assertThat("Expected to find link with rel " + rel + " but found none in " + s, links.findLinkWithRel(rel, s),
notNullValue());
}
};
}

View File

@@ -68,10 +68,10 @@ public class PersistentEntitySerializationTests {
String s = writer.toString();
Link fatherLink = linkDiscoverer.findLinkWithRel("people.people.father", s);
Link fatherLink = linkDiscoverer.findLinkWithRel("person.father", s);
assertThat(fatherLink.getHref(), endsWith(new UriTemplate("/{id}/father").expand(person.getId()).toString()));
Link siblingLink = linkDiscoverer.findLinkWithRel("people.people.siblings", s);
Link siblingLink = linkDiscoverer.findLinkWithRel("person.siblings", s);
assertThat(siblingLink.getHref(), endsWith(new UriTemplate("/{id}/siblings").expand(person.getId()).toString()));
}
}

View File

@@ -61,13 +61,13 @@ public class MongoWebTests extends AbstractWebIntegrationTests {
*/
@Override
protected Iterable<String> expectedRootLinkRels() {
return Arrays.asList("profile");
return Arrays.asList("profiles");
}
@Test
public void foo() throws Exception {
Link profileLink = discoverUnique("profile");
Link profileLink = discoverUnique("profiles");
follow(profileLink).andExpect(jsonPath("$.content").value(hasSize(2)));
}
}