Skip to content

Commit

Permalink
Merge pull request #227 from xenit-eu/revert-224-hal-forms-create-and…
Browse files Browse the repository at this point in the history
…-upload

Revert "Hal Forms for multipart create-and-upload request"
  • Loading branch information
rschev authored May 13, 2024
2 parents 55ff46f + 42ecda0 commit 01b6d81
Show file tree
Hide file tree
Showing 13 changed files with 103 additions and 337 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import com.contentgrid.spring.data.rest.webmvc.DefaultDomainTypeToHalFormsPayloadMetadataConverter;
import com.contentgrid.spring.data.rest.webmvc.DomainTypeToHalFormsPayloadMetadataConverter;
import com.contentgrid.spring.querydsl.mapping.CollectionFiltersMapping;
import java.util.Optional;
import org.springframework.content.commons.mappingcontext.MappingContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
Expand Down Expand Up @@ -38,14 +36,14 @@ DomainTypeMapping halFormsFormMappingDomainTypeMapping(@PlainMapping DomainTypeM
@Bean
DomainTypeToHalFormsPayloadMetadataConverter DomainTypeToHalFormsPayloadMetadataConverter(
@FormMapping DomainTypeMapping formDomainTypeMapping,
CollectionFiltersMapping collectionFiltersMapping,
Optional<MappingContext> contentMappingContext
CollectionFiltersMapping collectionFiltersMapping
) {
return new DefaultDomainTypeToHalFormsPayloadMetadataConverter(
formDomainTypeMapping,
collectionFiltersMapping,
contentMappingContext
collectionFiltersMapping
);
}



}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.contentgrid.spring.data.rest.mediatype.html;

import java.io.File;
import java.time.Instant;
import java.time.LocalDate;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -14,7 +13,6 @@
* <ul>
* <li>{@link Boolean} maps to {@code checkbox}</li>
* <li>{@link Instant} maps to {@code datetime}</li>
* <li>Content maps to {@code file}</li>
* </ul>
*/
@Slf4j
Expand All @@ -34,10 +32,6 @@ public String getInputType(Class<?> type) {
return "datetime";
}

if (File.class.equals(type)) {
return "file";
}

