Skip to content

Commit

Permalink
Polymorphic persistence Issue #70 (#83)
Browse files Browse the repository at this point in the history
* +Test first: New Testcase for Issue #70

* Intermediate state

* Intermediate state

* Cleanup for new annotations

* Additional tests and fixes

* +javadoc

* -sonar code smells

* -sonar code smells

* -instantiation of inner classes in non public classes is possible now

* Dublicated paths refeactored

* Increased precision in tests

* BeanConverterExtensions can be configured using annotations

* Cleaning and Javadoc

* CodeSmells beseitigt

* linting

* Polymorphic persistence ist now possible. Deserializing works in minimal testcases

* Review

* +Test first: New Testcase for Issue #70

* Polymorphic persistence ist now possible. Deserializing works in minimal testcases

* Update README.md

* Update README.md

* Update README.md

* Removed some typos

* Update README.md

* New graphics and explanations

* Update README.md

* Update README.md

* Refactoring

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* transparency

* transparency reverted

* text revised

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Additional tests

* Refactorings

* Generic properties of Beans can be deserialized using PolymorphicPersistentBean

* The class was moved to the new branch refactoring_simplified_bean_extension. The class is not used yet, and it messes up the test coverage

* Refactoring

* The class was moved to the new branch refactoring_simplified_bean_extension. The class is not used yet, and it messes up the test coverage

* Housekeeping

* Even more tests

* Added javadoc in PolymorphicPersistentBean.java

Co-authored-by: JanSchankin <jas@treeno.de>
Co-authored-by: chb <chb@ppi.de>
Co-authored-by: jas <jan.schankin@ppi.de>
  • Loading branch information
4 people committed Nov 26, 2021
1 parent 33f84cf commit 8a1dfea
Show file tree
Hide file tree
Showing 23 changed files with 667 additions and 60 deletions.
13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ data from a Database:

<img src="/docs/assets/deepsampler-demo-unsampled.png?raw=true" alt="A DAO somewhere inside a compound reads data from a database" width="50%"/>

In order to be independent of the database, we can now attach a stub to the DAO using DeepSampler. After switching
DeepSampler to recording-mode, we can start the test. If a method of the DAO is called during the test, DeepSampler
records the method parameters and the return value. The intercepted data will be saved to a JSON-file, that can be used
as a sample for stubbed tests.
In order to be independent from the database, we can now attach a stub to the methods of the DAO using DeepSampler. If we run the test with DeepSampler in recording-mode, every
call to the method will be intercepted and all data, that was passed to it, or returned by it, is recorded. The recorded data, the sample, will be saved to
a JSON-file.

<img src="/docs/assets/deepsampler-demo-recorder.png?raw=true" alt="All calls to the DAO get intercepted and parameters and return values are recorded" width="50%"/>

