Support to receive aggregate references as request parameters.
We now support using AggregateReference as type to bind request parameters taking URIs pointing to related aggregates. The default resolution will try to resolve the entire URI via UriToEntityConverter but one can also provide a function that can extract any part of the URI to be then resolved into either an identifier, aggregate instance or jMolecules Association against the ConversionService. Fixes #2239.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.data.rest.core;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* Represents a reference to an aggregate backed by a URI. It can be resolved into an aggregate identifier or the
|
||||
* aggregate instance itself.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 4.1
|
||||
*/
|
||||
public interface AggregateReference<T, ID> {
|
||||
|
||||
/**
|
||||
* Returns the source {@link URI}.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
URI getUri();
|
||||
|
||||
/**
|
||||
* Creates a new {@link AggregateReference} resolving the identifier source value from the given
|
||||
* {@link UriComponents}.
|
||||
*
|
||||
* @param extractor must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
AggregateReference<T, ID> withIdSource(Function<UriComponents, Object> extractor);
|
||||
|
||||
/**
|
||||
* Resolves the underlying URI into a full aggregate, potentially applying the configured identifier extractor.
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
* @see #withIdSource(Function)
|
||||
*/
|
||||
@Nullable
|
||||
T resolveAggregate();
|
||||
|
||||
/**
|
||||
* Resolves the underlying URI into an aggregate identifier, potentially applying the registered identifier extractor.
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
* @see #withIdSource(Function)
|
||||
*/
|
||||
@Nullable
|
||||
ID resolveId();
|
||||
|
||||
/**
|
||||
* Resolves the underlying URI into a full aggregate, potentially applying the configured identifier extractor.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
* @throws IllegalStateException in case the value resolved is {@literal null}.
|
||||
*/
|
||||
default T resolveRequiredAggregate() {
|
||||
|
||||
T result = resolveAggregate();
|
||||
|
||||
if (result == null) {
|
||||
throw new IllegalStateException("Resolving the aggregate resulted in null");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the underlying URI into an aggregate identifier, potentially applying the registered identifier extractor.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
* @throws IllegalStateException in case the value resolved is {@literal null}.
|
||||
*/
|
||||
default ID resolveRequiredId() {
|
||||
|
||||
ID result = resolveId();
|
||||
|
||||
if (result == null) {
|
||||
throw new IllegalStateException("Resolving the aggregate identifier resulted in null");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.data.rest.core;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jmolecules.ddd.types.AggregateRoot;
|
||||
import org.jmolecules.ddd.types.Association;
|
||||
import org.jmolecules.ddd.types.Identifier;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* An {@link AggregateReference} that can also resolve into jMolecules {@link Association} instances.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 4.1
|
||||
*/
|
||||
public interface AssociationAggregateReference<T extends AggregateRoot<T, ID>, ID extends Identifier>
|
||||
extends AggregateReference<T, ID> {
|
||||
|
||||
/**
|
||||
* Resolves the underlying URI into an {@link Association}, potentially applying the configured identifier extractor.
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
* @see #withIdSource(Function)
|
||||
*/
|
||||
@Nullable
|
||||
default Association<T, ID> resolveAssociation() {
|
||||
return Association.forId(resolveId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the underlying URI into an {@link Association}, potentially applying the configured identifier extractor.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
* @throws IllegalStateException in case the value resolved is {@literal null}.
|
||||
*/
|
||||
@SuppressWarnings("null")
|
||||
default Association<T, ID> resolveRequiredAssociation() {
|
||||
return Association.forId(resolveRequiredId());
|
||||
}
|
||||
|
||||
@Override
|
||||
AssociationAggregateReference<T, ID> withIdSource(Function<UriComponents, Object> extractor);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.data.rest.core;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* An {@link AggregateReference} implementation that resolves the source URI given a {@link Function} or into a fixed
|
||||
* value.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 4.1
|
||||
*/
|
||||
public class ResolvingAggregateReference<T, ID> implements AggregateReference<T, ID> {
|
||||
|
||||
private static final Function<URI, UriComponents> STARTER = it -> UriComponentsBuilder.fromUri(it).build();
|
||||
|
||||
private final URI source;
|
||||
private final Function<URI, ? extends Object> extractor;
|
||||
private final Function<Object, ? extends T> aggregateResolver;
|
||||
private final Function<Object, ? extends ID> identifierResolver;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ResolvingAggregateReference} for the given {@link URI} to eventually resolve the final value
|
||||
* against the given resolver function.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @param resolver must not be {@literal null}.
|
||||
*/
|
||||
public ResolvingAggregateReference(URI source, Function<Object, ? extends T> aggregateResolver,
|
||||
Function<Object, ? extends ID> identifierResolver) {
|
||||
|
||||
this(source, aggregateResolver, identifierResolver, it -> it);
|
||||
}
|
||||
|
||||
protected ResolvingAggregateReference(URI source, Function<Object, ? extends T> aggregateResolver,
|
||||
Function<Object, ? extends ID> identifierResolver, Function<URI, ? extends Object> extractor) {
|
||||
|
||||
Assert.notNull(source, "Source URI must not be null!");
|
||||
Assert.notNull(aggregateResolver, "Aggregate resolver must not be null!");
|
||||
Assert.notNull(identifierResolver, "Identifier resolver must not be null!");
|
||||
|
||||
this.source = source;
|
||||
this.aggregateResolver = aggregateResolver;
|
||||
this.identifierResolver = identifierResolver;
|
||||
this.extractor = extractor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ResolvingAggregateReference} for the given {@link URI} resolving in the given fixed value.
|
||||
* Primarily for testing purposes.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @param value can be {@literal null}.
|
||||
*/
|
||||
public ResolvingAggregateReference(URI source, @Nullable T value, ID identifier) {
|
||||
this(source, __ -> value, __ -> identifier, it -> it);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.rest.core.Foo#getURI()
|
||||
*/
|
||||
@Override
|
||||
public URI getUri() {
|
||||
return source;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.rest.core.AggregateReference#resolveId()
|
||||
*/
|
||||
@Override
|
||||
public ID resolveId() {
|
||||
return extractor.andThen(identifierResolver).apply(source);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.rest.core.AggregateReference#resolveAggregate()
|
||||
*/
|
||||
@Override
|
||||
public T resolveAggregate() {
|
||||
return extractor.andThen(aggregateResolver).apply(source);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.rest.core.AggregateReference#withExtractor(java.util.function.Function)
|
||||
*/
|
||||
@Override
|
||||
public AggregateReference<T, ID> withIdSource(Function<UriComponents, Object> extractor) {
|
||||
return new ResolvingAggregateReference<>(source, aggregateResolver, identifierResolver, STARTER.andThen(extractor));
|
||||
}
|
||||
}
|
||||
@@ -16,21 +16,21 @@
|
||||
package org.springframework.data.rest.core;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.core.convert.ConversionFailedException;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.ConditionalGenericConverter;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.data.repository.support.Repositories;
|
||||
import org.springframework.data.repository.support.RepositoryInvokerFactory;
|
||||
import org.springframework.data.util.ReflectionUtils;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -39,14 +39,17 @@ import org.springframework.util.Assert;
|
||||
* @author Jon Brisbin
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class UriToEntityConverter implements ConditionalGenericConverter {
|
||||
public class UriToEntityConverter implements GenericConverter {
|
||||
|
||||
private static final TypeDescriptor URI_TYPE = TypeDescriptor.valueOf(URI.class);
|
||||
private static final Class<?> ASSOCIATION_TYPE = ReflectionUtils
|
||||
.loadIfPresent("org.jmolecules.ddd.types.Association", UriToEntityConverter.class.getClassLoader());
|
||||
|
||||
private final PersistentEntities entities;
|
||||
private final RepositoryInvokerFactory invokerFactory;
|
||||
private final Repositories repositories;
|
||||
private final Supplier<ConversionService> conversionService;
|
||||
|
||||
private final Set<ConvertiblePair> convertiblePairs;
|
||||
private final Set<Class<?>> identifierTypes;
|
||||
|
||||
/**
|
||||
* Creates a new {@link UriToEntityConverter} using the given {@link PersistentEntities},
|
||||
@@ -57,52 +60,79 @@ public class UriToEntityConverter implements ConditionalGenericConverter {
|
||||
* @param repositories must not be {@literal null}.
|
||||
*/
|
||||
public UriToEntityConverter(PersistentEntities entities, RepositoryInvokerFactory invokerFactory,
|
||||
Repositories repositories) {
|
||||
Supplier<ConversionService> conversionService) {
|
||||
|
||||
Assert.notNull(entities, "PersistentEntities must not be null");
|
||||
Assert.notNull(invokerFactory, "RepositoryInvokerFactory must not be null");
|
||||
Assert.notNull(repositories, "Repositories must not be null");
|
||||
Assert.notNull(conversionService, "ConversionService must not be null!");
|
||||
|
||||
Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>();
|
||||
this.convertiblePairs = new HashSet<ConvertiblePair>();
|
||||
this.identifierTypes = new HashSet<>();
|
||||
|
||||
for (TypeInformation<?> domainType : entities.getManagedTypes()) {
|
||||
|
||||
Class<?> rawType = domainType.getType();
|
||||
Optional<PersistentEntity<?, ? extends PersistentProperty<?>>> entity = entities.getPersistentEntity(rawType);
|
||||
var rawType = domainType.getType();
|
||||
var entity = entities.getPersistentEntity(rawType);
|
||||
|
||||
if (entity.map(it -> it.hasIdProperty()).orElse(false)) {
|
||||
entity.filter(it -> it.hasIdProperty()).ifPresent(it -> {
|
||||
convertiblePairs.add(new ConvertiblePair(URI.class, domainType.getType()));
|
||||
}
|
||||
registerIdentifierType(it.getRequiredIdProperty().getType());
|
||||
});
|
||||
}
|
||||
|
||||
this.convertiblePairs = Collections.unmodifiableSet(convertiblePairs);
|
||||
this.entities = entities;
|
||||
this.invokerFactory = invokerFactory;
|
||||
this.repositories = repositories;
|
||||
this.conversionService = conversionService;
|
||||
|
||||
if (ASSOCIATION_TYPE != null) {
|
||||
registerIdentifierType(ASSOCIATION_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
return !sourceType.equals(URI_TYPE) ? false
|
||||
: repositories.getRepositoryInformationFor(targetType.getType()).isPresent();
|
||||
private void registerIdentifierType(Class<?> type) {
|
||||
|
||||
convertiblePairs.add(new ConvertiblePair(URI.class, type));
|
||||
identifierTypes.add(type);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return convertiblePairs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
@Nullable
|
||||
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
|
||||
Optional<PersistentEntity<?, ? extends PersistentProperty<?>>> entity = entities
|
||||
.getPersistentEntity(targetType.getType());
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (identifierTypes.contains(targetType.getType())) {
|
||||
|
||||
var segment = getIdentifierSegment(source, sourceType, targetType);
|
||||
|
||||
return conversionService.get().convert(segment, TypeDescriptor.valueOf(String.class), targetType);
|
||||
}
|
||||
|
||||
var entity = entities.getPersistentEntity(targetType.getType());
|
||||
|
||||
if (!entity.isPresent()) {
|
||||
throw new ConversionFailedException(sourceType, targetType, source,
|
||||
new IllegalArgumentException("No PersistentEntity information available for " + targetType.getType()));
|
||||
new IllegalArgumentException(
|
||||
"No PersistentEntity information available for " + targetType.getType()));
|
||||
}
|
||||
|
||||
var segment = getIdentifierSegment(source, sourceType, targetType);
|
||||
|
||||
return invokerFactory.getInvokerFor(targetType.getType())
|
||||
.invokeFindById(segment)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static String getIdentifierSegment(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
|
||||
URI uri = (URI) source;
|
||||
String[] parts = uri.getPath().split("/");
|
||||
|
||||
@@ -111,6 +141,6 @@ public class UriToEntityConverter implements ConditionalGenericConverter {
|
||||
"Cannot resolve URI " + uri + "; Is it local or remote; Only local URIs are resolvable"));
|
||||
}
|
||||
|
||||
return invokerFactory.getInvokerFor(targetType.getType()).invokeFindById(parts[parts.length - 1]).orElse(null);
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.springframework.data.rest.core;
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.data.rest.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ResolvingAggregateReference}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class ResolvingAggregateReferenceUnitTests {
|
||||
|
||||
@Test // GH-2239
|
||||
void usesResolverForFinalInstanceLookup() {
|
||||
|
||||
var reference = new ResolvingAggregateReference<>(URI.create("/foo/42"), it -> "aggregate", it -> 42L);
|
||||
|
||||
assertThat(reference.resolveAggregate()).isEqualTo("aggregate");
|
||||
assertThat(reference.resolveId()).isEqualTo(42);
|
||||
}
|
||||
|
||||
@Test // GH-2239
|
||||
void appliesCustomExtractor() {
|
||||
|
||||
var reference = new ResolvingAggregateReference<>(URI.create("/foo/42"), it -> "aggregate",
|
||||
it -> Long.valueOf(it.toString())).withIdSource(it -> it.getPathSegments().get(1));
|
||||
|
||||
assertThat(reference.resolveId()).isEqualTo(42);
|
||||
}
|
||||
}
|
||||
@@ -18,29 +18,37 @@ package org.springframework.data.rest.core;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jmolecules.ddd.types.AggregateRoot;
|
||||
import org.jmolecules.ddd.types.Association;
|
||||
import org.jmolecules.ddd.types.Identifier;
|
||||
import org.jmolecules.spring.PrimitivesToAssociationConverter;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionFailedException;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.support.Repositories;
|
||||
import org.springframework.data.repository.support.RepositoryInvoker;
|
||||
import org.springframework.data.repository.support.RepositoryInvokerFactory;
|
||||
import org.springframework.data.util.ClassTypeInformation;
|
||||
import org.springframework.data.rest.core.UriToEntityConverterUnitTests.JMoleculesAggregateRoot.JMoleculesIdentifier;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link UriToEntityConverter}.
|
||||
@@ -53,22 +61,28 @@ class UriToEntityConverterUnitTests {
|
||||
static final TypeDescriptor URI_TYPE = TypeDescriptor.valueOf(URI.class);
|
||||
static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class);
|
||||
static final TypeDescriptor ENTITY_TYPE = TypeDescriptor.valueOf(Entity.class);
|
||||
static final TypeDescriptor UUID_ENTITY_TYPE = TypeDescriptor.valueOf(UuidEntity.class);
|
||||
static final TypeDescriptor UUID_TYPE = TypeDescriptor.valueOf(UUID.class);
|
||||
static final TypeDescriptor UNKNOWN_TYPE = TypeDescriptor.valueOf(Override.class);
|
||||
|
||||
@Mock Repositories repositories;
|
||||
@Mock RepositoryInvokerFactory invokerFactory;
|
||||
|
||||
KeyValueMappingContext<?, ?> context;
|
||||
UriToEntityConverter converter;
|
||||
ConversionService conversionService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
|
||||
var conversionService = new DefaultFormattingConversionService();
|
||||
conversionService.addConverter(new PrimitivesToAssociationConverter(() -> conversionService));
|
||||
|
||||
this.context = new KeyValueMappingContext<>();
|
||||
this.context.setInitialEntitySet(new HashSet<Class<?>>(Arrays.asList(Entity.class, NonEntity.class)));
|
||||
this.context.setInitialEntitySet(Set.of(Entity.class, NonEntity.class, UuidEntity.class));
|
||||
this.context.afterPropertiesSet();
|
||||
|
||||
this.converter = new UriToEntityConverter(new PersistentEntities(Arrays.asList(this.context)), invokerFactory,
|
||||
repositories);
|
||||
this.converter = new UriToEntityConverter(new PersistentEntities(List.of(this.context)), invokerFactory,
|
||||
() -> conversionService);
|
||||
}
|
||||
|
||||
@Test // DATAREST-427
|
||||
@@ -80,25 +94,6 @@ class UriToEntityConverterUnitTests {
|
||||
assertThat(result).doesNotContain(new ConvertiblePair(URI.class, NonEntity.class));
|
||||
}
|
||||
|
||||
@Test // DATAREST-427
|
||||
void cannotConvertEntityWithIdPropertyIfStringConversionMissing() {
|
||||
assertThat(converter.matches(URI_TYPE, ENTITY_TYPE)).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAREST-427
|
||||
void canConvertEntityWithIdPropertyAndFromStringConversionPossible() {
|
||||
|
||||
doReturn(Optional.of(mock(RepositoryInformation.class))).when(repositories)
|
||||
.getRepositoryInformationFor(ENTITY_TYPE.getType());
|
||||
|
||||
assertThat(converter.matches(URI_TYPE, ENTITY_TYPE)).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATAREST-427
|
||||
void cannotConvertEntityWithoutIdentifier() {
|
||||
assertThat(converter.matches(URI_TYPE, TypeDescriptor.valueOf(NonEntity.class))).isFalse();
|
||||
}
|
||||
|
||||
@Test // DATAREST-427
|
||||
void invokesConverterWithLastUriPathSegment() {
|
||||
|
||||
@@ -115,7 +110,7 @@ class UriToEntityConverterUnitTests {
|
||||
void rejectsUnknownType() {
|
||||
|
||||
assertThatExceptionOfType(ConversionFailedException.class) //
|
||||
.isThrownBy(() -> converter.convert(URI.create("/foo/1"), URI_TYPE, STRING_TYPE));
|
||||
.isThrownBy(() -> converter.convert(URI.create("/foo/1"), URI_TYPE, UNKNOWN_TYPE));
|
||||
}
|
||||
|
||||
@Test // DATAREST-427
|
||||
@@ -129,18 +124,19 @@ class UriToEntityConverterUnitTests {
|
||||
void rejectsNullPersistentEntities() {
|
||||
|
||||
assertThatIllegalArgumentException() //
|
||||
.isThrownBy(() -> new UriToEntityConverter(null, invokerFactory, repositories));
|
||||
.isThrownBy(
|
||||
() -> new UriToEntityConverter(null, invokerFactory, () -> conversionService));
|
||||
}
|
||||
|
||||
@Test // DATAREST-741
|
||||
void rejectsNullRepositoryInvokerFactory() {
|
||||
|
||||
assertThatIllegalArgumentException() //
|
||||
.isThrownBy(() -> new UriToEntityConverter(mock(PersistentEntities.class), null, repositories));
|
||||
.isThrownBy(() -> new UriToEntityConverter(mock(PersistentEntities.class), null, () -> conversionService));
|
||||
}
|
||||
|
||||
@Test // DATAREST-741
|
||||
void rejectsNullRepositories() {
|
||||
void rejectsNullConversionService() {
|
||||
|
||||
assertThatIllegalArgumentException() //
|
||||
.isThrownBy(() -> new UriToEntityConverter(mock(PersistentEntities.class), invokerFactory, null));
|
||||
@@ -153,16 +149,59 @@ class UriToEntityConverterUnitTests {
|
||||
void doesNotRegisterTypeWithUnmanagedRawType() {
|
||||
|
||||
PersistentEntities entities = mock(PersistentEntities.class);
|
||||
doReturn(Streamable.of(ClassTypeInformation.OBJECT)).when(entities).getManagedTypes();
|
||||
doReturn(Streamable.of(TypeInformation.OBJECT)).when(entities).getManagedTypes();
|
||||
|
||||
new UriToEntityConverter(entities, invokerFactory, repositories);
|
||||
new UriToEntityConverter(entities, invokerFactory, () -> conversionService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesIdentifierType() {
|
||||
|
||||
var uuid = UUID.randomUUID();
|
||||
|
||||
assertThat(converter.convert(URI.create("/foo/" + uuid), STRING_TYPE, UUID_TYPE)).isEqualTo(uuid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesAssociations() {
|
||||
|
||||
var typeDescriptor = new TypeDescriptor(
|
||||
ResolvableType.forClassWithGenerics(Association.class, JMoleculesAggregateRoot.class,
|
||||
JMoleculesIdentifier.class),
|
||||
null, null);
|
||||
|
||||
var uuid = UUID.randomUUID();
|
||||
|
||||
assertThat(converter.convert(URI.create("/foo/" + uuid), URI_TYPE, typeDescriptor))
|
||||
.isInstanceOfSatisfying(Association.class, it -> {
|
||||
assertThat(it.getId()).isEqualTo(JMoleculesIdentifier.of(uuid));
|
||||
});
|
||||
}
|
||||
|
||||
static class Entity {
|
||||
@Id String id;
|
||||
}
|
||||
|
||||
static class UuidEntity {
|
||||
@Id UUID id;
|
||||
}
|
||||
|
||||
static class NonEntity {
|
||||
String value;
|
||||
}
|
||||
|
||||
static class JMoleculesAggregateRoot implements AggregateRoot<JMoleculesAggregateRoot, JMoleculesIdentifier> {
|
||||
|
||||
@Override
|
||||
public JMoleculesIdentifier getId() {
|
||||
return JMoleculesIdentifier.of(UUID.randomUUID());
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
static class JMoleculesIdentifier implements Identifier {
|
||||
UUID id;
|
||||
}
|
||||
}
|
||||
|
||||
private static void someMethod(Association<JMoleculesAggregateRoot, JMoleculesIdentifier> association) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user