#1483 - Support for HAL-FORMS options element.
We now support defining the options element [0] for a HAL-FORMS property by registering a lookup of HalFormsOptions instances on HalFormsConfiguration. [0] https://rwcbook.github.io/hal-forms/#options-element
This commit is contained in:
@@ -207,6 +207,11 @@ public abstract class AffordanceModel {
|
||||
default Optional<PropertyMetadata> getPropertyMetadata(String name) {
|
||||
return stream().filter(it -> it.hasName(name)).findFirst();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
default Class<?> getType() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,6 +360,16 @@ public abstract class AffordanceModel {
|
||||
return mediaTypes;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#getType()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Class<?> getType() {
|
||||
return metadata.getType();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
@@ -391,7 +406,7 @@ public abstract class AffordanceModel {
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public interface PropertyMetadata {
|
||||
public interface PropertyMetadata extends Named {
|
||||
|
||||
/**
|
||||
* The name of the property.
|
||||
|
||||
@@ -325,17 +325,11 @@ public class Affordances implements AffordanceOperations {
|
||||
|
||||
String name = method.toString().toLowerCase();
|
||||
|
||||
ResolvableType type = TypeBasedPayloadMetadata.class.isInstance(inputMetdata) //
|
||||
Class<?> type = TypeBasedPayloadMetadata.class.isInstance(inputMetdata) //
|
||||
? TypeBasedPayloadMetadata.class.cast(inputMetdata).getType() //
|
||||
: null;
|
||||
|
||||
if (type == null) {
|
||||
return name;
|
||||
}
|
||||
|
||||
Class<?> resolvedType = type.resolve();
|
||||
|
||||
return resolvedType == null ? name : name.concat(resolvedType.getSimpleName());
|
||||
return type == null ? name : name.concat(type.getSimpleName());
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.io.IOException;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
|
||||
/**
|
||||
* A Jackson serializer triggering message resolution via a {@link MessageResolver} for {@link MessageSourceResolvable}
|
||||
* instances about to be serialized.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.3
|
||||
*/
|
||||
public class MessageSourceResolvableSerializer extends StdSerializer<MessageSourceResolvable> {
|
||||
|
||||
private static final long serialVersionUID = 4302540100251549622L;
|
||||
|
||||
private final MessageResolver resolver;
|
||||
|
||||
/**
|
||||
* Creates a new {@link MessageSourceResolvableSerializer} for the given {@link MessageResolver}.
|
||||
*
|
||||
* @param resolver must not be {@literal null}.
|
||||
*/
|
||||
public MessageSourceResolvableSerializer(MessageResolver resolver) {
|
||||
|
||||
super(MessageSourceResolvable.class);
|
||||
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("null")
|
||||
public void serialize(MessageSourceResolvable value, JsonGenerator gen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
gen.writeString(resolver.resolve(value));
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public class PropertyUtils {
|
||||
|
||||
return Object.class.equals(resolved) //
|
||||
? InputPayloadMetadata.NONE //
|
||||
: new TypeBasedPayloadMetadata(domainType, lookupExposedProperties(resolved));
|
||||
: new TypeBasedPayloadMetadata(resolved, lookupExposedProperties(resolved));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata;
|
||||
import org.springframework.hateoas.AffordanceModel.Named;
|
||||
import org.springframework.hateoas.AffordanceModel.PropertyMetadata;
|
||||
@@ -38,16 +37,16 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
class TypeBasedPayloadMetadata implements InputPayloadMetadata {
|
||||
|
||||
private final ResolvableType type;
|
||||
private final Class<?> type;
|
||||
private final SortedMap<String, PropertyMetadata> properties;
|
||||
private final List<MediaType> mediaTypes;
|
||||
|
||||
TypeBasedPayloadMetadata(ResolvableType type, Stream<PropertyMetadata> properties) {
|
||||
TypeBasedPayloadMetadata(Class<?> type, Stream<PropertyMetadata> properties) {
|
||||
this(type, new TreeMap<>(
|
||||
properties.collect(Collectors.toMap(PropertyMetadata::getName, Function.identity()))), Collections.emptyList());
|
||||
}
|
||||
|
||||
TypeBasedPayloadMetadata(ResolvableType type, SortedMap<String, PropertyMetadata> properties,
|
||||
TypeBasedPayloadMetadata(Class<?> type, SortedMap<String, PropertyMetadata> properties,
|
||||
List<MediaType> mediaTypes) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
@@ -86,13 +85,10 @@ class TypeBasedPayloadMetadata implements InputPayloadMetadata {
|
||||
*/
|
||||
@Override
|
||||
public List<String> getI18nCodes() {
|
||||
|
||||
Class<?> type = this.type.resolve(Object.class);
|
||||
|
||||
return Arrays.asList(type.getName(), type.getSimpleName());
|
||||
}
|
||||
|
||||
ResolvableType getType() {
|
||||
public Class<?> getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.springframework.hateoas.Links;
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
import org.springframework.hateoas.mediatype.ConfigurableHandlerInstantiator;
|
||||
import org.springframework.hateoas.mediatype.MessageResolver;
|
||||
import org.springframework.hateoas.mediatype.MessageSourceResolvableSerializer;
|
||||
import org.springframework.hateoas.mediatype.hal.HalConfiguration.RenderSingleLinks;
|
||||
import org.springframework.hateoas.server.LinkRelationProvider;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -742,6 +743,7 @@ public class Jackson2HalModule extends SimpleModule {
|
||||
|
||||
registerInstance(new HalResourcesSerializer(mapper, halConfiguration));
|
||||
registerInstance(new HalLinkListSerializer(curieProvider, mapper, resolver, halConfiguration));
|
||||
registerInstance(new MessageSourceResolvableSerializer(resolver));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,10 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.AffordanceModel.PropertyMetadata;
|
||||
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -38,6 +40,7 @@ public class HalFormsConfiguration {
|
||||
private final HalConfiguration halConfiguration;
|
||||
private final Map<Class<?>, String> patterns;
|
||||
private final Consumer<ObjectMapper> objectMapperCustomizer;
|
||||
private final HalFormsOptionsFactory options;
|
||||
|
||||
/**
|
||||
* Creates a new {@link HalFormsConfiguration} backed by a default {@link HalConfiguration}.
|
||||
@@ -52,19 +55,21 @@ public class HalFormsConfiguration {
|
||||
* @param halConfiguration must not be {@literal null}.
|
||||
*/
|
||||
public HalFormsConfiguration(HalConfiguration halConfiguration) {
|
||||
this(halConfiguration, new HashMap<>(), __ -> {});
|
||||
this(halConfiguration, new HashMap<>(), new HalFormsOptionsFactory(), __ -> {});
|
||||
}
|
||||
|
||||
private HalFormsConfiguration(HalConfiguration halConfiguration, Map<Class<?>, String> patterns,
|
||||
@Nullable Consumer<ObjectMapper> objectMapperCustomizer) {
|
||||
HalFormsOptionsFactory options, @Nullable Consumer<ObjectMapper> objectMapperCustomizer) {
|
||||
|
||||
Assert.notNull(halConfiguration, "HalConfiguration must not be null!");
|
||||
Assert.notNull(patterns, "Patterns must not be null!");
|
||||
Assert.notNull(objectMapperCustomizer, "ObjectMapper customizer must not be null!");
|
||||
Assert.notNull(options, "HalFormsSuggests must not be null!");
|
||||
|
||||
this.halConfiguration = halConfiguration;
|
||||
this.patterns = patterns;
|
||||
this.objectMapperCustomizer = objectMapperCustomizer;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +106,7 @@ public class HalFormsConfiguration {
|
||||
Map<Class<?>, String> newPatterns = new HashMap<>(patterns);
|
||||
newPatterns.put(type, pattern);
|
||||
|
||||
return new HalFormsConfiguration(halConfiguration, newPatterns, objectMapperCustomizer);
|
||||
return new HalFormsConfiguration(halConfiguration, newPatterns, options, objectMapperCustomizer);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +122,7 @@ public class HalFormsConfiguration {
|
||||
|
||||
return this.objectMapperCustomizer == objectMapperCustomizer //
|
||||
? this //
|
||||
: new HalFormsConfiguration(halConfiguration, patterns, objectMapperCustomizer);
|
||||
: new HalFormsConfiguration(halConfiguration, patterns, options, objectMapperCustomizer);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,10 +141,40 @@ public class HalFormsConfiguration {
|
||||
return this;
|
||||
}
|
||||
|
||||
public HalConfiguration getHalConfiguration() {
|
||||
/**
|
||||
* Returns a new {@link HalFormsConfiguration} with the given
|
||||
*
|
||||
* @param <T>
|
||||
* @param type the
|
||||
* @param property
|
||||
* @param creator
|
||||
* @return
|
||||
*/
|
||||
public <T> HalFormsConfiguration withOptions(Class<T> type, String property,
|
||||
Function<PropertyMetadata, HalFormsOptions> creator) {
|
||||
|
||||
return new HalFormsConfiguration(halConfiguration, patterns, options.withOptions(type, property, creator),
|
||||
objectMapperCustomizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying {@link HalConfiguration}.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
HalConfiguration getHalConfiguration() {
|
||||
return halConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link HalFormsOptionsFactory} to look up {@link HalFormsOptions} from payload and property metadata.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
HalFormsOptionsFactory getOptionsFactory() {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the regular expression pattern that is registered for the given type.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* 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.hal.forms;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Representation of HAL-FORMS {@code options} attribute.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @see https://rwcbook.github.io/hal-forms/#options-element
|
||||
* @since 1.3
|
||||
*/
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
public interface HalFormsOptions {
|
||||
|
||||
/**
|
||||
* Creates a new {@link Inline} options representation listing the given values.
|
||||
*
|
||||
* @param values must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static Inline inline(Object... values) {
|
||||
|
||||
Assert.notNull(values, "Values must not be null!");
|
||||
|
||||
return inline(Arrays.asList(values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Inline} options representation listing the given collection of values.
|
||||
*
|
||||
* @param values must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static Inline inline(Collection<Object> values) {
|
||||
|
||||
Assert.notNull(values, "Values must not be null!");
|
||||
|
||||
return new Inline(values, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Remote} options representation using the given {@link Link}.
|
||||
*
|
||||
* @param link must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static Remote remote(Link link) {
|
||||
|
||||
Assert.notNull(link, "Link must not be null!");
|
||||
|
||||
return new Remote(link, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Remote} options representation using the given href.
|
||||
*
|
||||
* @param href must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static Remote remote(String href) {
|
||||
|
||||
Assert.hasText(href, "Href must not by null or empty!");
|
||||
|
||||
return remote(Link.of(href));
|
||||
}
|
||||
|
||||
/**
|
||||
* The field to look up the prompt from.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
String getPromptField();
|
||||
|
||||
/**
|
||||
* The field to use as the value to be sent.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
String getValueField();
|
||||
|
||||
/**
|
||||
* Returns the minimum number of items to be selected.
|
||||
*
|
||||
* @return {@literal null}, 0 or a positive {@link Long}.
|
||||
*/
|
||||
@Nullable
|
||||
Long getMinItems();
|
||||
|
||||
/**
|
||||
* Returns the maximum number of items to be selected.
|
||||
*
|
||||
* @return {@literal null} or a positive {@link Long}.
|
||||
*/
|
||||
@Nullable
|
||||
Long getMaxItems();
|
||||
|
||||
public static abstract class AbstractHalFormsOptions<T extends AbstractHalFormsOptions<T>>
|
||||
implements HalFormsOptions {
|
||||
|
||||
private final @Nullable String promptField, valueField;
|
||||
private final @Nullable Long minItems, maxItems;
|
||||
|
||||
protected AbstractHalFormsOptions(@Nullable String promptRef, @Nullable String valueRef, @Nullable Long minItems,
|
||||
@Nullable Long maxItems) {
|
||||
|
||||
Assert.isTrue(minItems == null || minItems >= 0, "MinItems must be greater than or equal to 0!");
|
||||
|
||||
this.promptField = promptRef;
|
||||
this.valueField = valueRef;
|
||||
this.minItems = minItems;
|
||||
this.maxItems = maxItems;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.mediatype.hal.forms.HalFormsOptions#getPromptRef()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public String getPromptField() {
|
||||
return promptField;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.mediatype.hal.forms.HalFormsOptions#getValueField()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public String getValueField() {
|
||||
return valueField;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.mediatype.hal.forms.HalFormsOptions#getMinItems()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Long getMinItems() {
|
||||
return minItems;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.mediatype.hal.forms.HalFormsOptions#getMaxItems()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Long getMaxItems() {
|
||||
return maxItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the given field to be used as prompt field.
|
||||
*
|
||||
* @param promptField must either be {@literal null} or actually have text.
|
||||
* @return
|
||||
*/
|
||||
public T withPromptField(String promptField) {
|
||||
|
||||
if (promptField != null && !StringUtils.hasText(promptField)) {
|
||||
throw new IllegalArgumentException("Prompt field has to either be null or actually have text!");
|
||||
}
|
||||
|
||||
return with(promptField, valueField, minItems, maxItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the given field to be used as value field.
|
||||
*
|
||||
* @param valueField must either be {@literal null} or actually have text.
|
||||
* @return
|
||||
*/
|
||||
public T withValueField(String valueField) {
|
||||
|
||||
if (valueField != null && !StringUtils.hasText(valueField)) {
|
||||
throw new IllegalArgumentException("Value field has to either be null or actually have text!");
|
||||
}
|
||||
|
||||
return with(promptField, valueField, minItems, maxItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the minimum number of items to be selected.
|
||||
*
|
||||
* @param minItems must be {@literal null} or greater than or equal to zero.
|
||||
* @return
|
||||
*/
|
||||
public T withMinItems(Long minItems) {
|
||||
|
||||
if (minItems != null && minItems < 0) {
|
||||
throw new IllegalArgumentException("minItems has to be null or greater or equal to zero!");
|
||||
}
|
||||
|
||||
return with(promptField, valueField, minItems, maxItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the maximum number of items to be selected.
|
||||
*
|
||||
* @param maxItems must be {@literal null} or greater than zero.
|
||||
* @return
|
||||
*/
|
||||
public T withMaxItems(Long maxItems) {
|
||||
|
||||
if (maxItems != null && maxItems <= 0) {
|
||||
throw new IllegalArgumentException("maxItems has to be null or greater than zero!");
|
||||
}
|
||||
|
||||
return with(promptField, valueField, minItems, maxItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new concrete {@link AbstractHalFormsOptions}
|
||||
*
|
||||
* @param promptRef
|
||||
* @param valueRef
|
||||
* @param minItems
|
||||
* @param maxItems
|
||||
* @return
|
||||
*/
|
||||
protected abstract T with(@Nullable String promptRef, @Nullable String valueRef, @Nullable Long minItems,
|
||||
@Nullable Long maxItems);
|
||||
}
|
||||
|
||||
public static class Inline extends AbstractHalFormsOptions<Inline> {
|
||||
|
||||
private final Collection<Object> inline;
|
||||
|
||||
/**
|
||||
* @param values
|
||||
* @param promptRef
|
||||
* @param valueRef
|
||||
*/
|
||||
private Inline(Collection<Object> values, @Nullable String promptRef, @Nullable String valueRef,
|
||||
@Nullable Long minItems, @Nullable Long maxItems) {
|
||||
|
||||
super(promptRef, valueRef, minItems, maxItems);
|
||||
|
||||
Assert.notNull(values, "Values must not be null!");
|
||||
|
||||
this.inline = values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inline values.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@JsonProperty
|
||||
public Collection<Object> getInline() {
|
||||
return inline;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.mediatype.hal.forms.HalFormsOptions.AbstractHalFormsOptions#with(java.lang.String, java.lang.String, java.lang.Long, java.lang.Long)
|
||||
*/
|
||||
@Override
|
||||
protected Inline with(@Nullable String promptRef, @Nullable String valueRef, @Nullable Long minItems,
|
||||
@Nullable Long maxItems) {
|
||||
return new Inline(inline, promptRef, valueRef, minItems, maxItems);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of a remote options element.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public static class Remote extends AbstractHalFormsOptions<Remote> {
|
||||
|
||||
private final Link link;
|
||||
|
||||
private Remote(Link link, @Nullable String promptRef, @Nullable String valueRef, @Nullable Long minItems,
|
||||
@Nullable Long maxItems) {
|
||||
|
||||
super(promptRef, valueRef, minItems, maxItems);
|
||||
|
||||
Assert.notNull(link, "Link must not be null!");
|
||||
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Link} pointing to the resource returning option values.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@JsonProperty
|
||||
public Link getLink() {
|
||||
return link;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.mediatype.hal.forms.HalFormsOptions.Foo#withFoo(java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
protected Remote with(@Nullable String promptRef, @Nullable String valueRef, @Nullable Long minItems,
|
||||
@Nullable Long maxItems) {
|
||||
return new Remote(link, promptRef, valueRef, minItems, maxItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.hal.forms;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.hateoas.AffordanceModel.PayloadMetadata;
|
||||
import org.springframework.hateoas.AffordanceModel.PropertyMetadata;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Factory implementation to register creator functions to eventually create {@link HalFormsOptions} from
|
||||
* {@link PropertyMetadata} to decouple the registration (via {@link HalFormsConfiguration}) from the consumption during
|
||||
* rendering.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.3
|
||||
*/
|
||||
class HalFormsOptionsFactory {
|
||||
|
||||
private final Map<Class<?>, Map<String, Function<PropertyMetadata, HalFormsOptions>>> options;
|
||||
|
||||
/**
|
||||
* Creates a new, empty {@link HalFormsOptionsFactory}.
|
||||
*/
|
||||
public HalFormsOptionsFactory() {
|
||||
this.options = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy-constructor to keep {@link HalFormsConfiguration} immutable during registrations.
|
||||
*
|
||||
* @param options must not be {@literal null}.
|
||||
*/
|
||||
private HalFormsOptionsFactory(Map<Class<?>, Map<String, Function<PropertyMetadata, HalFormsOptions>>> options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link Function} to create a {@link HalFormsOptions} instance from the given {@link PropertyMetadata}
|
||||
* to supply options for the given property of the given type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @param property
|
||||
* @param creator
|
||||
* @return
|
||||
* @see HalFormsOptions#inline(Object...)
|
||||
* @see HalFormsOptions#remote(org.springframework.hateoas.Link)
|
||||
*/
|
||||
HalFormsOptionsFactory withOptions(Class<?> type, String property,
|
||||
Function<PropertyMetadata, HalFormsOptions> creator) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
Assert.hasText(property, "Property must not be null or empty!");
|
||||
Assert.notNull(creator, "Creator function must not be null!");
|
||||
|
||||
Map<Class<?>, Map<String, Function<PropertyMetadata, HalFormsOptions>>> options = new HashMap<>(this.options);
|
||||
|
||||
options.compute(type, (it, map) -> {
|
||||
|
||||
if (map == null) {
|
||||
map = new HashMap<>();
|
||||
}
|
||||
|
||||
map.put(property, creator);
|
||||
|
||||
return map;
|
||||
});
|
||||
|
||||
return new HalFormsOptionsFactory(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link HalFormsOptions} to be used for the property with the given {@link PayloadMetadata} and
|
||||
* {@link PropertyMetadata}.
|
||||
*
|
||||
* @param payload must not be {@literal null}.
|
||||
* @param property must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
HalFormsOptions getOptions(PayloadMetadata payload, PropertyMetadata property) {
|
||||
|
||||
Assert.notNull(payload, "Payload metadata must not be null!");
|
||||
Assert.notNull(property, "Property metadata must not be null!");
|
||||
|
||||
Class<?> type = payload.getType();
|
||||
String name = property.getName();
|
||||
|
||||
Map<String, Function<PropertyMetadata, HalFormsOptions>> map = options.get(type);
|
||||
|
||||
if (map == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Function<PropertyMetadata, HalFormsOptions> function = map.get(name);
|
||||
|
||||
return function == null ? null : function.apply(property);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.hal.forms;
|
||||
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.hateoas.mediatype.MessageSourceResolvableSerializer;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* A value object to describe prompted values for HAL-FORMS {@code options}' {@code inline} attribute or responses of
|
||||
* resources pointed to by the {@code link} object.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @see https://rwcbook.github.io/hal-forms/#options-element
|
||||
* @since 1.3
|
||||
*/
|
||||
public class HalFormsPromptedValue {
|
||||
|
||||
private final Object prompt;
|
||||
private final Object value;
|
||||
|
||||
/**
|
||||
* Creates a new {@link HalFormsPromptedValue} for the given prompt and value.
|
||||
*
|
||||
* @param prompt must not be {@literal null}.
|
||||
* @param value must not be {@literal null}.
|
||||
*/
|
||||
private HalFormsPromptedValue(Object prompt, Object value) {
|
||||
|
||||
Assert.notNull(prompt, "Prompt must not be null!");
|
||||
Assert.notNull(value, "Value must not be null!");
|
||||
|
||||
this.prompt = prompt;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HalFormsPromptedValue} with the given plain prompt and value.
|
||||
*
|
||||
* @param prompt must not be {@literal null} or empty.
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public static HalFormsPromptedValue of(String prompt, Object value) {
|
||||
|
||||
Assert.hasText(prompt, "Prompt must not be null or empty!");
|
||||
Assert.notNull(value, "Value must not be null!");
|
||||
|
||||
return new HalFormsPromptedValue(prompt, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HalFormsPromptedValue} with the given prompt key to be used for i18nization and value.
|
||||
*
|
||||
* @param promptKey must not be {@literal null} or empty.
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public static HalFormsPromptedValue ofI18ned(String promptKey, Object value) {
|
||||
|
||||
Assert.hasText(promptKey, "Prompt key must not be null or empty!");
|
||||
Assert.notNull(value, "Value must not be null!");
|
||||
|
||||
return new HalFormsPromptedValue(new I18nizedPrompt(promptKey, value), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the prompt to be used. Can be a pre-resolved {@link String} or a value to be resolved into a String during
|
||||
* serialization.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
@JsonProperty
|
||||
public Object getPrompt() {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
@JsonProperty
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for a prompt to be i18ned via a {@link MessageSourceResolvableSerializer} during serialization.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@JsonSerialize(using = MessageSourceResolvableSerializer.class)
|
||||
private static class I18nizedPrompt extends DefaultMessageSourceResolvable {
|
||||
|
||||
private static final long serialVersionUID = 7262804826421266153L;
|
||||
|
||||
I18nizedPrompt(String promptKey, Object value) {
|
||||
super(new String[] { promptKey }, new Object[] { value }, promptKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ final class HalFormsProperty implements Named {
|
||||
private final @JsonInclude(Include.NON_DEFAULT) boolean readOnly, required;
|
||||
private final @Nullable Long min, max, minLength, maxLength;
|
||||
private final @Nullable HtmlInputType type;
|
||||
private final @Nullable HalFormsOptions options;
|
||||
|
||||
HalFormsProperty() {
|
||||
|
||||
@@ -60,11 +61,13 @@ final class HalFormsProperty implements Named {
|
||||
this.minLength = null;
|
||||
this.maxLength = null;
|
||||
this.type = null;
|
||||
this.options = null;
|
||||
}
|
||||
|
||||
private HalFormsProperty(String name, boolean readOnly, String value, String prompt, String regex, boolean templated,
|
||||
boolean required, boolean multi, String placeholder, @Nullable Long min, @Nullable Long max,
|
||||
@Nullable Long minLength, @Nullable Long maxLength, @Nullable HtmlInputType type) {
|
||||
@Nullable Long minLength, @Nullable Long maxLength, @Nullable HtmlInputType type,
|
||||
@Nullable HalFormsOptions options) {
|
||||
|
||||
Assert.notNull(name, "name must not be null!");
|
||||
|
||||
@@ -82,6 +85,7 @@ final class HalFormsProperty implements Named {
|
||||
this.minLength = minLength;
|
||||
this.maxLength = maxLength;
|
||||
this.type = type;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +110,7 @@ final class HalFormsProperty implements Named {
|
||||
|
||||
return this.name == name ? this
|
||||
: new HalFormsProperty(name, this.readOnly, this.value, this.prompt, this.regex, this.templated, this.required,
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +123,7 @@ final class HalFormsProperty implements Named {
|
||||
|
||||
return this.readOnly == readOnly ? this
|
||||
: new HalFormsProperty(this.name, readOnly, this.value, this.prompt, this.regex, this.templated, this.required,
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +136,7 @@ final class HalFormsProperty implements Named {
|
||||
|
||||
return this.value == value ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, value, this.prompt, this.regex, this.templated, this.required,
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +149,7 @@ final class HalFormsProperty implements Named {
|
||||
|
||||
return this.prompt == prompt ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, prompt, this.regex, this.templated, this.required,
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +162,7 @@ final class HalFormsProperty implements Named {
|
||||
|
||||
return this.regex == regex ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, regex, this.templated, this.required,
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +185,7 @@ final class HalFormsProperty implements Named {
|
||||
|
||||
return this.templated == templated ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, templated, this.required,
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,7 +198,7 @@ final class HalFormsProperty implements Named {
|
||||
|
||||
return this.required == required ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated, required,
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,7 +211,8 @@ final class HalFormsProperty implements Named {
|
||||
|
||||
return this.multi == multi ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type);
|
||||
this.required, multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type,
|
||||
this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,72 +225,93 @@ final class HalFormsProperty implements Named {
|
||||
|
||||
return this.placeholder == placeholder ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, placeholder, this.min, this.max, this.minLength, this.maxLength, this.type);
|
||||
this.required, this.multi, placeholder, this.min, this.max, this.minLength, this.maxLength, this.type,
|
||||
this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing {@literal min}.
|
||||
*
|
||||
* @param min
|
||||
* @return
|
||||
* @param min can be {@literal null}
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
HalFormsProperty withMin(@Nullable Long min) {
|
||||
|
||||
return Objects.equals(this.min, min) ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, this.placeholder, min, this.max, this.minLength, this.maxLength, this.type);
|
||||
this.required, this.multi, this.placeholder, min, this.max, this.minLength, this.maxLength, this.type,
|
||||
this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing {@literal max}.
|
||||
*
|
||||
* @param max
|
||||
* @return
|
||||
* @param max can be {@literal null}
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
HalFormsProperty withMax(@Nullable Long max) {
|
||||
|
||||
return Objects.equals(this.max, max) ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, this.placeholder, this.min, max, this.minLength, this.maxLength, this.type);
|
||||
this.required, this.multi, this.placeholder, this.min, max, this.minLength, this.maxLength, this.type,
|
||||
this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing {@literal minLength}.
|
||||
*
|
||||
* @param minLength
|
||||
* @return
|
||||
* @param minLength can be {@literal null}
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
HalFormsProperty withMinLength(@Nullable Long minLength) {
|
||||
|
||||
return Objects.equals(this.minLength, minLength) ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, this.placeholder, this.min, this.max, minLength, this.maxLength, this.type);
|
||||
this.required, this.multi, this.placeholder, this.min, this.max, minLength, this.maxLength, this.type,
|
||||
this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing {@literal maxLength}.
|
||||
*
|
||||
* @param maxLength
|
||||
* @return
|
||||
* @param maxLength can be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
HalFormsProperty withMaxLength(@Nullable Long maxLength) {
|
||||
|
||||
return Objects.equals(this.maxLength, maxLength) ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, this.placeholder, this.min, this.max, this.minLength, maxLength, this.type);
|
||||
this.required, this.multi, this.placeholder, this.min, this.max, this.minLength, maxLength, this.type,
|
||||
this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HalFormsProperty} by copying attributes and replacing {@literal type}.
|
||||
*
|
||||
* @param type
|
||||
* @return
|
||||
* @param type can be {@literal null}
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
HalFormsProperty withType(@Nullable HtmlInputType type) {
|
||||
|
||||
return Objects.equals(this.type, type) ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, type);
|
||||
this.required, this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, type,
|
||||
this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HalFormsProperty} by copying attributes and replacing {@literal options}.
|
||||
*
|
||||
* @param options can be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
HalFormsProperty withOptions(@Nullable HalFormsOptions options) {
|
||||
|
||||
return Objects.equals(this.options, options) ? this
|
||||
: new HalFormsProperty(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated,
|
||||
this.required, this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type,
|
||||
options);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -388,6 +414,15 @@ final class HalFormsProperty implements Named {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the suggest
|
||||
*/
|
||||
@Nullable
|
||||
@JsonProperty
|
||||
HalFormsOptions getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
@@ -411,7 +446,9 @@ final class HalFormsProperty implements Named {
|
||||
&& Objects.equals(this.value, that.value) //
|
||||
&& Objects.equals(this.prompt, that.prompt) //
|
||||
&& Objects.equals(this.regex, that.regex) //
|
||||
&& Objects.equals(this.placeholder, that.placeholder);
|
||||
&& Objects.equals(this.placeholder, that.placeholder) //
|
||||
&& Objects.equals(this.type, that.type) //
|
||||
&& Objects.equals(this.options, that.options);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -422,7 +459,7 @@ final class HalFormsProperty implements Named {
|
||||
public int hashCode() {
|
||||
|
||||
return Objects.hash(this.name, this.readOnly, this.value, this.prompt, this.regex, this.templated, this.required,
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength);
|
||||
this.multi, this.placeholder, this.min, this.max, this.minLength, this.maxLength, this.type, this.options);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -445,6 +482,8 @@ final class HalFormsProperty implements Named {
|
||||
+ ", max=" + this.max //
|
||||
+ ", minLength=" + this.minLength //
|
||||
+ ", maxLength=" + this.maxLength //
|
||||
+ ", type=" + this.type //
|
||||
+ ", options=" + this.options //
|
||||
+ ")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ class HalFormsPropertyFactory {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
HalFormsOptionsFactory options = configuration.getOptionsFactory();
|
||||
|
||||
return model.createProperties((payload, metadata) -> {
|
||||
|
||||
String inputTypeSource = metadata.getInputType();
|
||||
@@ -94,7 +96,8 @@ class HalFormsPropertyFactory {
|
||||
.withMinLength(metadata.getMinLength())
|
||||
.withMaxLength(metadata.getMaxLength())
|
||||
.withRegex(lookupRegex(metadata)) //
|
||||
.withType(inputType);
|
||||
.withType(inputType) //
|
||||
.withOptions(options.getOptions(payload, metadata));
|
||||
|
||||
Function<String, I18nedPropertyMetadata> factory = I18nedPropertyMetadata.factory(payload, property);
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.util.stream.Stream;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.AffordanceModel.PayloadMetadata;
|
||||
@@ -114,7 +113,7 @@ public class AffordancesUnitTests {
|
||||
public PayloadMetadataAssert isBackedBy(Class<?> type) {
|
||||
|
||||
Assertions.assertThat(actual).isInstanceOfSatisfying(TypeBasedPayloadMetadata.class, it -> {
|
||||
Assertions.assertThat(it.getType()).isEqualTo(ResolvableType.forClass(type));
|
||||
Assertions.assertThat(it.getType()).isEqualTo(type);
|
||||
});
|
||||
|
||||
return this;
|
||||
|
||||
@@ -19,6 +19,8 @@ import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -155,6 +157,31 @@ class HalFormsTemplateBuilderUnitTest {
|
||||
assertThat(templates.get("default").getContentType()).isEqualTo(mediaType.toString());
|
||||
}
|
||||
|
||||
@Test // #1483
|
||||
void rendersRegisteredSuggest() {
|
||||
|
||||
List<Object> values = Arrays.asList("1234123412341234", "4321432143214321");
|
||||
|
||||
HalFormsConfiguration configuration = new HalFormsConfiguration()
|
||||
.withOptions(PatternExample.class, "number", metadata -> HalFormsOptions.inline(values));
|
||||
|
||||
RepresentationModel<?> models = new RepresentationModel<>(
|
||||
Affordances.of(Link.of("/example", LinkRelation.of("create")))
|
||||
.afford(HttpMethod.POST)
|
||||
.withInput(PatternExample.class)
|
||||
.toLink());
|
||||
|
||||
Map<String, HalFormsTemplate> templates = new HalFormsTemplateBuilder(configuration, MessageResolver.DEFAULTS_ONLY)
|
||||
.findTemplates(models);
|
||||
|
||||
assertThat(templates.get("default").getPropertyByName("number"))
|
||||
.hasValueSatisfying(it -> {
|
||||
assertThat(it.getOptions()).isNotNull()
|
||||
.isInstanceOfSatisfying(HalFormsOptions.Inline.class,
|
||||
inline -> assertThat(inline.getInline()).isEqualTo(values));
|
||||
});
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class PatternExample extends RepresentationModel<PatternExample> {
|
||||
|
||||
|
||||
@@ -62,12 +62,15 @@ import org.springframework.hateoas.mediatype.hal.HalTestUtils;
|
||||
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
|
||||
import org.springframework.hateoas.mediatype.hal.SimpleAnnotatedPojo;
|
||||
import org.springframework.hateoas.mediatype.hal.SimplePojo;
|
||||
import org.springframework.hateoas.mediatype.hal.forms.HalFormsOptions.Inline;
|
||||
import org.springframework.hateoas.mediatype.hal.forms.HalFormsOptions.Remote;
|
||||
import org.springframework.hateoas.server.LinkRelationProvider;
|
||||
import org.springframework.hateoas.server.core.AnnotationLinkRelationProvider;
|
||||
import org.springframework.hateoas.server.core.DelegatingLinkRelationProvider;
|
||||
import org.springframework.hateoas.server.core.EmbeddedWrappers;
|
||||
import org.springframework.hateoas.support.EmployeeResource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
@@ -75,6 +78,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.jayway.jsonpath.PathNotFoundException;
|
||||
|
||||
@@ -558,6 +562,34 @@ class Jackson2HalFormsIntegrationTest {
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test // #1483
|
||||
void rendersPromptedOptionsValues() throws Exception {
|
||||
|
||||
Inline inline = HalFormsOptions.inline(HalFormsPromptedValue.ofI18ned("some.prompt", "myValue"));
|
||||
|
||||
StaticMessageSource source = new StaticMessageSource();
|
||||
source.addMessage("some.prompt", Locale.US, "My Prompt");
|
||||
|
||||
ContextualMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source);
|
||||
|
||||
assertThat(JsonPath.parse(mapper.writeObject(inline)).read("$.inline[0].prompt", String.class))
|
||||
.isEqualTo("My Prompt");
|
||||
}
|
||||
|
||||
@Test // #1483
|
||||
void rendersRemoteOptions() {
|
||||
|
||||
Link link = Link.of("/foo{?bar}").withType(MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
Remote remote = HalFormsOptions.remote(link);
|
||||
|
||||
DocumentContext result = JsonPath.parse(getCuriedObjectMapper().writeObject(remote));
|
||||
|
||||
assertThat(result.read("$.link.href", String.class)).isEqualTo("/foo{?bar}");
|
||||
assertThat(result.read("$.link.type", String.class)).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertThat(result.read("$.link.templated", boolean.class)).isTrue();
|
||||
}
|
||||
|
||||
private void assertThatPathDoesNotExist(Object toMarshall, String path) throws Exception {
|
||||
|
||||
String json = getCuriedObjectMapper().writeObject(toMarshall);
|
||||
|
||||
Reference in New Issue
Block a user