/*
* Copyright 2013-2017 the original author or authors.
*
* 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 org.cloudfoundry.operations;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
/**
* {@code TestObjects} provides a generic utility which transforms a builder object of type {@code T}, by calling its configuration methods with default values.
* <p>
* A <i>builder object</i> of type <b>T</b> is an object with a {@code build()} method returning a <i>built object</i> whose type has a {@code builder} of type <b>T</b>.
* <p>
* A <i>built object</i> of type <b>B</b> is an object with a {@code builder()} method returning a <i>builder object</i> which builds type <b>B</b>.
* <p>
* The exported static methods are {@link #fill T fill(T, String)} and {@link #fill T fill(T)}. The {@code T} argument must be an object of builder type (which is returned as result),
* and the {@code String} is a <i>modifier</i> which is used to augment the {@code String} values set. The modifier must not be {@code null}.
* <p>
* {@code fill(b)} is equivalent to {@code fill(b, "")}.
* <p>
* {@code TestObjects} populates builder objects with test values. Builder setter methods are called with standard values based upon the parameter type and the name of the setter method.
* <ul>
* <li>{@code enum} types are set to the first enumerated constant value.</li>
* <li>{@link Boolean} types are set to {@code true}.</li>
* <li>{@link Date} types are set to {@code new Date(0)}.</li>
* <li>{@link Double} types are set to {@code 1.0}.</li>
* <li>{@link Duration} types are set to a duration of 15 seconds.</li>
* <li>{@link Integer} or {@link Long} types are set to {@code 1}.</li>
* <li>{@link Iterable} types are set to empty.</li>
* <li>{@link Map} types are set to empty.</li>
* <li>{@link String} types are set to {@code "test-"+modifier+settername}.</li>
* <li>Types of <i>built objects</i> are set to a value built from a (recursively) {@code fill()}ed builder instance.</li>
* </ul>
* <p>
* Only public, chainable, single-parameter setter methods which have a corresponding getter (on the type built) are configured.
* <p>
* Non-builder objects, or builder objects that build {@code *Request} types, are rejected (by assertion failure).
*/
public abstract class TestObjects {
private TestObjects() { // do not instantiate this class
}
/**
* Fill a builder by calling its configuration methods with default values.
*
* @param builder the builder to fill
* @param <T> The type of the builder
* @return the filled builder
*/
public static <T> T fill(T builder) {
return fill(builder, Optional.empty());
}
/**
* Fill a builder by calling its configuration methods with default values.
*
* @param builder the builder to fill
* @param modifier a modifier for the values of {@link String} types
* @param <T> The type of the builder
* @return the filled builder
*/
public static <T> T fill(T builder, String modifier) {
return fill(builder, Optional.of(modifier));
}
private static boolean buildsRequestType(Class<?> builderType) {
return getBuiltType(builderType).getName().endsWith("Request");
}
private static <T> T fill(T builder, Optional<String> modifier) {
Class<?> builderType = builder.getClass();
assertThat(isBuilderType(builderType)).as("Cannot fill type %s", builderType.getName()).isTrue();
assertThat(buildsRequestType(builderType)).as("Do not fill Request types").isFalse();
List<Method> builderMethods = getMethods(builderType);
Set<String> builtGetters = getBuiltGetters(builderType);
return getConfigurationMethods(builderType, builderMethods, builtGetters).stream()
.collect(() -> builder, (b, method) -> ReflectionUtils.invokeMethod(method, b, getConfiguredValue(method, modifier)), (a, b) -> {
});
}
private static Method getBuildMethod(Class<?> builderType) {
return ReflectionUtils.findMethod(builderType, "build");
}
private static Method getBuilderMethod(Class<?> builderType) {
return ReflectionUtils.findMethod(builderType, "builder");
}
private static Set<String> getBuiltGetters(Class<?> builderType) {
Class<?> builtType = getBuiltType(builderType);
return Arrays.stream(ReflectionUtils.getUniqueDeclaredMethods(builtType))
.map(Method::getName)
.filter(s -> s.startsWith("get"))
.collect(Collectors.toSet());
}
private static Class<?> getBuiltType(Class<?> builderType) {
return getBuildMethod(builderType).getReturnType();
}
private static List<Method> getConfigurationMethods(Class<?> builderType, List<Method> builderMethods, Set<String> builtGetters) {
return builderMethods.stream()
.filter(TestObjects::isPublic)
.filter(returnsThisType(builderType))
.filter(TestObjects::hasSingleParameter)
.filter(method -> hasMatchingGetter(method, builtGetters))
.collect(Collectors.toList());
}
private static Object getConfiguredBuilder(Class<?> parameterType, Optional<String> modifier) {
Object builder = ReflectionUtils.invokeMethod(getBuilderMethod(parameterType), null);
Method buildMethod = getBuildMethod(builder.getClass());
return ReflectionUtils.invokeMethod(buildMethod, fill(builder, modifier));
}
private static Object getConfiguredEnum(Class<?> parameterType) {
return parameterType.getEnumConstants()[0];
}
private static String getConfiguredString(Method method, Optional<String> modifier) {
return modifier
.map(m -> String.format("test-%s%s", m, method.getName()))
.orElse(String.format("test-%s", method.getName()));
}
@SuppressWarnings("unchecked")
private static Object getConfiguredValue(Method configurationMethod, Optional<String> modifier) {
Class<?> parameterType = getParameter(configurationMethod).getType();
if (isBuiltType(parameterType)) {
return getConfiguredBuilder(parameterType, modifier);
} else if (Enum.class.isAssignableFrom(parameterType)) {
return getConfiguredEnum(parameterType);
} else if (parameterType == Boolean.class) {
return Boolean.TRUE;
} else if (parameterType == Date.class) {
return new Date(0);
} else if (parameterType == Double.class) {
return 1D;
} else if (parameterType == Duration.class) {
return Duration.ofSeconds(15);
} else if (parameterType == Integer.class) {
return 1;
} else if (parameterType == Iterable.class) {
return Collections.emptyList();
} else if (parameterType == Long.class) {
return 1L;
} else if (parameterType == Map.class) {
return Collections.emptyMap();
} else if (parameterType == String.class) {
return getConfiguredString(configurationMethod, modifier);
} else {
throw new IllegalStateException(String.format("Unable to configure %s", configurationMethod));
}
}
private static List<Method> getMethods(Class<?> builderType) {
return Arrays.asList(ReflectionUtils.getUniqueDeclaredMethods(builderType));
}
private static Parameter getParameter(Method method) {
return method.getParameters()[0];
}
private static boolean hasMatchingGetter(Method method, Set<String> builtGetters) {
String propertyName = method.getName();
String candidate = String.format("get%s%s", propertyName.substring(0, 1).toUpperCase(), propertyName.substring(1));
return builtGetters.contains(candidate);
}
private static boolean hasSingleParameter(Method method) {
return 1 == method.getParameterCount();
}
private static boolean isBuilderType(Class<?> aType) {
return Optional.ofNullable(getBuildMethod(aType))
.map(Method::getReturnType)
.map(TestObjects::getBuilderMethod)
.map(Method::getReturnType)
.map(aType::equals)
.orElse(false);
}
private static boolean isBuiltType(Class<?> aType) {
return Optional.ofNullable(getBuilderMethod(aType))
.map(Method::getReturnType)
.map(TestObjects::getBuildMethod)
.map(Method::getReturnType)
.map(aType::equals)
.orElse(false);
}
private static boolean isPublic(Method method) {
return Modifier.isPublic(method.getModifiers());
}
private static Predicate<Method> returnsThisType(Class<?> aType) {
return method -> aType == method.getReturnType();
}
}