Skip to content
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

Introduce HalFormsConfiguration API to sort properties. #1284

Open
wants to merge 2 commits 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<version>1.1.0.BUILD-SNAPSHOT</version>
<version>1.1.0.HATEOAS-1114-SNAPSHOT</version>

<name>Spring HATEOAS</name>
<url>https://github.com/spring-projects/spring-hateoas</url>
Expand Down
28 changes: 28 additions & 0 deletions src/main/asciidoc/mediatypes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ Spring HATEOAS allows to customize those by shaping the model type for the input

For types that you cannot annotate manually, you can register a custom pattern via a `HalFormsConfiguration` bean present in the application context.

.Registering regex patterns for types
====
[source, java]
----
@Configuration
Expand All @@ -235,9 +237,35 @@ class CustomConfiguration {
}
}
----
====

This setup will cause the HAL-FORMS template properties for representation model properties of type `CreditCardNumber` to declare a `regex` field with value `[0-9]{16}`.

[[mediatypes.hal-forms.property-order]]
=== Ordering template properties

By default, HAL-FORMS properties are gleaned from the related domain object using Spring Framework's `BeanUtils`. There is no defined ordering. If you wish to enforce a
particular order on these properties, you can specify that through `HalFormsConfiguration`.

.Registering order of template properties for types
====
[source,java]
----
@Configuration
class CustomConfiguration {

@Bean
HalFormsConfiguration halFormsConfiguration() {

HalFormsConfiguration configuration = new HalFormsConfiguration();
configuration.withFieldOrderFor(Employee.class, "employeeId", "name", "role");
}
}
----
====

The listed properties will be rendered first, followed by any not cited.

[[mediatypes.hal-forms.i18n]]
=== Internationalization of form attributes
HAL-FORMS contains attributes that are intended for human interpretation, like a template's title or property prompts.
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/org/springframework/hateoas/AffordanceModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ static InputPayloadMetadata from(PayloadMetadata metadata) {
* @return
*/
List<String> getI18nCodes();

Optional<ResolvableType> getType();
}

