Skip to content

Commit

Permalink
feat(ContainerMethod): @ContainerMethod supports extracting actual da…
Browse files Browse the repository at this point in the history
…ta from specified properties in the wrapper class (GitHub #266)
  • Loading branch information
Createsequence committed Apr 29, 2024
1 parent 54c0c68 commit b02c2e3
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,32 @@
*/
Class<?>[] bindMethodParamTypes() default {};

/**
* <p>When the return value is a wrapper class,
* we can specify to obtain the dataset to be processed
* from the specific field of the wrapper class,
* and then use it to be data source of the container.
*
* <p>For example:
* <pre type="code">{@code
* // general response
* public static class Result<T> {
* private Integer code;
* private T data; // objects to be processed
* }
* // process general response
* @ContainerMethod(resultType = Foo.class, on = "data")
* public Result<List<Foo>> requestFoo() { // do something }
* }</pre>
* The return value of the method is<i>Result</i>, but the data is in <i>Result.data</i>,
* obtain data from specific fields for <i>on</i>.
*
* @return field name
* @since 2.8.0
* @see AutoOperate#on()
*/
String on() default "";

/**
* Batch operation.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import cn.crane4j.core.util.CollectionUtils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.Collection;
Expand All @@ -14,6 +16,7 @@
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -56,6 +59,25 @@ public abstract class MethodInvokerContainer implements Container<Object> {
@Nullable
protected final Object target;

/**
* The extractor will be used to extract the data from the wrapped object.
*/
@Setter
@Nullable
protected MethodInvoker extractor;

/**
* extract data from the result if possible.
*
* @param result result
* @return extracted data
* @since 2.8.0
*/
@Nullable
protected Object extractIfPossible(@NonNull Object result) {
return Objects.nonNull(extractor) ? extractor.invoke(result) : result;
}

/**
* Create a standard method data source container.
*
Expand Down Expand Up @@ -139,10 +161,10 @@ public SingleKey(String namespace, MethodInvoker methodInvoker, @Nullable Object
@Override
public Map<Object, ?> get(Collection<Object> keys) {
Map<Object, Object> results = new HashMap<>(keys.size());
keys.forEach(key -> {
Object result = methodInvoker.invoke(target, key);
results.put(key, result);
});
keys.forEach(key -> Optional.ofNullable(methodInvoker.invoke(target, key))
.map(this::extractIfPossible)
.ifPresent(result -> results.put(key, result))
);
return results;
}
}
Expand All @@ -167,11 +189,10 @@ public StandardMethodInvokerContainer(String namespace, MethodInvoker methodInvo
@Override
public Map<Object, ?> get(Collection<Object> keys) {
Object[] arguments = resolveArguments(keys);
Object result = methodInvoker.invoke(target, arguments);
if (Objects.isNull(result)) {
return Collections.emptyMap();
}
return resolveResult(keys, result);
return Optional.ofNullable(methodInvoker.invoke(target, arguments))
.map(this::extractIfPossible)
.map(result -> resolveResult(keys, result))
.orElse(Collections.emptyMap());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,16 @@ public List<Container<Object>> get(
}

private Container<Object> createContainer(Object source, Method method, ContainerMethod annotation) {
return methodInvokerContainerCreator.createContainer(
source, method, annotation.type(), annotation.namespace(),
annotation.resultType(), annotation.resultKey(), annotation.duplicateStrategy()
);
MethodInvokerContainerCreator.MethodInvokerContainerCreation containerCreation = MethodInvokerContainerCreator.MethodInvokerContainerCreation.builder()
.target(source)
.method(method)
.mappingType(annotation.type())
.namespace(annotation.namespace())
.resultType(annotation.resultType())
.resultKey(annotation.resultKey())
.duplicateStrategy(annotation.duplicateStrategy())
.on(annotation.on())
.build();
return methodInvokerContainerCreator.createContainer(containerCreation);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
* Support class for {@link MethodInvokerContainer} creation.
Expand All @@ -39,69 +40,37 @@ public class MethodInvokerContainerCreator {
protected final PropertyOperator propertyOperator;
protected final ConverterManager converterManager;

/**
* Create a {@link MethodInvokerContainer} from the given method.
*
* @param target method's calling object, if the method is static, it can be null
* @param method method
* @param mappingType mapping type
* @param namespace namespace, if null, use method name as namespace
* @param resultType result type
* @param resultKey result key
* @param duplicateStrategy duplicate strategy
* @return {@link MethodInvokerContainer}
*/
public MethodInvokerContainer createContainer(
@Nullable Object target, Method method, MappingType mappingType,
@Nullable String namespace, Class<?> resultType, String resultKey, DuplicateStrategy duplicateStrategy) {
log.debug("create method container from [{}]", method);
MethodInvokerContainerCreation containerCreation = MethodInvokerContainerCreation.builder()
.target(target)
.method(method)
.methodInvoker(getMethodInvoker(target, method))
.mappingType(mappingType)
.namespace(namespace)
.resultType(resultType)
.resultKey(resultKey)
.duplicateStrategy(duplicateStrategy)
.build();
return doCreateContainer(containerCreation);
}

/**
* Create a {@link MethodInvokerContainer} from the given method.
*
* @param target method's calling object, if the method is static, it can be null
* @param methodInvoker method invoker
* @param mappingType mapping type
* @param namespace namespace, if null, use method name as namespace
* @param resultType result type
* @param resultKey result key
* @param duplicateStrategy duplicate strategy
* @return {@link MethodInvokerContainer}
*/
public MethodInvokerContainer createContainer(
@Nullable Object target, MethodInvoker methodInvoker, MappingType mappingType,
@Nullable String namespace, Class<?> resultType, String resultKey, DuplicateStrategy duplicateStrategy) {
MethodInvokerContainerCreation containerCreation = MethodInvokerContainerCreation.builder()
.target(target)
.methodInvoker(methodInvoker)
.mappingType(mappingType)
.namespace(namespace)
.resultType(resultType)
.resultKey(resultKey)
.duplicateStrategy(duplicateStrategy)
.build();
return doCreateContainer(containerCreation);
}

private MethodInvokerContainer doCreateContainer(MethodInvokerContainerCreation containerCreation) {
MethodInvokerContainerCreation containerCreation) {
Object target = containerCreation.getTarget();
Method method = containerCreation.getMethod();
String namespace = getNamespace(method, containerCreation.getNamespace());
MappingType mappingType = containerCreation.getMappingType();
MethodInvoker methodInvoker = containerCreation.getMethodInvoker();
Object target = containerCreation.getTarget();
MethodInvoker methodInvoker = Optional.ofNullable(containerCreation.getMethodInvoker())
.orElseGet(() -> adaptMethodToInvoker(target, method));

MethodInvokerContainer container = doCreateContainer(
containerCreation, mappingType, target, methodInvoker, method, namespace);

// https://github.com/opengoofy/crane4j/issues/266
String extractProperty = containerCreation.getOn();
if (StringUtils.isNotEmpty(extractProperty)) {
MethodInvoker extractor = (t, args) ->
propertyOperator.readProperty(t.getClass(), t, extractProperty);
container.setExtractor(extractor);
}

if (Objects.isNull(method)) {
log.info("create method invoker container [{}], mapping type is [{}]", container.getNamespace(), mappingType);
} else {
log.info("create method invoker container [{}] for method [{}], mapping type is [{}]", container.getNamespace(), method, mappingType);
}
return container;
}

private MethodInvokerContainer doCreateContainer(
MethodInvokerContainerCreation containerCreation, MappingType mappingType,
Object target, MethodInvoker methodInvoker, Method method, String namespace) {
MethodInvokerContainer container;
if (mappingType == MappingType.NO_MAPPING) {
container = doCreateNoMappingContainer(target, methodInvoker, method, namespace);
Expand All @@ -112,26 +81,18 @@ private MethodInvokerContainer doCreateContainer(MethodInvokerContainerCreation
doCreateSingleKeyContainer(target, methodInvoker, namespace) :
doCreateOrderOfKeysContainer(target, methodInvoker, method, namespace);
} else if (mappingType == MappingType.ONE_TO_ONE) {
container = doCreateOneToOneContainer(
target, methodInvoker, method, namespace,
container = doCreateOneToOneContainer(target, methodInvoker, method, namespace,
containerCreation.getResultType(), containerCreation.getResultKey(),
containerCreation.getDuplicateStrategy()
);
} else if (mappingType == MappingType.ONE_TO_MANY) {
container = doCreateOneToManyContainer(
target, methodInvoker, method, namespace,
container = doCreateOneToManyContainer(target, methodInvoker, method, namespace,
containerCreation.getResultType(), containerCreation.getResultKey(),
containerCreation.getDuplicateStrategy()
);
} else {
throw new Crane4jException("unsupported mapping type: " + mappingType);
}

if (Objects.isNull(method)) {
log.debug("create method invoker container [{}], mapping type is [{}]", container.getNamespace(), mappingType);
} else {
log.debug("create method invoker container [{}] for method [{}], mapping type is [{}]", container.getNamespace(), method, mappingType);
}
return container;
}

Expand Down Expand Up @@ -188,7 +149,7 @@ protected MethodInvokerContainer doCreateOneToManyContainer(
* otherwise invoke method on target object
*/
@NonNull
protected MethodInvoker getMethodInvoker(Object target, Method method) {
protected MethodInvoker adaptMethodToInvoker(Object target, Method method) {
MethodInvoker invoker = ReflectiveMethodInvoker.create(target, method, false);
return ParameterConvertibleMethodInvoker.create(invoker, converterManager, method.getParameterTypes());
}
Expand Down Expand Up @@ -255,7 +216,7 @@ protected MethodInvoker findKeyGetter(Class<?> resultType, String resultKey) {

@Getter
@Builder
private static class MethodInvokerContainerCreation {
public static class MethodInvokerContainerCreation {
@Nullable
private final Object target;
@Nullable
Expand All @@ -267,5 +228,7 @@ private static class MethodInvokerContainerCreation {
private final Class<?> resultType;
private final String resultKey;
private final DuplicateStrategy duplicateStrategy;
@Nullable
private final String on;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,18 @@ private Container<?> createQueryContainer(String namespace) {
private MethodInvokerContainer doCreateContainer(
String namespace, String keyProperty, Repository<T> repository, QueryInfo queryInfo, Set<String> queryColumns, String keyColumn) {
MethodInvoker methodInvoker = createMethodInvoker(
namespace, repository, queryColumns, keyColumn, keyProperty
);
return methodInvokerContainerCreator.createContainer(
repository.getTarget(), methodInvoker, queryInfo.getMappingType(),
namespace, repository.getEntityType(), keyProperty, DuplicateStrategy.ALERT
namespace, repository, queryColumns, keyColumn, keyProperty
);
MethodInvokerContainerCreator.MethodInvokerContainerCreation containerCreation = MethodInvokerContainerCreator.MethodInvokerContainerCreation.builder()
.target(repository.getTarget())
.methodInvoker(methodInvoker)
.mappingType(queryInfo.getMappingType())
.namespace(namespace)
.resultType(repository.getEntityType())
.resultKey(keyProperty)
.duplicateStrategy(DuplicateStrategy.ALERT)
.build();
return methodInvokerContainerCreator.createContainer(containerCreation);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.junit.Assert;
import org.junit.Test;

Expand Down Expand Up @@ -110,6 +111,20 @@ public void getWhenNotKeyExtractor() {
Assert.assertEquals(foo2, map.get("1"));
}

@Test
public void getWhenWrapper() {
MethodInvokerContainer container = MethodInvokerContainer.oneToOne(
MethodInvokerContainer.class.getSimpleName(),
(t, arg) -> service.wrapperMethod((Collection<String>)arg[0]),
service, t -> ((Foo) t).key, DuplicateStrategy.ALERT
);
container.setExtractor((t, args) -> ((Result<Collection<Foo>>)t).getData());
Assert.assertEquals(MethodInvokerContainer.class.getSimpleName(), container.getNamespace());
Map<Object, ?> map = container.get(Arrays.asList("2", "1"));
Assert.assertEquals(foo1, map.get("1"));
Assert.assertEquals(foo2, map.get("2"));
}

@AllArgsConstructor
@EqualsAndHashCode
@Getter
Expand All @@ -128,5 +143,14 @@ public Map<String, Foo> mappedMethod(Collection<String> key) {
public List<Foo> noneMappedMethod(Collection<String> key) {
return Objects.isNull(key) ? null : Arrays.asList(foo1, foo2);
}
public Result<Collection<Foo>> wrapperMethod(Collection<String> key) {
return new Result<>(Arrays.asList(foo1, foo2));
}
}

@Getter
@RequiredArgsConstructor
public static class Result<T> {
private final T data;
}
}
Loading

0 comments on commit b02c2e3

Please sign in to comment.