#199 - Refactorings in HalEmbeddedBuilder.

We now provide a dedicated EmbeddedWrappers API that HalEmbeddedBuilder uses to transparently handle the collection enforcement etc. It also allow manually hand in EmbeddedWrapper instances to enforce a certain behavior independent of the global settings in HalEmbeddedBuilder.

Simplified HalResourcesSerializer in Jackson2Hal module as we now don't have to deploy custom serializers for the embedded elements anymore.

Supersedes some of the API introduced for #195.
This commit is contained in:
Oliver Gierke
2014-06-11 14:38:21 +02:00
parent 40ceba3c47
commit 0b4b3c1ad9
7 changed files with 368 additions and 170 deletions

View File

@@ -1,31 +0,0 @@
/*
* Copyright 2014 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;
/**
* Interface to mark objects that are aware of the rel they'd like to be exposed under.
*
* @author Oliver Gierke
*/
public interface RelAware {
/**
* Returns the rel to be used with the given object.
*
* @return
*/
String getRel();
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2014 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.core;
import org.springframework.hateoas.Resource;
/**
* A wrapper to handle values to be embedded into a {@link Resource}.
*
* @author Oliver Gierke
*/
public interface EmbeddedWrapper {
/**
* Returns the rel to be used when embedding. If this returns {@literal null}, the rel will be calculated based on the
* type of the embedded value.
*
* @return
*/
String getRel();
/**
* Returns whether the wrapper hast the given rel.
*
* @param rel can be {@literal null}.
* @return
*/
boolean hasRel(String rel);
/**
* Returns whether the wrapper is a collection value.
*
* @return
*/
boolean isCollectionValue();
/**
* Returns the actual value to embed.
*
* @return
*/
Object getValue();
/**
* Returns the type to be used to calculate a type based rel.
*
* @return
*/
Class<?> getRelTargetType();
}

View File

@@ -0,0 +1,239 @@
/*
* Copyright 2014 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.core;
import java.util.Collection;
import java.util.Collections;
import org.springframework.aop.support.AopUtils;
import org.springframework.hateoas.Resource;
import org.springframework.util.Assert;
/**
* Interface to mark objects that are aware of the rel they'd like to be exposed under.
*
* @author Oliver Gierke
*/
public class EmbeddedWrappers {
private final boolean preferCollections;
/**
* Creates a new {@link EmbeddedWrappers}.
*
* @param preferCollections whether wrappers for single elements should rather treat the value as collection.
*/
public EmbeddedWrappers(boolean preferCollections) {
this.preferCollections = preferCollections;
}
/**
* Creates a new {@link EmbeddedWrapper} that
*
* @param source
* @return
*/
public EmbeddedWrapper wrap(Object source) {
return wrap(source, AbstractEmbeddedWrapper.NO_REL);
}
/**
* Creates a new {@link EmbeddedWrapper} with the given rel.
*
* @param source can be {@literal null}, will return {@literal null} if so.
* @param rel must not be {@literal null} or empty.
* @return
*/
@SuppressWarnings("unchecked")
public EmbeddedWrapper wrap(Object source, String rel) {
if (source == null) {
return null;
}
if (source instanceof EmbeddedWrapper) {
return (EmbeddedWrapper) source;
}
if (source instanceof Collection) {
return new EmbeddedCollection((Collection<Object>) source, rel);
}
if (preferCollections) {
return new EmbeddedCollection(Collections.singleton(source), rel);
}
return new EmbeddedElement(source, rel);
}
private static abstract class AbstractEmbeddedWrapper implements EmbeddedWrapper {
private static final String NO_REL = "___norel___";
private final String rel;
/**
* Creates a new {@link AbstractEmbeddedWrapper} with the given rel.
*
* @param rel must not be {@literal null} or empty.
*/
public AbstractEmbeddedWrapper(String rel) {
Assert.hasText(rel, "Rel must not be null or empty!");
this.rel = rel;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.EmbeddedWrapper#getRel()
*/
@Override
public String getRel() {
return NO_REL.equals(rel) ? null : rel;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.EmbeddedWrapper#hasRel(java.lang.String)
*/
@Override
public boolean hasRel(String rel) {
return this.rel.equals(rel);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.EmbeddedWrapper#getRelTargetType()
*/
@Override
@SuppressWarnings("unchecked")
public Class<?> getRelTargetType() {
Object peek = peek();
peek = peek instanceof Resource ? ((Resource<Object>) peek).getContent() : peek;
return AopUtils.getTargetClass(peek);
}
/**
* Peek into the wrapped element. The object returned is used to determine the actual value type of the wrapper.
*
* @return
*/
protected abstract Object peek();
}
/**
* {@link EmbeddedWrapper} for a single element.
*
* @author Oliver Gierke
*/
private static class EmbeddedElement extends AbstractEmbeddedWrapper {
private final Object value;
/**
* Creates a new {@link EmbeddedElement} for the given value and rel.
*
* @param value must not be {@literal null}.
* @param rel must not be {@literal null} or empty.
*/
public EmbeddedElement(Object value, String rel) {
super(rel);
Assert.notNull(value, "Value must not be null!");
this.value = value;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.EmbeddedWrapper#getValue()
*/
@Override
public Object getValue() {
return value;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.EmbeddedWrappers.AbstractElementWrapper#peek()
*/
@Override
protected Object peek() {
return getValue();
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.EmbeddedWrapper#isCollectionValue()
*/
@Override
public boolean isCollectionValue() {
return false;
}
}
/**
* {@link EmbeddedWrapper} for a collection of elements.
*
* @author Oliver Gierke
*/
private static class EmbeddedCollection extends AbstractEmbeddedWrapper {
private final Collection<Object> value;
/**
* @param value must not be {@literal null} or empty.
* @param rel must not be {@literal null} or empty.
*/
public EmbeddedCollection(Collection<Object> value, String rel) {
super(rel);
Assert.notNull(value, "Collection must not be null!");
Assert.notEmpty(value, "Collection must not be empty");
this.value = value;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.EmbeddedWrapper#getValue()
*/
@Override
public Collection<Object> getValue() {
return value;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.EmbeddedWrappers.AbstractEmbeddedWrapper#peek()
*/
@Override
protected Object peek() {
return value.iterator().next();
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.EmbeddedWrapper#isCollectionValue()
*/
@Override
public boolean isCollectionValue() {
return true;
}
}
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright 2013 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.core;
import org.springframework.hateoas.Resource;
/**
* Simple helper class.
*
* @author Oliver Gierke
*/
public class ObjectUtils {
/**
* Returns the resource type of the given object. In case a {@link Resource} is given, it will return the
* {@link Resource} content's type.
*
* @param object can be {@literal null}.
* @return
*/
public static Object getResourceType(Object object) {
if (object instanceof Resource) {
return ((Resource<?>) object).getContent();
}
return object;
}
}

View File

@@ -16,17 +16,18 @@
package org.springframework.hateoas.hal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.aop.support.AopUtils;
import org.springframework.hateoas.RelAware;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.core.ObjectUtils;
import org.springframework.hateoas.core.EmbeddedWrapper;
import org.springframework.hateoas.core.EmbeddedWrappers;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Builder class that allows collecting objects under the relation types defined for the objects but moving from the
@@ -39,11 +40,9 @@ class HalEmbeddedBuilder {
private static final String DEFAULT_REL = "content";
private final Map<String, List<Object>> embeddeds = new HashMap<String, List<Object>>();
private final Map<String, Object> embeddeds = new HashMap<String, Object>();
private final RelProvider provider;
private final boolean preferCollectionRels;
private boolean relAwareFound;
private final EmbeddedWrappers wrappers;
/**
* Creates a new {@link HalEmbeddedBuilder} using the given {@link RelProvider} and prefer collection rels flag.
@@ -56,76 +55,76 @@ class HalEmbeddedBuilder {
Assert.notNull(provider, "Relprovider must not be null!");
this.provider = provider;
this.preferCollectionRels = preferCollectionRels;
this.wrappers = new EmbeddedWrappers(preferCollectionRels);
}
/**
* Adds the given value to the embeddeds. Will skip doing so if the value is {@literal null} or the content of a
* {@link Resource} is {@literal null}.
*
* @param value
* @param value can be {@literal null}.
*/
public void add(Object value) {
public void add(Object source) {
if (ObjectUtils.getResourceType(value) == null) {
EmbeddedWrapper wrapper = wrappers.wrap(source);
if (wrapper == null) {
return;
}
String rel = getDefaultedRelFor(value, true);
String collectionRel = getDefaultedRelFor(wrapper, true);
String collectionOrItemRel = collectionRel;
if (!embeddeds.containsKey(rel)) {
rel = getDefaultedRelFor(value, preferCollectionRels);
if (!embeddeds.containsKey(collectionRel)) {
collectionOrItemRel = getDefaultedRelFor(wrapper, wrapper.isCollectionValue());
}
List<Object> currentValue = embeddeds.get(rel);
Object currentValue = embeddeds.get(collectionOrItemRel);
Object value = wrapper.getValue();
if (currentValue == null) {
ArrayList<Object> arrayList = new ArrayList<Object>();
arrayList.add(value);
embeddeds.put(rel, arrayList);
} else if (currentValue.size() == 1) {
currentValue.add(value);
embeddeds.remove(rel);
embeddeds.put(getDefaultedRelFor(value, true), currentValue);
} else {
currentValue.add(value);
if (currentValue == null && !wrapper.isCollectionValue()) {
embeddeds.put(collectionOrItemRel, value);
return;
}
List<Object> list = new ArrayList<Object>();
list.addAll(asCollection(currentValue));
list.addAll(asCollection(wrapper.getValue()));
embeddeds.remove(collectionOrItemRel);
embeddeds.put(collectionRel, list);
}
private String getDefaultedRelFor(Object value, boolean forCollection) {
@SuppressWarnings("unchecked")
private Collection<Object> asCollection(Object source) {
return source instanceof Collection ? (Collection<Object>) source : source == null ? Collections.emptySet()
: Collections.singleton(source);
}
Object unwrapped = ObjectUtils.getResourceType(value);
private String getDefaultedRelFor(EmbeddedWrapper wrapper, boolean forCollection) {
if (value instanceof RelAware) {
this.relAwareFound = true;
return ((RelAware) value).getRel();
String valueRel = wrapper.getRel();
if (StringUtils.hasText(valueRel)) {
return valueRel;
}
if (provider == null) {
return DEFAULT_REL;
}
Class<?> type = AopUtils.getTargetClass(unwrapped);
Class<?> type = wrapper.getRelTargetType();
String rel = forCollection ? provider.getCollectionResourceRelFor(type) : provider.getItemResourceRelFor(type);
return rel == null ? DEFAULT_REL : rel;
}
/**
* Returns whether the builder only created collection rels.
*
* @return
*/
public boolean hasOnlyCollections() {
return preferCollectionRels && !relAwareFound;
}
/**
* Returns the added objects keyed up by their relation types.
*
* @return
*/
public Map<String, List<Object>> asMap() {
public Map<String, Object> asMap() {
return Collections.unmodifiableMap(embeddeds);
}
}

View File

@@ -269,18 +269,7 @@ public class Jackson2HalModule extends SimpleModule {
builder.add(resource);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Resource.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
JsonSerializer<Object> valueSerializer = builder.hasOnlyCollections() ? provider.findValueSerializer(valueType,
property) : new OptionalListJackson2Serializer(property);
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
provider.findKeySerializer(keyType, null), valueSerializer, null);
serializer.serialize(builder.asMap(), jgen, provider);
provider.findValueSerializer(Map.class, property).serialize(builder.asMap(), jgen, provider);
}
@Override