From 020de45c1b422b665b071c0326526effab3f0359 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 25 Mar 2014 10:44:28 +0100 Subject: [PATCH] DATAREST-230 - Support for serving Alps documents. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the support to expose additional resources that serve ALPS [0] resources to document the available state transitions and representations. The exposure is enabled by default and can be customized using the RepositoryRestConfiguration.metadataConfiguration() object. Currently the set of descriptors exposed includes: - A descriptor for the representation of the domain type. Linkable associations are represented as safe descriptors, ones that are not linked are semantic descriptors. - Descriptors for each supported HTTP method for both the collection and item resources to indicate the ability to update, create, delete etc. - Safe descriptors (e.g. to access the collection or item resource) get potentially available customizations (pagination, projections) attached through nested descriptors. Documentation An ALPS descriptor contains a doc attribute to carry semantic information for the end user or a potential client to display. The information can be described in two ways: the first one is the @Description annotation that captures the plain text information one wants to get listed. It is supported in @RepositoryRestResource, on query methods, projection interfaces and accessors etc. The preferred approach however is to use a resource bundle rest-messages.properties. For each descriptor we will resolve a key starting with rest.description followed by a dot path into the resource. By default, doc attributes are only rendered if the resource bundle contains an entry for the relevant key. You can enforce displaying unresolved keys by configuring MetadataConfiguration.omitUnresolvableDescriptionKeys(…). For representation descriptors and and the parameter list of query method descriptors we will display enum values by default as comma-separated list. The list is also available as message resolution argument, so that you can refer to the list in your description message via the {0} placeholder. TODOs: - Improve descriptors for associations (indicate ability to update etc.) [0] ALPS - http://alps.io --- pom.xml | 11 +- .../rest/core/annotation/Description.java | 2 +- .../core/config/MetadataConfiguration.java | 66 +++ .../ProjectionDefinitionConfiguration.java | 22 + .../config/RepositoryRestConfiguration.java | 34 +- .../AnnotationBasedResourceDescription.java | 28 +- .../core/mapping/MethodResourceMapping.java | 5 +- .../rest/core/mapping/ParameterMetadata.java | 103 ++++ .../rest/core/mapping/ParametersMetadata.java | 69 +++ .../RepositoryMethodResourceMapping.java | 20 +- .../mapping/RepositoryResourceMappings.java | 6 +- .../ResolvableResourceDescriptionSupport.java | 9 - .../mapping/SimpleResourceDescription.java | 66 +-- .../TypeBasedCollectionResourceMapping.java | 8 + .../mapping/TypedResourceDescription.java | 82 ++++ ...ositoryMethodResourceMappingUnitTests.java | 6 +- spring-data-rest-webmvc/pom.xml | 30 +- .../data/rest/webmvc/BaseUri.java | 25 + .../webmvc/RepositorySearchController.java | 21 +- .../data/rest/webmvc/alps/AlpsController.java | 149 ++++++ .../alps/AlpsJsonHttpMessageConverter.java | 97 ++++ .../webmvc/alps/AlpsResourceProcessor.java | 68 +++ ...eInformationToAlpsDescriptorConverter.java | 453 ++++++++++++++++++ .../RepositoryRestMvcConfiguration.java | 66 ++- .../rest/webmvc/json/JacksonMetadata.java | 85 ++++ ...PersistentEntityToJsonSchemaConverter.java | 16 +- .../webmvc/support/RepositoryEntityLinks.java | 3 +- .../webmvc/support/RepositoryLinkBuilder.java | 28 +- .../data/rest/webmvc/util/UriUtils.java | 16 +- .../rest-default-messages.properties | 4 + .../webmvc/AbstractWebIntegrationTests.java | 18 + ...itorySearchControllerIntegrationTests.java | 8 +- .../alps/AlpsControllerIntegrationTests.java | 119 +++++ .../data/rest/webmvc/jpa/BookRepository.java | 2 +- .../data/rest/webmvc/jpa/Order.java | 8 + .../data/rest/webmvc/jpa/OrderRepository.java | 8 +- .../data/rest/webmvc/jpa/OrderSummary.java | 3 + .../data/rest/webmvc/jpa/Type.java | 24 + .../support/RepositoryLinkBuildUnitTests.java | 5 +- 39 files changed, 1643 insertions(+), 150 deletions(-) create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/MetadataConfiguration.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ParameterMetadata.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ParametersMetadata.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypedResourceDescription.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsJsonHttpMessageConverter.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsResourceProcessor.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonMetadata.java create mode 100644 spring-data-rest-webmvc/src/main/resources/rest-default-messages.properties create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/alps/AlpsControllerIntegrationTests.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Type.java diff --git a/pom.xml b/pom.xml index 1a5300236..b862fb3d4 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ 3.2.0.BUILD-SNAPSHOT 1.5.0.BUILD-SNAPSHOT - 4.2.0.Final + 4.3.5.Final false @@ -74,13 +74,6 @@ - - org.hibernate.javax.persistence - hibernate-jpa-2.0-api - 1.0.1.Final - test - - org.hibernate hibernate-entitymanager @@ -119,7 +112,7 @@ org.hsqldb hsqldb - 2.2.8 + 2.3.2 test 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 373cafeae..f65002de1 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 @@ -27,7 +27,7 @@ import java.lang.annotation.Target; * @author Jon Brisbin * @author Oliver Gierke */ -@Target({ ElementType.FIELD, ElementType.METHOD }) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface Description { diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/MetadataConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/MetadataConfiguration.java new file mode 100644 index 000000000..95c5a01f3 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/MetadataConfiguration.java @@ -0,0 +1,66 @@ +/* + * 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.config; + +/** + * Configuration for metadata exposure. + * + * @author Oliver Gierke + */ +public class MetadataConfiguration { + + private boolean omitUnresolvableDescriptionKeys = true; + private boolean alpsEnabled = true; + + /** + * Configures whether to omit documentation attributes for unresolvable resource bundle keys. Defaults to + * {@literal true}, which means that an unsuccessful attempt to resolve the message will cause no documentation entry + * to be rendered for the metadata resources. + * + * @param omitUnresolvableDescriptionKeys whether to omit documentation attributes for unresolvable resource bundle + * keys. + */ + public void setOmitUnresolvableDescriptionKeys(boolean omitUnresolvableDescriptionKeys) { + this.omitUnresolvableDescriptionKeys = omitUnresolvableDescriptionKeys; + } + + /** + * Returns whether to omit documentation attributes for unresolvable resource bundle keys. + * + * @return the omitUnresolvableDescriptionKeys + */ + public boolean omitUnresolvableDescriptionKeys() { + return omitUnresolvableDescriptionKeys; + } + + /** + * Configures whether to expose the ALPS resources. + * + * @param alpsEnabled the alpsEnabled to set + */ + public void setAlpsEnabled(boolean enableAlps) { + this.alpsEnabled = enableAlps; + } + + /** + * Returns whether the ALPS resources are exposed. + * + * @return the alpsEnabled + */ + public boolean alpsEnabled() { + return alpsEnabled; + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/ProjectionDefinitionConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/ProjectionDefinitionConfiguration.java index 9175fe344..03147f335 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/ProjectionDefinitionConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/ProjectionDefinitionConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.data.rest.core.config; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.rest.core.projection.ProjectionDefinitions; @@ -142,6 +143,27 @@ public class ProjectionDefinitionConfiguration implements ProjectionDefinitions return false; } + /** + * Returns all projections registered for the given source type. + * + * @param sourceType must not be {@literal null}. + * @return + */ + public Map> getProjectionsFor(Class sourceType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + + Map> result = new HashMap>(); + + for (Entry> entry : projectionDefinitions.entrySet()) { + if (entry.getKey().sourceType.equals(sourceType)) { + result.put(entry.getKey().name, entry.getValue()); + } + } + + return result; + } + /** * Value object to define lookup keys for projections. * diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java index c0088b830..85ce70ea4 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java @@ -45,23 +45,29 @@ public class RepositoryRestConfiguration { private ResourceMappingConfiguration domainMappings = new ResourceMappingConfiguration(); private ResourceMappingConfiguration repoMappings = new ResourceMappingConfiguration(); private final ProjectionDefinitionConfiguration projectionConfiguration; + private final MetadataConfiguration metadataConfiguration; /** * Creates a new default {@link RepositoryRestConfiguration}. */ public RepositoryRestConfiguration() { - this(new ProjectionDefinitionConfiguration()); + this(new ProjectionDefinitionConfiguration(), new MetadataConfiguration()); } /** * Creates a new {@link RepositoryRestConfiguration} with the given {@link ProjectionDefinitionConfiguration}. * * @param projectionConfiguration must not be {@literal null}. + * @param metadataConfiguration must not be {@literal null}. */ - public RepositoryRestConfiguration(ProjectionDefinitionConfiguration projectionConfiguration) { + public RepositoryRestConfiguration(ProjectionDefinitionConfiguration projectionConfiguration, + MetadataConfiguration metadataConfiguration) { Assert.notNull(projectionConfiguration, "ProjectionDefinitionConfiguration must not be null!"); + Assert.notNull(metadataConfiguration, "MetadataConfiguration must not be null!"); + this.projectionConfiguration = projectionConfiguration; + this.metadataConfiguration = metadataConfiguration; } /** @@ -76,14 +82,25 @@ public class RepositoryRestConfiguration { /** * The base URI against which the exporter should calculate its links. * - * @param baseUri The base URI. + * @param baseUri must not be {@literal null}. */ public RepositoryRestConfiguration setBaseUri(URI baseUri) { - Assert.notNull(baseUri, "The baseUri cannot be null."); + Assert.notNull(baseUri, "The base URI cannot be null."); this.baseUri = baseUri; return this; } + /** + * The base URI against which the exporter should calculate its links. + * + * @param baseUri must not be {@literal null}. + */ + public RepositoryRestConfiguration setBaseUri(String baseUri) { + Assert.notNull(baseUri, "The base URI cannot be null."); + this.baseUri = URI.create(baseUri); + return this; + } + /** * Get the default size of {@link org.springframework.data.domain.Pageable}s. Default is 20. * @@ -381,4 +398,13 @@ public class RepositoryRestConfiguration { public ProjectionDefinitionConfiguration projectionConfiguration() { return projectionConfiguration; } + + /** + * Returns the {@link MetadataConfiguration} to customize metadata exposure. + * + * @return + */ + public MetadataConfiguration metadataConfiguration() { + return metadataConfiguration; + } } 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 index 5d2f9674f..b73d75cb7 100644 --- 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.rest.core.mapping; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.rest.core.annotation.Description; import org.springframework.http.MediaType; import org.springframework.util.Assert; @@ -26,7 +27,7 @@ import org.springframework.util.StringUtils; * * @author Oliver Gierke */ -class AnnotationBasedResourceDescription extends ResolvableResourceDescriptionSupport { +public class AnnotationBasedResourceDescription extends ResolvableResourceDescriptionSupport { private final String message; private final ResourceDescription fallback; @@ -37,7 +38,7 @@ class AnnotationBasedResourceDescription extends ResolvableResourceDescriptionSu * @param description must not be {@literal null}. * @param fallback must not be {@literal null}. */ - AnnotationBasedResourceDescription(Description description, ResourceDescription fallback) { + public AnnotationBasedResourceDescription(Description description, ResourceDescription fallback) { Assert.notNull(description, "Description must not be null!"); Assert.notNull(fallback, "Fallback resource description must not be null!"); @@ -46,9 +47,28 @@ class AnnotationBasedResourceDescription extends ResolvableResourceDescriptionSu this.fallback = fallback; } - /** - * @return the message + public AnnotationBasedResourceDescription(Class type, ResourceDescription fallback) { + + Description description = AnnotationUtils.findAnnotation(type, Description.class); + + this.message = description == null ? null : description.value(); + this.fallback = fallback; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.MessageSourceResolvable#getCodes() */ + @Override + public String[] getCodes() { + return fallback.getCodes(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceDescription#getMessage() + */ + @Override public String getMessage() { return StringUtils.hasText(message) ? message : fallback.getMessage(); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java index c33242d87..fb8a66964 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java @@ -16,7 +16,6 @@ package org.springframework.data.rest.core.mapping; import java.lang.reflect.Method; -import java.util.List; /** * A {@link ResourceMapping} that is backed by a {@link Method}. @@ -33,9 +32,9 @@ public interface MethodResourceMapping extends ResourceMapping { Method getMethod(); /** - * Returns the names of the parameters the method exposes. + * Returns {@link ParameterMetadata} instances for all named parameters. * * @return */ - List getParameterNames(); + ParametersMetadata getParametersMetadata(); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ParameterMetadata.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ParameterMetadata.java new file mode 100644 index 000000000..b675647ee --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ParameterMetadata.java @@ -0,0 +1,103 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.mapping; + +import org.springframework.core.MethodParameter; +import org.springframework.data.rest.core.annotation.Description; +import org.springframework.util.Assert; + +/** + * Value object to capture metadata for query method parameters. + * + * @author Oliver Gierke + */ +public final class ParameterMetadata { + + private final String name; + private final ResourceDescription description; + + /** + * Creates a new {@link ParameterMetadata} for the given {@link MethodParameter} and base rel. + * + * @param name must not be {@literal null} or empty. + * @param baseRel must not be {@literal null} or empty. + */ + public ParameterMetadata(MethodParameter parameter, String baseRel) { + + this.name = parameter.getParameterName(); + + Assert.hasText(name, "Parameter must not be null or empty!"); + Assert.hasText(baseRel, "Method rel must not be null!"); + + ResourceDescription fallback = TypedResourceDescription.defaultFor(baseRel.concat(".").concat(name), + parameter.getParameterType()); + Description annotation = parameter.getParameterAnnotation(Description.class); + + this.description = annotation == null ? fallback : new AnnotationBasedResourceDescription(annotation, fallback); + } + + /** + * Return sthe name of the method parameter. + * + * @return the name + */ + public String getName() { + return name; + } + + /** + * Returns the description for the method parameter. + * + * @return the description + */ + public ResourceDescription getDescription() { + return description; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof ParameterMetadata)) { + return false; + } + + ParameterMetadata that = (ParameterMetadata) obj; + return this.name.equals(that.name) && this.description.equals(that.description); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + + result += 31 * name.hashCode(); + result += 31 * description.hashCode(); + + return result; + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ParametersMetadata.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ParametersMetadata.java new file mode 100644 index 000000000..4951d7dc9 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ParametersMetadata.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.mapping; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Value object for a list of {@link ParameterMetadata} instances. + * + * @author Oliver Gierke + */ +public class ParametersMetadata implements Iterable { + + private final List parameterMetadata; + + /** + * Creates a new {@link ParametersMetadata} instance for the given {@link ParameterMetadata} instances. + * + * @param parameterMetadata must not be {@literal null}. + */ + ParametersMetadata(List parameterMetadata) { + + Assert.notNull(parameterMetadata, "Parameter metadata must not be null!"); + + this.parameterMetadata = parameterMetadata; + } + + /** + * Returns all parameter names. + * + * @return + */ + public List getParameterNames() { + + List names = new ArrayList(parameterMetadata.size()); + + for (ParameterMetadata metadata : parameterMetadata) { + names.add(metadata.getName()); + } + + return names; + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return parameterMetadata.iterator(); + } +} 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 eb875f79b..c5b1e1340 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 @@ -47,7 +47,7 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { private final Method method; private final boolean paging; - private final List parameterNames; + private final List parameterMetadata; /** * Creates a new {@link RepositoryMethodResourceMapping} for the given {@link Method}. @@ -61,24 +61,24 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { Assert.notNull(resourceMapping, "ResourceMapping must not be null!"); RestResource annotation = AnnotationUtils.findAnnotation(method, RestResource.class); + String resourceRel = resourceMapping.getRel(); this.isExported = annotation != null ? annotation.exported() : true; this.rel = annotation == null || !StringUtils.hasText(annotation.rel()) ? method.getName() : annotation.rel(); this.path = annotation == null || !StringUtils.hasText(annotation.path()) ? new Path(method.getName()) : new Path( annotation.path()); this.method = method; - this.parameterNames = discoverParameterNames(method); + this.parameterMetadata = discoverParameterMetadata(method, resourceRel.concat(".").concat(rel)); this.paging = Arrays.asList(method.getParameterTypes()).contains(Pageable.class); } - private static final List discoverParameterNames(Method method) { + private static final List discoverParameterMetadata(Method method, String baseRel) { - List result = new ArrayList(); + List result = new ArrayList(); for (MethodParameter parameter : new MethodParameters(method, PARAM_VALUE).getParameters()) { - String name = parameter.getParameterName(); - if (name != null) { - result.add(name); + if (StringUtils.hasText(parameter.getParameterName())) { + result.add(new ParameterMetadata(parameter, baseRel)); } } @@ -123,11 +123,11 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { /* * (non-Javadoc) - * @see org.springframework.data.rest.core.mapping.MethodResourceMapping#getParameterNames() + * @see org.springframework.data.rest.core.mapping.MethodResourceMapping#getParameterMetadata() */ @Override - public List getParameterNames() { - return parameterNames; + public ParametersMetadata getParametersMetadata() { + return new ParametersMetadata(parameterMetadata); } /* diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java index 9a0cc31ce..44f8de13e 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java @@ -210,7 +210,7 @@ public class RepositoryResourceMappings implements ResourceMappings { * (non-Javadoc) * @see org.springframework.data.rest.core.mapping.ResourceMetadataProvider#getMappingFor(org.springframework.data.mapping.PersistentProperty) */ - ResourceMapping getMappingFor(PersistentProperty property) { + public ResourceMapping getMappingFor(PersistentProperty property) { ResourceMapping propertyMapping = propertyCache.get(property); @@ -322,8 +322,8 @@ public class RepositoryResourceMappings implements ResourceMappings { @Override public ResourceDescription getDescription() { - ResourceDescription fallback = SimpleResourceDescription.defaultFor(property, - ownerTypeMapping.getItemResourceRel()); + ResourceDescription fallback = TypedResourceDescription.defaultFor(ownerTypeMapping.getItemResourceRel(), + property); if (description != null) { return new AnnotationBasedResourceDescription(description, fallback); 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 index ac6816757..f26d99ef7 100644 --- 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 @@ -24,15 +24,6 @@ import org.springframework.context.MessageSourceResolvable; */ 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() 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 index 0fef09276..58f69e71b 100644 --- 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 @@ -15,8 +15,8 @@ */ package org.springframework.data.rest.core.mapping; -import org.springframework.data.mapping.PersistentProperty; import org.springframework.http.MediaType; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -24,49 +24,61 @@ import org.springframework.util.StringUtils; */ public class SimpleResourceDescription extends ResolvableResourceDescriptionSupport { - private static final String DEFAULT_KEY_PREFIX = "rest.description"; - private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.TEXT_PLAIN; + protected static final String DEFAULT_KEY_PREFIX = "rest.description"; + protected static final MediaType DEFAULT_MEDIA_TYPE = MediaType.TEXT_PLAIN; - private String message; - private MediaType type; + private final String message; + private final MediaType mediaType; + + /** + * Creates a new {@link SimpleResourceDescription} with the given message and {@link MediaType}. + * + * @param message must not be {@literal null} or empty. + * @param mediaType must not be {@literal null} or empty. + */ + protected SimpleResourceDescription(String message, MediaType mediaType) { + + Assert.hasText(message, "Message must not be null or empty!"); + Assert.notNull(mediaType, "MediaType must not be null!"); - 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); + this.mediaType = mediaType; } public static ResourceDescription defaultFor(String rel) { - - String message = String.format("%s.%s", DEFAULT_KEY_PREFIX, rel); - return new SimpleResourceDescription(message, DEFAULT_MEDIA_TYPE); + return new SimpleResourceDescription(String.format("%s.%s", DEFAULT_KEY_PREFIX, rel), DEFAULT_MEDIA_TYPE); } - public static ResourceDescription defaultForCollection(Class type) { - return null; - } - - public static ResourceDescription defaultForMethod(RepositoryMethodResourceMapping mapping) { - return null; - } - - /** - * @return the message + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceDescription#getMessage() */ public String getMessage() { return message; } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceDescription#getType() + */ public MediaType getType() { - return type; + return mediaType; } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceDescription#isDefault() + */ public boolean isDefault() { return StringUtils.hasText(message) && message.startsWith(DEFAULT_KEY_PREFIX); } + + /* + * (non-Javadoc) + * @see org.springframework.context.MessageSourceResolvable#getCodes() + */ + @Override + public String[] getCodes() { + return new String[] { message }; + } } 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 d3972f686..89f6f02a3 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 @@ -127,6 +127,14 @@ class TypeBasedCollectionResourceMapping implements CollectionResourceMapping { ResourceDescription fallback = SimpleResourceDescription.defaultFor(getRel()); + 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/TypedResourceDescription.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypedResourceDescription.java new file mode 100644 index 000000000..1e22a3685 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypedResourceDescription.java @@ -0,0 +1,82 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.mapping; + +import java.util.Arrays; + +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.http.MediaType; +import org.springframework.util.StringUtils; + +/** + * {@link SimpleResourceDescription} that additionally captures a type to be able to potentially create a reasonable + * default message. The implementation will do so for enum types by rendering the available values as default message + * and also provide them as arguments for message resolution. + * + * @author Oliver Gierke + */ +public class TypedResourceDescription extends SimpleResourceDescription { + + private final Class type; + + /** + * Creates a new {@link TypedResourceDescription} for the given message, {@link MediaType} and type. + * + * @param message must not be {@literal null} or empty. + * @param mediaType must not be {@literal null} or empty. + * @param type can be {@literal null}, defaults to {@link Object}. + */ + private TypedResourceDescription(String message, MediaType mediaType, Class type) { + + super(message, mediaType); + + this.type = type == null ? Object.class : type; + } + + public static ResourceDescription defaultFor(String rel, PersistentProperty property) { + + String message = String.format("%s.%s.%s", DEFAULT_KEY_PREFIX, rel, property.getName()); + return new TypedResourceDescription(message, DEFAULT_MEDIA_TYPE, property.getType()); + } + + public static ResourceDescription defaultFor(String rel, Class type) { + + String message = String.format("%s.%s", DEFAULT_KEY_PREFIX, rel); + return new TypedResourceDescription(message, DEFAULT_MEDIA_TYPE, type); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResolvableResourceDescriptionSupport#getArguments() + */ + @Override + public Object[] getArguments() { + return type.isEnum() ? new Object[] { getEnumValues(type) } : new Object[0]; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResolvableResourceDescriptionSupport#getDefaultMessage() + */ + @Override + public String getDefaultMessage() { + return type.isEnum() ? getEnumValues(type) : null; + } + + private String getEnumValues(Class type) { + return StringUtils.collectionToDelimitedString(Arrays.asList(type.getEnumConstants()), ", "); + } +} diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java index 9837fdad1..32535b028 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java @@ -67,7 +67,7 @@ public class RepositoryMethodResourceMappingUnitTests { Method method = PersonRepository.class.getMethod("findByLastname", String.class); MethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); - assertThat(mapping.getParameterNames(), is(emptyIterable())); + assertThat(mapping.getParametersMetadata().getParameterNames(), is(emptyIterable())); } /** @@ -79,8 +79,8 @@ public class RepositoryMethodResourceMappingUnitTests { Method method = PersonRepository.class.getMethod("findByFirstname", String.class); MethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); - assertThat(mapping.getParameterNames(), hasSize(1)); - assertThat(mapping.getParameterNames(), hasItem("firstname")); + assertThat(mapping.getParametersMetadata().getParameterNames(), hasSize(1)); + assertThat(mapping.getParametersMetadata().getParameterNames(), hasItem("firstname")); } /** diff --git a/spring-data-rest-webmvc/pom.xml b/spring-data-rest-webmvc/pom.xml index 338da2ea9..3b0b58808 100644 --- a/spring-data-rest-webmvc/pom.xml +++ b/spring-data-rest-webmvc/pom.xml @@ -39,21 +39,6 @@ provided - - - - org.springframework - spring-orm - true - - - - org.hibernate.javax.persistence - hibernate-jpa-2.0-api - 1.0.1.Final - true - - @@ -101,6 +86,21 @@ 1.7 + + + + org.springframework + spring-orm + true + + + + org.hibernate.javax.persistence + hibernate-jpa-2.0-api + 1.0.1.Final + true + + diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUri.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUri.java index 0a3205653..145409b00 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUri.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUri.java @@ -26,6 +26,7 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UrlPathHelper; @@ -54,6 +55,15 @@ public class BaseUri { this.baseUri = URI.create(trimTrailingCharacter(trimTrailingCharacter(uriString, '/'), '/')); } + /** + * Creates a new {@link BaseUri} with the given URI as base. + * + * @param uri must not be {@literal null}. + */ + public BaseUri(String uri) { + this(URI.create(uri)); + } + /** * Returns the base URI. * @@ -131,4 +141,19 @@ public class BaseUri { return null; } + + /** + * Returns a new {@link UriComponentsBuilder} for the base URI. If the base URI is not absolute, it'll lokup the URI + * for the current servlet mapping and extend it accordingly. + * + * @return + */ + public UriComponentsBuilder getUriComponentsBuilder() { + + if (baseUri.isAbsolute()) { + return UriComponentsBuilder.fromUri(baseUri); + } + + return ServletUriComponentsBuilder.fromCurrentServletMapping().path(baseUri.toString()); + } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java index ccb3dbb52..520242a0d 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -19,7 +19,6 @@ import static org.springframework.data.rest.webmvc.ControllerUtils.*; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -28,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.data.rest.core.invoke.RepositoryInvoker; import org.springframework.data.rest.core.mapping.MethodResourceMapping; +import org.springframework.data.rest.core.mapping.ParameterMetadata; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.SearchResourceMappings; import org.springframework.data.web.PagedResourcesAssembler; @@ -38,6 +38,9 @@ import org.springframework.hateoas.Links; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; +import org.springframework.hateoas.TemplateVariable; +import org.springframework.hateoas.TemplateVariable.VariableType; +import org.springframework.hateoas.TemplateVariables; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -45,7 +48,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -63,7 +65,6 @@ class RepositorySearchController extends AbstractRepositoryRestController { private static final String SEARCH = "/search"; private static final String BASE_MAPPING = "/{repository}" + SEARCH; - private static final String PARAMETER_NAME_TEMPALTE_PATTERN = "{?%s}"; private final EntityLinks entityLinks; private final ResourceMappings mappings; @@ -302,8 +303,13 @@ class RepositorySearchController extends AbstractRepositoryRestController { continue; } - String parameterTemplateVariable = getParameterTemplateVariable(mapping.getParameterNames()); - String href = builder.slash(mapping.getPath()).toString().concat(parameterTemplateVariable); + TemplateVariables variables = new TemplateVariables(); + + for (ParameterMetadata metadata : mapping.getParametersMetadata()) { + variables = variables.concat(new TemplateVariable(metadata.getName(), VariableType.REQUEST_PARAM)); + } + + String href = builder.slash(mapping.getPath()).toString().concat(variables.toString()); Link link = new Link(href, mapping.getRel()); @@ -317,11 +323,6 @@ class RepositorySearchController extends AbstractRepositoryRestController { return new Links(links); } - private static String getParameterTemplateVariable(Collection parameters) { - String parameterString = StringUtils.collectionToCommaDelimitedString(parameters); - return parameters.isEmpty() ? "" : String.format(PARAMETER_NAME_TEMPALTE_PATTERN, parameterString); - } - /** * Verifies that the given {@link RootResourceInformation} has searches exposed. * diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java new file mode 100644 index 000000000..591ea4009 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java @@ -0,0 +1,149 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.alps; + +import static org.springframework.web.bind.annotation.RequestMethod.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.BaseUri; +import org.springframework.data.rest.webmvc.RepositoryRestController; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.data.rest.webmvc.RootResourceInformation; +import org.springframework.hateoas.alps.Alps; +import org.springframework.hateoas.alps.Descriptor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Controller exposing semantic documentation for the resources exposed using the Application Level Profile Semantics + * format. + * + * @author Oliver Gierke + * @see http://alps.io + */ +@RepositoryRestController +@RequestMapping(AlpsController.ALPS_ROOT_MAPPING) +public class AlpsController { + + static final String ALPS_ROOT_MAPPING = "/alps"; + + private final Repositories repositories; + private final ResourceMappings mappings; + private final RepositoryRestConfiguration configuration; + + /** + * Creates a new {@link AlpsController} for the given {@link Repositories}, + * {@link RootResourceInformationToAlpsDescriptorConverter} and {@link ResourceMappings}. + * + * @param repositories must not be {@literal null}. + * @param mappings must not be {@literal null}. + * @param configuration must not be {@literal null}. + */ + @Autowired + public AlpsController(Repositories repositories, ResourceMappings mappings, RepositoryRestConfiguration configuration) { + + Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(mappings, "ResourceMappings must not be null!"); + Assert.notNull(configuration, "MetadataConfiguration must not be null!"); + + this.repositories = repositories; + this.mappings = mappings; + this.configuration = configuration; + } + + /** + * Exposes the allowed HTTP methods for the ALPS resources. + * + * @return + */ + @RequestMapping(value = { "", "/{repository}" }, method = OPTIONS) + HttpEntity alpsOptions() { + + verifyAlpsEnabled(); + + HttpHeaders headers = new HttpHeaders(); + headers.setAllow(Collections.singleton(HttpMethod.GET)); + + return new ResponseEntity(headers, HttpStatus.OK); + } + + /** + * Exposes a resource to contain descriptors pointing to the discriptors for individual resources. + * + * @return + */ + @RequestMapping(method = GET) + HttpEntity alps() { + + verifyAlpsEnabled(); + + List descriptors = new ArrayList(); + + for (Class domainType : repositories) { + + ResourceMetadata mapping = mappings.getMappingFor(domainType); + + if (mapping.isExported()) { + + BaseUri baseUri = new BaseUri(configuration.getBaseUri()); + UriComponentsBuilder builder = baseUri.getUriComponentsBuilder().path(ALPS_ROOT_MAPPING); + String href = builder.path(mapping.getPath().toString()).build().toUriString(); + descriptors.add(Alps.descriptor().name(mapping.getRel()).href(href).build()); + } + } + + Alps alps = Alps.alps().// + descriptors(descriptors).// + build(); + + return new ResponseEntity(alps, HttpStatus.OK); + } + + /** + * Exposes an ALPS resource to describe an individual repository resource. + * + * @param information + * @return + */ + @RequestMapping(value = "/{repository}", method = GET) + HttpEntity descriptor(RootResourceInformation information) { + + verifyAlpsEnabled(); + + return new ResponseEntity(information, HttpStatus.OK); + } + + private void verifyAlpsEnabled() { + + if (!configuration.metadataConfiguration().alpsEnabled()) { + throw new ResourceNotFoundException(); + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsJsonHttpMessageConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsJsonHttpMessageConverter.java new file mode 100644 index 000000000..b53ba8440 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsJsonHttpMessageConverter.java @@ -0,0 +1,97 @@ +/* + * 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.alps; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Arrays; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.rest.webmvc.RootResourceInformation; +import org.springframework.hateoas.alps.Alps; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * {@link HttpMessageConverter} to render {@link Alps} and {@link RootResourceInformation} instances as + * {@code application/alps+json}. + * + * @author Oliver Gierke + */ +public class AlpsJsonHttpMessageConverter extends MappingJackson2HttpMessageConverter { + + private static final MediaType ALPS_MEDIA_TYPE = MediaType.parseMediaType("application/alps+json"); + + private final RootResourceInformationToAlpsDescriptorConverter converter; + + /** + * Creates a new {@link AlpsJsonHttpMessageConverter} for the given {@link Converter}. + * + * @param converter must not be {@literal null}. + */ + public AlpsJsonHttpMessageConverter(RootResourceInformationToAlpsDescriptorConverter converter) { + + Assert.notNull(converter, "Converter must not be null!"); + + this.converter = converter; + + ObjectMapper mapper = getObjectMapper(); + mapper.setSerializationInclusion(Include.NON_EMPTY); + + setPrettyPrint(true); + setSupportedMediaTypes(Arrays.asList(ALPS_MEDIA_TYPE, MediaType.APPLICATION_JSON, MediaType.ALL)); + } + + /* + * (non-Javadoc) + * @see org.springframework.http.converter.json.MappingJackson2HttpMessageConverter#canWrite(java.lang.Class, org.springframework.http.MediaType) + */ + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return (clazz.isAssignableFrom(Alps.class) || clazz.isAssignableFrom(RootResourceInformation.class)) + && super.canWrite(clazz, mediaType); + } + + /* + * (non-Javadoc) + * @see org.springframework.http.converter.json.MappingJackson2HttpMessageConverter#canRead(java.lang.reflect.Type, java.lang.Class, org.springframework.http.MediaType) + */ + @Override + public boolean canRead(Type type, Class contextClass, MediaType mediaType) { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.http.converter.json.MappingJackson2HttpMessageConverter#writeInternal(java.lang.Object, org.springframework.http.HttpOutputMessage) + */ + @Override + protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, + HttpMessageNotWritableException { + + Object toWrite = object instanceof RootResourceInformation ? converter.convert((RootResourceInformation) object) + : object; + + super.writeInternal(toWrite, outputMessage); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsResourceProcessor.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsResourceProcessor.java new file mode 100644 index 000000000..9636716ce --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsResourceProcessor.java @@ -0,0 +1,68 @@ +/* + * 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.alps; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.data.rest.webmvc.BaseUri; +import org.springframework.data.rest.webmvc.RepositoryLinksResource; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.ResourceProcessor; +import org.springframework.util.Assert; + +/** + * {@link ResourceProcessor} to add a {@code profile} link to the root resource to point to the ALPS resources in case + * the support for ALPS is activated. + * + * @author Oliver Gierke + */ +public class AlpsResourceProcessor implements ResourceProcessor { + + private static final String PROFILE_REL = "profile"; + + private final RepositoryRestConfiguration configuration; + + /** + * Creates a new {@link AlpsResourceProcessor} with the given {@link RepositoryRestConfiguration}. + * + * @param configuration must not be {@literal null}. + */ + @Autowired + public AlpsResourceProcessor(RepositoryRestConfiguration configuration) { + + Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!"); + + this.configuration = configuration; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.ResourceProcessor#process(org.springframework.hateoas.ResourceSupport) + */ + @Override + public RepositoryLinksResource process(RepositoryLinksResource resource) { + + if (configuration.metadataConfiguration().alpsEnabled()) { + + BaseUri baseUri = new BaseUri(configuration.getBaseUri()); + String href = baseUri.getUriComponentsBuilder().path(AlpsController.ALPS_ROOT_MAPPING).build().toString(); + + resource.add(new Link(href, PROFILE_REL)); + } + + return resource; + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java new file mode 100644 index 000000000..586398442 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java @@ -0,0 +1,453 @@ +/* + * 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.alps; + +import static org.springframework.hateoas.alps.Alps.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.SimpleAssociationHandler; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.core.annotation.Description; +import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.data.rest.core.mapping.AnnotationBasedResourceDescription; +import org.springframework.data.rest.core.mapping.MethodResourceMapping; +import org.springframework.data.rest.core.mapping.ParameterMetadata; +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.core.mapping.SimpleResourceDescription; +import org.springframework.data.rest.webmvc.ResourceType; +import org.springframework.data.rest.webmvc.RootResourceInformation; +import org.springframework.data.rest.webmvc.json.JacksonMetadata; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; +import org.springframework.data.rest.webmvc.mapping.PropertyMappings; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.TemplateVariable; +import org.springframework.hateoas.alps.Alps; +import org.springframework.hateoas.alps.Descriptor; +import org.springframework.hateoas.alps.Descriptor.DescriptorBuilder; +import org.springframework.hateoas.alps.Doc; +import org.springframework.hateoas.alps.Format; +import org.springframework.hateoas.alps.Type; +import org.springframework.hateoas.mvc.ControllerLinkBuilder; +import org.springframework.http.HttpMethod; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; + +/** + * Converter to create Alps {@link Descriptor} instances for a {@link RootResourceInformation}. + * + * @author Oliver Gierke + */ +public class RootResourceInformationToAlpsDescriptorConverter { + + private static final List UNDOCUMENTED_METHODS = Arrays.asList(HttpMethod.OPTIONS, HttpMethod.HEAD); + + private final Repositories repositories; + private final PersistentEntities persistentEntities; + private final ResourceMappings mappings; + private final EntityLinks entityLinks; + private final MessageSourceAccessor messageSource; + private final RepositoryRestConfiguration configuration; + private final ObjectMapper mapper; + + /** + * Creates a new {@link RootResourceInformationToAlpsDescriptorConverter} instance. + * + * @param mappings must not be {@literal null}. + * @param repositories must not be {@literal null}. + * @param entities must not be {@literal null}. + * @param entityLinks must not be {@literal null}. + * @param messageSource must not be {@literal null}. + * @param configuration must not be {@literal null}. + * @param mapper must not be {@literal null}. + */ + public RootResourceInformationToAlpsDescriptorConverter(ResourceMappings mappings, Repositories repositories, + PersistentEntities entities, EntityLinks entityLinks, MessageSourceAccessor messageSource, + RepositoryRestConfiguration configuration, ObjectMapper mapper) { + + this.mappings = mappings; + this.persistentEntities = entities; + this.repositories = repositories; + this.entityLinks = entityLinks; + this.messageSource = messageSource; + this.configuration = configuration; + this.mapper = mapper; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + public Alps convert(RootResourceInformation resourceInformation) { + + Class type = resourceInformation.getDomainType(); + List descriptors = new ArrayList(); + + Descriptor representationDescriptor = buildRepresentationDescriptor(type); + + descriptors.add(representationDescriptor); + + for (HttpMethod method : resourceInformation.getSupportedMethods(ResourceType.COLLECTION)) { + + if (!UNDOCUMENTED_METHODS.contains(method)) { + descriptors.add(buildCollectionResourceDescriptor(type, resourceInformation, representationDescriptor, method)); + } + } + + for (HttpMethod method : resourceInformation.getSupportedMethods(ResourceType.ITEM)) { + + if (!UNDOCUMENTED_METHODS.contains(method)) { + descriptors.add(buildItemResourceDescriptor(resourceInformation, representationDescriptor, method)); + } + } + + descriptors.addAll(buildSearchResourceDescriptors(resourceInformation.getPersistentEntity())); + + return Alps.alps().descriptors(descriptors).build(); + } + + private Descriptor buildRepresentationDescriptor(Class type) { + + ResourceMetadata metadata = mappings.getMappingFor(type); + + return descriptor().// + id(metadata.getItemResourceRel().concat("-representation")).// + doc(getDocFor(metadata.getItemResourceDescription())).// + descriptors(buildPropertyDescriptors(type, metadata.getItemResourceRel())).// + build(); + } + + private Descriptor buildCollectionResourceDescriptor(Class type, RootResourceInformation resourceInformation, + Descriptor representationDescriptor, HttpMethod method) { + + ResourceMetadata metadata = mappings.getMappingFor(type); + + List nestedDescriptors = new ArrayList(); + nestedDescriptors.addAll(getPaginationDescriptors(type, method)); + nestedDescriptors.addAll(getProjectionDescriptor(type, method)); + + Type descriptorType = getType(method); + return descriptor().// + id(prefix(method).concat(metadata.getRel())).// + name(metadata.getRel()).// + type(descriptorType).// + doc(getDocFor(metadata.getDescription())).// + rt("#" + representationDescriptor.getId()).// + descriptors(nestedDescriptors).build(); + } + + /** + * Builds a descriptor for the projection parameter of the given resource. + * + * @param metadata + * @param projectionConfiguration + * @return + */ + private Descriptor buildProjectionDescriptor(ResourceMetadata metadata) { + + ProjectionDefinitionConfiguration projectionConfiguration = configuration.projectionConfiguration(); + String projectionParameterName = projectionConfiguration.getParameterName(); + + Map> projections = projectionConfiguration.getProjectionsFor(metadata.getDomainType()); + List projectionDescriptors = new ArrayList(projections.size()); + + for (Entry> projection : projections.entrySet()) { + + Class type = projection.getValue(); + String key = String.format("%s.%s.%s", metadata.getRel(), projectionParameterName, projection.getKey()); + ResourceDescription fallback = SimpleResourceDescription.defaultFor(key); + AnnotationBasedResourceDescription projectionDescription = new AnnotationBasedResourceDescription(type, fallback); + + projectionDescriptors.add(// + descriptor().// + type(Type.SEMANTIC).// + name(projection.getKey()).// + doc(getDocFor(projectionDescription)).// + descriptors(createJacksonDescriptor(projection.getKey(), type)).// + build()); + } + + return descriptor().// + type(Type.SEMANTIC).// + name(projectionParameterName).// + doc(getDocFor(SimpleResourceDescription.defaultFor(projectionParameterName))).// + descriptors(projectionDescriptors).build(); + } + + private List createJacksonDescriptor(String name, Class type) { + + List descriptors = new ArrayList(); + + for (BeanPropertyDefinition definition : new JacksonMetadata(mapper, type)) { + + AnnotatedMethod getter = definition.getGetter(); + Description description = getter.getAnnotation(Description.class); + ResourceDescription fallback = SimpleResourceDescription.defaultFor(String.format("%s.%s", name, + definition.getName())); + ResourceDescription resourceDescription = description == null ? null : new AnnotationBasedResourceDescription( + description, fallback); + + descriptors.add(// + descriptor().// + name(definition.getName()).// + type(Type.SEMANTIC).// + doc(getDocFor(resourceDescription)).// + build()); + } + + return descriptors; + } + + private Descriptor buildItemResourceDescriptor(RootResourceInformation resourceInformation, + Descriptor representationDescriptor, HttpMethod method) { + + PersistentEntity entity = resourceInformation.getPersistentEntity(); + ResourceMetadata metadata = mappings.getMappingFor(entity.getType()); + + return descriptor().// + id(prefix(method).concat(metadata.getItemResourceRel())).// + name(metadata.getItemResourceRel()).// + type(getType(method)).// + doc(getDocFor(metadata.getItemResourceDescription())).// + rt("#".concat(representationDescriptor.getId())). // + descriptors(getProjectionDescriptor(entity.getType(), method)).// + build(); + } + + private List getProjectionDescriptor(Class type, HttpMethod method) { + + if (!Type.SAFE.equals(getType(method))) { + return Collections.emptyList(); + } + + ProjectionDefinitionConfiguration projectionConfiguration = configuration.projectionConfiguration(); + + return projectionConfiguration.hasProjectionFor(type) ? Arrays.asList(buildProjectionDescriptor(mappings + .getMappingFor(type))) : Collections. emptyList(); + } + + /** + * Creates the {@link Descriptor}s for pagination parameters. + * + * @param type + * @return + */ + private List getPaginationDescriptors(Class type, HttpMethod method) { + + RepositoryInformation information = repositories.getRepositoryInformationFor(type); + + if (!information.isPagingRepository() || !getType(method).equals(Type.SAFE)) { + return Collections.emptyList(); + } + + Link linkToCollectionResource = entityLinks.linkToCollectionResource(type); + List variables = linkToCollectionResource.getVariables(); + List descriptors = new ArrayList(variables.size()); + + ProjectionDefinitionConfiguration projectionConfiguration = configuration.projectionConfiguration(); + + for (TemplateVariable variable : variables) { + + // Skip projection parameter + if (projectionConfiguration.getParameterName().equals(variable.getName())) { + continue; + } + + ResourceDescription description = SimpleResourceDescription.defaultFor(variable.getDescription()); + + descriptors.add(// + descriptor().// + name(variable.getName()).// + type(Type.SEMANTIC).// + doc(getDocFor(description)).// + build()); + } + + return descriptors; + } + + private List buildPropertyDescriptors(Class type, final String baseRel) { + + PersistentEntity entity = persistentEntities.getPersistentEntity(type); + final List propertyDescriptors = new ArrayList(); + final JacksonMetadata jackson = new JacksonMetadata(mapper, type); + final PropertyMappings propertyMappings = new PropertyMappings(mappings); + final AssociationLinks associationLinks = new AssociationLinks(mappings); + + entity.doWithProperties(new SimplePropertyHandler() { + + @Override + public void doWithPersistentProperty(PersistentProperty property) { + + BeanPropertyDefinition propertyDefinition = jackson.getDefinitionFor(property); + ResourceMapping propertyMapping = propertyMappings.getMappingFor(property); + + if (propertyDefinition != null) { + propertyDescriptors.add(// + descriptor(). // + type(Type.SEMANTIC).// + name(propertyDefinition.getName()).// + doc(getDocFor(propertyMapping.getDescription())).// + build()); + } + } + }); + + entity.doWithAssociations(new SimpleAssociationHandler() { + + @Override + public void doWithAssociation(Association> association) { + + PersistentProperty property = association.getInverse(); + ResourceMapping mapping = propertyMappings.getMappingFor(property); + + DescriptorBuilder builder = descriptor().// + name(mapping.getRel()).doc(getDocFor(mapping.getDescription())); + + if (associationLinks.isLinkableAssociation(property)) { + + ResourceMetadata targetTypeMapping = mappings.getMappingFor(property.getActualType()); + String localPath = targetTypeMapping.getRel().concat("#").concat(targetTypeMapping.getItemResourceRel()); + Link link = ControllerLinkBuilder.linkTo(AlpsController.class).slash(localPath).withSelfRel(); + + builder.// + type(Type.SAFE).// + rt(link.getHref()); + + } else { + + List nestedDescriptors = buildPropertyDescriptors(property.getActualType(), baseRel.concat(".") + .concat(mapping.getRel())); + + builder = builder.// + type(Type.SEMANTIC).// + descriptors(nestedDescriptors); + } + + propertyDescriptors.add(builder.build()); + } + }); + + return propertyDescriptors; + } + + private Collection buildSearchResourceDescriptors(PersistentEntity entity) { + + ResourceMetadata metadata = mappings.getMappingFor(entity.getType()); + List descriptors = new ArrayList(); + + for (MethodResourceMapping methodMapping : metadata.getSearchResourceMappings()) { + + List parameterDescriptors = new ArrayList(); + + for (ParameterMetadata parameterMetadata : methodMapping.getParametersMetadata()) { + + parameterDescriptors.add(// + descriptor().// + name(parameterMetadata.getName()).// + doc(getDocFor(parameterMetadata.getDescription())).// + type(Type.SEMANTIC)// + .build()); + } + + descriptors.add(descriptor().// + type(Type.SAFE).// + name(methodMapping.getRel()).// + descriptors(parameterDescriptors).// + build()); + } + + return descriptors; + } + + private Doc getDocFor(ResourceDescription description) { + + if (description == null) { + return null; + } + + String message = resolveMessage(description); + return message == null ? null : new Doc(message, Format.TEXT); + } + + private String resolveMessage(ResourceDescription description) { + + if (!description.isDefault()) { + return description.getMessage(); + } + + try { + return messageSource.getMessage(description); + } catch (NoSuchMessageException o_O) { + return configuration.metadataConfiguration().omitUnresolvableDescriptionKeys() ? null : description.getMessage(); + } + } + + private static String prefix(HttpMethod method) { + + switch (method) { + case GET: + return "get-"; + case POST: + return "create-"; + case DELETE: + return "delete-"; + case PUT: + return "update-"; + case PATCH: + return "patch-"; + default: + throw new IllegalArgumentException(method.name()); + } + } + + private static Type getType(HttpMethod method) { + + switch (method) { + case GET: + return Type.SAFE; + case PUT: + case DELETE: + return Type.IDEMPOTENT; + case POST: + case PATCH: + return Type.UNSAFE; + default: + return null; + } + } +} 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 ae0d39e33..f1d0da8fa 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 @@ -23,11 +23,13 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -39,6 +41,7 @@ import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.io.ClassPathResource; import org.springframework.data.domain.PageRequest; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoModule; @@ -48,6 +51,7 @@ import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.UriToEntityConverter; +import org.springframework.data.rest.core.config.MetadataConfiguration; import org.springframework.data.rest.core.config.Projection; import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; @@ -68,6 +72,9 @@ import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter; import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping; import org.springframework.data.rest.webmvc.RestMediaTypes; import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver; +import org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter; +import org.springframework.data.rest.webmvc.alps.AlpsResourceProcessor; +import org.springframework.data.rest.webmvc.alps.RootResourceInformationToAlpsDescriptorConverter; import org.springframework.data.rest.webmvc.convert.StringToDistanceConverter; import org.springframework.data.rest.webmvc.convert.StringToPointConverter; import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter; @@ -225,11 +232,16 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon configuration.addProjection(projection); } - RepositoryRestConfiguration config = new RepositoryRestConfiguration(configuration); + RepositoryRestConfiguration config = new RepositoryRestConfiguration(configuration, metadataConfiguration()); configureRepositoryRestConfiguration(config); return config; } + @Bean + public MetadataConfiguration metadataConfiguration() { + return new MetadataConfiguration(); + } + @Bean public BaseUri baseUri() { return new BaseUri(config().getBaseUri()); @@ -337,11 +349,21 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon @Bean public MessageSourceAccessor resourceDescriptionMessageSourceAccessor() { - ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); - messageSource.setBasename("classpath:rest-messages"); - messageSource.setUseCodeAsDefaultMessage(true); + try { - return new MessageSourceAccessor(messageSource); + PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); + propertiesFactoryBean.setLocation(new ClassPathResource("rest-default-messages.properties")); + propertiesFactoryBean.afterPropertiesSet(); + + ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); + messageSource.setBasename("classpath:rest-messages"); + messageSource.setCommonMessages(propertiesFactoryBean.getObject()); + + return new MessageSourceAccessor(messageSource); + + } catch (Exception o_O) { + throw new BeanCreationException("resourceDescriptionMessageSourceAccessor", "", o_O); + } } /** @@ -431,7 +453,6 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon * @return */ @Bean - @SuppressWarnings("rawtypes") public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() { List> messageConverters = defaultMessageConverters(); @@ -468,6 +489,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return mapping; } + @Bean + public RequestMappingHandlerMapping fallbackMapping() { + return new RequestMappingHandlerMapping(); + } + @Bean public ResourceMappings resourceMappings() { @@ -517,6 +543,10 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon List> messageConverters = new ArrayList>(); + if (config().metadataConfiguration().alpsEnabled()) { + messageConverters.add(new AlpsJsonHttpMessageConverter(alpsConverter())); + } + if (config().getDefaultMediaType().equals(MediaTypes.HAL_JSON)) { messageConverters.add(halJacksonHttpMessageConverter()); messageConverters.add(jacksonHttpMessageConverter()); @@ -524,6 +554,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon messageConverters.add(jacksonHttpMessageConverter()); messageConverters.add(halJacksonHttpMessageConverter()); } + messageConverters.add(uriListHttpMessageConverter()); return messageConverters; @@ -620,6 +651,29 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return new AnnotatedTypeScanner(Projection.class).findTypes(packagesToScan); } + // + // ALPS support + // + + @Bean + public RootResourceInformationToAlpsDescriptorConverter alpsConverter() { + + Repositories repositories = repositories(); + PersistentEntities persistentEntities = persistentEntities(); + RepositoryEntityLinks entityLinks = entityLinks(); + MessageSourceAccessor messageSourceAccessor = resourceDescriptionMessageSourceAccessor(); + RepositoryRestConfiguration config = config(); + ResourceMappings resourceMappings = resourceMappings(); + + return new RootResourceInformationToAlpsDescriptorConverter(resourceMappings, repositories, persistentEntities, + entityLinks, messageSourceAccessor, config, objectMapper()); + } + + @Bean + public AlpsResourceProcessor alpsResourceProcessor() { + return new AlpsResourceProcessor(config()); + } + /** * Override this method to add additional configuration. * diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonMetadata.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonMetadata.java new file mode 100644 index 000000000..3ee0689ee --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonMetadata.java @@ -0,0 +1,85 @@ +/* + * 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 java.util.Iterator; +import java.util.List; + +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; + +/** + * Value object to abstract Jackson based bean metadata of a given type. + * + * @author Oliver Gierke + */ +public class JacksonMetadata implements Iterable { + + private final List definitions; + + /** + * Creates a new {@link JacksonMetadata} instance for the given {@link ObjectMapper} and type. + * + * @param mapper must not be {@literal null}. + * @param type must not be {@literal null}. + */ + public JacksonMetadata(ObjectMapper mapper, Class type) { + + Assert.notNull(mapper, "ObjectMapper must not be null!"); + Assert.notNull(type, "Type must not be null!"); + + SerializationConfig serializationConfig = mapper.getSerializationConfig(); + JavaType javaType = serializationConfig.constructType(type); + BeanDescription description = serializationConfig.introspect(javaType); + + this.definitions = description.findProperties(); + } + + /** + * Returns the {@link BeanPropertyDefinition} for the given {@link PersistentProperty}. + * + * @param property must not be {@literal null}. + * @return can be {@literal null} in case there's no Jackson property to be exposed for the given + * {@link PersistentProperty}. + */ + public BeanPropertyDefinition getDefinitionFor(PersistentProperty property) { + + Assert.notNull(property, "PersistentProperty must not be null!"); + + for (BeanPropertyDefinition definition : definitions) { + if (definition.getInternalName().equals(property.getName())) { + return definition; + } + } + + return null; + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return definitions.iterator(); + } +} 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 8df9795b4..67467f084 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 @@ -20,6 +20,7 @@ import static org.springframework.util.StringUtils.*; import java.util.HashSet; import java.util.Set; +import org.springframework.context.NoSuchMessageException; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; @@ -114,8 +115,8 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric final PersistentEntity persistentEntity = repositories.getPersistentEntity((Class) source); final ResourceMetadata metadata = mappings.getMappingFor(persistentEntity.getType()); - final JsonSchema jsonSchema = new JsonSchema(persistentEntity.getName(), accessor.getMessage(metadata - .getItemResourceDescription())); + final JsonSchema jsonSchema = new JsonSchema(persistentEntity.getName(), + resolveMessage(metadata.getItemResourceDescription())); persistentEntity.doWithProperties(new SimplePropertyHandler() { @@ -131,7 +132,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric ResourceMapping propertyMapping = metadata.getMappingFor(persistentProperty); ResourceDescription description = propertyMapping.getDescription(); - String message = accessor.getMessage(description); + String message = resolveMessage(description); Property property = persistentProperty.isCollectionLike() ? // new ArrayProperty("array", message, false) @@ -151,4 +152,13 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric return jsonSchema; } + + private String resolveMessage(ResourceDescription description) { + + try { + return accessor.getMessage(description); + } catch (NoSuchMessageException o_O) { + return description.getMessage(); + } + } } 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 ae4f9fb4d..6fbe62c7c 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 @@ -26,6 +26,7 @@ import org.springframework.data.rest.core.config.ProjectionDefinitionConfigurati import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.BaseUri; import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter; import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver; @@ -99,7 +100,7 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { public LinkBuilder linkFor(Class type) { ResourceMetadata metadata = mappings.getMappingFor(type); - return new RepositoryLinkBuilder(metadata, config.getBaseUri()); + return new RepositoryLinkBuilder(metadata, new BaseUri(config.getBaseUri())); } /* diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuilder.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuilder.java index 7b2459225..82f71ac26 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuilder.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuilder.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. @@ -19,11 +19,11 @@ import java.net.URI; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.BaseUri; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkBuilder; import org.springframework.hateoas.core.LinkBuilderSupport; import org.springframework.util.Assert; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder; /** @@ -41,8 +41,8 @@ public class RepositoryLinkBuilder extends LinkBuilderSupport variables = new org.springframework.web.util.UriTemplate(mapping).match(lookupPath); + String value = variables.get(variable); - Map variables = new org.springframework.web.util.UriTemplate(mapping).match(lookupPath); - String value = variables.get(variable); - - if (value != null) { - return value; - } + if (value != null) { + return value; } return null; diff --git a/spring-data-rest-webmvc/src/main/resources/rest-default-messages.properties b/spring-data-rest-webmvc/src/main/resources/rest-default-messages.properties new file mode 100644 index 000000000..6bae86bf2 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/resources/rest-default-messages.properties @@ -0,0 +1,4 @@ +rest.description.pagination.page.description=The page to return. +rest.description.pagination.size.description=The size of the page to return. +rest.description.pagination.sort.description=The sorting criteria to use to calculate the content of the page. +rest.description.projection=The projection that shall be applied when rendering the response. Acceptable values available in nested descriptors. diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java index e895e8cf0..7bdef8ddd 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java @@ -460,6 +460,24 @@ public abstract class AbstractWebIntegrationTests { } } + /** + * @see DATAREST-230 + */ + @Test + public void exposesDescriptionAsAlpsDocuments() throws Exception { + + MediaType ALPS_MEDIA_TYPE = MediaType.valueOf("application/alps+json"); + + MockHttpServletResponse response = request("/"); + Link profileLink = assertHasLinkWithRel("profile", response); + + mvc.perform(// + get(profileLink.expand().getHref()).// + accept(ALPS_MEDIA_TYPE)).// + andExpect(status().isOk()).// + andExpect(content().contentType(ALPS_MEDIA_TYPE)); + } + protected abstract Iterable expectedRootLinkRels(); protected Map getPayloadToPost() throws Exception { diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java index e83dcad1c..a0cba6d11 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java @@ -25,9 +25,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.ResourceTester.HasSelfLink; import org.springframework.data.rest.webmvc.jpa.Address; +import org.springframework.data.rest.webmvc.jpa.Author; import org.springframework.data.rest.webmvc.jpa.CreditCard; import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; -import org.springframework.data.rest.webmvc.jpa.Order; import org.springframework.data.rest.webmvc.jpa.Person; import org.springframework.data.rest.webmvc.jpa.TestDataPopulator; import org.springframework.hateoas.PagedResources; @@ -73,14 +73,12 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll @Test(expected = ResourceNotFoundException.class) public void returns404ForUnexportedRepository() { - controller.listSearches(getResourceInformation(CreditCard.class)); } @Test(expected = ResourceNotFoundException.class) public void returns404ForRepositoryWithoutSearches() { - - controller.listSearches(getResourceInformation(Order.class)); + controller.listSearches(getResourceInformation(Author.class)); } @Test @@ -105,7 +103,7 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll */ @Test(expected = ResourceNotFoundException.class) public void doesNotExposeHeadForSearchResourceIfResourceDoesnHaveSearches() { - controller.headForSearches(getResourceInformation(Order.class)); + controller.headForSearches(getResourceInformation(Author.class)); } /** diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/alps/AlpsControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/alps/AlpsControllerIntegrationTests.java new file mode 100644 index 000000000..5f4966f21 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/alps/AlpsControllerIntegrationTests.java @@ -0,0 +1,119 @@ +/* + * 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.alps; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests; +import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.LinkDiscoverers; +import org.springframework.hateoas.core.JsonPathLinkDiscoverer; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +/** + * Integration tests for {@link AlpsController}. + * + * @author Oliver Gierke + */ +@WebAppConfiguration +@ContextConfiguration(classes = { JpaRepositoryConfig.class, AlpsControllerIntegrationTests.Config.class }) +public class AlpsControllerIntegrationTests extends AbstractControllerIntegrationTests { + + @Autowired WebApplicationContext context; + @Autowired LinkDiscoverers discoverers; + + @Configuration + static class Config { + + @Bean + public LinkDiscoverer alpsLinkDiscoverer() { + return new JsonPathLinkDiscoverer("$.descriptors[?(@.name == '%s')].href", + MediaType.valueOf("application/alps+json")); + } + } + + protected MockMvc mvc; + + @Before + public void setUp() { + mvc = MockMvcBuilders.webAppContextSetup(context).build(); + } + + /** + * @see DATAREST-230 + */ + @Test + public void exposesProfileLink() throws Exception { + + mvc.perform(get("/")).// + andExpect(status().is2xxSuccessful()).// + andExpect(jsonPath("$._links.profile.href", endsWith(AlpsController.ALPS_ROOT_MAPPING))); + } + + /** + * @see DATAREST-230 + */ + @Test + public void alpsResourceExposesResourcePerCollectionResource() throws Exception { + + Link profileLink = discoverUnique("/", "profile"); + + assertThat(discoverUnique(profileLink.getHref(), "orders"), is(notNullValue())); + assertThat(discoverUnique(profileLink.getHref(), "people"), is(notNullValue())); + } + + /** + * @see DATAREST-230 + */ + @Test + public void exposesAlpsCollectionResources() throws Exception { + + Link profileLink = discoverUnique("/", "profile"); + Link peopleLink = discoverUnique(profileLink.getHref(), "people"); + + mvc.perform(get(peopleLink.getHref())).// + andDo(print()).// + andExpect(jsonPath("$.version").value("1.0")).// + andExpect(jsonPath("$.descriptors[*].name", hasItems("people", "person"))); + } + + private Link discoverUnique(String href, String rel) throws Exception { + + MockHttpServletResponse response = mvc.perform(get(href)).// + andExpect(status().is2xxSuccessful()).// + andReturn().getResponse(); + + LinkDiscoverer discoverer = discoverers.getLinkDiscovererFor(MediaType.valueOf(response.getContentType())); + return discoverer.findLinkWithRel(rel, response.getContentAsString()); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java index 17ca86201..716ea0ff0 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.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. diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Order.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Order.java index 00189122d..97476b3cf 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Order.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Order.java @@ -41,6 +41,7 @@ public class Order { private Person creator; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)// private List lineItems = new ArrayList(); + private Type type = Type.TAKE_AWAY; public Order(Person creator) { this.creator = creator; @@ -72,4 +73,11 @@ public class Order { public BigDecimal getPrice() { return new BigDecimal(2.50); } + + /** + * @return the type + */ + public Type getType() { + return type; + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderRepository.java index e1fd96085..f610e8980 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderRepository.java @@ -15,13 +15,19 @@ */ package org.springframework.data.rest.webmvc.jpa; +import java.util.List; + import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.Description; import org.springframework.data.rest.core.annotation.RepositoryRestResource; /** * @author Oliver Gierke */ -@RepositoryRestResource +@RepositoryRestResource(collectionResourceDescription = @Description("Collection resource description"), + itemResourceDescription = @Description("Item resource description.")) public interface OrderRepository extends CrudRepository { + List findByType(@Param("type") Type type); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderSummary.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderSummary.java index a3017f8ac..f650250c8 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderSummary.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderSummary.java @@ -17,13 +17,16 @@ package org.springframework.data.rest.webmvc.jpa; import java.math.BigDecimal; +import org.springframework.data.rest.core.annotation.Description; import org.springframework.data.rest.core.config.Projection; /** * @author Oliver Gierke */ @Projection(name = "summary", types = Order.class) +@Description("A summary of an order.") public interface OrderSummary { + @Description("Price!!") BigDecimal getPrice(); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Type.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Type.java new file mode 100644 index 000000000..f95a48092 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Type.java @@ -0,0 +1,24 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.jpa; + +/** + * @author Oliver Gierke + */ +public enum Type { + + IN_STORE, TAKE_AWAY; +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuildUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuildUnitTests.java index b7ae2cdb5..80ff8480a 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuildUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuildUnitTests.java @@ -18,13 +18,12 @@ package org.springframework.data.rest.webmvc.support; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; -import java.net.URI; - import org.junit.Test; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.rest.core.mapping.MappingResourceMetadata; import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.BaseUri; import org.springframework.data.rest.webmvc.WebTestUtils; import org.springframework.data.rest.webmvc.mongodb.Profile; import org.springframework.hateoas.Link; @@ -62,7 +61,7 @@ public class RepositoryLinkBuildUnitTests { MongoPersistentEntity entity = context.getPersistentEntity(Profile.class); ResourceMetadata metadata = new MappingResourceMetadata(entity); - RepositoryLinkBuilder builder = new RepositoryLinkBuilder(metadata, URI.create(baseUri)); + RepositoryLinkBuilder builder = new RepositoryLinkBuilder(metadata, new BaseUri(baseUri)); Link link = builder.withSelfRel(); assertThat(link.getHref(), is(expectedUri));