/**
Expand Down Expand Up @@ -210,7 +212,17 @@ public <T extends Named> T customize(T target, Function<PropertyMetadata, T> cus
public List<String> getI18nCodes() {
return Collections.emptyList();
}
}

@Override
public Optional<ResolvableType> getType() {

if (metadata instanceof InputPayloadMetadata) {
((InputPayloadMetadata) metadata).getType();
}

return Optional.empty();
}
}

/**
* Metadata about the property model of a representation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,17 +308,10 @@ private String getNameOrDefault() {

String name = method.toString().toLowerCase();

ResolvableType 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 inputMetdata.getType() //
.map(ResolvableType::resolve) //
.map(resolvedType -> resolvedType == null ? name : name.concat(resolvedType.getSimpleName())) //
.orElse(name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Function;
Expand All @@ -39,7 +40,7 @@
*/
class TypeBasedPayloadMetadata implements InputPayloadMetadata {

private final @Getter(AccessLevel.PACKAGE) ResolvableType type;
private final ResolvableType type;
private final SortedMap<String, PropertyMetadata> properties;

TypeBasedPayloadMetadata(ResolvableType type, Stream<PropertyMetadata> properties) {
Expand Down Expand Up @@ -93,4 +94,9 @@ public List<String> getI18nCodes() {

return Arrays.asList(type.getName(), type.getSimpleName());
}

@Override
public Optional<ResolvableType> getType() {
return Optional.ofNullable(this.type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

Expand All @@ -36,6 +38,7 @@ public class HalFormsConfiguration {

private final @Getter HalConfiguration halConfiguration;
private final Map<Class<?>, String> patterns = new HashMap<>();
private final Map<Class<?>, List<String>> fieldOrder = new HashMap<>();

/**
* Creates a new {@link HalFormsConfiguration} backed by a default {@link HalConfiguration}.
Expand All @@ -60,4 +63,15 @@ public HalFormsConfiguration registerPattern(Class<?> type, String pattern) {
Optional<String> getTypePatternFor(ResolvableType type) {
return Optional.ofNullable(patterns.get(type.resolve(Object.class)));
}

public HalFormsConfiguration withFieldOrderFor(Class<?> type, String... fieldNames) {

this.fieldOrder.put(type, Arrays.asList(fieldNames));

return this;
}

Optional<List<String>> getFieldOrderFor(ResolvableType type) {
return Optional.ofNullable(fieldOrder.get(type.resolve(Object.class)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public Map<String, HalFormsTemplate> findTemplates(RepresentationModel<?> resour
.map(property -> it.hasHttpMethod(HttpMethod.PATCH) ? property.withRequired(false) : property)
.collect(Collectors.toList());

propertiesWithPrompt = sorted(propertiesWithPrompt, it.getInput());

HalFormsTemplate template = HalFormsTemplate.forMethod(it.getHttpMethod()) //
.withProperties(propertiesWithPrompt);

Expand All @@ -89,6 +91,33 @@ public Map<String, HalFormsTemplate> findTemplates(RepresentationModel<?> resour
return templates;
}

private List<HalFormsProperty> sorted(List<HalFormsProperty> properties, InputPayloadMetadata input) {

return input.getType() //
.flatMap(configuration::getFieldOrderFor) //
.map(fieldsToSortBy -> {

List<HalFormsProperty> propertiesToSort = new ArrayList<>(properties);
List<HalFormsProperty> sortedProperties = new ArrayList<>();

for (String propertyName : fieldsToSortBy) {
properties.stream() //
.filter(halFormsProperty -> halFormsProperty.getName().equals(propertyName)) //
.findFirst() //
.ifPresent(halFormsProperty -> {
sortedProperties.add(halFormsProperty);
propertiesToSort.remove(halFormsProperty);
});
}

// Whatever properties weren't listed, add them at the end.
sortedProperties.addAll(propertiesToSort);

return sortedProperties;
}) //
.orElse(properties);
}

public PropertyCustomizations forMetadata(InputPayloadMetadata metadata) {
return new PropertyCustomizations(metadata);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static org.assertj.core.api.Assertions.*;

import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;

Expand Down Expand Up @@ -113,8 +114,8 @@ public PayloadMetadataAssert(PayloadMetadata actual) {

public PayloadMetadataAssert isBackedBy(Class<?> type) {

Assertions.assertThat(actual).isInstanceOfSatisfying(TypeBasedPayloadMetadata.class, it -> {
Assertions.assertThat(it.getType()).isEqualTo(ResolvableType.forClass(type));
assertThat(actual).isInstanceOfSatisfying(TypeBasedPayloadMetadata.class, it -> {
assertThat(it.getType()).isEqualTo(Optional.of(ResolvableType.forClass(type)));
});

return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.springframework.hateoas.mediatype.hal.forms;

import lombok.Data;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.Affordances;
import org.springframework.hateoas.mediatype.MessageResolver;
import org.springframework.http.HttpMethod;

import static org.assertj.core.api.Assertions.*;

public class HalFormsPropertyOrderingUnitTest {

private RepresentationModel<?> model;

@BeforeEach
void setUp() {

this.model = new RepresentationModel<>(Affordances.of(Link.of("/example")) //
.afford(HttpMethod.POST) //
.withInput(Thing.class) //
.toLink());
}

@Test
void noCustomOrdering() {

HalFormsConfiguration halFormsConfiguration = new HalFormsConfiguration();

assertThat(createTemplate(halFormsConfiguration).getProperties()).flatExtracting(HalFormsProperty::getName)
.containsExactly("a", "b", "z");

}

@Test
void specifyAllProperties() {

HalFormsConfiguration halFormsConfiguration = new HalFormsConfiguration() //
.withFieldOrderFor(Thing.class, "z", "b", "a");

assertThat(createTemplate(halFormsConfiguration).getProperties()).flatExtracting(HalFormsProperty::getName)
.containsExactly("z", "b", "a");
}

@Test
void specifySomeProperties() {

HalFormsConfiguration halFormsConfiguration = new HalFormsConfiguration() //
.withFieldOrderFor(Thing.class, "z");

assertThat(createTemplate(halFormsConfiguration).getProperties()).flatExtracting(HalFormsProperty::getName)
.containsExactly("z", "a", "b");
}

@Test
void nonExistentProperty() {

HalFormsConfiguration halFormsConfiguration = new HalFormsConfiguration() //
.withFieldOrderFor(Thing.class, "q", "b");

assertThat(createTemplate(halFormsConfiguration).getProperties()).flatExtracting(HalFormsProperty::getName)
.containsExactly("b", "a", "z");
}

private HalFormsTemplate createTemplate(HalFormsConfiguration halFormsConfiguration) {
return new HalFormsTemplateBuilder(halFormsConfiguration, MessageResolver.DEFAULTS_ONLY).findTemplates(this.model)
.get("default");
}

@Data
private static class Thing {

private String a;
private String b;
private String z;
}
}