#784 - Introduce UBER+JSON mediatype.
Based on https://rawgit.com/uber-hypermedia/specification/master/uber-hypermedia.html.
This commit is contained in:
@@ -76,4 +76,14 @@ public class MediaTypes {
|
||||
* Public constant media type for {@code application/vnd.collection+json}.
|
||||
*/
|
||||
public static final MediaType COLLECTION_JSON = MediaType.valueOf(COLLECTION_JSON_VALUE);
|
||||
|
||||
/**
|
||||
* A String equivalent of {@link MediaTypes#UBER_JSON_VALUE}.
|
||||
*/
|
||||
public static final String UBER_JSON_VALUE = "application/vnd.amundsen-uber+json";
|
||||
|
||||
/**
|
||||
* Public constant media type for {@code application/vnd.amundsen-uber+json}.
|
||||
*/
|
||||
public static final MediaType UBER_JSON = MediaType.parseMediaType(UBER_JSON_VALUE);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
|
||||
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
|
||||
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator;
|
||||
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberHandlerInstantiator;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
@@ -96,7 +98,7 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
|
||||
if (converters.stream()
|
||||
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
|
||||
.map(AbstractJackson2HttpMessageConverter.class::cast)
|
||||
.map(converter -> converter.getObjectMapper())
|
||||
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
|
||||
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
|
||||
|
||||
return;
|
||||
@@ -124,8 +126,29 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
|
||||
if (hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
|
||||
converters.add(0, createCollectionJsonConverter(objectMapper, linkRelationMessageSource));
|
||||
}
|
||||
|
||||
if (hypermediaTypes.contains(HypermediaType.UBER)) {
|
||||
converters.add(0, createUberJsonConverter(objectMapper));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param objectMapper
|
||||
* @return
|
||||
*/
|
||||
protected MappingJackson2HttpMessageConverter createUberJsonConverter(ObjectMapper objectMapper) {
|
||||
|
||||
ObjectMapper mapper = objectMapper.copy();
|
||||
|
||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
mapper.registerModule(new Jackson2UberModule());
|
||||
mapper.setHandlerInstantiator(new UberHandlerInstantiator());
|
||||
|
||||
return new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
ResourceSupport.class, Arrays.asList(UBER_JSON), mapper);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param objectMapper
|
||||
* @param linkRelationMessageSource
|
||||
|
||||
@@ -86,7 +86,14 @@ public @interface EnableHypermediaSupport {
|
||||
*
|
||||
* @see http://amundsen.com/media-types/collection/format/
|
||||
*/
|
||||
COLLECTION_JSON;
|
||||
COLLECTION_JSON,
|
||||
|
||||
/**
|
||||
* UBER Hypermedia
|
||||
*
|
||||
* @see http://uberhypermedia.org/
|
||||
*/
|
||||
UBER;
|
||||
|
||||
private static Set<HypermediaType> HAL_BASED_MEDIATYPES = EnumSet.of(HAL, HAL_FORMS);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.hateoas.collectionjson.CollectionJsonLinkDiscoverer;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
|
||||
import org.springframework.hateoas.hal.HalLinkDiscoverer;
|
||||
import org.springframework.hateoas.hal.forms.HalFormsLinkDiscoverer;
|
||||
import org.springframework.hateoas.uber.UberLinkDiscoverer;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
@@ -96,6 +97,9 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
|
||||
case COLLECTION_JSON:
|
||||
definition = new RootBeanDefinition(CollectionJsonLinkDiscoverer.class);
|
||||
break;
|
||||
case UBER:
|
||||
definition = new RootBeanDefinition(UberLinkDiscoverer.class);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(String.format("Unsupported hypermedia type %s!", type));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.hateoas.support;
|
||||
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.Resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
|
||||
/**
|
||||
@@ -36,4 +39,18 @@ public final class JacksonHelper {
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a {@literal Resources<Resource<?>>}?
|
||||
*
|
||||
* @param type
|
||||
* @return
|
||||
*/
|
||||
public static boolean isResourcesOfResource(JavaType type) {
|
||||
|
||||
return
|
||||
Resources.class.isAssignableFrom(type.getRawClass())
|
||||
&&
|
||||
Resource.class.isAssignableFrom(type.containedType(0).getRawClass());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ public class PropertyUtils {
|
||||
.collect(HashMap::new,
|
||||
(hashMap, descriptor) -> {
|
||||
try {
|
||||
hashMap.put(descriptor.getName(), descriptor.getReadMethod().invoke(object));
|
||||
Method readMethod = descriptor.getReadMethod();
|
||||
ReflectionUtils.makeAccessible(readMethod);
|
||||
hashMap.put(descriptor.getName(), readMethod.invoke(object));
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -85,7 +87,8 @@ public class PropertyUtils {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static Object createObjectFromProperties(Class<?> clazz, Map<String, Object> properties) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T createObjectFromProperties(Class<T> clazz, Map<String, Object> properties) {
|
||||
|
||||
Object obj = BeanUtils.instantiateClass(clazz);
|
||||
|
||||
@@ -102,7 +105,7 @@ public class PropertyUtils {
|
||||
});
|
||||
});
|
||||
|
||||
return obj;
|
||||
return (T) obj;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,741 @@
|
||||
/*
|
||||
* Copyright 2017-2018 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.hateoas.uber;
|
||||
|
||||
import static org.springframework.hateoas.PagedResources.*;
|
||||
import static org.springframework.hateoas.support.JacksonHelper.*;
|
||||
import static org.springframework.hateoas.uber.UberData.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.hateoas.support.JacksonHelper;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.Version;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
|
||||
import com.fasterxml.jackson.databind.cfg.MapperConfig;
|
||||
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
|
||||
import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import com.fasterxml.jackson.databind.introspect.Annotated;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.ContainerSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
|
||||
/**
|
||||
* Jackson {@link SimpleModule} for {@literal UBER+JSON} serializers and deserializers.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
public class Jackson2UberModule extends SimpleModule {
|
||||
|
||||
public Jackson2UberModule() {
|
||||
|
||||
super("uber-module", new Version(1, 0, 0, null, "org.springframework.hateoas", "spring-hateoas"));
|
||||
|
||||
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
|
||||
setMixInAnnotation(Resource.class, ResourceMixin.class);
|
||||
setMixInAnnotation(Resources.class, ResourcesMixin.class);
|
||||
setMixInAnnotation(PagedResources.class, PagedResourcesMixin.class);
|
||||
|
||||
addSerializer(new UberPagedResourcesSerializer());
|
||||
addSerializer(new UberResourcesSerializer());
|
||||
addSerializer(new UberResourceSerializer());
|
||||
addSerializer(new UberResourceSupportSerializer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link JsonSerializer} to render {@link ResourceSupport} into {@literal UBER+JSON}.
|
||||
*/
|
||||
static class UberResourceSupportSerializer extends ContainerSerializer<ResourceSupport> implements ContextualSerializer {
|
||||
|
||||
private final BeanProperty property;
|
||||
|
||||
UberResourceSupportSerializer(BeanProperty property) {
|
||||
|
||||
super(ResourceSupport.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
UberResourceSupportSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(ResourceSupport value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
|
||||
UberDocument doc = new UberDocument()
|
||||
.withUber(new Uber()
|
||||
.withVersion("1.0")
|
||||
.withData(extractLinksAndContent(value)));
|
||||
|
||||
provider
|
||||
.findValueSerializer(UberDocument.class, property)
|
||||
.serialize(doc, gen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(ResourceSupport value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new UberResourceSupportSerializer(property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link JsonSerializer} to render {@link Resource} into {@literal UBER+JSON}.
|
||||
*/
|
||||
static class UberResourceSerializer extends ContainerSerializer<Resource<?>> implements ContextualSerializer {
|
||||
|
||||
private final BeanProperty property;
|
||||
|
||||
UberResourceSerializer(BeanProperty property) {
|
||||
|
||||
super(Resource.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
UberResourceSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Resource<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
|
||||
UberDocument doc = new UberDocument()
|
||||
.withUber(new Uber()
|
||||
.withVersion("1.0")
|
||||
.withData(extractLinksAndContent(value)));
|
||||
|
||||
provider
|
||||
.findValueSerializer(UberDocument.class, property)
|
||||
.serialize(doc, gen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(Resource<?> value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new UberResourceSerializer(property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link JsonSerializer} to render {@link Resources} into {@literal UBER+JSON}.
|
||||
*/
|
||||
static class UberResourcesSerializer extends ContainerSerializer<Resources<?>> implements ContextualSerializer {
|
||||
|
||||
private BeanProperty property;
|
||||
|
||||
UberResourcesSerializer(BeanProperty property) {
|
||||
|
||||
super(Resources.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
UberResourcesSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Resources<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
|
||||
UberDocument doc = new UberDocument()
|
||||
.withUber(new Uber()
|
||||
.withVersion("1.0")
|
||||
.withData(extractLinksAndContent(value)));
|
||||
|
||||
provider
|
||||
.findValueSerializer(UberDocument.class, property)
|
||||
.serialize(doc, gen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(Resources<?> value) {
|
||||
return value.getContent().size() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new UberResourcesSerializer(property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link JsonSerializer} to render {@link PagedResources} into {@literal UBER+JSON}.
|
||||
*/
|
||||
static class UberPagedResourcesSerializer extends ContainerSerializer<PagedResources<?>> implements ContextualSerializer {
|
||||
|
||||
private BeanProperty property;
|
||||
|
||||
UberPagedResourcesSerializer(BeanProperty property) {
|
||||
|
||||
super(PagedResources.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
UberPagedResourcesSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(PagedResources<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
|
||||
UberDocument doc = new UberDocument()
|
||||
.withUber(new Uber()
|
||||
.withVersion("1.0")
|
||||
.withData(extractLinksAndContent(value)));
|
||||
|
||||
provider
|
||||
.findValueSerializer(UberDocument.class, property)
|
||||
.serialize(doc, gen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(PagedResources<?> value) {
|
||||
return value.getContent().size() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new UberPagedResourcesSerializer(property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link StdSerializer} to translate {@link UberAction} into the proper JSON representation.
|
||||
*/
|
||||
static class UberActionSerializer extends StdSerializer<UberAction> {
|
||||
|
||||
UberActionSerializer() {
|
||||
super(UberAction.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(UberAction value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
gen.writeString(value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link StdDeserializer} to deserialize {@link ResourceSupport}.
|
||||
*/
|
||||
static class UberResourceSupportDeserializer extends ContainerDeserializerBase<ResourceSupport> implements ContextualDeserializer {
|
||||
|
||||
private JavaType contentType;
|
||||
|
||||
UberResourceSupportDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
UberResourceSupportDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceSupport deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
|
||||
UberDocument doc = p.getCodec().readValue(p, UberDocument.class);
|
||||
|
||||
return doc.getUber().getData().stream()
|
||||
.filter(uberData -> !StringUtils.isEmpty(uberData.getName()))
|
||||
.findFirst()
|
||||
.map(uberData -> {
|
||||
|
||||
Map<String, Object> properties = uberData.getData().stream()
|
||||
.collect(Collectors.toMap(UberData::getName, UberData::getValue));
|
||||
|
||||
ResourceSupport obj = (ResourceSupport) PropertyUtils.createObjectFromProperties(this.getContentType().getRawClass(), properties);
|
||||
|
||||
obj.add(doc.getUber().getLinks());
|
||||
|
||||
return obj;
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
|
||||
ResourceSupport resourceSupport = new ResourceSupport();
|
||||
resourceSupport.add(doc.getUber().getLinks());
|
||||
|
||||
return resourceSupport;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
JavaType vc = property.getType().getContentType();
|
||||
return new UberResourceSupportDeserializer(vc);
|
||||
} else {
|
||||
return new UberResourceSupportDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesor for deserializer use for deserializing content values.
|
||||
*/
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link StdDeserializer} to deserialize {@link Resource}.
|
||||
*/
|
||||
static class UberResourceDeserializer extends ContainerDeserializerBase<Resource<?>> implements ContextualDeserializer {
|
||||
|
||||
private JavaType contentType;
|
||||
|
||||
UberResourceDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
UberResourceDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
|
||||
UberDocument doc = p.getCodec().readValue(p, UberDocument.class);
|
||||
|
||||
return doc.getUber().getData().stream()
|
||||
.filter(uberData -> !StringUtils.isEmpty(uberData.getName()))
|
||||
.findFirst()
|
||||
.map(uberData -> {
|
||||
|
||||
List<Link> links = doc.getUber().getLinks();
|
||||
|
||||
// Primitive type
|
||||
if (uberData.getData().size() == 1 && uberData.getData().get(0).getName() == null) {
|
||||
Object scalarValue = uberData.getData().get(0).getValue();
|
||||
return new Resource<>(scalarValue, links);
|
||||
}
|
||||
|
||||
Map<String, Object> properties = uberData.getData().stream()
|
||||
.collect(Collectors.toMap(UberData::getName, UberData::getValue));
|
||||
|
||||
JavaType rootType = JacksonHelper.findRootType(this.contentType);
|
||||
|
||||
Object value = PropertyUtils.createObjectFromProperties(rootType.getRawClass(), properties);
|
||||
|
||||
return new Resource<>(value, links);
|
||||
})
|
||||
.orElseThrow(() -> new IllegalStateException("No data entry containing a 'value' was found in this document!"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
JavaType vc = property.getType().getContentType();
|
||||
return new UberResourceDeserializer(vc);
|
||||
} else {
|
||||
return new UberResourceDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesor for deserializer use for deserializing content values.
|
||||
*/
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link StdDeserializer} to deserialize {@link Resources}.
|
||||
*/
|
||||
static class UberResourcesDeserializer extends ContainerDeserializerBase<Resources<?>> implements ContextualDeserializer {
|
||||
|
||||
private JavaType contentType;
|
||||
|
||||
UberResourcesDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
UberResourcesDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resources<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
|
||||
JavaType rootType = JacksonHelper.findRootType(this.contentType);
|
||||
|
||||
UberDocument doc = p.getCodec().readValue(p, UberDocument.class);
|
||||
|
||||
return extractResources(doc, rootType, this.contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for declared type of contained value elements; either exact
|
||||
* type, or one of its supertypes.
|
||||
*/
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
JavaType vc = property.getType().getContentType();
|
||||
return new UberResourcesDeserializer(vc);
|
||||
} else {
|
||||
return new UberResourcesDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesor for deserializer use for deserializing content values.
|
||||
*/
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link StdDeserializer} to deserialize {@link PagedResources}.
|
||||
*/
|
||||
static class UberPagedResourcesDeserializer extends ContainerDeserializerBase<PagedResources<?>> implements ContextualDeserializer {
|
||||
|
||||
private JavaType contentType;
|
||||
|
||||
UberPagedResourcesDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
UberPagedResourcesDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PagedResources<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
|
||||
JavaType rootType = JacksonHelper.findRootType(this.contentType);
|
||||
|
||||
UberDocument doc = p.getCodec().readValue(p, UberDocument.class);
|
||||
|
||||
Resources<?> resources = extractResources(doc, rootType, this.contentType);
|
||||
PageMetadata pageMetadata = extractPagingMetadata(doc);
|
||||
|
||||
return new PagedResources<>(resources.getContent(), pageMetadata, resources.getLinks());
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for declared type of contained value elements; either exact
|
||||
* type, or one of its supertypes.
|
||||
*/
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
JavaType vc = property.getType().getContentType();
|
||||
return new UberPagedResourcesDeserializer(vc);
|
||||
} else {
|
||||
return new UberPagedResourcesDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesor for deserializer use for deserializing content values.
|
||||
*/
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an {@link UberDocument} into a {@link Resources}.
|
||||
*
|
||||
* @param doc
|
||||
* @param rootType
|
||||
* @param contentType
|
||||
* @return
|
||||
*/
|
||||
private static Resources<?> extractResources(UberDocument doc, JavaType rootType, JavaType contentType) {
|
||||
|
||||
List<Object> content = new ArrayList<>();
|
||||
|
||||
for (UberData uberData : doc.getUber().getData()) {
|
||||
|
||||
if (uberData.getName() != null && uberData.getName().equals("page")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (uberData.getLinks().isEmpty()) {
|
||||
|
||||
List<Link> resourceLinks = new ArrayList<>();
|
||||
Resource<?> resource = null;
|
||||
|
||||
for (UberData item : uberData.getData()) {
|
||||
if (item.getRel() != null) {
|
||||
item.getRel().forEach(rel -> resourceLinks.add(new Link(item.getUrl(), rel)));
|
||||
} else {
|
||||
|
||||
// Primitive type
|
||||
if (item.getData().size() == 1 && item.getData().get(0).getName() == null) {
|
||||
|
||||
Object scalarValue = item.getData().get(0).getValue();
|
||||
resource = new Resource<>(scalarValue, uberData.getLinks());
|
||||
|
||||
} else {
|
||||
|
||||
Map<String, Object> properties = item.getData().stream()
|
||||
.collect(Collectors.toMap(UberData::getName, UberData::getValue));
|
||||
Object obj = PropertyUtils.createObjectFromProperties(rootType.getRawClass(), properties);
|
||||
resource = new Resource<>(obj, uberData.getLinks());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resource != null) {
|
||||
resource.add(resourceLinks);
|
||||
content.add(resource);
|
||||
} else {
|
||||
throw new RuntimeException("No content!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isResourcesOfResource(contentType)) {
|
||||
/*
|
||||
* Either return a Resources<Resource<T>>...
|
||||
*/
|
||||
return new Resources<>(content, doc.getUber().getLinks());
|
||||
} else {
|
||||
/*
|
||||
* ...or return a Resources<T>
|
||||
*/
|
||||
|
||||
List<Object> resourceLessContent = content.stream()
|
||||
.map(item -> (Resource<?>) item)
|
||||
.map(Resource::getContent)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new Resources<>(resourceLessContent, doc.getUber().getLinks());
|
||||
}
|
||||
}
|
||||
|
||||
private static PageMetadata extractPagingMetadata(UberDocument doc) {
|
||||
|
||||
return doc.getUber().getData().stream()
|
||||
.filter(uberData -> uberData.getName() != null && uberData.getName().equals("page"))
|
||||
.findFirst()
|
||||
.map(uberData -> {
|
||||
int size = 0;
|
||||
int number = 0;
|
||||
int totalElements = 0;
|
||||
int totalPages = 0;
|
||||
|
||||
for (UberData data : uberData.getData()) {
|
||||
if (data.getName().equals("size")) {
|
||||
size = (int) data.getValue();
|
||||
}
|
||||
|
||||
if (data.getName().equals("number")) {
|
||||
number = (int) data.getValue();
|
||||
}
|
||||
|
||||
if (data.getName().equals("totalElements")) {
|
||||
totalElements = (int) data.getValue();
|
||||
}
|
||||
|
||||
if (data.getName().equals("totalPages")) {
|
||||
totalPages = (int) data.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return new PageMetadata(size, number, totalElements, totalPages);
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer deserializer to handle {@link UberAction}.
|
||||
*/
|
||||
static class UberActionDeserializer extends StdDeserializer<UberAction> {
|
||||
|
||||
UberActionDeserializer() {
|
||||
super(UberAction.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UberAction deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
return UberAction.valueOf(p.getText().toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
public static class UberHandlerInstantiator extends HandlerInstantiator {
|
||||
|
||||
private final Map<Class<?>, Object> serializers = new HashMap<>();
|
||||
|
||||
public UberHandlerInstantiator() {
|
||||
|
||||
this.serializers.put(UberResourceSupportSerializer.class, new UberResourceSupportSerializer());
|
||||
this.serializers.put(UberResourceSerializer.class, new UberResourceSerializer());
|
||||
this.serializers.put(UberResourcesSerializer.class, new UberResourcesSerializer());
|
||||
this.serializers.put(UberPagedResourcesSerializer.class, new UberPagedResourcesSerializer());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> deserClass) {
|
||||
return (JsonDeserializer<?>) findInstance(deserClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> keyDeserClass) {
|
||||
return (KeyDeserializer) findInstance(keyDeserClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
|
||||
return (JsonSerializer<?>) findInstance(serClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated, Class<?> builderClass) {
|
||||
return (TypeResolverBuilder<?>) findInstance(builderClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
|
||||
return (TypeIdResolver) findInstance(resolverClass);
|
||||
}
|
||||
|
||||
private Object findInstance(Class<?> type) {
|
||||
|
||||
Object result = this.serializers.get(type);
|
||||
return result != null ? result : BeanUtils.instantiateClass(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.uber;
|
||||
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberPagedResourcesDeserializer;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to handle {@link PagedResources}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@JsonDeserialize(using = UberPagedResourcesDeserializer.class)
|
||||
abstract class PagedResourcesMixin<T> extends PagedResources<T> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.uber;
|
||||
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberResourceDeserializer;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to handle {@link Resource}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@JsonDeserialize(using = UberResourceDeserializer.class)
|
||||
abstract class ResourceMixin<T> extends Resource<T> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.uber;
|
||||
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberResourceSupportDeserializer;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to handle {@link ResourceSupport}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@JsonDeserialize(using = UberResourceSupportDeserializer.class)
|
||||
abstract class ResourceSupportMixin extends ResourceSupport {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.uber;
|
||||
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberResourcesDeserializer;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to handle {@link Resources}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@JsonDeserialize(using = UberResourcesDeserializer.class)
|
||||
abstract class ResourcesMixin<T> extends Resources<T> {
|
||||
|
||||
}
|
||||
74
src/main/java/org/springframework/hateoas/uber/Uber.java
Normal file
74
src/main/java/org/springframework/hateoas/uber/Uber.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.uber;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Enclosing collection in an UBER representation.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
class Uber {
|
||||
|
||||
private String version;
|
||||
private List<UberData> data;
|
||||
private UberError error;
|
||||
|
||||
@JsonCreator
|
||||
Uber(@JsonProperty("version") String version, @JsonProperty("data") List<UberData> data,
|
||||
@JsonProperty("error") UberError error) {
|
||||
|
||||
this.version = version;
|
||||
this.data = data;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
Uber() {
|
||||
this("1.0", null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract rel and url from every {@link UberData} entry.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
List<Link> getLinks() {
|
||||
|
||||
return this.data.stream()
|
||||
.flatMap(uberData -> uberData.getLinks().stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
107
src/main/java/org/springframework/hateoas/uber/UberAction.java
Normal file
107
src/main/java/org/springframework/hateoas/uber/UberAction.java
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2014-2017 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.hateoas.uber;
|
||||
|
||||
import static org.springframework.hateoas.uber.Jackson2UberModule.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* Embodies possible actions for an UBER representation, mapped onto {@link HttpMethod}s.
|
||||
*
|
||||
* @author Dietrich Schulten
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@JsonSerialize(using = UberActionSerializer.class)
|
||||
@JsonDeserialize(using = UberActionDeserializer.class)
|
||||
enum UberAction {
|
||||
|
||||
/**
|
||||
* POST
|
||||
*/
|
||||
APPEND(HttpMethod.POST),
|
||||
|
||||
/**
|
||||
* PATCH
|
||||
*/
|
||||
PARTIAL(HttpMethod.PATCH),
|
||||
|
||||
/**
|
||||
* GET
|
||||
*/
|
||||
READ(HttpMethod.GET),
|
||||
|
||||
/**
|
||||
* DELETE
|
||||
*/
|
||||
REMOVE(HttpMethod.DELETE),
|
||||
|
||||
/**
|
||||
* PUT
|
||||
*/
|
||||
REPLACE(HttpMethod.PUT);
|
||||
|
||||
private final HttpMethod httpMethod;
|
||||
|
||||
UberAction(HttpMethod method) {
|
||||
this.httpMethod = method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the related Spring Web {@link HttpMethod}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
HttpMethod getMethod() {
|
||||
return this.httpMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name().toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert an {@link HttpMethod} into an {@link UberAction}.
|
||||
* @param method
|
||||
* @return
|
||||
*/
|
||||
static UberAction fromMethod(HttpMethod method) {
|
||||
|
||||
return Arrays.stream(UberAction.values())
|
||||
.filter(action -> action.httpMethod == method)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unsupported method: " + method));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps given request method to uber action. GET will be mapped as {@literal null} since it is the default.
|
||||
*
|
||||
* @param method to map
|
||||
* @return action, or null for GET
|
||||
*/
|
||||
static UberAction forRequestMethod(HttpMethod method) {
|
||||
return HttpMethod.GET == method ? null : fromMethod(method);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.uber;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
/**
|
||||
* {@link AffordanceModel} for {@literal UBER+JSON}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
class UberAffordanceModel extends AffordanceModel {
|
||||
|
||||
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
|
||||
|
||||
private final @Getter Collection<MediaType> mediaTypes = Collections.singleton(MediaTypes.UBER_JSON);
|
||||
|
||||
private final @Getter List<UberData> inputProperties;
|
||||
private final @Getter List<UberData> queryProperties;
|
||||
|
||||
UberAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
super(name, link, httpMethod, inputType, queryMethodParameters, outputType);
|
||||
|
||||
this.inputProperties = determineAffordanceInputs();
|
||||
this.queryProperties = determineQueryProperties();
|
||||
}
|
||||
|
||||
private List<UberData> determineAffordanceInputs() {
|
||||
|
||||
if (ENTITY_ALTERING_METHODS.contains(getHttpMethod())) {
|
||||
|
||||
return PropertyUtils.findPropertyNames(getInputType()).stream()
|
||||
.map(propertyName -> new UberData()
|
||||
.withName(propertyName)
|
||||
.withValue(""))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform GET-based query parameters (e.g. {@literal &query}) into a list of {@link UberData} objects.
|
||||
*/
|
||||
private List<UberData> determineQueryProperties() {
|
||||
|
||||
if (!getHttpMethod().equals(HttpMethod.GET)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (getHttpMethod().equals(HttpMethod.GET)) {
|
||||
return getQueryMethodParameters().stream()
|
||||
.map(queryParameter -> new UberData()
|
||||
.withName(queryParameter.getName())
|
||||
.withValue(""))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
UberAction getAction() {
|
||||
return UberAction.forRequestMethod(getHttpMethod());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.uber;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.core.AffordanceModelFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
/**
|
||||
* {@link AffordanceModelFactory} for {@literal UBER+JSON}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
public class UberAffordanceModelFactory implements AffordanceModelFactory {
|
||||
|
||||
private final @Getter MediaType mediaType = MediaTypes.UBER_JSON;
|
||||
|
||||
@Override
|
||||
public AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
return new UberAffordanceModel(name, link, httpMethod, inputType, queryMethodParameters, outputType);
|
||||
}
|
||||
}
|
||||
456
src/main/java/org/springframework/hateoas/uber/UberData.java
Normal file
456
src/main/java/org/springframework/hateoas/uber/UberData.java
Normal file
@@ -0,0 +1,456 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.uber;
|
||||
|
||||
import static com.fasterxml.jackson.annotation.JsonInclude.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Core element containing either a {@link Link} or a single property inside an {@link UberDocument}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
class UberData {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String label;
|
||||
private List<String> rel;
|
||||
private String url;
|
||||
private UberAction action;
|
||||
private boolean transclude;
|
||||
private String model;
|
||||
private List<String> sending;
|
||||
private List<String> accepting;
|
||||
private Object value;
|
||||
private List<UberData> data;
|
||||
|
||||
@JsonCreator
|
||||
UberData(@JsonProperty("id") String id, @JsonProperty("name") String name,
|
||||
@JsonProperty("label") String label, @JsonProperty("rel") List<String> rel,
|
||||
@JsonProperty("url") String url, @JsonProperty("action") UberAction action,
|
||||
@JsonProperty("transclude") boolean transclude, @JsonProperty("model") String model,
|
||||
@JsonProperty("sending") List<String> sending, @JsonProperty("accepting") List<String> accepting,
|
||||
@JsonProperty("value") Object value, @JsonProperty("data") List<UberData> data) {
|
||||
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.label = label;
|
||||
this.rel = rel;
|
||||
this.url = url;
|
||||
this.action = action;
|
||||
this.transclude = transclude;
|
||||
this.model = model;
|
||||
this.sending = sending;
|
||||
this.accepting = accepting;
|
||||
this.value = value;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
UberData() {
|
||||
this(null, null, null, null, null, UberAction.READ, false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't render if it's {@link UberAction#READ}.
|
||||
*/
|
||||
public UberAction getAction() {
|
||||
|
||||
if (this.action == UberAction.READ) {
|
||||
return null;
|
||||
}
|
||||
return this.action;
|
||||
}
|
||||
|
||||
/*
|
||||
* Don't render if {@literal null}.
|
||||
*/
|
||||
public List<String> getRel() {
|
||||
|
||||
if (this.rel == null || this.rel.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return this.rel;
|
||||
}
|
||||
|
||||
/*
|
||||
* Don't render if {@literal null}.
|
||||
*/
|
||||
public List<UberData> getData() {
|
||||
|
||||
if (this.data == null || this.data.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Use a {@link Boolean} to support returning {@literal null}, and if it is {@literal null}, don't render.
|
||||
*/
|
||||
public Boolean isTemplated() {
|
||||
|
||||
return Optional.ofNullable(this.url)
|
||||
.map(s -> s.contains("{?") ? true : null)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public void setTemplated(boolean __) {
|
||||
// Ignore since "templated" is a virtual property
|
||||
}
|
||||
|
||||
/*
|
||||
* Use a {@link Boolean} to support returning {@literal null}, and if it is {@literal null}, don't render.
|
||||
*/
|
||||
public Boolean isTransclude() {
|
||||
return this.transclude ? this.transclude : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the links found in this {@link UberData}.
|
||||
*/
|
||||
@JsonIgnore
|
||||
public List<Link> getLinks() {
|
||||
|
||||
return Optional.ofNullable(this.rel)
|
||||
.map(rels -> rels.stream()
|
||||
.map(rel -> new Link(this.url, rel))
|
||||
.collect(Collectors.toList()))
|
||||
.orElse(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple scalar types that can be encoded by value, not type.
|
||||
*/
|
||||
private final static HashSet<Class<?>> PRIMITIVE_TYPES = new HashSet<>(Arrays.asList(
|
||||
String.class
|
||||
));
|
||||
|
||||
/**
|
||||
* Set of all Spring HATEOAS resource types.
|
||||
*/
|
||||
private static final HashSet<Class<?>> RESOURCE_TYPES = new HashSet<>(Arrays.asList(
|
||||
ResourceSupport.class,
|
||||
Resource.class,
|
||||
Resources.class,
|
||||
PagedResources.class
|
||||
));
|
||||
|
||||
/**
|
||||
* Convert a {@link ResourceSupport} into a list of {@link UberData}s, containing links and content.
|
||||
*
|
||||
* @param resource
|
||||
* @return
|
||||
*/
|
||||
static List<UberData> extractLinksAndContent(ResourceSupport resource) {
|
||||
|
||||
List<UberData> data = extractLinks(resource);
|
||||
|
||||
extractContent(resource).ifPresent(data::add);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a {@link Resource} into a list of {@link UberData}s, containing links and content.
|
||||
*
|
||||
* @param resource
|
||||
* @return
|
||||
*/
|
||||
static List<UberData> extractLinksAndContent(Resource<?> resource) {
|
||||
|
||||
List<UberData> data = extractLinks(resource);
|
||||
|
||||
extractContent(resource.getContent()).ifPresent(data::add);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert {@link Resources} into a list of {@link UberData}, with each item nested in a sub-UberData.
|
||||
*
|
||||
* @param resources
|
||||
* @return
|
||||
*/
|
||||
static List<UberData> extractLinksAndContent(Resources<?> resources) {
|
||||
|
||||
List<UberData> data = extractLinks(resources);
|
||||
|
||||
data.addAll(resources.getContent().stream()
|
||||
.map(UberData::doExtractLinksAndContent)
|
||||
.map(uberData -> new UberData().withData(uberData))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static List<UberData> extractLinksAndContent(PagedResources<?> resources) {
|
||||
|
||||
List<UberData> collectionOfResources = extractLinksAndContent((Resources<?>) resources);
|
||||
|
||||
if (resources.getMetadata() != null ) {
|
||||
|
||||
collectionOfResources.add(new UberData()
|
||||
.withName("page")
|
||||
.withData(Arrays.asList(
|
||||
new UberData()
|
||||
.withName("number")
|
||||
.withValue(resources.getMetadata().getNumber()),
|
||||
new UberData()
|
||||
.withName("size")
|
||||
.withValue(resources.getMetadata().getSize()),
|
||||
new UberData()
|
||||
.withName("totalElements")
|
||||
.withValue(resources.getMetadata().getTotalElements()),
|
||||
new UberData()
|
||||
.withName("totalPages")
|
||||
.withValue(resources.getMetadata().getTotalPages()))));
|
||||
}
|
||||
|
||||
return collectionOfResources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a {@link List} of {@link Link}s into a list of {@link UberData}.
|
||||
*
|
||||
* @param links
|
||||
* @return
|
||||
*/
|
||||
private static List<UberData> extractLinks(List<Link> links) {
|
||||
|
||||
return urlRelMap(links).entrySet().stream()
|
||||
.map(entry -> new UberData()
|
||||
.withUrl(entry.getKey())
|
||||
.withRel(entry.getValue().getRels()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the direct {@link Link}s and {@link Affordance}-based links from a {@link ResourceSupport}.
|
||||
*
|
||||
* @param resource
|
||||
* @return
|
||||
*/
|
||||
private static List<UberData> extractLinks(ResourceSupport resource) {
|
||||
|
||||
List<UberData> data = new ArrayList<>();
|
||||
|
||||
List<UberData> links = extractLinks(resource.getLinks());
|
||||
List<UberData> affordanceBasedLinks = extractAffordances(resource.getLinks());
|
||||
|
||||
if (affordanceBasedLinks.isEmpty()) {
|
||||
data.addAll(links);
|
||||
} else {
|
||||
data.addAll(mergeDeclaredLinksIntoAffordanceLinks(affordanceBasedLinks, links));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an object's properties into an {@link UberData}.
|
||||
*
|
||||
* @param content
|
||||
* @return
|
||||
*/
|
||||
private static Optional<UberData> extractContent(Object content) {
|
||||
|
||||
if (!RESOURCE_TYPES.contains(content.getClass())) {
|
||||
|
||||
return Optional.of(new UberData()
|
||||
.withName(StringUtils.uncapitalize(content.getClass().getSimpleName()))
|
||||
.withData(extractProperties(content)));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract links and content from an object of any type.
|
||||
*/
|
||||
private static List<UberData> doExtractLinksAndContent(Object item) {
|
||||
|
||||
if (item instanceof Resource) {
|
||||
return extractLinksAndContent((Resource<?>) item);
|
||||
}
|
||||
|
||||
if (item instanceof ResourceSupport) {
|
||||
return extractLinksAndContent((ResourceSupport) item);
|
||||
}
|
||||
|
||||
return extractLinksAndContent(new Resource<>(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a {@list List} of {@link Link}s into a {@link Map}, where you can see ALL the rels of a given
|
||||
* link.
|
||||
*
|
||||
* @param links
|
||||
* @return a map with links mapping onto a {@link List} of rels
|
||||
*/
|
||||
private static Map<String, LinkAndRels> urlRelMap(List<Link> links) {
|
||||
|
||||
Map<String, LinkAndRels> urlRelMap = new LinkedHashMap<>();
|
||||
|
||||
links.forEach(link -> {
|
||||
LinkAndRels linkAndRels = urlRelMap.computeIfAbsent(link.getHref(), s -> new LinkAndRels());
|
||||
linkAndRels.setLink(link);
|
||||
linkAndRels.getRels().add(link.getRel());
|
||||
});
|
||||
|
||||
return urlRelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the {@link Affordance}s for a set of {@link Link}s, and convert them into {@link UberData}.
|
||||
*
|
||||
* @param links
|
||||
* @return
|
||||
*/
|
||||
private static List<UberData> extractAffordances(List<Link> links) {
|
||||
|
||||
return links.stream()
|
||||
.flatMap(link -> link.getAffordances().stream())
|
||||
.map(affordance -> (UberAffordanceModel) affordance.getAffordanceModel(MediaTypes.UBER_JSON))
|
||||
.map(model -> {
|
||||
|
||||
if (model.getHttpMethod().equals(HttpMethod.GET)) {
|
||||
|
||||
String suffix = model.getQueryProperties().stream()
|
||||
.map(UberData::getName)
|
||||
.collect(Collectors.joining(","));
|
||||
|
||||
if (!model.getQueryMethodParameters().isEmpty()) {
|
||||
suffix = "{?" + suffix + "}";
|
||||
}
|
||||
|
||||
return new UberData()
|
||||
.withName(model.getName())
|
||||
.withRel(Arrays.asList(model.getName()))
|
||||
.withUrl(model.getLink().expand().getHref() + suffix)
|
||||
.withAction(model.getAction());
|
||||
|
||||
} else {
|
||||
|
||||
return new UberData()
|
||||
.withName(model.getName())
|
||||
.withRel(Arrays.asList(model.getName()))
|
||||
.withUrl(model.getLink().expand().getHref())
|
||||
.withModel(model.getInputProperties().stream()
|
||||
.map(UberData::getName)
|
||||
.map(property -> property + "={" + property + "}")
|
||||
.collect(Collectors.joining("&")))
|
||||
.withAction(model.getAction());
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a list of {@link Affordance}-based {@link Link}s, and overlay them with intersecting, declared {@link Link}s.
|
||||
*
|
||||
* @param affordanceBasedLinks
|
||||
* @param links
|
||||
* @return
|
||||
*/
|
||||
private static List<UberData> mergeDeclaredLinksIntoAffordanceLinks(List<UberData> affordanceBasedLinks, List<UberData> links) {
|
||||
|
||||
return affordanceBasedLinks.stream()
|
||||
.flatMap(affordance -> links.stream()
|
||||
.filter(link -> link.getUrl().equals(affordance.getUrl()))
|
||||
.map(link -> {
|
||||
|
||||
if (link.getAction() == affordance.getAction()) {
|
||||
|
||||
List<String> rels = new ArrayList<>(link.getRel());
|
||||
|
||||
rels.addAll(affordance.getRel());
|
||||
|
||||
return affordance
|
||||
.withName(rels.get(0))
|
||||
.withRel(rels);
|
||||
} else {
|
||||
return affordance;
|
||||
}
|
||||
}))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the payload of a {@link Resource} into {@link UberData}.
|
||||
*
|
||||
* @param obj
|
||||
* @return
|
||||
*/
|
||||
private static List<UberData> extractProperties(Object obj) {
|
||||
|
||||
if (PRIMITIVE_TYPES.contains(obj.getClass())) {
|
||||
return Arrays.asList(new UberData()
|
||||
.withValue(obj));
|
||||
}
|
||||
|
||||
return PropertyUtils.findProperties(obj).entrySet().stream()
|
||||
.map(entry -> new UberData()
|
||||
.withName(entry.getKey())
|
||||
.withValue(entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Holds both a {@link Link} and related {@literal rels}.
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
private static class LinkAndRels {
|
||||
|
||||
private Link link;
|
||||
private List<String> rels = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.uber;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Top-level element in an UBER representation.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
class UberDocument {
|
||||
|
||||
private Uber uber;
|
||||
|
||||
@JsonCreator
|
||||
UberDocument(@JsonProperty("version") String version, @JsonProperty("data") List<UberData> data,
|
||||
@JsonProperty("error") UberError error) {
|
||||
this.uber = new Uber(version, data, error);
|
||||
}
|
||||
|
||||
UberDocument() {
|
||||
this("1.0", null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an object into a {@link UberDocument}.
|
||||
*
|
||||
* @param object
|
||||
* @return
|
||||
*/
|
||||
static UberDocument toUberDocument(final Object object) {
|
||||
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (object instanceof UberDocument) {
|
||||
return (UberDocument) object;
|
||||
}
|
||||
|
||||
if (object instanceof Iterable) {
|
||||
|
||||
} else if (object instanceof Map) {
|
||||
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Don't know how to handle type : " + object.getClass());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.uber;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* UBER representation of an error.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
class UberError {
|
||||
|
||||
private List<UberData> data;
|
||||
|
||||
@JsonCreator
|
||||
UberError(@JsonProperty("data") List<UberData> data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
UberError() {
|
||||
this(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2014-2018 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.hateoas.uber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.LinkDiscoverer;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Find links by rel in an UBER representation.
|
||||
*
|
||||
* TODO: Pending https://github.com/json-path/JsonPath/issues/429, replace deserializing solution with JsonPath-based expression "$.uber.data[?(@.rel.indexOf('%s') != -1)].url"
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
public class UberLinkDiscoverer implements LinkDiscoverer {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
UberLinkDiscoverer() {
|
||||
|
||||
this.mapper = new ObjectMapper();
|
||||
this.mapper.registerModules(new Jackson2UberModule());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link findLinkWithRel(String rel, String representation) {
|
||||
|
||||
return getLinks(representation).stream()
|
||||
.filter(link -> link.getRel().equals(rel))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link findLinkWithRel(String rel, InputStream representation) {
|
||||
|
||||
return getLinks(representation).stream()
|
||||
.filter(link -> link.getRel().equals(rel))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Link> findLinksWithRel(String rel, String representation) {
|
||||
|
||||
return getLinks(representation).stream()
|
||||
.filter(link -> link.getRel().equals(rel))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Link> findLinksWithRel(String rel, InputStream representation) {
|
||||
|
||||
return getLinks(representation).stream()
|
||||
.filter(link -> link.getRel().equals(rel))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MediaType delimiter) {
|
||||
return delimiter.isCompatibleWith(MediaTypes.UBER_JSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize the entire document to find links.
|
||||
*
|
||||
* @param json
|
||||
* @return
|
||||
*/
|
||||
private List<Link> getLinks(String json) {
|
||||
|
||||
try {
|
||||
return this.mapper.readValue(json, UberDocument.class).getUber().getLinks();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize the entire document to find links.
|
||||
*
|
||||
* @param stream
|
||||
* @return
|
||||
*/
|
||||
private List<Link> getLinks(InputStream stream) {
|
||||
|
||||
try {
|
||||
return this.mapper.readValue(stream, UberDocument.class).getUber().getLinks();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
org.springframework.hateoas.core.AffordanceModelFactory=org.springframework.hateoas.hal.forms.HalFormsAffordanceModelFactory,org.springframework.hateoas.collectionjson.CollectionJsonAffordanceModelFactory
|
||||
org.springframework.hateoas.core.AffordanceModelFactory=org.springframework.hateoas.hal.forms.HalFormsAffordanceModelFactory,org.springframework.hateoas.collectionjson.CollectionJsonAffordanceModelFactory,org.springframework.hateoas.uber.UberAffordanceModelFactory
|
||||
|
||||
@@ -314,17 +314,25 @@ public class LinkUnitTest {
|
||||
assertThat(link.getHref()).isEqualTo("/");
|
||||
assertThat(link.getRel()).isEqualTo(Link.REL_SELF);
|
||||
assertThat(link.getAffordances()).hasSize(1);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModels()).hasSize(2);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModels()).hasSize(3);
|
||||
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getName()).isEqualTo("name");
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getInputType().resolve()).isEqualTo(Employee.class);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getQueryMethodParameters()).hasSize(0);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getOutputType().resolve()).isEqualTo(Employee.class);
|
||||
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getName()).isEqualTo("name");
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getInputType().resolve()).isEqualTo(Employee.class);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getQueryMethodParameters()).hasSize(0);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getOutputType().resolve()).isEqualTo(Employee.class);
|
||||
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getName()).isEqualTo("name");
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getInputType().resolve()).isEqualTo(Employee.class);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getQueryMethodParameters()).hasSize(0);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getOutputType().resolve()).isEqualTo(Employee.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -335,17 +343,25 @@ public class LinkUnitTest {
|
||||
assertThat(link.getHref()).isEqualTo("/");
|
||||
assertThat(link.getRel()).isEqualTo(Link.REL_SELF);
|
||||
assertThat(link.getAffordances()).hasSize(1);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModels()).hasSize(2);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModels()).hasSize(3);
|
||||
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getName()).isEqualTo("postEmployee");
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getInputType().resolve()).isEqualTo(Employee.class);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getQueryMethodParameters()).hasSize(0);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getOutputType().resolve()).isEqualTo(Employee.class);
|
||||
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getName()).isEqualTo("postEmployee");
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getInputType().resolve()).isEqualTo(Employee.class);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getQueryMethodParameters()).hasSize(0);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getOutputType().resolve()).isEqualTo(Employee.class);
|
||||
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getName()).isEqualTo("postEmployee");
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getInputType().resolve()).isEqualTo(Employee.class);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getQueryMethodParameters()).hasSize(0);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getOutputType().resolve()).isEqualTo(Employee.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -356,17 +372,24 @@ public class LinkUnitTest {
|
||||
assertThat(link.getHref()).isEqualTo("/");
|
||||
assertThat(link.getRel()).isEqualTo(Link.REL_SELF);
|
||||
assertThat(link.getAffordances()).hasSize(1);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModels()).hasSize(2);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModels()).hasSize(3);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getName()).isEqualTo("postEmployee");
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getInputType().resolve()).isEqualTo(Employee.class);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getQueryMethodParameters()).hasSize(0);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.COLLECTION_JSON).getOutputType().resolve()).isEqualTo(Employee.class);
|
||||
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getName()).isEqualTo("postEmployee");
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getInputType().resolve()).isEqualTo(Employee.class);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getQueryMethodParameters()).hasSize(0);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.HAL_FORMS_JSON).getOutputType().resolve()).isEqualTo(Employee.class);
|
||||
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getName()).isEqualTo("postEmployee");
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getInputType().resolve()).isEqualTo(Employee.class);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getQueryMethodParameters()).hasSize(0);
|
||||
assertThat(link.getAffordances().get(0).getAffordanceModel(MediaTypes.UBER_JSON).getOutputType().resolve()).isEqualTo(Employee.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.springframework.hateoas.hal.HalLinkDiscoverer;
|
||||
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
|
||||
import org.springframework.hateoas.hal.forms.HalFormsLinkDiscoverer;
|
||||
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
|
||||
import org.springframework.hateoas.uber.UberLinkDiscoverer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
|
||||
@@ -87,10 +88,16 @@ public class EnableHypermediaSupportIntegrationTest {
|
||||
assertHalFormsSetupForConfigClass(HalFormsConfig.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bootstrapJsonCollectionConfiguration() {
|
||||
assertCollectionJsonSetupForConfigClass(CollectionJsonConfig.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bootstrapUberConfiguration() {
|
||||
assertUberSetupForConfigClass(UberConfig.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registersHalLinkDiscoverers() {
|
||||
|
||||
@@ -133,6 +140,19 @@ public class EnableHypermediaSupportIntegrationTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registersUberLinkDiscoverers() {
|
||||
|
||||
withContext(UberConfig.class, context -> {
|
||||
|
||||
LinkDiscoverers discoverers = context.getBean(LinkDiscoverers.class);
|
||||
|
||||
assertThat(discoverers).isNotNull();
|
||||
assertThat(discoverers.getLinkDiscovererFor(MediaTypes.UBER_JSON)).isInstanceOf(UberLinkDiscoverer.class);
|
||||
assertRelProvidersSetUp(context);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bootstrapsHalConfigurationForSubclass() {
|
||||
assertHalSetupForConfigClass(ExtendedHalConfig.class);
|
||||
@@ -148,6 +168,11 @@ public class EnableHypermediaSupportIntegrationTest {
|
||||
assertCollectionJsonSetupForConfigClass(ExtendedCollectionJsonConfig.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bootstrapsUberConfigurationForSubclass() {
|
||||
assertUberSetupForConfigClass(ExtendedUberConfig.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #134, #219
|
||||
*/
|
||||
@@ -247,11 +272,46 @@ public class EnableHypermediaSupportIntegrationTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void uberSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() {
|
||||
|
||||
withContext(UberConfig.class, context -> {
|
||||
|
||||
RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class);
|
||||
|
||||
assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes())
|
||||
.hasSize(1)
|
||||
.contains(MediaTypes.UBER_JSON);
|
||||
|
||||
boolean found = false;
|
||||
|
||||
for (HandlerMethodArgumentResolver resolver : getResolvers(adapter)) {
|
||||
|
||||
if (resolver instanceof AbstractMessageConverterMethodArgumentResolver) {
|
||||
|
||||
found = true;
|
||||
|
||||
AbstractMessageConverterMethodArgumentResolver processor = (AbstractMessageConverterMethodArgumentResolver) resolver;
|
||||
List<HttpMessageConverter<?>> converters = (List<HttpMessageConverter<?>>) ReflectionTestUtils
|
||||
.getField(processor, "messageConverters");
|
||||
|
||||
assertThat(converters.get(0)).isInstanceOf(TypeConstrainedMappingJackson2HttpMessageConverter.class);
|
||||
assertThat(converters.get(0).getSupportedMediaTypes())
|
||||
.hasSize(1)
|
||||
.contains(MediaTypes.UBER_JSON);
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(found).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #293
|
||||
*/
|
||||
@Test
|
||||
public void registersHttpMessageConvertersForRestTemplate() {
|
||||
public void registersHalHttpMessageConvertersForRestTemplate() {
|
||||
|
||||
withContext(HalConfig.class, context -> {
|
||||
|
||||
@@ -323,6 +383,18 @@ public class EnableHypermediaSupportIntegrationTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registersUberHttpMessageConvertersForRestTemplate() {
|
||||
|
||||
withContext(UberConfig.class, context -> {
|
||||
RestTemplate template = context.getBean(RestTemplate.class);
|
||||
|
||||
assertThat(template.getMessageConverters().get(0).getSupportedMediaTypes())
|
||||
.hasSize(1)
|
||||
.contains(MediaTypes.UBER_JSON);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #341
|
||||
*/
|
||||
@@ -362,6 +434,17 @@ public class EnableHypermediaSupportIntegrationTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuresDefaultObjectMapperForUberToIgnoreUnknownProperties() {
|
||||
|
||||
withContext(UberConfig.class, context -> {
|
||||
|
||||
assertObjectMapper(context, MediaTypes.UBER_JSON, mapper -> {
|
||||
assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyDefaultHalConfigurationRendersSingleItemAsSingleItem() throws JsonProcessingException {
|
||||
|
||||
@@ -389,7 +472,7 @@ public class EnableHypermediaSupportIntegrationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyRenderSingleLinkAsArrayViaOverridingBean() throws JsonProcessingException {
|
||||
public void verifyRenderSingleLinkAsArrayViaOverridingBean() {
|
||||
|
||||
withContext(RenderLinkAsSingleLinksConfig.class, context -> {
|
||||
|
||||
@@ -468,6 +551,18 @@ public class EnableHypermediaSupportIntegrationTest {
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertUberSetupForConfigClass(Class<?> configClass) {
|
||||
|
||||
withContext(configClass, context -> {
|
||||
|
||||
assertEntityLinksSetUp(context);
|
||||
assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(UberLinkDiscoverer.class);
|
||||
|
||||
RequestMappingHandlerAdapter rmha = context.getBean(RequestMappingHandlerAdapter.class);
|
||||
assertThat(rmha.getMessageConverters().get(0)).isInstanceOf(MappingJackson2HttpMessageConverter.class);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to mitigate API changes between Spring 3.2 and 4.0.
|
||||
*
|
||||
@@ -581,4 +676,26 @@ public class EnableHypermediaSupportIntegrationTest {
|
||||
static class AlternateDelegateConfig {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebMvc
|
||||
@Configuration
|
||||
@Import(DelegateUberHypermediaConfig.class)
|
||||
static class UberConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ExtendedUberConfig extends UberConfig {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableHypermediaSupport(type = HypermediaType.UBER)
|
||||
static class DelegateUberHypermediaConfig {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.collection.IsCollectionWithSize.*;
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
|
||||
|
||||
@@ -270,6 +271,176 @@ public class MultiMediatypeWebMvcIntegrationTest {
|
||||
.andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].required", is(false)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleEmployeeUber() throws Exception {
|
||||
|
||||
this.mockMvc.perform(get("/employees/0").accept(MediaTypes.UBER_JSON)) //
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk()) //
|
||||
|
||||
.andExpect(jsonPath("$.uber.version", is("1.0")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data.*", hasSize(5)))
|
||||
.andExpect(jsonPath("$.uber.data[0].name", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[0]", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[1]", is("findOne")))
|
||||
.andExpect(jsonPath("$.uber.data[0].url", is("http://localhost/employees/0")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[1].name", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].rel[0]", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].url", is("http://localhost/employees/0")))
|
||||
.andExpect(jsonPath("$.uber.data[1].action", is("replace")))
|
||||
.andExpect(jsonPath("$.uber.data[1].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].name", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].rel[0]", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].url", is("http://localhost/employees/0")))
|
||||
.andExpect(jsonPath("$.uber.data[2].action", is("partial")))
|
||||
.andExpect(jsonPath("$.uber.data[2].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].name", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[3].rel[0]", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[3].rel[1]", is("all")))
|
||||
.andExpect(jsonPath("$.uber.data[3].url", is("http://localhost/employees")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[4].name", is("employee")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data.*", hasSize(2)))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[0].name", is("role")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[0].value", is("ring bearer")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[1].name", is("name")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[1].value", is("Frodo Baggins")))
|
||||
;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectionOfEmployeesUber() throws Exception {
|
||||
|
||||
this.mockMvc.perform(get("/employees").accept(MediaTypes.UBER_JSON)) //
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk()) //
|
||||
|
||||
.andExpect(jsonPath("$.uber.version", is("1.0")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data.*", hasSize(4)))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[0].name", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[0]", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[1]", is("all")))
|
||||
.andExpect(jsonPath("$.uber.data[0].url", is("http://localhost/employees")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[1].name", is("newEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].rel[0]", is("newEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].url", is("http://localhost/employees")))
|
||||
.andExpect(jsonPath("$.uber.data[1].action", is("append")))
|
||||
.andExpect(jsonPath("$.uber.data[1].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].data[0].name", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[0].rel[0]", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[0].rel[1]", is("findOne")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[0].url", is("http://localhost/employees/0")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].data[1].name", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[1].rel[0]", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[1].url", is("http://localhost/employees/0")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[1].action", is("replace")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[1].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].data[2].name", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[2].rel[0]", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[2].url", is("http://localhost/employees/0")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[2].action", is("partial")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[2].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].data[3].rel[0]", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[3].rel[1]", is("all")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[3].url", is("http://localhost/employees")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].data[4].name", is("employee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[4].data[0].name", is("role")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[4].data[0].value", is("ring bearer")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[4].data[1].name", is("name")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[4].data[1].value", is("Frodo Baggins")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].data[0].name", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[0].rel[0]", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[0].rel[1]", is("findOne")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[0].url", is("http://localhost/employees/1")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].data[1].name", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[1].rel[0]", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[1].url", is("http://localhost/employees/1")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[1].action", is("replace")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[1].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].data[2].name", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[2].rel[0]", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[2].url", is("http://localhost/employees/1")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[2].action", is("partial")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[2].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].data[3].rel[0]", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[3].rel[1]", is("all")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[3].url", is("http://localhost/employees")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].data[4].name", is("employee")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[4].data[0].name", is("role")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[4].data[0].value", is("burglar")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[4].data[1].name", is("name")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[4].data[1].value", is("Bilbo Baggins")))
|
||||
;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createNewEmployeeUber() throws Exception {
|
||||
|
||||
String input = MappingUtils.read(new ClassPathResource("../uber/create-employee.json", getClass()));
|
||||
|
||||
this.mockMvc.perform(post("/employees")
|
||||
.content(input)
|
||||
.contentType(MediaTypes.UBER_JSON))
|
||||
.andDo(print())
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(header().stringValues(HttpHeaders.LOCATION, "http://localhost/employees/2"));
|
||||
|
||||
this.mockMvc.perform(get("/employees/2").accept(MediaTypes.UBER_JSON))
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk()) //
|
||||
|
||||
.andExpect(jsonPath("$.uber.version", is("1.0")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data.*", hasSize(5)))
|
||||
.andExpect(jsonPath("$.uber.data[0].name", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[0]", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[1]", is("findOne")))
|
||||
.andExpect(jsonPath("$.uber.data[0].url", is("http://localhost/employees/2")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[1].name", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].rel[0]", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].url", is("http://localhost/employees/2")))
|
||||
.andExpect(jsonPath("$.uber.data[1].action", is("replace")))
|
||||
.andExpect(jsonPath("$.uber.data[1].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].name", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].rel[0]", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].url", is("http://localhost/employees/2")))
|
||||
.andExpect(jsonPath("$.uber.data[2].action", is("partial")))
|
||||
.andExpect(jsonPath("$.uber.data[2].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].name", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[3].rel[0]", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[3].rel[1]", is("all")))
|
||||
.andExpect(jsonPath("$.uber.data[3].url", is("http://localhost/employees")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[4].name", is("employee")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data.*", hasSize(2)))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[0].name", is("role")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[0].value", is("gardener")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[1].name", is("name")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[1].value", is("Samwise Gamgee")))
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
static class EmployeeController {
|
||||
|
||||
@@ -403,7 +574,7 @@ public class MultiMediatypeWebMvcIntegrationTest {
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@EnableHypermediaSupport(type = { HypermediaType.COLLECTION_JSON, HypermediaType.HAL_FORMS })
|
||||
@EnableHypermediaSupport(type = { HypermediaType.HAL, HypermediaType.COLLECTION_JSON, HypermediaType.HAL_FORMS, HypermediaType.UBER })
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
* Copyright 2017 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.hateoas.uber;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.hateoas.AbstractJackson2MarshallingIntegrationTest;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Links;
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.hateoas.support.MappingUtils;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberHandlerInstantiator;
|
||||
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class Jackson2UberIntegrationTest extends AbstractJackson2MarshallingIntegrationTest {
|
||||
|
||||
static final Links PAGINATION_LINKS = new Links(
|
||||
new Link("localhost", Link.REL_SELF),
|
||||
new Link("foo", Link.REL_NEXT),
|
||||
new Link("bar", Link.REL_PREVIOUS));
|
||||
|
||||
@Before
|
||||
public void setUpModule() {
|
||||
|
||||
this.mapper.registerModule(new Jackson2UberModule());
|
||||
this.mapper.setHandlerInstantiator(new UberHandlerInstantiator());
|
||||
this.mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void rendersSingleLinkAsObject() throws Exception {
|
||||
|
||||
ResourceSupport resourceSupport = new ResourceSupport();
|
||||
resourceSupport.add(new Link("localhost").withSelfRel());
|
||||
|
||||
assertThat(write(resourceSupport)).isEqualTo(MappingUtils.read(new ClassPathResource("resource-support.json", getClass())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void deserializeSingleLink() throws Exception {
|
||||
|
||||
ResourceSupport expected = new ResourceSupport();
|
||||
expected.add(new Link("localhost"));
|
||||
assertThat(read(MappingUtils.read(new ClassPathResource("resource-support.json", getClass())), ResourceSupport.class))
|
||||
.isEqualTo(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void rendersMultipleLinkAsArray() throws Exception {
|
||||
|
||||
ResourceSupport resourceSupport = new ResourceSupport();
|
||||
resourceSupport.add(new Link("localhost"));
|
||||
resourceSupport.add(new Link("localhost2").withRel("orders"));
|
||||
|
||||
assertThat(write(resourceSupport)).isEqualTo(MappingUtils.read(new ClassPathResource("resource-support-2.json", getClass())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void deserializeMultipleLinks() throws Exception {
|
||||
|
||||
ResourceSupport expected = new ResourceSupport();
|
||||
expected.add(new Link("localhost"));
|
||||
expected.add(new Link("localhost2").withRel("orders"));
|
||||
|
||||
assertThat(read(MappingUtils.read(new ClassPathResource("resource-support-2.json", getClass())), ResourceSupport.class))
|
||||
.isEqualTo(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void rendersSimpleResourcesAsEmbedded() throws Exception {
|
||||
|
||||
List<String> content = new ArrayList<>();
|
||||
content.add("first");
|
||||
content.add("second");
|
||||
|
||||
Resources<String> resources = new Resources<>(content);
|
||||
resources.add(new Link("localhost"));
|
||||
|
||||
assertThat(write(resources)).isEqualTo(MappingUtils.read(new ClassPathResource("resources.json", getClass())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void deserializesSimpleResourcesWithNoLinks() throws Exception {
|
||||
|
||||
List<String> content = new ArrayList<>();
|
||||
content.add("first");
|
||||
content.add("second");
|
||||
|
||||
Resources<String> expected = new Resources<>(content);
|
||||
expected.add(new Link("localhost"));
|
||||
|
||||
String resourcesJson = MappingUtils.read(new ClassPathResource("resources.json", getClass()));
|
||||
JavaType resourcesType = mapper.getTypeFactory().constructParametricType(Resources.class, String.class);
|
||||
Resources<String> result = mapper.readValue(resourcesJson, resourcesType);
|
||||
|
||||
assertThat(result).isEqualTo(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void deserializeComplexResourcesSimply() throws IOException {
|
||||
|
||||
List<Resource<String>> content = new ArrayList<>();
|
||||
content.add(new Resource<>("first"));
|
||||
content.add(new Resource<>("second"));
|
||||
|
||||
Resources<Resource<String>> expected = new Resources<>(content);
|
||||
expected.add(new Link("localhost"));
|
||||
|
||||
String resourcesJson = MappingUtils.read(new ClassPathResource("resources.json", getClass()));
|
||||
|
||||
JavaType resourcesType = mapper.getTypeFactory().constructParametricType(Resources.class,
|
||||
mapper.getTypeFactory().constructParametricType(Resource.class, String.class));
|
||||
|
||||
Resources<Resource<String>> result = mapper.readValue(resourcesJson, resourcesType);
|
||||
|
||||
assertThat(result).isEqualTo(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void renderSimpleResource() throws Exception {
|
||||
|
||||
Resource<String> data = new Resource<>("first", new Link("localhost"));
|
||||
assertThat(write(data)).isEqualTo(MappingUtils.read(new ClassPathResource("resource.json", getClass())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void renderResourceWithCustomRel() throws Exception {
|
||||
|
||||
Resource<String> data2 = new Resource<>("second", new Link("localhost").withRel("custom"));
|
||||
assertThat(write(data2)).isEqualTo(MappingUtils.read(new ClassPathResource("resource2.json", getClass())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void renderResourceWithMultipleLinks() throws Exception {
|
||||
|
||||
Resource<String> data3 = new Resource<>("third",
|
||||
new Link("localhost"),
|
||||
new Link("second").withRel("second"),
|
||||
new Link("third").withRel("third"));
|
||||
assertThat(write(data3)).isEqualTo(MappingUtils.read(new ClassPathResource("resource3.json", getClass())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void renderResourceWithMultipleRels() throws Exception {
|
||||
|
||||
Resource<String> data4 = new Resource<>("third",
|
||||
new Link("localhost"),
|
||||
new Link("localhost").withRel("http://example.org/rels/todo"),
|
||||
new Link("second").withRel("second"),
|
||||
new Link("third").withRel("third"));
|
||||
assertThat(write(data4)).isEqualTo(MappingUtils.read(new ClassPathResource("resource4.json", getClass())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void deserializeResource() throws IOException {
|
||||
|
||||
JavaType resourceStringType = mapper.getTypeFactory().constructParametricType(Resource.class, String.class);
|
||||
|
||||
Resource expected = new Resource<>("first", new Link("localhost"));
|
||||
Resource<String> actual = mapper.readValue(MappingUtils.read(new ClassPathResource("resource.json", getClass())),
|
||||
resourceStringType);
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
|
||||
Resource<String> expected2 = new Resource<>("second", new Link("localhost").withRel("custom"));
|
||||
Resource<String> actual2 = mapper.readValue(MappingUtils.read(new ClassPathResource("resource2.json", getClass())),
|
||||
resourceStringType);
|
||||
|
||||
assertThat(actual2).isEqualTo(expected2);
|
||||
|
||||
Resource<String> expected3 = new Resource<>("third",
|
||||
new Link("localhost"),
|
||||
new Link("second").withRel("second"),
|
||||
new Link("third").withRel("third"));
|
||||
Resource<String> actual3 = mapper.readValue(MappingUtils.read(new ClassPathResource("resource3.json", getClass())),
|
||||
resourceStringType);
|
||||
|
||||
assertThat(actual3).isEqualTo(expected3);
|
||||
|
||||
Resource<String> expected4 = new Resource<>("third",
|
||||
new Link("localhost"),
|
||||
new Link("localhost").withRel("http://example.org/rels/todo"),
|
||||
new Link("second").withRel("second"),
|
||||
new Link("third").withRel("third"));
|
||||
Resource<String> actual4 = mapper.readValue(MappingUtils.read(new ClassPathResource("resource4.json", getClass())),
|
||||
resourceStringType);
|
||||
|
||||
assertThat(actual4).isEqualTo(expected4);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void renderComplexStructure() throws Exception {
|
||||
|
||||
List<Resource<String>> data = new ArrayList<>();
|
||||
data.add(new Resource<>("first", new Link("localhost"), new Link("orders").withRel("orders")));
|
||||
data.add(new Resource<>("second", new Link("remotehost"), new Link("order").withRel("orders")));
|
||||
|
||||
Resources<Resource<String>> resources = new Resources<>(data);
|
||||
resources.add(new Link("localhost"));
|
||||
resources.add(new Link("/page/2").withRel("next"));
|
||||
|
||||
assertThat(write(resources)).isEqualTo(MappingUtils.read(new ClassPathResource("resources-with-resource-objects.json", getClass())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void deserializeResources() throws Exception {
|
||||
|
||||
List<Resource<String>> data = new ArrayList<Resource<String>>();
|
||||
data.add(new Resource<>("first", new Link("localhost"), new Link("orders").withRel("orders")));
|
||||
data.add(new Resource<>("second", new Link("remotehost"), new Link("order").withRel("orders")));
|
||||
|
||||
Resources expected = new Resources<>(data);
|
||||
expected.add(new Link("localhost"));
|
||||
expected.add(new Link("/page/2").withRel("next"));
|
||||
|
||||
Resources<Resource<String>> actual = mapper.readValue(MappingUtils.read(new ClassPathResource("resources-with-resource-objects.json", getClass())),
|
||||
mapper.getTypeFactory().constructParametricType(Resources.class,
|
||||
mapper.getTypeFactory().constructParametricType(Resource.class, String.class)));
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void deserializeResourcesSimply() throws Exception {
|
||||
|
||||
List<String> data = new ArrayList<>();
|
||||
data.add("first");
|
||||
data.add("second");
|
||||
|
||||
Resources expected = new Resources<>(data);
|
||||
expected.add(new Link("localhost"));
|
||||
expected.add(new Link("/page/2").withRel("next"));
|
||||
|
||||
Resources<String> actual = mapper.readValue(MappingUtils.read(new ClassPathResource("resources-with-resource-objects.json", getClass())),
|
||||
mapper.getTypeFactory().constructParametricType(Resources.class, String.class));
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void serializeWrappedSimplePojo() throws Exception {
|
||||
|
||||
Employee employee = new Employee("Frodo", "ring bearer");
|
||||
Resource<Employee> expected = new Resource<>(employee, new Link("/employees/1").withSelfRel());
|
||||
|
||||
String actual = MappingUtils.read(new ClassPathResource("resource-with-simple-pojo.json", getClass()));
|
||||
|
||||
assertThat(write(expected)).isEqualTo(actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void deserializeWrappedSimplePojo() throws IOException {
|
||||
|
||||
Employee employee = new Employee("Frodo", "ring bearer");
|
||||
Resource<Employee> expected = new Resource<>(employee, new Link("/employees/1").withSelfRel());
|
||||
|
||||
Resource<Employee> actual = mapper.readValue(MappingUtils.read(new ClassPathResource("resource-with-simple-pojo.json", getClass())),
|
||||
mapper.getTypeFactory().constructParametricType(Resource.class, Employee.class));
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void serializeConcreteResourceSupport() throws Exception {
|
||||
|
||||
EmployeeResource expected = new EmployeeResource("Frodo", "ring bearer");
|
||||
expected.add(new Link("/employees/1").withSelfRel());
|
||||
expected.add(new Link("/employees").withRel("employees"));
|
||||
|
||||
String actual = MappingUtils.read(new ClassPathResource("resource-support-pojo.json", getClass()));
|
||||
|
||||
assertThat(write(expected)).isEqualTo(actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void deserializeConcreteResourceSupport() throws Exception {
|
||||
|
||||
EmployeeResource expected = new EmployeeResource("Frodo", "ring bearer");
|
||||
expected.add(new Link("/employees/1").withSelfRel());
|
||||
expected.add(new Link("/employees").withRel("employees"));
|
||||
|
||||
EmployeeResource actual = mapper.readValue(
|
||||
MappingUtils.read(new ClassPathResource("resource-support-pojo.json", getClass())),
|
||||
EmployeeResource.class);
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void serializesPagedResource() throws Exception {
|
||||
|
||||
String actual = write(setupAnnotatedPagedResources());
|
||||
assertThat(actual).isEqualTo(MappingUtils.read(new ClassPathResource("paged-resources.json", getClass())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void deserializesPagedResource() throws Exception {
|
||||
|
||||
PagedResources<Resource<Employee>> result = mapper.readValue(MappingUtils.read(new ClassPathResource("paged-resources.json", getClass())),
|
||||
mapper.getTypeFactory().constructParametricType(PagedResources.class,
|
||||
mapper.getTypeFactory().constructParametricType(Resource.class, Employee.class)));
|
||||
|
||||
assertThat(result).isEqualTo(setupAnnotatedPagedResources());
|
||||
}
|
||||
|
||||
private static Resources<Resource<Employee>> setupAnnotatedPagedResources() {
|
||||
|
||||
List<Resource<Employee>> content = new ArrayList<>();
|
||||
Employee employee = new Employee("Frodo", "ring bearer");
|
||||
Resource<Employee> employeeResource = new Resource<>(employee, new Link("/employees/1").withSelfRel());
|
||||
content.add(employeeResource);
|
||||
|
||||
return new PagedResources<>(
|
||||
content,
|
||||
new PagedResources.PageMetadata(2, 0, 4),
|
||||
PAGINATION_LINKS);
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
static class Employee {
|
||||
|
||||
private String name;
|
||||
private String role;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
static class EmployeeResource extends ResourceSupport {
|
||||
|
||||
private String name;
|
||||
private String role;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2013-2018 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.hateoas.uber;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.hateoas.support.MappingUtils.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.hateoas.LinkDiscoverer;
|
||||
import org.springframework.hateoas.core.AbstractLinkDiscovererUnitTest;
|
||||
import org.springframework.hateoas.hal.HalLinkDiscoverer;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link HalLinkDiscoverer}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class UberLinkDiscovererUnitTest extends AbstractLinkDiscovererUnitTest {
|
||||
|
||||
LinkDiscoverer discoverer = new UberLinkDiscoverer();
|
||||
String sample;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
|
||||
this.discoverer = new UberLinkDiscoverer();
|
||||
this.sample = read(new ClassPathResource("link-discovery.json", getClass()));
|
||||
}
|
||||
/**
|
||||
* @see #314
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void discoversFullyQualifiedRel() {
|
||||
assertThat(getDiscoverer().findLinkWithRel("http://foo.com/bar", this.sample)).isNotNull();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LinkDiscoverer getDiscoverer() {
|
||||
return discoverer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInputString() {
|
||||
return this.sample;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInputStringWithoutLinkContainer() {
|
||||
return "{ \"uber\" : { \"data\" : []}}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
/*
|
||||
* Copyright 2017 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.hateoas.uber;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.collection.IsCollectionWithSize.*;
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
|
||||
import static org.springframework.hateoas.support.MappingUtils.*;
|
||||
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 static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
|
||||
import org.springframework.hateoas.support.Employee;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration
|
||||
public class UberWebMvcIntegrationTest {
|
||||
|
||||
@Autowired WebApplicationContext context;
|
||||
|
||||
MockMvc mockMvc;
|
||||
|
||||
private static Map<Integer, Employee> EMPLOYEES;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
this.mockMvc = webAppContextSetup(this.context).build();
|
||||
|
||||
EMPLOYEES = new TreeMap<>();
|
||||
|
||||
EMPLOYEES.put(0, new Employee("Frodo Baggins", "ring bearer"));
|
||||
EMPLOYEES.put(1, new Employee("Bilbo Baggins", "burglar"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void singleEmployee() throws Exception {
|
||||
|
||||
this.mockMvc.perform(get("/employees/0").accept(MediaTypes.UBER_JSON)) //
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk()) //
|
||||
|
||||
.andExpect(jsonPath("$.uber.version", is("1.0")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data.*", hasSize(5)))
|
||||
.andExpect(jsonPath("$.uber.data[0].name", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[0]", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[1]", is("findOne")))
|
||||
.andExpect(jsonPath("$.uber.data[0].url", is("http://localhost/employees/0")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[1].name", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].rel[0]", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].url", is("http://localhost/employees/0")))
|
||||
.andExpect(jsonPath("$.uber.data[1].action", is("replace")))
|
||||
.andExpect(jsonPath("$.uber.data[1].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].name", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].rel[0]", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].url", is("http://localhost/employees/0")))
|
||||
.andExpect(jsonPath("$.uber.data[2].action", is("partial")))
|
||||
.andExpect(jsonPath("$.uber.data[2].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].name", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[3].rel[0]", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[3].rel[1]", is("all")))
|
||||
.andExpect(jsonPath("$.uber.data[3].url", is("http://localhost/employees")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[4].name", is("employee")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data.*", hasSize(2)))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[0].name", is("role")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[0].value", is("ring bearer")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[1].name", is("name")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[1].value", is("Frodo Baggins")))
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void collectionOfEmployees() throws Exception {
|
||||
|
||||
this.mockMvc.perform(get("/employees").accept(MediaTypes.UBER_JSON)) //
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk()) //
|
||||
|
||||
.andExpect(jsonPath("$.uber.version", is("1.0")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data.*", hasSize(4)))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[0].name", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[0]", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[1]", is("all")))
|
||||
.andExpect(jsonPath("$.uber.data[0].url", is("http://localhost/employees")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[1].name", is("newEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].rel[0]", is("newEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].url", is("http://localhost/employees")))
|
||||
.andExpect(jsonPath("$.uber.data[1].action", is("append")))
|
||||
.andExpect(jsonPath("$.uber.data[1].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].data[0].name", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[0].rel[0]", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[0].rel[1]", is("findOne")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[0].url", is("http://localhost/employees/0")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].data[1].name", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[1].rel[0]", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[1].url", is("http://localhost/employees/0")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[1].action", is("replace")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[1].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].data[2].name", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[2].rel[0]", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[2].url", is("http://localhost/employees/0")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[2].action", is("partial")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[2].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].data[3].rel[0]", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[3].rel[1]", is("all")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[3].url", is("http://localhost/employees")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].data[4].name", is("employee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[4].data[0].name", is("role")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[4].data[0].value", is("ring bearer")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[4].data[1].name", is("name")))
|
||||
.andExpect(jsonPath("$.uber.data[2].data[4].data[1].value", is("Frodo Baggins")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].data[0].name", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[0].rel[0]", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[0].rel[1]", is("findOne")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[0].url", is("http://localhost/employees/1")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].data[1].name", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[1].rel[0]", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[1].url", is("http://localhost/employees/1")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[1].action", is("replace")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[1].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].data[2].name", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[2].rel[0]", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[2].url", is("http://localhost/employees/1")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[2].action", is("partial")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[2].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].data[3].rel[0]", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[3].rel[1]", is("all")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[3].url", is("http://localhost/employees")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].data[4].name", is("employee")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[4].data[0].name", is("role")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[4].data[0].value", is("burglar")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[4].data[1].name", is("name")))
|
||||
.andExpect(jsonPath("$.uber.data[3].data[4].data[1].value", is("Bilbo Baggins")))
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #784
|
||||
*/
|
||||
@Test
|
||||
public void createNewEmployee() throws Exception {
|
||||
|
||||
String input = read(new ClassPathResource("create-employee.json", getClass()));
|
||||
|
||||
this.mockMvc.perform(post("/employees")
|
||||
.content(input)
|
||||
.contentType(MediaTypes.UBER_JSON))
|
||||
.andDo(print())
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(header().stringValues(HttpHeaders.LOCATION, "http://localhost/employees/2"));
|
||||
|
||||
this.mockMvc.perform(get("/employees/2").accept(MediaTypes.UBER_JSON))
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk()) //
|
||||
|
||||
.andExpect(jsonPath("$.uber.version", is("1.0")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data.*", hasSize(5)))
|
||||
.andExpect(jsonPath("$.uber.data[0].name", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[0]", is("self")))
|
||||
.andExpect(jsonPath("$.uber.data[0].rel[1]", is("findOne")))
|
||||
.andExpect(jsonPath("$.uber.data[0].url", is("http://localhost/employees/2")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[1].name", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].rel[0]", is("updateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[1].url", is("http://localhost/employees/2")))
|
||||
.andExpect(jsonPath("$.uber.data[1].action", is("replace")))
|
||||
.andExpect(jsonPath("$.uber.data[1].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[2].name", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].rel[0]", is("partiallyUpdateEmployee")))
|
||||
.andExpect(jsonPath("$.uber.data[2].url", is("http://localhost/employees/2")))
|
||||
.andExpect(jsonPath("$.uber.data[2].action", is("partial")))
|
||||
.andExpect(jsonPath("$.uber.data[2].model", is("name={name}&role={role}")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[3].name", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[3].rel[0]", is("employees")))
|
||||
.andExpect(jsonPath("$.uber.data[3].rel[1]", is("all")))
|
||||
.andExpect(jsonPath("$.uber.data[3].url", is("http://localhost/employees")))
|
||||
|
||||
.andExpect(jsonPath("$.uber.data[4].name", is("employee")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data.*", hasSize(2)))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[0].name", is("role")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[0].value", is("gardener")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[1].name", is("name")))
|
||||
.andExpect(jsonPath("$.uber.data[4].data[1].value", is("Samwise Gamgee")))
|
||||
;
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class EmployeeController {
|
||||
|
||||
@GetMapping("/employees")
|
||||
public Resources<Resource<Employee>> all() {
|
||||
|
||||
// Create a list of Resource<Employee>'s to return
|
||||
List<Resource<Employee>> employees = new ArrayList<>();
|
||||
|
||||
// Fetch each Resource<Employee> using the controller's findOne method.
|
||||
for (int i = 0; i < EMPLOYEES.size(); i++) {
|
||||
employees.add(findOne(i));
|
||||
}
|
||||
|
||||
// Generate an "Affordance" based on this method (the "self" link)
|
||||
Link selfLink = linkTo(methodOn(EmployeeController.class).all()).withSelfRel()
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null)))
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).search(null, null)));
|
||||
|
||||
// Return the collection of employee resources along with the composite affordance
|
||||
return new Resources<>(employees, selfLink);
|
||||
}
|
||||
|
||||
@GetMapping("/employees/search")
|
||||
public Resources<Resource<Employee>> search(@RequestParam(value="name", required=false) String name,
|
||||
@RequestParam(value="role", required=false) String role) {
|
||||
|
||||
// Create a list of Resource<Employee>'s to return
|
||||
List<Resource<Employee>> employees = new ArrayList<>();
|
||||
|
||||
// Fetch each Resource<Employee> using the controller's findOne method.
|
||||
for (int i = 0; i < EMPLOYEES.size(); i++) {
|
||||
Resource<Employee> employeeResource = findOne(i);
|
||||
|
||||
boolean nameMatches = Optional.ofNullable(name)
|
||||
.map(s -> employeeResource.getContent().getName().contains(s))
|
||||
.orElse(true);
|
||||
|
||||
boolean roleMatches = Optional.ofNullable(role)
|
||||
.map( s -> employeeResource.getContent().getRole().contains(s))
|
||||
.orElse(true);
|
||||
|
||||
if (nameMatches && roleMatches) {
|
||||
employees.add(employeeResource);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate an "Affordance" based on this method (the "self" link)
|
||||
Link selfLink = linkTo(methodOn(EmployeeController.class).all()).withSelfRel()
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null)))
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).search(null, null)));
|
||||
|
||||
// Return the collection of employee resources along with the composite affordance
|
||||
return new Resources<>(employees, selfLink);
|
||||
}
|
||||
|
||||
@GetMapping("/employees/{id}")
|
||||
public Resource<Employee> findOne(@PathVariable Integer id) {
|
||||
|
||||
// Start the affordance with the "self" link, i.e. this method.
|
||||
Link findOneLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel();
|
||||
|
||||
// Define final link as means to find entire collection.
|
||||
Link employeesLink = linkTo(methodOn(EmployeeController.class).all()).withRel("employees");
|
||||
|
||||
// Return the affordance + a link back to the entire collection resource.
|
||||
return new Resource<>(EMPLOYEES.get(id),
|
||||
findOneLink
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))) //
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).partiallyUpdateEmployee(null, id))),
|
||||
employeesLink);
|
||||
}
|
||||
|
||||
@PostMapping("/employees")
|
||||
public ResponseEntity<?> newEmployee(@RequestBody Resource<Employee> employee) {
|
||||
|
||||
int newEmployeeId = EMPLOYEES.size();
|
||||
|
||||
EMPLOYEES.put(newEmployeeId, employee.getContent());
|
||||
|
||||
try {
|
||||
return ResponseEntity.created(new URI(findOne(newEmployeeId).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse("")))
|
||||
.build();
|
||||
} catch (URISyntaxException e) {
|
||||
return ResponseEntity.badRequest().body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/employees/{id}")
|
||||
public ResponseEntity<?> updateEmployee(@RequestBody Resource<Employee> employee, @PathVariable Integer id) {
|
||||
|
||||
EMPLOYEES.put(id, employee.getContent());
|
||||
|
||||
try {
|
||||
return ResponseEntity.noContent().location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse("")))
|
||||
.build();
|
||||
} catch (URISyntaxException e) {
|
||||
return ResponseEntity.badRequest().body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PatchMapping("/employees/{id}")
|
||||
public ResponseEntity<?> partiallyUpdateEmployee(@RequestBody Resource<Employee> employee, @PathVariable Integer id) {
|
||||
|
||||
Employee oldEmployee = EMPLOYEES.get(id);
|
||||
Employee newEmployee = oldEmployee;
|
||||
|
||||
if (employee.getContent().getName() != null) {
|
||||
newEmployee = newEmployee.withName(employee.getContent().getName());
|
||||
}
|
||||
|
||||
if (employee.getContent().getRole() != null) {
|
||||
newEmployee = newEmployee.withRole(employee.getContent().getRole());
|
||||
}
|
||||
|
||||
EMPLOYEES.put(id, newEmployee);
|
||||
|
||||
try {
|
||||
return ResponseEntity.noContent().location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse("")))
|
||||
.build();
|
||||
} catch (URISyntaxException e) {
|
||||
return ResponseEntity.badRequest().body(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@EnableHypermediaSupport(type = { HypermediaType.UBER})
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
EmployeeController employeeController() {
|
||||
return new EmployeeController();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"uber": {
|
||||
"version": "1.0",
|
||||
"data": [
|
||||
{
|
||||
"name": "employee",
|
||||
"data": [
|
||||
{
|
||||
"name": "role",
|
||||
"value": "gardener"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"value": "Samwise Gamgee"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"uber" :
|
||||
{
|
||||
"version" : "1.0",
|
||||
"data" :
|
||||
[
|
||||
{
|
||||
"rel" : ["self"],
|
||||
"url" : "selfHref"
|
||||
},
|
||||
{
|
||||
"rel" : ["relation"],
|
||||
"url" : "firstHref"
|
||||
},
|
||||
{
|
||||
"rel" : ["relation"],
|
||||
"url" : "secondHref"
|
||||
},
|
||||
{
|
||||
"rel" : ["http://foo.com/bar"],
|
||||
"url" : "fullRelHref"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "localhost"
|
||||
}, {
|
||||
"rel" : [ "next" ],
|
||||
"url" : "foo"
|
||||
}, {
|
||||
"rel" : [ "prev" ],
|
||||
"url" : "bar"
|
||||
}, {
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "/employees/1"
|
||||
}, {
|
||||
"name" : "employee",
|
||||
"data" : [ {
|
||||
"name" : "role",
|
||||
"value" : "ring bearer"
|
||||
}, {
|
||||
"name" : "name",
|
||||
"value" : "Frodo"
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"name" : "page",
|
||||
"data" : [ {
|
||||
"name" : "number",
|
||||
"value" : 0
|
||||
}, {
|
||||
"name" : "size",
|
||||
"value" : 2
|
||||
}, {
|
||||
"name" : "totalElements",
|
||||
"value" : 4
|
||||
}, {
|
||||
"name" : "totalPages",
|
||||
"value" : 2
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"_links" : {
|
||||
"collection" : {
|
||||
"href" : "/employees"
|
||||
},
|
||||
"self" : {
|
||||
"href" : "/employees/1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "localhost"
|
||||
}, {
|
||||
"rel" : [ "orders" ],
|
||||
"url" : "localhost2"
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "/employees/1"
|
||||
}, {
|
||||
"rel" : [ "employees" ],
|
||||
"url" : "/employees"
|
||||
}, {
|
||||
"name" : "employeeResource",
|
||||
"data" : [ {
|
||||
"name" : "role",
|
||||
"value" : "ring bearer"
|
||||
}, {
|
||||
"name" : "name",
|
||||
"value" : "Frodo"
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "localhost"
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "/employees/1"
|
||||
}, {
|
||||
"name" : "employee",
|
||||
"data" : [ {
|
||||
"name" : "role",
|
||||
"value" : "ring bearer"
|
||||
}, {
|
||||
"name" : "name",
|
||||
"value" : "Frodo"
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "localhost"
|
||||
}, {
|
||||
"name" : "string",
|
||||
"data" : [ {
|
||||
"value" : "first"
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "custom" ],
|
||||
"url" : "localhost"
|
||||
}, {
|
||||
"name" : "string",
|
||||
"data" : [ {
|
||||
"value" : "second"
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "localhost"
|
||||
}, {
|
||||
"rel" : [ "second" ],
|
||||
"url" : "second"
|
||||
}, {
|
||||
"rel" : [ "third" ],
|
||||
"url" : "third"
|
||||
}, {
|
||||
"name" : "string",
|
||||
"data" : [ {
|
||||
"value" : "third"
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "self", "http://example.org/rels/todo" ],
|
||||
"url" : "localhost"
|
||||
}, {
|
||||
"rel" : [ "second" ],
|
||||
"url" : "second"
|
||||
}, {
|
||||
"rel" : [ "third" ],
|
||||
"url" : "third"
|
||||
}, {
|
||||
"name" : "string",
|
||||
"data" : [ {
|
||||
"value" : "third"
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "localhost"
|
||||
}, {
|
||||
"rel" : [ "next" ],
|
||||
"url" : "/page/2"
|
||||
}, {
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "localhost"
|
||||
}, {
|
||||
"rel" : [ "orders" ],
|
||||
"url" : "orders"
|
||||
}, {
|
||||
"name" : "string",
|
||||
"data" : [ {
|
||||
"value" : "first"
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "remotehost"
|
||||
}, {
|
||||
"rel" : [ "orders" ],
|
||||
"url" : "order"
|
||||
}, {
|
||||
"name" : "string",
|
||||
"data" : [ {
|
||||
"value" : "second"
|
||||
} ]
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"uber" : {
|
||||
"version" : "1.0",
|
||||
"data" : [ {
|
||||
"rel" : [ "self" ],
|
||||
"url" : "localhost"
|
||||
}, {
|
||||
"data" : [ {
|
||||
"name" : "string",
|
||||
"data" : [ {
|
||||
"value" : "first"
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"data" : [ {
|
||||
"name" : "string",
|
||||
"data" : [ {
|
||||
"value" : "second"
|
||||
} ]
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user