#784 - Introduce UBER+JSON mediatype.

Based on https://rawgit.com/uber-hypermedia/specification/master/uber-hypermedia.html.
This commit is contained in:
Greg Turnquist
2018-06-07 09:03:28 -05:00
parent 820f66179f
commit 8da3a851ce
40 changed files with 3468 additions and 12 deletions

View File

@@ -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);
}

View File

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

View File

@@ -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);

View File

@@ -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));
}

View File

@@ -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());
}
}

View File

@@ -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;
}
/**

View File

@@ -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);
}
}
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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 {
}

View File

@@ -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> {
}

View 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());
}
}

View 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);
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View 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<>();
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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 {
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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\" : []}}";
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,20 @@
{
"uber": {
"version": "1.0",
"data": [
{
"name": "employee",
"data": [
{
"name": "role",
"value": "gardener"
},
{
"name": "name",
"value": "Samwise Gamgee"
}
]
}
]
}
}

View File

@@ -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"
}
]
}
}

View File

@@ -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
} ]
} ]
}
}

View File

@@ -0,0 +1,10 @@
{
"_links" : {
"collection" : {
"href" : "/employees"
},
"self" : {
"href" : "/employees/1"
}
}
}

View File

@@ -0,0 +1,12 @@
{
"uber" : {
"version" : "1.0",
"data" : [ {
"rel" : [ "self" ],
"url" : "localhost"
}, {
"rel" : [ "orders" ],
"url" : "localhost2"
} ]
}
}

View File

@@ -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"
} ]
} ]
}
}

View File

@@ -0,0 +1,9 @@
{
"uber" : {
"version" : "1.0",
"data" : [ {
"rel" : [ "self" ],
"url" : "localhost"
} ]
}
}

View File

@@ -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"
} ]
} ]
}
}

View File

@@ -0,0 +1,14 @@
{
"uber" : {
"version" : "1.0",
"data" : [ {
"rel" : [ "self" ],
"url" : "localhost"
}, {
"name" : "string",
"data" : [ {
"value" : "first"
} ]
} ]
}
}

View File

@@ -0,0 +1,14 @@
{
"uber" : {
"version" : "1.0",
"data" : [ {
"rel" : [ "custom" ],
"url" : "localhost"
}, {
"name" : "string",
"data" : [ {
"value" : "second"
} ]
} ]
}
}

View File

@@ -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"
} ]
} ]
}
}

View File

@@ -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"
} ]
} ]
}
}

View File

@@ -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"
} ]
} ]
} ]
}
}

View File

@@ -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"
} ]
} ]
} ]
}
}