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 545ac78684
commit cd834f539a
5 changed files with 752 additions and 55 deletions

View File

@@ -4,7 +4,9 @@
[[mapping.entities]]
== Entity Mapping
_Spring Data for Pivotal GemFire_ provides support to map entities that will be stored in a Region in the Pivotal GemFire In-Memory Data Grid.
_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 Pivotal GemFire Region
@@ -125,28 +127,255 @@ either as a local cache transaction or a global transaction.
== Mapping PDX Serializer
_Spring Data for Pivotal GemFire_ provides a custom
http://geode.apache.org/releases/latest/javadoc/org/apache/geode/pdx/PdxSerializer.html[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 default constructor, a singly declared constructor or an explicitly annotated constructor annotated with
the `@PersistenceConstructor` annotation).
http://gemfire-95-javadocs.docs.pivotal.io/org/apache/geode/pdx/PdxSerializer.html[PdxSerializer] implementation
that uses the mapping information to customize entity serialization.
To provide values for constructor parameters it will read fields with name of the constructor parameters from
the supplied http://geode.apache.org/releases/latest/javadoc/org/apache/geode/pdx/PdxReader.html[PdxReader].
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).
.Using @Value on entity constructor parameters
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) {
// …
}
}
----
====
An entity class annotated in this way will have the field `foo` read from the `PdxReader` and passed to the constructor
parameter value for `firstname`. The value for `lastname` will be the _Spring_ bean with the 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

