Skip to content

Commit

Permalink
🔨 #109 Generic values and null-values can now also be converted
Browse files Browse the repository at this point in the history
  • Loading branch information
JanSchankin committed Mar 14, 2022
1 parent 00c6b7f commit 02b0041
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class BeanExtensionSamplerFixture implements SamplerFixture {
public void defineSamplers() {
PersistentSample.of(testServiceSampler.getCat()).hasId("CatStub");
PersistentSample.of(testServiceSampler.getOptionalCatsName()).hasId("getOptionalCatsName");
PersistentSample.of(testServiceSampler.getOptionalCat()).hasId("getOptionalCat");
PersistentSample.of(testServiceSampler.getOptionalGenericCat()).hasId("getOptionalGenericCat");
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2022 PPI AG (Hamburg, Germany)
* This program is made available under the terms of the MIT License.
*/

package de.ppi.deepsampler.junit;

public class Dog {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2022 PPI AG (Hamburg, Germany)
* This program is made available under the terms of the MIT License.
*/

package de.ppi.deepsampler.junit;

public class GenericCat<T> {

private T prey;

public T getPrey() {
return prey;
}

public void setPrey(T prey) {
this.prey = prey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,21 @@ public void setCatsName(String catsName) {
}

public Optional<String> getOptionalCatsName() {
return Optional.of(catsName);
return Optional.ofNullable(catsName);
}

public Optional<Cat> getOptionalCat() {
return Optional.of(new Cat(catsName));
}

public Optional<GenericCat<Dog>> getOptionalGenericCat() {
GenericCat<Dog> genericCat = new GenericCat<>();
Dog dog = new Dog();
dog.setName(catsName);

genericCat.setPrey(dog);

return Optional.of(genericCat);
}

public void setDefaultInstant(Instant defaultInstant) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,86 @@ void samplerWithOptionalValueCanBeLoaded() {
.isPresent()
.hasValue(CATS_NAME_AS_IT_SHOULD_BE_RECORDED);
}

@Test
@Order(4)
@SaveSamples(OPTIONAL_VALUE_SAMPLE_FILE)
void samplerWithOptionalEmptyCanBeRecorded() throws IOException {
// GIVEN
testService.setCatsName(null);

// WHEN
// Call the method that should be recorded
testService.getOptionalCatsName();

// THEN
assertThatFileDoesNotExistOrOtherwiseDeleteIt(EXPECTED_SAVED_FILE_INCLUDING_ROOT_PATH);
}

@Test
@Order(5)
@LoadSamples(OPTIONAL_VALUE_SAMPLE_FILE)
void samplerWithOptionalEmptyCanBeLoaded() {
// GIVEN
testService.setCatsName("This name should be overridden by the stub");

assertThat(testService.getOptionalCatsName()).isEmpty();
}

@Test
@Order(6)
@SaveSamples(OPTIONAL_VALUE_SAMPLE_FILE)
void samplerWithOptionalObjectCanBeRecorded() throws IOException {
// GIVEN
testService.setCatsName(CATS_NAME_AS_IT_SHOULD_BE_RECORDED);

// WHEN
// Call the method that should be recorded
testService.getOptionalCat();

// THEN
assertThatFileDoesNotExistOrOtherwiseDeleteIt(EXPECTED_SAVED_FILE_INCLUDING_ROOT_PATH);
}

@Test
@Order(7)
@LoadSamples(OPTIONAL_VALUE_SAMPLE_FILE)
void samplerWithOptionalObjectCanBeLoaded() {
// GIVEN
testService.setCatsName("This name should be overridden by the stub");

assertThat(testService.getOptionalCat())
.isPresent()
.map(Cat::getName)
.hasValue(CATS_NAME_AS_IT_SHOULD_BE_RECORDED);
}

@Test
@Order(8)
@SaveSamples(OPTIONAL_VALUE_SAMPLE_FILE)
void samplerWithOptionalGenericObjectCanBeRecorded() throws IOException {
// GIVEN
testService.setCatsName(CATS_NAME_AS_IT_SHOULD_BE_RECORDED);

// WHEN
// Call the method that should be recorded
testService.getOptionalGenericCat();

// THEN
assertThatFileDoesNotExistOrOtherwiseDeleteIt(EXPECTED_SAVED_FILE_INCLUDING_ROOT_PATH);
}

@Test
@Order(9)
@LoadSamples(OPTIONAL_VALUE_SAMPLE_FILE)
void samplerWithOptionalGenericObjectCanBeLoaded() {
// GIVEN
testService.setCatsName("This name should be overridden by the stub");

assertThat(testService.getOptionalGenericCat())
.isPresent()
.map(GenericCat::getPrey)
.map(Dog::getName)
.hasValue(CATS_NAME_AS_IT_SHOULD_BE_RECORDED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ private List<Object> unwrapValue(final String id, final Type[] parameterTypes, f
}
for (int i = 0; i < parameterPersistentBeans.size(); ++i) {
final ParameterizedType parameterType = parameterTypes[i] instanceof ParameterizedType ? (ParameterizedType) parameterTypes[i] : null;
final Class<?> parameterClass = ReflectionTools.getClass(parameterTypes[i]);
final Class<?> parameterClass = ReflectionTools.getRawClass(parameterTypes[i]);
final Object persistentBean = parameterPersistentBeans.get(i);

params.add(unwrapValue(parameterClass, parameterType, persistentBean));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,35 @@ public static boolean hasPrimitiveTypeParameters(Type type, int numberOfParamete


/**
* Returns the {@link Class} behind type.
* If type is a {@link ParameterizedType} the raw type, that a {@link ParameterizedType} contains, is returned.
* If type is a simple {@link Class}, type itself is cast to {@link Class} and returned.
*
* @param type The type for which the Class is wanted.
* @param <T> The type of the Class
* @return If type is a {@link ParameterizedType}, the raw Class is returned, otherwise a casted version of type.
* @return If type is a {@link ParameterizedType}, the raw Class is returned, otherwise a cast version of type.
*/
@SuppressWarnings("unchecked")
public static <T> Class<T> getClass(Type type) {
public static <T> Class<T> getRawClass(Type type) {
if (type instanceof ParameterizedType) {
return (Class<T>) ((ParameterizedType) type).getRawType();
}

return (Class<T>) type;
}

/**
* Casts type to a {@link ParameterizedType} if type is a {@link ParameterizedType}. Otherwise, null is returned.
* @param type A {@link Type} that is expected to be a {@link ParameterizedType}
* @return returns a cast type or null if type is not a {@link ParameterizedType}.
*/
public static ParameterizedType getParameterizedType(Type type) {
if (type instanceof ParameterizedType) {
return (ParameterizedType) type;
}

return null;
}


/**
* Creates an empty array. The dimensions of the new array will be the same as of templateArray. The componentType of the
Expand Down Expand Up @@ -214,8 +228,8 @@ public static Class<?> getRootComponentType(Class<?> array) {
return array;
}

if (getClass(componentType).isArray()) {
return getRootComponentType(getClass(componentType).getComponentType());
if (getRawClass(componentType).isArray()) {
return getRootComponentType(getRawClass(componentType).getComponentType());
}

return componentType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,16 @@ public interface BeanConverterExtension {
* persistence api, to the original object.
* <p>
* It is also possible to deserialize other types then {@link PersistentBean} if the underlying persistence api is fully
* capable of deserializing this type. I.E. {@link java.util.List}s can be deserialized by Jackson (the default JSON persistence api)
* but the elements of the list might be generic {@link PersistentBean}s. The {@link CollectionExtension} would leave it to
* Jackson to deserialize the List, but it would iterate over that List to revert the {@link PersistentBean}s inside of that list.
* capable of deserializing this type.
* <p>
* revert() is not called, if the persisted value is null. In that case, null will be returned as the deserialized value
* without calling an extension. If null needs to be converted, the value must be encapsulated in a
* {@link de.ppi.deepsampler.persistence.bean.DefaultPersistentBean}.
*
* @param persistentBean the generic bean
* @param targetClass the {@link Class} of the type that will created from the persistentBean.
* @param targetType the {@link ParameterizedType} fo the type that will be created from persistentBean, This parameter can only be supplied
* if the type is actually a generic type. If this is not the case, beanType is null.
* @param targetClass the {@link Class} of the type that will be created from the persistentBean.
* @param targetType the {@link ParameterizedType} for the type that will be created from persistentBean, This parameter can only be supplied
* if the type is actually a generic type. If this is not the case, targetType is null.
* @param persistentBeanConverter the current {@link PersistentBeanConverter} that may be used to revert sub objects of persistentBean.
* @param <T> type of the original bean
* @return original bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class MapPrimitiveKeyExtension extends StandardBeanConverterExtension {

@Override
public boolean isProcessable(Class<?> beanClass, ParameterizedType beanType) {
return Map.class.isAssignableFrom(ReflectionTools.getClass(beanClass))
return Map.class.isAssignableFrom(ReflectionTools.getRawClass(beanClass))
&& beanType.getActualTypeArguments().length == 2
&& ReflectionTools.hasPrimitiveTypeParameters(beanType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,29 @@

package de.ppi.deepsampler.persistence.bean.ext;

import de.ppi.deepsampler.persistence.bean.DefaultPersistentBean;
import de.ppi.deepsampler.persistence.bean.PersistentBeanConverter;
import de.ppi.deepsampler.persistence.bean.ReflectionTools;
import de.ppi.deepsampler.persistence.model.PersistentBean;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Optional;

/**
* A {@link BeanConverterExtension} that is able to record and load {@link Optional} values. The {@link Optional} itself
* will be ignored, only the value is recorded. An empty {@link Optional} is treated like null.
*
* A {@link BeanConverterExtension} that is able to record and load {@link Optional} values.
* The {@link Optional} is encapsulated in a {@link DefaultPersistentBean}, because the {@link PersistentBeanConverter}
* does not pass null values to {@link BeanConverterExtension}s during deserialization. Instead, null values are returned
* as the deserialized value without further conversions. This is a design decision that is based on the assumption that
* null values usually don't need to be converted in most cases.
* <p>
* {@link OptionalExtension} is capable of de-/serializing recursively, so that {@link Optional}s value will also be
* converted, if necessary.
*/
public class OptionalExtension extends StandardBeanConverterExtension {

public static final String OPTIONAL_PROPERTY = "optionalValue";

@Override
public boolean isProcessable(Class<?> beanClass, ParameterizedType beanType) {
return Optional.class.isAssignableFrom(beanClass);
Expand All @@ -25,15 +36,29 @@ public boolean isProcessable(Class<?> beanClass, ParameterizedType beanType) {
@Override
public Object convert(Object originalBean, ParameterizedType beanType,
PersistentBeanConverter persistentBeanConverter) {
Optional<?> optional = (Optional<?>) originalBean;
return optional.orElse(null);
// since isProcessable makes sure, that originalBean is an Optional, we can safely assume, that beanType exists
// and has exactly 1 type argument.
Type optionalsValueType = beanType.getActualTypeArguments()[0];
Object convertedOptionalValue = ((Optional<?>) originalBean).map(o -> persistentBeanConverter.convert(o, optionalsValueType))
.orElse(null);

DefaultPersistentBean persistentBean = new DefaultPersistentBean();
persistentBean.putValue(OPTIONAL_PROPERTY, convertedOptionalValue);

return persistentBean;
}

@Override
@SuppressWarnings("unchecked")
public <T> T revert(Object persistentBean, Class<T> targetClass, ParameterizedType type,
PersistentBeanConverter persistentBeanConverter) {
return (T) Optional.ofNullable(persistentBean);
Object optionalValue = ((PersistentBean) persistentBean).getValue(OPTIONAL_PROPERTY);

Class<?> optionalsValueType = ReflectionTools.getRawClass(type.getActualTypeArguments()[0]);
ParameterizedType optionalsValueParameterizedType = ReflectionTools.getParameterizedType(type.getActualTypeArguments()[0]);

Object revertedValue = persistentBeanConverter.revert(optionalValue, optionalsValueType, optionalsValueParameterizedType);
return (T) Optional.ofNullable(revertedValue);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ void classAndTypeCanBeRetrieved() throws NoSuchMethodException {
ParameterizedType type = (ParameterizedType) getGenericReturnType("getStringCollection");
Class<?> someClass = String.class;

assertNotNull(ReflectionTools.getClass(type));
assertNotNull(ReflectionTools.getClass(someClass));
assertNotNull(ReflectionTools.getRawClass(type));
assertNotNull(ReflectionTools.getRawClass(someClass));
}

@Test
Expand Down

0 comments on commit 02b0041

Please sign in to comment.