From 7bd414e79a03de8982306388ef20c6974e5b7042 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 21 Jan 2021 14:07:36 +0100 Subject: [PATCH] #1441 - Introduce AffordanceModelFactory.getAffordanceModel(ConfiguredAffordance). Introduce a new overload in AffordanceModelFactory so that implementations get a single object handed to work with for better extensibility in the future. Introduced ConfigurableAffordance to be used instead of AffordanceBuilder in assignments and ConfiguredAffordance to separate creation and consumption of affordances during setup. --- .../hateoas/AffordanceModel.java | 43 ++- .../mediatype/AffordanceModelFactory.java | 28 +- .../mediatype/AffordanceOperations.java | 1 - .../hateoas/mediatype/Affordances.java | 295 +++++++++--------- .../mediatype/ConfigurableAffordance.java | 176 +++++++++++ .../mediatype/ConfiguredAffordance.java | 79 +++++ .../mediatype/TypeBasedPayloadMetadata.java | 32 +- .../CollectionJsonAffordanceModel.java | 33 +- .../CollectionJsonAffordanceModelFactory.java | 27 +- .../mediatype/uber/UberAffordanceModel.java | 11 +- .../uber/UberAffordanceModelFactory.java | 26 +- 11 files changed, 564 insertions(+), 187 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/mediatype/ConfigurableAffordance.java create mode 100644 src/main/java/org/springframework/hateoas/mediatype/ConfiguredAffordance.java diff --git a/src/main/java/org/springframework/hateoas/AffordanceModel.java b/src/main/java/org/springframework/hateoas/AffordanceModel.java index 897c1b02..ceb93d25 100644 --- a/src/main/java/org/springframework/hateoas/AffordanceModel.java +++ b/src/main/java/org/springframework/hateoas/AffordanceModel.java @@ -26,6 +26,7 @@ import java.util.stream.Stream; import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -248,6 +249,24 @@ public abstract class AffordanceModel { * @return */ List getI18nCodes(); + + /** + * Creates a new {@link InputPayloadMetadata} with the given {@link MediaType} assigned. + * + * @param mediaType can be {@literal null}. + * @return will never be {@literal null}. + * @since 1.3 + */ + InputPayloadMetadata withMediaType(@Nullable MediaType mediaType); + + /** + * Returns the {@link MediaType} that the payload requires. + * + * @return can be {@literal null}. + * @since 1.3 + */ + @Nullable + MediaType getMediaType(); } /** @@ -258,13 +277,15 @@ public abstract class AffordanceModel { private static class DelegatingInputPayloadMetadata implements InputPayloadMetadata { private final PayloadMetadata metadata; + private final MediaType mediaType; public static DelegatingInputPayloadMetadata of(PayloadMetadata metadata) { - return new DelegatingInputPayloadMetadata(metadata); + return new DelegatingInputPayloadMetadata(metadata, null); } - private DelegatingInputPayloadMetadata(PayloadMetadata metadata) { + private DelegatingInputPayloadMetadata(PayloadMetadata metadata, MediaType mediaType) { this.metadata = metadata; + this.mediaType = mediaType; } /* @@ -303,6 +324,24 @@ public abstract class AffordanceModel { return Collections.emptyList(); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#withMediaType(org.springframework.http.MediaType) + */ + @Override + public InputPayloadMetadata withMediaType(MediaType mediaType) { + return new DelegatingInputPayloadMetadata(metadata, mediaType); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#getMediaType() + */ + @Override + public MediaType getMediaType() { + return mediaType; + } + /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) diff --git a/src/main/java/org/springframework/hateoas/mediatype/AffordanceModelFactory.java b/src/main/java/org/springframework/hateoas/mediatype/AffordanceModelFactory.java index c07a210f..2cee389b 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/AffordanceModelFactory.java +++ b/src/main/java/org/springframework/hateoas/mediatype/AffordanceModelFactory.java @@ -24,8 +24,11 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.QueryParameter; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.util.Assert; /** + * SPI for media type implementations to create a specific {@link AffordanceModel} for a {@link ConfiguredAffordance}. + * * @author Greg Turnquist * @author Oliver Gierke */ @@ -48,7 +51,28 @@ public interface AffordanceModelFactory { * @param queryMethodParameters * @param outputType * @return + * @deprecated since 1.3 in favor of {@link #getAffordanceModel(ConfiguredAffordance)}. Will be removed in 1.4. */ - AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, InputPayloadMetadata inputType, - List queryMethodParameters, PayloadMetadata outputType); + @Deprecated + default AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, + InputPayloadMetadata inputType, + List queryMethodParameters, PayloadMetadata outputType) { + throw new IllegalStateException( + "This method needs to be implemented unless you implement getAffordanceModel(ConfiguredAffordance)!"); + } + + /** + * Return the {@link AffordanceModel} for the given {@link ConfiguredAffordance}. + * + * @param configured will never be {@literal null}. + * @return must not be {@literal null}. + * @since 1.3 + */ + default AffordanceModel getAffordanceModel(ConfiguredAffordance configured) { + + Assert.notNull(configured, "Configured affordance must not be null!"); + + return getAffordanceModel(configured.getNameOrDefault(), configured.getTarget(), configured.getMethod(), + configured.getInputMetadata(), configured.getQueryParameters(), configured.getOutputMetadata()); + } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/AffordanceOperations.java b/src/main/java/org/springframework/hateoas/mediatype/AffordanceOperations.java index 1d7e2100..abc539e6 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/AffordanceOperations.java +++ b/src/main/java/org/springframework/hateoas/mediatype/AffordanceOperations.java @@ -17,7 +17,6 @@ package org.springframework.hateoas.mediatype; import org.springframework.hateoas.Affordance; import org.springframework.hateoas.Link; -import org.springframework.hateoas.mediatype.Affordances.AffordanceBuilder; /** * Operations commons to all builder APIs. diff --git a/src/main/java/org/springframework/hateoas/mediatype/Affordances.java b/src/main/java/org/springframework/hateoas/mediatype/Affordances.java index 489014c7..96304427 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/Affordances.java +++ b/src/main/java/org/springframework/hateoas/mediatype/Affordances.java @@ -26,12 +26,12 @@ import java.util.stream.Stream; import org.springframework.core.ResolvableType; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.hateoas.Affordance; -import org.springframework.hateoas.AffordanceModel; import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata; import org.springframework.hateoas.AffordanceModel.PayloadMetadata; import org.springframework.hateoas.Link; import org.springframework.hateoas.QueryParameter; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -82,7 +82,7 @@ public class Affordances implements AffordanceOperations { /* * (non-Javadoc) - * @see org.springframework.hateoas.mediatype.TerminalOperations#build() + * @see org.springframework.hateoas.mediatype.AffordanceOperations#toLink() */ public Link toLink() { return link; @@ -92,11 +92,14 @@ public class Affordances implements AffordanceOperations { * Builder API for {@link Affordance} instances. * * @author Oliver Drotbohm + * @deprecated since 1.3, refer to {@link ConfiguredAffordance} instead. Will be made private in 1.4. + * @see ConfigurableAffordance + * @see ConfiguredAffordance */ - public static class AffordanceBuilder implements AffordanceOperations { + @Deprecated + public static class AffordanceBuilder implements ConfigurableAffordance, ConfiguredAffordance { private final Affordances context; - private final HttpMethod method; private final Link target; private final InputPayloadMetadata inputMetdata; @@ -106,17 +109,7 @@ public class Affordances implements AffordanceOperations { private @Nullable String name; private AffordanceBuilder(Affordances context, HttpMethod method, Link target, InputPayloadMetadata inputMetdata, - PayloadMetadata outputMetadata) { - - this.context = context; - this.method = method; - this.target = target; - this.inputMetdata = inputMetdata; - this.outputMetadata = outputMetadata; - } - - private AffordanceBuilder(Affordances context, HttpMethod method, Link target, InputPayloadMetadata inputMetdata, - PayloadMetadata outputMetadata, List parameters, String name) { + PayloadMetadata outputMetadata, List parameters, @Nullable String name) { this.context = context; this.method = method; @@ -127,42 +120,38 @@ public class Affordances implements AffordanceOperations { this.name = name; } - /** - * Registers the given type as input and output model for the affordance. - * - * @param type must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withInputAndOutput(java.lang.Class) */ + @Override public AffordanceBuilder withInputAndOutput(Class type) { return withInput(type).withOutput(type); } - /** - * Registers the given {@link ResolvableType} as input and output model for the affordance. - * - * @param type must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withInputAndOutput(org.springframework.core.ResolvableType) */ + @Override public AffordanceBuilder withInputAndOutput(ResolvableType type) { return withInput(type).withOutput(type); } - /** - * Registers the given {@link PayloadMetadata} as input and output model. - * - * @param metadata must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withInputAndOutput(org.springframework.hateoas.AffordanceModel.PayloadMetadata) */ + @Override public AffordanceBuilder withInputAndOutput(PayloadMetadata metadata) { return withInput(metadata).withOutput(metadata); } - /** - * Registers the given type as input model for the affordance. - * - * @param type must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withInput(java.lang.Class) */ + @Override public AffordanceBuilder withInput(Class type) { Assert.notNull(type, "Type must not be null!"); @@ -170,12 +159,11 @@ public class Affordances implements AffordanceOperations { return withInput(ResolvableType.forClass(type)); } - /** - * Registers the given {@link ResolvableType} as input model for the affordance. - * - * @param type must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withInput(org.springframework.core.ResolvableType) */ + @Override public AffordanceBuilder withInput(ResolvableType type) { Assert.notNull(type, "Type must not be null!"); @@ -183,12 +171,11 @@ public class Affordances implements AffordanceOperations { return withInput(PropertyUtils.getExposedProperties(type)); } - /** - * Registers the given {@link PayloadMetadata} as input model. - * - * @param metadata must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withInput(org.springframework.hateoas.AffordanceModel.PayloadMetadata) */ + @Override public AffordanceBuilder withInput(PayloadMetadata metadata) { InputPayloadMetadata inputMetadata = InputPayloadMetadata.from(metadata); @@ -196,62 +183,65 @@ public class Affordances implements AffordanceOperations { return new AffordanceBuilder(context, method, target, inputMetadata, outputMetadata, parameters, name); } - /** - * Registers the given type as the output model. - * - * @param type must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withOutput(java.lang.Class) */ + @Override public AffordanceBuilder withOutput(Class type) { return withOutput(ResolvableType.forClass(type)); } - /** - * Registers the given {@link ResolvableType} as the output model. - * - * @param type must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withOutput(org.springframework.core.ResolvableType) */ + @Override public AffordanceBuilder withOutput(ResolvableType type) { return withOutput(PropertyUtils.getExposedProperties(type)); } - /** - * Registers the given {@link PayloadMetadata} as output model. - * - * @param metadata must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withOutput(org.springframework.hateoas.AffordanceModel.PayloadMetadata) */ + @Override public AffordanceBuilder withOutput(PayloadMetadata metadata) { return new AffordanceBuilder(context, method, target, inputMetdata, metadata, parameters, name); } - /** - * Replaces the current {@link QueryParameter} list with the given ones. - * - * @param parameters must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withInputMediaType(org.springframework.http.MediaType) */ + @Override + public AffordanceBuilder withInputMediaType(@Nullable MediaType inputMediaType) { + return withInput(inputMetdata.withMediaType(inputMediaType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withParameters(org.springframework.hateoas.QueryParameter[]) + */ + @Override public AffordanceBuilder withParameters(QueryParameter... parameters) { return withParameters(Arrays.asList(parameters)); } - /** - * Replaces the current {@link QueryParameter} list with the given ones. - * - * @param parameters must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withParameters(java.util.List) */ + @Override public AffordanceBuilder withParameters(List parameters) { return new AffordanceBuilder(context, method, target, inputMetdata, outputMetadata, parameters, name); } - /** - * Adds the given {@link QueryParameter}s to the {@link Affordance} to build. - * - * @param parameters must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#addParameters(org.springframework.hateoas.QueryParameter[]) */ + @Override public AffordanceBuilder addParameters(QueryParameter... parameters) { List newParameters = new ArrayList<>(this.parameters.size() + parameters.length); @@ -261,18 +251,50 @@ public class Affordances implements AffordanceOperations { return new AffordanceBuilder(context, method, target, inputMetdata, outputMetadata, newParameters, name); } - /** - * Concludes the creation of the current {@link Affordance} to build and starts a new one. - * - * @param method must not be {@literal null}. - * @return - * @see #build() - * @see #toLink() + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withTarget(org.springframework.hateoas.Link) */ + @Override + public ConfigurableAffordance withTarget(Link target) { + + Assert.notNull(target, "Target must not be null!"); + + return this.target == target ? this + : new AffordanceBuilder(this.context, this.method, target, this.inputMetdata, this.outputMetadata, + this.parameters, this.name); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withName(java.lang.String) + */ + @Override + public ConfigurableAffordance withName(@Nullable String name) { + + return this.name == name ? this + : new AffordanceBuilder(this.context, this.method, this.target, this.inputMetdata, this.outputMetadata, + this.parameters, name); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#andAfford(org.springframework.http.HttpMethod) + */ + @Override public AffordanceBuilder andAfford(HttpMethod method) { return build().afford(method); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#build() + */ + @Override + public Affordances build() { + return Affordances.of(toLink()); + } + /* * (non-Javadoc) * @see org.springframework.hateoas.mediatype.AffordanceOperations#toLink() @@ -282,47 +304,11 @@ public class Affordances implements AffordanceOperations { return context.link.andAffordance(buildAffordance()); } - /** - * Builds the {@link Affordance} currently under construction and returns in alongside the ones already contained in - * the {@link Link} the buildup started from. - * - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfiguredAffordance#getNameOrDefault() */ - public Affordances build() { - return Affordances.of(toLink()); - } - - /** - * Builds an {@link Affordance} from the current state of the builder. - * - * @return must not be {@literal null}. - */ - private Affordance buildAffordance() { - - return factories.stream() // - .collect(collectingAndThen(toMap(AffordanceModelFactory::getMediaType, // - it -> createModel(it, parameters == null ? Collections.emptyList() : parameters)), // - Affordance::new)); - } - - /** - * Creates a new {@link AffordanceModel} using the given {@link AffordanceModelFactory} and {@link QueryParameter}s. - * - * @param factory must not be {@literal null}. - * @param parameters must not be {@literal null}. - * @return will never be {@literal null}. - */ - private AffordanceModel createModel(AffordanceModelFactory factory, List parameters) { - return factory.getAffordanceModel(getNameOrDefault(), target, method, inputMetdata, parameters, outputMetadata); - } - - /** - * Returns the explicitly configured name of the {@link Affordance} or calculates a default based on the - * {@link HttpMethod} and type backing it. - * - * @return - */ - private String getNameOrDefault() { + public String getNameOrDefault() { if (name != null) { return name; @@ -343,30 +329,61 @@ public class Affordances implements AffordanceOperations { return resolvedType == null ? name : name.concat(resolvedType.getSimpleName()); } - /** - * Create a new {@link AffordanceBuilder} by copying all attributes and replacing the {@literal target}. - * - * @param target - * @return + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfiguredAffordance#getMethod() */ - public AffordanceBuilder withTarget(Link target) { + @Override + public HttpMethod getMethod() { + return method; + } - return this.target == target ? this - : new AffordanceBuilder(this.context, this.method, target, this.inputMetdata, this.outputMetadata, - this.parameters, this.name); + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfiguredAffordance#getInputMetadata() + */ + @Override + public InputPayloadMetadata getInputMetadata() { + return inputMetdata; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfiguredAffordance#getOutputMetadata() + */ + @Override + public PayloadMetadata getOutputMetadata() { + return outputMetadata; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfiguredAffordance#getTarget() + */ + @Override + public Link getTarget() { + return target; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfiguredAffordance#getQueryParameters() + */ + @Override + public List getQueryParameters() { + return parameters; } /** - * Create a new {@link AffordanceBuilder} by copying all attributes and replacing the {@literal name}. - * - * @param name - * @return + * Builds an {@link Affordance} from the current state of the builder. + * + * @return must not be {@literal null}. */ - public AffordanceBuilder withName(@Nullable String name) { + private Affordance buildAffordance() { - return this.name == name ? this - : new AffordanceBuilder(this.context, this.method, this.target, this.inputMetdata, this.outputMetadata, - this.parameters, name); + return factories.stream() // + .collect(collectingAndThen(toMap(AffordanceModelFactory::getMediaType, // + it -> it.getAffordanceModel(this)), Affordance::new)); } } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/ConfigurableAffordance.java b/src/main/java/org/springframework/hateoas/mediatype/ConfigurableAffordance.java new file mode 100644 index 00000000..a1eb3df8 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mediatype/ConfigurableAffordance.java @@ -0,0 +1,176 @@ +/* + * Copyright 2021 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 + * + * https://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.mediatype; + +import java.util.List; + +import org.springframework.core.ResolvableType; +import org.springframework.hateoas.Affordance; +import org.springframework.hateoas.AffordanceModel.PayloadMetadata; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.QueryParameter; +import org.springframework.hateoas.mediatype.Affordances.AffordanceBuilder; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; + +/** + * An affordance in creation. Superseding {@link AffordanceBuilder} to build up affordances manually to clearly + * distinguish between building the affordance and consuming the configured state. + * + * @author Oliver Drotbohm + * @since 1.3 + */ +public interface ConfigurableAffordance extends AffordanceOperations { + + /** + * Registers the given type as input and output model for the affordance. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withInputAndOutput(Class type); + + /** + * Registers the given {@link ResolvableType} as input and output model for the affordance. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withInputAndOutput(ResolvableType type); + + /** + * Registers the given {@link PayloadMetadata} as input and output model. + * + * @param metadata must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withInputAndOutput(PayloadMetadata metadata); + + /** + * Registers the given type as input model for the affordance. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withInput(Class type); + + /** + * Registers the given {@link ResolvableType} as input model for the affordance. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withInput(ResolvableType type); + + /** + * Registers the given {@link PayloadMetadata} as input model. + * + * @param metadata must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withInput(PayloadMetadata metadata); + + /** + * Registers the given type as the output model. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withOutput(Class type); + + /** + * Registers the given {@link ResolvableType} as the output model. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withOutput(ResolvableType type); + + /** + * Registers the given {@link PayloadMetadata} as output model. + * + * @param metadata must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withOutput(PayloadMetadata metadata); + + /** + * Registers the input to expect to be of the given {@link MediaType}. + * + * @param inputMediaType + * @return + */ + ConfigurableAffordance withInputMediaType(@Nullable MediaType inputMediaType); + + /** + * Replaces the current {@link QueryParameter} list with the given ones. + * + * @param parameters must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withParameters(QueryParameter... parameters); + + /** + * Replaces the current {@link QueryParameter} list with the given ones. + * + * @param parameters must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withParameters(List parameters); + + /** + * Adds the given {@link QueryParameter}s to the {@link Affordance} to build. + * + * @param parameters must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance addParameters(QueryParameter... parameters); + + /** + * Concludes the creation of the current {@link Affordance} to build and starts a new one. + * + * @param method must not be {@literal null}. + * @return + * @see #build() + * @see #toLink() + */ + ConfigurableAffordance andAfford(HttpMethod method); + + /** + * Builds the {@link Affordance} currently under construction and returns in alongside the ones already contained in + * the {@link Link} the buildup started from. + * + * @return will never be {@literal null}. + */ + Affordances build(); + + /** + * Create a new {@link AffordanceBuilder} by copying all attributes and replacing the {@literal target}. + * + * @param target + * @return + */ + ConfigurableAffordance withTarget(Link target); + + /** + * Create a new {@link AffordanceBuilder} by copying all attributes and replacing the {@literal name}. + * + * @param name can be {@literal null}. + * @return + */ + ConfigurableAffordance withName(@Nullable String name); +} diff --git a/src/main/java/org/springframework/hateoas/mediatype/ConfiguredAffordance.java b/src/main/java/org/springframework/hateoas/mediatype/ConfiguredAffordance.java new file mode 100644 index 00000000..00cb7cbf --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mediatype/ConfiguredAffordance.java @@ -0,0 +1,79 @@ +/* + * Copyright 2021 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 + * + * https://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.mediatype; + +import java.util.List; + +import org.springframework.hateoas.Affordance; +import org.springframework.hateoas.AffordanceModel; +import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata; +import org.springframework.hateoas.AffordanceModel.PayloadMetadata; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.QueryParameter; +import org.springframework.http.HttpMethod; + +/** + * A configure affordance for inspection by media type implementations to create {@link AffordanceModel} instances. + * + * @author Oliver Drotbohm + * @since 1.3 + * @see AffordanceModelFactory#getAffordanceModel(ConfiguredAffordance) + */ +public interface ConfiguredAffordance { + + /** + * Returns the explicitly configured name of the {@link Affordance} or calculates a default based on the + * {@link HttpMethod} and type backing it. + * + * @return will never be {@literal null}. + */ + String getNameOrDefault(); + + /** + * Returns the affordance's target. + * + * @return will never be {@literal null}. + */ + Link getTarget(); + + /** + * The {@link HttpMethod} of the affordance. + * + * @return will never be {@literal null}. + */ + HttpMethod getMethod(); + + /** + * Metadata about the input payload. + * + * @return will never be {@literal null}. + */ + InputPayloadMetadata getInputMetadata(); + + /** + * The parameters of the affordance. + * + * @return will never be {@literal null}. + */ + List getQueryParameters(); + + /** + * Metadata about the output payload. + * + * @return will never be {@literal null}. + */ + PayloadMetadata getOutputMetadata(); +} diff --git a/src/main/java/org/springframework/hateoas/mediatype/TypeBasedPayloadMetadata.java b/src/main/java/org/springframework/hateoas/mediatype/TypeBasedPayloadMetadata.java index 44752a4a..8021feb8 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/TypeBasedPayloadMetadata.java +++ b/src/main/java/org/springframework/hateoas/mediatype/TypeBasedPayloadMetadata.java @@ -27,6 +27,8 @@ import org.springframework.core.ResolvableType; import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata; import org.springframework.hateoas.AffordanceModel.Named; import org.springframework.hateoas.AffordanceModel.PropertyMetadata; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; /** * {@link InputPayloadMetadata} implementation based on a Java type. @@ -37,12 +39,19 @@ class TypeBasedPayloadMetadata implements InputPayloadMetadata { private final ResolvableType type; private final SortedMap properties; + private final @Nullable MediaType mediaType; TypeBasedPayloadMetadata(ResolvableType type, Stream properties) { + this(type, new TreeMap<>( + properties.collect(Collectors.toMap(PropertyMetadata::getName, Function.identity()))), null); + } + + TypeBasedPayloadMetadata(ResolvableType type, SortedMap properties, + @Nullable MediaType mediaType) { this.type = type; - this.properties = new TreeMap<>( - properties.collect(Collectors.toMap(PropertyMetadata::getName, Function.identity()))); + this.properties = properties; + this.mediaType = mediaType; } /* @@ -81,4 +90,23 @@ class TypeBasedPayloadMetadata implements InputPayloadMetadata { ResolvableType getType() { return this.type; } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#withMediaType(org.springframework.http.MediaType) + */ + @Override + public InputPayloadMetadata withMediaType(@Nullable MediaType mediaType) { + return new TypeBasedPayloadMetadata(type, properties, mediaType); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#getMediaType() + */ + @Nullable + @Override + public MediaType getMediaType() { + return mediaType; + } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonAffordanceModel.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonAffordanceModel.java index 15eb862a..cb39c036 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonAffordanceModel.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonAffordanceModel.java @@ -24,9 +24,10 @@ import java.util.stream.Collectors; import org.springframework.hateoas.Affordance; import org.springframework.hateoas.AffordanceModel; -import org.springframework.hateoas.Link; import org.springframework.hateoas.QueryParameter; +import org.springframework.hateoas.mediatype.ConfiguredAffordance; import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; /** * {@link AffordanceModel} for Collection+JSON. @@ -42,10 +43,11 @@ class CollectionJsonAffordanceModel extends AffordanceModel { private final List inputProperties; private final List queryProperties; - public CollectionJsonAffordanceModel(String name, Link link, HttpMethod httpMethod, InputPayloadMetadata inputType, - List queryMethodParameters, PayloadMetadata outputType) { + CollectionJsonAffordanceModel(ConfiguredAffordance configured) { - super(name, link, httpMethod, inputType, queryMethodParameters, outputType); + super(configured.getNameOrDefault(), configured.getTarget(), + configured.getMethod(), configured.getInputMetadata(), configured.getQueryParameters(), + configured.getOutputMetadata()); this.inputProperties = determineInputs(); this.queryProperties = determineQueryProperties(); @@ -94,20 +96,35 @@ class CollectionJsonAffordanceModel extends AffordanceModel { return this.queryProperties; } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.AffordanceModel#equals(java.lang.Object) + */ @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { - if (this == o) + if (this == o) { return true; - if (!(o instanceof CollectionJsonAffordanceModel)) + } + + if (!(o instanceof CollectionJsonAffordanceModel)) { return false; - if (!super.equals(o)) + } + + if (!super.equals(o)) { return false; + } + CollectionJsonAffordanceModel that = (CollectionJsonAffordanceModel) o; + return Objects.equals(this.inputProperties, that.inputProperties) && Objects.equals(this.queryProperties, that.queryProperties); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.AffordanceModel#hashCode() + */ @Override public int hashCode() { return Objects.hash(super.hashCode(), this.inputProperties, this.queryProperties); diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonAffordanceModelFactory.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonAffordanceModelFactory.java index 0d5b96f3..c7d9458c 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonAffordanceModelFactory.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonAffordanceModelFactory.java @@ -15,34 +15,35 @@ */ package org.springframework.hateoas.mediatype.collectionjson; -import java.util.List; - import org.springframework.hateoas.AffordanceModel; -import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata; -import org.springframework.hateoas.AffordanceModel.PayloadMetadata; -import org.springframework.hateoas.Link; import org.springframework.hateoas.MediaTypes; -import org.springframework.hateoas.QueryParameter; import org.springframework.hateoas.mediatype.AffordanceModelFactory; -import org.springframework.http.HttpMethod; +import org.springframework.hateoas.mediatype.ConfiguredAffordance; import org.springframework.http.MediaType; /** * Factory for creating {@link CollectionJsonAffordanceModel}s. * * @author Greg Turnquist + * @author Oliver Drotbohm */ class CollectionJsonAffordanceModelFactory implements AffordanceModelFactory { - private final MediaType mediaType = MediaTypes.COLLECTION_JSON; - + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.AffordanceModelFactory#getAffordanceModel(org.springframework.hateoas.mediatype.ConfiguredAffordance) + */ @Override - public AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, - InputPayloadMetadata inputType, List queryMethodParameters, PayloadMetadata outputType) { - return new CollectionJsonAffordanceModel(name, link, httpMethod, inputType, queryMethodParameters, outputType); + public AffordanceModel getAffordanceModel(ConfiguredAffordance configured) { + return new CollectionJsonAffordanceModel(configured); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.AffordanceModelFactory#getMediaType() + */ + @Override public MediaType getMediaType() { - return this.mediaType; + return MediaTypes.COLLECTION_JSON; } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModel.java b/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModel.java index 2dfe1cec..2cbf2afd 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModel.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModel.java @@ -23,9 +23,8 @@ import java.util.Set; import java.util.stream.Collectors; import org.springframework.hateoas.AffordanceModel; -import org.springframework.hateoas.Link; import org.springframework.hateoas.MediaTypes; -import org.springframework.hateoas.QueryParameter; +import org.springframework.hateoas.mediatype.ConfiguredAffordance; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; @@ -46,9 +45,11 @@ class UberAffordanceModel extends AffordanceModel { private final List inputProperties; private final List queryProperties; - UberAffordanceModel(String name, Link link, HttpMethod httpMethod, InputPayloadMetadata inputType, - List queryMethodParameters, PayloadMetadata outputType) { - super(name, link, httpMethod, inputType, queryMethodParameters, outputType); + UberAffordanceModel(ConfiguredAffordance configured) { + + super(configured.getNameOrDefault(), configured.getTarget(), + configured.getMethod(), configured.getInputMetadata(), configured.getQueryParameters(), + configured.getOutputMetadata()); this.inputProperties = determineAffordanceInputs(); this.queryProperties = determineQueryProperties(); diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModelFactory.java b/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModelFactory.java index a6a0141f..2ec4f2dd 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModelFactory.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModelFactory.java @@ -15,16 +15,10 @@ */ package org.springframework.hateoas.mediatype.uber; -import java.util.List; - import org.springframework.hateoas.AffordanceModel; -import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata; -import org.springframework.hateoas.AffordanceModel.PayloadMetadata; -import org.springframework.hateoas.Link; import org.springframework.hateoas.MediaTypes; -import org.springframework.hateoas.QueryParameter; import org.springframework.hateoas.mediatype.AffordanceModelFactory; -import org.springframework.http.HttpMethod; +import org.springframework.hateoas.mediatype.ConfiguredAffordance; import org.springframework.http.MediaType; /** @@ -35,19 +29,21 @@ import org.springframework.http.MediaType; */ class UberAffordanceModelFactory implements AffordanceModelFactory { - private final MediaType mediaType = MediaTypes.UBER_JSON; + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.AffordanceModelFactory#getAffordanceModel(org.springframework.hateoas.mediatype.ConfiguredAffordance) + */ + @Override + public AffordanceModel getAffordanceModel(ConfiguredAffordance configured) { + return new UberAffordanceModel(configured); + } /* * (non-Javadoc) - * @see org.springframework.hateoas.AffordanceModelFactory#getAffordanceModel(java.lang.String, org.springframework.hateoas.Link, org.springframework.http.HttpMethod, org.springframework.hateoas.mediatype.PayloadMetadata, java.util.List, org.springframework.core.ResolvableType) + * @see org.springframework.hateoas.mediatype.AffordanceModelFactory#getMediaType() */ @Override - public AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, - InputPayloadMetadata inputType, List queryMethodParameters, PayloadMetadata outputType) { - return new UberAffordanceModel(name, link, httpMethod, inputType, queryMethodParameters, outputType); - } - public MediaType getMediaType() { - return this.mediaType; + return MediaTypes.UBER_JSON; } }