All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.itsallcode.matcher.auto.AutoConfigBuilder Maven / Gradle / Ivy
package org.itsallcode.matcher.auto;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.Matchers.emptyArray;
import java.io.File;
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.time.Instant;
import java.time.LocalDate;
import java.time.temporal.Temporal;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.StreamSupport;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.collection.IsArray;
import org.hamcrest.collection.IsMapContaining;
import org.itsallcode.matcher.config.ConfigurableMatcher;
import org.itsallcode.matcher.config.MatcherConfig;
import org.itsallcode.matcher.config.MatcherConfig.Builder;
class AutoConfigBuilder {
private static final Logger LOG = Logger.getLogger(AutoConfigBuilder.class.getName());
private static final Set> SIMPLE_TYPES = Collections.unmodifiableSet(new HashSet<>(asList(String.class,
Long.class, Integer.class, Byte.class, Boolean.class, Float.class, Double.class, Character.class,
Short.class, BigInteger.class, BigDecimal.class, Calendar.class, Date.class, java.sql.Date.class,
java.sql.Timestamp.class, Instant.class, LocalDate.class,
Temporal.class, Currency.class,
File.class, Path.class, UUID.class, Class.class, Package.class, Enum.class, URL.class, URI.class)));
private static final Set IGNORED_METHOD_NAMES = new HashSet<>(
asList("getClass", "getProtectionDomain", "getClassLoader", "getURLs", "hashCode", "toString"));
private final T expected;
private final Builder configBuilder;
private final boolean isRecord;
AutoConfigBuilder(final T expected, final boolean isRecord) {
this.expected = expected;
this.isRecord = isRecord;
this.configBuilder = MatcherConfig.builder(expected);
}
MatcherConfig build() {
Arrays.stream(expected.getClass().getMethods()) //
.filter(this::isNotIgnored) //
.filter(this::isGetterMethodName) //
.filter(this::isGetterMethodSignature) //
.sorted(Comparator.comparing(this::hasSimpleReturnType).reversed() //
.thenComparing(this::hasArrayReturnType) //
.thenComparing(Method::getName)) //
.forEach(this::addConfigForGetter);
return configBuilder.build();
}
@SuppressWarnings("unchecked")
static Matcher createEqualToMatcher(final T expected) {
if (expected == null) {
return (Matcher) Matchers.nullValue();
}
final Class extends Object> type = expected.getClass();
if (type.isArray()) {
return createArrayMatcher(expected);
}
if (isSimpleType(type)) {
return Matchers.equalTo(expected);
}
if (Map.class.isAssignableFrom(type)) {
return createMapContainsMatcher(expected);
}
if (Iterable.class.isAssignableFrom(type)) {
return createIterableContainsMatcher(expected);
}
if (Optional.class.isAssignableFrom(type)) {
return createOptionalMatcher(expected);
}
final MatcherConfig config = AutoConfigBuilder.create(expected).build();
return new ConfigurableMatcher<>(config);
}
static AutoConfigBuilder create(final T expected) {
return new AutoConfigBuilder<>(expected, isRecord(expected.getClass()));
}
private static boolean isRecord(final Class> type) {
final Method isRecord;
try {
isRecord = type.getClass().getMethod("isRecord");
} catch (NoSuchMethodException | SecurityException e) {
LOG.log(Level.FINEST, e,
() -> "Method Class.isRecord() does not exist, " + type.getName() + " is probably not a record");
return false;
}
try {
return (boolean) isRecord.invoke(type);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
LOG.log(Level.WARNING, e, () -> "Invocation of " + isRecord + " failed for " + type);
return false;
}
}
@SuppressWarnings("unchecked")
private static Matcher createArrayMatcher(final Object expected) {
final Class componentType = (Class) expected.getClass().getComponentType();
if (componentType.isPrimitive()) {
return (Matcher) Matchers.equalTo(expected);
}
final Object[] expectedArray = (Object[]) expected;
if (expectedArray.length == 0) {
return (Matcher) emptyArray();
}
if (isSimpleType(componentType)) {
final Matcher arrayContaining = Matchers.arrayContaining(expectedArray);
return (Matcher) arrayContaining;
}
final List> matchers = Arrays.stream(expectedArray).map(AutoMatcher::equalTo).collect(toList());
@SuppressWarnings("rawtypes")
final Matcher arrayContaining = IsArray.array(matchers.toArray(new Matcher[0]));
return (Matcher) arrayContaining;
}
@SuppressWarnings("unchecked")
private static Matcher createMapContainsMatcher(final T expected) {
final Map expectedMap = (Map) expected;
final Collection> matchers = new ArrayList<>();
matchers.add(mapSizeMatcher(expectedMap));
for (final Entry expectedEntry : expectedMap.entrySet()) {
matchers.add((Matcher super T>) IsMapContaining.hasEntry(createEqualToMatcher(expectedEntry.getKey()),
createEqualToMatcher(expectedEntry.getValue())));
}
return Matchers.allOf(matchers);
}
private static ConfigurableMatcher mapSizeMatcher(final Map expectedMap) {
@SuppressWarnings("unchecked")
final MatcherConfig config = (MatcherConfig) MatcherConfig.builder(expectedMap)
.addEqualsProperty("size", Map::size).build();
return new ConfigurableMatcher<>(config);
}
private static Matcher createIterableContainsMatcher(final T expected) {
@SuppressWarnings("unchecked")
final Iterable expectedIterable = (Iterable) expected;
final Object[] elements = StreamSupport.stream(expectedIterable //
.spliterator(), false) //
.toArray();
@SuppressWarnings("unchecked")
final Matcher matcher = (Matcher) AutoMatcher.contains(elements);
return matcher;
}
@SuppressWarnings("unchecked")
private static Matcher createOptionalMatcher(final T expected) {
final Optional expectedOptional = (Optional) expected;
if (expectedOptional.isEmpty()) {
return (Matcher) OptionalMatchers.isEmpty();
}
return (Matcher) OptionalMatchers.isPresentAnd(AutoMatcher.equalTo(expectedOptional.get()));
}
private boolean isNotIgnored(final Method method) {
return !IGNORED_METHOD_NAMES.contains(method.getName());
}
private boolean isGetterMethodSignature(final Method method) {
return method.getParameterCount() == 0 //
&& !method.getReturnType().equals(Void.TYPE);
}
private boolean isGetterMethodName(final Method method) {
if (isRecord) {
return true;
}
final String methodName = method.getName();
return methodName.startsWith("get")
|| methodName.startsWith("is");
}
private void addConfigForGetter(final Method method) {
final String propertyName = getPropertyName(method.getName());
LOG.finest(() -> "Adding general property '" + propertyName + "' for getter " + method);
configBuilder.addProperty(propertyName, createGetter(method), AutoMatcher::equalTo);
}
private boolean hasArrayReturnType(final Method method) {
return method.getReturnType().isArray();
}
private Function createGetter(final Method method) {
return object -> getPropertyValue(method, object);
}
private boolean hasSimpleReturnType(final Method method) {
final Class extends Object> type = method.getReturnType();
if (type.isPrimitive() || type.isEnum()) {
return true;
}
return isSimpleType(type);
}
private static boolean isSimpleType(final Class extends Object> type) {
for (final Class> simpleType : SIMPLE_TYPES) {
if (simpleType.isAssignableFrom(type)) {
return true;
}
}
return false;
}
static String getPropertyName(final String methodName) {
final int prefixLength;
if (methodName.startsWith("get")) {
prefixLength = 3;
} else if (methodName.startsWith("is")) {
prefixLength = 2;
} else {
return methodName;
}
if (methodName.length() == prefixLength) {
return methodName;
}
final String propertyName = methodName.substring(prefixLength);
return decapitalize(propertyName);
}
private static String decapitalize(final String string) {
return Character.toLowerCase(string.charAt(0)) + string.substring(1);
}
@SuppressWarnings({ "unchecked", "java:S3011" }) // Need to use reflection and setAccessible()
private static P getPropertyValue(final Method method, final T object) {
final Class> declaringClass = method.getDeclaringClass();
if (!declaringClass.isInstance(object)) {
throw new AssertionError("Expected object of type " + declaringClass.getName() + " but got "
+ object.getClass().getName() + ": " + object.toString());
}
if (!Modifier.isPublic(declaringClass.getModifiers())) {
method.setAccessible(true);
}
try {
return (P) method.invoke(object);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new IllegalStateException("Error invoking method " + method + " on object " + object + " of type "
+ object.getClass().getName(), e);
}
}
}