Skip to content

Commit 6884846

Browse files
committed
Support for HAL-FORMS value element
1 parent a796509 commit 6884846

File tree

4 files changed

+159
-7
lines changed

4 files changed

+159
-7
lines changed

src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsConfiguration.java

+25-6
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class HalFormsConfiguration {
4646
private final Map<Class<?>, String> patterns;
4747
private final Consumer<ObjectMapper> objectMapperCustomizer;
4848
private final HalFormsOptionsFactory options;
49+
private final HalFormsValueFactory values;
4950
private final List<MediaType> mediaTypes;
5051

5152
/**
@@ -61,24 +62,26 @@ public HalFormsConfiguration() {
6162
* @param halConfiguration must not be {@literal null}.
6263
*/
6364
public HalFormsConfiguration(HalConfiguration halConfiguration) {
64-
this(halConfiguration, new HashMap<>(), new HalFormsOptionsFactory(), __ -> {},
65+
this(halConfiguration, new HashMap<>(), new HalFormsOptionsFactory(), new HalFormsValueFactory(), __ -> {},
6566
Collections.singletonList(MediaTypes.HAL_FORMS_JSON));
6667
}
6768

6869
private HalFormsConfiguration(HalConfiguration halConfiguration, Map<Class<?>, String> patterns,
69-
HalFormsOptionsFactory options, @Nullable Consumer<ObjectMapper> objectMapperCustomizer,
70+
HalFormsOptionsFactory options, HalFormsValueFactory values, @Nullable Consumer<ObjectMapper> objectMapperCustomizer,
7071
List<MediaType> mediaTypes) {
7172

7273
Assert.notNull(halConfiguration, "HalConfiguration must not be null!");
7374
Assert.notNull(patterns, "Patterns must not be null!");
7475
Assert.notNull(objectMapperCustomizer, "ObjectMapper customizer must not be null!");
7576
Assert.notNull(options, "HalFormsSuggests must not be null!");
77+
Assert.notNull(values, "HalFormsValueFactory must not be null!");
7678
Assert.notNull(mediaTypes, "Media types must not be null!");
7779

7880
this.halConfiguration = halConfiguration;
7981
this.patterns = patterns;
8082
this.objectMapperCustomizer = objectMapperCustomizer;
8183
this.options = options;
84+
this.values = values;
8285
this.mediaTypes = new ArrayList<>(mediaTypes);
8386
}
8487

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

100-
return new HalFormsConfiguration(halConfiguration, newPatterns, options, objectMapperCustomizer, mediaTypes);
103+
return new HalFormsConfiguration(halConfiguration, newPatterns, options, values, objectMapperCustomizer, mediaTypes);
101104
}
102105

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

114117
return this.objectMapperCustomizer == objectMapperCustomizer //
115118
? this //
116-
: new HalFormsConfiguration(halConfiguration, patterns, options, objectMapperCustomizer, mediaTypes);
119+
: new HalFormsConfiguration(halConfiguration, patterns, options, values, objectMapperCustomizer, mediaTypes);
117120
}
118121

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

139-
return new HalFormsConfiguration(halConfiguration, patterns, options, objectMapperCustomizer, newMediaTypes);
142+
return new HalFormsConfiguration(halConfiguration, patterns, options, values, objectMapperCustomizer, newMediaTypes);
140143
}
141144

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

170-
return new HalFormsConfiguration(halConfiguration, patterns, options.withOptions(type, property, creator),
173+
return new HalFormsConfiguration(halConfiguration, patterns, options.withOptions(type, property, creator), values,
174+
objectMapperCustomizer, mediaTypes);
175+
}
176+
177+
public <T> HalFormsConfiguration withValues(Class<T> type, String property,
178+
Function<PropertyMetadata, String> creator) {
179+
180+
return new HalFormsConfiguration(halConfiguration, patterns, options, values.withValues(type, property, creator),
171181
objectMapperCustomizer, mediaTypes);
172182
}
173183

@@ -189,6 +199,15 @@ HalFormsOptionsFactory getOptionsFactory() {
189199
return options;
190200
}
191201

