diff --git a/src/main/java/org/springframework/hateoas/RelAware.java b/src/main/java/org/springframework/hateoas/RelAware.java deleted file mode 100644 index 90b9ea3d..00000000 --- a/src/main/java/org/springframework/hateoas/RelAware.java +++ /dev/null @@ -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(); -} diff --git a/src/main/java/org/springframework/hateoas/core/EmbeddedWrapper.java b/src/main/java/org/springframework/hateoas/core/EmbeddedWrapper.java new file mode 100644 index 00000000..9f77cb95 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/EmbeddedWrapper.java @@ -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(); +} diff --git a/src/main/java/org/springframework/hateoas/core/EmbeddedWrappers.java b/src/main/java/org/springframework/hateoas/core/EmbeddedWrappers.java new file mode 100644 index 00000000..1ed80305 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/EmbeddedWrappers.java @@ -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) 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) 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 value; + + /** + * @param value must not be {@literal null} or empty. + * @param rel must not be {@literal null} or empty. + */ + public EmbeddedCollection(Collection 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 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; + } + } +} diff --git a/src/main/java/org/springframework/hateoas/core/ObjectUtils.java b/src/main/java/org/springframework/hateoas/core/ObjectUtils.java deleted file mode 100644 index 2963e750..00000000 --- a/src/main/java/org/springframework/hateoas/core/ObjectUtils.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java b/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java index 83b07c4b..f269ac9e 100644 --- a/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java +++ b/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java @@ -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> embeddeds = new HashMap>(); + private final Map embeddeds = new HashMap(); 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 currentValue = embeddeds.get(rel); + Object currentValue = embeddeds.get(collectionOrItemRel); + Object value = wrapper.getValue(); - if (currentValue == null) { - ArrayList arrayList = new ArrayList(); - 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 list = new ArrayList(); + 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 asCollection(Object source) { + return source instanceof Collection ? (Collection) 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> asMap() { + public Map asMap() { return Collections.unmodifiableMap(embeddeds); } } diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index 210e510f..468ea8f5 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -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 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 diff --git a/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java index 18c54d06..2d7d3187 100644 --- a/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java @@ -24,10 +24,8 @@ import java.util.Map; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; -import org.springframework.beans.BeanUtils; -import org.springframework.hateoas.RelAware; import org.springframework.hateoas.RelProvider; -import org.springframework.hateoas.Resource; +import org.springframework.hateoas.core.EmbeddedWrappers; import org.springframework.hateoas.core.EvoInflectorRelProvider; /** @@ -48,20 +46,20 @@ public class HalEmbeddedBuilderUnitTest { @Test public void rendersSingleElementsWithSingleEntityRel() { - Map> map = setUpBuilder("foo", 1L); + Map map = setUpBuilder("foo", 1L); - assertThat(map.get("string"), Matchers.> allOf(hasSize(1), hasItem("foo"))); - assertThat(map.get("long"), Matchers.> allOf(hasSize(1), hasItem(1L))); + assertThat(map.get("string"), is((Object) "foo")); + assertThat(map.get("long"), is((Object) 1L)); } @Test public void rendersMultipleElementsWithCollectionResourceRel() { - Map> map = setUpBuilder("foo", "bar", 1L); + Map map = setUpBuilder("foo", "bar", 1L); assertThat(map.containsKey("string"), is(false)); - assertThat(map.get("strings"), Matchers.> allOf(hasSize(2), Matchers. hasItems("foo", "bar"))); - assertThat(map.get("long"), Matchers.> allOf(hasSize(1), hasItem(1L))); + assertThat(map.get("long"), is((Object) 1L)); + assertHasValues(map, "strings", "foo", "bar"); } /** @@ -70,25 +68,11 @@ public class HalEmbeddedBuilderUnitTest { @Test public void correctlyPilesUpResourcesInCollectionRel() { - Map> map = setUpBuilder("foo", "bar", "foobar", 1L); + Map map = setUpBuilder("foo", "bar", "foobar", 1L); assertThat(map.containsKey("string"), is(false)); - assertThat(map.get("strings"), - Matchers.> allOf(hasSize(3), Matchers. hasItems("foo", "bar", "foobar"))); - assertThat(map.get("long"), Matchers.> allOf(hasSize(1), hasItem(1L))); - } - - /** - * @see #81, #83 - */ - @Test - public void addsNoEmbeddedsForResourceWithoutContent() { - - Resource resource = BeanUtils.instantiateClass(Resource.class); - HalEmbeddedBuilder halEmbeddedBuilder = new HalEmbeddedBuilder(provider, true); - halEmbeddedBuilder.add(resource); - - assertThat(halEmbeddedBuilder.asMap().isEmpty(), is(true)); + assertHasValues(map, "strings", "foo", "bar", "foobar"); + assertThat(map.get("long"), is((Object) 1L)); } /** @@ -101,7 +85,7 @@ public class HalEmbeddedBuilderUnitTest { builder.add("Sample"); assertThat(builder.asMap().get("string"), is(nullValue())); - assertThat(builder.asMap().get("strings"), hasItem("Sample")); + assertHasValues(builder.asMap(), "strings", "Sample"); } /** @@ -110,11 +94,12 @@ public class HalEmbeddedBuilderUnitTest { @Test public void doesNotPreferCollectionsIfRelAwareWasAdded() { - HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, true); - builder.add(new Sample()); + EmbeddedWrappers wrappers = new EmbeddedWrappers(false); - assertThat(builder.hasOnlyCollections(), is(false)); - assertThat(builder.asMap().get("foo"), is(notNullValue())); + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, true); + builder.add(wrappers.wrap("MyValue", "foo")); + + assertThat(builder.asMap().get("foo"), is(instanceOf(String.class))); } /** @@ -125,7 +110,15 @@ public class HalEmbeddedBuilderUnitTest { new HalEmbeddedBuilder(null, false); } - private Map> setUpBuilder(Object... values) { + private static void assertHasValues(Map source, String rel, Object... values) { + + Object value = source.get(rel); + + assertThat(value, is(instanceOf(List.class))); + assertThat((List) value, Matchers.> allOf(hasSize(values.length), hasItems(values))); + } + + private Map setUpBuilder(Object... values) { HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, false); @@ -135,16 +128,4 @@ public class HalEmbeddedBuilderUnitTest { return builder.asMap(); } - - static class Sample implements RelAware { - - /* - * (non-Javadoc) - * @see org.springframework.hateoas.RelAware#getRel() - */ - @Override - public String getRel() { - return "foo"; - } - } }