diff --git a/lombok.config b/lombok.config index 5bad9682..d17e623d 100644 --- a/lombok.config +++ b/lombok.config @@ -1,3 +1,4 @@ lombok.anyConstructor.suppressConstructorProperties=true lombok.nonNull.exceptionType = IllegalArgumentException lombok.log.fieldName = LOG +lombok.addLombokGeneratedAnnotation = true diff --git a/src/main/java/org/springframework/hateoas/server/EntityLinks.java b/src/main/java/org/springframework/hateoas/server/EntityLinks.java index b7f5cd0b..8823f7f7 100644 --- a/src/main/java/org/springframework/hateoas/server/EntityLinks.java +++ b/src/main/java/org/springframework/hateoas/server/EntityLinks.java @@ -15,17 +15,21 @@ */ package org.springframework.hateoas.server; +import java.util.function.Function; + import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.Identifiable; import org.springframework.hateoas.Link; +import org.springframework.hateoas.server.TypedEntityLinks.ExtendedTypedEntityLinks; import org.springframework.plugin.core.Plugin; +import org.springframework.util.Assert; /** * Accessor to links pointing to controllers backing an entity type. The {@link IllegalArgumentException} potentially * thrown by the declared methods will only appear if the {@link #supports(Class)} method has returned {@literal false} * and the method has been invoked anyway, i.e. if {@link #supports(Class)} returns {@literal true} it's safe to invoke * the interface methods an the exception will never be thrown. - * + * * @author Oliver Gierke */ public interface EntityLinks extends Plugin> { @@ -33,7 +37,7 @@ public interface EntityLinks extends Plugin> { /** * Returns a {@link LinkBuilder} able to create links to the controller managing the given entity type. Expects a * controller being mapped to a fully expanded URI template (i.e. not path variables being used). - * + * * @param type the entity type to point to, must not be {@literal null}. * @return the {@link LinkBuilder} pointing to the collection resource. Will never be {@literal null}. * @throws IllegalArgumentException in case the given type is unknown the entity links infrastructure. @@ -43,7 +47,7 @@ public interface EntityLinks extends Plugin> { /** * Returns a {@link LinkBuilder} able to create links to the controller managing the given entity type, unfolding the * given parameters into the URI template the backing controller is mapped to. - * + * * @param type the entity type to point to, must not be {@literal null}. * @return the {@link LinkBuilder} pointing to the collection resource. * @throws IllegalArgumentException in case the given type is unknown the entity links infrastructure. @@ -52,19 +56,37 @@ public interface EntityLinks extends Plugin> { /** * Returns a {@link LinkBuilder} able to create links to the controller managing the given entity type and id. - * Implementations will know about the URI structure being used to expose single-resource URIs. - * + * Implementations will know about the URI structure being used to expose item-resource URIs. + * * @param type the entity type to point to, must not be {@literal null}. * @param id the id of the object of the handed type, {@link Identifiable}s will be unwrapped. - * @return the {@link LinkBuilder} pointing to the single resource identified by the given type and id. Will never be + * @return the {@link LinkBuilder} pointing to the item resource identified by the given type and id. Will never be * {@literal null}. * @throws IllegalArgumentException in case the given type is unknown the entity links infrastructure. */ LinkBuilder linkForItemResource(Class type, Object id); + /** + * Returns a {@link LinkBuilder} able to create links to the controller managing the given entity type and identifier + * extractor. Implementations will know about the URI structure being used to expose item-resource URIs. + * + * @param type the entity to point to, must not be {@literal null}. + * @param identifierExtractor an extractor function to determine the id of the given entity, must not be + * {@literal null}. + * @return the {@link LinkBuilder} pointing to the item resource identified by the given entity. Will never be + * {@literal null}. + * @throws IllegalArgumentException in case the given type is unknown the entity links infrastructure. + */ + default LinkBuilder linkForItemResource(T entity, Function identifierExtractor) { + + Assert.notNull(identifierExtractor, "Identifier extractor must not be null!"); + + return linkForItemResource(entity.getClass(), identifierExtractor.apply(entity)); + } + /** * Returns a {@link LinkBuilder} able to create links to the controller managing the given entity. - * + * * @see #linkForItemResource(Class, Object) * @param entity the entity type to point to, must not be {@literal null}. * @return the {@link LinkBuilder} pointing the given entity. Will never be {@literal null}. @@ -75,7 +97,7 @@ public interface EntityLinks extends Plugin> { /** * Creates a {@link Link} pointing to the collection resource of the given type. The relation type of the link will be * determined by the implementation class and should be defaulted to {@link IanaLinkRelations#SELF}. - * + * * @param type the entity type to point to, must not be {@literal null}. * @return the {@link Link} pointing to the collection resource exposed for the given entity. Will never be * {@literal null}. @@ -84,9 +106,9 @@ public interface EntityLinks extends Plugin> { Link linkToCollectionResource(Class type); /** - * Creates a {@link Link} pointing to single resource backing the given entity type and id. The relation type of the + * Creates a {@link Link} pointing to item resource backing the given entity type and id. The relation type of the * link will be determined by the implementation class and should be defaulted to {@link IanaLinkRelations#SELF}. - * + * * @param type the entity type to point to, must not be {@literal null}. * @param id the identifier of the entity of the given type * @return the {@link Link} pointing to the resource exposed for the entity with the given type and id. Will never be @@ -96,12 +118,49 @@ public interface EntityLinks extends Plugin> { Link linkToItemResource(Class type, Object id); /** - * Creates a {@link Link} pointing to single resource backing the given entity. The relation type of the link will be + * Creates a {@link Link} pointing to item resource backing the given entity and identifier extractor. The relation + * type of the link will be determined by the implementation class and should be defaulted to + * {@link IanaLinkRelations#SELF}. + * + * @param type the entity to point to, must not be {@literal null}. + * @param identifierExtractor an extractor function to determine the id of the given entity. + * @return the {@link Link} pointing to the resource exposed for the given entity. Will never be {@literal null}. + * @throws IllegalArgumentException in case the given type is unknown the entity links infrastructure. + */ + default Link linkToItemResource(T entity, Function identifierExtractor) { + return linkToItemResource(entity.getClass(), identifierExtractor.apply(entity)); + } + + /** + * Creates a {@link Link} pointing to item resource backing the given entity. The relation type of the link will be * determined by the implementation class and should be defaulted to {@link IanaLinkRelations#SELF}. - * + * * @param entity the entity type to point to, must not be {@literal null}. * @return the {@link Link} pointing to the resource exposed for the given entity. Will never be {@literal null}. * @throws IllegalArgumentException in case the type of the given entity is unknown the entity links infrastructure. */ Link linkToItemResource(Identifiable entity); + + /** + * Creates a {@link TypedEntityLinks} instance using the given identifier extractor function. + * + * @param the type of entity to be handled. + * @param extractor the extractor to use to derive an identifier from the given entity. + * @return + */ + default TypedEntityLinks forType(Function extractor) { + return new TypedEntityLinks<>(extractor, this); + } + + /** + * Creates a {@link TypedEntityLinks} instance using the given type and identifier extractor function. + * + * @param the type of entity to be handled. + * @param type the type of entity. + * @param extractor the extractor to use to derive an identifier from the given entity. + * @return + */ + default ExtendedTypedEntityLinks forType(Class type, Function extractor) { + return new ExtendedTypedEntityLinks<>(extractor, this, type); + } } diff --git a/src/main/java/org/springframework/hateoas/server/TypedEntityLinks.java b/src/main/java/org/springframework/hateoas/server/TypedEntityLinks.java new file mode 100644 index 00000000..086ecef4 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/server/TypedEntityLinks.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 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.server; + +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.function.Function; + +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; +import org.springframework.util.Assert; + +/** + * Entity links API to create {@link Link}s and {@link LinkBuilder} instances based on an identifier function. + * + * @author Oliver Drotbohm + * @see EntityLinks#forType(Function) + * @see EntityLinks#forType(Class, Function) + */ +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class TypedEntityLinks { + + private final @NonNull Function identifierExtractor; + private final @NonNull EntityLinks entityLinks; + + /** + * Returns a {@link LinkBuilder} able to create links to the controller managing the given entity. Implementations + * will know about the URI structure being used to expose item-resource URIs. + * + * @param type the entity to point to, must not be {@literal null}. + * @return the {@link LinkBuilder} pointing to the item resource identified by the given entity. Will never be + * {@literal null}. + * @throws IllegalArgumentException in case the given type is unknown the entity links infrastructure. + */ + public LinkBuilder linkForItemResource(T entity) { + return entityLinks.linkForItemResource(entity.getClass(), identifierExtractor.apply(entity)); + } + + /** + * Creates a {@link Link} pointing to item resource backing the given entity. The relation type of the link will be + * determined by the implementation class and should be defaulted to {@link IanaLinkRelations#SELF}. + * + * @param type the entity to point to, must not be {@literal null}. + * @return the {@link Link} pointing to the resource exposed for the given entity. Will never be {@literal null}. + * @throws IllegalArgumentException in case the given type is unknown the entity links infrastructure. + */ + public Link linkToItemResource(T entity) { + return entityLinks.linkToItemResource(entity.getClass(), identifierExtractor.apply(entity)); + } + + /** + * Extension of {@link TypedEntityLinks} that exposes the ability to create links to collection resources as well. + * + * @author Oliver Drotbohm + */ + public static class ExtendedTypedEntityLinks extends TypedEntityLinks { + + private final Class type; + private final EntityLinks delegate; + + ExtendedTypedEntityLinks(Function identifierExtractor, EntityLinks delegate, Class type) { + + super(identifierExtractor, delegate); + + Assert.notNull(type, "Type must not be null!"); + + this.type = type; + this.delegate = delegate; + } + + /** + * Creates a {@link Link} pointing to the collection resource of the configured type. The relation type of the link + * will be determined by the implementation class and should be defaulted to {@link IanaLinkRelations#SELF}. + * + * @param type the entity type to point to, must not be {@literal null}. + * @return the {@link Link} pointing to the collection resource exposed for the configured entity type. Will never + * be {@literal null}. + * @throws IllegalArgumentException in case the given type is unknown the entity links infrastructure. + */ + public Link linkToCollectionResource() { + return delegate.linkToCollectionResource(type); + } + } +} diff --git a/src/test/java/org/springframework/hateoas/server/core/ControllerEntityLinksUnitTest.java b/src/test/java/org/springframework/hateoas/server/core/ControllerEntityLinksUnitTest.java index 9d875b5b..d4c8c648 100755 --- a/src/test/java/org/springframework/hateoas/server/core/ControllerEntityLinksUnitTest.java +++ b/src/test/java/org/springframework/hateoas/server/core/ControllerEntityLinksUnitTest.java @@ -22,6 +22,8 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; +import lombok.Value; + import java.util.Arrays; import org.junit.Test; @@ -33,6 +35,8 @@ import org.springframework.hateoas.server.EntityLinks; import org.springframework.hateoas.server.ExposesResourceFor; import org.springframework.hateoas.server.LinkBuilder; import org.springframework.hateoas.server.LinkBuilderFactory; +import org.springframework.hateoas.server.TypedEntityLinks; +import org.springframework.hateoas.server.TypedEntityLinks.ExtendedTypedEntityLinks; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -109,6 +113,42 @@ public class ControllerEntityLinksUnitTest extends TestUtils { .withMessageContaining(ExposesResourceFor.class.getName()); } + @Test // #843 + public void returnsItemResourceLinkForExtractorFunction() { + + when(linkBuilderFactory.linkTo(SampleController.class, new Object[0])).thenReturn(linkTo(SampleController.class)); + + ControllerEntityLinks entityLinks = new ControllerEntityLinks(singleton(SampleController.class), + linkBuilderFactory); + + assertThat(entityLinks.linkToItemResource(new Person(42L), Person::getId).getHref()).endsWith("/person/42"); + } + + @Test // #843 + public void returnsTypeEntityLinks() { + + when(linkBuilderFactory.linkTo(SampleController.class, new Object[0])).thenReturn(linkTo(SampleController.class)); + + TypedEntityLinks entityLinks = new ControllerEntityLinks(singleton(SampleController.class), + linkBuilderFactory).forType(Person::getId); + + Person person = new Person(42L); + + assertThat(entityLinks.linkForItemResource(person).withSelfRel().getHref()).endsWith("/person/42"); + assertThat(entityLinks.linkToItemResource(person).getHref()).endsWith("/person/42"); + } + + @Test // #843 + public void returnsExtendedTypedEntityLinks() { + + when(linkBuilderFactory.linkTo(SampleController.class, new Object[0])).thenReturn(linkTo(SampleController.class)); + + ExtendedTypedEntityLinks entityLinks = new ControllerEntityLinks(singleton(SampleController.class), + linkBuilderFactory).forType(Person.class, Person::getId); + + assertThat(entityLinks.linkToCollectionResource().getHref()).endsWith("/person"); + } + @Controller @ExposesResourceFor(Person.class) @RequestMapping("/person") @@ -121,7 +161,10 @@ public class ControllerEntityLinksUnitTest extends TestUtils { static class InvalidController {} - static class Person {} + @Value + static class Person { + Long id; + } static class Order {} }