202+
/**
203+
* Returns the {@link HalFormsValueFactory} to look up value from payload and property metadata.
204+
*
205+
* @return will never be {@literal null}.
206+
*/
207+
HalFormsValueFactory getValuesFactory() {
208+
return values;
209+
}
210+
192211
/**
193212
* Returns the regular expression pattern that is registered for the given type.
194213
*

src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsPropertyFactory.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public List<HalFormsProperty> createProperties(HalFormsAffordanceModel model) {
7979
}
8080

8181
HalFormsOptionsFactory optionsFactory = configuration.getOptionsFactory();
82+
HalFormsValueFactory valuesFactory = configuration.getValuesFactory();
8283

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

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

101102
Function<String, I18nedPropertyMetadata> factory = I18nedPropertyMetadata.factory(payload, property);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.mediatype.hal.forms;
17+
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
import java.util.function.Function;
21+
22+
import org.springframework.hateoas.AffordanceModel;
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
*
28+
* Factory implementation to register creator functions to eventually create value from
29+
* {@link AffordanceModel.PropertyMetadata} to decouple the registration (via {@link HalFormsConfiguration}) from the consumption during
30+
* rendering.
31+
*
32+
* @author Réda Housni Alaoui
33+
*/
34+
class HalFormsValueFactory {
35+
36+
private final Map<Class<?>, Map<String, Function<AffordanceModel.PropertyMetadata, String>>> values;
37+
38+
/**
39+
* Creates a new, empty {@link HalFormsValueFactory}.
40+
*/
41+
public HalFormsValueFactory() {
42+
this.values = new HashMap<>();
43+
}
44+
45+
/**
46+
* Copy-constructor to keep {@link HalFormsValueFactory} immutable during registrations.
47+
*
48+
* @param values must not be {@literal null}.
49+
*/
50+
private HalFormsValueFactory(Map<Class<?>, Map<String, Function<AffordanceModel.PropertyMetadata, String>>> values) {
51+
this.values = values;
52+
}
53+
54+
/**
55+
* Registers a {@link Function} to create a {@link String} instance from the given {@link AffordanceModel.PropertyMetadata}
56+
* to supply value for the given property of the given type.
57+
*
58+
* @param type must not be {@literal null}.
59+
*/
60+
HalFormsValueFactory withValues(Class<?> type, String property,
61+
Function<AffordanceModel.PropertyMetadata, String> creator) {
62+
63+
Assert.notNull(type, "Type must not be null!");
64+
Assert.hasText(property, "Property must not be null or empty!");
65+
Assert.notNull(creator, "Creator function must not be null!");
66+
67+
Map<Class<?>, Map<String, Function<AffordanceModel.PropertyMetadata, String>>> values = new HashMap<>(this.values);
68+
69+
values.compute(type, (it, map) -> {
70+
71+
if (map == null) {
72+
map = new HashMap<>();
73+
}
74+
75+
map.put(property, creator);
76+
77+
return map;
78+
});
79+
80+
return new HalFormsValueFactory(values);
81+
}
82+
83+
/**
84+
* Returns the value to be used for the property with the given {@link AffordanceModel.PayloadMetadata} and
85+
* {@link AffordanceModel.PropertyMetadata}.
86+
*
87+
* @param payload must not be {@literal null}.
88+
* @param property must not be {@literal null}.
89+
*/
90+
@Nullable
91+
String getValue(AffordanceModel.PayloadMetadata payload, AffordanceModel.PropertyMetadata property) {
92+
93+
Assert.notNull(payload, "Payload metadata must not be null!");
94+
Assert.notNull(property, "Property metadata must not be null!");
95+
96+
Class<?> type = payload.getType();
97+
String name = property.getName();
98+
99+
Map<String, Function<AffordanceModel.PropertyMetadata, String>> map = values.get(type);
100+
101+
if (map == null) {
102+
return null;
103+
}
104+
105+
Function<AffordanceModel.PropertyMetadata, String> function = map.get(name);
106+
107+
return function == null ? null : function.apply(property);
108+
}
109+
110+
}

src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplateBuilderUnitTest.java

+22
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,28 @@ void rendersInlineOptions() {
201201
});
202202
}
203203

204+
@Test
205+
void rendersValues() {
206+
String value = "1234123412341234";
207+
208+
HalFormsConfiguration configuration = new HalFormsConfiguration() //
209+
.withValues(PatternExample.class, "number", metadata -> value);
210+
211+
RepresentationModel<?> models = new RepresentationModel<>(
212+
Affordances.of(Link.of("/example", LinkRelation.of("create"))) //
213+
.afford(HttpMethod.POST) //
214+
.withInput(PatternExample.class) //
215+
.toLink());
216+
217+
Map<String, HalFormsTemplate> templates = new HalFormsTemplateBuilder(configuration, MessageResolver.DEFAULTS_ONLY)
218+
.findTemplates(models);
219+
220+
assertThat(templates.get("default").getPropertyByName("number")) //
221+
.hasValueSatisfying(it -> {
222+
assertThat(it.getValue()).isEqualTo(value);
223+
});
224+
}
225+
204226
@Test // #1510
205227
void propagatesSelectedValueToProperty() {
206228

0 commit comments

Comments
 (0)