Skip to content

Commit

Permalink
Support to receive aggregate references as request parameters.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odrotbohm committed Mar 19, 2023
1 parent 6d0034f commit e4bca53
Show file tree
Hide file tree
Showing 21 changed files with 1,025 additions and 129 deletions.
14 changes: 14 additions & 0 deletions spring-data-rest-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading

0 comments on commit e4bca53

Please sign in to comment.