DATAREST-638 - Consolidated metadata under a single profile link.
Moved /alps into a separate link underneath /profile, so that /schema can ALSO be served there as well. Also added a profile link to the collection resources, linking to collection-specific metadata. We now use strict content negotiation for each form of metadata so clients don't have to navigate a labyrinth of links. To preserve backwards compatibility, make ALPS the default metadata type. Original pull request: #196.
This commit is contained in:
committed by
Oliver Gierke
parent
5f7f912e69
commit
345c198a75
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import static org.springframework.web.bind.annotation.RequestMethod.*;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.repository.support.Repositories;
|
||||
import org.springframework.data.rest.core.Path;
|
||||
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.core.mapping.RepositoryResourceMappings;
|
||||
import org.springframework.data.rest.core.mapping.ResourceMapping;
|
||||
import org.springframework.data.rest.core.mapping.ResourceMetadata;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
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.bind.annotation.RequestMethod;
|
||||
|
||||
/**
|
||||
* Profile-based controller exposing multiple forms of metadata.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @see DATAREST-638
|
||||
* @since 2.4
|
||||
*/
|
||||
@BasePathAwareController
|
||||
public class ProfileController {
|
||||
|
||||
public static final String PROFILE_ROOT_MAPPING = "/profile";
|
||||
public static final String RESOURCE_PROFILE_MAPPING = PROFILE_ROOT_MAPPING + "/{repository}";
|
||||
|
||||
private final RepositoryRestConfiguration configuration;
|
||||
private final RepositoryResourceMappings mappings;
|
||||
private final Repositories repositories;
|
||||
|
||||
/**
|
||||
* Wire up the controller with a copy of {@link RepositoryRestConfiguration}.
|
||||
*
|
||||
* @param configuration must not be {@literal null}.
|
||||
* @param mappings must not be {@literal null}.
|
||||
* @param repositories must not be {@literal null}.
|
||||
*/
|
||||
@Autowired
|
||||
public ProfileController(RepositoryRestConfiguration configuration, RepositoryResourceMappings mappings,
|
||||
Repositories repositories) {
|
||||
|
||||
Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!");
|
||||
Assert.notNull(mappings, "RepositoryResourceMappings must not be null!");
|
||||
Assert.notNull(repositories, "Repositories must not be null!");
|
||||
|
||||
this.configuration = configuration;
|
||||
this.mappings = mappings;
|
||||
this.repositories = repositories;
|
||||
}
|
||||
|
||||
/**
|
||||
* List the OPTIONS for this controller.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = PROFILE_ROOT_MAPPING, method = RequestMethod.OPTIONS)
|
||||
public HttpEntity<?> profileOptions() {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAllow(Collections.singleton(HttpMethod.GET));
|
||||
|
||||
return new ResponseEntity<Object>(headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* List a profile link for each exported repository.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = PROFILE_ROOT_MAPPING, method = GET)
|
||||
HttpEntity<ResourceSupport> listAllFormsOfMetadata() {
|
||||
|
||||
ResourceSupport profile = new ResourceSupport();
|
||||
|
||||
profile.add(new Link(getRootPath(this.configuration)).withSelfRel());
|
||||
|
||||
for (Class<?> domainType : this.repositories) {
|
||||
|
||||
ResourceMetadata mapping = this.mappings.getMetadataFor(domainType);
|
||||
|
||||
if (mapping.isExported()) {
|
||||
profile.add(new Link(getPath(this.configuration, mapping), mapping.getRel()));
|
||||
}
|
||||
}
|
||||
|
||||
return new ResponseEntity<ResourceSupport>(profile, HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return href for the profile root link of a given baseUri.
|
||||
*
|
||||
* @param configuration is the source of the app's baseUri.
|
||||
* @return
|
||||
*/
|
||||
public static String getRootPath(RepositoryRestConfiguration configuration) {
|
||||
|
||||
BaseUri baseUri = new BaseUri(configuration.getBaseUri());
|
||||
return baseUri.getUriComponentsBuilder().path(ProfileController.PROFILE_ROOT_MAPPING).build().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return href for the profile link of a given baseUri and domain type mapping.
|
||||
*
|
||||
* @param configuration is the source of the app's baseUri.
|
||||
* @param mapping provides the resource's path.
|
||||
* @return
|
||||
*/
|
||||
public static String getPath(RepositoryRestConfiguration configuration, ResourceMapping mapping) {
|
||||
|
||||
if (mapping == null) {
|
||||
return getRootPath(configuration);
|
||||
} else {
|
||||
return getRootPath(configuration) + mapping.getPath();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
* Copyright 2015 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.
|
||||
@@ -13,55 +13,50 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.rest.webmvc.alps;
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
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
|
||||
* {@link ResourceProcessor} to add a {@code profile} link to the root resource to point to multiple forms of metadata.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @see DATAREST-638
|
||||
* @since 2.4
|
||||
*/
|
||||
public class AlpsResourceProcessor implements ResourceProcessor<RepositoryLinksResource> {
|
||||
public class ProfileResourceProcessor implements ResourceProcessor<RepositoryLinksResource> {
|
||||
|
||||
private static final String PROFILE_REL = "profile";
|
||||
static final String PROFILE_REL = "profile";
|
||||
|
||||
private final RepositoryRestConfiguration configuration;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AlpsResourceProcessor} with the given {@link RepositoryRestConfiguration}.
|
||||
*
|
||||
* Creates a new {@link ProfileResourceProcessor} with the given {@link RepositoryRestConfiguration}.
|
||||
*
|
||||
* @param configuration must not be {@literal null}.
|
||||
*/
|
||||
@Autowired
|
||||
public AlpsResourceProcessor(RepositoryRestConfiguration configuration) {
|
||||
public ProfileResourceProcessor(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)
|
||||
/**
|
||||
* Add a link to the {@link ProfileController}'s base URI to the app's root URI.
|
||||
*
|
||||
* @param resource
|
||||
* @return
|
||||
*/
|
||||
@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));
|
||||
}
|
||||
resource.add(new Link(ProfileController.getRootPath(this.configuration), PROFILE_REL));
|
||||
|
||||
return resource;
|
||||
}
|
||||
@@ -202,6 +202,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
Link baseLink = entityLinks.linkToPagedResource(resourceInformation.getDomainType(), pageable.isDefault() ? null
|
||||
: pageable.getPageable());
|
||||
|
||||
links.add(new Link(ProfileController.getPath(this.config, metadata), ProfileResourceProcessor.PROFILE_REL));
|
||||
|
||||
Resources<?> result = toResources(results, assembler, metadata.getDomainType(), baseLink);
|
||||
result.add(links);
|
||||
return result;
|
||||
|
||||
@@ -31,12 +31,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
* @author Oliver Gierke
|
||||
* @author Greg Turnquist
|
||||
* @see http://json-schema.org/
|
||||
*/
|
||||
@RepositoryRestController
|
||||
@BasePathAwareController
|
||||
class RepositorySchemaController {
|
||||
|
||||
private static final String BASE_MAPPING = "/{repository}";
|
||||
|
||||
private final PersistentEntityToJsonSchemaConverter jsonSchemaConverter;
|
||||
|
||||
/**
|
||||
@@ -48,6 +48,7 @@ class RepositorySchemaController {
|
||||
public RepositorySchemaController(PersistentEntityToJsonSchemaConverter jsonSchemaConverter) {
|
||||
|
||||
Assert.notNull(jsonSchemaConverter, "PersistentEntityToJsonSchemaConverter must not be null!");
|
||||
|
||||
this.jsonSchemaConverter = jsonSchemaConverter;
|
||||
}
|
||||
|
||||
@@ -57,7 +58,8 @@ class RepositorySchemaController {
|
||||
* @param resourceInformation will never be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = BASE_MAPPING + "/schema", method = GET)
|
||||
@RequestMapping(value = ProfileController.RESOURCE_PROFILE_MAPPING, method = GET,
|
||||
produces = RestMediaTypes.SCHEMA_JSON_VALUE)
|
||||
public HttpEntity<JsonSchema> schema(RootResourceInformation resourceInformation) {
|
||||
|
||||
JsonSchema schema = jsonSchemaConverter.convert(resourceInformation.getDomainType());
|
||||
|
||||
@@ -22,21 +22,27 @@ import org.springframework.http.MediaType;
|
||||
* Constants to refer to supported media types.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class RestMediaTypes {
|
||||
|
||||
public static final String SPRING_DATA_COMPACT_JSON_VALUE = "application/x-spring-data-compact+json";
|
||||
public static final String TEXT_URI_LIST_VALUE = "text/uri-list";
|
||||
|
||||
public static final MediaType HAL_JSON = MediaTypes.HAL_JSON;
|
||||
|
||||
public static final MediaType JSON_PATCH_JSON = MediaType.valueOf("application/json-patch+json");
|
||||
public static final MediaType MERGE_PATCH_JSON = MediaType.valueOf("application/merge-patch+json");
|
||||
|
||||
public static final MediaType SCHEMA_JSON = MediaType.valueOf("application/schema+json");
|
||||
public static final String ALPS_JSON_VALUE = "application/alps+json";
|
||||
public static final MediaType ALPS_JSON = MediaType.parseMediaType(ALPS_JSON_VALUE);
|
||||
|
||||
public static final String SCHEMA_JSON_VALUE = "application/schema+json";
|
||||
public static final MediaType SCHEMA_JSON = MediaType.valueOf(SCHEMA_JSON_VALUE);
|
||||
|
||||
public static final MediaType SPRING_DATA_VERBOSE_JSON = MediaType.valueOf("application/x-spring-data-verbose+json");
|
||||
|
||||
public static final String SPRING_DATA_COMPACT_JSON_VALUE = "application/x-spring-data-compact+json";
|
||||
public static final MediaType SPRING_DATA_COMPACT_JSON = MediaType.valueOf(SPRING_DATA_COMPACT_JSON_VALUE);
|
||||
|
||||
public static final String TEXT_URI_LIST_VALUE = "text/uri-list";
|
||||
public static final MediaType TEXT_URI_LIST = MediaType.valueOf(TEXT_URI_LIST_VALUE);
|
||||
}
|
||||
|
||||
@@ -17,43 +17,37 @@ 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.BasePathAwareController;
|
||||
import org.springframework.data.rest.webmvc.BaseUri;
|
||||
import org.springframework.data.rest.webmvc.ProfileController;
|
||||
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
|
||||
import org.springframework.data.rest.webmvc.RestMediaTypes;
|
||||
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.MediaType;
|
||||
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
|
||||
* @author Greg Turnquist
|
||||
* @see http://alps.io
|
||||
*/
|
||||
@BasePathAwareController
|
||||
public class AlpsController {
|
||||
|
||||
static final String ALPS_ROOT_MAPPING = "/alps";
|
||||
static final String ALPS_RESOURCE_MAPPING = ALPS_ROOT_MAPPING + "/{repository}";
|
||||
|
||||
private final Repositories repositories;
|
||||
private final ResourceMappings mappings;
|
||||
private final RepositoryRestConfiguration configuration;
|
||||
@@ -83,7 +77,7 @@ public class AlpsController {
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = { ALPS_ROOT_MAPPING, ALPS_RESOURCE_MAPPING }, method = OPTIONS)
|
||||
@RequestMapping(value = ProfileController.RESOURCE_PROFILE_MAPPING, method = OPTIONS, produces = RestMediaTypes.ALPS_JSON_VALUE)
|
||||
HttpEntity<?> alpsOptions() {
|
||||
|
||||
verifyAlpsEnabled();
|
||||
@@ -94,45 +88,14 @@ public class AlpsController {
|
||||
return new ResponseEntity<Object>(headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes a resource to contain descriptors pointing to the discriptors for individual resources.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = ALPS_ROOT_MAPPING, method = GET)
|
||||
HttpEntity<Alps> alps() {
|
||||
|
||||
verifyAlpsEnabled();
|
||||
|
||||
List<Descriptor> descriptors = new ArrayList<Descriptor>();
|
||||
|
||||
for (Class<?> domainType : repositories) {
|
||||
|
||||
ResourceMetadata mapping = mappings.getMetadataFor(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 = ALPS_RESOURCE_MAPPING, method = GET)
|
||||
@RequestMapping(value = ProfileController.RESOURCE_PROFILE_MAPPING, method = GET,
|
||||
produces = { MediaType.ALL_VALUE, RestMediaTypes.ALPS_JSON_VALUE })
|
||||
HttpEntity<RootResourceInformation> descriptor(RootResourceInformation information) {
|
||||
|
||||
verifyAlpsEnabled();
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.util.Arrays;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.rest.webmvc.RestMediaTypes;
|
||||
import org.springframework.data.rest.webmvc.RootResourceInformation;
|
||||
import org.springframework.hateoas.alps.Alps;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -42,8 +43,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
public class AlpsJsonHttpMessageConverter extends MappingJackson2HttpMessageConverter
|
||||
implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private static final MediaType ALPS_MEDIA_TYPE = MediaType.parseMediaType("application/alps+json");
|
||||
|
||||
private final RootResourceInformationToAlpsDescriptorConverter converter;
|
||||
|
||||
/**
|
||||
@@ -61,7 +60,7 @@ public class AlpsJsonHttpMessageConverter extends MappingJackson2HttpMessageConv
|
||||
mapper.setSerializationInclusion(Include.NON_EMPTY);
|
||||
|
||||
setPrettyPrint(true);
|
||||
setSupportedMediaTypes(Arrays.asList(ALPS_MEDIA_TYPE, MediaType.APPLICATION_JSON, MediaType.ALL));
|
||||
setSupportedMediaTypes(Arrays.asList(RestMediaTypes.ALPS_JSON, MediaType.APPLICATION_JSON, MediaType.ALL));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.springframework.data.rest.core.mapping.ResourceMetadata;
|
||||
import org.springframework.data.rest.core.mapping.ResourceType;
|
||||
import org.springframework.data.rest.core.mapping.SimpleResourceDescription;
|
||||
import org.springframework.data.rest.core.mapping.SupportedHttpMethods;
|
||||
import org.springframework.data.rest.webmvc.ProfileController;
|
||||
import org.springframework.data.rest.webmvc.RootResourceInformation;
|
||||
import org.springframework.data.rest.webmvc.json.JacksonMetadata;
|
||||
import org.springframework.data.rest.webmvc.mapping.AssociationLinks;
|
||||
@@ -60,7 +61,6 @@ 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;
|
||||
@@ -147,9 +147,11 @@ public class RootResourceInformationToAlpsDescriptorConverter {
|
||||
|
||||
ResourceMetadata metadata = mappings.getMetadataFor(type);
|
||||
|
||||
String href = ProfileController.getPath(this.configuration, metadata);
|
||||
|
||||
return descriptor().//
|
||||
id(getRepresentationDescriptorId(metadata)).//
|
||||
href(entityLinks.linkFor(type).slash("schema").toString()).//
|
||||
href(href).//
|
||||
doc(getDocFor(metadata.getItemResourceDescription())).//
|
||||
descriptors(buildPropertyDescriptors(type, metadata.getItemResourceRel())).//
|
||||
build();
|
||||
@@ -178,7 +180,6 @@ public class RootResourceInformationToAlpsDescriptorConverter {
|
||||
* Builds a descriptor for the projection parameter of the given resource.
|
||||
*
|
||||
* @param metadata
|
||||
* @param projectionConfiguration
|
||||
* @return
|
||||
*/
|
||||
private Descriptor buildProjectionDescriptor(ResourceMetadata metadata) {
|
||||
@@ -353,10 +354,11 @@ public class RootResourceInformationToAlpsDescriptorConverter {
|
||||
name(mapping.getRel()).doc(getDocFor(mapping.getDescription()));
|
||||
|
||||
ResourceMetadata targetTypeMetadata = mappings.getMetadataFor(property.getActualType());
|
||||
String localPath = targetTypeMetadata.getRel().concat("#")
|
||||
.concat(getRepresentationDescriptorId(targetTypeMetadata));
|
||||
Link link = ControllerLinkBuilder.linkTo(AlpsController.class).slash(AlpsController.ALPS_ROOT_MAPPING)
|
||||
.slash(localPath).withSelfRel();
|
||||
|
||||
String href = ProfileController.getPath(configuration, targetTypeMetadata) +
|
||||
"#" + getRepresentationDescriptorId(targetTypeMetadata);
|
||||
|
||||
Link link = new Link(href).withSelfRel();
|
||||
|
||||
builder.//
|
||||
type(Type.SAFE).//
|
||||
|
||||
@@ -73,6 +73,7 @@ import org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFac
|
||||
import org.springframework.data.rest.webmvc.BasePathAwareController;
|
||||
import org.springframework.data.rest.webmvc.BasePathAwareHandlerMapping;
|
||||
import org.springframework.data.rest.webmvc.BaseUri;
|
||||
import org.springframework.data.rest.webmvc.ProfileResourceProcessor;
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestController;
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestExceptionHandler;
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
|
||||
@@ -80,7 +81,6 @@ 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.UriListHttpMessageConverter;
|
||||
import org.springframework.data.rest.webmvc.json.DomainObjectReader;
|
||||
@@ -143,6 +143,7 @@ import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Jon Brisbin
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Configuration
|
||||
@EnableHypermediaSupport(type = HypermediaType.HAL)
|
||||
@@ -769,8 +770,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AlpsResourceProcessor alpsResourceProcessor() {
|
||||
return new AlpsResourceProcessor(config());
|
||||
public ProfileResourceProcessor profileResourceProcessor(RepositoryRestConfiguration config) {
|
||||
return new ProfileResourceProcessor(config);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.util.Map;
|
||||
import net.minidev.json.JSONArray;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -64,6 +65,7 @@ public abstract class CommonWebTests extends AbstractWebIntegrationTests {
|
||||
|
||||
/**
|
||||
* @see DATAREST-113
|
||||
* @see DATAREST-638
|
||||
*/
|
||||
@Test
|
||||
public void exposesSchemasForResourcesExposed() throws Exception {
|
||||
@@ -75,12 +77,18 @@ public abstract class CommonWebTests extends AbstractWebIntegrationTests {
|
||||
Link link = client.assertHasLinkWithRel(rel, response);
|
||||
|
||||
// Resource
|
||||
client.request(link);
|
||||
client.follow(link).andExpect(status().is2xxSuccessful());
|
||||
|
||||
// Schema - TODO:Improve by using hypermedia
|
||||
mvc.perform(get(link.expand().getHref() + "/schema").//
|
||||
accept(MediaType.parseMediaType("application/schema+json"))).//
|
||||
andExpect(status().isOk());
|
||||
Link profileLink = client.discoverUnique(link, "profile");
|
||||
|
||||
// Default metadata
|
||||
client.follow(profileLink).andExpect(status().is2xxSuccessful());
|
||||
|
||||
// JSON Schema
|
||||
client.follow(profileLink, RestMediaTypes.SCHEMA_JSON).andExpect(status().is2xxSuccessful());
|
||||
|
||||
// ALPS
|
||||
client.follow(profileLink, RestMediaTypes.ALPS_JSON).andExpect(status().is2xxSuccessful());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
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.Configuration;
|
||||
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
|
||||
import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.LinkDiscoverers;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Series of tests to verify {@link ProfileController} serves ALPS and JSON Schema metadata from the root level and
|
||||
* at collection resource levels.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 2.4
|
||||
*/
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration(classes = { JpaRepositoryConfig.class, ProfileIntegrationTests.Config.class})
|
||||
public class ProfileIntegrationTests extends AbstractControllerIntegrationTests {
|
||||
|
||||
@Autowired WebApplicationContext context;
|
||||
@Autowired LinkDiscoverers discoverers;
|
||||
|
||||
private static final String ROOT_URI = "/api";
|
||||
|
||||
@Configuration
|
||||
static class Config extends RepositoryRestConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
|
||||
config.setBasePath(ROOT_URI);
|
||||
}
|
||||
}
|
||||
|
||||
TestMvcClient client;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
this.client = new TestMvcClient(mvc, this.discoverers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-230
|
||||
* @see DATAREST-638
|
||||
*/
|
||||
@Test
|
||||
public void exposesProfileLink() throws Exception {
|
||||
|
||||
client.follow(ROOT_URI)//
|
||||
.andExpect(status().is2xxSuccessful())//
|
||||
.andExpect(jsonPath("$._links.profile.href", endsWith(ProfileController.PROFILE_ROOT_MAPPING)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-230
|
||||
* @see DATAREST-638
|
||||
*/
|
||||
@Test
|
||||
public void profileRootLinkContainsMetadataForEachRepo() throws Exception {
|
||||
|
||||
Link profileLink = client.discoverUnique(new Link(ROOT_URI), ProfileResourceProcessor.PROFILE_REL);
|
||||
|
||||
assertThat(client.discoverUnique(profileLink, "self", MediaType.ALL), is(notNullValue()));
|
||||
assertThat(client.discoverUnique(profileLink, "people", MediaType.ALL), is(notNullValue()));
|
||||
assertThat(client.discoverUnique(profileLink, "items", MediaType.ALL), is(notNullValue()));
|
||||
assertThat(client.discoverUnique(profileLink, "authors", MediaType.ALL), is(notNullValue()));
|
||||
assertThat(client.discoverUnique(profileLink, "books", MediaType.ALL), is(notNullValue()));
|
||||
assertThat(client.discoverUnique(profileLink, "orders", MediaType.ALL), is(notNullValue()));
|
||||
assertThat(client.discoverUnique(profileLink, "receipts", MediaType.ALL), is(notNullValue()));
|
||||
assertThat(client.discoverUnique(profileLink, "addresses", MediaType.ALL), is(notNullValue()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @see DATAREST-638
|
||||
*/
|
||||
@Test
|
||||
public void profileLinkOnCollectionResourceLeadsToRepositorySpecificMetadata() throws Exception {
|
||||
|
||||
Link peopleLink = client.discoverUnique(new Link(ROOT_URI), "people");
|
||||
Link profileLink = client.discoverUnique(peopleLink, ProfileResourceProcessor.PROFILE_REL);
|
||||
|
||||
client.follow(profileLink, RestMediaTypes.ALPS_JSON)
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, RestMediaTypes.ALPS_JSON_VALUE));
|
||||
|
||||
client.follow(profileLink, RestMediaTypes.SCHEMA_JSON)
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, RestMediaTypes.SCHEMA_JSON_VALUE));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -174,7 +174,30 @@ public class TestMvcClient {
|
||||
* @throws Exception
|
||||
*/
|
||||
public ResultActions follow(String href) throws Exception {
|
||||
return mvc.perform(get(href));
|
||||
return follow(href, MediaType.ALL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Folow Link with a specific Accept header (media type).
|
||||
*
|
||||
* @param link
|
||||
* @param accept
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public ResultActions follow(Link link, MediaType accept) throws Exception {
|
||||
return follow(link.expand().getHref(), accept);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow URL supplied as a string with a specific Accept header.
|
||||
* @param href
|
||||
* @param accept
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public ResultActions follow(String href, MediaType accept) throws Exception {
|
||||
return mvc.perform(get(href).header(HttpHeaders.ACCEPT, accept.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,15 +17,19 @@ package org.springframework.data.rest.webmvc.alps;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
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.core.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests;
|
||||
import org.springframework.data.rest.webmvc.ProfileController;
|
||||
import org.springframework.data.rest.webmvc.RestMediaTypes;
|
||||
import org.springframework.data.rest.webmvc.TestMvcClient;
|
||||
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
|
||||
import org.springframework.data.rest.webmvc.jpa.Item;
|
||||
@@ -82,31 +86,21 @@ public class AlpsControllerIntegrationTests extends AbstractControllerIntegratio
|
||||
* @see DATAREST-230
|
||||
*/
|
||||
@Test
|
||||
public void exposesProfileLink() throws Exception {
|
||||
|
||||
client.follow("/")//
|
||||
.andExpect(status().is2xxSuccessful())//
|
||||
.andExpect(jsonPath("$._links.profile.href", endsWith(AlpsController.ALPS_ROOT_MAPPING)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-230
|
||||
*/
|
||||
@Test
|
||||
public void alpsResourceExposesResourcePerCollectionResource() throws Exception {
|
||||
public void exposesAlpsCollectionResources() throws Exception {
|
||||
|
||||
Link profileLink = client.discoverUnique("profile");
|
||||
Link peopleLink = client.discoverUnique(profileLink, "people", MediaType.ALL);
|
||||
|
||||
assertThat(client.discoverUnique(profileLink, "orders", MediaType.ALL), is(notNullValue()));
|
||||
assertThat(client.discoverUnique(profileLink, "people", MediaType.ALL), is(notNullValue()));
|
||||
assertThat(client.discoverUnique(profileLink, "items", MediaType.ALL), is(notNullValue()));
|
||||
client.follow(peopleLink, RestMediaTypes.ALPS_JSON)//
|
||||
.andExpect(jsonPath("$.version").value("1.0"))//
|
||||
.andExpect(jsonPath("$.descriptors[*].name", hasItems("people", "person")));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-230
|
||||
* @see DATAREST-638
|
||||
*/
|
||||
@Test
|
||||
public void exposesAlpsCollectionResources() throws Exception {
|
||||
public void verifyThatAlpsIsDefaultProfileFormat() throws Exception {
|
||||
|
||||
Link profileLink = client.discoverUnique("profile");
|
||||
Link peopleLink = client.discoverUnique(profileLink, "people", MediaType.ALL);
|
||||
@@ -114,6 +108,7 @@ public class AlpsControllerIntegrationTests extends AbstractControllerIntegratio
|
||||
client.follow(peopleLink)//
|
||||
.andExpect(jsonPath("$.version").value("1.0"))//
|
||||
.andExpect(jsonPath("$.descriptors[*].name", hasItems("people", "person")));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +120,7 @@ public class AlpsControllerIntegrationTests extends AbstractControllerIntegratio
|
||||
Link profileLink = client.discoverUnique("profile");
|
||||
Link itemsLink = client.discoverUnique(profileLink, "items", MediaType.ALL);
|
||||
|
||||
client.follow(itemsLink)//
|
||||
client.follow(itemsLink, RestMediaTypes.ALPS_JSON)//
|
||||
// Exposes standard property
|
||||
.andExpect(jsonPath("$.descriptors[*].descriptors[*].name", hasItems("name")))
|
||||
// Does not expose explicitly @JsonIgnored property
|
||||
@@ -145,8 +140,8 @@ public class AlpsControllerIntegrationTests extends AbstractControllerIntegratio
|
||||
|
||||
assertThat(itemsLink, is(notNullValue()));
|
||||
|
||||
client.follow(itemsLink)//
|
||||
.andExpect(jsonPath("$.descriptors[?(@.id == 'item-representation')].href", is(notNullValue())));
|
||||
client.follow(itemsLink, RestMediaTypes.ALPS_JSON)//
|
||||
.andExpect(jsonPath("$.descriptors[?(@.id == 'item-representation')][0].href", endsWith("/profile/items")));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,8 +158,9 @@ public class AlpsControllerIntegrationTests extends AbstractControllerIntegratio
|
||||
jsonPath += "descriptors[?(@.name == 'father')][0]."; // First father descriptor
|
||||
jsonPath += "rt"; // Return type
|
||||
|
||||
client.follow(usersLink)//
|
||||
.andExpect(jsonPath(jsonPath, allOf(containsString("alps"), endsWith("-representation"))));
|
||||
client.follow(usersLink, RestMediaTypes.ALPS_JSON)//
|
||||
.andExpect(jsonPath(jsonPath,
|
||||
allOf(containsString(ProfileController.PROFILE_ROOT_MAPPING), endsWith("-representation"))));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +172,7 @@ public class AlpsControllerIntegrationTests extends AbstractControllerIntegratio
|
||||
Link profileLink = client.discoverUnique("profile");
|
||||
Link itemsLink = client.discoverUnique(profileLink, "items", MediaType.ALL);
|
||||
|
||||
client.follow(itemsLink)//
|
||||
client.follow(itemsLink, RestMediaTypes.ALPS_JSON)//
|
||||
// Exposes identifier if configured to
|
||||
.andExpect(jsonPath("$.descriptors[*].descriptors[*].name", hasItems("id", "name")));
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ document would look like this:
|
||||
"href" : "http://localhost:8080/addresses"
|
||||
},
|
||||
"profile" : {
|
||||
"href" : "http://localhost:8080/alps"
|
||||
"href" : "http://localhost:8080/profile"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,29 +36,29 @@ A *profile* link, as defined in https://tools.ietf.org/html/rfc6906[RFC 6906], i
|
||||
http://tools.ietf.org/html/draft-amundsen-richardson-foster-alps-00[ALPS draft spec] is meant to define a particular profile format
|
||||
which we'll explore further down in this section.
|
||||
|
||||
If you navigate into the *profile* link at `localhost:8080/alps`, you would see something like this:
|
||||
If you navigate into the *profile* link at `localhost:8080/profile`, you would see something like this:
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
{
|
||||
"version" : "1.0",
|
||||
"descriptors" : [ {
|
||||
"href" : "http://localhost:8080/alps/persons",
|
||||
"name" : "persons"
|
||||
}, {
|
||||
"href" : "http://localhost:8080/alps/addresses",
|
||||
"name" : "addresses"
|
||||
} ]
|
||||
"_links" : {
|
||||
"self" : {
|
||||
"href" : "http://localhost:8080/profile"
|
||||
},
|
||||
"persons" : {
|
||||
"href" : "http://localhost:8080/profile/persons"
|
||||
},
|
||||
"addresses" : {
|
||||
"href" : "http://localhost:8080/profile/addresses"
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
IMPORTANT: At the root level, *profile* is a single link and hence can't handle serving up more than one application profile. That
|
||||
is why you must navigate to `/alps` to find a link for each resource's ALPS metadata.
|
||||
is why you must navigate to `/profile` to find a link for each resource's metadata.
|
||||
|
||||
NOTE: This JSON document has a media type of `application/alps+json`. This is different than the previous JSON document, which had
|
||||
a media type of `application/hal+json`. These formats are different and governed by different specs.
|
||||
|
||||
Let's navigate to `/alps/persons` and look at the profile data for a `Person` resource.
|
||||
Let's navigate to `/profile/persons` and look at the profile data for a `Person` resource.
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
@@ -78,7 +78,7 @@ Let's navigate to `/alps/persons` and look at the profile data for a `Person` re
|
||||
}, {
|
||||
"name" : "address",
|
||||
"type" : "SAFE",
|
||||
"rt" : "http://localhost:8080/addresses#address"
|
||||
"rt" : "http://localhost:8080/profile/addresses#address"
|
||||
} ]
|
||||
}, {
|
||||
"id" : "create-persons", <2>
|
||||
@@ -119,7 +119,32 @@ of the attributes.
|
||||
<2> After the resource representation are all the supported operations. This one is how to create a new `Person`.
|
||||
<3> The name is *persons*, which indicates that a POST should be applied to the whole collection, not a single *person*.
|
||||
<4> The *type* is `UNSAFE` because this operation can alter the state of the system.
|
||||
<5>
|
||||
|
||||
NOTE: This JSON document has a media type of `application/alps+json`. This is different than the previous JSON document, which had
|
||||
a media type of `application/hal+json`. These formats are different and governed by different specs.
|
||||
|
||||
You will also find a "profile" link shown in the collection of *_links* when you are looking at a collection resource.
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
{
|
||||
"_links" : {
|
||||
"self" : {
|
||||
"href" : "http://localhost:8080/persons" <1>
|
||||
},
|
||||
... other links ...
|
||||
"profile" : {
|
||||
"href" : "http://localhost:8080/profile/persons" <2>
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
<1> This HAL document respresents the `Person` collection.
|
||||
<2> It has a *profile* link to the same URI for metadata.
|
||||
|
||||
The *profile* link, again, will serve up ALPS by default or if you use an http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1[Accept header] of *application/alps+json*.
|
||||
|
||||
[[metadata.alps.control-types]]
|
||||
=== Hypermedia control types
|
||||
@@ -253,7 +278,7 @@ As you can see, this defines details to display for a `Person` resource. They al
|
||||
"format" : "TEXT"
|
||||
},
|
||||
"type" : "SAFE",
|
||||
"rt" : "http://localhost:8080/addresses#address"
|
||||
"rt" : "http://localhost:8080/profile/addresses#address"
|
||||
} ]
|
||||
}
|
||||
...
|
||||
@@ -271,9 +296,108 @@ NOTE: Spring MVC (which is the essence of a Spring Data REST application) suppor
|
||||
properties files with different messages.
|
||||
|
||||
|
||||
//= JSON Schema
|
||||
[[metadata.json-schema]]
|
||||
== JSON Schema
|
||||
|
||||
//TBD
|
||||
http://json-schema.org/[JSON Schema] is another form of metadata supported by Spring Data REST. Per their website, JSON Schema has the following advantages:
|
||||
|
||||
* describes your existing data format
|
||||
* clear, human- and machine-readable documentation
|
||||
* complete structural validation, useful for automated testing and validating client-submitted data
|
||||
|
||||
As shown in the <<metadata.alps,previous section>>, you can reach this data by navigating from the root URI to the "profile" link.
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
{
|
||||
"_links" : {
|
||||
"self" : {
|
||||
"href" : "http://localhost:8080/profile"
|
||||
},
|
||||
"persons" : {
|
||||
"href" : "http://localhost:8080/profile/persons"
|
||||
},
|
||||
"addresses" : {
|
||||
"href" : "http://localhost:8080/profile/addresses"
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
These links are the same as shown earlier. To retrieve JSON Schema you invoke them with Accept header *application/schema+json*.
|
||||
|
||||
In this case, if you executed `curl -H 'Accept:application/schema+json' http://localhost:8080/profile/persons`, you would see something like this:
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
{
|
||||
"title" : "org.springframework.data.rest.webmvc.jpa.Person", <1>
|
||||
"properties" : { <2>
|
||||
"firstName" : {
|
||||
"readOnly" : false,
|
||||
"type" : "string"
|
||||
},
|
||||
"lastName" : {
|
||||
"readOnly" : false,
|
||||
"type" : "string"
|
||||
},
|
||||
"siblings" : {
|
||||
"readOnly" : false,
|
||||
"type" : "string",
|
||||
"format" : "uri"
|
||||
},
|
||||
"created" : {
|
||||
"readOnly" : false,
|
||||
"type" : "string",
|
||||
"format" : "date-time"
|
||||
},
|
||||
"father" : {
|
||||
"readOnly" : false,
|
||||
"type" : "string",
|
||||
"format" : "uri"
|
||||
},
|
||||
"weight" : {
|
||||
"readOnly" : false,
|
||||
"type" : "integer"
|
||||
},
|
||||
"height" : {
|
||||
"readOnly" : false,
|
||||
"type" : "integer"
|
||||
}
|
||||
},
|
||||
"descriptors" : { },
|
||||
"type" : "object",
|
||||
"$schema" : "http://json-schema.org/draft-04/schema#"
|
||||
}
|
||||
----
|
||||
|
||||
<1> The type that was exported
|
||||
<2> A listing of properties
|
||||
|
||||
There are more details if your resources have links to other resources.
|
||||
|
||||
You will also find a "profile" link shown in the collection of *_links* when you are looking at a collection resource.
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
{
|
||||
"_links" : {
|
||||
"self" : {
|
||||
"href" : "http://localhost:8080/persons" <1>
|
||||
},
|
||||
... other links ...
|
||||
"profile" : {
|
||||
"href" : "http://localhost:8080/profile/persons" <2>
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
<1> This HAL document respresents the `Person` collection.
|
||||
<2> It has a *profile* link to the same URI for metadata.
|
||||
|
||||
The *profile* link, again, will serve up <<metadata.alps,ALPS>> by default. If you supply it with an http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1[Accept header] of *application/schema+json*, it will render the JSON Schema representation.
|
||||
|
||||
//= JSON Patch
|
||||
|
||||
|
||||
Reference in New Issue
Block a user