From a90e2f32c310f7d40adca4d0a5e6cd4d2b758bc0 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 10 Feb 2015 14:49:43 +0100 Subject: [PATCH] #286 - EmbeddedWrappers now allows empty collections if a rel or type is provided. EmbeddedWrappers.wrap(Object, String) now allows empty collections. Also a new emptyCollectionOfType(Class) was introduced to allow the RelProvider kick in and dynamically resolve the lazily. Added more details to the JavaDoc of EmbeddedWrapper to make clear that we either expect a static rel returned or a type to resolve the rel from. Make HalEmbeddedBuilder detect invalid implementations and reject them although that should never occur if users use EmbeddedWrappers factory. --- .../hateoas/core/EmbeddedWrapper.java | 12 ++- .../hateoas/core/EmbeddedWrappers.java | 91 ++++++++++++++++++- .../hateoas/hal/HalEmbeddedBuilder.java | 7 +- .../core/EmbeddedWrappersUnitTests.java | 75 +++++++++++++++ .../hal/HalEmbeddedBuilderUnitTest.java | 12 ++- 5 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 src/test/java/org/springframework/hateoas/core/EmbeddedWrappersUnitTests.java diff --git a/src/main/java/org/springframework/hateoas/core/EmbeddedWrapper.java b/src/main/java/org/springframework/hateoas/core/EmbeddedWrapper.java index 9f77cb95..a699b3f6 100644 --- a/src/main/java/org/springframework/hateoas/core/EmbeddedWrapper.java +++ b/src/main/java/org/springframework/hateoas/core/EmbeddedWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -26,14 +26,16 @@ 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. + * type returned by {@link #getRelTargetType()}. A wrapper returning {@literal null} for both {@link #getRel()} and + * {@link #getRelTargetType()} is considered invalid. * * @return + * @see #getRelTargetType() */ String getRel(); /** - * Returns whether the wrapper hast the given rel. + * Returns whether the wrapper has the given rel. * * @param rel can be {@literal null}. * @return @@ -55,7 +57,9 @@ public interface EmbeddedWrapper { Object getValue(); /** - * Returns the type to be used to calculate a type based rel. + * Returns the type to be used to calculate a type based rel. Can return {@literal null} in case an explicit rel is + * returned in {@link #getRel()}. A wrapper returning {@literal null} for both {@link #getRel()} and + * {@link #getRelTargetType()} is considered invalid. * * @return */ diff --git a/src/main/java/org/springframework/hateoas/core/EmbeddedWrappers.java b/src/main/java/org/springframework/hateoas/core/EmbeddedWrappers.java index 1ed80305..90e515bc 100644 --- a/src/main/java/org/springframework/hateoas/core/EmbeddedWrappers.java +++ b/src/main/java/org/springframework/hateoas/core/EmbeddedWrappers.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -50,6 +50,16 @@ public class EmbeddedWrappers { return wrap(source, AbstractEmbeddedWrapper.NO_REL); } + /** + * Creates an {@link EmbeddedWrapper} for an empty {@link Collection} with the given element type. + * + * @param type must not be {@literal null}. + * @return + */ + public EmbeddedWrapper emptyCollectionOf(Class type) { + return new EmptyCollectionEmbeddedWrapper(type); + } + /** * Creates a new {@link EmbeddedWrapper} with the given rel. * @@ -123,6 +133,11 @@ public class EmbeddedWrappers { public Class getRelTargetType() { Object peek = peek(); + + if (peek == null) { + return null; + } + peek = peek instanceof Resource ? ((Resource) peek).getContent() : peek; return AopUtils.getTargetClass(peek); @@ -204,7 +219,10 @@ public class EmbeddedWrappers { super(rel); Assert.notNull(value, "Collection must not be null!"); - Assert.notEmpty(value, "Collection must not be empty"); + + if (AbstractEmbeddedWrapper.NO_REL.equals(rel) && value.isEmpty()) { + throw new IllegalArgumentException("Cannot wrap an empty collection with no rel given!"); + } this.value = value; } @@ -224,7 +242,7 @@ public class EmbeddedWrappers { */ @Override protected Object peek() { - return value.iterator().next(); + return value.isEmpty() ? null : value.iterator().next(); } /* @@ -236,4 +254,71 @@ public class EmbeddedWrappers { return true; } } + + /** + * An {@link EmbeddedWrapper} to simulate a {@link Collection} of a given element type. + * + * @author Oliver Gierke + * @since 0.17 + */ + private static class EmptyCollectionEmbeddedWrapper implements EmbeddedWrapper { + + private final Class type; + + /** + * Creates a new {@link EmptyCollectionEmbeddedWrapper}. + * + * @param type must not be {@literal null}. + */ + public EmptyCollectionEmbeddedWrapper(Class type) { + + Assert.notNull(type, "Element type must not be null!"); + this.type = type; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.EmbeddedWrapper#getRel() + */ + @Override + public String getRel() { + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.EmbeddedWrapper#getValue() + */ + @Override + public Object getValue() { + return Collections.emptySet(); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.EmbeddedWrapper#getRelTargetType() + */ + @Override + public Class getRelTargetType() { + return type; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.EmbeddedWrapper#isCollectionValue() + */ + @Override + public boolean isCollectionValue() { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.EmbeddedWrapper#hasRel(java.lang.String) + */ + @Override + public boolean hasRel(String rel) { + return false; + } + } } diff --git a/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java b/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java index 8cd0dc73..5ae1dcbc 100644 --- a/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java +++ b/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2015 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. @@ -39,6 +39,7 @@ import org.springframework.util.StringUtils; class HalEmbeddedBuilder { private static final String DEFAULT_REL = "content"; + private static final String INVALID_EMBEDDED_WRAPPER = "Embedded wrapper %s returned null for both the static rel and the rel target type! Make sure one of the two returns a non-null value!"; private final Map embeddeds = new HashMap(); private final RelProvider provider; @@ -117,6 +118,10 @@ class HalEmbeddedBuilder { Class type = wrapper.getRelTargetType(); + if (type == null) { + throw new IllegalStateException(String.format(INVALID_EMBEDDED_WRAPPER, wrapper)); + } + String rel = forCollection ? provider.getCollectionResourceRelFor(type) : provider.getItemResourceRelFor(type); if (curieProvider != null) { diff --git a/src/test/java/org/springframework/hateoas/core/EmbeddedWrappersUnitTests.java b/src/test/java/org/springframework/hateoas/core/EmbeddedWrappersUnitTests.java new file mode 100644 index 00000000..968e1171 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/core/EmbeddedWrappersUnitTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 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 static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Collection; +import java.util.Collections; + +import org.junit.Test; + +/** + * Unit tests for {@link EmbeddedWrappers}. + * + * @author Oliver Gierke + */ +public class EmbeddedWrappersUnitTests { + + EmbeddedWrappers wrappers = new EmbeddedWrappers(false); + + /** + * @see #286 + */ + @Test + @SuppressWarnings("rawtypes") + public void createsWrapperForEmptyCollection() { + + EmbeddedWrapper wrapper = wrappers.emptyCollectionOf(String.class); + + assertEmptyCollectionValue(wrapper); + assertThat(wrapper.getRel(), is(nullValue())); + assertThat(wrapper.getRelTargetType(), is(equalTo((Class) String.class))); + } + + /** + * @see #286 + */ + @Test + public void createsWrapperForEmptyCollectionAndExplicitRel() { + + EmbeddedWrapper wrapper = wrappers.wrap(Collections.emptySet(), "rel"); + + assertEmptyCollectionValue(wrapper); + assertThat(wrapper.getRel(), is("rel")); + assertThat(wrapper.getRelTargetType(), is(nullValue())); + } + + /** + * @see #286 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsEmptyCollectionWithoutExplicitRel() { + wrappers.wrap(Collections.emptySet()); + } + + private static void assertEmptyCollectionValue(EmbeddedWrapper wrapper) { + + assertThat(wrapper.getValue(), is(instanceOf(Collection.class))); + assertThat((Collection) wrapper.getValue(), is(empty())); + } +} diff --git a/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java index 2693af5e..c1bc70ec 100644 --- a/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2015 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. @@ -17,6 +17,7 @@ package org.springframework.hateoas.hal; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.List; import java.util.Map; @@ -26,6 +27,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.UriTemplate; +import org.springframework.hateoas.core.EmbeddedWrapper; import org.springframework.hateoas.core.EmbeddedWrappers; import org.springframework.hateoas.core.EvoInflectorRelProvider; @@ -164,6 +166,14 @@ public class HalEmbeddedBuilderUnitTest { assertHasValues(builder.asMap(), "curie:strings", "Sample"); } + /** + * @see #286 + */ + @Test(expected = IllegalStateException.class) + public void rejectsInvalidEmbeddedWrapper() { + new HalEmbeddedBuilder(provider, curieProvider, false).add(mock(EmbeddedWrapper.class)); + } + @SuppressWarnings("unchecked") private static void assertHasValues(Map source, String rel, Object... values) {