if (inputType == null) {
log.trace("Type {} not mapped", type.getSimpleName());
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,22 @@
import com.contentgrid.spring.data.rest.mapping.Property;
import com.contentgrid.spring.querydsl.mapping.CollectionFilter;
import com.contentgrid.spring.querydsl.mapping.CollectionFiltersMapping;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.Value;
import lombok.With;
import org.apache.commons.lang.CharUtils;
import org.springframework.content.commons.annotations.ContentId;
import org.springframework.content.commons.annotations.MimeType;
import org.springframework.content.commons.annotations.OriginalFileName;
import org.springframework.content.commons.mappingcontext.MappingContext;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata;
Expand All @@ -38,39 +31,26 @@
import org.springframework.hateoas.mediatype.html.HtmlInputType;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;

@RequiredArgsConstructor
public class DefaultDomainTypeToHalFormsPayloadMetadataConverter implements
DomainTypeToHalFormsPayloadMetadataConverter {

private final DomainTypeMapping formMapping;
private final CollectionFiltersMapping searchMapping;
// Optional means it only gets autowired if available
private final Optional<MappingContext> contentMappingContext;

@Override
public PayloadMetadata convertToCreatePayloadMetadata(Class<?> domainType) {
List<PropertyMetadata> properties = new ArrayList<>();
extractPropertyMetadataForForms(formMapping.forDomainType(domainType),
domainType,
"", // path prefix starts empty
(prop) -> (!prop.isReadOnly() && !prop.isIgnored() && prop.findAnnotation(MimeType.class).isEmpty() && prop.findAnnotation(
OriginalFileName.class).isEmpty()) || prop.findAnnotation(ContentId.class).isPresent(),
this::propertyToMetadataForCreateForm
).forEachOrdered(properties::add);
extractPropertyMetadataForForms(formMapping.forDomainType(domainType))
.forEachOrdered(properties::add);
return new ClassnameI18nedPayloadMetadata(domainType, properties);
}

@Override
public PayloadMetadata convertToUpdatePayloadMetadata(Class<?> domainType) {
List<PropertyMetadata> properties = new ArrayList<>();
extractPropertyMetadataForForms(formMapping.forDomainType(domainType),
domainType,
"", // path prefix starts empty
(prop) -> (!prop.isReadOnly() && !prop.isIgnored()),
this::propertyToMetadataForUpdateForm
)
extractPropertyMetadataForForms(formMapping.forDomainType(domainType))
.filter(property -> !Objects.equals(property.getInputType(), HtmlInputType.URL_VALUE))
.forEachOrdered(properties::add);
return new ClassnameI18nedPayloadMetadata(domainType, properties);
Expand All @@ -91,86 +71,87 @@ public PayloadMetadata convertToSearchPayloadMetadata(Class<?> domainType) {
return new ClassnameI18nedPayloadMetadata(domainType, properties);
}

private Stream<PropertyMetadata> extractPropertyMetadataForForms(Container entity, Class<?> domainType,
String pathPrefix, Predicate<Property> filter, PropertyMetadataFactory attributeFactory) {
private Stream<PropertyMetadata> extractPropertyMetadataForForms(Container entity) {
var output = Stream.<PropertyMetadata>builder();
entity.doWithProperties(new RecursivePropertyConsumer(
output,
attributeFactory,
filter,
domainType,
pathPrefix
(property) -> new BasicPropertyMetadata(property.getName(),
property.getTypeInformation().toTypeDescriptor().getResolvableType())
.withRequired(property.isRequired())
.withReadOnly(false),

this::extractPropertyMetadataForForms
));

entity.doWithAssociations(new RecursivePropertyConsumer(
output,
(property, a, b) -> new BasicPropertyMetadata(property.getName(),
property -> new BasicPropertyMetadata(property.getName(),
property.getTypeInformation().toTypeDescriptor().getResolvableType())
.withInputType(HtmlInputType.URL_VALUE)
.withRequired(property.isRequired())
.withReadOnly(false),
filter,
domainType,
pathPrefix
this::extractPropertyMetadataForForms
));
return output.build();
}

private PropertyMetadata propertyToMetadataForCreateForm(Property property, Class<?> domainClass, String path) {
@RequiredArgsConstructor
private static class RecursivePropertyConsumer implements Consumer<Property> {
private final Consumer<PropertyMetadata> output;
private final Function<Property, PropertyMetadata> factory;
private final Function<Container, Stream<PropertyMetadata>> recursor;

var contentPropertyKey = contentMappingContext.flatMap(context -> context.getContentPropertyMap(domainClass)
.entrySet()
.stream()
.filter(entry -> pathMatches(entry.getValue().getContentIdPropertyPath(), path))
.findFirst()
.map(Entry::getKey));
@Override
public void accept(Property property) {
if (property.isIgnored() || property.isReadOnly()) {
return;
}

if (contentPropertyKey.isPresent()) {
return new BasicPropertyMetadata(contentPropertyKey.get(), ResolvableType.forClass(File.class))
.withRequired(property.isRequired())
.withReadOnly(false);
property.nestedContainer().ifPresentOrElse(container -> {
recursor.apply(container)
.map(propertyMetadata -> new PrefixedPropertyMetadata(property.getName(), propertyMetadata))
.forEachOrdered(output);
}, () -> {
output.accept(factory.apply(property));
});
}

return new BasicPropertyMetadata(path,
property.getTypeInformation().toTypeDescriptor().getResolvableType())
.withRequired(property.isRequired())
.withReadOnly(false);
}

private PropertyMetadata propertyToMetadataForUpdateForm(Property property, Class<?> domainClass, String path) {
return new BasicPropertyMetadata(path,
property.getTypeInformation().toTypeDescriptor().getResolvableType())
.withRequired(property.isRequired())
.withReadOnly(false);
}
@RequiredArgsConstructor
@ToString
private static class PrefixedPropertyMetadata implements PropertyMetadata {

private final String prefix;
private final PropertyMetadata delegate;

@RequiredArgsConstructor
private class RecursivePropertyConsumer implements Consumer<Property> {
private final Consumer<PropertyMetadata> output;
private final PropertyMetadataFactory factory;
private final Predicate<Property> filter;
@Override
public String getName() {
return prefix + "." + delegate.getName();
}

private final Class<?> domainType;
private final String pathPrefix;
@Override
public boolean isRequired() {
return delegate.isRequired();
}

@Override
public boolean isReadOnly() {
return delegate.isReadOnly();
}

@Override
public void accept(Property property) {
if (!filter.test(property)) {
return;
}
public Optional<String> getPattern() {
return delegate.getPattern();
}

String path = pathPrefix.length() == 0
? property.getName()
: pathPrefix + "." + property.getName();
@Override
public ResolvableType getType() {
return delegate.getType();
}

property.nestedContainer().ifPresentOrElse(container -> {
extractPropertyMetadataForForms(container, domainType, path, filter, factory)
.forEachOrdered(output);
}, () -> {
output.accept(factory.build(property, domainType, path));
});
@Override
public String getInputType() {
return delegate.getInputType();
}
}

Expand Down Expand Up @@ -246,43 +227,4 @@ public Class<?> getType() {
}
}

@FunctionalInterface
static interface PropertyMetadataFactory {
PropertyMetadata build(Property property, Class<?> domainClass, String path);
}

private static boolean pathMatches(String contentIdPropertyPath, String givenPath) {
return Objects.equals(
// names that are reserved keywords, like "public", have a leading _ in their java property
StringUtils.trimLeadingCharacter(contentIdPropertyPath, '_'),
// to match the java field name, we must convert the snake_case from the given path to camelCase
underscoreToCamelCase(givenPath)
);
}

private static String underscoreToCamelCase(String underscored) {
int index = underscored.indexOf('_');
if (index == -1) {
return underscored;
}

int from = 0;
StringBuilder camel = new StringBuilder();
while (index != -1) {
camel.append(underscored, from, index);
if (index + 1 < underscored.length()) {
camel.append(Character.toUpperCase(underscored.charAt(index + 1)));
from = index + 2;
} else {
from = index + 1;
break;
}
index = underscored.indexOf('_', from);
}

camel.append(underscored.substring(from));

return camel.toString();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ void halFormsProfile(RootResourceInformation information, HttpServletResponse re
.andAfford(HttpMethod.POST)
.withName(IanaLinkRelations.CREATE_FORM_VALUE)
.withInput(toHalFormsPayloadMetadataConverter.convertToCreatePayloadMetadata(information.getDomainType()))
.withInputMediaType(MediaType.MULTIPART_FORM_DATA)
.withInputMediaType(MediaType.APPLICATION_JSON)
.andAfford(HttpMethod.PATCH) // This gets mapped to "GET" with the very ugly hack below
.withName(IanaLinkRelations.SEARCH_VALUE)
.withInput(toHalFormsPayloadMetadataConverter.convertToSearchPayloadMetadata(information.getDomainType()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ void entityLinkRelAddedToProfile() throws Exception {
name: "shipping-addresses",
href: "http://localhost/profile/shipping-addresses"
},
{
name: "shipping-labels",
href: "http://localhost/profile/shipping-labels"
},
{
name: "refunds",
href: "http://localhost/profile/refunds"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,6 @@ void entityLinkRelAddedToRoot() throws Exception {
href: "http://localhost/shipping-addresses{?page,size,sort}",
templated: true
},
{
name: "shipping-labels",
href: "http://localhost/shipping-labels{?page,size,sort}",
templated: true
},
{
name: "refunds",
href: "http://localhost/refunds{?page,size,sort}",
Expand Down
Loading

0 comments on commit 01b6d81

Please sign in to comment.