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:
@@ -62,6 +62,13 @@
|
||||
<artifactId>jackson-datatype-jdk8</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jmolecules</groupId>
|
||||
<artifactId>jmolecules-ddd</artifactId>
|
||||
<version>${jmolecules}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
@@ -76,6 +83,13 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jmolecules.integrations</groupId>
|
||||
<artifactId>jmolecules-spring</artifactId>
|
||||
<version>${jmolecules-integration}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -109,16 +109,18 @@ public class RepositoryTestsConfig {
|
||||
@Bean
|
||||
public Module persistentEntityModule() {
|
||||
|
||||
var conversionService = new DefaultConversionService();
|
||||
|
||||
RepositoryResourceMappings mappings = new RepositoryResourceMappings(repositories(), persistentEntities(),
|
||||
config());
|
||||
EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(),
|
||||
mock(PagingAndSortingTemplateVariables.class), PluginRegistry.of(DefaultIdConverter.INSTANCE));
|
||||
SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(persistentEntities(), entityLinks,
|
||||
Collections.<EntityLookup<?>> emptyList(), new DefaultConversionService());
|
||||
Collections.<EntityLookup<?>> emptyList(), conversionService);
|
||||
|
||||
DefaultRepositoryInvokerFactory invokerFactory = new DefaultRepositoryInvokerFactory(repositories());
|
||||
UriToEntityConverter uriToEntityConverter = new UriToEntityConverter(persistentEntities(), invokerFactory,
|
||||
repositories());
|
||||
() -> conversionService);
|
||||
|
||||
Associations associations = new Associations(mappings, config());
|
||||
LinkCollector collector = new DefaultLinkCollector(persistentEntities(), selfLinkProvider, associations);
|
||||
|
||||
@@ -116,16 +116,18 @@ class RepositoryTestsConfig {
|
||||
@Bean
|
||||
public Module persistentEntityModule() {
|
||||
|
||||
var conversionService = new DefaultConversionService();
|
||||
|
||||
RepositoryResourceMappings mappings = new RepositoryResourceMappings(repositories(), persistentEntities(),
|
||||
config());
|
||||
EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(),
|
||||
mock(PagingAndSortingTemplateVariables.class), PluginRegistry.of(DefaultIdConverter.INSTANCE));
|
||||
SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(persistentEntities(), entityLinks,
|
||||
Collections.emptyList(), new DefaultConversionService());
|
||||
Collections.emptyList(), conversionService);
|
||||
|
||||
DefaultRepositoryInvokerFactory invokerFactory = new DefaultRepositoryInvokerFactory(repositories());
|
||||
UriToEntityConverter uriToEntityConverter = new UriToEntityConverter(persistentEntities(), invokerFactory,
|
||||
repositories());
|
||||
() -> conversionService);
|
||||
|
||||
Associations associations = new Associations(mappings, config());
|
||||
LinkCollector collector = new DefaultLinkCollector(persistentEntities(), selfLinkProvider, associations);
|
||||
|
||||
@@ -1,44 +1,51 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-rest-tests</artifactId>
|
||||
<version>4.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>Spring Data REST Tests - Shop</name>
|
||||
<artifactId>spring-data-rest-tests-shop</artifactId>
|
||||
|
||||
<properties>
|
||||
<java-module-name>spring.data.rest.tests.shop</java-module-name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>spring-data-rest-tests-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-keyvalue</artifactId>
|
||||
<version>${springdata.keyvalue}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Explicit declaration required on Java 11 and above-->
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.annotation</groupId>
|
||||
<artifactId>jakarta.annotation-api</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jmolecules</groupId>
|
||||
<artifactId>jmolecules-ddd</artifactId>
|
||||
<version>${jmolecules}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jmolecules.integrations</groupId>
|
||||
<artifactId>jmolecules-spring</artifactId>
|
||||
<version>${jmolecules-integration}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jmolecules.integrations</groupId>
|
||||
<artifactId>jmolecules-jackson</artifactId>
|
||||
<version>${jmolecules-integration}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.tests.shop;
|
||||
|
||||
import org.springframework.data.rest.core.AggregateReference;
|
||||
import org.springframework.data.rest.core.AssociationAggregateReference;
|
||||
import org.springframework.data.rest.tests.shop.Order.OrderIdentifier;
|
||||
import org.springframework.data.rest.webmvc.BasePathAwareController;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
/**
|
||||
* Custom controller to mimic a user-defined one using {@link AggregateReference}s to receive references to other Spring
|
||||
* Data REST managed aggregates.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 4.1
|
||||
*/
|
||||
@ResponseBody
|
||||
@BasePathAwareController
|
||||
class CustomController {
|
||||
|
||||
@GetMapping("/order-custom-id")
|
||||
OrderIdentifier customOrderId(@RequestParam("order") AggregateReference<Order, OrderIdentifier> reference) {
|
||||
|
||||
return reference //
|
||||
.withIdSource(it -> it.getPathSegments().get(3)) //
|
||||
.resolveId();
|
||||
}
|
||||
|
||||
@GetMapping("/order-custom-association")
|
||||
OrderIdentifier customOrderAssociation(
|
||||
@RequestParam("order") AssociationAggregateReference<Order, OrderIdentifier> reference) {
|
||||
|
||||
return reference //
|
||||
.withIdSource(it -> it.getPathSegments().get(3)) //
|
||||
.resolveAssociation() //
|
||||
.getId();
|
||||
}
|
||||
|
||||
@GetMapping("/order-custom")
|
||||
OrderIdentifier customOrder(@RequestParam("order") AggregateReference<Order, OrderIdentifier> reference) {
|
||||
|
||||
return reference //
|
||||
.withIdSource(it -> it.getPathSegments().get(3)) //
|
||||
.resolveAggregate() //
|
||||
.getId();
|
||||
}
|
||||
}
|
||||
@@ -17,28 +17,32 @@ package org.springframework.data.rest.tests.shop;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jmolecules.ddd.types.AggregateRoot;
|
||||
import org.jmolecules.ddd.types.Identifier;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.Reference;
|
||||
import org.springframework.data.rest.core.config.Projection;
|
||||
import org.springframework.data.rest.tests.shop.LineItem.LineItemProductsOnlyProjection;
|
||||
import org.springframework.data.rest.tests.shop.Order.OrderIdentifier;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
* @author Craig Andrews
|
||||
*/
|
||||
@Value
|
||||
public class Order {
|
||||
public class Order implements AggregateRoot<Order, OrderIdentifier> {
|
||||
|
||||
@Projection(name = "itemsOnly", types = Order.class)
|
||||
public interface OrderItemsOnlyProjection {
|
||||
List<LineItemProductsOnlyProjection> getItems();
|
||||
}
|
||||
|
||||
private final @Id UUID id = UUID.randomUUID();
|
||||
private final @Id OrderIdentifier id = new OrderIdentifier(UUID.randomUUID());
|
||||
private final List<LineItem> items = new ArrayList<>();
|
||||
private final @Reference Customer customer;
|
||||
|
||||
@@ -47,4 +51,10 @@ public class Order {
|
||||
this.items.add(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Value
|
||||
static class OrderIdentifier implements Identifier, Serializable {
|
||||
private static final long serialVersionUID = -3362660123468974881L;
|
||||
UUID id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,12 @@
|
||||
*/
|
||||
package org.springframework.data.rest.tests.shop;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.rest.tests.shop.Order.OrderIdentifier;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public interface OrderRepository extends CrudRepository<Order, UUID> {
|
||||
public interface OrderRepository extends CrudRepository<Order, OrderIdentifier> {
|
||||
|
||||
}
|
||||
|
||||
@@ -19,17 +19,22 @@ import jakarta.annotation.PostConstruct;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.jmolecules.jackson.JMoleculesModule;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.map.repository.config.EnableMapRepositories;
|
||||
import org.springframework.data.rest.core.config.EntityLookupRegistrar;
|
||||
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.tests.shop.Customer.Gender;
|
||||
import org.springframework.data.rest.tests.shop.Product.ProductNameOnlyProjection;
|
||||
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
|
||||
import org.springframework.hateoas.EntityModel;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.server.RepresentationModelProcessor;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
@@ -68,6 +73,33 @@ class ShopConfiguration {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
CustomController customController() {
|
||||
return new CustomController();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RepositoryRestConfigurer repositoryRestConfigurer() {
|
||||
|
||||
return new RepositoryRestConfigurer() {
|
||||
|
||||
@Override
|
||||
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
|
||||
|
||||
EntityLookupRegistrar lookup = config.withEntityLookup();
|
||||
|
||||
lookup.forRepository(ProductRepository.class, Product::getName, ProductRepository::findByName);
|
||||
lookup.forValueRepository(LineItemTypeRepository.class, LineItemType::getName,
|
||||
LineItemTypeRepository::findByName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
|
||||
objectMapper.registerModule(new JMoleculesModule());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
|
||||
@@ -86,21 +118,4 @@ class ShopConfiguration {
|
||||
|
||||
orders.save(order);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class SpringDataRestConfiguration implements RepositoryRestConfigurer {
|
||||
|
||||
@Bean
|
||||
RepositoryRestConfigurer configurer() {
|
||||
|
||||
return RepositoryRestConfigurer.withConfig(config -> {
|
||||
|
||||
EntityLookupRegistrar lookup = config.withEntityLookup();
|
||||
|
||||
lookup.forRepository(ProductRepository.class, Product::getName, ProductRepository::findByName);
|
||||
lookup.forValueRepository(LineItemTypeRepository.class, LineItemType::getName,
|
||||
LineItemTypeRepository::findByName);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,19 +16,21 @@
|
||||
package org.springframework.data.rest.tests.shop;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.rest.tests.AbstractWebIntegrationTests;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
|
||||
@@ -42,6 +44,8 @@ import com.jayway.jsonpath.JsonPath;
|
||||
@ContextConfiguration(classes = ShopConfiguration.class)
|
||||
class ShopIntegrationTests extends AbstractWebIntegrationTests {
|
||||
|
||||
@Autowired OrderRepository orders;
|
||||
|
||||
@Test
|
||||
void rendersRepresentationCorrectly() throws Exception {
|
||||
|
||||
@@ -70,7 +74,6 @@ class ShopIntegrationTests extends AbstractWebIntegrationTests {
|
||||
|
||||
client.follow(client.discoverUnique("products").expand(arguments))//
|
||||
.andExpect(status().isOk())//
|
||||
.andDo(MockMvcResultHandlers.print()) //
|
||||
.andExpect(jsonPath("$._embedded.products[0].name", notNullValue()))//
|
||||
.andExpect(jsonPath("$._embedded.products[0].price").doesNotExist());
|
||||
}
|
||||
@@ -96,6 +99,36 @@ class ShopIntegrationTests extends AbstractWebIntegrationTests {
|
||||
.andExpect(jsonPath("$._embedded.orders[0].items[0].products[0]._links.beta").exists());
|
||||
}
|
||||
|
||||
@Test // GH-2239
|
||||
void triggersCustomControllerWithAggregateReferenceToId() throws Exception {
|
||||
|
||||
var uuid = UUID.randomUUID();
|
||||
|
||||
mvc.perform(get("/order-custom-id?order=/order/foo/bar/{id}", uuid))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().string("\"%s\"".formatted(uuid.toString())));
|
||||
}
|
||||
|
||||
@Test // GH-2239
|
||||
void triggersCustomControllerWithAggregateReferenceToAggregate() throws Exception {
|
||||
|
||||
var uuid = orders.findAll().iterator().next().getId().getId();
|
||||
|
||||
mvc.perform(get("/order-custom?order=/order/foo/bar/{id}", uuid))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().string("\"%s\"".formatted(uuid.toString())));
|
||||
}
|
||||
|
||||
@Test // GH-2239
|
||||
void triggersCustomControllerWithAggregateReferenceToAssociation() throws Exception {
|
||||
|
||||
var uuid = UUID.randomUUID();
|
||||
|
||||
mvc.perform(get("/order-custom-association?order=/order/foo/bar/{id}", uuid))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().string("\"%s\"".formatted(uuid.toString())));
|
||||
}
|
||||
|
||||
private static void expectRelatedResource(String name, ResultActions actions) throws Exception {
|
||||
|
||||
int dotIndex = name.lastIndexOf('.');
|
||||
|
||||
@@ -20,7 +20,9 @@ import java.util.function.Supplier;
|
||||
|
||||
import org.jmolecules.ddd.types.Entity;
|
||||
import org.jmolecules.ddd.types.Identifier;
|
||||
import org.jmolecules.spring.AssociationToPrimitivesConverter;
|
||||
import org.jmolecules.spring.IdentifierToPrimitivesConverter;
|
||||
import org.jmolecules.spring.PrimitivesToAssociationConverter;
|
||||
import org.jmolecules.spring.PrimitivesToIdentifierConverter;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
@@ -73,8 +75,13 @@ class JMoleculesConfigurer implements WebMvcConfigurer, RepositoryRestConfigurer
|
||||
|
||||
Supplier<ConversionService> supplier = () -> conversionService;
|
||||
|
||||
conversionService.addConverter(new PrimitivesToIdentifierConverter(supplier));
|
||||
conversionService.addConverter(new IdentifierToPrimitivesConverter(supplier));
|
||||
var primitivesToIdentifierConverter = new PrimitivesToIdentifierConverter(supplier);
|
||||
var identifierToPrimitivesConverter = new IdentifierToPrimitivesConverter(supplier);
|
||||
|
||||
conversionService.addConverter(primitivesToIdentifierConverter);
|
||||
conversionService.addConverter(identifierToPrimitivesConverter);
|
||||
conversionService.addConverter(new AssociationToPrimitivesConverter<>(identifierToPrimitivesConverter));
|
||||
conversionService.addConverter(new PrimitivesToAssociationConverter<>(primitivesToIdentifierConverter));
|
||||
}
|
||||
|
||||
@Lazy
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
@@ -320,13 +321,15 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
@Bean
|
||||
@Qualifier
|
||||
public DefaultFormattingConversionService defaultConversionService(PersistentEntities persistentEntities,
|
||||
RepositoryInvokerFactory repositoryInvokerFactory, Repositories repositories) {
|
||||
RepositoryInvokerFactory repositoryInvokerFactory) {
|
||||
|
||||
DefaultFormattingConversionService conversionService = (DefaultFormattingConversionService) defaultConversionService;
|
||||
var conversionService = (DefaultFormattingConversionService) defaultConversionService;
|
||||
Supplier<ConversionService> supplier = () -> conversionService;
|
||||
|
||||
// Add Spring Data Commons formatters
|
||||
conversionService
|
||||
.addConverter(new UriToEntityConverter(persistentEntities, repositoryInvokerFactory, repositories));
|
||||
.addConverter(new UriToEntityConverter(persistentEntities, repositoryInvokerFactory, supplier));
|
||||
conversionService.addConverter(new StringToAggregateReferenceConverter(supplier));
|
||||
conversionService.addConverter(StringToLdapNameConverter.INSTANCE);
|
||||
addFormatters(conversionService);
|
||||
|
||||
@@ -638,8 +641,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
}
|
||||
|
||||
/**
|
||||
* Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in
|
||||
* the provided controller classes.
|
||||
* Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the
|
||||
* provided controller classes.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@@ -734,7 +737,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
PluginRegistry.of(getEntityLookups()));
|
||||
|
||||
return new PersistentEntityJackson2Module(associationLinks.get(), persistentEntities.get(),
|
||||
new UriToEntityConverter(persistentEntities.get(), repositoryInvokerFactory.get(), repositories.get()),
|
||||
new UriToEntityConverter(persistentEntities.get(), repositoryInvokerFactory.get(),
|
||||
() -> defaultConversionService),
|
||||
linkCollector, repositoryInvokerFactory.get(), lookupObjectSerializer, invoker.getObject(), assembler);
|
||||
}
|
||||
|
||||
@@ -954,7 +958,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
|
||||
objectMapper.registerModule(geoModule.getObject());
|
||||
objectMapper.registerModule(new AggregateReferenceResolvingModule(
|
||||
new UriToEntityConverter(persistentEntities.get(), repositoryInvokerFactory.get(), repositories.get()),
|
||||
new UriToEntityConverter(persistentEntities.get(), repositoryInvokerFactory.get(),
|
||||
() -> defaultConversionService),
|
||||
resourceMappings.get()));
|
||||
|
||||
if (repositoryRestConfiguration.get().isEnableEnumTranslation()) {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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.webmvc.config;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jmolecules.ddd.types.AggregateRoot;
|
||||
import org.jmolecules.ddd.types.Identifier;
|
||||
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;
|
||||
import org.springframework.data.rest.core.AggregateReference;
|
||||
import org.springframework.data.rest.core.AssociationAggregateReference;
|
||||
import org.springframework.data.rest.core.ResolvingAggregateReference;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* A {@link GenericConverter} to convert {@link String}s into {@link AggregateReference} instance for the latter to be
|
||||
* injectable into Spring MVC controller methods.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 4.1
|
||||
*/
|
||||
class StringToAggregateReferenceConverter implements GenericConverter {
|
||||
|
||||
private static final boolean JMOLECULES_PRESENT = ClassUtils.isPresent(
|
||||
"org.jmolecules.spring.IdentifierToPrimitivesConverter",
|
||||
StringToAggregateReferenceConverter.class.getClassLoader());
|
||||
private static final Class<?> ASSOCIATION_AGGREGATE_REFERENCE_TYPE = tryToLoadAssociationReferenceClass();
|
||||
|
||||
private final Supplier<ConversionService> conversionService;
|
||||
|
||||
/**
|
||||
* Creates a new {@link StringToAggregateReferenceConverter} for the given {@link ConversionService}.
|
||||
*
|
||||
* @param conversionService must not be {@literal null}.
|
||||
*/
|
||||
StringToAggregateReferenceConverter(Supplier<ConversionService> conversionService) {
|
||||
|
||||
Assert.notNull(conversionService, "ConversionService must not be null!");
|
||||
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
private static Class<?> tryToLoadAssociationReferenceClass() {
|
||||
|
||||
var classLoader = StringToAggregateReferenceConverter.class.getClassLoader();
|
||||
|
||||
if (!ClassUtils.isPresent("org.jmolecules.ddd.types.Association", classLoader)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return ClassUtils.forName("org.springframework.data.rest.core.AssociationAggregateReference", classLoader);
|
||||
} catch (ClassNotFoundException o_O) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return Set.of(new ConvertiblePair(String.class, AggregateReference.class));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AggregateReference<?, ?> convert(@Nullable Object source, TypeDescriptor sourceType,
|
||||
TypeDescriptor targetType) {
|
||||
|
||||
if (source == null) {
|
||||
throw new ConversionFailedException(sourceType, targetType, source,
|
||||
new IllegalArgumentException("Source value must not be null"));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
var uri = new URI(source.toString());
|
||||
var resolvableType = targetType.getResolvableType();
|
||||
|
||||
var aggregateDescriptor = new TypeDescriptor(resolvableType.getGeneric(0), null, targetType.getAnnotations());
|
||||
var identifierDescriptor = new TypeDescriptor(resolvableType.getGeneric(1), null, targetType.getAnnotations());
|
||||
|
||||
Function<Object, Object> aggregateResolver = it -> conversionService.get().convert(it, sourceType,
|
||||
aggregateDescriptor);
|
||||
Function<Object, Object> identifierResolver = it -> conversionService.get().convert(it, sourceType,
|
||||
identifierDescriptor);
|
||||
|
||||
var result = new ResolvingAggregateReference<>(uri, aggregateResolver, identifierResolver);
|
||||
|
||||
return JMOLECULES_PRESENT && resolvableType.toClass().equals(ASSOCIATION_AGGREGATE_REFERENCE_TYPE) //
|
||||
? withJMolecules(result) //
|
||||
: result;
|
||||
|
||||
} catch (URISyntaxException e) {
|
||||
throw new ConversionFailedException(sourceType, targetType, source, e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private static AggregateReference<?, ?> withJMolecules(AggregateReference<?, ?> source) {
|
||||
return new ResolvingAssociationAggregateReference(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link AssociationAggregateReference} delegating to a simple {@link AggregateReference}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 4.1
|
||||
*/
|
||||
private static class ResolvingAssociationAggregateReference<T extends AggregateRoot<T, ID>, ID extends Identifier>
|
||||
implements AssociationAggregateReference<T, ID> {
|
||||
|
||||
private AggregateReference<T, ID> delegate;
|
||||
|
||||
ResolvingAssociationAggregateReference(AggregateReference<T, ID> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getUri() {
|
||||
return delegate.getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ID resolveId() {
|
||||
return delegate.resolveId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T resolveAggregate() {
|
||||
return delegate.resolveAggregate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssociationAggregateReference<T, ID> withIdSource(Function<UriComponents, Object> extractor) {
|
||||
return new ResolvingAssociationAggregateReference<T, ID>(delegate.withIdSource(extractor));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.webmvc.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jmolecules.ddd.types.Association;
|
||||
import org.jmolecules.ddd.types.Identifier;
|
||||
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.support.DefaultConversionService;
|
||||
import org.springframework.data.rest.core.AggregateReference;
|
||||
import org.springframework.data.rest.core.AssociationAggregateReference;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link StringToAggregateReferenceConverter}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StringToAggregateReferenceConverterUnitTests {
|
||||
|
||||
@Mock ConversionService conversionService;
|
||||
|
||||
StringToAggregateReferenceConverter converter = new StringToAggregateReferenceConverter(() -> conversionService);
|
||||
|
||||
@Test // GH-2239
|
||||
void convertsUriIntoAggregateReference() {
|
||||
|
||||
var aggregate = new Object();
|
||||
|
||||
when(conversionService.convert(any(), any(), eq(toTypeDescriptor(Long.class)))).thenReturn(42L);
|
||||
when(conversionService.convert(any(), any(), eq(toTypeDescriptor(Object.class)))).thenReturn(aggregate);
|
||||
|
||||
var source = "/foo/42";
|
||||
|
||||
var result = converter.convert(source, TypeDescriptor.valueOf(String.class),
|
||||
toTypeDescriptor(AggregateReference.class, Object.class, Long.class));
|
||||
|
||||
assertThat(result.getUri()).isEqualTo(URI.create(source));
|
||||
assertThat(result.resolveId()).isEqualTo(42L);
|
||||
assertThat(result.resolveAggregate()).isEqualTo(aggregate);
|
||||
}
|
||||
|
||||
@Test // GH-2239
|
||||
void convertsUriIntoAggregateReferenceUsingCustomExtractor() {
|
||||
|
||||
var aggregate = new Object();
|
||||
|
||||
when(conversionService.convert(any(), any(), eq(toTypeDescriptor(Long.class)))).thenReturn(42L);
|
||||
when(conversionService.convert(any(), any(), eq(toTypeDescriptor(Object.class)))).thenReturn(aggregate);
|
||||
|
||||
var source = "/foo/42";
|
||||
|
||||
var result = converter.convert(source, TypeDescriptor.valueOf(String.class),
|
||||
toTypeDescriptor(AggregateReference.class, Object.class, Long.class));
|
||||
|
||||
result = result.withIdSource(it -> it.getPathSegments().get(1));
|
||||
|
||||
assertThat(result.getUri()).isEqualTo(URI.create(source));
|
||||
assertThat(result.resolveId()).isEqualTo(42L);
|
||||
assertThat(result.resolveAggregate()).isEqualTo(aggregate);
|
||||
}
|
||||
|
||||
@Test // GH-2239
|
||||
void createsAssociationAggregateReference() {
|
||||
|
||||
var identifier = new CustomIdentifier();
|
||||
|
||||
when(conversionService.convert(any(), any(), eq(toTypeDescriptor(CustomIdentifier.class)))).thenReturn(identifier);
|
||||
|
||||
var source = "/foo/42";
|
||||
|
||||
var result = converter.convert(source, TypeDescriptor.valueOf(String.class),
|
||||
toTypeDescriptor(AssociationAggregateReference.class, Object.class, CustomIdentifier.class));
|
||||
|
||||
assertThat(result).isInstanceOfSatisfying(AssociationAggregateReference.class, it -> {
|
||||
assertThat(it.resolveAssociation()).isNotNull()
|
||||
.extracting(Association::getId).isEqualTo(identifier);
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-2239
|
||||
void rejectsNullSource() {
|
||||
|
||||
assertThatExceptionOfType(ConversionFailedException.class)
|
||||
.isThrownBy(() -> converter.convert(null, TypeDescriptor.valueOf(String.class),
|
||||
toTypeDescriptor(AggregateReference.class, Object.class, UUID.class)));
|
||||
}
|
||||
|
||||
@Test // GH-2239
|
||||
void rejectsInvalidURI() {
|
||||
|
||||
assertThatExceptionOfType(ConversionFailedException.class)
|
||||
.isThrownBy(() -> converter.convert("@\\", TypeDescriptor.valueOf(String.class),
|
||||
toTypeDescriptor(AggregateReference.class, Object.class, UUID.class)));
|
||||
}
|
||||
|
||||
@Test // GH-2239
|
||||
void registersConversions() {
|
||||
|
||||
var service = new DefaultConversionService();
|
||||
service.addConverter(converter);
|
||||
|
||||
assertThat(service.canConvert(String.class, AggregateReference.class)).isTrue();
|
||||
assertThat(service.canConvert(String.class, AssociationAggregateReference.class)).isTrue();
|
||||
}
|
||||
|
||||
private static TypeDescriptor toTypeDescriptor(Class<?> type, Class<?>... generics) {
|
||||
|
||||
var resolvableType = ResolvableType.forClassWithGenerics(type, generics);
|
||||
|
||||
return new TypeDescriptor(resolvableType, null, null);
|
||||
}
|
||||
|
||||
private static class CustomIdentifier implements Identifier {}
|
||||
}
|
||||
@@ -1,43 +1,44 @@
|
||||
[[customizing-sdr.overriding-sdr-response-handlers]]
|
||||
= Overriding Spring Data REST Response Handlers
|
||||
|
||||
Sometimes, you may want to write a custom handler for a specific resource. To take advantage of Spring Data REST's settings, message converters, exception handling, and more, use the `@RepositoryRestController` annotation instead of a standard Spring MVC `@Controller` or `@RestController`. Controllers annotated with `@RepositoryRestController` are served from the API base path defined in `RepositoryRestConfiguration.setBasePath`, which is used by all other RESTful endpoints (for example, `/api`). The following example shows how to use the `@RepositoryRestController` annotation:
|
||||
Sometimes, you may want to write a custom handler for a specific resource.
|
||||
To take advantage of Spring Data REST's settings, message converters, exception handling, and more, use the `@RepositoryRestController` annotation instead of a standard Spring MVC `@Controller` or `@RestController`.
|
||||
Controllers annotated with `@RepositoryRestController` are served from the API base path defined in `RepositoryRestConfiguration.setBasePath`, which is used by all other RESTful endpoints (for example, `/api`).
|
||||
The following example shows how to use the `@RepositoryRestController` annotation:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@RepositoryRestController
|
||||
public class ScannerController {
|
||||
@BasePathAwareController
|
||||
class ScannerController {
|
||||
|
||||
private final ScannerRepository repository;
|
||||
private final ScannerRepository repository;
|
||||
|
||||
@Autowired
|
||||
public ScannerController(ScannerRepository repo) { // <1>
|
||||
repository = repo;
|
||||
}
|
||||
ScannerController(ScannerRepository repository) { // <1>
|
||||
repository = repository;
|
||||
}
|
||||
|
||||
@RequestMapping(method = GET, value = "/scanners/search/listProducers") // <2>
|
||||
public @ResponseBody ResponseEntity<?> getProducers() {
|
||||
List<String> producers = repository.listProducers(); // <3>
|
||||
@GetMapping(path = "/scanners/search/producers") // <2>
|
||||
ResponseEntity<?> getProducers() {
|
||||
|
||||
//
|
||||
// do some intermediate processing, logging, etc. with the producers
|
||||
//
|
||||
List<String> producers = repository.listProducers(); // <3>
|
||||
|
||||
CollectionModel<String> resources = CollectionModel.of(producers); // <4>
|
||||
//
|
||||
// do some intermediate processing, logging, etc. with the producers
|
||||
//
|
||||
|
||||
resources.add(linkTo(methodOn(ScannerController.class).getProducers()).withSelfRel()); // <5>
|
||||
CollectionModel<String> resources = CollectionModel.of(producers); // <4>
|
||||
|
||||
// add other links as needed
|
||||
resources.add(linkTo(methodOn(ScannerController.class).getProducers()).withSelfRel()); // <5>
|
||||
|
||||
return ResponseEntity.ok(resources); // <6>
|
||||
}
|
||||
// add other links as needed
|
||||
|
||||
return ResponseEntity.ok(resources); // <6>
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
<1> This example uses constructor injection.
|
||||
<2> This handler plugs in a custom handler for a Spring Data finder method.
|
||||
<2> This handler plugs in a custom handler method as query method resource
|
||||
<3> This handler uses the underlying repository to fetch data, but then does some form of post processing before returning the final data set to the client.
|
||||
<4> The results of type T need to be wrapped up in a Spring HATEOAS `CollectionModel<T>` object to return a collection. `EntityModel<T>` or `RepresentationModel<T>` are suitable wrappers for a single item, respectively.
|
||||
<5> Add a link back to this exact method as a `self` link.
|
||||
@@ -48,8 +49,51 @@ public class ScannerController {
|
||||
|
||||
IMPORTANT: In this example, the combined path is `RepositoryRestConfiguration.getBasePath()` + `/scanners/search/listProducers`.
|
||||
|
||||
[[customizing-sdr.aggregate-references]
|
||||
== Obtaining Aggregate References
|
||||
|
||||
For custom controllers receiving `PUT` and `POST` requests, the request body usually contains a JSON document that will use URIs to express references to other resources.
|
||||
For `GET` requests, those references are handed in via a request parameter.
|
||||
|
||||
As of Spring Data REST 4.1, we provide `AggregateReference<T, ID>` to be used as handler method parameter type to capture such references and resolve them into either the referenced aggregate's identifier, the aggregate itself or a jMolecules `Association`.
|
||||
All you need to do is declare an `@RequestParam` of that type and then consume either the identifier or the fully resolved aggregate.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@BasePathAwareController
|
||||
class ScannerController {
|
||||
|
||||
private final ScannerRepository repository;
|
||||
|
||||
ScannerController(ScannerRepository repository) {
|
||||
repository = repository;
|
||||
}
|
||||
|
||||
@GetMapping(path = "/scanners")
|
||||
ResponseEntity<?> getProducers(
|
||||
@RequestParam AggregateReference<Producer, ProducerIdentifier> producer) {
|
||||
|
||||
var identifier = producer.resolveRequiredId();
|
||||
// Alternatively
|
||||
var aggregate = producer.resolveRequiredAggregate();
|
||||
}
|
||||
|
||||
// Alternatively
|
||||
|
||||
@GetMapping(path = "/scanners")
|
||||
ResponseEntity<?> getProducers(
|
||||
@RequestParam AssociationAggregateReference<Producer, ProducerIdentifier> producer) {
|
||||
|
||||
var association = producer.resolveRequiredAssociation();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
In case you are using jMolecules, `AssociationAggregateReference` also allows you to obtain an `Association`.
|
||||
While both of the abstraction assume the value for the parameter to be a URI matching the scheme that Spring Data REST uses to expose item resources, that source value resolution can be customized by calling `….withIdSource(…)` on the reference instance to provide a function to extract the identifier value to be used for aggregate resolution eventually from the `UriComponents` obtained from the URI received.
|
||||
|
||||
[[customizing-sdr.overriding-sdr-response-handlers.annotations]]
|
||||
== @RepositoryRestResource VS. @BasePathAwareController
|
||||
== `@RepositoryRestResource` VS. `@BasePathAwareController`
|
||||
|
||||
If you are not interested in entity-specific operations but still want to build custom operations underneath `basePath`, such as Spring MVC views, resources, and others, use `@BasePathAwareController`.
|
||||
If you're using `@RepositoryRestController` on your custom controller, it will only handle the request if your request mappings blend into the URI space used by the repository.
|
||||
|
||||
Reference in New Issue
Block a user