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:
@@ -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