From d59ec3bdd4af312c836d82d3f51bf6686c837ace Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 24 Jan 2014 11:36:30 +0100 Subject: [PATCH] DATAREST-233 - Introduced @RepositoryRestResource. @RepositoryRestResource exposes more detailed attributes tailored to the use case of exposing a repository. @RestResource is still recognized on repository interfaces but we now issue a warning and indicate the new annotation to be used. Introduced a minimal ResourceDescription interface and let @Description be used within @RestResource and @RepositoryRestResource. We now generate default resource bundle keys and resolve them against a "rest-messages" resource bundle by default. JsonSchema converter now uses the rendered descriptions for schema descriptions. --- .../rest/core/annotation/Description.java | 27 ++++- .../annotation/RepositoryRestResource.java | 75 +++++++++++++ .../rest/core/annotation/RestResource.java | 25 +++++ .../AnnotationBasedResourceDescription.java | 71 ++++++++++++ .../mapping/CollectionResourceMapping.java | 16 ++- .../RepositoryAwareResourceInformation.java | 24 ++++- .../RepositoryCollectionResourceMapping.java | 101 ++++++++++++++++-- .../RepositoryMethodResourceMapping.java | 11 +- .../ResolvableResourceDescriptionSupport.java | 50 +++++++++ .../core/mapping/ResourceDescription.java | 45 ++++++++ .../rest/core/mapping/ResourceMapping.java | 11 +- .../rest/core/mapping/ResourceMappings.java | 37 ++++++- .../core/mapping/SearchResourceMappings.java | 13 ++- .../mapping/SimpleResourceDescription.java | 72 +++++++++++++ .../TypeBasedCollectionResourceMapping.java | 45 +++++++- .../core/support/RepositoryRelProvider.java | 13 ++- .../core/domain/jpa/PersonRepository.java | 25 ++++- ...stentPropertyResourceMappingUnitTests.java | 93 ++++++++++------ ...oryCollectionResourceMappingUnitTests.java | 18 +++- ... => ResourceMappingsIntegrationTests.java} | 2 +- ...edCollectionResourceMappingUnitTests.java} | 28 +++-- .../src/test/resources/logback.xml | 2 +- .../AbstractRepositoryRestController.java | 4 +- .../RepositoryRestMvcConfiguration.java | 23 +++- .../data/rest/webmvc/json/JsonSchema.java | 28 ++++- ...PersistentEntityToJsonSchemaConverter.java | 46 ++++++-- .../webmvc/support/RepositoryEntityLinks.java | 17 ++- .../rest/webmvc/jpa/PersonRepository.java | 6 +- ...tEntityToJsonSchemaConverterUnitTests.java | 43 ++++++++ .../test/resources/rest-messages.properties | 1 + 30 files changed, 875 insertions(+), 97 deletions(-) create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RepositoryRestResource.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/AnnotationBasedResourceDescription.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResolvableResourceDescriptionSupport.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceDescription.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SimpleResourceDescription.java rename spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/{ResourceMappingsIntegrationTest.java => ResourceMappingsIntegrationTests.java} (99%) rename spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/{TypeBasedCollectionResourceMappingUnitTest.java => TypeBasedCollectionResourceMappingUnitTests.java} (70%) create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java create mode 100644 spring-data-rest-webmvc/src/test/resources/rest-messages.properties diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/Description.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/Description.java index 67687da07..373cafeae 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/Description.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/Description.java @@ -1,3 +1,19 @@ +/* + * Copyright 2012-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.annotation; import java.lang.annotation.ElementType; @@ -6,10 +22,19 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** + * Annotation to descibe semantics of a resource. + * * @author Jon Brisbin + * @author Oliver Gierke */ -@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD }) +@Target({ ElementType.FIELD, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface Description { + + /** + * The textual description of the resource. Can be a resource bundle key for internationalization. + * + * @return + */ String value(); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RepositoryRestResource.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RepositoryRestResource.java new file mode 100644 index 000000000..1002294d7 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RepositoryRestResource.java @@ -0,0 +1,75 @@ +/* + * 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotate a {@link org.springframework.data.repository.Repository} with this to customize export mapping and rels. + * + * @author Oliver Gierke + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface RepositoryRestResource { + + /** + * Flag indicating whether this resource is exported at all. + * + * @return {@literal true} if the resource is to be exported, {@literal false} otherwise. + */ + boolean exported() default true; + + /** + * The path segment under which this resource is to be exported. + * + * @return A valid path segment. + */ + String path() default ""; + + /** + * The rel value to use when generating links to the collection resource. + * + * @return A valid rel value. + */ + String collectionResourceRel() default ""; + + /** + * The description of the collection resource. + * + * @return + */ + Description collectionResourceDescription() default @Description(value = ""); + + /** + * The rel value to use when generating links to the item resource. + * + * @return A valid rel value. + */ + String itemResourceRel() default ""; + + /** + * The description of the item resource. + * + * @return + */ + Description itemResourceDescription() default @Description(value = ""); +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RestResource.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RestResource.java index 91666d38a..1387b416c 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RestResource.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RestResource.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-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.annotation; import java.lang.annotation.ElementType; @@ -9,8 +24,12 @@ import java.lang.annotation.Target; /** * Annotate a {@link org.springframework.data.repository.Repository} with this to influence how it is exported and what * the value of the {@literal rel} attribute will be in links. + *

+ * As of Spring Data REST 2.0, prefer using {@link RepositoryRestResource} to also be able to customize the relation + * type and description for the item resources exposed by the repository. * * @author Jon Brisbin + * @author Oliver Gierke */ @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @@ -38,4 +57,10 @@ public @interface RestResource { */ String rel() default ""; + /** + * The description of the collection resource. + * + * @return + */ + Description description() default @Description(value = ""); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/AnnotationBasedResourceDescription.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/AnnotationBasedResourceDescription.java new file mode 100644 index 000000000..5d2f9674f --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/AnnotationBasedResourceDescription.java @@ -0,0 +1,71 @@ +/* + * 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 org.springframework.data.rest.core.annotation.Description; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A {@link ResourceDescription} that is customized based on a {@link Description} annotation. Allows to fall back on + * another {@link ResourceDescription} to provide defaults. + * + * @author Oliver Gierke + */ +class AnnotationBasedResourceDescription extends ResolvableResourceDescriptionSupport { + + private final String message; + private final ResourceDescription fallback; + + /** + * Creates a new {@link AnnotationBasedResourceDescription} for the given {@link Description} and fallback. + * + * @param description must not be {@literal null}. + * @param fallback must not be {@literal null}. + */ + AnnotationBasedResourceDescription(Description description, ResourceDescription fallback) { + + Assert.notNull(description, "Description must not be null!"); + Assert.notNull(fallback, "Fallback resource description must not be null!"); + + this.message = description.value(); + this.fallback = fallback; + } + + /** + * @return the message + */ + public String getMessage() { + return StringUtils.hasText(message) ? message : fallback.getMessage(); + } + + /** + * @return the mediaType + */ + public MediaType getType() { + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceDescription#isDefault() + */ + @Override + public boolean isDefault() { + return !StringUtils.hasText(message); + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CollectionResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CollectionResourceMapping.java index 4e719aa78..57b88d91a 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CollectionResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CollectionResourceMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * 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. @@ -22,5 +22,17 @@ package org.springframework.data.rest.core.mapping; */ public interface CollectionResourceMapping extends ResourceMapping { - String getSingleResourceRel(); + /** + * Returns the relation type pointing to the item resource within a collection. + * + * @return + */ + String getItemResourceRel(); + + /** + * Returns the {@link ResourceDescription} for the item resource. + * + * @return + */ + ResourceDescription getItemResourceDescription(); } 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 9f167982c..db6440f26 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 @@ -111,7 +111,7 @@ class RepositoryAwareResourceInformation implements ResourceMetadata { * @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#isExported() */ @Override - public Boolean isExported() { + public boolean isExported() { return mapping.isExported(); } @@ -129,8 +129,8 @@ class RepositoryAwareResourceInformation implements ResourceMetadata { * @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#getSingleResourceRel() */ @Override - public String getSingleResourceRel() { - return mapping.getSingleResourceRel(); + public String getItemResourceRel() { + return mapping.getItemResourceRel(); } /* @@ -151,6 +151,24 @@ class RepositoryAwareResourceInformation implements ResourceMetadata { return mapping.isPagingResource(); } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#getDescription() + */ + @Override + public ResourceDescription getDescription() { + return mapping.getDescription(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#getItemResourceDescription() + */ + @Override + public ResourceDescription getItemResourceDescription() { + return mapping.getItemResourceDescription(); + } + /* * (non-Javadoc) * @see org.springframework.data.rest.core.mapping.ResourceMetadata#getSearchResourceMappings() diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java index 54298b07e..69f79a79e 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java @@ -17,9 +17,12 @@ package org.springframework.data.rest.core.mapping; import java.lang.reflect.Modifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.rest.core.Path; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; import org.springframework.data.rest.core.support.RepositoriesUtils; import org.springframework.hateoas.RelProvider; @@ -37,9 +40,11 @@ import org.springframework.util.StringUtils; */ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { - private final boolean EVO_INFLECTOR_IS_PRESENT = ClassUtils.isPresent("org.atteo.evo.inflector.English", null); + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryCollectionResourceMapping.class); + private static final boolean EVO_INFLECTOR_IS_PRESENT = ClassUtils.isPresent("org.atteo.evo.inflector.English", null); private final RestResource annotation; + private final RepositoryRestResource repositoryAnnotation; private final CollectionResourceMapping domainTypeMapping; private final boolean repositoryIsExportCandidate; private final RepositoryMetadata metadata; @@ -66,11 +71,18 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { Assert.notNull(relProvider, "RelProvider must not be null!"); this.annotation = AnnotationUtils.findAnnotation(repositoryType, RestResource.class); + this.repositoryAnnotation = AnnotationUtils.findAnnotation(repositoryType, RepositoryRestResource.class); this.repositoryIsExportCandidate = Modifier.isPublic(repositoryType.getModifiers()); Class domainType = RepositoriesUtils.getDomainType(repositoryType); this.domainTypeMapping = EVO_INFLECTOR_IS_PRESENT ? new EvoInflectorTypeBasedCollectionResourceMapping(domainType, relProvider) : new TypeBasedCollectionResourceMapping(domainType, relProvider); + + if (annotation != null) { + LOGGER.warn( + "@RestResource detected to customize the repository resource for {}! Use @RepositoryRestResource instead!", + metadata.getRepositoryInterface().getName()); + } } /* @@ -80,8 +92,19 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { @Override public Path getPath() { - return annotation == null || !StringUtils.hasText(annotation.path()) ? domainTypeMapping.getPath() : new Path( - annotation.path()); + Path fallback = domainTypeMapping.getPath(); + + if (repositoryAnnotation != null) { + String path = repositoryAnnotation.path(); + return StringUtils.hasText(path) ? new Path(path) : fallback; + } + + if (annotation != null) { + String path = annotation.path(); + return StringUtils.hasText(path) ? new Path(path) : fallback; + } + + return fallback; } /* @@ -90,7 +113,20 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { */ @Override public String getRel() { - return annotation == null || !StringUtils.hasText(annotation.rel()) ? domainTypeMapping.getRel() : annotation.rel(); + + String fallback = domainTypeMapping.getRel(); + + if (repositoryAnnotation != null) { + String rel = repositoryAnnotation.collectionResourceRel(); + return StringUtils.hasText(rel) ? rel : fallback; + } + + if (annotation != null) { + String rel = annotation.rel(); + return StringUtils.hasText(rel) ? rel : fallback; + } + + return fallback; } /* @@ -98,8 +134,16 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { * @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#getSingleResourceRel() */ @Override - public String getSingleResourceRel() { - return domainTypeMapping.getSingleResourceRel(); + public String getItemResourceRel() { + + String fallback = domainTypeMapping.getItemResourceRel(); + + if (repositoryAnnotation != null) { + String rel = repositoryAnnotation.itemResourceRel(); + return StringUtils.hasText(rel) ? rel : fallback; + } + + return fallback; } /* @@ -107,8 +151,17 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { * @see org.springframework.data.rest.core.mapping.ResourceMapping#isExported() */ @Override - public Boolean isExported() { - return annotation == null ? repositoryIsExportCandidate && domainTypeMapping.isExported() : annotation.exported(); + public boolean isExported() { + + if (repositoryAnnotation != null) { + return repositoryAnnotation.exported(); + } + + if (annotation != null) { + return annotation.exported(); + } + + return repositoryIsExportCandidate && domainTypeMapping.isExported(); } /* @@ -119,4 +172,36 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { public boolean isPagingResource() { return metadata.isPagingRepository(); } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#getDescription() + */ + @Override + public ResourceDescription getDescription() { + + ResourceDescription fallback = SimpleResourceDescription.defaultFor(getRel()); + + if (repositoryAnnotation != null) { + return new AnnotationBasedResourceDescription(repositoryAnnotation.collectionResourceDescription(), fallback); + } + + return fallback; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#getItemResourceDescription() + */ + @Override + public ResourceDescription getItemResourceDescription() { + + ResourceDescription fallback = SimpleResourceDescription.defaultFor(getItemResourceRel()); + + if (repositoryAnnotation != null) { + return new AnnotationBasedResourceDescription(repositoryAnnotation.itemResourceDescription(), fallback); + } + + return fallback; + } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java index b9dcc3fb0..b75efb524 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java @@ -90,7 +90,7 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { * @see org.springframework.data.rest.core.mapping.ResourceMapping#isExported() */ @Override - public Boolean isExported() { + public boolean isExported() { return isExported; } @@ -138,4 +138,13 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { public boolean isPagingResource() { return paging; } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#getDescription() + */ + @Override + public ResourceDescription getDescription() { + return null; + } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResolvableResourceDescriptionSupport.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResolvableResourceDescriptionSupport.java new file mode 100644 index 000000000..bda50c0ca --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResolvableResourceDescriptionSupport.java @@ -0,0 +1,50 @@ +/* + * 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; + +/** + * + * @author Oliver Gierke + */ +public abstract class ResolvableResourceDescriptionSupport implements ResourceDescription { + + /* + * (non-Javadoc) + * @see org.springframework.context.MessageSourceResolvable#getCodes() + */ + @Override + public String[] getCodes() { + return new String[] { getMessage() }; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.MessageSourceResolvable#getArguments() + */ + @Override + public Object[] getArguments() { + return new Object[0]; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.MessageSourceResolvable#getDefaultMessage() + */ + @Override + public String getDefaultMessage() { + return null; + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceDescription.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceDescription.java new file mode 100644 index 000000000..3463e45aa --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceDescription.java @@ -0,0 +1,45 @@ +/* + * 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 org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.http.MediaType; + +/** + * A description of a resource. Resolvable to plain text by using a {@link MessageSource}. + * + * @author Oliver Gierke + */ +public interface ResourceDescription extends MessageSourceResolvable { + + /** + * Returns the description. This can be a message source code or a custom text format. Prefer resolving the + * {@link ResourceDescription} using a {@link MessageSource}. + * + * @return + */ + String getMessage(); + + /** + * Returns whether this is the default description. + * + * @return + */ + boolean isDefault(); + + MediaType getType(); +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMapping.java index 32132332b..bfb869dbe 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * 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. @@ -29,7 +29,7 @@ public interface ResourceMapping { * * @return will never be {@literal null}. */ - Boolean isExported(); + boolean isExported(); /** * Returns the relation for the resource exported. @@ -51,4 +51,11 @@ public interface ResourceMapping { * @return */ boolean isPagingResource(); + + /** + * Returns the resource's description. + * + * @return + */ + ResourceDescription getDescription(); } 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 8ba7dcee2..2e85dff85 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 @@ -26,6 +26,7 @@ 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; @@ -222,7 +223,8 @@ public class ResourceMappings implements Iterable { } ResourceMetadata propertyTypeMapping = getMappingFor(property.getActualType()); - propertyMapping = new PersistentPropertyResourceMapping(property, propertyTypeMapping); + ResourceMetadata ownerTypeMapping = getMappingFor(property.getOwner().getType()); + propertyMapping = new PersistentPropertyResourceMapping(property, propertyTypeMapping, ownerTypeMapping); propertyCache.put(property, propertyMapping); @@ -257,7 +259,9 @@ public class ResourceMappings implements Iterable { 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}. @@ -265,12 +269,16 @@ public class ResourceMappings implements Iterable { * @param property must not be {@literal null}. * @param exported whether the property is exported or not. */ - public PersistentPropertyResourceMapping(PersistentProperty property, ResourceMapping typeMapping) { + public PersistentPropertyResourceMapping(PersistentProperty property, ResourceMapping typeMapping, + CollectionResourceMapping ownerTypeMapping) { Assert.notNull(property, "PersistentProperty must not be null!"); + this.property = property; this.typeMapping = typeMapping; - this.annotation = property.findAnnotation(RestResource.class); + this.ownerTypeMapping = ownerTypeMapping; + this.annotation = property.isAssociation() ? property.findAnnotation(RestResource.class) : null; + this.description = property.findAnnotation(Description.class); } /* @@ -297,7 +305,7 @@ public class ResourceMappings implements Iterable { * @see org.springframework.data.rest.core.mapping.ResourceMapping#isExported() */ @Override - public Boolean isExported() { + public boolean isExported() { if (typeMapping == null) { return false; @@ -314,5 +322,26 @@ public class ResourceMappings implements Iterable { 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/SearchResourceMappings.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java index f859ed8d8..9497c02fb 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * 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. @@ -100,7 +100,7 @@ public class SearchResourceMappings implements Iterable, * @see org.springframework.data.rest.core.mapping.ResourceMapping#isExported() */ @Override - public Boolean isExported() { + public boolean isExported() { return !mappings.isEmpty(); } @@ -113,6 +113,15 @@ public class SearchResourceMappings implements Iterable, return false; } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#getDescription() + */ + @Override + public ResourceDescription getDescription() { + return null; + } + /* * (non-Javadoc) * @see java.lang.Iterable#iterator() diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SimpleResourceDescription.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SimpleResourceDescription.java new file mode 100644 index 000000000..0fef09276 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SimpleResourceDescription.java @@ -0,0 +1,72 @@ +/* + * 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 org.springframework.data.mapping.PersistentProperty; +import org.springframework.http.MediaType; +import org.springframework.util.StringUtils; + +/** + * @author Oliver Gierke + */ +public class SimpleResourceDescription extends ResolvableResourceDescriptionSupport { + + private static final String DEFAULT_KEY_PREFIX = "rest.description"; + private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.TEXT_PLAIN; + + private String message; + private MediaType type; + + private SimpleResourceDescription(String message, MediaType mediaType) { + this.message = message; + this.type = mediaType; + } + + public static ResourceDescription defaultFor(PersistentProperty property, String rel) { + + String message = String.format("%s.%s.%s", DEFAULT_KEY_PREFIX, rel, property.getName()); + return new SimpleResourceDescription(message, DEFAULT_MEDIA_TYPE); + } + + public static ResourceDescription defaultFor(String rel) { + + String message = String.format("%s.%s", DEFAULT_KEY_PREFIX, rel); + return new SimpleResourceDescription(message, DEFAULT_MEDIA_TYPE); + } + + public static ResourceDescription defaultForCollection(Class type) { + return null; + } + + public static ResourceDescription defaultForMethod(RepositoryMethodResourceMapping mapping) { + return null; + } + + /** + * @return the message + */ + public String getMessage() { + return message; + } + + public MediaType getType() { + return type; + } + + public boolean isDefault() { + return StringUtils.hasText(message) && message.startsWith(DEFAULT_KEY_PREFIX); + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java index d9ef57e14..c14c0a847 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java @@ -19,6 +19,7 @@ import java.lang.reflect.Modifier; import org.springframework.core.annotation.AnnotationUtils; 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.hateoas.RelProvider; import org.springframework.hateoas.core.EvoInflectorRelProvider; @@ -34,8 +35,9 @@ import org.springframework.util.StringUtils; class TypeBasedCollectionResourceMapping implements CollectionResourceMapping { private final Class type; - private final RestResource annotation; private final RelProvider relProvider; + private final RestResource annotation; + private final Description description; /** * Creates a new {@link TypeBasedCollectionResourceMapping} using the given type. @@ -60,6 +62,7 @@ class TypeBasedCollectionResourceMapping implements CollectionResourceMapping { this.type = type; this.relProvider = relProvider; this.annotation = AnnotationUtils.findAnnotation(type, RestResource.class); + this.description = AnnotationUtils.findAnnotation(type, Description.class); } /* @@ -79,7 +82,7 @@ class TypeBasedCollectionResourceMapping implements CollectionResourceMapping { * @see org.springframework.data.rest.core.mapping.ResourceMapping#isExported() */ @Override - public Boolean isExported() { + public boolean isExported() { return annotation == null ? Modifier.isPublic(type.getModifiers()) : annotation.exported(); } @@ -102,7 +105,7 @@ class TypeBasedCollectionResourceMapping implements CollectionResourceMapping { * @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#getSingleResourceRel() */ @Override - public String getSingleResourceRel() { + public String getItemResourceRel() { return relProvider.getSingleResourceRelFor(type); } @@ -115,6 +118,38 @@ class TypeBasedCollectionResourceMapping implements CollectionResourceMapping { return false; } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#getDescription() + */ + @Override + public ResourceDescription getDescription() { + + ResourceDescription fallback = SimpleResourceDescription.defaultFor(getRel()); + + return fallback; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#getItemResourceDescription() + */ + @Override + public ResourceDescription getItemResourceDescription() { + + ResourceDescription fallback = SimpleResourceDescription.defaultFor(getItemResourceRel()); + + if (annotation != null && StringUtils.hasText(annotation.description().value())) { + return new AnnotationBasedResourceDescription(annotation.description(), fallback); + } + + if (description != null) { + return new AnnotationBasedResourceDescription(description, fallback); + } + + return fallback; + } + /** * Returns the default path to be used if the path is not configured manually. * @@ -122,6 +157,10 @@ class TypeBasedCollectionResourceMapping implements CollectionResourceMapping { * @return */ protected String getDefaultPathFor(Class type) { + return getSimpleTypeName(type); + } + + private String getSimpleTypeName(Class type) { return StringUtils.uncapitalize(type.getSimpleName()); } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/RepositoryRelProvider.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/RepositoryRelProvider.java index b80a3650b..9376f92dd 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/RepositoryRelProvider.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/RepositoryRelProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * 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. @@ -17,8 +17,11 @@ package org.springframework.data.rest.core.support; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.hateoas.RelProvider; +import org.springframework.util.Assert; /** + * A {@link RelProvider} based on the {@link ResourceMappings} for the registered repositories. + * * @author Oliver Gierke */ public class RepositoryRelProvider implements RelProvider { @@ -26,11 +29,13 @@ public class RepositoryRelProvider implements RelProvider { private final ResourceMappings mappings; /** - * @param repositories - * @param config + * Creates a new {@link RepositoryRelProvider} for the given {@link ResourceMappings}. + * + * @param mappings must not be {@literal null}. */ public RepositoryRelProvider(ResourceMappings mappings) { + Assert.notNull(mappings, "ResourceMappings must not be null!"); this.mappings = mappings; } @@ -49,7 +54,7 @@ public class RepositoryRelProvider implements RelProvider { */ @Override public String getSingleResourceRelFor(Class type) { - return mappings.getMappingFor(type).getSingleResourceRel(); + return mappings.getMappingFor(type).getItemResourceRel(); } /* diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/jpa/PersonRepository.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/jpa/PersonRepository.java index 98773b638..f346f7d28 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/jpa/PersonRepository.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/jpa/PersonRepository.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-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.domain.jpa; import java.util.Date; @@ -7,6 +22,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; @@ -15,8 +31,9 @@ import org.springframework.format.annotation.DateTimeFormat.ISO; * A repository to manage {@link Person}s. * * @author Jon Brisbin + * @author Oliver Gierke */ -@RestResource(rel = "people", path = "people") +@RepositoryRestResource(collectionResourceRel = "people", path = "people") public interface PersonRepository extends PagingAndSortingRepository { @RestResource(rel = "firstname", path = "firstname") @@ -29,9 +46,9 @@ public interface PersonRepository extends PagingAndSortingRepository persistentEntity = mappingContext.getPersistentEntity(Entity.class); - - @Mock ResourceMapping typeMapping; - - @Before - public void setUp() { - when(typeMapping.isExported()).thenReturn(true); - } /** * @see DATAREST-175 @@ -57,13 +48,12 @@ public class PersistentPropertyResourceMappingUnitTests { @Test public void usesPropertyNameAsDefaultResourceMappingRelAndPath() { - MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty("first"); - ResourceMapping propertyMapping = new PersistentPropertyResourceMapping(persistentProperty, typeMapping); + ResourceMapping mapping = getPropertyMappingFor(Entity.class, "first"); - assertThat(propertyMapping, is(notNullValue())); - assertThat(propertyMapping.getPath(), is(new Path("first"))); - assertThat(propertyMapping.getRel(), is("first")); - assertThat(propertyMapping.isExported(), is(true)); + assertThat(mapping, is(notNullValue())); + assertThat(mapping.getPath(), is(new Path("first"))); + assertThat(mapping.getRel(), is("first")); + assertThat(mapping.isExported(), is(true)); } /** @@ -72,13 +62,12 @@ public class PersistentPropertyResourceMappingUnitTests { @Test public void considersMappingAnnotationOnDomainClassProperty() { - MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty("second"); - ResourceMapping propertyMapping = new PersistentPropertyResourceMapping(persistentProperty, typeMapping); + ResourceMapping mapping = getPropertyMappingFor(Entity.class, "second"); - assertThat(propertyMapping, is(notNullValue())); - assertThat(propertyMapping.getPath(), is(new Path("secPath"))); - assertThat(propertyMapping.getRel(), is("secRel")); - assertThat(propertyMapping.isExported(), is(false)); + assertThat(mapping, is(notNullValue())); + assertThat(mapping.getPath(), is(new Path("secPath"))); + assertThat(mapping.getRel(), is("secRel")); + assertThat(mapping.isExported(), is(false)); } /** @@ -87,29 +76,67 @@ public class PersistentPropertyResourceMappingUnitTests { @Test public void considersMappingAnnotationOnDomainClassPropertyMethod() { - MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty("third"); - ResourceMapping propertyMapping = new PersistentPropertyResourceMapping(persistentProperty, typeMapping); + ResourceMapping mapping = getPropertyMappingFor(Entity.class, "third"); - assertThat(propertyMapping, is(notNullValue())); - assertThat(propertyMapping.getPath(), is(new Path("thirdPath"))); - assertThat(propertyMapping.getRel(), is("thirdRel")); - assertThat(propertyMapping.isExported(), is(false)); + assertThat(mapping, is(notNullValue())); + assertThat(mapping.getPath(), is(new Path("thirdPath"))); + assertThat(mapping.getRel(), is("thirdRel")); + assertThat(mapping.isExported(), is(false)); } - static class Entity { + @Test + public void returnsDefaultDescriptionKey() { - Related first, third; + ResourceMapping mapping = getPropertyMappingFor(Entity.class, "second"); + ResourceDescription description = mapping.getDescription(); + + assertThat(description.isDefault(), is(true)); + assertThat(description.getMessage(), is("rest.description.entity.second")); + } + + /** + * @see DATAREST-??? + */ + @Test + public void considersAtDescription() { + + ResourceMapping mapping = getPropertyMappingFor(Entity.class, "fourth"); + + ResourceDescription description = mapping.getDescription(); + assertThat(description.isDefault(), is(false)); + assertThat(description.getMessage(), is("Some description")); + } + + private ResourceMapping getPropertyMappingFor(Class entity, String propertyName) { + + MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(entity); + MongoPersistentProperty property = persistentEntity.getPersistentProperty(propertyName); + + CollectionResourceMapping entityResourceMapping = new TypeBasedCollectionResourceMapping(entity); + ResourceMapping propertyTypeMapping = new TypeBasedCollectionResourceMapping(property.getType()); + + return new PersistentPropertyResourceMapping(property, propertyTypeMapping, entityResourceMapping); + } + + public static class Entity { + + Related first; + @DBRef Related third; + + @DBRef// @RestResource(path = "secPath", rel = "secRel", exported = false)// List second; + @Description("Some description") String fourth; + @RestResource(path = "thirdPath", rel = "thirdRel", exported = false) public Related getThird() { return third; } } - static class Related { + public static class Related { } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java index df7973096..184896f1c 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java @@ -25,6 +25,7 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.rest.core.Path; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; /** @@ -41,7 +42,7 @@ public class RepositoryCollectionResourceMappingUnitTests { assertThat(mapping.getPath(), is(new Path("persons"))); assertThat(mapping.getRel(), is("persons")); - assertThat(mapping.getSingleResourceRel(), is("person")); + assertThat(mapping.getItemResourceRel(), is("person")); assertThat(mapping.isExported(), is(true)); } @@ -52,7 +53,7 @@ public class RepositoryCollectionResourceMappingUnitTests { assertThat(mapping.getPath(), is(new Path("bar"))); assertThat(mapping.getRel(), is("foo")); - assertThat(mapping.getSingleResourceRel(), is("annotatedPerson")); + assertThat(mapping.getItemResourceRel(), is("annotatedPerson")); assertThat(mapping.isExported(), is(false)); } @@ -63,7 +64,7 @@ public class RepositoryCollectionResourceMappingUnitTests { assertThat(mapping.getPath(), is(new Path("/trumpsAll"))); assertThat(mapping.getRel(), is("foo")); - assertThat(mapping.getSingleResourceRel(), is("annotatedPerson")); + assertThat(mapping.getItemResourceRel(), is("annotatedPerson")); assertThat(mapping.isExported(), is(true)); } @@ -82,6 +83,14 @@ public class RepositoryCollectionResourceMappingUnitTests { assertThat(getResourceMappingFor(PersonRepository.class).isPagingResource(), is(true)); } + @Test + public void discoversCustomizationsUsingRestRepositoryResource() { + + CollectionResourceMapping mapping = getResourceMappingFor(RepositoryAnnotatedRepository.class); + assertThat(mapping.getRel(), is("foo")); + assertThat(mapping.getItemResourceRel(), is("bar")); + } + private static CollectionResourceMapping getResourceMappingFor(Class repositoryInterface) { RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); @@ -106,4 +115,7 @@ public class RepositoryCollectionResourceMappingUnitTests { public static class PublicClass {} interface PackageProtectedRepository extends Repository {} + + @RepositoryRestResource(collectionResourceRel = "foo", itemResourceRel = "bar") + interface RepositoryAnnotatedRepository extends Repository {} } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ResourceMappingsIntegrationTest.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ResourceMappingsIntegrationTests.java similarity index 99% rename from spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ResourceMappingsIntegrationTest.java rename to spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ResourceMappingsIntegrationTests.java index 68967fd46..d48aeba4f 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ResourceMappingsIntegrationTest.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ResourceMappingsIntegrationTests.java @@ -48,7 +48,7 @@ import org.springframework.transaction.annotation.Transactional; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = JpaRepositoryConfig.class) @Transactional -public class ResourceMappingsIntegrationTest { +public class ResourceMappingsIntegrationTests { @Autowired ListableBeanFactory factory; diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMappingUnitTest.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMappingUnitTests.java similarity index 70% rename from spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMappingUnitTest.java rename to spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMappingUnitTests.java index 5077d594a..34fc0ed53 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMappingUnitTest.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMappingUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * 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. @@ -21,15 +21,13 @@ import static org.junit.Assert.*; import org.junit.Test; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.core.annotation.RestResource; -import org.springframework.data.rest.core.mapping.CollectionResourceMapping; -import org.springframework.data.rest.core.mapping.TypeBasedCollectionResourceMapping; /** * Unit tests for {@link TypeBasedCollectionResourceMapping}. * * @author Oliver Gierke */ -public class TypeBasedCollectionResourceMappingUnitTest { +public class TypeBasedCollectionResourceMappingUnitTests { @Test public void defaultsMappingsByType() { @@ -38,7 +36,7 @@ public class TypeBasedCollectionResourceMappingUnitTest { assertThat(mapping.getPath(), is(new Path("sample"))); assertThat(mapping.getRel(), is("samples")); - assertThat(mapping.getSingleResourceRel(), is("sample")); + assertThat(mapping.getItemResourceRel(), is("sample")); assertThat(mapping.isExported(), is(true)); } @@ -49,7 +47,7 @@ public class TypeBasedCollectionResourceMappingUnitTest { assertThat(mapping.getPath(), is(new Path("customizedSample"))); assertThat(mapping.getRel(), is("myRel")); - assertThat(mapping.getSingleResourceRel(), is("customizedSample")); + assertThat(mapping.getItemResourceRel(), is("customizedSample")); assertThat(mapping.isExported(), is(true)); } @@ -64,6 +62,24 @@ public class TypeBasedCollectionResourceMappingUnitTest { assertThat(mapping.isExported(), is(false)); } + /** + * @see + */ + @Test + public void usesDefaultDescriptionIfNoAnnotationPresent() { + + CollectionResourceMapping mapping = new TypeBasedCollectionResourceMapping(Sample.class); + ResourceDescription description = mapping.getDescription(); + + assertThat(description.isDefault(), is(true)); + assertThat(description.getMessage(), is("rest.description.samples")); + + ResourceDescription itemDescription = mapping.getItemResourceDescription(); + + assertThat(itemDescription.isDefault(), is(true)); + assertThat(itemDescription.getMessage(), is("rest.description.sample")); + } + public interface Sample {} interface HiddenSample {} diff --git a/spring-data-rest-core/src/test/resources/logback.xml b/spring-data-rest-core/src/test/resources/logback.xml index ad5cbef50..f1b93ebdd 100644 --- a/spring-data-rest-core/src/test/resources/logback.xml +++ b/spring-data-rest-core/src/test/resources/logback.xml @@ -7,7 +7,7 @@ - + diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java index 4cec36e8f..ec52a80fd 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-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. @@ -200,7 +200,7 @@ class AbstractRepositoryRestController implements MessageSourceAware, Initializi ResourceMetadata repoMapping = repoRequest.getResourceMetadata(); Link selfLink = resource.getLink("self"); - String rel = repoMapping.getSingleResourceRel(); + String rel = repoMapping.getItemResourceRel(); return new Link(selfLink.getHref(), rel); } 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 5092d57be..2ee349131 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-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. @@ -30,6 +30,8 @@ import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; import org.springframework.context.annotation.Lazy; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.repository.support.Repositories; @@ -39,6 +41,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.ResourceDescription; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.support.DomainObjectMerger; import org.springframework.data.rest.core.util.UUIDConverter; @@ -268,7 +271,23 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() { - return new PersistentEntityToJsonSchemaConverter(repositories(), resourceMappings()); + return new PersistentEntityToJsonSchemaConverter(repositories(), resourceMappings(), + resourceDescriptionMessageSourceAccessor()); + } + + /** + * The {@link MessageSourceAccessor} to provide messages for {@link ResourceDescription}s being rendered. + * + * @return + */ + @Bean + public MessageSourceAccessor resourceDescriptionMessageSourceAccessor() { + + ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); + messageSource.setBasename("classpath:rest-messages"); + messageSource.setUseCodeAsDefaultMessage(true); + + return new MessageSourceAccessor(messageSource); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java index 5b1fab7a5..19a239d76 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-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.json; import java.util.ArrayList; @@ -5,16 +20,20 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.hateoas.Resource; +import com.fasterxml.jackson.annotation.JsonProperty; + /** + * Model class to render JSON schema documents. + * * @author Jon Brisbin + * @author Oliver Gierke */ public class JsonSchema extends Resource> { private final String name; - @SuppressWarnings("unused") private final String description; + private final String description; public JsonSchema(String name, String description) { super(new HashMap()); @@ -26,6 +45,10 @@ public class JsonSchema extends Resource> { return name; } + public String getDescription() { + return description; + } + @JsonProperty("properties") @Override public Map getContent() { @@ -46,6 +69,7 @@ public class JsonSchema extends Resource> { } public static class Property { + private final String type; private final String description; private final boolean required; 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 8b4af4dce..c9040a69f 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 @@ -1,3 +1,18 @@ +/* + * Copyright 2012-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.json; import static org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.*; @@ -10,6 +25,7 @@ import java.util.Set; 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; @@ -18,9 +34,12 @@ 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.rest.core.annotation.Description; +import org.springframework.data.rest.core.mapping.ResourceDescription; +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.json.JsonSchema.ArrayProperty; +import org.springframework.data.rest.webmvc.json.JsonSchema.Property; import org.springframework.data.rest.webmvc.support.RepositoryLinkBuilder; import org.springframework.hateoas.Link; import org.springframework.util.Assert; @@ -37,6 +56,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric private final Set convertiblePairs = new HashSet(); private final ResourceMappings mappings; private final Repositories repositories; + private final MessageSourceAccessor accessor; /** * Creates a new {@link PersistentEntityToJsonSchemaConverter} for the given {@link Repositories} and @@ -44,14 +64,17 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric * * @param repositories must not be {@literal null}. * @param mappings must not be {@literal null}. + * @param accessor */ - public PersistentEntityToJsonSchemaConverter(Repositories repositories, ResourceMappings mappings) { + public PersistentEntityToJsonSchemaConverter(Repositories repositories, ResourceMappings mappings, + MessageSourceAccessor accessor) { Assert.notNull(repositories, "Repositories must not be null!"); Assert.notNull(mappings, "ResourceMappings must not be null!"); this.repositories = repositories; this.mappings = mappings; + this.accessor = accessor; for (Class domainType : repositories) { convertiblePairs.add(new ConvertiblePair(domainType, JsonSchema.class)); @@ -90,9 +113,8 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric PersistentEntity persistentEntity = repositories.getPersistentEntity((Class) source); final ResourceMetadata metadata = mappings.getMappingFor(persistentEntity.getType()); - String entityDesc = persistentEntity.getType().isAnnotationPresent(Description.class) ? persistentEntity.getType() - .getAnnotation(Description.class).value() : null; - final JsonSchema jsonSchema = new JsonSchema(persistentEntity.getName(), entityDesc); + final JsonSchema jsonSchema = new JsonSchema(persistentEntity.getName(), accessor.getMessage(metadata + .getItemResourceDescription())); persistentEntity.doWithProperties(new SimplePropertyHandler() { @@ -106,12 +128,16 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric Class propertyType = persistentProperty.getType(); String type = uncapitalize(propertyType.getSimpleName()); - boolean notNull = persistentProperty.isAnnotationPresent(NotNull.class); - Description descriptionAnnotation = persistentProperty.findAnnotation(Description.class); - String desc = descriptionAnnotation == null ? null : descriptionAnnotation.value(); + ResourceMapping propertyMapping = metadata.getMappingFor(persistentProperty); + + boolean notNull = persistentProperty.isAnnotationPresent(NotNull.class); + ResourceDescription description = propertyMapping.getDescription(); + String message = accessor.getMessage(description); + + Property property = persistentProperty.isCollectionLike() ? // + new ArrayProperty("array", message, notNull) + : new Property(type, message, notNull); - JsonSchema.Property property = persistentProperty.isCollectionLike() ? new JsonSchema.ArrayProperty("array", - desc, notNull) : new JsonSchema.Property(type, desc, notNull); jsonSchema.addProperty(persistentProperty.getName(), property); } }); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java index 01f677b5f..6ec0575c4 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-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.support; import org.springframework.beans.factory.annotation.Autowired; @@ -112,6 +127,6 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { public Link linkToSingleResource(Class type, Object id) { ResourceMetadata metadata = mappings.getMappingFor(type); - return linkFor(type).slash(id).withRel(metadata.getSingleResourceRel()); + return linkFor(type).slash(id).withRel(metadata.getItemResourceRel()); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonRepository.java index 28cbeea06..49537bbc0 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * 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. @@ -22,6 +22,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; @@ -30,8 +31,9 @@ import org.springframework.format.annotation.DateTimeFormat.ISO; * A repository to manage {@link Person}s. * * @author Jon Brisbin + * @author Oliver Gierke */ -@RestResource(rel = "people", path = "people") +@RepositoryRestResource(collectionResourceRel = "people", path = "people") public interface PersonRepository extends PagingAndSortingRepository { @RestResource(rel = "firstname", path = "firstname") diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java new file mode 100644 index 000000000..22eb1df85 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java @@ -0,0 +1,43 @@ +/* + * 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.json; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests; +import org.springframework.data.rest.webmvc.mongodb.MongoDbRepositoryConfig; +import org.springframework.data.rest.webmvc.mongodb.Profile; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Oliver Gierke + */ +@ContextConfiguration(classes = MongoDbRepositoryConfig.class) +public class PersistentEntityToJsonSchemaConverterUnitTests extends AbstractControllerIntegrationTests { + + @Autowired PersistentEntityToJsonSchemaConverter converter; + + @Test + public void addsDescriptionToSchemaRoot() { + + JsonSchema schema = converter.convert(Profile.class); + + assertThat(schema.getDescription(), is("Profile description")); + } +} diff --git a/spring-data-rest-webmvc/src/test/resources/rest-messages.properties b/spring-data-rest-webmvc/src/test/resources/rest-messages.properties new file mode 100644 index 000000000..c388374b3 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/resources/rest-messages.properties @@ -0,0 +1 @@ +rest.description.profile=Profile description \ No newline at end of file