@@ -17,6 +17,8 @@ package org.springframework.data.gemfire.mapping;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import org.apache.geode.pdx.PdxReader;
import org.apache.geode.pdx.PdxSerializer;
@@ -30,6 +32,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.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
@@ -67,42 +70,62 @@ import org.springframework.util.ObjectUtils;
*/
public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAware {
private final ConversionService conversionService;
private EntityInstantiators entityInstantiators;
private final GemfireMappingContext mappingContext;
private final Logger logger = LoggerFactory.getLogger(getClass());
private Map<?, PdxSerializer> customPdxSerializers;
// TODO: decide what to do with this; SpELContext is not used
private SpELContext context;
protected static final String COM_GEMSTONE_GEMFIRE_PACKAGE_NAME = "com.gemstone.gemfire";
protected static final String ORG_APACHE_GEODE_PACKAGE_NAME = "org.apache.geode";
/**
* 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(@Nullable 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(@Nullable 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
@@ -161,6 +184,23 @@ 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 Predicate<Class<?>> typeFilters = TypeFilters.EXCLUDE_NULL_TYPES
.and(TypeFilters.EXCLUDE_COM_GEMSTONE_GEMFIRE_TYPES)
.and(TypeFilters.EXCLUDE_ORG_APACHE_GEODE_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}.
@@ -187,14 +227,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);
}
/**
@@ -205,7 +245,7 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = new SpELContext(this.context, applicationContext);
this.spelContext = new SpELContext(this.spelContext, applicationContext);
}
/**
@@ -232,7 +272,7 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
*/
public void setCustomPdxSerializers(@NonNull Map<?, PdxSerializer> customPdxSerializers) {
Assert.notNull(customPdxSerializers, "Custom PdxSerializers are required");
Assert.notNull(customPdxSerializers, "Custom PdxSerializers must not be null");
this.customPdxSerializers = customPdxSerializers;
}
@@ -324,7 +364,7 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
*/
public void setGemfireInstantiators(@NonNull EntityInstantiators entityInstantiators) {
Assert.notNull(entityInstantiators, "EntityInstantiators are required");
Assert.notNull(entityInstantiators, "EntityInstantiators must not be null");
this.entityInstantiators = entityInstantiators;
}
@@ -409,12 +449,53 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
* @see org.springframework.data.gemfire.mapping.GemfirePersistentEntity
* @see #getMappingContext()
*/
protected GemfirePersistentEntity<?> getPersistentEntity(Class<?> entityType) {
protected GemfirePersistentEntity<?> getPersistentEntity(@NonNull Class<?> entityType) {
return getMappingContext().getPersistentEntity(entityType);
}
/**
* Sets the {@link Predicate 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 Predicate type filters},
* this set operation combines the given {@link Predicate type filters} with
* the exiting {@link Predicate type filters} joined by {@literal and}.
*
* @param typeFilters {@link Predicate type filters} used to to filter {@link Class type} serializable
* by this {@link MappingPdxSerializer PDX serializer}.
* @see java.util.function.Predicate
*/
public void setTypeFilters(@Nullable Predicate<Class<?>> typeFilters) {
this.typeFilters = typeFilters != null ? this.typeFilters.and(typeFilters) : this.typeFilters;
}
/**
* Returns the {@link Predicate type filters} used to filter {@link Class types} serializable
* by this {@link MappingPdxSerializer PdxSerializer}.
*
* @return the resolved {@link Predicate type filter}.
* @see java.util.function.Predicate
*/
protected Predicate<Class<?>> getTypeFilters() {
return this.typeFilters;
}
@Override
public Object fromData(Class<?> type, PdxReader reader) {
return getTypeFilters().test(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 org.apache.geode.pdx.PdxReader
* @see java.lang.Object
* @see java.lang.Class
*/
Object doFromData(Class<?> type, PdxReader reader) {
GemfirePersistentEntity<?> entity = getPersistentEntity(type);
@@ -453,8 +534,8 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
catch (Exception cause) {
throw new MappingException(String.format(
"While setting value [%1$s] of property [%2$s] for entity of type [%3$s] from PDX%4$s",
value, persistentProperty.getName(), type, (customPdxSerializer != null ?
String.format(" using custom PdxSerializer [%1$s]", customPdxSerializer) : "")), cause);
value, persistentProperty.getName(), type, (customPdxSerializer != null ?
String.format(" using custom PdxSerializer [%1$s]", customPdxSerializer) : "")), cause);
}
}
});
@@ -462,6 +543,21 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
return propertyAccessor.getBean();
}
/**
* 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)
@@ -470,12 +566,26 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
}
@Override
@SuppressWarnings("unchecked")
public boolean toData(Object value, PdxWriter writer) {
return getTypeFilters().test(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 org.apache.geode.pdx.PdxWriter
* @see java.lang.Object
*/
@SuppressWarnings("unchecked")
boolean doToData(Object value, PdxWriter writer) {
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) {
PersistentPropertyAccessor propertyAccessor =
@@ -527,11 +637,67 @@ public class MappingPdxSerializer implements PdxSerializer, ApplicationContextAw
return true;
}
return false;
}
/**
* 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
*/
@Nullable
Class<?> resolveType(@Nullable Object obj) {
return obj != null ? obj.getClass() : null;
}
public enum TypeFilters implements Filter<Class<?>> {
EXCLUDE_COM_GEMSTONE_GEMFIRE_TYPES {
@Override
public boolean accept(@Nullable Class<?> obj) {
return Optional.ofNullable(obj)
.filter(type -> !type.getPackage().getName().startsWith(COM_GEMSTONE_GEMFIRE_PACKAGE_NAME))
.isPresent();
}
},
EXCLUDE_NULL_TYPES {
@Override
public boolean accept(@Nullable Class<?> obj) {
return obj != null;
}
},
EXCLUDE_ORG_APACHE_GEODE_TYPES {
@Override
public boolean accept(Class<?> obj) {
return Optional.ofNullable(obj)
.filter(type -> !type.getPackage().getName().startsWith(ORG_APACHE_GEODE_PACKAGE_NAME))
.isPresent();
}
},
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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 java.util.function.Predicate;
import org.springframework.lang.Nullable;
/**
* 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.
* @see java.util.function.Predicate
* @since 1.0.0
*/
public interface Filter<T> extends Predicate<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(@Nullable T obj);
/**
* Tests whether the given {@link Object} matches the criteria defined by this {@link Filter}.
*
* @param obj {@link Object} to test.
* @return a boolean value indicating whether the given {@link Object} matches the criteria
* defined by this {@link Filter}.
* @see #accept(Object)
*/
@Override
default boolean test(T obj) {
return accept(obj);
}
}

