SGF-745 - Add ability to filter types de/serialized by the o.s.d.g.mapping.MappingPdxSerializer.

This commit is contained in:
John Blum
2018-05-14 15:07:06 -07:00
parent 05bbdcd278
commit f93e0e2602
6 changed files with 906 additions and 65 deletions

View File

@@ -4,7 +4,10 @@
[[mapping.entities]]
== Entity Mapping
Spring Data GemFire provides support to map entities that will be stored in a GemFire data grid. The mapping metadata is defined using annotations at the domain classes just like this:
_Spring Data for Pivotal GemFire_ provides support to map entities that will be stored in a Region
in the Geode In-Memory Data Grid.
The mapping metadata is defined using annotations on application domain classes just like this:
.Mapping a domain class to a GemFire Region
====
@@ -66,15 +69,26 @@ public interface CustomerRepository extends GemfireRepository<Person, String> {
[[mapping.pdx-serializer]]
== Mapping PDX Serializer
Spring Data GemFire provides a custom `PDXSerializer` implementation that uses the mapping information to customize entity serialization. Beyond that it allows customizing the entity instantiation by using the Spring Data `EntityInstantiator` abstraction. By default the serializer uses a `ReflectionEntityInstantiator` that will use the persistence constructor of the mapped entity (either the single declared one or explicitly annoted with `@PersistenceConstructor`). To provide values for constructor parameters it will read fields with name of the constructor parameters from the `PDXReader` supplied.
_Spring Data for Pivotal GemFire_ provides a custom
http://gemfire-95-javadocs.docs.pivotal.io/org/apache/geode/pdx/PdxSerializer.html[PdxSerializer] implementation
that uses the mapping information to customize entity serialization.
.Using @Value on entity constructor parameters
Beyond that, it also allows customizing entity instantiation by using the Spring Data `EntityInstantiator` abstraction.
By default, the serializer uses a `ReflectionEntityInstantiator` that will use the persistence constructor of
the mapped entity (either the default constructor, a singly declared constructor or an explicitly annotated constructor
annotated with the `@PersistenceConstructor` annotation).
To provide arguments for constructor parameters, the serializer will read fields with the named constructor parameter,
explicitly specified using Spring's `@Value` annotation, from the supplied
http://gemfire-95-javadocs.docs.pivotal.io/org/apache/geode/pdx/PdxReader.html[PdxReader].
.Using `@Value` on entity constructor parameters
====
[source,java]
----
public class Person {
public Person(@Value("#root.foo") String firstname, @Value("bean") String lastname) {
public Person(@Value("#root.foo") String firstName, @Value("bean") String lastName) {
// …
}
@@ -82,5 +96,230 @@ public class Person {
----
====
The entity annotated as such will get the field `foo` read from the `PDXReader` and handed as constructor parameter value for `firstname`. The value for `lastname` will be the Spring bean with name `bean`.
An entity class annotated in this way will have the field `foo` read from the `PdxReader` and passed as the value
for the constructor parameter, `firstname`. The value for `lastName` will be a _Spring_ bean with the name `bean`.
In addition to the custom instantiation logic and strategy provided by `EntityInstantiators`
the `MappingPdxSerializer` also provides capabilities above and beyond even Pivotal GemFire's own
http://gemfire-95-javadocs.docs.pivotal.io/org/apache/geode/pdx/ReflectionBasedAutoSerializer.html[`ReflectionBasedAutoSerializer`].
While Pivotal GemFire's `ReflectionBasedAutoSerializer` conveniently uses Java Reflection to populate entities as well as
use _Regular Expressions_ to identify types that should be handled (de/serialized) by the `ReflectionBasedAutoSerializer`,
it cannot, unlike `MappingPdxSerializer`, perform the following:
1. Register custom `PdxSerializer` objects per entity field/property names and/or types.
2. Conveniently identifies ID properties.
3. Automatically handles *read-only* properties.
4. Automatically handles *transient* properties.
5. Allows more robust *type filtering* in a `null`-safe manner (e.g. not limited to only expressing types via Regex).
We now explore each feature of the `MappingPdxSerializer` in a bit more detail.
[[mapping.pdx-serializer.custom-serialization]]
=== Custom PdxSerializer Registration
The `MappingPdxSerializer` gives you the ability to register custom `PdxSerializers` based on an entity's
field/property names and/or types.
For instance, suppose you have defined an entity type modeling a `User` as...
[source,java]
----
package example.app.auth.model;
public class User {
private String name;
private Password password;
...
}
----
While the `User's` "name" probably does not require any special logic to serialize the value for name, serializing
the `Password` might require additional logic in order to handle the sensitive nature of the field or property.
Perhaps you want to protect the password when sending the value over the network, between a client and a server,
and you only want to store the _Salted Hash_. When using the `MappingPdxSerializer` you can register
a custom `PdxSerializer` to handle the `User's` `Password`, like so...
.Registering custom `PdxSerializers` by POJO field/property type
====
[source,java]
----
Map<?, PdxSerializer> customPdxSerializers = new HashMap<>();
customPdxSerializers.put(Password.class, new SaltedHashPasswordPdxSerializer());
mappingPdxSerializer.setCustomPdxSerializers(customPdxSerializers);
----
After registering the application-defined `SaltedHashPasswordPdxSerializer` instance with the `Password`
application domain model type, the `MappingPdxSerializer` will consult the custom `PdxSerializer` to
de/serialize *all* `Password` objects regardless of the containing object (e.g. `User`).
However, suppose you only want to customize the serialization of `Passwords` on `User` objects, specifically.
Then, you can register the custom `PdxSerializer` for the `User` type only by specifying the fully-qualified
name of the `Class's` field/property. For example:
.Registering custom `PdxSerializers` by POJO field/property name
====
[source,java]
----
Map<?, PdxSerializer> customPdxSerializers = new HashMap<>();
customPdxSerializers.put("example.app.auth.model.User.password", new SaltedHashPasswordPdxSerializer());
mappingPdxSerializer.setCustomPdxSerializers(customPdxSerializers);
----
Notice the use of the fully-qualified field/propety name (i.e. "example.app.auth.model.User.password")
as the custom `PdxSerializer` registration key.
NOTE: You could construct the registration key using a more logical code snippet, such as:
`User.class.getName().concat(".password");` This is recommended over the example shown above. The example was simply
trying to be very explicit in the semantics of registration.
[[mapping.pdx-serializer.id-properties]]
=== Mapping ID Properties
Like Pivotal GemFire's `ReflectionBasedAutoSerializer`, SDG's `MappingPdxSerializer` is also able to determine
the identifier of the entity. However, `MappingPdxSerializer` does so by using Spring Data's mapping meta-data,
specifically by finding the entity property designated as the identifier using the
https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/annotation/Id.html[`@Id`] Spring Data annotation.
For example:
[source,java]
----
class Customer {
@Id
Long id;
...
}
----
In this case, the `Customer's` `id` field will be marked as the identifier field in the PDX type meta-data using
http://gemfire-95-javadocs.docs.pivotal.io/org/apache/geode/pdx/PdxWriter.html#markIdentityField-java.lang.String-[`PdxWriter.markIdentifierField(:String)`]
when the `PdxSerializer.toData(..)` method is called during serialization.
[[mapping.pdx-serializer.read-only-properties]]
=== Mapping Read-only Properties
What happens when your entity defines a read-only property?
First, it is important to understand what a "read-only" property is. If you define a POJO following the http://www.oracle.com/technetwork/java/javase/documentation/spec-136004.html[JavaBeans]
specification (as Spring does), and you have defined a POJO with some read-only property as follows:
[source,java]
----
package example;
class ApplicationDomainType {
private AnotherType readOnly;
public AnotherType getReadOnly() [
this.readOnly;
}
...
}
----
Then the `readOnly` property is "read-only" because it does not provide a setter method; it only has a getter method.
In this case, the `readOnly` property (not to be confused with the `readOnly` `DomainType` field)
is considered "read-only".
As such, the `MappingPdxSerializer` will not try to write this value back when populating the instance of `DomainType`
in the `PdxSerializer.fromData(:Class<?>, :PdxReader)` method.
This is useful in situations where you might be returning a view or projection of some entity type and you only want
to write state that is writable. Perhaps the view or projection of the entity is based on authorization or some other
criteria. The point is, you can leverage this feature as is appropriate for your application use cases and requirements.
If you want the field/property to always be written then simply define a setter.
[[mapping.pdx-serializer.transient-properties]]
=== Mapping Transient Properties
Likewise, what happens when your entity defines `transient` properties?
You would expect the `transient` fields/properties of your entity not to be serialized to the stream of PDX bytes
when serializing entity. And, that is exactly what happens, unlike Pivotal GemFire's own
`ReflectionBasedAutoSerializer`, which serializes everything accessible from the object via _Java Reflection_.
The `MappingPdxSerializer` will not serialize any fields or properties which are qualified as transient either using
Java's `transient` keyword (in the case of fields) or when using the
https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/annotation/Transient.html[`@Transient`]
Spring Data annotation on either fields or properties.
For example, if you defined an enity with transient fields and properties, like so...
[source,java]
----
package example;
class Process {
private transient int id;
private File workingDirectory;
private String name;
private Type type;
@Transient
public String getHostname() {
...
}
...
}
----
Neither the `Process` `id` field nor the readable `hostname` property will be written to the PDX serialized bytes.
[[mapping.pdx-serializer.type-filtering]]
=== Filtering by Class types
Similar to Pivotal GemFire's `ReflectionBasedAutoSerializer`, SDG's `MappingPdxSerializer` allows a user to filter
the types of objects that the `MappingPdxSerializer` will handle, i.e. de/serialize.
However, unlike Pivotal GemFire's `ReflectionBasedAutoSerializer`, which uses complex _Regular Expressions_ to express
which types the serializer will handle, SDG's `MappingPdxSerializer` uses the much more robust
https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html[`java.util.function.Predicate`] interface
and API to express type matching criteria.
Plus, if you feel strongly about using _Regular Expressions_, then you can always implement a `Predicate` using
_Java's_ https://docs.oracle.com/javase/8/docs/api/java/util/regex/package-summary.html[_Regular Expression_ support].
The nice part about Java's `Predicate` interface is that you can compose `Predicates` using the convenient
and appropriate API:
https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html#and-java.util.function.Predicate-[`and(:Predicate)`],
https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html#or-java.util.function.Predicate-[`or(:Predicate)`]
and https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html#negate--[`negate()`].
For example:
[source,java]
----
Predicate<Class<?>> customerTypes =
type -> Customer.class.getPackage().getName().startsWith(type.getName());
Predicate typeFilters = customerTypes
.or(type -> User.class.isAssignble(type)) // Include User sub-types (e.g. Admin, Guest, etc)
.and(type -> !Reference.class.getPackage(type.getPackage()); // Exclude all Reference types
mappingPdxSerializer.setTypeFilters(typeFilters);
----
NOTE: In addition to setting your own type filtering `Predicates`, SDG's `MappingPdxSerializer` now automatically
registers pre-canned `Predicates` that filters types from the `org.apache.geode` package along with `null` objects
when calling `PdxSerializer.toData(:Object, :PdxWriter)` or `null` `Class` types when calling
`PdxSerializer.fromData(:Class<?>, :PdxReader)` methods.

View File

@@ -15,6 +15,9 @@
*/
package org.springframework.data.gemfire.mapping;
import static org.springframework.data.gemfire.mapping.MappingPdxSerializer.ExcludeComGemstoneGemFireTypesFilter.EXCLUDE_COM_GEMSTONE_GEMFIRE_TYPES;
import static org.springframework.data.gemfire.mapping.MappingPdxSerializer.ExcludeNullTypesFilter.EXCLUDE_NULL_TYPES;
import java.util.Collections;
import java.util.Map;
@@ -22,8 +25,8 @@ import com.gemstone.gemfire.pdx.PdxReader;
import com.gemstone.gemfire.pdx.PdxSerializer;
import com.gemstone.gemfire.pdx.PdxWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -31,6 +34,7 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.convert.EntityInstantiator;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.gemfire.util.Filter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
@@ -65,42 +69,59 @@ import org.springframework.util.ObjectUtils;
*/
public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAware {
private final ConversionService conversionService;
private EntityInstantiators entityInstantiators;
private final GemfireMappingContext mappingContext;
protected final Log log = LogFactory.getLog(getClass());
private Map<?, PdxSerializer> customPdxSerializers;
// TODO: decide what to do with this; SpELContext is not used
private SpELContext context;
/**
* Factory method used to construct a new instance of {@link MappingPdxSerializer} initialized with
* a provided {@link GemfireMappingContext} and default {@link ConversionService}.
*
* @return a new instance of {@link MappingPdxSerializer}.
* @see #create(GemfireMappingContext, ConversionService)
* @see #newMappingContext()
* @see #newConversionService()
*/
public static MappingPdxSerializer newMappingPdxSerializer() {
return create(newMappingContext(), newConversionService());
}
/**
* Factory method used to construct a new instance of {@link MappingPdxSerializer} initialized with
* the given {@link ConversionService} and a provided {@link GemfireMappingContext}.
*
* @param conversionService {@link ConversionService} used to convert persistent values to entity properties.
* @return a new instance of {@link MappingPdxSerializer} initialized with the given {@link ConversionService}.
* @see org.springframework.core.convert.ConversionService
* @see #create(GemfireMappingContext, ConversionService)
* @see #newMappingContext()
*/
public static MappingPdxSerializer create(ConversionService conversionService) {
return create(newMappingContext(), conversionService);
}
/**
* Factory method used to construct a new instance of {@link MappingPdxSerializer} initialized with
* the given {@link GemfireMappingContext mapping context} supplying entity mapping meta-data,
* using a provided, default {@link ConversionService}.
*
* @param mappingContext {@link GemfireMappingContext} used to supply entity mapping meta-data.
* @return a new instance of {@link MappingPdxSerializer} initialized with
* the given {@link GemfireMappingContext mapping context}.
* @see org.springframework.data.gemfire.mapping.GemfireMappingContext
* @see #create(GemfireMappingContext, ConversionService)
* @see #newConversionService()
*/
public static MappingPdxSerializer create(GemfireMappingContext mappingContext) {
return create(mappingContext, newConversionService());
}
/**
* Factory method used to construct a new instance of the {@link MappingPdxSerializer} initialized with
* Factory method used to construct a new instance of {@link MappingPdxSerializer} initialized with
* the given {@link GemfireMappingContext mapping context} and {@link ConversionService conversion service}.
*
* If either the {@link GemfireMappingContext mapping context} or the {@link ConversionService conversion service}
* are {@literal null}, then this factory method will provide default instances for each.
*
* @param mappingContext {@link GemfireMappingContext} used by the {@link MappingPdxSerializer} to map
* between application domain object types and PDX serialized bytes based on the entity mapping meta-data.
* @param conversionService {@link ConversionService} used by the {@link MappingPdxSerializer} to convert
* PDX serialized data to application object property types.
* @param mappingContext {@link GemfireMappingContext} used to map between application domain model object types
* and PDX serialized bytes based on the entity's mapping meta-data.
* @param conversionService {@link ConversionService} used to convert persistent values to entity properties.
* @return an initialized instance of the {@link MappingPdxSerializer}.
* @see org.springframework.core.convert.ConversionService
* @see org.springframework.data.gemfire.mapping.MappingPdxSerializer
@@ -159,6 +180,21 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
return mappingContext != null ? mappingContext : newMappingContext();
}
private final ConversionService conversionService;
private EntityInstantiators entityInstantiators;
private final GemfireMappingContext mappingContext;
private final Logger logger = LoggerFactory.getLogger(getClass());
private Map<?, PdxSerializer> customPdxSerializers;
private Filter typeFilters = EXCLUDE_NULL_TYPES.and(EXCLUDE_COM_GEMSTONE_GEMFIRE_TYPES);
// TODO: decide what to do with this; SpELContext is not used
private SpELContext spelContext;
/**
* Constructs a new instance of {@link MappingPdxSerializer} using a default {@link GemfireMappingContext}
* and {@link DefaultConversionService}.
@@ -185,14 +221,14 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
*/
public MappingPdxSerializer(GemfireMappingContext mappingContext, ConversionService conversionService) {
Assert.notNull(mappingContext, "MappingContext is required");
Assert.notNull(conversionService, "ConversionService is required");
Assert.notNull(mappingContext, "MappingContext must not be null");
Assert.notNull(conversionService, "ConversionService must not be null");
this.mappingContext = mappingContext;
this.conversionService = conversionService;
this.entityInstantiators = new EntityInstantiators();
this.customPdxSerializers = Collections.emptyMap();
this.context = new SpELContext(PdxReaderPropertyAccessor.INSTANCE);
this.spelContext = new SpELContext(PdxReaderPropertyAccessor.INSTANCE);
}
/**
@@ -200,7 +236,7 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = new SpELContext(context, applicationContext);
this.spelContext = new SpELContext(this.spelContext, applicationContext);
}
/**
@@ -211,7 +247,7 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
* @see org.springframework.core.convert.ConversionService
*/
protected ConversionService getConversionService() {
return conversionService;
return this.conversionService;
}
/**
@@ -226,7 +262,7 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
*/
public void setCustomPdxSerializers(Map<?, PdxSerializer> customPdxSerializers) {
Assert.notNull(customPdxSerializers, "Custom PdxSerializers are required");
Assert.notNull(customPdxSerializers, "Custom PdxSerializers must not be null");
this.customPdxSerializers = customPdxSerializers;
}
@@ -307,10 +343,31 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
/**
* Configures the {@link EntityInstantiator}s used to create the instances read by this PdxSerializer.
*
* @param gemfireInstantiators must not be {@literal null}.
* @param entityInstantiators {@link EntityInstantiator EntityInstantiators} used to create the instances
* read by this {@link PdxSerializer}; must not be {@literal null}.
* @see org.springframework.data.convert.EntityInstantiator
*/
public void setGemfireInstantiators(EntityInstantiators entityInstantiators) {
Assert.notNull(entityInstantiators, "EntityInstantiators must not be null");
this.entityInstantiators = entityInstantiators;
}
/**
* Configures the {@link EntityInstantiator EntityInstantiators} used to create the instances
* read by this {@link PdxSerializer}.
*
* @param gemfireInstantiators mapping of {@link Class types} to {@link EntityInstantiator} objects;
* must not be {@literal null}.
* @see org.springframework.data.convert.EntityInstantiator
* @see java.util.Map
>>>>>>> 3ed1599... SGF-745 - Add ability to filter types de/serialized by the o.s.d.g.mapping.MappingPdxSerializer.
*/
public void setGemfireInstantiators(Map<Class<?>, EntityInstantiator> gemfireInstantiators) {
Assert.notNull(gemfireInstantiators, "GemFire EntityInstantiators are required");
this.entityInstantiators = new EntityInstantiators(gemfireInstantiators);
}
@@ -333,14 +390,14 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
}
/**
* Returns a reference to the configured {@link Log} used to log {@link String messages}
* Returns a reference to the configured {@link Logger} used to log {@link String messages}
* about the functions of this {@link PdxSerializer}.
*
* @return a reference to the configured {@link Log}.
* @return a reference to the configured {@link Logger}.
* @see org.apache.commons.logging.Log
*/
protected Log getLogger() {
return this.log;
protected Logger getLogger() {
return this.logger;
}
/**
@@ -378,8 +435,49 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
return getMappingContext().getPersistentEntity(entityType);
}
/**
* Sets the {@link Filter type filters} used to filter {@link Class types} serializable
* by this {@link MappingPdxSerializer PDX serializer}.
*
* This operation is null-safe and rather than overriding the existing {@link Filter type filters},
* this set operation combines the given {@link Filter type filters} with
* the exiting {@link Filter type filters} joined by {@literal and}.
*
* @param typeFilters {@link Filter type filters} used to to filter {@link Class type} serializable
* by this {@link MappingPdxSerializer PDX serializer}.
* @see org.springframework.data.gemfire.util.Filter
*/
public void setTypeFilters(Filter<Class<?>> typeFilters) {
this.typeFilters = typeFilters != null ? this.typeFilters.and(typeFilters) : this.typeFilters;
}
/**
* Returns the {@link Filter type filters} used to filter {@link Class types} serializable
* by this {@link MappingPdxSerializer PdxSerializer}.
*
* @return the resolved {@link Filter type filter}.
* @see org.springframework.data.gemfire.util.Filter
*/
protected Filter<Class<?>> getTypeFilters() {
return this.typeFilters;
}
@Override
public Object fromData(final Class<?> type, final PdxReader reader) {
public Object fromData(Class<?> type, PdxReader reader) {
return getTypeFilters().accept(type) ? doFromData(type, reader) : null;
}
/**
* Converts a set of PDX serialized bytes to an {@link Object} of the specified {@link Class type}.
*
* @param type desired {@link Class type} of the {@link Object}.
* @param reader {@link PdxReader} used to access the PDX bytes to convert.
* @return an {@link Object} of the specified {@link Class type} converted from the PDX bytes.
* @see com.gemstone.gemfire.pdx.PdxReader
* @see java.lang.Object
* @see java.lang.Class
*/
Object doFromData(final Class<?> type, final PdxReader reader) {
final GemfirePersistentEntity<?> entity = getPersistentEntity(type);
@@ -412,10 +510,6 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
? customPdxSerializer.fromData(persistentProperty.getType(), reader)
: reader.readField(persistentProperty.getName()));
if (getLogger().isDebugEnabled()) {
getLogger().debug(String.format("... with value [%s]", value));
}
propertyAccessor.setProperty(persistentProperty, value);
}
catch (Exception cause) {
@@ -431,7 +525,21 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
return propertyAccessor.getBean();
}
/* (non-Javadoc) */
/**
* Determines whether the {@link PersistentProperty persistent property}
* of the given {@link PersistentEntity entity } is writable.
*
* The {@link PersistentProperty persistent property} is considered {@literal writable} if the property
* is not a constructor parameter of the {@link PersistentEntity entity's} {@link Class type}, the property
* has a {@literal setter} method and the property is not {@literal transient}.
*
* @param entity {@link GemfirePersistentEntity} containing the {@link GemfirePersistentProperty property}.
* @param persistentProperty {@link GemfirePersistentProperty} to evaluate.
* @return a boolean value indicating whether the {@link PersistentProperty persistent property}
* of the given {@link PersistentEntity entity } is writable.
* @see org.springframework.data.gemfire.mapping.GemfirePersistentEntity
* @see org.springframework.data.gemfire.mapping.GemfirePersistentProperty
*/
boolean isWritable(GemfirePersistentEntity<?> entity, GemfirePersistentProperty persistentProperty) {
return !entity.isConstructorArgument(persistentProperty)
@@ -440,12 +548,26 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
}
@Override
public boolean toData(Object value, PdxWriter writer) {
return getTypeFilters().accept(resolveType(value)) && doToData(value, writer);
}
/**
* Converts the given {@link Object} into a stream of PDX bytes.
*
* @param value {@link Object} to convert.
* @param writer {@link PdxWriter} used to stream the given {@link Object} into a stream of PDX bytes.
* @return a boolean value indicating whether this {@link MappingPdxSerializer PDX serializer} was able to
* write the given {@link Object} as a stream of PDX bytes.
* @see com.gemstone.gemfire.pdx.PdxWriter
* @see java.lang.Object
*/
@SuppressWarnings("unchecked")
public boolean toData(Object value, final PdxWriter writer) {
boolean doToData(Object value, final PdxWriter writer) {
final GemfirePersistentEntity<?> entity = getPersistentEntity(value);
// Entity will be null for simple types
// Entity will be null for simple types (e.g. int, Long, String, etc).
if (entity != null) {
final PersistentPropertyAccessor propertyAccessor =
@@ -501,12 +623,56 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
return true;
}
return false;
}
/* (non-Javadoc) */
/**
* Determines whether the given {@link PersistentProperty persistent property} is readable.
*
* The {@link PersistentProperty persistent property} is considered {@literal readable}
* if the property is not {@literal transient}.
*
* @param persistentProperty {@link GemfirePersistentProperty} to evaluate.
* @return a boolean value indicating whether the {@link PersistentProperty persistent property}
* is readable.
* @see org.springframework.data.gemfire.mapping.GemfirePersistentProperty
*/
boolean isReadable(GemfirePersistentProperty persistentProperty) {
return !persistentProperty.isTransient();
}
/**
* Resolves the {@link Class type} of the given {@link Object}.
*
* @param obj {@link Object} to evaluate.
* @return the {@link Class type} of the given {@link Object}.
* @see java.lang.Object#getClass()
* @see java.lang.Class
*/
Class<?> resolveType(Object obj) {
return obj != null ? obj.getClass() : null;
}
public static class ExcludeComGemstoneGemFireTypesFilter extends org.springframework.data.gemfire.util.AbstractFilter<Class<?>> {
public static final Filter<Class<?>> EXCLUDE_COM_GEMSTONE_GEMFIRE_TYPES =
new ExcludeComGemstoneGemFireTypesFilter();
protected static final String COM_GEMSTONE_GEMFIRE_PACKAGE_NAME = "com.gemstone.gemfire";
@Override
public boolean accept(Class<?> type) {
return type != null && !type.getPackage().getName().startsWith(COM_GEMSTONE_GEMFIRE_PACKAGE_NAME);
}
}
public static class ExcludeNullTypesFilter extends org.springframework.data.gemfire.util.AbstractFilter<Class<?>> {
public static final Filter<Class<?>> EXCLUDE_NULL_TYPES = new ExcludeNullTypesFilter();
@Override
public boolean accept(Class<?> type) {
return type != null;
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2018 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.springframework.data.gemfire.util;
/**
* The {@link AbstractFilter} class is an abstract base class encapsulating functionality
* common to all {@link Filter} implementations.
*
* @author John Blum
* @see org.springframework.data.gemfire.util.Filter
* @since 1.9.0
*/
public abstract class AbstractFilter<T> implements Filter<T> {
/**
* Combines this {@link Filter} with the given {@link Filter} using {@literal logical AND}.
*
* @param filter {@link Filter} to compose with this {@link Filter}.
* @return a new {@link Filter} composed of this {@link Filter} and the given {@link Filter}
* using {@literal logical AND}.
* @see org.springframework.data.gemfire.util.Filter
*/
@Override
public Filter<T> and(final Filter<T> filter) {
return new AbstractFilter<T>() {
@Override
public boolean accept(T obj) {
return AbstractFilter.this.accept(obj) && filter.accept(obj);
}
};
}
/**
* Negates the result of the {@link #accept(Object)} method.
*
* @return a new {@link Filter} negating the results of the {@link #accept(Object)} method.
* @see org.springframework.data.gemfire.util.Filter
*/
@Override
public Filter<T> negate() {
return new AbstractFilter<T>() {
@Override
public boolean accept(T obj) {
return !AbstractFilter.this.accept(obj);
}
};
}
/**
* Combines this {@link Filter} with the given {@link Filter} using {@literal logical OR}.
*
* @param filter {@link Filter} to compose with this {@link Filter}.
* @return a new {@link Filter} composed of this {@link Filter} and the given {@link Filter}
* using {@literal logical OR}.
* @see org.springframework.data.gemfire.util.Filter
*/
@Override
public Filter<T> or(final Filter<T> filter) {
return new AbstractFilter<T>() {
@Override
public boolean accept(T obj) {
return AbstractFilter.this.accept(obj) || filter.accept(obj);
}
};
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2018 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.springframework.data.gemfire.util;
/**
* The {@link Filter} interface defines a contract for filtering {@link Object objects}.
*
* @author John Blum
* @param <T> {@link Class type} of {@link Object objects} being filtered.
* @since 1.0.0
*/
public interface Filter<T> {
/**
* Evaluates the given {@link Object} and determines whether the {@link Object} is accepted
* based on the filter criteria.
*
* @param obj {@link Object} to filter.
* @return a boolean value indicating whether this {@link Filter} accepts the given {@link Object}
* based on the filter criteria.
*/
boolean accept(T obj);
/**
* Combines this {@link Filter} with the given {@link Filter} using {@literal logical AND}.
*
* @param filter {@link Filter} to compose with this {@link Filter}.
* @return a new {@link Filter} composed of this {@link Filter} and the given {@link Filter}
* using {@literal logical AND}.
* @see org.springframework.data.gemfire.util.Filter
*/
Filter<T> and(Filter<T> filter);
/**
* Negates the result of the {@link #accept(Object)} method.
*
* @return a new {@link Filter} negating the results of the {@link #accept(Object)} method.
* @see org.springframework.data.gemfire.util.Filter
*/
Filter<T> negate();
/**
* Combines this {@link Filter} with the given {@link Filter} using {@literal logical OR}.
*
* @param filter {@link Filter} to compose with this {@link Filter}.
* @return a new {@link Filter} composed of this {@link Filter} and the given {@link Filter}
* using {@literal logical OR}.
* @see org.springframework.data.gemfire.util.Filter
*/
Filter<T> or(Filter<T> filter);
}

View File

@@ -20,10 +20,14 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -47,9 +51,17 @@ import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.EntityInstantiator;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.gemfire.repository.sample.Account;
import org.springframework.data.gemfire.repository.sample.Address;
import org.springframework.data.gemfire.repository.sample.Customer;
import org.springframework.data.gemfire.repository.sample.Person;
import org.springframework.data.gemfire.repository.sample.Programmer;
import org.springframework.data.gemfire.repository.sample.RootUser;
import org.springframework.data.gemfire.repository.sample.User;
import org.springframework.data.gemfire.test.model.Gender;
import org.springframework.data.gemfire.test.support.MapBuilder;
import org.springframework.data.gemfire.util.AbstractFilter;
import org.springframework.data.gemfire.util.Filter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.MappingException;
@@ -77,27 +89,27 @@ import org.springframework.data.mapping.model.ParameterValueProvider;
@RunWith(MockitoJUnitRunner.class)
public class MappingPdxSerializerUnitTests {
ConversionService conversionService;
GemfireMappingContext mappingContext;
MappingPdxSerializer pdxSerializer;
private ConversionService conversionService;
@Mock
EntityInstantiator mockInstantiator;
private EntityInstantiator mockInstantiator;
private GemfireMappingContext mappingContext;
private MappingPdxSerializer pdxSerializer;
@Mock
PdxReader mockReader;
private PdxReader mockReader;
@Mock
PdxWriter mockWriter;
private PdxWriter mockWriter;
@Before
public void setUp() {
this.conversionService = new GenericConversionService();
this.mappingContext = new GemfireMappingContext();
this.pdxSerializer = new MappingPdxSerializer(this.mappingContext, this.conversionService);
this.pdxSerializer = spy(new MappingPdxSerializer(this.mappingContext, this.conversionService));
}
private String toFullyQualifiedPropertyName(PersistentProperty<?> persistentProperty) {
@@ -138,7 +150,7 @@ public class MappingPdxSerializerUnitTests {
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("ConversionService is required");
assertThat(expected).hasMessage("ConversionService must not be null");
assertThat(expected).hasNoCause();
throw expected;
@@ -153,7 +165,7 @@ public class MappingPdxSerializerUnitTests {
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("MappingContext is required");
assertThat(expected).hasMessage("MappingContext must not be null");
assertThat(expected).hasNoCause();
throw expected;
@@ -227,7 +239,7 @@ public class MappingPdxSerializerUnitTests {
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Custom PdxSerializers are required");
assertThat(expected).hasMessage("Custom PdxSerializers must not be null");
assertThat(expected).hasNoCause();
throw expected;
@@ -236,7 +248,7 @@ public class MappingPdxSerializerUnitTests {
@Test
@SuppressWarnings("all")
public void getCustomPdxSerializerForMappedPersistentPropertyReturnsSerializerForProperty() {
public void getCustomPdxSerializerForMappedPersistentPropertyReturnsPdxSerializerForProperty() {
PdxSerializer mockNamedSerializer = mock(PdxSerializer.class);
PdxSerializer mockPropertySerializer = mock(PdxSerializer.class);
@@ -257,7 +269,7 @@ public class MappingPdxSerializerUnitTests {
@Test
@SuppressWarnings("all")
public void getCustomPdxSerializerForMappedPersistentPropertyReturnsSerializerForPropertyName() {
public void getCustomPdxSerializerForMappedPersistentPropertyReturnsPdxSerializerForPropertyName() {
PdxSerializer mockNamedSerializer = mock(PdxSerializer.class);
PdxSerializer mockTypedSerializer = mock(PdxSerializer.class);
@@ -276,7 +288,7 @@ public class MappingPdxSerializerUnitTests {
@Test
@SuppressWarnings("all")
public void getCustomPdxSerializerForMappedPersistentPropertyReturnsSerializerForPropertyType() {
public void getCustomPdxSerializerForMappedPersistentPropertyReturnsPdxSerializerForPropertyType() {
PdxSerializer mockNamedSerializer = mock(PdxSerializer.class);
PdxSerializer mockTypedSerializer = mock(PdxSerializer.class);
@@ -309,7 +321,7 @@ public class MappingPdxSerializerUnitTests {
@Test
@SuppressWarnings("deprecation")
public void getCustomSerializerForMappedType() {
public void getCustomSerializerForMappedTypeReturnsPdxSerializer() {
PdxSerializer mockPdxSerializer = mock(PdxSerializer.class);
@@ -343,6 +355,31 @@ public class MappingPdxSerializerUnitTests {
verify(mockProperty, times(1)).getOwner();
}
@Test
public void setGemfireInstantiatorsWithEntityInstantiators() {
EntityInstantiators mockEntityInstantiators = mock(EntityInstantiators.class);
this.pdxSerializer.setGemfireInstantiators(mockEntityInstantiators);
assertThat(this.pdxSerializer.getGemfireInstantiators()).isSameAs(mockEntityInstantiators);
}
@Test(expected = IllegalArgumentException.class)
public void setGemfireInstantiatorsWithNullEntityInstantiators() {
try {
this.pdxSerializer.setGemfireInstantiators((EntityInstantiators) null);
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("EntityInstantiators must not be null");
assertThat(expected).hasNoCause();
throw expected;
}
}
@Test
public void setGemfireInstantiatorsWithMappingOfClassTypesToEntityInstantiators() {
@@ -358,11 +395,11 @@ public class MappingPdxSerializerUnitTests {
public void setGemfireInstantiatorsWithNullMap() {
try {
this.pdxSerializer.setGemfireInstantiators(null);
this.pdxSerializer.setGemfireInstantiators((EntityInstantiators) null);
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("GemFire EntityInstantiators are required");
assertThat(expected).hasMessage("EntityInstantiators must not be null");
assertThat(expected).hasNoCause();
throw expected;
@@ -606,7 +643,64 @@ public class MappingPdxSerializerUnitTests {
}
@Test
public void toDataSerializesApplicationDomainObjectToPdx() {
public void fromDataWithTypeFilterAcceptsApplicationDomainTypes() {
Filter<Class<?>> packageBasedTypeFilter = new AbstractFilter<Class<?>>() {
@Override
public boolean accept(Class<?> type) {
return type != null && User.class.getPackage().equals(type.getPackage());
}
};
this.pdxSerializer.setTypeFilters(packageBasedTypeFilter);
doReturn("test").when(this.pdxSerializer).doFromData(any(Class.class), any(PdxReader.class));
assertThat(this.pdxSerializer.fromData(Account.class, this.mockReader)).isEqualTo("test");
assertThat(this.pdxSerializer.fromData(Customer.class, this.mockReader)).isEqualTo("test");
assertThat(this.pdxSerializer.fromData(Programmer.class, this.mockReader)).isEqualTo("test");
assertThat(this.pdxSerializer.fromData(User.class, this.mockReader)).isEqualTo("test");
assertThat(this.pdxSerializer.fromData(org.springframework.data.gemfire.test.model.Person.class, this.mockReader)).isNull();
assertThat(this.pdxSerializer.fromData(null, this.mockReader)).isNull();
verify(this.pdxSerializer, times(1)).doFromData(eq(Account.class), eq(this.mockReader));
verify(this.pdxSerializer, times(1)).doFromData(eq(Customer.class), eq(this.mockReader));
verify(this.pdxSerializer, times(1)).doFromData(eq(Programmer.class), eq(this.mockReader));
verify(this.pdxSerializer, times(1)).doFromData(eq(User.class), eq(this.mockReader));
verify(this.pdxSerializer, never()).doFromData(isNull(Class.class), eq(this.mockReader));
verify(this.pdxSerializer, never())
.doFromData(eq(org.springframework.data.gemfire.test.model.Person.class), eq(this.mockReader));
}
@Test
public void fromDataWithTypeFilterFiltersApplicationDomainTypeReturnsNull() {
Filter<Class<?>> applicationDomainTypeFilter = new AbstractFilter<Class<?>>() {
@Override
public boolean accept(Class<?> type) {
return type != null && !ApplicationDomainType.class.equals(type);
}
};
this.pdxSerializer.setTypeFilters(applicationDomainTypeFilter);
assertThat(this.pdxSerializer.fromData(ApplicationDomainType.class, this.mockReader)).isNull();
}
@Test
public void fromDataWithTypeFilterFiltersNullTypeReturnsNull() {
assertThat(this.pdxSerializer.fromData(null, this.mockReader)).isNull();
}
@Test
public void fromDataWithTypeFilterFiltersPivotalGemFireTypeReturnsNull() {
assertThat(this.pdxSerializer.fromData(com.gemstone.gemfire.cache.EntryEvent.class, this.mockReader)).isNull();
}
@Test
public void toDataSerializesApplicationDomainObjectToPdxBytes() {
Address address = new Address();
@@ -687,4 +781,67 @@ public class MappingPdxSerializerUnitTests {
verify(this.mockWriter, never()).markIdentityField(anyString());
}
}
@Test
public void toDataFiltersPivotalGemFireTypeReturnsFalse() {
assertThat(this.pdxSerializer.toData(mock(com.gemstone.gemfire.cache.EntryEvent.class), this.mockWriter)).isFalse();
}
@Test
public void toDataFiltersNullTypeReturnsFalse() {
assertThat(this.pdxSerializer.toData(null, this.mockWriter)).isFalse();
}
@Test
public void toDataFiltersApplicationDomainTypeReturnsFalse() {
Filter<Class<?>> applicationDomainTypeFilter = new AbstractFilter<Class<?>>() {
@Override
public boolean accept(Class<?> type) {
return type != null && !ApplicationDomainType.class.equals(type);
}
};
this.pdxSerializer.setTypeFilters(applicationDomainTypeFilter);
assertThat(this.pdxSerializer.toData(new ApplicationDomainType(), this.mockWriter)).isFalse();
}
@Test
public void toDataAcceptsApplicationDomainObjectTypeReturnsTrue() {
org.springframework.data.gemfire.test.model.Person jonDoe =
new org.springframework.data.gemfire.test.model.Person("Jon", "Doe",
null, Gender.MALE);
Filter<Class<?>> packageBasedTypeFilter = new AbstractFilter<Class<?>>() {
@Override
public boolean accept(Class<?> type) {
return type != null && User.class.getPackage().equals(type.getPackage());
}
};
this.pdxSerializer.setTypeFilters(packageBasedTypeFilter);
doReturn(true).when(this.pdxSerializer).doToData(any(), any(PdxWriter.class));
assertThat(this.pdxSerializer.toData(new Account(1L), this.mockWriter)).isTrue();
assertThat(this.pdxSerializer.toData(new Customer(1L), this.mockWriter)).isTrue();
assertThat(this.pdxSerializer.toData(new Programmer("jxblum"), this.mockWriter)).isTrue();
assertThat(this.pdxSerializer.toData(new RootUser("jxblum"), this.mockWriter)).isTrue();
assertThat(this.pdxSerializer.toData(null, this.mockWriter)).isFalse();
assertThat(this.pdxSerializer.toData(jonDoe, this.mockWriter)).isFalse();
verify(this.pdxSerializer, times(1)).doToData(isA(Account.class), eq(this.mockWriter));
verify(this.pdxSerializer, times(1)).doToData(isA(Customer.class), eq(this.mockWriter));
verify(this.pdxSerializer, times(1)).doToData(isA(Programmer.class), eq(this.mockWriter));
verify(this.pdxSerializer, times(1)).doToData(isA(RootUser.class), eq(this.mockWriter));
verify(this.pdxSerializer, never()).doToData(isNull(), eq(this.mockWriter));
verify(this.pdxSerializer, never()).doToData(isA(jonDoe.getClass()), eq(this.mockWriter));
}
private static class ApplicationDomainType { }
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2018 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.springframework.data.gemfire.util;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.Test;
/**
* Unit tests for {@link Filter}.
*
* @author John Blum
* @see org.junit.Test
* @see org.mockito.Mockito
* @see org.springframework.data.gemfire.util.Filter
* @since 1.0.0
*/
public class FilterUnitTests {
@Test
@SuppressWarnings("unchecked")
public void andIsCorrect() {
Filter mockFilterOne = mock(AbstractFilter.class);
Filter mockFilterTwo = mock(AbstractFilter.class);
Filter mockFilterThree = mock(AbstractFilter.class);
Filter mockFilterFour = mock(AbstractFilter.class);
when(mockFilterOne.accept(any())).thenReturn(false);
when(mockFilterOne.and(any(Filter.class))).thenCallRealMethod();
when(mockFilterTwo.accept(any())).thenReturn(true);
when(mockFilterTwo.and(any(Filter.class))).thenCallRealMethod();
when(mockFilterThree.accept(any())).thenReturn(false);
when(mockFilterThree.and(any(Filter.class))).thenCallRealMethod();
when(mockFilterFour.accept(any())).thenReturn(true);
when(mockFilterFour.and(any(Filter.class))).thenCallRealMethod();
assertThat(mockFilterOne.and(mockFilterTwo).accept("test")).isFalse();
assertThat(mockFilterOne.and(mockFilterThree).accept("test")).isFalse();
assertThat(mockFilterTwo.and(mockFilterThree).accept("test")).isFalse();
assertThat(mockFilterTwo.and(mockFilterFour).accept("test")).isTrue();
verify(mockFilterOne, times(2)).accept(eq("test"));
verify(mockFilterTwo, times(2)).accept(eq("test"));
verify(mockFilterThree, times(1)).accept(eq("test"));
verify(mockFilterFour, times(1)).accept(eq("test"));
}
@Test
@SuppressWarnings("unchecked")
public void negateReturnsFalseForTrue() {
Filter<Object> mockFilter = mock(AbstractFilter.class);
when(mockFilter.accept(any())).thenReturn(true);
when(mockFilter.negate()).thenCallRealMethod();
assertThat(mockFilter.negate().accept("test")).isFalse();
verify(mockFilter, times(1)).accept(eq("test"));
}
@Test
@SuppressWarnings("unchecked")
public void negateReturnsTrueForFalse() {
Filter<Object> mockFilter = mock(AbstractFilter.class);
when(mockFilter.accept(any())).thenReturn(false);
when(mockFilter.negate()).thenCallRealMethod();
assertThat(mockFilter.negate().accept("test")).isTrue();
verify(mockFilter, times(1)).accept(eq("test"));
}
@Test
@SuppressWarnings("unchecked")
public void orIsCorrect() {
Filter mockFilterOne = mock(AbstractFilter.class);
Filter mockFilterTwo = mock(AbstractFilter.class);
Filter mockFilterThree = mock(AbstractFilter.class);
Filter mockFilterFour = mock(AbstractFilter.class);
when(mockFilterOne.accept(any())).thenReturn(false);
when(mockFilterOne.or(any(Filter.class))).thenCallRealMethod();
when(mockFilterTwo.accept(any())).thenReturn(true);
when(mockFilterTwo.or(any(Filter.class))).thenCallRealMethod();
when(mockFilterThree.accept(any())).thenReturn(false);
when(mockFilterThree.or(any(Filter.class))).thenCallRealMethod();
when(mockFilterFour.accept(any())).thenReturn(true);
when(mockFilterFour.or(any(Filter.class))).thenCallRealMethod();
assertThat(mockFilterOne.or(mockFilterTwo).accept("test")).isTrue();
assertThat(mockFilterOne.or(mockFilterThree).accept("test")).isFalse();
assertThat(mockFilterTwo.or(mockFilterThree).accept("test")).isTrue();
assertThat(mockFilterTwo.or(mockFilterFour).accept("test")).isTrue();
verify(mockFilterOne, times(2)).accept(eq("test"));
verify(mockFilterTwo, times(3)).accept(eq("test"));
verify(mockFilterThree, times(1)).accept(eq("test"));
verify(mockFilterFour, never()).accept(eq("test"));
}
}