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; } }