View File

@@ -17,13 +17,17 @@
package org.springframework.data.gemfire.mapping;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
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;
@@ -48,8 +52,14 @@ 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.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
@@ -79,13 +89,13 @@ public class MappingPdxSerializerUnitTests {
ConversionService conversionService;
@Mock
EntityInstantiator mockInstantiator;
GemfireMappingContext mappingContext;
MappingPdxSerializer pdxSerializer;
@Mock
EntityInstantiator mockInstantiator;
@Mock
PdxReader mockReader;
@@ -97,7 +107,7 @@ public class MappingPdxSerializerUnitTests {
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 +148,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 +163,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 +237,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 +246,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 +267,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 +286,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 +319,7 @@ public class MappingPdxSerializerUnitTests {
@Test
@SuppressWarnings("deprecation")
public void getCustomSerializerForMappedType() {
public void getCustomSerializerForMappedTypeReturnsPdxSerializer() {
PdxSerializer mockPdxSerializer = mock(PdxSerializer.class);
@@ -361,7 +371,7 @@ public class MappingPdxSerializerUnitTests {
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("EntityInstantiators are required");
assertThat(expected).hasMessage("EntityInstantiators must not be null");
assertThat(expected).hasNoCause();
throw expected;
@@ -627,7 +637,48 @@ public class MappingPdxSerializerUnitTests {
}
@Test
public void toDataSerializesApplicationDomainObjectToPdx() {
public void fromDataWithTypeFilterAcceptsApplicationDomainTypes() {
this.pdxSerializer.setTypeFilters(type -> User.class.getPackage().equals(type.getPackage()));
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(), eq(this.mockReader));
verify(this.pdxSerializer, never())
.doFromData(eq(org.springframework.data.gemfire.test.model.Person.class), eq(this.mockReader));
}
@Test
public void fromDataWithTypeFilterFiltersApacheGeodeTypeReturnsNull() {
assertThat(this.pdxSerializer.fromData(org.apache.geode.cache.EntryEvent.class, this.mockReader)).isNull();
}
@Test
public void fromDataWithTypeFilterFiltersApplicationDomainTypeReturnsNull() {
this.pdxSerializer.setTypeFilters(type -> !ApplicationDomainType.class.equals(type));
assertThat(this.pdxSerializer.fromData(ApplicationDomainType.class, this.mockReader)).isNull();
}
@Test
public void fromDataWithTypeFilterFiltersNullTypeReturnsNull() {
assertThat(this.pdxSerializer.fromData(null, this.mockReader)).isNull();
}
@Test
public void toDataSerializesApplicationDomainObjectToPdxBytes() {
Address address = new Address();
@@ -708,4 +759,51 @@ public class MappingPdxSerializerUnitTests {
verify(this.mockWriter, never()).markIdentityField(anyString());
}
}
@Test
public void toDataFiltersNullTypeReturnsFalse() {
assertThat(this.pdxSerializer.toData(null, this.mockWriter)).isFalse();
}
@Test
public void toDataFiltersApacheGeodeTypeReturnsFalse() {
assertThat(this.pdxSerializer.toData(mock(org.apache.geode.cache.EntryEvent.class), this.mockWriter)).isFalse();
}
@Test
public void toDataFiltersApplicationDomainTypeReturnsFalse() {
this.pdxSerializer.setTypeFilters(type -> !ApplicationDomainType.class.equals(type));
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);
this.pdxSerializer.setTypeFilters(type -> User.class.getPackage().equals(type.getPackage()));
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,149 @@
/*
* 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.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.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(Filter.class);
Filter mockFilterTwo = mock(Filter.class);
Filter mockFilterThree = mock(Filter.class);
Filter mockFilterFour = mock(Filter.class);
when(mockFilterOne.accept(any())).thenReturn(false);
when(mockFilterOne.and(any())).thenCallRealMethod();
when(mockFilterOne.test(any())).thenCallRealMethod();
when(mockFilterTwo.accept(any())).thenReturn(true);
when(mockFilterTwo.and(any())).thenCallRealMethod();
when(mockFilterTwo.test(any())).thenCallRealMethod();
when(mockFilterThree.accept(any())).thenReturn(false);
when(mockFilterThree.and(any())).thenCallRealMethod();
when(mockFilterThree.test(any())).thenCallRealMethod();
when(mockFilterFour.accept(any())).thenReturn(true);
when(mockFilterFour.and(any())).thenCallRealMethod();
when(mockFilterFour.test(any())).thenCallRealMethod();
assertThat(mockFilterOne.and(mockFilterTwo).test("test")).isFalse();
assertThat(mockFilterOne.and(mockFilterThree).test("test")).isFalse();
assertThat(mockFilterTwo.and(mockFilterThree).test("test")).isFalse();
assertThat(mockFilterTwo.and(mockFilterFour).test("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 orIsCorrect() {
Filter mockFilterOne = mock(Filter.class);
Filter mockFilterTwo = mock(Filter.class);
Filter mockFilterThree = mock(Filter.class);
Filter mockFilterFour = mock(Filter.class);
when(mockFilterOne.accept(any())).thenReturn(false);
when(mockFilterOne.or(any())).thenCallRealMethod();
when(mockFilterOne.test(any())).thenCallRealMethod();
when(mockFilterTwo.accept(any())).thenReturn(true);
when(mockFilterTwo.or(any())).thenCallRealMethod();
when(mockFilterTwo.test(any())).thenCallRealMethod();
when(mockFilterThree.accept(any())).thenReturn(false);
when(mockFilterThree.or(any())).thenCallRealMethod();
when(mockFilterThree.test(any())).thenCallRealMethod();
when(mockFilterFour.accept(any())).thenReturn(true);
when(mockFilterFour.or(any())).thenCallRealMethod();
when(mockFilterFour.test(any())).thenCallRealMethod();
assertThat(mockFilterOne.or(mockFilterTwo).test("test")).isTrue();
assertThat(mockFilterOne.or(mockFilterThree).test("test")).isFalse();
assertThat(mockFilterTwo.or(mockFilterThree).test("test")).isTrue();
assertThat(mockFilterTwo.or(mockFilterFour).test("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"));
}
@Test
@SuppressWarnings("unchecked")
public void negateReturnsFalseForTrue() {
Filter<Object> mockFilter = mock(Filter.class);
when(mockFilter.accept(any())).thenReturn(true);
when(mockFilter.negate()).thenCallRealMethod();
when(mockFilter.test(any())).thenCallRealMethod();
assertThat(mockFilter.negate().test("test")).isFalse();
verify(mockFilter, times(1)).accept(eq("test"));
}
@Test
@SuppressWarnings("unchecked")
public void predicateTestCallsFilterAcceptReturnsTrue() {
Filter<Object> mockFilter = mock(Filter.class);
when(mockFilter.accept(any())).thenReturn(true);
when(mockFilter.test(any())).thenCallRealMethod();
assertThat(mockFilter.test("test")).isTrue();
verify(mockFilter, times(1)).accept(eq("test"));
}
@Test
@SuppressWarnings("unchecked")
public void predicateTestCallsFilterAcceptReturnsFalse() {
Filter<Object> mockFilter = mock(Filter.class);
when(mockFilter.accept(any())).thenReturn(false);
when(mockFilter.test(any())).thenCallRealMethod();
assertThat(mockFilter.test("test")).isFalse();
verify(mockFilter, times(1)).accept(eq("test"));
}
}