From cb13fa0c57d54f9e6a3fb97c99a027a2025fd315 Mon Sep 17 00:00:00 2001 From: "ah.jo" Date: Wed, 8 Nov 2023 13:19:32 +0900 Subject: [PATCH] Add a PropertySelector for Java Getter (#813) --- .../JavaGetterMethodPropertySelector.java | 57 ++++++++ .../JavaGetterMethodReference.java | 35 +++++ .../JavaGetterPropertySelector.java | 52 +++++++ .../JavaGetterPropertySelectors.java | 137 ++++++++++++++++++ .../JoinJavaGetterPropertySelector.java | 44 ++++++ .../api/type/KotlinTypeDetector.java | 45 ++++++ .../fixturemonkey/tests/java/JavaTest.java | 50 +++++++ .../fixturemonkey/tests/kotlin/KotlinTest.kt | 70 +++++++++ 8 files changed, 490 insertions(+) create mode 100644 fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterMethodPropertySelector.java create mode 100644 fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterMethodReference.java create mode 100644 fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterPropertySelector.java create mode 100644 fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterPropertySelectors.java create mode 100644 fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JoinJavaGetterPropertySelector.java create mode 100644 fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/type/KotlinTypeDetector.java create mode 100644 fixture-monkey-tests/kotlin-tests/src/test/kotlin/com/navercorp/fixturemonkey/tests/kotlin/KotlinTest.kt diff --git a/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterMethodPropertySelector.java b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterMethodPropertySelector.java new file mode 100644 index 000000000..089a1f6a0 --- /dev/null +++ b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterMethodPropertySelector.java @@ -0,0 +1,57 @@ +/* + * Fixture Monkey + * + * Copyright (c) 2021-present NAVER Corp. + * + * 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 + * + * http://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 com.navercorp.fixturemonkey.api.experimental; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +import com.navercorp.fixturemonkey.api.property.Property; +import com.navercorp.fixturemonkey.api.property.PropertyNameResolver; + +/** + * It is designed to select and represent a property through a getter method reference in Java. + * + * @param The type of the object that the getter method operates on. + * @param The type of the property that is being selected. + */ +@API(since = "1.0.0", status = Status.EXPERIMENTAL) +public final class JavaGetterMethodPropertySelector implements JavaGetterPropertySelector { + private final Class type; + private final Property property; + + public JavaGetterMethodPropertySelector(Class type, Property property) { + this.type = type; + this.property = property; + } + + public static JavaGetterMethodPropertySelector javaGetter( + JavaGetterMethodReference methodReference + ) { + return JavaGetterPropertySelectors.resolvePropertySelector(methodReference); + } + + public Class getType() { + return type; + } + + @Override + public String generate(PropertyNameResolver propertyNameResolver) { + return propertyNameResolver.resolve(property); + } +} diff --git a/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterMethodReference.java b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterMethodReference.java new file mode 100644 index 000000000..abccc66f3 --- /dev/null +++ b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterMethodReference.java @@ -0,0 +1,35 @@ +/* + * Fixture Monkey + * + * Copyright (c) 2021-present NAVER Corp. + * + * 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 + * + * http://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 com.navercorp.fixturemonkey.api.experimental; + +import java.io.Serializable; +import java.util.function.Function; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * It represents a functional interface for referencing getter methods in Java. + * + * @param The type of the input parameter (typically the object containing the property). + * @param The return type of the getter method (the type of the property being retrieved). + */ +@API(since = "1.0.0", status = Status.EXPERIMENTAL) +public interface JavaGetterMethodReference extends Function, Serializable { +} diff --git a/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterPropertySelector.java b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterPropertySelector.java new file mode 100644 index 000000000..9b9496419 --- /dev/null +++ b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterPropertySelector.java @@ -0,0 +1,52 @@ +/* + * Fixture Monkey + * + * Copyright (c) 2021-present NAVER Corp. + * + * 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 + * + * http://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 com.navercorp.fixturemonkey.api.experimental; + +import java.util.Arrays; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +import com.navercorp.fixturemonkey.api.expression.ExpressionGenerator; +import com.navercorp.fixturemonkey.api.property.PropertySelector; + +@API(since = "1.0.0", status = Status.EXPERIMENTAL) +interface JavaGetterPropertySelector extends PropertySelector, ExpressionGenerator { + default JoinJavaGetterPropertySelector into(JavaGetterMethodReference methodReference) { + JavaGetterMethodPropertySelector next = + JavaGetterPropertySelectors.resolvePropertySelector(methodReference); + + return new JoinJavaGetterPropertySelector<>( + Arrays.asList( + this, + propertyNameResolver -> ".", + next + ) + ); + } + + default JoinJavaGetterPropertySelector container(Class elementType, int index) { + return new JoinJavaGetterPropertySelector( + Arrays.asList( + this, + propertyNameResolver -> "[" + index + "]" + ) + ); + } +} diff --git a/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterPropertySelectors.java b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterPropertySelectors.java new file mode 100644 index 000000000..5435cddda --- /dev/null +++ b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JavaGetterPropertySelectors.java @@ -0,0 +1,137 @@ +/* + * Fixture Monkey + * + * Copyright (c) 2021-present NAVER Corp. + * + * 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 + * + * http://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 com.navercorp.fixturemonkey.api.experimental; + +import java.beans.PropertyDescriptor; +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; + +import javax.annotation.Nullable; + +import com.navercorp.fixturemonkey.api.property.CompositeProperty; +import com.navercorp.fixturemonkey.api.property.FieldProperty; +import com.navercorp.fixturemonkey.api.property.Property; +import com.navercorp.fixturemonkey.api.property.PropertyDescriptorProperty; +import com.navercorp.fixturemonkey.api.type.KotlinTypeDetector; +import com.navercorp.fixturemonkey.api.type.TypeCache; + +abstract class JavaGetterPropertySelectors { + private static final String GET_PREFIX = "get"; + private static final String IS_PREFIX = "is"; + + @SuppressWarnings("unchecked") + static JavaGetterMethodPropertySelector resolvePropertySelector( + JavaGetterMethodReference methodRef + ) { + try { + Class methodRefClass = methodRef.getClass(); + Method replaceMethod = methodRefClass.getDeclaredMethod("writeReplace"); + replaceMethod.setAccessible(true); + SerializedLambda lambda = (SerializedLambda)replaceMethod.invoke(methodRef); + String className = lambda.getImplClass().replace('/', '.'); + ClassLoader classLoader; + if (methodRefClass.getClassLoader() != null) { + classLoader = methodRefClass.getClassLoader(); + } else { + classLoader = JavaGetterPropertySelectors.class.getClassLoader(); + } + + Class targetClass = (Class)Class.forName(className, true, classLoader); + if (KotlinTypeDetector.isKotlinType(targetClass)) { + throw new IllegalArgumentException("Kotlin type could not resolve property name. type: " + targetClass); + } + + String fieldName = resolveFieldName(targetClass, lambda.getImplMethodName()); + Property fieldProperty = resolveFieldProperty(targetClass, fieldName); + Property propertyDescriptorProperty = resolvePropertyDescriptorProperty(targetClass, fieldName); + + Property resolvedProperty; + if (fieldProperty != null && propertyDescriptorProperty != null) { + resolvedProperty = new CompositeProperty(fieldProperty, propertyDescriptorProperty); + } else if (fieldProperty != null) { + resolvedProperty = fieldProperty; + } else if (propertyDescriptorProperty != null) { + resolvedProperty = propertyDescriptorProperty; + } else { + throw new IllegalArgumentException( + "Could not resolve a field or a JavaBeans getter by given lambda. type: " + targetClass + ); + } + + return new JavaGetterMethodPropertySelector<>(targetClass, resolvedProperty); + } catch (Exception ex) { + throw new IllegalArgumentException( + "Could not resolve a field or a JavaBeans getter by given lambda. lambda: " + methodRef, + ex + ); + } + + } + + @Nullable + private static String resolveFieldName(Class targetClass, String methodName) { + if (hasPrefix(GET_PREFIX, methodName)) { + return stripPrefixPropertyName(targetClass, methodName, GET_PREFIX.length()); + } else if (hasPrefix(IS_PREFIX, methodName)) { + return stripPrefixPropertyName(targetClass, methodName, IS_PREFIX.length()); + } else if (isValidField(targetClass, methodName)) { + // class could be using property-style getters (e.g. java record) + return methodName; + } + + return null; + } + + @Nullable + private static Property resolveFieldProperty(Class targetClass, String fieldName) { + Map fieldsByName = TypeCache.getFieldsByName(targetClass); + if (!fieldsByName.containsKey(fieldName)) { + return null; + } + return new FieldProperty(fieldsByName.get(fieldName)); + } + + @Nullable + private static Property resolvePropertyDescriptorProperty(Class targetClass, String fieldName) { + Map propertyDescriptorsByPropertyName = + TypeCache.getPropertyDescriptorsByPropertyName(targetClass); + + if (!propertyDescriptorsByPropertyName.containsKey(fieldName)) { + return null; + } + return new PropertyDescriptorProperty(propertyDescriptorsByPropertyName.get(fieldName)); + } + + private static String stripPrefixPropertyName(Class targetClass, String methodName, int prefixLength) { + char[] ch = methodName.toCharArray(); + ch[prefixLength] = Character.toLowerCase(ch[prefixLength]); + String fieldName = new String(ch, prefixLength, ch.length - prefixLength); + return isValidField(targetClass, fieldName) ? fieldName : null; + } + + private static boolean hasPrefix(String prefix, String methodName) { + return methodName.startsWith(prefix) && methodName.length() > prefix.length(); + } + + private static boolean isValidField(Class type, String fieldName) { + return TypeCache.getFieldsByName(type).containsKey(fieldName); + } +} diff --git a/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JoinJavaGetterPropertySelector.java b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JoinJavaGetterPropertySelector.java new file mode 100644 index 000000000..ed166f799 --- /dev/null +++ b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/experimental/JoinJavaGetterPropertySelector.java @@ -0,0 +1,44 @@ +/* + * Fixture Monkey + * + * Copyright (c) 2021-present NAVER Corp. + * + * 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 + * + * http://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 com.navercorp.fixturemonkey.api.experimental; + +import java.util.List; +import java.util.stream.Collectors; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +import com.navercorp.fixturemonkey.api.expression.ExpressionGenerator; +import com.navercorp.fixturemonkey.api.property.PropertyNameResolver; + +@API(since = "1.0.0", status = Status.EXPERIMENTAL) +public final class JoinJavaGetterPropertySelector implements JavaGetterPropertySelector { + private final List expressionGenerators; + + public JoinJavaGetterPropertySelector(List expressionGenerators) { + this.expressionGenerators = expressionGenerators; + } + + @Override + public String generate(PropertyNameResolver propertyNameResolver) { + return expressionGenerators.stream() + .map(it -> it.generate(propertyNameResolver)) + .collect(Collectors.joining()); + } +} diff --git a/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/type/KotlinTypeDetector.java b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/type/KotlinTypeDetector.java new file mode 100644 index 000000000..11396713c --- /dev/null +++ b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/type/KotlinTypeDetector.java @@ -0,0 +1,45 @@ +/* + * Fixture Monkey + * + * Copyright (c) 2021-present NAVER Corp. + * + * 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 + * + * http://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 com.navercorp.fixturemonkey.api.type; + +import java.lang.annotation.Annotation; + +import javax.annotation.Nullable; + +@SuppressWarnings("unchecked") +public abstract class KotlinTypeDetector { + @Nullable + private static final Class kotlinMetadata; + + static { + Class metadata; + ClassLoader classLoader = KotlinTypeDetector.class.getClassLoader(); + try { + metadata = Class.forName("kotlin.Metadata", false, classLoader); + } catch (ClassNotFoundException ex) { + // Kotlin API not available - no Kotlin support + metadata = null; + } + kotlinMetadata = (Class)metadata; + } + + public static boolean isKotlinType(Class clazz) { + return (kotlinMetadata != null && clazz.getDeclaredAnnotation(kotlinMetadata) != null); + } +} diff --git a/fixture-monkey-tests/java-tests/src/test/java/com/navercorp/fixturemonkey/tests/java/JavaTest.java b/fixture-monkey-tests/java-tests/src/test/java/com/navercorp/fixturemonkey/tests/java/JavaTest.java index 2263611fb..fa8e12802 100644 --- a/fixture-monkey-tests/java-tests/src/test/java/com/navercorp/fixturemonkey/tests/java/JavaTest.java +++ b/fixture-monkey-tests/java-tests/src/test/java/com/navercorp/fixturemonkey/tests/java/JavaTest.java @@ -18,6 +18,7 @@ package com.navercorp.fixturemonkey.tests.java; +import static com.navercorp.fixturemonkey.api.experimental.JavaGetterMethodPropertySelector.javaGetter; import static com.navercorp.fixturemonkey.api.instantiator.Instantiator.constructor; import static com.navercorp.fixturemonkey.api.instantiator.Instantiator.factoryMethod; import static com.navercorp.fixturemonkey.tests.TestEnvironment.TEST_COUNT; @@ -1100,4 +1101,53 @@ void nestedObject() { then(actual).isNotNull(); } + + @RepeatedTest(TEST_COUNT) + void setJavaGetter() { + String actual = SUT.giveMeBuilder(JavaTypeObject.class) + .set(javaGetter(JavaTypeObject::getString), "test") + .sample() + .getString(); + + then(actual).isEqualTo("test"); + } + + @RepeatedTest(TEST_COUNT) + void setJavaGetterInto() { + String actual = SUT.giveMeBuilder(RootJavaTypeObject.class) + .set(javaGetter(RootJavaTypeObject::getValue).into(JavaTypeObject::getString), "test") + .sample() + .getValue() + .getString(); + + then(actual).isEqualTo("test"); + } + + @RepeatedTest(TEST_COUNT) + void setJavaGetterCollection() { + String actual = SUT.giveMeBuilder(ContainerObject.class) + .size("list", 1) + .set(javaGetter(ContainerObject::getList).container(String.class, 0), "test") + .sample() + .getList() + .get(0); + + then(actual).isEqualTo("test"); + } + + @RepeatedTest(TEST_COUNT) + void setJavaGetterCollectionElement() { + String actual = SUT.giveMeBuilder(ContainerObject.class) + .size("complexList", 1) + .set( + javaGetter(ContainerObject::getComplexList) + .container(JavaTypeObject.class, 0) + .into(JavaTypeObject::getString), "test") + .sample() + .getComplexList() + .get(0) + .getString(); + + then(actual).isEqualTo("test"); + } } diff --git a/fixture-monkey-tests/kotlin-tests/src/test/kotlin/com/navercorp/fixturemonkey/tests/kotlin/KotlinTest.kt b/fixture-monkey-tests/kotlin-tests/src/test/kotlin/com/navercorp/fixturemonkey/tests/kotlin/KotlinTest.kt new file mode 100644 index 000000000..c20e36cb7 --- /dev/null +++ b/fixture-monkey-tests/kotlin-tests/src/test/kotlin/com/navercorp/fixturemonkey/tests/kotlin/KotlinTest.kt @@ -0,0 +1,70 @@ +/* + * Fixture Monkey + * + * Copyright (c) 2021-present NAVER Corp. + * + * 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 + * + * http://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 com.navercorp.fixturemonkey.tests.kotlin + +import com.navercorp.fixturemonkey.FixtureMonkey +import com.navercorp.fixturemonkey.api.experimental.JavaGetterMethodPropertySelector.javaGetter +import com.navercorp.fixturemonkey.kotlin.KotlinPlugin +import com.navercorp.fixturemonkey.kotlin.giveMeBuilder +import com.navercorp.fixturemonkey.kotlin.giveMeExperimentalBuilder +import com.navercorp.fixturemonkey.kotlin.instantiator.instantiateBy +import com.navercorp.fixturemonkey.tests.kotlin.JavaConstructorTestSpecs.JavaTypeObject +import org.assertj.core.api.BDDAssertions.then +import org.assertj.core.api.BDDAssertions.thenThrownBy +import org.junit.jupiter.api.Test + +class KotlinTest { + @Test + fun kotlinObjectUseJavaGetterThrows() { + class KotlinObject(val value: String) + + thenThrownBy { + SUT.giveMeBuilder() + .set(javaGetter(KotlinObject::value), "test") + .sample() + .value + }.cause + .isExactlyInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("Kotlin type could not resolve property name.") + } + + @Test + fun javaGetter() { + val expected = "test" + + val actual = SUT.giveMeExperimentalBuilder() + .instantiateBy { + constructor { + parameter("string") + parameter() + } + } + .set(javaGetter(JavaTypeObject::getString), expected) + .sample() + .string + + then(actual).isEqualTo(expected) + } + + companion object { + private val SUT: FixtureMonkey = FixtureMonkey.builder() + .plugin(KotlinPlugin()) + .build() + } +}