DATAREST-230 - Support for serving Alps documents.
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
This commit is contained in:
11
pom.xml
11
pom.xml
@@ -33,7 +33,7 @@
|
||||
<springdata.neo4j>3.2.0.BUILD-SNAPSHOT</springdata.neo4j>
|
||||
<springdata.gemfire>1.5.0.BUILD-SNAPSHOT</springdata.gemfire>
|
||||
|
||||
<hibernate.version>4.2.0.Final</hibernate.version>
|
||||
<hibernate.version>4.3.5.Final</hibernate.version>
|
||||
|
||||
<bundlor.enabled>false</bundlor.enabled>
|
||||
</properties>
|
||||
@@ -74,13 +74,6 @@
|
||||
|
||||
<!-- Test dependencies -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate.javax.persistence</groupId>
|
||||
<artifactId>hibernate-jpa-2.0-api</artifactId>
|
||||
<version>1.0.1.Final</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-entitymanager</artifactId>
|
||||
@@ -119,7 +112,7 @@
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>2.2.8</version>
|
||||
<version>2.3.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Class<?>> getProjectionsFor(Class<?> sourceType) {
|
||||
|
||||
Assert.notNull(sourceType, "Source type must not be null!");
|
||||
|
||||
Map<String, Class<?>> result = new HashMap<String, Class<?>>();
|
||||
|
||||
for (Entry<ProjectionDefinitionKey, Class<?>> 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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<String> getParameterNames();
|
||||
ParametersMetadata getParametersMetadata();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ParameterMetadata> {
|
||||
|
||||
private final List<ParameterMetadata> parameterMetadata;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ParametersMetadata} instance for the given {@link ParameterMetadata} instances.
|
||||
*
|
||||
* @param parameterMetadata must not be {@literal null}.
|
||||
*/
|
||||
ParametersMetadata(List<ParameterMetadata> parameterMetadata) {
|
||||
|
||||
Assert.notNull(parameterMetadata, "Parameter metadata must not be null!");
|
||||
|
||||
this.parameterMetadata = parameterMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all parameter names.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<String> getParameterNames() {
|
||||
|
||||
List<String> names = new ArrayList<String>(parameterMetadata.size());
|
||||
|
||||
for (ParameterMetadata metadata : parameterMetadata) {
|
||||
names.add(metadata.getName());
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<ParameterMetadata> iterator() {
|
||||
return parameterMetadata.iterator();
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping {
|
||||
private final Method method;
|
||||
private final boolean paging;
|
||||
|
||||
private final List<String> parameterNames;
|
||||
private final List<ParameterMetadata> 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<String> discoverParameterNames(Method method) {
|
||||
private static final List<ParameterMetadata> discoverParameterMetadata(Method method, String baseRel) {
|
||||
|
||||
List<String> result = new ArrayList<String>();
|
||||
List<ParameterMetadata> result = new ArrayList<ParameterMetadata>();
|
||||
|
||||
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<String> getParameterNames() {
|
||||
return parameterNames;
|
||||
public ParametersMetadata getParametersMetadata() {
|
||||
return new ParametersMetadata(parameterMetadata);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()), ", ");
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,21 +39,6 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Optional store specifics -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-orm</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate.javax.persistence</groupId>
|
||||
<artifactId>hibernate-jpa-2.0-api</artifactId>
|
||||
<version>1.0.1.Final</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson -->
|
||||
|
||||
<dependency>
|
||||
@@ -101,6 +86,21 @@
|
||||
<version>1.7</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Optional store specifics -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-orm</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate.javax.persistence</groupId>
|
||||
<artifactId>hibernate-jpa-2.0-api</artifactId>
|
||||
<version>1.0.1.Final</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> 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.
|
||||
*
|
||||
|
||||
@@ -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<Object>(headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes a resource to contain descriptors pointing to the discriptors for individual resources.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(method = GET)
|
||||
HttpEntity<Alps> alps() {
|
||||
|
||||
verifyAlpsEnabled();
|
||||
|
||||
List<Descriptor> descriptors = new ArrayList<Descriptor>();
|
||||
|
||||
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>(alps, HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes an ALPS resource to describe an individual repository resource.
|
||||
*
|
||||
* @param information
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/{repository}", method = GET)
|
||||
HttpEntity<RootResourceInformation> descriptor(RootResourceInformation information) {
|
||||
|
||||
verifyAlpsEnabled();
|
||||
|
||||
return new ResponseEntity<RootResourceInformation>(information, HttpStatus.OK);
|
||||
}
|
||||
|
||||
private void verifyAlpsEnabled() {
|
||||
|
||||
if (!configuration.metadataConfiguration().alpsEnabled()) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<RepositoryLinksResource> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<HttpMethod> 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<Descriptor> descriptors = new ArrayList<Descriptor>();
|
||||
|
||||
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<Descriptor> nestedDescriptors = new ArrayList<Descriptor>();
|
||||
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<String, Class<?>> projections = projectionConfiguration.getProjectionsFor(metadata.getDomainType());
|
||||
List<Descriptor> projectionDescriptors = new ArrayList<Descriptor>(projections.size());
|
||||
|
||||
for (Entry<String, Class<?>> 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<Descriptor> createJacksonDescriptor(String name, Class<?> type) {
|
||||
|
||||
List<Descriptor> descriptors = new ArrayList<Descriptor>();
|
||||
|
||||
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<Descriptor> 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.<Descriptor> emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link Descriptor}s for pagination parameters.
|
||||
*
|
||||
* @param type
|
||||
* @return
|
||||
*/
|
||||
private List<Descriptor> 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<TemplateVariable> variables = linkToCollectionResource.getVariables();
|
||||
List<Descriptor> descriptors = new ArrayList<Descriptor>(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<Descriptor> buildPropertyDescriptors(Class<?> type, final String baseRel) {
|
||||
|
||||
PersistentEntity<?, ?> entity = persistentEntities.getPersistentEntity(type);
|
||||
final List<Descriptor> propertyDescriptors = new ArrayList<Descriptor>();
|
||||
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<? extends PersistentProperty<?>> 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<Descriptor> nestedDescriptors = buildPropertyDescriptors(property.getActualType(), baseRel.concat(".")
|
||||
.concat(mapping.getRel()));
|
||||
|
||||
builder = builder.//
|
||||
type(Type.SEMANTIC).//
|
||||
descriptors(nestedDescriptors);
|
||||
}
|
||||
|
||||
propertyDescriptors.add(builder.build());
|
||||
}
|
||||
});
|
||||
|
||||
return propertyDescriptors;
|
||||
}
|
||||
|
||||
private Collection<Descriptor> buildSearchResourceDescriptors(PersistentEntity<?, ?> entity) {
|
||||
|
||||
ResourceMetadata metadata = mappings.getMappingFor(entity.getType());
|
||||
List<Descriptor> descriptors = new ArrayList<Descriptor>();
|
||||
|
||||
for (MethodResourceMapping methodMapping : metadata.getSearchResourceMappings()) {
|
||||
|
||||
List<Descriptor> parameterDescriptors = new ArrayList<Descriptor>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
|
||||
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -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<BeanPropertyDefinition> {
|
||||
|
||||
private final List<BeanPropertyDefinition> 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<BeanPropertyDefinition> iterator() {
|
||||
return definitions.iterator();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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<RepositoryLinkBuil
|
||||
* @param metadata must not be {@literal null}.
|
||||
* @param baseUri
|
||||
*/
|
||||
public RepositoryLinkBuilder(ResourceMetadata metadata, URI baseUri) {
|
||||
this(metadata, prepareBuilder(baseUri, metadata));
|
||||
public RepositoryLinkBuilder(ResourceMetadata metadata, BaseUri baseUri) {
|
||||
this(metadata, baseUri.getUriComponentsBuilder().path(metadata.getPath().toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,26 +59,6 @@ public class RepositoryLinkBuilder extends LinkBuilderSupport<RepositoryLinkBuil
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the {@link UriComponentsBuilder} pointing to the root repository path. If the given URI is an absolute one
|
||||
* (starting with {@code http://}) we'll use it as is and fallback to lookup the root URI of the current request's
|
||||
* servlet mapping appending the base URI.
|
||||
*
|
||||
* @param baseUri must not be {@literal null}.
|
||||
* @param metadata must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static UriComponentsBuilder prepareBuilder(URI baseUri, ResourceMetadata metadata) {
|
||||
|
||||
Assert.notNull(baseUri, "Base URI must not be null!");
|
||||
Assert.notNull(metadata, "ResourceMetadata must not be null!");
|
||||
|
||||
UriComponentsBuilder builder = baseUri.isAbsolute() ? UriComponentsBuilder.fromUri(baseUri)
|
||||
: ServletUriComponentsBuilder.fromCurrentServletMapping().path(baseUri.toString());
|
||||
|
||||
return builder.path(metadata.getPath().toString());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.core.LinkBuilderSupport#slash(java.lang.Object)
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.data.rest.webmvc.util;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.hateoas.core.AnnotationMappingDiscoverer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@@ -28,6 +29,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
*/
|
||||
public abstract class UriUtils {
|
||||
|
||||
private static AnnotationMappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(RequestMapping.class);
|
||||
|
||||
private UriUtils() {}
|
||||
|
||||
/**
|
||||
@@ -43,16 +46,13 @@ public abstract class UriUtils {
|
||||
Assert.hasText(variable, "Variable name must not be null or empty!");
|
||||
Assert.notNull(method, "Method must not be null!");
|
||||
|
||||
RequestMapping annotation = method.getAnnotation(RequestMapping.class);
|
||||
String mapping = DISCOVERER.getMapping(method);
|
||||
|
||||
for (String mapping : annotation.value()) {
|
||||
Map<String, String> variables = new org.springframework.web.util.UriTemplate(mapping).match(lookupPath);
|
||||
String value = variables.get(variable);
|
||||
|
||||
Map<String, String> 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;
|
||||
|
||||
@@ -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.
|
||||
@@ -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<String> expectedRootLinkRels();
|
||||
|
||||
protected Map<String, String> getPayloadToPost() throws Exception {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -41,6 +41,7 @@ public class Order {
|
||||
private Person creator;
|
||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)//
|
||||
private List<LineItem> lineItems = new ArrayList<LineItem>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Order, Long> {
|
||||
|
||||
List<Order> findByType(@Param("type") Type type);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user