Skip to content

It is hard to add dynamic HalFormsOptions #2391

@cpoulsen-dezide

Description

@cpoulsen-dezide

Caveat: The Spring HATEOAS project is rather new to me, so I may have missed something obvious / this may be a documentation issue.

I have been fiddling with adding dynamic HalFormsOptions to my API. The route I ended up taking was the following:

  1. Seeing that there is a hook for static HalFormsOptions in the HalFormsConfiguration I decided to lean on that and did:
new HalFormsConfiguration(halConfiguration)
  .withOptions(RespondRequest.class, "responseKey",
      metadata -> {
          if(metadata instanceof HalFormsPropertyMetadata halFormsPropertyMetadata) { // HalFormsPropertyMetadata is my own delegating impl
              return halFormsPropertyMetadata.getOptions();
          } else {
              return null; // could also throw here as the metadata does not live up to the expectations
          }});
  1. As the Affordance type from WebMvcLinkBuilder.afford() is locked down, I create the affordance manually, like the docs indicate and try to setup the proper AffordanceModel.InputPayloadMetadata:
AffordanceModel.InputPayloadMetadata input =
    PropertyUtils.getExposedProperties(ResolvableType.forClass(RespondRequest.class));
  1. Next problem was to customize my property inside the input metadata? (The .customize() method does not mutate anything). I created my own InputPayloadMetadata to mutate the properties:
final class HalFormsPayloadMetadata implements AffordanceModel.InputPayloadMetadata {
    private final Class<?> type;
    private final SortedMap<String, AffordanceModel.PropertyMetadata> properties;
    private final List<MediaType> mediaTypes;
    private final List<String> i18nCodes;

    public HalFormsPayloadMetadata(AffordanceModel.InputPayloadMetadata payload) {
        this(payload.getType(),
            new TreeMap<>(payload.stream().collect(Collectors.toMap(AffordanceModel.Named::getName, Function.identity()))),
            payload.getMediaTypes(),
                payload.getI18nCodes());
    }

    private HalFormsPayloadMetadata(Class<?> type, SortedMap<String, AffordanceModel.PropertyMetadata> properties,
                                   List<MediaType> mediaTypes, List<String> i18nCodes) {
        this.type = type;
        this.properties = properties;
        this.mediaTypes = List.copyOf(mediaTypes);
        this.i18nCodes = List.copyOf(i18nCodes);
    }

    /**
     * Manipulate the existing properties, the mapper should just return the properties that it is not interested in
      * @param mapper
     * @return a new payload metadata
     */
    public HalFormsPayloadMetadata mapProperty(Function<AffordanceModel.PropertyMetadata, AffordanceModel.PropertyMetadata> mapper) {
        return new HalFormsPayloadMetadata(this.type,
                new TreeMap<>(this.properties.values().stream().map(mapper)
                        .collect(Collectors.toMap(AffordanceModel.Named::getName, Function.identity()))),
                this.mediaTypes, this.i18nCodes);
    }

    // the other methods from the interface
   ..
} 
  1. In my assembler i can now create my custom HalFormsPropertyMetadata with the dynamic options, create a new HalFormsPayloadMetadata with the normal property descriptor replaced with my custom one and let the framework do its thing.

I find the dance around manipulating the state of the InputPayloadMetadata a bit unnecessary (having to implement my own version just to manipulate a property. Some variant of the InputPayloadMetadata.customize() that just returns a new InputPayloadMetadata instance would have gone a long way.

I can live with having to implement a custom property implementation, but it also seems like having a Map<String,Object> getCustomData() on AffordanceModel.PropertyMetadata could offer a lot of exiting options.

Ideally it would be possible to obtain a builder from an Affordance and just manipulate everything through that. "custom data" does not have to be tied to a media type, so it could go into the general representation. That would allow me to use the provided stuff for most of the work and take over, when something else, like dynamic options or other manipulation is needed.

I kind of hope that I missed some mechanism for setting up Input payload metadata without having to create my own, so thoughts are very welcome!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions