From e90780406703b89307ed7ca11315528760473309 Mon Sep 17 00:00:00 2001 From: huangchengxing <841396397@qq.com> Date: Wed, 20 Sep 2023 23:56:49 +0800 Subject: [PATCH 1/2] feat(DuplicateStrategy): support when building containers based on enumerations or methods, if there are multiple corresponding values for the same key, it is allowed to be processed according to a specific policy (Gitee #I832VQ) --- .../cn/crane4j/annotation/ContainerEnum.java | 8 ++ .../crane4j/annotation/ContainerMethod.java | 8 ++ .../crane4j/annotation/DuplicateStrategy.java | 96 +++++++++++++++++++ .../core/container/EnumContainerBuilder.java | 35 ++++++- .../container/MethodInvokerContainer.java | 16 +++- .../DefaultMethodContainerFactory.java | 17 +++- .../container/EnumContainerBuilderTest.java | 31 +++++- .../core/util/DuplicateStrategyTest.java | 33 +++++++ .../crane4j/core/util/ReflectUtilsTest.java | 17 +++- 9 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 crane4j-annotation/src/main/java/cn/crane4j/annotation/DuplicateStrategy.java create mode 100644 crane4j-core/src/test/java/cn/crane4j/core/util/DuplicateStrategyTest.java diff --git a/crane4j-annotation/src/main/java/cn/crane4j/annotation/ContainerEnum.java b/crane4j-annotation/src/main/java/cn/crane4j/annotation/ContainerEnum.java index 692cc714..44858eaf 100644 --- a/crane4j-annotation/src/main/java/cn/crane4j/annotation/ContainerEnum.java +++ b/crane4j-annotation/src/main/java/cn/crane4j/annotation/ContainerEnum.java @@ -38,4 +38,12 @@ public @interface ContainerEnum { * @return value field name */ String value() default ""; + + /** + * The strategy for handling duplicate keys. + * + * @return strategy + * @since 2.2.0 + */ + DuplicateStrategy duplicateStrategy() default DuplicateStrategy.ALERT; } diff --git a/crane4j-annotation/src/main/java/cn/crane4j/annotation/ContainerMethod.java b/crane4j-annotation/src/main/java/cn/crane4j/annotation/ContainerMethod.java index fe981e61..8fa8db0e 100644 --- a/crane4j-annotation/src/main/java/cn/crane4j/annotation/ContainerMethod.java +++ b/crane4j-annotation/src/main/java/cn/crane4j/annotation/ContainerMethod.java @@ -69,6 +69,14 @@ public @interface ContainerMethod { */ MappingType type() default MappingType.ONE_TO_ONE; + /** + * The strategy for handling duplicate keys. + * + * @return strategy + * @since 2.2.0 + */ + DuplicateStrategy duplicateStrategy() default DuplicateStrategy.ALERT; + /** * The key field of the data source object returned by the method.
* If {@link #type()} is {@link MappingType#MAPPED}, this parameter is ignored. diff --git a/crane4j-annotation/src/main/java/cn/crane4j/annotation/DuplicateStrategy.java b/crane4j-annotation/src/main/java/cn/crane4j/annotation/DuplicateStrategy.java new file mode 100644 index 00000000..14d2fff9 --- /dev/null +++ b/crane4j-annotation/src/main/java/cn/crane4j/annotation/DuplicateStrategy.java @@ -0,0 +1,96 @@ +package cn.crane4j.annotation; + +import lombok.RequiredArgsConstructor; + +/** + * An enumeration that defines the strategy for handling duplicate keys. + * + * @author huangchengxing + * @since 2.2.0 + */ +@RequiredArgsConstructor +public enum DuplicateStrategy { + + /** + * Throws an exception when the key already exists. + * + * @see IllegalArgumentException + */ + ALERT(new Selector() { + @Override + public V choose(K key, V oldVal, V newVal) { + throw new IllegalArgumentException( + "Duplicate key [" + key + "] has been associated with value [" + oldVal + "]," + + " can no longer be associated with [" + newVal +"]" + ); + } + }), + + /** + * When the key already exists, discard the old key value. + */ + DISCARD_NEW(new Selector() { + @Override + public V choose(K key, V oldVal, V newVal) { + return oldVal; + } + }), + + /** + * When the keys are the same, discard the new key value. + */ + DISCARD_OLD(new Selector() { + @Override + public V choose(K key, V oldVal, V newVal) { + return newVal; + } + }), + + /** + * When the keys are the same, discard the new value and old value, return null. + */ + DISCARD(new Selector() { + @Override + public V choose(K key, V oldVal, V newVal) { + return null; + } + }) + + ; + + /** + * selector + */ + private final Selector selector; + + /** + * Choose to return one of the old and new values. + * + * @param key key + * @param oldVal old val + * @param newVal new val + * @return val + */ + public V choose(K key, V oldVal, V newVal) { + return selector.choose(key, oldVal, newVal); + } + + /** + * Internal interface, used to select the value to be returned when the key is duplicated. + * + * @author huangchengxing + */ + @FunctionalInterface + private interface Selector { + + /** + * Choose to return one of the old and new values. + * + * @param key key + * @param oldVal old val + * @param newVal new val + * @return val + */ + V choose(K key, V oldVal, V newVal); + } +} diff --git a/crane4j-core/src/main/java/cn/crane4j/core/container/EnumContainerBuilder.java b/crane4j-core/src/main/java/cn/crane4j/core/container/EnumContainerBuilder.java index 701971b2..8a739fb6 100644 --- a/crane4j-core/src/main/java/cn/crane4j/core/container/EnumContainerBuilder.java +++ b/crane4j-core/src/main/java/cn/crane4j/core/container/EnumContainerBuilder.java @@ -1,6 +1,7 @@ package cn.crane4j.core.container; import cn.crane4j.annotation.ContainerEnum; +import cn.crane4j.annotation.DuplicateStrategy; import cn.crane4j.core.exception.Crane4jException; import cn.crane4j.core.support.AnnotationFinder; import cn.crane4j.core.support.MethodInvoker; @@ -12,11 +13,10 @@ import cn.crane4j.core.util.StringUtils; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; /** * A builder class for creating {@link Container}s from enumerations. @@ -73,6 +73,14 @@ public class EnumContainerBuilder> { @NonNull private PropertyOperator propertyOperator = DEFAULT_PROPERTY_OPERATOR; + /** + * Processing strategy when the key is duplicated. + * + * @since 2.2.0 + */ + @NonNull + private DuplicateStrategy duplicateStrategy = DuplicateStrategy.ALERT; + /** * Creates a new instance of the builder with the specified enum type. * @@ -171,6 +179,18 @@ public class EnumContainerBuilder> { return this; } + /** + * Set processing strategy of when the key is duplicated. + * + * @param duplicateStrategy coverage strategy + * @return this builder instance + * @since 2.2.0 + */ + public EnumContainerBuilder duplicateStrategy(DuplicateStrategy duplicateStrategy) { + this.duplicateStrategy = duplicateStrategy; + return this; + } + // ============== component ============== /** @@ -209,9 +229,15 @@ public class EnumContainerBuilder> { resolveConfigFromAnnotation(annotation); } } + // build container - Map enumMap = (Map) (Map) Stream.of(enumType.getEnumConstants()) - .collect(Collectors.toMap(keyGetter, valueGetter)); + Map enumMap = new HashMap<>(enumType.getEnumConstants().length); + for (T e : enumType.getEnumConstants()) { + K key = (K)keyGetter.apply(e); + Object newVal = valueGetter.apply(e); + enumMap.compute(key, (k, oldVal) -> + Objects.isNull(oldVal) ? newVal : duplicateStrategy.choose(k, oldVal, newVal)); + } namespace = StringUtils.emptyToDefault(this.namespace, enumType.getSimpleName()); return ImmutableMapContainer.forMap(namespace, enumMap); } @@ -220,6 +246,7 @@ public class EnumContainerBuilder> { if (namespace == null) { namespace = StringUtils.emptyToDefault(annotation.namespace(), enumType.getSimpleName()); } + duplicateStrategy = annotation.duplicateStrategy(); boolean hasKey = StringUtils.isNotEmpty(annotation.key()); boolean hasValue = StringUtils.isNotEmpty(annotation.value()); if (hasKey && keyGetter == DEFAULT_KEY_GETTER) { diff --git a/crane4j-core/src/main/java/cn/crane4j/core/container/MethodInvokerContainer.java b/crane4j-core/src/main/java/cn/crane4j/core/container/MethodInvokerContainer.java index e3301827..c204d6d0 100644 --- a/crane4j-core/src/main/java/cn/crane4j/core/container/MethodInvokerContainer.java +++ b/crane4j-core/src/main/java/cn/crane4j/core/container/MethodInvokerContainer.java @@ -1,5 +1,6 @@ package cn.crane4j.core.container; +import cn.crane4j.annotation.DuplicateStrategy; import cn.crane4j.annotation.MappingType; import cn.crane4j.core.support.MethodInvoker; import cn.crane4j.core.support.container.MethodContainerFactory; @@ -7,13 +8,15 @@ import cn.crane4j.core.support.container.MethodInvokerContainerCreator; import cn.crane4j.core.util.Asserts; import cn.crane4j.core.util.CollectionUtils; import lombok.Getter; +import lombok.Setter; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.function.Function; import java.util.stream.Collectors; /** @@ -43,6 +46,9 @@ public class MethodInvokerContainer implements Container { private final Object methodSource; private final KeyExtractor keyExtractor; private final MappingType mappingType; + @Setter + @NonNull + private DuplicateStrategy duplicateStrategy = DuplicateStrategy.ALERT; /** * Build a method data source container. @@ -89,7 +95,13 @@ public class MethodInvokerContainer implements Container { Collection invokeResults = CollectionUtils.adaptObjectToCollection(invokeResult); // one to one if (mappingType == MappingType.ONE_TO_ONE) { - return invokeResults.stream().collect(Collectors.toMap(keyExtractor::getKey, Function.identity())); + Map results = new HashMap<>(invokeResults.size()); + invokeResults.forEach(newVal -> { + Object k = keyExtractor.getKey(newVal); + results.compute(k, (key, oldVal) -> + Objects.isNull(oldVal) ? newVal : duplicateStrategy.choose(key, oldVal, newVal)); + }); + return results; } // one to many return invokeResults.stream().collect(Collectors.groupingBy(keyExtractor::getKey)); diff --git a/crane4j-core/src/main/java/cn/crane4j/core/support/container/DefaultMethodContainerFactory.java b/crane4j-core/src/main/java/cn/crane4j/core/support/container/DefaultMethodContainerFactory.java index 2482225e..0b87d325 100644 --- a/crane4j-core/src/main/java/cn/crane4j/core/support/container/DefaultMethodContainerFactory.java +++ b/crane4j-core/src/main/java/cn/crane4j/core/support/container/DefaultMethodContainerFactory.java @@ -74,12 +74,19 @@ public class DefaultMethodContainerFactory implements MethodContainerFactory { * @return data source containers */ @Override - public List> get(@Nullable Object source, Method method, Collection annotations) { + public List> get( + @Nullable Object source, Method method, Collection annotations) { return annotations.stream() - .map(annotation -> methodInvokerContainerCreator.createContainer( - source, method, annotation.type(), annotation.namespace(), - annotation.resultType(), annotation.resultKey() - )) + .map(annotation -> createContainer(source, method, annotation)) .collect(Collectors.toList()); } + + private Container createContainer(Object source, Method method, ContainerMethod annotation) { + MethodInvokerContainer container = methodInvokerContainerCreator.createContainer( + source, method, annotation.type(), annotation.namespace(), + annotation.resultType(), annotation.resultKey() + ); + container.setDuplicateStrategy(annotation.duplicateStrategy()); + return container; + } } diff --git a/crane4j-core/src/test/java/cn/crane4j/core/container/EnumContainerBuilderTest.java b/crane4j-core/src/test/java/cn/crane4j/core/container/EnumContainerBuilderTest.java index db1d2dc9..6825c96c 100644 --- a/crane4j-core/src/test/java/cn/crane4j/core/container/EnumContainerBuilderTest.java +++ b/crane4j-core/src/test/java/cn/crane4j/core/container/EnumContainerBuilderTest.java @@ -1,6 +1,7 @@ package cn.crane4j.core.container; import cn.crane4j.annotation.ContainerEnum; +import cn.crane4j.annotation.DuplicateStrategy; import cn.crane4j.core.exception.Crane4jException; import cn.crane4j.core.support.SimpleAnnotationFinder; import cn.crane4j.core.support.converter.HutoolConverterManager; @@ -34,7 +35,8 @@ public class EnumContainerBuilderTest { @SuppressWarnings("unchecked") @Test public void nonAnnotatedEnum() { - EnumContainerBuilder builder = EnumContainerBuilder.of(FooEnum.class); + EnumContainerBuilder builder = EnumContainerBuilder.of(FooEnum.class) + .duplicateStrategy(DuplicateStrategy.ALERT); Assert.assertThrows(Crane4jException.class, () -> builder.key("")); Assert.assertThrows(Crane4jException.class, () -> builder.value("")); @@ -98,10 +100,25 @@ public class EnumContainerBuilderTest { Assert.assertEquals(AnnotatedEnum.TWO.getKey(), data.get(AnnotatedEnum.TWO.getValue())); } + @Test + public void duplicateKeyEnum() { + // annotated + Container container = EnumContainerBuilder.of(DuplicateKeyEnum.class) + .namespace("test") + .annotationFinder(new SimpleAnnotationFinder()) + .propertyOperator(new ReflectivePropertyOperator(new HutoolConverterManager())) + .key("value") + .value("key") + .build(); + + Assert.assertEquals("test", container.getNamespace()); + Map data = container.get(null); + Assert.assertEquals(DuplicateKeyEnum.TWO.getKey(), data.get(DuplicateKeyEnum.ONE.getValue())); + } @Getter private enum FooEnum { - ONE, TWO; + ONE, TWO } @ContainerEnum(namespace = "AnnotatedEnum", key = "key", value = "value") @@ -113,4 +130,14 @@ public class EnumContainerBuilderTest { private final int key; private final String value; } + + @ContainerEnum(namespace = "DuplicateKeyEnum", key = "key", value = "value", duplicateStrategy = DuplicateStrategy.DISCARD_OLD) + @Getter + @RequiredArgsConstructor + private enum DuplicateKeyEnum { + ONE(1, "one"), + TWO(1, "one2"); + private final int key; + private final String value; + } } diff --git a/crane4j-core/src/test/java/cn/crane4j/core/util/DuplicateStrategyTest.java b/crane4j-core/src/test/java/cn/crane4j/core/util/DuplicateStrategyTest.java new file mode 100644 index 00000000..7678e099 --- /dev/null +++ b/crane4j-core/src/test/java/cn/crane4j/core/util/DuplicateStrategyTest.java @@ -0,0 +1,33 @@ +package cn.crane4j.core.util; + +import cn.crane4j.annotation.DuplicateStrategy; +import org.junit.Assert; +import org.junit.Test; + +/** + * test for {@link DuplicateStrategy} + * + * @author huangchengxing + */ +public class DuplicateStrategyTest { + + @Test + public void testAlert() { + Assert.assertThrows(IllegalArgumentException.class, () -> DuplicateStrategy.ALERT.choose("key", "oldVal", "newVal")); + } + + @Test + public void testDiscardNew() { + Assert.assertEquals("oldVal", DuplicateStrategy.DISCARD_NEW.choose("key", "oldVal", "newVal")); + } + + @Test + public void testDiscardOld() { + Assert.assertEquals("newVal", DuplicateStrategy.DISCARD_OLD.choose("key", "oldVal", "newVal")); + } + + @Test + public void testDiscard() { + Assert.assertNull(DuplicateStrategy.DISCARD.choose("key", "oldVal", "newVal")); + } +} diff --git a/crane4j-core/src/test/java/cn/crane4j/core/util/ReflectUtilsTest.java b/crane4j-core/src/test/java/cn/crane4j/core/util/ReflectUtilsTest.java index 8df4a6f6..03f9b499 100644 --- a/crane4j-core/src/test/java/cn/crane4j/core/util/ReflectUtilsTest.java +++ b/crane4j-core/src/test/java/cn/crane4j/core/util/ReflectUtilsTest.java @@ -32,6 +32,7 @@ import java.util.stream.Stream; * * @author huangchengxing */ +@SuppressWarnings("unused") public class ReflectUtilsTest { @Test @@ -111,6 +112,7 @@ public class ReflectUtilsTest { Assert.assertEquals("arg0arg1", result); } + @SneakyThrows @Test public void invokeRaw() { Method method = ReflectUtil.getMethod(ReflectUtilsTest.class, "method2", String.class, String.class); @@ -122,6 +124,19 @@ public class ReflectUtilsTest { Assert.assertThrows(Crane4jException.class, () -> ReflectUtils.invokeRaw(this, method, "arg0")); Assert.assertThrows(Crane4jException.class, () -> ReflectUtils.invokeRaw(this, method, 12)); + + // if throw InvocationTargetException, actual exception will be throw out + Method m3 = ReflectUtilsTest.class.getDeclaredMethod("method3"); + Assert.assertNotNull(m3); + Exception ex = null; + try { + ReflectUtils.invokeRaw(m3, m3, "arg0"); + } catch (Exception e) { + ex = e; + } + Assert.assertNotNull(ex); + Assert.assertTrue(ex instanceof Crane4jException); + Assert.assertTrue(ex.getCause() instanceof IllegalArgumentException); } @Test @@ -347,5 +362,5 @@ public class ReflectUtilsTest { private static String method2(String param1, String param2) { return param1 + param2; } - + private void method3() {} } \ No newline at end of file -- Gitee From c1c4b57985bf13b2f76612aa9e766cccff2df82c Mon Sep 17 00:00:00 2001 From: huangchengxing <841396397@qq.com> Date: Wed, 20 Sep 2023 23:59:51 +0800 Subject: [PATCH 2/2] feat(DuplicateStrategy): support when building containers based on enumerations or methods, if there are multiple corresponding values for the same key, it is allowed to be processed according to a specific policy (Gitee #I832VQ) --- .../src/main/java/cn/crane4j/core/util/ReflectUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/crane4j-core/src/main/java/cn/crane4j/core/util/ReflectUtils.java b/crane4j-core/src/main/java/cn/crane4j/core/util/ReflectUtils.java index 8cb1ca24..55fff1e2 100644 --- a/crane4j-core/src/main/java/cn/crane4j/core/util/ReflectUtils.java +++ b/crane4j-core/src/main/java/cn/crane4j/core/util/ReflectUtils.java @@ -532,6 +532,7 @@ public class ReflectUtils { } } + @SuppressWarnings("all") public static void setAccessible(T accessibleObject) { if (!accessibleObject.isAccessible()) { accessibleObject.setAccessible(true); -- Gitee