From 719a4b3af323499abff49fddbfbf042beb11d198 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 21 Jan 2014 13:58:01 +0100 Subject: [PATCH] #142, #137 - CurieProvider can now return multiple curies. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed `getCurieInformation(…)` to receive a `Links` instance with the previously added links and is forced to return a collection of curies so that multiple ones can be resolved transparently. Made Curie value class protected to be able to reuse it from extensions of DefaultCurieProvider. Switched to the custom HATEOAS UriTemplate instance instead of the standard Spring MVC one. Polished JavaDoc of DefaultCurieProvider. Removed some of the deprecation warnings that were introduced by the upgrade to Jackson 2.3. Polished (read: reactivated) some test cases and slightly changed the internals of UriTemplate works. Added TemplateVariables.asList(). --- .../hateoas/TemplateVariables.java | 9 ++ .../springframework/hateoas/UriTemplate.java | 28 ++++-- .../hateoas/hal/CurieProvider.java | 9 +- .../hateoas/hal/DefaultCurieProvider.java | 16 +++- .../hateoas/hal/Jackson2HalModule.java | 17 ++-- .../hateoas/TemplateVariablesUnitTest.java | 91 +++++++++++++++++++ .../core/MethodParametersUnitTest.java | 65 +++++++++++++ .../hal/DefaultCurieProviderUnitTest.java | 75 +++++++++++++++ .../hal/DefaultCurieProviderUnitTests.java | 2 +- .../hal/Jackson2HalIntegrationTest.java | 31 ++++++- 10 files changed, 318 insertions(+), 25 deletions(-) create mode 100644 src/test/java/org/springframework/hateoas/TemplateVariablesUnitTest.java create mode 100644 src/test/java/org/springframework/hateoas/core/MethodParametersUnitTest.java create mode 100644 src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java diff --git a/src/main/java/org/springframework/hateoas/TemplateVariables.java b/src/main/java/org/springframework/hateoas/TemplateVariables.java index a481882d..eae3e448 100644 --- a/src/main/java/org/springframework/hateoas/TemplateVariables.java +++ b/src/main/java/org/springframework/hateoas/TemplateVariables.java @@ -90,6 +90,15 @@ public class TemplateVariables implements Iterable { return concat(variables.variables); } + /** + * Returns the contained {@link TemplateVariable}s as {@link List}. + * + * @return + */ + public List asList() { + return this.variables; + } + /* * (non-Javadoc) * @see java.lang.Iterable#iterator() diff --git a/src/main/java/org/springframework/hateoas/UriTemplate.java b/src/main/java/org/springframework/hateoas/UriTemplate.java index cced35b0..6e2c68bb 100644 --- a/src/main/java/org/springframework/hateoas/UriTemplate.java +++ b/src/main/java/org/springframework/hateoas/UriTemplate.java @@ -40,7 +40,7 @@ public class UriTemplate implements Iterable { private static final Pattern VARIABLE_REGEX = Pattern.compile("\\{([\\?\\&#/]?)([\\w\\,]+)\\}"); - private final List variables = new ArrayList(); + private final TemplateVariables variables;; private String baseUri; /** @@ -53,24 +53,27 @@ public class UriTemplate implements Iterable { Assert.hasText(template, "Template must not be null or empty!"); Matcher matcher = VARIABLE_REGEX.matcher(template); + int baseUriEndIndex = template.length(); + List variables = new ArrayList(); while (matcher.find()) { - if (baseUri == null) { - this.baseUri = template.substring(0, matcher.start(0)); + int start = matcher.start(0); + + if (start < baseUriEndIndex) { + baseUriEndIndex = start; } VariableType type = VariableType.from(matcher.group(1)); String[] names = matcher.group(2).split(","); for (String name : names) { - this.variables.add(new TemplateVariable(name, type)); + variables.add(new TemplateVariable(name, type)); } } - if (this.baseUri == null) { - this.baseUri = template; - } + this.variables = new TemplateVariables(variables); + this.baseUri = template.substring(0, baseUriEndIndex); } /** @@ -94,7 +97,7 @@ public class UriTemplate implements Iterable { * @return */ public List getVariables() { - return this.variables; + return this.variables.asList(); } /** @@ -162,6 +165,15 @@ public class UriTemplate implements Iterable { return this.variables.iterator(); } + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return baseUri + variables.toString(); + } + /** * Appends the value for the given {@link TemplateVariable} to the given {@link UriComponentsBuilder}. * diff --git a/src/main/java/org/springframework/hateoas/hal/CurieProvider.java b/src/main/java/org/springframework/hateoas/hal/CurieProvider.java index a98f9ad3..36905b9d 100644 --- a/src/main/java/org/springframework/hateoas/hal/CurieProvider.java +++ b/src/main/java/org/springframework/hateoas/hal/CurieProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-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. @@ -15,13 +15,17 @@ */ package org.springframework.hateoas.hal; +import java.util.Collection; + import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; /** * API to provide HAL curie information for links. * * @see http://tools.ietf.org/html/draft-kelly-json-hal#section-8.2 * @author Oliver Gierke + * @since 0.9 */ public interface CurieProvider { @@ -38,7 +42,8 @@ public interface CurieProvider { * Returns an object to render as the base curie information. Implementations have to make sure, the retunred * instances renders as defined in the spec. * + * @param links the {@link Links} that have been added to the response so far. * @return must not be {@literal null}. */ - Object getCurieInformation(); + Collection getCurieInformation(Links links); } diff --git a/src/main/java/org/springframework/hateoas/hal/DefaultCurieProvider.java b/src/main/java/org/springframework/hateoas/hal/DefaultCurieProvider.java index 3d873389..39393752 100644 --- a/src/main/java/org/springframework/hateoas/hal/DefaultCurieProvider.java +++ b/src/main/java/org/springframework/hateoas/hal/DefaultCurieProvider.java @@ -15,13 +15,20 @@ */ package org.springframework.hateoas.hal; +import java.util.Collection; +import java.util.Collections; + import org.springframework.hateoas.IanaRels; import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; +import org.springframework.hateoas.UriTemplate; import org.springframework.util.Assert; -import org.springframework.web.util.UriTemplate; /** + * Default implementation of {@link CurieProvider} rendering a single configurable {@link UriTemplate} based curie. + * * @author Oliver Gierke + * @since 0.9 */ public class DefaultCurieProvider implements CurieProvider { @@ -48,8 +55,8 @@ public class DefaultCurieProvider implements CurieProvider { * @see org.springframework.hateoas.hal.CurieProvider#getCurieInformation() */ @Override - public Curie getCurieInformation() { - return curie; + public Collection getCurieInformation(Links links) { + return Collections.singleton(curie); } /* @@ -70,8 +77,7 @@ public class DefaultCurieProvider implements CurieProvider { * * @author Oliver Gierke */ - @SuppressWarnings("unused") - private static class Curie extends Link { + protected static class Curie extends Link { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index 1ed455ca..d0391ad3 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -18,7 +18,6 @@ package org.springframework.hateoas.hal; import java.io.IOException; import java.lang.reflect.Type; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; @@ -28,6 +27,7 @@ import java.util.Map; import org.springframework.beans.BeanUtils; import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; @@ -132,6 +132,7 @@ public class Jackson2HalModule extends SimpleModule { // sort links according to their relation Map> sortedLinks = new LinkedHashMap>(); + List links = new ArrayList(); boolean prefixingRequired = curieProvider != null; boolean curiedLinkPresent = false; @@ -148,13 +149,15 @@ public class Jackson2HalModule extends SimpleModule { sortedLinks.put(rel, new ArrayList()); } + links.add(link); sortedLinks.get(rel).add(link); } if (prefixingRequired && curiedLinkPresent) { - Object curieInformation = curieProvider.getCurieInformation(); - List curies = new ArrayList(); - curies.add(Arrays.asList(curieInformation)); + + ArrayList curies = new ArrayList(); + curies.add(curieProvider.getCurieInformation(new Links(links))); + sortedLinks.put("curies", curies); } @@ -164,7 +167,7 @@ public class Jackson2HalModule extends SimpleModule { JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType); MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null, - provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property)); + provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property), null); serializer.serialize(sortedLinks, jgen, provider); } @@ -270,7 +273,7 @@ public class Jackson2HalModule extends SimpleModule { JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType); MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null, - provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property)); + provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property), null); serializer.serialize(builder.asMap(), jgen, provider); } @@ -451,6 +454,7 @@ public class Jackson2HalModule extends SimpleModule { private static final long serialVersionUID = 6420432361123210955L; + @SuppressWarnings("deprecation") public HalLinkListDeserializer() { super(List.class); } @@ -524,6 +528,7 @@ public class Jackson2HalModule extends SimpleModule { this(null, vc); } + @SuppressWarnings("deprecation") private HalResourcesDeserializer(Class type, JavaType contentType) { super(type); diff --git a/src/test/java/org/springframework/hateoas/TemplateVariablesUnitTest.java b/src/test/java/org/springframework/hateoas/TemplateVariablesUnitTest.java new file mode 100644 index 00000000..d1f8a49e --- /dev/null +++ b/src/test/java/org/springframework/hateoas/TemplateVariablesUnitTest.java @@ -0,0 +1,91 @@ +/* + * 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; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.hateoas.TemplateVariable.VariableType; + +/** + * Unit tests for {@link TemplateVariables}. + * + * @author Oliver Gierke + */ +public class TemplateVariablesUnitTest { + + /** + * @see #137 + */ + @Test + public void rendersNoTempalteVariablesAsEmptyString() { + assertThat(TemplateVariables.NONE.toString(), is("")); + } + + /** + * @see #137 + */ + @Test + public void rendersSingleVariableCorrectly() { + + TemplateVariables variables = new TemplateVariables(new TemplateVariable("foo", VariableType.SEGMENT)); + assertThat(variables.toString(), is("{/foo}")); + } + + /** + * @see #137 + */ + @Test + public void combinesMultipleVariablesOfTheSameType() { + + TemplateVariable first = new TemplateVariable("foo", VariableType.REQUEST_PARAM); + TemplateVariable second = new TemplateVariable("bar", VariableType.REQUEST_PARAM); + + TemplateVariables variables = new TemplateVariables(first, second); + + assertThat(variables.toString(), is("{?foo,bar}")); + } + + /** + * @see #137 + */ + @Test + public void combinesMultipleVariablesOfTheDifferentType() { + + TemplateVariable first = new TemplateVariable("foo", VariableType.SEGMENT); + TemplateVariable second = new TemplateVariable("bar", VariableType.REQUEST_PARAM); + + TemplateVariables variables = new TemplateVariables(first, second); + + assertThat(variables.toString(), is("{/foo}{?bar}")); + } + + /** + * @see #137 + */ + @Test + public void concatsVariables() { + + TemplateVariables first = new TemplateVariables(new TemplateVariable("foo", VariableType.SEGMENT)); + TemplateVariables second = new TemplateVariables(new TemplateVariable("bar", VariableType.REQUEST_PARAM)); + + TemplateVariables variables = first.concat(second); + + assertThat(variables.toString(), is("{/foo}{?bar}")); + } + +} diff --git a/src/test/java/org/springframework/hateoas/core/MethodParametersUnitTest.java b/src/test/java/org/springframework/hateoas/core/MethodParametersUnitTest.java new file mode 100644 index 00000000..13a3f90c --- /dev/null +++ b/src/test/java/org/springframework/hateoas/core/MethodParametersUnitTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-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 static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.MethodParameter; + +/** + * Unit tests for {@link MethodParameters}. + * + * @author Oliver Gierke + */ +public class MethodParametersUnitTest { + + @Test + public void prefersAnnotatedParameterOverDiscovered() throws Exception { + + Method method = Sample.class.getMethod("method", String.class, String.class, Object.class); + MethodParameters parameters = new MethodParameters(method, new AnnotationAttribute(Qualifier.class)); + + assertThat(parameters.getParameter("param"), is(notNullValue())); + assertThat(parameters.getParameter("foo"), is(notNullValue())); + assertThat(parameters.getParameter("another"), is(nullValue())); + } + + /** + * @see #138 + */ + @Test + public void returnsParametersOfAGivenType() throws Exception { + + Method method = Sample.class.getMethod("method", String.class, String.class, Object.class); + MethodParameters methodParameters = new MethodParameters(method); + + List objectParameters = methodParameters.getParametersOfType(Object.class); + assertThat(objectParameters, hasSize(1)); + assertThat(objectParameters.get(0).getParameterIndex(), is(2)); + } + + static class Sample { + + public void method(String param, @Qualifier("foo") String another, Object object) {} + } + +} diff --git a/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java b/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java new file mode 100644 index 00000000..8dbfc3d0 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013-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.hal; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.UriTemplate; + +/** + * Unit tests for {@link DefaultCurieProvider}. + * + * @author Oliver Gierke + */ +public class DefaultCurieProviderUnitTest { + + private static final UriTemplate URI_TEMPLATE = new UriTemplate("http://localhost:8080/rels/{rel}"); + + CurieProvider provider = new DefaultCurieProvider("acme", URI_TEMPLATE); + + @Test(expected = IllegalArgumentException.class) + public void preventsNullCurieName() { + new DefaultCurieProvider(null, URI_TEMPLATE); + } + + @Test(expected = IllegalArgumentException.class) + public void preventsEmptyCurieName() { + new DefaultCurieProvider("", URI_TEMPLATE); + } + + @Test(expected = IllegalArgumentException.class) + public void preventsNullUriTemplateName() { + new DefaultCurieProvider("acme", null); + } + + @Test(expected = IllegalArgumentException.class) + public void preventsUriTemplateWithoutVariable() { + new DefaultCurieProvider("acme", new UriTemplate("http://localhost:8080/rels")); + } + + @Test(expected = IllegalArgumentException.class) + public void preventsUriTemplateWithMoreThanOneVariable() { + new DefaultCurieProvider("acme", new UriTemplate("http://localhost:8080/rels/{rel}/{another}")); + } + + @Test + public void doesNotPrefixIanaRels() { + assertThat(provider.getNamespacedRelFrom(new Link("http://amazon.com")), is("self")); + } + + @Test + public void prefixesNormalRels() { + assertThat(provider.getNamespacedRelFrom(new Link("http://amazon.com", "book")), is("acme:book")); + } + + @Test + public void doesNotPrefixQualifiedRels() { + assertThat(provider.getNamespacedRelFrom(new Link("http://amazon.com", "custom:rel")), is("custom:rel")); + } +} diff --git a/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTests.java b/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTests.java index 0f7597e8..4f99af64 100644 --- a/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTests.java +++ b/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTests.java @@ -20,7 +20,7 @@ import static org.junit.Assert.*; import org.junit.Test; import org.springframework.hateoas.Link; -import org.springframework.web.util.UriTemplate; +import org.springframework.hateoas.UriTemplate; /** * Unit tests for {@link DefaultCurieProvider}. diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java index 66999344..acc9bcb7 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -19,6 +19,8 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; @@ -32,9 +34,9 @@ import org.springframework.hateoas.PagedResources.PageMetadata; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; +import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.core.AnnotationRelProvider; import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator; -import org.springframework.web.util.UriTemplate; import com.fasterxml.jackson.databind.ObjectMapper; @@ -61,6 +63,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg static final Links PAGINATION_LINKS = new Links(new Link("foo", Link.REL_NEXT), new Link("bar", Link.REL_PREVIOUS)); static final String CURIED_DOCUMENT = "{\"_links\":{\"self\":{\"href\":\"foo\"},\"foo:myrel\":{\"href\":\"bar\"},\"curies\":[{\"href\":\"http://localhost:8080/rels/{rel}\",\"name\":\"foo\",\"template\":true}]}}"; + static final String MULTIPLE_CURIES_DOCUMENT = "{\"_links\":{\"default:myrel\":{\"href\":\"foo\"},\"curies\":[{\"href\":\"bar\",\"name\":\"foo\"},{\"href\":\"foo\",\"name\":\"bar\"}]}}"; static final String SINGLE_NON_CURIE_LINK = "{\"_links\":{\"self\":{\"href\":\"foo\"}}}"; static final String EMPTY_DOCUMENT = "{}"; @@ -322,6 +325,25 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg assertThat(write(support), is(LINK_TEMPLATE)); } + /** + * @see #142 + */ + @Test + public void rendersMultipleCuries() throws Exception { + + Resources resources = new Resources(Collections.emptySet()); + resources.add(new Link("foo", "myrel")); + + CurieProvider provider = new DefaultCurieProvider("default", new UriTemplate("/doc{?rel}")) { + @Override + public Collection getCurieInformation(Links links) { + return Arrays.asList(new Curie("foo", "bar"), new Curie("bar", "foo")); + } + }; + + assertThat(getCuriedObjectMapper(provider).writeValueAsString(resources), is(MULTIPLE_CURIES_DOCUMENT)); + } + private static Resources> setupAnnotatedPagedResources() { List> content = new ArrayList>(); @@ -351,11 +373,14 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg private static ObjectMapper getCuriedObjectMapper() { - CurieProvider curieProvider = new DefaultCurieProvider("foo", new UriTemplate("http://localhost:8080/rels/{rel}")); + return getCuriedObjectMapper(new DefaultCurieProvider("foo", new UriTemplate("http://localhost:8080/rels/{rel}"))); + } + + private static ObjectMapper getCuriedObjectMapper(CurieProvider provider) { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new Jackson2HalModule()); - mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), curieProvider)); + mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), provider)); return mapper; }