#784 - Introduce UBER+JSON mediatype.
Based on https://rawgit.com/uber-hypermedia/specification/master/uber-hypermedia.html.
This commit is contained in:
@@ -76,4 +76,14 @@ public class MediaTypes {
|
||||
* Public constant media type for {@code application/vnd.collection+json}.
|
||||
*/
|
||||
public static final MediaType COLLECTION_JSON = MediaType.valueOf(COLLECTION_JSON_VALUE);
|
||||
|
||||
/**
|
||||
* A String equivalent of {@link MediaTypes#UBER_JSON_VALUE}.
|
||||
*/
|
||||
public static final String UBER_JSON_VALUE = "application/vnd.amundsen-uber+json";
|
||||
|
||||
/**
|
||||
* Public constant media type for {@code application/vnd.amundsen-uber+json}.
|
||||
*/
|
||||
public static final MediaType UBER_JSON = MediaType.parseMediaType(UBER_JSON_VALUE);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
|
||||
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
|
||||
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator;
|
||||
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberHandlerInstantiator;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
@@ -96,7 +98,7 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
|
||||
if (converters.stream()
|
||||
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
|
||||
.map(AbstractJackson2HttpMessageConverter.class::cast)
|
||||
.map(converter -> converter.getObjectMapper())
|
||||
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
|
||||
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
|
||||
|
||||
return;
|
||||
@@ -124,8 +126,29 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
|
||||
if (hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
|
||||
converters.add(0, createCollectionJsonConverter(objectMapper, linkRelationMessageSource));
|
||||
}
|
||||
|
||||
if (hypermediaTypes.contains(HypermediaType.UBER)) {
|
||||
converters.add(0, createUberJsonConverter(objectMapper));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param objectMapper
|
||||
* @return
|
||||
*/
|
||||
protected MappingJackson2HttpMessageConverter createUberJsonConverter(ObjectMapper objectMapper) {
|
||||
|
||||
ObjectMapper mapper = objectMapper.copy();
|
||||
|
||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
mapper.registerModule(new Jackson2UberModule());
|
||||
mapper.setHandlerInstantiator(new UberHandlerInstantiator());
|
||||
|
||||
return new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
ResourceSupport.class, Arrays.asList(UBER_JSON), mapper);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param objectMapper
|
||||
* @param linkRelationMessageSource
|
||||
|
||||
@@ -86,7 +86,14 @@ public @interface EnableHypermediaSupport {
|
||||
*
|
||||
* @see http://amundsen.com/media-types/collection/format/
|
||||
*/
|
||||
COLLECTION_JSON;
|
||||
COLLECTION_JSON,
|
||||
|
||||
/**
|
||||
* UBER Hypermedia
|
||||
*
|
||||
* @see http://uberhypermedia.org/
|
||||
*/
|
||||
UBER;
|
||||
|
||||
private static Set<HypermediaType> HAL_BASED_MEDIATYPES = EnumSet.of(HAL, HAL_FORMS);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.hateoas.collectionjson.CollectionJsonLinkDiscoverer;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
|
||||
import org.springframework.hateoas.hal.HalLinkDiscoverer;
|
||||
import org.springframework.hateoas.hal.forms.HalFormsLinkDiscoverer;
|
||||
import org.springframework.hateoas.uber.UberLinkDiscoverer;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
@@ -96,6 +97,9 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
|
||||
case COLLECTION_JSON:
|
||||
definition = new RootBeanDefinition(CollectionJsonLinkDiscoverer.class);
|
||||
break;
|
||||
case UBER:
|
||||
definition = new RootBeanDefinition(UberLinkDiscoverer.class);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(String.format("Unsupported hypermedia type %s!", type));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.hateoas.support;
|
||||
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.Resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
|
||||
/**
|
||||
@@ -36,4 +39,18 @@ public final class JacksonHelper {
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a {@literal Resources<Resource<?>>}?
|
||||
*
|
||||
* @param type
|
||||
* @return
|
||||
*/
|
||||
public static boolean isResourcesOfResource(JavaType type) {
|
||||
|
||||
return
|
||||
Resources.class.isAssignableFrom(type.getRawClass())
|
||||
&&
|
||||
Resource.class.isAssignableFrom(type.containedType(0).getRawClass());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ public class PropertyUtils {
|
||||
.collect(HashMap::new,
|
||||
(hashMap, descriptor) -> {
|
||||
try {
|
||||
hashMap.put(descriptor.getName(), descriptor.getReadMethod().invoke(object));
|
||||
Method readMethod = descriptor.getReadMethod();
|
||||
ReflectionUtils.makeAccessible(readMethod);
|
||||
hashMap.put(descriptor.getName(), readMethod.invoke(object));
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -85,7 +87,8 @@ public class PropertyUtils {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static Object createObjectFromProperties(Class<?> clazz, Map<String, Object> properties) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T createObjectFromProperties(Class<T> clazz, Map<String, Object> properties) {
|
||||
|
||||
Object obj = BeanUtils.instantiateClass(clazz);
|
||||
|
||||
@@ -102,7 +105,7 @@ public class PropertyUtils {
|
||||
});
|
||||
});
|
||||
|
||||
return obj;
|
||||
return (T) obj;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,741 @@
|
||||
/*
|
||||
* Copyright 2017-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import static org.springframework.hateoas.PagedResources.*;
|
||||
import static org.springframework.hateoas.support.JacksonHelper.*;
|
||||
import static org.springframework.hateoas.uber.UberData.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.hateoas.support.JacksonHelper;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.Version;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
|
||||
import com.fasterxml.jackson.databind.cfg.MapperConfig;
|
||||
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
|
||||
import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import com.fasterxml.jackson.databind.introspect.Annotated;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.ContainerSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
|
||||
/**
|
||||
* Jackson {@link SimpleModule} for {@literal UBER+JSON} serializers and deserializers.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
public class Jackson2UberModule extends SimpleModule {
|
||||
|
||||
public Jackson2UberModule() {
|
||||
|
||||
super("uber-module", new Version(1, 0, 0, null, "org.springframework.hateoas", "spring-hateoas"));
|
||||
|
||||
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
|
||||
setMixInAnnotation(Resource.class, ResourceMixin.class);
|
||||
setMixInAnnotation(Resources.class, ResourcesMixin.class);
|
||||
setMixInAnnotation(PagedResources.class, PagedResourcesMixin.class);
|
||||
|
||||
addSerializer(new UberPagedResourcesSerializer());
|
||||
addSerializer(new UberResourcesSerializer());
|
||||
addSerializer(new UberResourceSerializer());
|
||||
addSerializer(new UberResourceSupportSerializer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link JsonSerializer} to render {@link ResourceSupport} into {@literal UBER+JSON}.
|
||||
*/
|
||||
static class UberResourceSupportSerializer extends ContainerSerializer<ResourceSupport> implements ContextualSerializer {
|
||||
|
||||
private final BeanProperty property;
|
||||
|
||||
UberResourceSupportSerializer(BeanProperty property) {
|
||||
|
||||
super(ResourceSupport.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
UberResourceSupportSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(ResourceSupport value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
|
||||
UberDocument doc = new UberDocument()
|
||||
.withUber(new Uber()
|
||||
.withVersion("1.0")
|
||||
.withData(extractLinksAndContent(value)));
|
||||
|
||||
provider
|
||||
.findValueSerializer(UberDocument.class, property)
|
||||
.serialize(doc, gen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(ResourceSupport value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new UberResourceSupportSerializer(property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link JsonSerializer} to render {@link Resource} into {@literal UBER+JSON}.
|
||||
*/
|
||||
static class UberResourceSerializer extends ContainerSerializer<Resource<?>> implements ContextualSerializer {
|
||||
|
||||
private final BeanProperty property;
|
||||
|
||||
UberResourceSerializer(BeanProperty property) {
|
||||
|
||||
super(Resource.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
UberResourceSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Resource<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
|
||||
UberDocument doc = new UberDocument()
|
||||
.withUber(new Uber()
|
||||
.withVersion("1.0")
|
||||
.withData(extractLinksAndContent(value)));
|
||||
|
||||
provider
|
||||
.findValueSerializer(UberDocument.class, property)
|
||||
.serialize(doc, gen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(Resource<?> value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new UberResourceSerializer(property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link JsonSerializer} to render {@link Resources} into {@literal UBER+JSON}.
|
||||
*/
|
||||
static class UberResourcesSerializer extends ContainerSerializer<Resources<?>> implements ContextualSerializer {
|
||||
|
||||
private BeanProperty property;
|
||||
|
||||
UberResourcesSerializer(BeanProperty property) {
|
||||
|
||||
super(Resources.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
UberResourcesSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Resources<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
|
||||
UberDocument doc = new UberDocument()
|
||||
.withUber(new Uber()
|
||||
.withVersion("1.0")
|
||||
.withData(extractLinksAndContent(value)));
|
||||
|
||||
provider
|
||||
.findValueSerializer(UberDocument.class, property)
|
||||
.serialize(doc, gen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(Resources<?> value) {
|
||||
return value.getContent().size() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new UberResourcesSerializer(property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link JsonSerializer} to render {@link PagedResources} into {@literal UBER+JSON}.
|
||||
*/
|
||||
static class UberPagedResourcesSerializer extends ContainerSerializer<PagedResources<?>> implements ContextualSerializer {
|
||||
|
||||
private BeanProperty property;
|
||||
|
||||
UberPagedResourcesSerializer(BeanProperty property) {
|
||||
|
||||
super(PagedResources.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
UberPagedResourcesSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(PagedResources<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
|
||||
UberDocument doc = new UberDocument()
|
||||
.withUber(new Uber()
|
||||
.withVersion("1.0")
|
||||
.withData(extractLinksAndContent(value)));
|
||||
|
||||
provider
|
||||
.findValueSerializer(UberDocument.class, property)
|
||||
.serialize(doc, gen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(PagedResources<?> value) {
|
||||
return value.getContent().size() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new UberPagedResourcesSerializer(property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link StdSerializer} to translate {@link UberAction} into the proper JSON representation.
|
||||
*/
|
||||
static class UberActionSerializer extends StdSerializer<UberAction> {
|
||||
|
||||
UberActionSerializer() {
|
||||
super(UberAction.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(UberAction value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
gen.writeString(value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link StdDeserializer} to deserialize {@link ResourceSupport}.
|
||||
*/
|
||||
static class UberResourceSupportDeserializer extends ContainerDeserializerBase<ResourceSupport> implements ContextualDeserializer {
|
||||
|
||||
private JavaType contentType;
|
||||
|
||||
UberResourceSupportDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
UberResourceSupportDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceSupport deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
|
||||
UberDocument doc = p.getCodec().readValue(p, UberDocument.class);
|
||||
|
||||
return doc.getUber().getData().stream()
|
||||
.filter(uberData -> !StringUtils.isEmpty(uberData.getName()))
|
||||
.findFirst()
|
||||
.map(uberData -> {
|
||||
|
||||
Map<String, Object> properties = uberData.getData().stream()
|
||||
.collect(Collectors.toMap(UberData::getName, UberData::getValue));
|
||||
|
||||
ResourceSupport obj = (ResourceSupport) PropertyUtils.createObjectFromProperties(this.getContentType().getRawClass(), properties);
|
||||
|
||||
obj.add(doc.getUber().getLinks());
|
||||
|
||||
return obj;
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
|
||||
ResourceSupport resourceSupport = new ResourceSupport();
|
||||
resourceSupport.add(doc.getUber().getLinks());
|
||||
|
||||
return resourceSupport;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
JavaType vc = property.getType().getContentType();
|
||||
return new UberResourceSupportDeserializer(vc);
|
||||
} else {
|
||||
return new UberResourceSupportDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesor for deserializer use for deserializing content values.
|
||||
*/
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link StdDeserializer} to deserialize {@link Resource}.
|
||||
*/
|
||||
static class UberResourceDeserializer extends ContainerDeserializerBase<Resource<?>> implements ContextualDeserializer {
|
||||
|
||||
private JavaType contentType;
|
||||
|
||||
UberResourceDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
UberResourceDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
|
||||
UberDocument doc = p.getCodec().readValue(p, UberDocument.class);
|
||||
|
||||
return doc.getUber().getData().stream()
|
||||
.filter(uberData -> !StringUtils.isEmpty(uberData.getName()))
|
||||
.findFirst()
|
||||
.map(uberData -> {
|
||||
|
||||
List<Link> links = doc.getUber().getLinks();
|
||||
|
||||
// Primitive type
|
||||
if (uberData.getData().size() == 1 && uberData.getData().get(0).getName() == null) {
|
||||
Object scalarValue = uberData.getData().get(0).getValue();
|
||||
return new Resource<>(scalarValue, links);
|
||||
}
|
||||
|
||||
Map<String, Object> properties = uberData.getData().stream()
|
||||
.collect(Collectors.toMap(UberData::getName, UberData::getValue));
|
||||
|
||||
JavaType rootType = JacksonHelper.findRootType(this.contentType);
|
||||
|
||||
Object value = PropertyUtils.createObjectFromProperties(rootType.getRawClass(), properties);
|
||||
|
||||
return new Resource<>(value, links);
|
||||
})
|
||||
.orElseThrow(() -> new IllegalStateException("No data entry containing a 'value' was found in this document!"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
JavaType vc = property.getType().getContentType();
|
||||
return new UberResourceDeserializer(vc);
|
||||
} else {
|
||||
return new UberResourceDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesor for deserializer use for deserializing content values.
|
||||
*/
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link StdDeserializer} to deserialize {@link Resources}.
|
||||
*/
|
||||
static class UberResourcesDeserializer extends ContainerDeserializerBase<Resources<?>> implements ContextualDeserializer {
|
||||
|
||||
private JavaType contentType;
|
||||
|
||||
UberResourcesDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
UberResourcesDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resources<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
|
||||
JavaType rootType = JacksonHelper.findRootType(this.contentType);
|
||||
|
||||
UberDocument doc = p.getCodec().readValue(p, UberDocument.class);
|
||||
|
||||
return extractResources(doc, rootType, this.contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for declared type of contained value elements; either exact
|
||||
* type, or one of its supertypes.
|
||||
*/
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
JavaType vc = property.getType().getContentType();
|
||||
return new UberResourcesDeserializer(vc);
|
||||
} else {
|
||||
return new UberResourcesDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesor for deserializer use for deserializing content values.
|
||||
*/
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link StdDeserializer} to deserialize {@link PagedResources}.
|
||||
*/
|
||||
static class UberPagedResourcesDeserializer extends ContainerDeserializerBase<PagedResources<?>> implements ContextualDeserializer {
|
||||
|
||||
private JavaType contentType;
|
||||
|
||||
UberPagedResourcesDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
UberPagedResourcesDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PagedResources<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
|
||||
JavaType rootType = JacksonHelper.findRootType(this.contentType);
|
||||
|
||||
UberDocument doc = p.getCodec().readValue(p, UberDocument.class);
|
||||
|
||||
Resources<?> resources = extractResources(doc, rootType, this.contentType);
|
||||
PageMetadata pageMetadata = extractPagingMetadata(doc);
|
||||
|
||||
return new PagedResources<>(resources.getContent(), pageMetadata, resources.getLinks());
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for declared type of contained value elements; either exact
|
||||
* type, or one of its supertypes.
|
||||
*/
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
JavaType vc = property.getType().getContentType();
|
||||
return new UberPagedResourcesDeserializer(vc);
|
||||
} else {
|
||||
return new UberPagedResourcesDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesor for deserializer use for deserializing content values.
|
||||
*/
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an {@link UberDocument} into a {@link Resources}.
|
||||
*
|
||||
* @param doc
|
||||
* @param rootType
|
||||
* @param contentType
|
||||
* @return
|
||||
*/
|
||||
private static Resources<?> extractResources(UberDocument doc, JavaType rootType, JavaType contentType) {
|
||||
|
||||
List<Object> content = new ArrayList<>();
|
||||
|
||||
for (UberData uberData : doc.getUber().getData()) {
|
||||
|
||||
if (uberData.getName() != null && uberData.getName().equals("page")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (uberData.getLinks().isEmpty()) {
|
||||
|
||||
List<Link> resourceLinks = new ArrayList<>();
|
||||
Resource<?> resource = null;
|
||||
|
||||
for (UberData item : uberData.getData()) {
|
||||
if (item.getRel() != null) {
|
||||
item.getRel().forEach(rel -> resourceLinks.add(new Link(item.getUrl(), rel)));
|
||||
} else {
|
||||
|
||||
// Primitive type
|
||||
if (item.getData().size() == 1 && item.getData().get(0).getName() == null) {
|
||||
|
||||
Object scalarValue = item.getData().get(0).getValue();
|
||||
resource = new Resource<>(scalarValue, uberData.getLinks());
|
||||
|
||||
} else {
|
||||
|
||||
Map<String, Object> properties = item.getData().stream()
|
||||
.collect(Collectors.toMap(UberData::getName, UberData::getValue));
|
||||
Object obj = PropertyUtils.createObjectFromProperties(rootType.getRawClass(), properties);
|
||||
resource = new Resource<>(obj, uberData.getLinks());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resource != null) {
|
||||
resource.add(resourceLinks);
|
||||
content.add(resource);
|
||||
} else {
|
||||
throw new RuntimeException("No content!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isResourcesOfResource(contentType)) {
|
||||
/*
|
||||
* Either return a Resources<Resource<T>>...
|
||||
*/
|
||||
return new Resources<>(content, doc.getUber().getLinks());
|
||||
} else {
|
||||
/*
|
||||
* ...or return a Resources<T>
|
||||
*/
|
||||
|
||||
List<Object> resourceLessContent = content.stream()
|
||||
.map(item -> (Resource<?>) item)
|
||||
.map(Resource::getContent)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new Resources<>(resourceLessContent, doc.getUber().getLinks());
|
||||
}
|
||||
}
|
||||
|
||||
private static PageMetadata extractPagingMetadata(UberDocument doc) {
|
||||
|
||||
return doc.getUber().getData().stream()
|
||||
.filter(uberData -> uberData.getName() != null && uberData.getName().equals("page"))
|
||||
.findFirst()
|
||||
.map(uberData -> {
|
||||
int size = 0;
|
||||
int number = 0;
|
||||
int totalElements = 0;
|
||||
int totalPages = 0;
|
||||
|
||||
for (UberData data : uberData.getData()) {
|
||||
if (data.getName().equals("size")) {
|
||||
size = (int) data.getValue();
|
||||
}
|
||||
|
||||
if (data.getName().equals("number")) {
|
||||
number = (int) data.getValue();
|
||||
}
|
||||
|
||||
if (data.getName().equals("totalElements")) {
|
||||
totalElements = (int) data.getValue();
|
||||
}
|
||||
|
||||
if (data.getName().equals("totalPages")) {
|
||||
totalPages = (int) data.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return new PageMetadata(size, number, totalElements, totalPages);
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer deserializer to handle {@link UberAction}.
|
||||
*/
|
||||
static class UberActionDeserializer extends StdDeserializer<UberAction> {
|
||||
|
||||
UberActionDeserializer() {
|
||||
super(UberAction.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UberAction deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
return UberAction.valueOf(p.getText().toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
public static class UberHandlerInstantiator extends HandlerInstantiator {
|
||||
|
||||
private final Map<Class<?>, Object> serializers = new HashMap<>();
|
||||
|
||||
public UberHandlerInstantiator() {
|
||||
|
||||
this.serializers.put(UberResourceSupportSerializer.class, new UberResourceSupportSerializer());
|
||||
this.serializers.put(UberResourceSerializer.class, new UberResourceSerializer());
|
||||
this.serializers.put(UberResourcesSerializer.class, new UberResourcesSerializer());
|
||||
this.serializers.put(UberPagedResourcesSerializer.class, new UberPagedResourcesSerializer());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> deserClass) {
|
||||
return (JsonDeserializer<?>) findInstance(deserClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> keyDeserClass) {
|
||||
return (KeyDeserializer) findInstance(keyDeserClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
|
||||
return (JsonSerializer<?>) findInstance(serClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated, Class<?> builderClass) {
|
||||
return (TypeResolverBuilder<?>) findInstance(builderClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
|
||||
return (TypeIdResolver) findInstance(resolverClass);
|
||||
}
|
||||
|
||||
private Object findInstance(Class<?> type) {
|
||||
|
||||
Object result = this.serializers.get(type);
|
||||
return result != null ? result : BeanUtils.instantiateClass(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberPagedResourcesDeserializer;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to handle {@link PagedResources}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@JsonDeserialize(using = UberPagedResourcesDeserializer.class)
|
||||
abstract class PagedResourcesMixin<T> extends PagedResources<T> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberResourceDeserializer;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to handle {@link Resource}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@JsonDeserialize(using = UberResourceDeserializer.class)
|
||||
abstract class ResourceMixin<T> extends Resource<T> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberResourceSupportDeserializer;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to handle {@link ResourceSupport}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@JsonDeserialize(using = UberResourceSupportDeserializer.class)
|
||||
abstract class ResourceSupportMixin extends ResourceSupport {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.hateoas.uber.Jackson2UberModule.UberResourcesDeserializer;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to handle {@link Resources}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@JsonDeserialize(using = UberResourcesDeserializer.class)
|
||||
abstract class ResourcesMixin<T> extends Resources<T> {
|
||||
|
||||
}
|
||||
74
src/main/java/org/springframework/hateoas/uber/Uber.java
Normal file
74
src/main/java/org/springframework/hateoas/uber/Uber.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Enclosing collection in an UBER representation.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
class Uber {
|
||||
|
||||
private String version;
|
||||
private List<UberData> data;
|
||||
private UberError error;
|
||||
|
||||
@JsonCreator
|
||||
Uber(@JsonProperty("version") String version, @JsonProperty("data") List<UberData> data,
|
||||
@JsonProperty("error") UberError error) {
|
||||
|
||||
this.version = version;
|
||||
this.data = data;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
Uber() {
|
||||
this("1.0", null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract rel and url from every {@link UberData} entry.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
List<Link> getLinks() {
|
||||
|
||||
return this.data.stream()
|
||||
.flatMap(uberData -> uberData.getLinks().stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
107
src/main/java/org/springframework/hateoas/uber/UberAction.java
Normal file
107
src/main/java/org/springframework/hateoas/uber/UberAction.java
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2014-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import static org.springframework.hateoas.uber.Jackson2UberModule.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* Embodies possible actions for an UBER representation, mapped onto {@link HttpMethod}s.
|
||||
*
|
||||
* @author Dietrich Schulten
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@JsonSerialize(using = UberActionSerializer.class)
|
||||
@JsonDeserialize(using = UberActionDeserializer.class)
|
||||
enum UberAction {
|
||||
|
||||
/**
|
||||
* POST
|
||||
*/
|
||||
APPEND(HttpMethod.POST),
|
||||
|
||||
/**
|
||||
* PATCH
|
||||
*/
|
||||
PARTIAL(HttpMethod.PATCH),
|
||||
|
||||
/**
|
||||
* GET
|
||||
*/
|
||||
READ(HttpMethod.GET),
|
||||
|
||||
/**
|
||||
* DELETE
|
||||
*/
|
||||
REMOVE(HttpMethod.DELETE),
|
||||
|
||||
/**
|
||||
* PUT
|
||||
*/
|
||||
REPLACE(HttpMethod.PUT);
|
||||
|
||||
private final HttpMethod httpMethod;
|
||||
|
||||
UberAction(HttpMethod method) {
|
||||
this.httpMethod = method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the related Spring Web {@link HttpMethod}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
HttpMethod getMethod() {
|
||||
return this.httpMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name().toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert an {@link HttpMethod} into an {@link UberAction}.
|
||||
* @param method
|
||||
* @return
|
||||
*/
|
||||
static UberAction fromMethod(HttpMethod method) {
|
||||
|
||||
return Arrays.stream(UberAction.values())
|
||||
.filter(action -> action.httpMethod == method)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unsupported method: " + method));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps given request method to uber action. GET will be mapped as {@literal null} since it is the default.
|
||||
*
|
||||
* @param method to map
|
||||
* @return action, or null for GET
|
||||
*/
|
||||
static UberAction forRequestMethod(HttpMethod method) {
|
||||
return HttpMethod.GET == method ? null : fromMethod(method);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
/**
|
||||
* {@link AffordanceModel} for {@literal UBER+JSON}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
class UberAffordanceModel extends AffordanceModel {
|
||||
|
||||
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
|
||||
|
||||
private final @Getter Collection<MediaType> mediaTypes = Collections.singleton(MediaTypes.UBER_JSON);
|
||||
|
||||
private final @Getter List<UberData> inputProperties;
|
||||
private final @Getter List<UberData> queryProperties;
|
||||
|
||||
UberAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
super(name, link, httpMethod, inputType, queryMethodParameters, outputType);
|
||||
|
||||
this.inputProperties = determineAffordanceInputs();
|
||||
this.queryProperties = determineQueryProperties();
|
||||
}
|
||||
|
||||
private List<UberData> determineAffordanceInputs() {
|
||||
|
||||
if (ENTITY_ALTERING_METHODS.contains(getHttpMethod())) {
|
||||
|
||||
return PropertyUtils.findPropertyNames(getInputType()).stream()
|
||||
.map(propertyName -> new UberData()
|
||||
.withName(propertyName)
|
||||
.withValue(""))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform GET-based query parameters (e.g. {@literal &query}) into a list of {@link UberData} objects.
|
||||
*/
|
||||
private List<UberData> determineQueryProperties() {
|
||||
|
||||
if (!getHttpMethod().equals(HttpMethod.GET)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (getHttpMethod().equals(HttpMethod.GET)) {
|
||||
return getQueryMethodParameters().stream()
|
||||
.map(queryParameter -> new UberData()
|
||||
.withName(queryParameter.getName())
|
||||
.withValue(""))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
UberAction getAction() {
|
||||
return UberAction.forRequestMethod(getHttpMethod());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.core.AffordanceModelFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
/**
|
||||
* {@link AffordanceModelFactory} for {@literal UBER+JSON}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
public class UberAffordanceModelFactory implements AffordanceModelFactory {
|
||||
|
||||
private final @Getter MediaType mediaType = MediaTypes.UBER_JSON;
|
||||
|
||||
@Override
|
||||
public AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
return new UberAffordanceModel(name, link, httpMethod, inputType, queryMethodParameters, outputType);
|
||||
}
|
||||
}
|
||||
456
src/main/java/org/springframework/hateoas/uber/UberData.java
Normal file
456
src/main/java/org/springframework/hateoas/uber/UberData.java
Normal file
@@ -0,0 +1,456 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import static com.fasterxml.jackson.annotation.JsonInclude.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Core element containing either a {@link Link} or a single property inside an {@link UberDocument}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
class UberData {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String label;
|
||||
private List<String> rel;
|
||||
private String url;
|
||||
private UberAction action;
|
||||
private boolean transclude;
|
||||
private String model;
|
||||
private List<String> sending;
|
||||
private List<String> accepting;
|
||||
private Object value;
|
||||
private List<UberData> data;
|
||||
|
||||
@JsonCreator
|
||||
UberData(@JsonProperty("id") String id, @JsonProperty("name") String name,
|
||||
@JsonProperty("label") String label, @JsonProperty("rel") List<String> rel,
|
||||
@JsonProperty("url") String url, @JsonProperty("action") UberAction action,
|
||||
@JsonProperty("transclude") boolean transclude, @JsonProperty("model") String model,
|
||||
@JsonProperty("sending") List<String> sending, @JsonProperty("accepting") List<String> accepting,
|
||||
@JsonProperty("value") Object value, @JsonProperty("data") List<UberData> data) {
|
||||
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.label = label;
|
||||
this.rel = rel;
|
||||
this.url = url;
|
||||
this.action = action;
|
||||
this.transclude = transclude;
|
||||
this.model = model;
|
||||
this.sending = sending;
|
||||
this.accepting = accepting;
|
||||
this.value = value;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
UberData() {
|
||||
this(null, null, null, null, null, UberAction.READ, false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't render if it's {@link UberAction#READ}.
|
||||
*/
|
||||
public UberAction getAction() {
|
||||
|
||||
if (this.action == UberAction.READ) {
|
||||
return null;
|
||||
}
|
||||
return this.action;
|
||||
}
|
||||
|
||||
/*
|
||||
* Don't render if {@literal null}.
|
||||
*/
|
||||
public List<String> getRel() {
|
||||
|
||||
if (this.rel == null || this.rel.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return this.rel;
|
||||
}
|
||||
|
||||
/*
|
||||
* Don't render if {@literal null}.
|
||||
*/
|
||||
public List<UberData> getData() {
|
||||
|
||||
if (this.data == null || this.data.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Use a {@link Boolean} to support returning {@literal null}, and if it is {@literal null}, don't render.
|
||||
*/
|
||||
public Boolean isTemplated() {
|
||||
|
||||
return Optional.ofNullable(this.url)
|
||||
.map(s -> s.contains("{?") ? true : null)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public void setTemplated(boolean __) {
|
||||
// Ignore since "templated" is a virtual property
|
||||
}
|
||||
|
||||
/*
|
||||
* Use a {@link Boolean} to support returning {@literal null}, and if it is {@literal null}, don't render.
|
||||
*/
|
||||
public Boolean isTransclude() {
|
||||
return this.transclude ? this.transclude : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the links found in this {@link UberData}.
|
||||
*/
|
||||
@JsonIgnore
|
||||
public List<Link> getLinks() {
|
||||
|
||||
return Optional.ofNullable(this.rel)
|
||||
.map(rels -> rels.stream()
|
||||
.map(rel -> new Link(this.url, rel))
|
||||
.collect(Collectors.toList()))
|
||||
.orElse(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple scalar types that can be encoded by value, not type.
|
||||
*/
|
||||
private final static HashSet<Class<?>> PRIMITIVE_TYPES = new HashSet<>(Arrays.asList(
|
||||
String.class
|
||||
));
|
||||
|
||||
/**
|
||||
* Set of all Spring HATEOAS resource types.
|
||||
*/
|
||||
private static final HashSet<Class<?>> RESOURCE_TYPES = new HashSet<>(Arrays.asList(
|
||||
ResourceSupport.class,
|
||||
Resource.class,
|
||||
Resources.class,
|
||||
PagedResources.class
|
||||
));
|
||||
|
||||
/**
|
||||
* Convert a {@link ResourceSupport} into a list of {@link UberData}s, containing links and content.
|
||||
*
|
||||
* @param resource
|
||||
* @return
|
||||
*/
|
||||
static List<UberData> extractLinksAndContent(ResourceSupport resource) {
|
||||
|
||||
List<UberData> data = extractLinks(resource);
|
||||
|
||||
extractContent(resource).ifPresent(data::add);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a {@link Resource} into a list of {@link UberData}s, containing links and content.
|
||||
*
|
||||
* @param resource
|
||||
* @return
|
||||
*/
|
||||
static List<UberData> extractLinksAndContent(Resource<?> resource) {
|
||||
|
||||
List<UberData> data = extractLinks(resource);
|
||||
|
||||
extractContent(resource.getContent()).ifPresent(data::add);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert {@link Resources} into a list of {@link UberData}, with each item nested in a sub-UberData.
|
||||
*
|
||||
* @param resources
|
||||
* @return
|
||||
*/
|
||||
static List<UberData> extractLinksAndContent(Resources<?> resources) {
|
||||
|
||||
List<UberData> data = extractLinks(resources);
|
||||
|
||||
data.addAll(resources.getContent().stream()
|
||||
.map(UberData::doExtractLinksAndContent)
|
||||
.map(uberData -> new UberData().withData(uberData))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static List<UberData> extractLinksAndContent(PagedResources<?> resources) {
|
||||
|
||||
List<UberData> collectionOfResources = extractLinksAndContent((Resources<?>) resources);
|
||||
|
||||
if (resources.getMetadata() != null ) {
|
||||
|
||||
collectionOfResources.add(new UberData()
|
||||
.withName("page")
|
||||
.withData(Arrays.asList(
|
||||
new UberData()
|
||||
.withName("number")
|
||||
.withValue(resources.getMetadata().getNumber()),
|
||||
new UberData()
|
||||
.withName("size")
|
||||
.withValue(resources.getMetadata().getSize()),
|
||||
new UberData()
|
||||
.withName("totalElements")
|
||||
.withValue(resources.getMetadata().getTotalElements()),
|
||||
new UberData()
|
||||
.withName("totalPages")
|
||||
.withValue(resources.getMetadata().getTotalPages()))));
|
||||
}
|
||||
|
||||
return collectionOfResources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a {@link List} of {@link Link}s into a list of {@link UberData}.
|
||||
*
|
||||
* @param links
|
||||
* @return
|
||||
*/
|
||||
private static List<UberData> extractLinks(List<Link> links) {
|
||||
|
||||
return urlRelMap(links).entrySet().stream()
|
||||
.map(entry -> new UberData()
|
||||
.withUrl(entry.getKey())
|
||||
.withRel(entry.getValue().getRels()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the direct {@link Link}s and {@link Affordance}-based links from a {@link ResourceSupport}.
|
||||
*
|
||||
* @param resource
|
||||
* @return
|
||||
*/
|
||||
private static List<UberData> extractLinks(ResourceSupport resource) {
|
||||
|
||||
List<UberData> data = new ArrayList<>();
|
||||
|
||||
List<UberData> links = extractLinks(resource.getLinks());
|
||||
List<UberData> affordanceBasedLinks = extractAffordances(resource.getLinks());
|
||||
|
||||
if (affordanceBasedLinks.isEmpty()) {
|
||||
data.addAll(links);
|
||||
} else {
|
||||
data.addAll(mergeDeclaredLinksIntoAffordanceLinks(affordanceBasedLinks, links));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an object's properties into an {@link UberData}.
|
||||
*
|
||||
* @param content
|
||||
* @return
|
||||
*/
|
||||
private static Optional<UberData> extractContent(Object content) {
|
||||
|
||||
if (!RESOURCE_TYPES.contains(content.getClass())) {
|
||||
|
||||
return Optional.of(new UberData()
|
||||
.withName(StringUtils.uncapitalize(content.getClass().getSimpleName()))
|
||||
.withData(extractProperties(content)));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract links and content from an object of any type.
|
||||
*/
|
||||
private static List<UberData> doExtractLinksAndContent(Object item) {
|
||||
|
||||
if (item instanceof Resource) {
|
||||
return extractLinksAndContent((Resource<?>) item);
|
||||
}
|
||||
|
||||
if (item instanceof ResourceSupport) {
|
||||
return extractLinksAndContent((ResourceSupport) item);
|
||||
}
|
||||
|
||||
return extractLinksAndContent(new Resource<>(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a {@list List} of {@link Link}s into a {@link Map}, where you can see ALL the rels of a given
|
||||
* link.
|
||||
*
|
||||
* @param links
|
||||
* @return a map with links mapping onto a {@link List} of rels
|
||||
*/
|
||||
private static Map<String, LinkAndRels> urlRelMap(List<Link> links) {
|
||||
|
||||
Map<String, LinkAndRels> urlRelMap = new LinkedHashMap<>();
|
||||
|
||||
links.forEach(link -> {
|
||||
LinkAndRels linkAndRels = urlRelMap.computeIfAbsent(link.getHref(), s -> new LinkAndRels());
|
||||
linkAndRels.setLink(link);
|
||||
linkAndRels.getRels().add(link.getRel());
|
||||
});
|
||||
|
||||
return urlRelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the {@link Affordance}s for a set of {@link Link}s, and convert them into {@link UberData}.
|
||||
*
|
||||
* @param links
|
||||
* @return
|
||||
*/
|
||||
private static List<UberData> extractAffordances(List<Link> links) {
|
||||
|
||||
return links.stream()
|
||||
.flatMap(link -> link.getAffordances().stream())
|
||||
.map(affordance -> (UberAffordanceModel) affordance.getAffordanceModel(MediaTypes.UBER_JSON))
|
||||
.map(model -> {
|
||||
|
||||
if (model.getHttpMethod().equals(HttpMethod.GET)) {
|
||||
|
||||
String suffix = model.getQueryProperties().stream()
|
||||
.map(UberData::getName)
|
||||
.collect(Collectors.joining(","));
|
||||
|
||||
if (!model.getQueryMethodParameters().isEmpty()) {
|
||||
suffix = "{?" + suffix + "}";
|
||||
}
|
||||
|
||||
return new UberData()
|
||||
.withName(model.getName())
|
||||
.withRel(Arrays.asList(model.getName()))
|
||||
.withUrl(model.getLink().expand().getHref() + suffix)
|
||||
.withAction(model.getAction());
|
||||
|
||||
} else {
|
||||
|
||||
return new UberData()
|
||||
.withName(model.getName())
|
||||
.withRel(Arrays.asList(model.getName()))
|
||||
.withUrl(model.getLink().expand().getHref())
|
||||
.withModel(model.getInputProperties().stream()
|
||||
.map(UberData::getName)
|
||||
.map(property -> property + "={" + property + "}")
|
||||
.collect(Collectors.joining("&")))
|
||||
.withAction(model.getAction());
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a list of {@link Affordance}-based {@link Link}s, and overlay them with intersecting, declared {@link Link}s.
|
||||
*
|
||||
* @param affordanceBasedLinks
|
||||
* @param links
|
||||
* @return
|
||||
*/
|
||||
private static List<UberData> mergeDeclaredLinksIntoAffordanceLinks(List<UberData> affordanceBasedLinks, List<UberData> links) {
|
||||
|
||||
return affordanceBasedLinks.stream()
|
||||
.flatMap(affordance -> links.stream()
|
||||
.filter(link -> link.getUrl().equals(affordance.getUrl()))
|
||||
.map(link -> {
|
||||
|
||||
if (link.getAction() == affordance.getAction()) {
|
||||
|
||||
List<String> rels = new ArrayList<>(link.getRel());
|
||||
|
||||
rels.addAll(affordance.getRel());
|
||||
|
||||
return affordance
|
||||
.withName(rels.get(0))
|
||||
.withRel(rels);
|
||||
} else {
|
||||
return affordance;
|
||||
}
|
||||
}))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the payload of a {@link Resource} into {@link UberData}.
|
||||
*
|
||||
* @param obj
|
||||
* @return
|
||||
*/
|
||||
private static List<UberData> extractProperties(Object obj) {
|
||||
|
||||
if (PRIMITIVE_TYPES.contains(obj.getClass())) {
|
||||
return Arrays.asList(new UberData()
|
||||
.withValue(obj));
|
||||
}
|
||||
|
||||
return PropertyUtils.findProperties(obj).entrySet().stream()
|
||||
.map(entry -> new UberData()
|
||||
.withName(entry.getKey())
|
||||
.withValue(entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Holds both a {@link Link} and related {@literal rels}.
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
private static class LinkAndRels {
|
||||
|
||||
private Link link;
|
||||
private List<String> rels = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Top-level element in an UBER representation.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
class UberDocument {
|
||||
|
||||
private Uber uber;
|
||||
|
||||
@JsonCreator
|
||||
UberDocument(@JsonProperty("version") String version, @JsonProperty("data") List<UberData> data,
|
||||
@JsonProperty("error") UberError error) {
|
||||
this.uber = new Uber(version, data, error);
|
||||
}
|
||||
|
||||
UberDocument() {
|
||||
this("1.0", null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an object into a {@link UberDocument}.
|
||||
*
|
||||
* @param object
|
||||
* @return
|
||||
*/
|
||||
static UberDocument toUberDocument(final Object object) {
|
||||
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (object instanceof UberDocument) {
|
||||
return (UberDocument) object;
|
||||
}
|
||||
|
||||
if (object instanceof Iterable) {
|
||||
|
||||
} else if (object instanceof Map) {
|
||||
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Don't know how to handle type : " + object.getClass());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* UBER representation of an error.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
class UberError {
|
||||
|
||||
private List<UberData> data;
|
||||
|
||||
@JsonCreator
|
||||
UberError(@JsonProperty("data") List<UberData> data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
UberError() {
|
||||
this(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.hateoas.uber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.LinkDiscoverer;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Find links by rel in an UBER representation.
|
||||
*
|
||||
* TODO: Pending https://github.com/json-path/JsonPath/issues/429, replace deserializing solution with JsonPath-based expression "$.uber.data[?(@.rel.indexOf('%s') != -1)].url"
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 1.0
|
||||
*/
|
||||
public class UberLinkDiscoverer implements LinkDiscoverer {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
UberLinkDiscoverer() {
|
||||
|
||||
this.mapper = new ObjectMapper();
|
||||
this.mapper.registerModules(new Jackson2UberModule());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link findLinkWithRel(String rel, String representation) {
|
||||
|
||||
return getLinks(representation).stream()
|
||||
.filter(link -> link.getRel().equals(rel))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link findLinkWithRel(String rel, InputStream representation) {
|
||||
|
||||
return getLinks(representation).stream()
|
||||
.filter(link -> link.getRel().equals(rel))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Link> findLinksWithRel(String rel, String representation) {
|
||||
|
||||
return getLinks(representation).stream()
|
||||
.filter(link -> link.getRel().equals(rel))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Link> findLinksWithRel(String rel, InputStream representation) {
|
||||
|
||||
return getLinks(representation).stream()
|
||||
.filter(link -> link.getRel().equals(rel))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MediaType delimiter) {
|
||||
return delimiter.isCompatibleWith(MediaTypes.UBER_JSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize the entire document to find links.
|
||||
*
|
||||
* @param json
|
||||
* @return
|
||||
*/
|
||||
private List<Link> getLinks(String json) {
|
||||
|
||||
try {
|
||||
return this.mapper.readValue(json, UberDocument.class).getUber().getLinks();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize the entire document to find links.
|
||||
*
|
||||
* @param stream
|
||||
* @return
|
||||
*/
|
||||
private List<Link> getLinks(InputStream stream) {
|
||||
|
||||
try {
|
||||
return this.mapper.readValue(stream, UberDocument.class).getUber().getLinks();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user