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