Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ContainerMethod): @ContainerMethod supports extracting actual data from specified properties in the wrapper class (GitHub #266) #277

Merged
merged 1 commit into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading