diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 421d54db..1fef7e58 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -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 { [[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 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 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> 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. diff --git a/src/main/java/org/springframework/data/gemfire/mapping/MappingPdxSerializer.java b/src/main/java/org/springframework/data/gemfire/mapping/MappingPdxSerializer.java index 3c9adb57..d14a5358 100644 --- a/src/main/java/org/springframework/data/gemfire/mapping/MappingPdxSerializer.java +++ b/src/main/java/org/springframework/data/gemfire/mapping/MappingPdxSerializer.java @@ -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 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 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 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, 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> 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> 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> { + + public static final Filter> 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> { + + public static final Filter> EXCLUDE_NULL_TYPES = new ExcludeNullTypesFilter(); + + @Override + public boolean accept(Class type) { + return type != null; + } + } } diff --git a/src/main/java/org/springframework/data/gemfire/util/AbstractFilter.java b/src/main/java/org/springframework/data/gemfire/util/AbstractFilter.java new file mode 100644 index 00000000..3aa798ea --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/util/AbstractFilter.java @@ -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 implements Filter { + + /** + * 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 and(final Filter filter) { + + return new AbstractFilter() { + + @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 negate() { + + return new AbstractFilter() { + + @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 or(final Filter filter) { + + return new AbstractFilter() { + + @Override + public boolean accept(T obj) { + return AbstractFilter.this.accept(obj) || filter.accept(obj); + } + }; + } +} diff --git a/src/main/java/org/springframework/data/gemfire/util/Filter.java b/src/main/java/org/springframework/data/gemfire/util/Filter.java new file mode 100644 index 00000000..650f59f0 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/util/Filter.java @@ -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 {@link Class type} of {@link Object objects} being filtered. + * @since 1.0.0 + */ +public interface Filter { + + /** + * 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 and(Filter 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 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 or(Filter filter); + +} diff --git a/src/test/java/org/springframework/data/gemfire/mapping/MappingPdxSerializerUnitTests.java b/src/test/java/org/springframework/data/gemfire/mapping/MappingPdxSerializerUnitTests.java index bad4fe3b..3faf3823 100644 --- a/src/test/java/org/springframework/data/gemfire/mapping/MappingPdxSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/gemfire/mapping/MappingPdxSerializerUnitTests.java @@ -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> packageBasedTypeFilter = new AbstractFilter>() { + + @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> applicationDomainTypeFilter = new AbstractFilter>() { + + @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> applicationDomainTypeFilter = new AbstractFilter>() { + + @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> packageBasedTypeFilter = new AbstractFilter>() { + + @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 { } + } diff --git a/src/test/java/org/springframework/data/gemfire/util/FilterUnitTests.java b/src/test/java/org/springframework/data/gemfire/util/FilterUnitTests.java new file mode 100644 index 00000000..98201190 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/util/FilterUnitTests.java @@ -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 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 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")); + } +}