Expand All @@ -27,10 +26,8 @@ private MyDao myDaoSampler;
PersistentSample.of(myDaoSampler.load(Matchers.anyInt()));
```

If we repeat the test with DeepSampler switched to player-mode, the original method will not be called anymore. Instead,
a recorded sample from the JSON-file will be returned. If the method is called with particular parameters, DeepSampler
looks for a sample, that has been recorded with the same parameters. This is how even longer tests, with several varying
calls to stubs, can be replayed.
If we repeat the test with DeepSampler switched to player-mode, the original method will not be called anymore. Instead a recorded sample from the JSON-file will be returned.
If the method is called with particular parameters, DeepSampler looks for a sample that has been recorded with the same parameters. This is how even longer tests with several varying calls to stubs can be replayed.

<img src="/docs/assets/deepsampler-demo-player.png?raw=true" alt="Only samples from the previous recording are returned by the stub" width="50%"/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public static <T> T createProxy(final Class<T> cls, final MethodHandler proxyBeh
return (T) proxyObject;
}



public static boolean isProxyClass(final Class<?> aClass) {
return javassist.util.proxy.ProxyFactory.isProxyClass(aClass);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ private void addToPersistentMap(final Map<JsonPersistentSampleMethod, JsonPersis
final JsonPersistentSampleMethod persistentSampleMethod = new JsonPersistentSampleMethod(sample.getSampleId());
final JsonPersistentActualSample jsonPersistentActualSample = new JsonPersistentActualSample();

final Type returnType = sample.getSampledMethod().getMethod().getGenericReturnType();
final ParameterizedType parameterizedReturnType = returnType instanceof ParameterizedType ? (ParameterizedType) returnType : null;
final Type declaredReturnType = sample.getSampledMethod().getMethod().getGenericReturnType();
final Type[] argumentTypes = sample.getSampledMethod().getMethod().getGenericParameterTypes();

for (final MethodCall call : calls) {
final List<Object> argsAsPersistentBeans = convertArguments(call.getArgs(), argumentTypes, persistentSamplerContext);
final Object returnValuePersistentBean = persistentSamplerContext.getPersistentBeanConverter().convert(call.getReturnValue(), parameterizedReturnType);

final Object returnValuePersistentBean = persistentSamplerContext.getPersistentBeanConverter().convert(call.getReturnValue(), declaredReturnType);
final JsonPersistentParameter newParameters = new JsonPersistentParameter(argsAsPersistentBeans);

if (!callWithSameParametersExists(jsonPersistentActualSample, newParameters)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import de.ppi.deepsampler.core.internal.SampleHandling;
import de.ppi.deepsampler.core.model.*;
import de.ppi.deepsampler.persistence.PersistentSamplerContext;
import de.ppi.deepsampler.persistence.bean.PolymorphicPersistentBean;
import de.ppi.deepsampler.persistence.bean.ReflectionTools;
import de.ppi.deepsampler.persistence.bean.ext.BeanConverterExtension;
import de.ppi.deepsampler.persistence.error.PersistenceException;
Expand Down Expand Up @@ -61,7 +62,7 @@ public PersistentSampleManager addBeanExtension(final BeanConverterExtension bea
* End of chain method: call {@link SourceManager#save(Map, PersistentSamplerContext)} on all added {@link SourceManager}s.
*/
public void record() {
for (final SourceManager sourceManager: sourceManagerList) {
for (final SourceManager sourceManager : sourceManagerList) {
sourceManager.save(ExecutionRepository.getInstance().getAll(), persistentSamplerContext);
}
}
Expand All @@ -71,7 +72,7 @@ public void record() {
* all loaded samples to the DeepSampler repositories.
*/
public void load() {
for (final SourceManager sourceManager: sourceManagerList) {
for (final SourceManager sourceManager : sourceManagerList) {
final PersistentModel persistentModel = sourceManager.load();

mergeSamplesFromPersistenceIntoSampleRepository(persistentModel);
Expand All @@ -87,7 +88,7 @@ public void load() {
* This method merges the samples from the persistence (e.g. JSON-File) into manually defined Samplers and Samples. The order of the
* Samplers is defined by the Samplers in the test class or the compound. Samples from the file will be inserted in the list of Samples
* at the position where the matching samplers have been defined.
*
* <p>
* E.G. Someone could now first define a matcher that matches only on a particular parameter of the value
* "Picard". The second matcher could then by anyString(). The first Sample would then be used only if the correct parameter is supplied and in all
* other cases the second sampler would be used.
Expand Down Expand Up @@ -141,7 +142,7 @@ private List<SampleDefinition> createSampleDefinitionForEachPersistentSample(Per
List<SampleDefinition> usedPersistentCalls = new ArrayList<>();
List<SampleDefinition> unusedPersistentCalls = new ArrayList<>();

for(PersistentSampleMethod persistentSampleMethod : persistentSamples.getSampleMethodToSampleMap().keySet()) {
for (PersistentSampleMethod persistentSampleMethod : persistentSamples.getSampleMethodToSampleMap().keySet()) {

if (persistentSampleMethod.getSampleMethodId().equals(sampler.getSampleId())) {
List<PersistentMethodCall> calls = persistentSamples.getSampleMethodToSampleMap().get(persistentSampleMethod).getAllCalls();
Expand Down Expand Up @@ -175,11 +176,19 @@ private SampleDefinition combinePersistentSampleAndDefinedSampler(final SampleDe
final PersistentMethodCall call) {
final List<Object> parameterEnvelopes = call.getPersistentParameter().getParameter();
final Object returnValueEnvelope = call.getPersistentReturnValue();
final Class<?> returnClass;
final SampledMethod sampledMethod = matchingSample.getSampledMethod();

if (returnValueEnvelope instanceof PolymorphicPersistentBean) {
returnClass = ReflectionTools.getOriginalClassFromPolymorphicPersistentBean((PolymorphicPersistentBean) returnValueEnvelope);
} else {
returnClass = sampledMethod.getMethod().getReturnType();
}

final Type[] parameterTypes = sampledMethod.getMethod().getGenericParameterTypes();
final Type genericReturnType = sampledMethod.getMethod().getGenericReturnType();
final ParameterizedType parameterizedReturnType = genericReturnType instanceof ParameterizedType ? (ParameterizedType) genericReturnType : null;
final Class<?> returnClass = sampledMethod.getMethod().getReturnType();

final String joinPointId = persistentSampleMethod.getSampleMethodId();

final List<Object> parameterValues = unwrapValue(joinPointId, parameterTypes, parameterEnvelopes);
Expand All @@ -204,7 +213,7 @@ private List<Object> unwrapValue(final String id, final Type[] parameterTypes, f
"not match the number of persistent parameters (%s:%s)!", id, parameterTypes, parameterPersistentBeans);
}
for (int i = 0; i < parameterPersistentBeans.size(); ++i) {
final ParameterizedType parameterType = parameterTypes[i] instanceof ParameterizedType ? (ParameterizedType) parameterTypes[i] : null;
final ParameterizedType parameterType = parameterTypes[i] instanceof ParameterizedType ? (ParameterizedType) parameterTypes[i] : null;
final Class<?> parameterClass = ReflectionTools.getClass(parameterTypes[i]);
final Object persistentBean = parameterPersistentBeans.get(i);

Expand All @@ -214,7 +223,6 @@ private List<Object> unwrapValue(final String id, final Type[] parameterTypes, f
}



private Object unwrapValue(final Class<?> targetClass, final ParameterizedType type, final Object persistentBean) {
return persistentSamplerContext.getPersistentBeanConverter().revert(persistentBean, targetClass, type);
}
Expand All @@ -224,7 +232,7 @@ private List<ParameterMatcher<?>> toMatcher(final List<Object> params, List<Para
List<ParameterMatcher<?>> resultingParameterMatcher = new ArrayList<>();
for (int i = 0; i < params.size(); ++i) {
Object param = params.get(i);
ParameterMatcher<?> parameterMatcher = parameterMatchers.get(i);
ParameterMatcher<?> parameterMatcher = parameterMatchers.get(i);

if (parameterMatcher instanceof ComboMatcher) {
resultingParameterMatcher.add(s -> ((ComboMatcher<Object>) parameterMatcher).getPersistentMatcher().matches(s, param));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@
* DeepSampler saves Beans by converting them in an abstract model that enables DeepSampler to omit type information in persistent files.
* This approach makes persistent beans less vulnerable to refactorings. E.g. it is not necessary to rename classes in persistent Sample-files
* if classes are renamed during refactorings.
*
* <p>
* The concrete serialization / Deserialization is done by an underlying persistence api. PersistentBeanConverter is only responsible to
* to create an intermediate data structures for cases where the persistence api is not capable to serialize / deserialize the original
* data on its own.
*
*/
public class PersistentBeanConverter {

Expand All @@ -38,11 +37,11 @@ public void addExtension(final BeanConverterExtension extension) {
/**
* Reverts an abstract model from the persistence to the original bean.
*
* @param persistentBean an object that has been deserialized from a persistence api (e.g. some JSON-API). This object
* might already be the original bean it the persistence api was able to deserialize it. Otherwise
* it is the abstract model represented by {@link PersistentBean}
* @param persistentBean an object that has been deserialized from a persistence api (e.g. some JSON-API). This object
* might already be the original bean it the persistence api was able to deserialize it. Otherwise
* it is the abstract model represented by {@link PersistentBean}
* @param parameterizedType The Type of the original bean
* @param <T> the original bean.
* @param <T> the original bean.
* @return the original deserialized bean.
*/
@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -74,28 +73,36 @@ public <T> T revert(final Object persistentBean, final Class<T> originalBeanClas

/**
* Converts an original bean to the abstract model (most likely {@link PersistentBean} that is used to save the original bean to e.g. JSON.
*
* @param originalBean The original Bean that is supposed to be persisted.
* @param <T> The type of the persistent bean.
* @param <T> The type of the persistent bean.
* @return The object that will be sent to the underlying persistence api. This might be a {@link PersistentBean} or the original bean if
* the persistence api is expected to be able to serialize the original bean directly.
*/
@SuppressWarnings("unchecked")
public <T> T convert(final Object originalBean, ParameterizedType parameterizedType) {
if (isTransformationNotNecessary(originalBean, parameterizedType)) {
public <T> T convert(final Object originalBean, Type type) {
final ParameterizedType parameterizedReturnType = type instanceof ParameterizedType ? (ParameterizedType) type : null;
if (isTransformationNotNecessary(originalBean, parameterizedReturnType)) {
return (T) originalBean;
}

if (originalBean.getClass().isArray()) {
return (T) convertArray((Object[]) originalBean);
return (T) convertObjectArray((Object[]) originalBean);
}

final List<BeanConverterExtension> applicableExtensions = findApplicableExtensions(originalBean.getClass(), parameterizedType);

final List<BeanConverterExtension> applicableExtensions = findApplicableExtensions(originalBean.getClass(), parameterizedReturnType);
if (!applicableExtensions.isEmpty()) {
// Only use the first one!
return (T) applicableExtensions.get(0).convert(originalBean, parameterizedType, this);
return (T) applicableExtensions.get(0).convert(originalBean, parameterizedReturnType, this);
}

if (originalBean.getClass().equals(type)
|| (parameterizedReturnType != null && originalBean.getClass().equals(parameterizedReturnType.getRawType()))) {
return (T) convertToPersistentBean(originalBean);
}

return (T) convertToPersistentBean(originalBean);
return (T) convertToPolymorphicPersistentBean(originalBean.getClass().getTypeName(), originalBean);
}

@SuppressWarnings("unchecked")
Expand All @@ -117,20 +124,28 @@ private <T> T[] revertPersistentBeanArray(final Object persistentBeanArray, fina
return (T[]) originalBeansArray;
}

private <T> T revertPersistentBean(final PersistentBean value, final Class<T> originalBeanClass) {
@SuppressWarnings("unchecked")
private <T> T revertPersistentBean(final PersistentBean value, final Class<T> declaredOriginalBeanClass) {
final Class<T> valueType;
if (value instanceof PolymorphicPersistentBean) {
valueType = (Class<T>) ReflectionTools.getOriginalClassFromPolymorphicPersistentBean((PolymorphicPersistentBean) value);
} else {
valueType = declaredOriginalBeanClass;
}

final T instance;
final Map<Field, String> fields = getAllFields(originalBeanClass);
final Map<Field, String> fields = getAllFields(valueType);

if (hasFinalFields(fields)) {
instance = instantiateUsingMatchingConstructor(originalBeanClass, value, fields);
instance = instantiateUsingMatchingConstructor(valueType, value, fields);
} else {
instance = instantiate(originalBeanClass);
instance = instantiate(valueType);

for (final Map.Entry<Field, String> entry : fields.entrySet()) {
final Field field = entry.getKey();
final String key = entry.getValue();

transferFromBean(value, instance, field, key);
transferFieldFromBean(value, instance, field, key);
}
}
return instance;
Expand Down Expand Up @@ -183,10 +198,10 @@ private boolean hasFinalFields(final Map<Field, String> fields) {
.anyMatch(entry -> Modifier.isFinal(entry.getKey().getModifiers()));
}

private <T> void transferFromBean(final PersistentBean persistentBean, final T instance, final Field field, final String key) {
Object lookedUpValueInBean = persistentBean.getValue(key);
private <T> void transferFieldFromBean(final PersistentBean persistentBean, final T instance, final Field field, final String fieldKeyInPersistentBean) {
Object lookedUpValueInBean = persistentBean.getValue(fieldKeyInPersistentBean);
if (lookedUpValueInBean != null) {
if (lookedUpValueInBean instanceof DefaultPersistentBean) {
if (lookedUpValueInBean instanceof PersistentBean) {
lookedUpValueInBean = revertPersistentBean((DefaultPersistentBean) lookedUpValueInBean, field.getType());
} else if (lookedUpValueInBean.getClass().isArray() && PersistentBean.class.isAssignableFrom(lookedUpValueInBean.getClass().getComponentType())) {
lookedUpValueInBean = revertPersistentBeanArray(lookedUpValueInBean, field.getType());
Expand All @@ -202,8 +217,7 @@ private <T> T instantiate(final Class<T> cls) {
}



private Object convertArray(final Object[] objects) {
private Object convertObjectArray(final Object[] objects) {
int[] dimensions = ReflectionTools.getArrayDimensions(objects);
Class<?> componentType = Array.newInstance(PersistentBean.class, dimensions).getClass();
Object persistentBeans = ReflectionTools.createEmptyArray(objects, componentType);
Expand All @@ -216,8 +230,20 @@ private Object convertArray(final Object[] objects) {
return persistentBeans;
}

private PersistentBean convertToPolymorphicPersistentBean(final String type, final Object obj) {
final Map<String, Object> valuesForBean = getValueMapForObjects(obj);

return new PolymorphicPersistentBean(valuesForBean, type);
}


private PersistentBean convertToPersistentBean(final Object obj) {
final Map<String, Object> valuesForBean = getValueMapForObjects(obj);

return new DefaultPersistentBean(valuesForBean);
}

private Map<String, Object> getValueMapForObjects(Object obj) {
final Map<Field, String> fieldStringMap = getAllFields(obj.getClass());

final Map<String, Object> valuesForBean = new HashMap<>();
Expand All @@ -235,8 +261,7 @@ private PersistentBean convertToPersistentBean(final Object obj) {

valuesForBean.put(keyForField, fieldValue);
}

return new DefaultPersistentBean(valuesForBean);
return valuesForBean;
}

@SuppressWarnings("java:S3011") // We need the possibility to set the values of private fields for deserialization.
Expand All @@ -262,7 +287,6 @@ private Object retrieveValue(final Object obj, final Field field) {
}



private Map<Field, String> getAllFields(final Class<?> cls) {
final Map<Field, String> fields = new LinkedHashMap<>();
Class<?> currentCls = cls;
Expand All @@ -280,7 +304,6 @@ private Map<Field, String> getAllFields(final Class<?> cls) {
}



private List<BeanConverterExtension> findApplicableExtensions(final Class<?> beanClass, final ParameterizedType parameterizedType) {
return beanConverterExtensions.stream().filter(ext -> ext.isProcessable(beanClass, parameterizedType)).collect(Collectors.toList());
}
Expand All @@ -289,11 +312,8 @@ private boolean isTransformationNotNecessary(final Object obj, final Parameteriz

return obj == null || ReflectionTools.isPrimitiveOrWrapper(obj.getClass()) || (!ReflectionTools.isObjectArray(obj.getClass()) && obj.getClass().isArray())
|| findApplicableExtensions(obj.getClass(), parameterizedType).stream()
.anyMatch(ext -> ext.skip(obj.getClass(), parameterizedType));
.anyMatch(ext -> ext.skip(obj.getClass(), parameterizedType));
}





}
Loading

0 comments on commit 8a1dfea

Please sign in to comment.