Skip to content

Support for HAL-FORMS value element #2039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class HalFormsConfiguration {
private final Map<Class<?>, String> patterns;
private final Consumer<ObjectMapper> objectMapperCustomizer;
private final HalFormsOptionsFactory options;
private final HalFormsValueFactory values;
private final List<MediaType> mediaTypes;

/**
Expand All @@ -61,24 +62,26 @@ public HalFormsConfiguration() {
* @param halConfiguration must not be {@literal null}.
*/
public HalFormsConfiguration(HalConfiguration halConfiguration) {
this(halConfiguration, new HashMap<>(), new HalFormsOptionsFactory(), __ -> {},
this(halConfiguration, new HashMap<>(), new HalFormsOptionsFactory(), new HalFormsValueFactory(), __ -> {},
Collections.singletonList(MediaTypes.HAL_FORMS_JSON));
}

private HalFormsConfiguration(HalConfiguration halConfiguration, Map<Class<?>, String> patterns,
HalFormsOptionsFactory options, @Nullable Consumer<ObjectMapper> objectMapperCustomizer,
HalFormsOptionsFactory options, HalFormsValueFactory values, @Nullable Consumer<ObjectMapper> objectMapperCustomizer,
List<MediaType> mediaTypes) {

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!");
Assert.notNull(values, "HalFormsValueFactory must not be null!");
Assert.notNull(mediaTypes, "Media types must not be null!");

this.halConfiguration = halConfiguration;
this.patterns = patterns;
this.objectMapperCustomizer = objectMapperCustomizer;
this.options = options;
this.values = values;
this.mediaTypes = new ArrayList<>(mediaTypes);
}

Expand All @@ -97,7 +100,7 @@ public HalFormsConfiguration withPattern(Class<?> type, String pattern) {
Map<Class<?>, String> newPatterns = new HashMap<>(patterns);
newPatterns.put(type, pattern);

return new HalFormsConfiguration(halConfiguration, newPatterns, options, objectMapperCustomizer, mediaTypes);
return new HalFormsConfiguration(halConfiguration, newPatterns, options, values, objectMapperCustomizer, mediaTypes);
}

/**
Expand All @@ -113,7 +116,7 @@ public HalFormsConfiguration withObjectMapperCustomizer(Consumer<ObjectMapper> o

return this.objectMapperCustomizer == objectMapperCustomizer //
? this //
: new HalFormsConfiguration(halConfiguration, patterns, options, objectMapperCustomizer, mediaTypes);
: new HalFormsConfiguration(halConfiguration, patterns, options, values, objectMapperCustomizer, mediaTypes);
}

/**
Expand All @@ -136,7 +139,7 @@ public HalFormsConfiguration withMediaType(MediaType mediaType) {
List<MediaType> newMediaTypes = new ArrayList<>(mediaTypes);
newMediaTypes.add(mediaTypes.size() - 1, mediaType);

return new HalFormsConfiguration(halConfiguration, patterns, options, objectMapperCustomizer, newMediaTypes);
return new HalFormsConfiguration(halConfiguration, patterns, options, values, objectMapperCustomizer, newMediaTypes);
}

/**
Expand Down Expand Up @@ -167,7 +170,14 @@ public HalFormsConfiguration customize(ObjectMapper mapper) {
public <T> HalFormsConfiguration withOptions(Class<T> type, String property,
Function<PropertyMetadata, HalFormsOptions> creator) {

return new HalFormsConfiguration(halConfiguration, patterns, options.withOptions(type, property, creator),
return new HalFormsConfiguration(halConfiguration, patterns, options.withOptions(type, property, creator), values,
objectMapperCustomizer, mediaTypes);
}

public <T> HalFormsConfiguration withValues(Class<T> type, String property,
Function<PropertyMetadata, String> creator) {

return new HalFormsConfiguration(halConfiguration, patterns, options, values.withValues(type, property, creator),
objectMapperCustomizer, mediaTypes);
}

Expand All @@ -189,6 +199,15 @@ HalFormsOptionsFactory getOptionsFactory() {
return options;
}

/**
* Returns the {@link HalFormsValueFactory} to look up value from payload and property metadata.
*
* @return will never be {@literal null}.
*/
HalFormsValueFactory getValuesFactory() {
return values;
}

/**
* Returns the regular expression pattern that is registered for the given type.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public List<HalFormsProperty> createProperties(HalFormsAffordanceModel model) {
}

HalFormsOptionsFactory optionsFactory = configuration.getOptionsFactory();
HalFormsValueFactory valuesFactory = configuration.getValuesFactory();

return model.createProperties((payload, metadata) -> {

Expand All @@ -95,7 +96,7 @@ public List<HalFormsProperty> createProperties(HalFormsAffordanceModel model) {
.withMaxLength(metadata.getMaxLength())
.withRegex(lookupRegex(metadata)) //
.withType(inputType) //
.withValue(options != null ? options.getSelectedValue() : null) //
.withValue(options != null ? options.getSelectedValue() : valuesFactory.getValue(payload, metadata)) //
.withOptions(options);

Function<String, I18nedPropertyMetadata> factory = I18nedPropertyMetadata.factory(payload, property);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2023 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;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
*
* Factory implementation to register creator functions to eventually create value from
* {@link AffordanceModel.PropertyMetadata} to decouple the registration (via {@link HalFormsConfiguration}) from the consumption during
* rendering.
*
* @author Réda Housni Alaoui
*/
class HalFormsValueFactory {

private final Map<Class<?>, Map<String, Function<AffordanceModel.PropertyMetadata, String>>> values;

/**
* Creates a new, empty {@link HalFormsValueFactory}.
*/
public HalFormsValueFactory() {
this.values = new HashMap<>();
}

/**
* Copy-constructor to keep {@link HalFormsValueFactory} immutable during registrations.
*
* @param values must not be {@literal null}.
*/
private HalFormsValueFactory(Map<Class<?>, Map<String, Function<AffordanceModel.PropertyMetadata, String>>> values) {
this.values = values;
}

/**
* Registers a {@link Function} to create a {@link String} instance from the given {@link AffordanceModel.PropertyMetadata}
* to supply value for the given property of the given type.
*
* @param type must not be {@literal null}.
*/
HalFormsValueFactory withValues(Class<?> type, String property,
Function<AffordanceModel.PropertyMetadata, String> 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<AffordanceModel.PropertyMetadata, String>>> values = new HashMap<>(this.values);

values.compute(type, (it, map) -> {

if (map == null) {
map = new HashMap<>();
}

map.put(property, creator);

return map;
});

return new HalFormsValueFactory(values);
}

/**
* Returns the value to be used for the property with the given {@link AffordanceModel.PayloadMetadata} and
* {@link AffordanceModel.PropertyMetadata}.
*
* @param payload must not be {@literal null}.
* @param property must not be {@literal null}.
*/
@Nullable
String getValue(AffordanceModel.PayloadMetadata payload, AffordanceModel.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<AffordanceModel.PropertyMetadata, String>> map = values.get(type);

if (map == null) {
return null;
}

Function<AffordanceModel.PropertyMetadata, String> function = map.get(name);

return function == null ? null : function.apply(property);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,28 @@ void rendersInlineOptions() {
});
}

@Test
void rendersValues() {
String value = "1234123412341234";

HalFormsConfiguration configuration = new HalFormsConfiguration() //
.withValues(PatternExample.class, "number", metadata -> value);

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.getValue()).isEqualTo(value);
});
}

@Test // #1510
void propagatesSelectedValueToProperty() {

Expand Down