Skip to content

Commit

Permalink
Add a PropertySelector for Java Getter (#813)
Browse files Browse the repository at this point in the history
  • Loading branch information
seongahjo authored Nov 8, 2023
1 parent ed695f1 commit cb13fa0
Show file tree
Hide file tree
Showing 8 changed files with 490 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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 <T> The type of the object that the getter method operates on.
* @param <U> The type of the property that is being selected.
*/
@API(since = "1.0.0", status = Status.EXPERIMENTAL)
public final class JavaGetterMethodPropertySelector<T, U> implements JavaGetterPropertySelector<T, U> {
private final Class<U> type;
private final Property property;

public JavaGetterMethodPropertySelector(Class<U> type, Property property) {
this.type = type;
this.property = property;
}

public static <T, R> JavaGetterMethodPropertySelector<T, R> javaGetter(
JavaGetterMethodReference<T, R> methodReference
) {
return JavaGetterPropertySelectors.resolvePropertySelector(methodReference);
}

public Class<U> getType() {
return type;
}

@Override
public String generate(PropertyNameResolver propertyNameResolver) {
return propertyNameResolver.resolve(property);
}
}
Original file line number Diff line number Diff line change
@@ -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 <T> The type of the input parameter (typically the object containing the property).
* @param <R> 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<T, R> extends Function<T, R>, Serializable {
}
Original file line number Diff line number Diff line change
@@ -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<T, U> extends PropertySelector, ExpressionGenerator {
default <R> JoinJavaGetterPropertySelector<U, R> into(JavaGetterMethodReference<U, R> methodReference) {
JavaGetterMethodPropertySelector<U, R> next =
JavaGetterPropertySelectors.resolvePropertySelector(methodReference);

return new JoinJavaGetterPropertySelector<>(
Arrays.asList(
this,
propertyNameResolver -> ".",
next
)
);
}

default <E> JoinJavaGetterPropertySelector<U, E> container(Class<E> elementType, int index) {
return new JoinJavaGetterPropertySelector<U, E>(
Arrays.asList(
this,
propertyNameResolver -> "[" + index + "]"
)
);
}
}
Original file line number Diff line number Diff line change
@@ -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 <T, R> JavaGetterMethodPropertySelector<T, R> resolvePropertySelector(
JavaGetterMethodReference<T, R> 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<R> targetClass = (Class<R>)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<String, Field> 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<String, PropertyDescriptor> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<T, U> implements JavaGetterPropertySelector<T, U> {
private final List<ExpressionGenerator> expressionGenerators;

public JoinJavaGetterPropertySelector(List<ExpressionGenerator> expressionGenerators) {
this.expressionGenerators = expressionGenerators;
}

@Override
public String generate(PropertyNameResolver propertyNameResolver) {
return expressionGenerators.stream()
.map(it -> it.generate(propertyNameResolver))
.collect(Collectors.joining());
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends Annotation> 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<? extends Annotation>)metadata;
}

public static boolean isKotlinType(Class<?> clazz) {
return (kotlinMetadata != null && clazz.getDeclaredAnnotation(kotlinMetadata) != null);
}
}
Loading

0 comments on commit cb13fa0

Please sign in to comment.