diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 02c6680c..efc74e9d 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -4,7 +4,9 @@ [[mapping.entities]] == Entity Mapping -_Spring Data Geode_ provides support to map entities that will be stored in a Region in the Geode In-Memory Data Grid. +_Spring Data for Apache Geode_ 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 Geode Region @@ -124,29 +126,256 @@ either as a local cache transaction or a global transaction. [[mapping.pdx-serializer]] == Mapping PDX Serializer -_Spring Data Geode_ provides a custom +_Spring Data for Apache Geode_ 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). +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://geode.apache.org/releases/latest/javadoc/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 Apache Geode's own +http://geode.apache.org/releases/latest/javadoc/org/apache/geode/pdx/ReflectionBasedAutoSerializer.html[`ReflectionBasedAutoSerializer`]. + +While Apache Geode'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 Apache Geode'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://geode.apache.org/releases/latest/javadoc/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 Apache Geode'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 Apache Geode'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 Apache Geode'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 4ee71714..1a08f850 100644 --- a/src/main/java/org/springframework/data/gemfire/mapping/MappingPdxSerializer.java +++ b/src/main/java/org/springframework/data/gemfire/mapping/MappingPdxSerializer.java @@ -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 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 customPdxSerializers; + + private Predicate> 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 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> 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> 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,7 +543,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) @@ -471,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 = @@ -528,12 +637,67 @@ 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 + */ + @Nullable + Class resolveType(@Nullable Object obj) { + return obj != null ? obj.getClass() : null; + } + + public enum TypeFilters implements Filter> { + + 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(); + } + }, + } } 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..02c03d91 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/util/Filter.java @@ -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 {@link Class type} of {@link Object objects} being filtered. + * @see java.util.function.Predicate + * @since 1.0.0 + */ +public interface Filter extends Predicate { + + /** + * 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); + } +} 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 2cf60f34..c95f5ddb 100644 --- a/src/test/java/org/springframework/data/gemfire/mapping/MappingPdxSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/gemfire/mapping/MappingPdxSerializerUnitTests.java @@ -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 { } + } 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..943e356d --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/util/FilterUnitTests.java @@ -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 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 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 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")); + } +}