diff --git a/src/main/asciidoc/reference/bootstrap-annotations-quickstart.adoc b/src/main/asciidoc/reference/bootstrap-annotations-quickstart.adoc index 365dad3b..1e5c0714 100644 --- a/src/main/asciidoc/reference/bootstrap-annotations-quickstart.adoc +++ b/src/main/asciidoc/reference/bootstrap-annotations-quickstart.adoc @@ -491,6 +491,30 @@ See <> for more details. See <> for more details. +[[bootstap-annotations-quickstart-cluster-defined-regions]] +== Configure Client Regions from Cluster-defined Regions + +Alternatively, you can define client [*PROXY] Regions from Regions already defined in the cluster +using `@EnableClusterDefinedRegions`, as follows: + +[source,java] +---- +@SpringBootApplication +@ClientCacheApplication +@EnableClusterDefinedRegions +@EnableGemfireRepositories +public class ClientApplication { + + public static void main(String[] args) { + SpringApplication.run(ClientApplication.class, args); + } + + ... +} +---- + +See <> for more details. + [[bootstap-annotations-quickstart-functions]] == Configure Functions diff --git a/src/main/asciidoc/reference/bootstrap-annotations.adoc b/src/main/asciidoc/reference/bootstrap-annotations.adoc index 1ce1c84f..0b53109d 100644 --- a/src/main/asciidoc/reference/bootstrap-annotations.adoc +++ b/src/main/asciidoc/reference/bootstrap-annotations.adoc @@ -1,5 +1,5 @@ [[bootstrap-annotation-config]] -= Bootstrapping {data-store-name} using Spring Annotations += Bootstrapping {data-store-name} with the Spring Container using Annotations {sdg-name} ({sdg-acronym}) 2.0 introduces a new annotation-based configuration model to configure and bootstrap {data-store-name} using the Spring container. @@ -66,7 +66,7 @@ You can find all the new {sdg-acronym} Java `Annotations` in the `org.springfram package. [[bootstrap-annotation-config-geode-applications]] -== Bootstrapping {data-store-name} Applications with Spring +== Configuring {data-store-name} Applications with Spring Like all Spring Boot applications that begin by annotating the application class with `@SpringBootApplication`, a Spring Boot application can easily become a {data-store-name} cache application by declaring any one of three @@ -83,8 +83,8 @@ that can be created with {data-store-name}: a client cache or a peer cache. You can configure a Spring Boot application as a {data-store-name} cache client with an instance of `ClientCache`, which can communicate with an existing cluster of {data-store-name} servers used to manage the application's data. -The client-server topology is the most common system architecture employed when using {data-store-name} and you -can make your Spring Boot application a cache client, with a `ClientCache` instance, simply by annotating it with +The client-server topology is the most common system architecture employed when using {data-store-name} and you can +make your Spring Boot application a cache client, with a `ClientCache` instance, simply by annotating it with `@ClientCacheApplication`. Alternatively, a Spring Boot application may be a peer member of a {data-store-name} cluster. That is, the application @@ -94,15 +94,15 @@ an "embedded", peer `Cache` instance when you annotate your application class wi By extension, a peer cache application may also serve as a `CacheServer` too, allowing cache clients to connect and perform data access operations on the server. This is accomplished by annotating the application class with `@CacheServerApplication` in place of `@PeerCacheApplication`, which creates a peer `Cache` instance along with -the `CacheServer`. +the `CacheServer` that allows cache clients to connect. NOTE: A {data-store-name} server is not necessarily a cache server by default. That is, a server is not necessarily set up to serve cache clients just because it is a server. A {data-store-name} server can be a peer member (data node) of the cluster managing data without serving any clients while other peer members in the cluster are indeed set up to serve clients in addition to managing data. It is also possible to set up certain peer members in the cluster as non-data nodes, called {x-data-store-docs}/developing/region_options/data_hosts_and_accessors.html[data accessors], -which do not store data, but act as a proxy to service clients as `CacheServers`. This is beyond the scope -of this document. +which do not store data, but act as a proxy to service clients as `CacheServers`. Many different topologies +and cluster arrangements are supported by {data-store-name}, but are beyond the scope of this document. By way of example, if you want to create a Spring Boot cache client application, start with the following: @@ -115,7 +115,7 @@ class ClientApplication { .. } ---- Or, if you want to create a Spring Boot application with an embedded peer `Cache` instance, where your application -will be a server and peer member of a cluster, or distributed system, formed by {data-store-name}, +will be a server and peer member of a cluster (distributed system) formed by {data-store-name}, start with the following: .Spring-based {data-store-name} embedded peer `Cache` application @@ -127,7 +127,7 @@ class ServerApplication { .. } ---- Alternatively, you can use the `@CacheServerApplication` annotation in place of `@PeerCacheApplication` to create -both an embedded peer `Cache` instance and a `CacheServer` running on `localhost`, listening on the default +both an embedded peer `Cache` instance along with a `CacheServer` running on `localhost`, listening on the default cache server port, `40404`, as follows: .Spring-based {data-store-name} embedded peer `Cache` application with `CacheServer` @@ -147,25 +147,25 @@ The most common and recommended approach is to use {data-store-name} Locators. NOTE: A cache client can connect to one or more Locators in the {data-store-name} cluster instead of directly to a `CacheServer`. The advantage of using Locators over direct `CacheServer` connections is that Locators provide metadata about the cluster to which the client is connected. This metadata includes information such as which servers contain -the data of interest or which servers have the least amount of load. A client `Pool` in conjuction with a Locator +the data of interest or which servers have the least amount of load. A client `Pool` in conjunction with a Locator also provides fail-over capabilities in case a `CacheServer` crashes. By enabling the `PARTITION` Region (PR) single-hop feature in the client `Pool`, the client is routed directly to the server containing the data requested and needed by the client. -NOTE: Locators are also peer members in a cluster. Locators actually constitute what makes up a cluster of {data-store-name} -nodes. That is, all nodes connected by a Locator are peers in the cluster, and new members use Locators to join a cluster -and find other members. +NOTE: Locators are also peer members in a cluster. Locators actually constitute what makes up a cluster of +{data-store-name} nodes. That is, all nodes connected by a Locator are peers in the cluster, and new members +use Locators to join a cluster and find other members. By default, {data-store-name} sets up a "DEFAULT" `Pool` connected to a `CacheServer` running on `localhost`, listening on port `40404` when a `ClientCache` instance is created. A `CacheServer` listens on port `40404`, -accepting connections from all system NICs. You do not need to do anything special to use the client-server topology. -Simply annotate your server-side Spring Boot application with `@CacheServerApplication` and your client-side Spring Boot -application with `@ClientCacheApplication`, and you are ready to go. +accepting connections on all system NICs. You do not need to do anything special to use the client-server topology. +Simply annotate your server-side Spring Boot application with `@CacheServerApplication` and your client-side +Spring Boot application with `@ClientCacheApplication`, and you are ready to go. If you prefer, you can even start your servers with Gfsh's `start server` command. Your Spring Boot `@ClientCacheApplication` -can still connect to the server regardless of how it was started. However, you may prefer to configure and start -your servers by using the {sdg-name} approach since a properly annotated Spring Boot application class is far more -intuitive and easier to debug. +can still connect to the server regardless of how it was started. However, you may prefer to configure and start your +servers by using the {sdg-name} approach since a properly annotated Spring Boot application class is far more intuitive +and easier to debug. As an application developer, you will no doubt want to customize the "DEFAULT" `Pool` set up by {data-store-name} to possibly connect to one or more Locators, as the following example demonstrates: @@ -691,7 +691,7 @@ the annotation attributes or corresponding configuration properties to adjust th Follow the earlier links for more details on HTTP support and the services provided. [[bootstrap-annotation-config-embedded-services-memcached]] -=== Configuring the embedded Memcached Server (Gemcached) +=== Configuring the Embedded Memcached Server (Gemcached) {data-store-name} also implements the Memcached protocol with the ability to service Memcached clients. That is, Memcached clients can connect to a {data-store-name} cluster and perform Memcached operations as if @@ -883,51 +883,6 @@ or associated configuration properties. See the https://docs.spring.io/spring-data/gemfire/docs/current/api/org/springframework/data/gemfire/config/annotation/EnablePdx.html[`@EnablePdx` annotation Javadoc] for more details. -[[bootstrap-annotation-config-ssl]] -== Configuring SSL - -Equally important to serializing data to be transferred over the wire is securing the data while in transit. -Of course, the common way to accomplish this in Java is by using the Secure Sockets Extension (SSE) -and Transport Layer Security (TLS). - -To enable SSL, annotate your application class with `@EnableSsl`, as follows: - -.Spring `ClientCache` application with SSL enabled -[source, java] ----- -@SpringBootApplication -@ClientCacheApplication -@EnableSsl -public class ClientApplication { .. } ----- - -Then you need to set the necessary SSL configuration attributes or properties: keystores, usernames/passwords, and so on. - -You can individually configure different {data-store-name} components (`GATEWAY`, `HTTP`, `JMX`, `LOCATOR`, and `SERVER`) -with SSL, or you can collectively configure them to use SSL by using the `CLUSTER` enumerated value. - -You can specify which {data-store-name} components the SSL configuration settings should applied by using -the nested `@EnableSsl` annotation, `components` attribute with enumerated values from the `Component` enum, -as follows: - -.Spring `ClientCache` application with SSL enabled by component -[source, java] ----- -@SpringBootApplication -@ClientCacheApplication -@EnableSsl(components = { GATEWAY, LOCATOR, SERVER }) -public class ClientApplication { .. } ----- - -In addition, you can also specify component-level SSL configuration (`ciphers`, `protocols` and `keystore`/`truststore` -information) by using the corresponding annotation attribute or associated configuration properties. - -See the https://docs.spring.io/spring-data/gemfire/docs/current/api/org/springframework/data/gemfire/config/annotation/EnableSsl.html[`@EnableSsl` annotation Javadoc] -for more details. - -More details on {data-store-name} SSL support can be found -{x-data-store-docs}/managing/security/ssl_overview.html[here]. - [[bootstrap-annotation-config-gemfire-properties]] == Configuring {data-store-name} Properties @@ -971,9 +926,9 @@ More details on {data-store-name} properties can be found == Configuring Regions So far, outside of PDX, our discussion has centered around configuring {data-store-name}'s more administrative functions: -creating a cache instance, starting embedded services, enabling logging, statistics, SSL, and using `gemfire.properties` -to affect low-level configuration and behavior. While all these configuration options are important, none of them -relate directly to your application. In other words, we still need some place to store our application data +creating a cache instance, starting embedded services, enabling logging and statistics, configuring PDX, and using +`gemfire.properties` to affect low-level configuration and behavior. While all these configuration options are important, +none of them relate directly to your application. In other words, we still need some place to store our application data and make it generally available and accessible. {data-store-name} organizes data in a cache into {x-data-store-docs}/basic_config/data_regions/chapter_overview.html[Regions]. @@ -981,9 +936,9 @@ You can think of a Region as a table in a relational database. Generally, a Regi which makes it more conducive for building effective indexes and writing queries. We cover indexing <>. -Previously, {sdg-name} users needed to explicitly define and declare the Regions used in their applications to store data +Previously, {sdg-name} users needed to explicitly define and declare the Regions used by their applications to store data by writing very verbose Spring configuration metadata, whether using {sdg-acronym}'s `FactoryBeans` from the API -in Spring's {spring-framework-docs}/core.html#beans-java[Java-based container configuration] +with Spring's {spring-framework-docs}/core.html#beans-java[Java-based container configuration] or using <>. The following example demonstrates how to configure a Region bean in Java: @@ -1011,7 +966,7 @@ class GemFireConfiguration { } ---- -The following example shows how to configure the same Region bean in XML: +The following example demonstrates how to configure the same Region bean in XML: .Example Region bean definition using {sdg-acronym}'s XML Namespace [source, xml] @@ -1022,8 +977,10 @@ The following example shows how to configure the same Region bean in XML: ---- While neither Java nor XML configuration is all that difficult to specify, either one can be cumbersome, especially if -an application requires a large number of Regions. Many relational database-based applications can literally -have hundreds or even thousands of tables. +an application requires a large number of Regions. Many relational database-based applications can have hundreds +or even thousands of tables. + +Defining and declaring all these Regions by hand would be cumbersome and error prone. Well, now there is a better way. Now you can define and configure Regions based on their application domain objects (entities) themselves. No longer do you need to explicitly define `Region` bean definitions in Spring configuration metadata, unless you require @@ -1037,7 +994,7 @@ NOTE: Most Spring Data application developers should already be familiar with th and {sdg-name}'s <>, which has been specifically customized to optimize data access operations for {data-store-name}. -First, an application developer starts by defining the application's domain objects, as follows: +First, an application developer starts by defining the application's domain objects (entities), as follows: .Application domain object type modeling a Book [source, java] @@ -1075,7 +1032,7 @@ operations (CRUD) along with support for simple queries (such as `findById(..)`) more sophisticated queries by declaring query methods on the repository interface (for example, `List findByAuthor(Author author);`). -Under the hood, {sdg-name} provides an implementation of your applications repository interfaces when +Under the hood, {sdg-name} provides an implementation of your application's repository interfaces when the Spring container is bootstrapped. {sdg-acronym} even implements the query methods you define so long as you follow the <>. @@ -1104,6 +1061,9 @@ TIP: Creating Regions from entity classes is most useful when using Spring Data {sdg-name}'s Repository support is enabled with the `@EnableGemfireRepositories` annotation, as shown in the preceding example. +NOTE: Currently, only entity classes explicitly annotated with `@Region` are picked up by the scan +and will have Regions created. If an entity class is not explicitly mapped with `@Region` no Region will be created. + By default, the `@EnableEntityDefinedRegions` annotation scans for entity classes recursively, starting from the package of the configuration class on which the `@EnableEntityDefinedRegions` annotation is declared. @@ -1148,13 +1108,13 @@ See {x-data-store-docs}/developing/region_options/storage_distribution_options.h in the {data-store-name} User Guide for more details. When you annotate your application domain object types with the generic `@Region` mapping annotation, {sdg-name} decides -which type of Region to create. {sdg-acronym}'s default strategy takes the cache type into consideration when determining -the type of Region to create. +which type of Region to create. {sdg-acronym}'s default strategy takes the cache type into consideration when +determining the type of Region to create. For example, if you declare the application as a `ClientCache` by using the `@ClientCacheApplication` annotation, -{sdg-acronym} creates a client `PROXY` `Region`. Alternatively, if you declare the application as a peer `Cache` -by using either the `@PeerCacheApplication` or `@CacheServerApplication` annotations, {sdg-acronym} creates -a server `PARTITION` `Region`. +{sdg-acronym} creates a client `PROXY` `Region` by default. Alternatively, if you declare the application as a +peer `Cache` by using either the `@PeerCacheApplication` or `@CacheServerApplication` annotations, +{sdg-acronym} creates a server `PARTITION` `Region` by default. Of course, you can always override the default when necessary. To override the default applied by {sdg-name}, four new Region mapping annotations have been introduced: @@ -1165,7 +1125,7 @@ four new Region mapping annotations have been introduced: * `@ReplicateRegion` The `@ClientRegion` mapping annotation is specific to client applications. All of the other Region mapping annotations -listed above can be used only in server applications that have an embedded peer `Cache`. +listed above can only be used in server applications that have an embedded peer `Cache`. It is sometimes necessary for client applications to create and use local-only Regions, perhaps to aggregate data from other Regions in order to analyze the data locally and carry out some function performed by the application @@ -1177,8 +1137,8 @@ accomplished with Idle-Timeout (TTI) and Time-To-Live (TTL) expiration policies NOTE: Region-level Idle-Timeout (TTI) and Time-To-Live (TTL) expiration policies are independent of and different from entry-level TTI and TTL expiration policies. -In any case, if you want to create a local-only client Region where the data is not going to be distributed to -a corresponding Region with the same name on the server, you can declare the `@ClientRegion` mapping annotation +In any case, if you want to create a local-only client Region where the data is not going to be distributed back to +a corresponding Region on the server with the same name, you can declare the `@ClientRegion` mapping annotation and set the `shortcut` attribute to `ClientRegionShortcut.LOCAL`, as follows: .Spring `ClientCache` application with a local-only, client Region @@ -1190,11 +1150,185 @@ class ClientLocalEntityType { .. } All Region type-specific annotations provide additional attributes that are both common across Region types as well as specific to only that type of Region. For example, the `collocatedWith` and `redundantCopies` attributes -in the `PartitionRegion` annotation apply to `PARTITION` Regions only. +in the `PartitionRegion` annotation apply to server-side, `PARTITION` Regions only. More details on {data-store-name} Region types can be found {x-data-store-docs}/developing/region_options/region_types.html[here]. +[[bootstrap-annotation-config-region-cluster-defined]] +=== Configured Cluster-defined Regions + +In addition to the `@EnableEntityDefinedRegions` annotation, {sdg-name} also provides the inverse annotation, +`@EnableClusterDefinedRegions`. Rather than basing your Regions on the entity classes defined and driven from +your application use cases (UC) and requirements (the most common and logical approach), alternatively, you can +declare your Regions from the Regions already defined in the cluster to which your `ClientCache` application +will connect. + +This allows you to centralize your configuration using the cluster of servers as the primary source of data definitions +and ensure that all client applications of the cluster have a consistent configuration. This is particularly useful +when quickly scaling up a large number instances of the same client application to handle the increased load +in a cloud-managed environment. + +The idea is, rather than the client application(s) driving the data dictionary, the user defines Regions +using {data-store-name}'s _Gfsh_ CLI shell tool. This has the added advantage that when additional peers are added +to the cluster, they too will also have and share the same configuration since it is remembered by {data-store-name}'s +_Cluster Configuration Service_. + +By way of example, a user might defined a Region in _Gfsh_, as follows: + +.Defining a Region with Gfsh +[source,txt] +---- +gfsh>create region --name=Books --type=PARTITION + Member | Status +--------- | -------------------------------------- +ServerOne | Region "/Books" created on "ServerOne" +ServerTwo | Region "/Books" created on "ServerTwo" + +gfsh>list regions +List of regions +--------------- +Books + +gfsh>describe region --name=/Books +.......................................................... +Name : Books +Data Policy : partition +Hosting Members : ServerTwo + ServerOne + +Non-Default Attributes Shared By Hosting Members + + Type | Name | Value +------ | ----------- | --------- +Region | size | 0 + | data-policy | PARTITION +---- + +With {data-store-name}'s _Cluster Configuration Service_, any additional peer members added to the cluster of servers +to handle the increased load (on the backend) will also have the same configuration, for example: + +.Adding an additional peer member to the cluster +[source,txt] +---- +gfsh>list members + Name | Id +--------- | ---------------------------------------------- +Locator | 10.0.0.121(Locator:68173:locator):1024 +ServerOne | 10.0.0.121(ServerOne:68242):1025 +ServerTwo | 10.0.0.121(ServerTwo:68372):1026 + +gfsh>start server --name=ServerThree --log-level=config --server-port=41414 +Starting a Geode Server in /Users/you/geode/cluster/ServerThree... +... +Server in /Users/you/geode/cluster/ServerThree... on 10.0.0.121[41414] as ServerThree is currently online. +Process ID: 68467 +Uptime: 3 seconds +Geode Version: 1.2.1 +Java Version: 1.8.0_152 +Log File: /Users/you/geode/cluster/ServerThree/ServerThree.log +JVM Arguments: -Dgemfire.default.locators=10.0.0.121[10334] + -Dgemfire.use-cluster-configuration=true + -Dgemfire.start-dev-rest-api=false + -Dgemfire.log-level=config + -XX:OnOutOfMemoryError=kill -KILL %p + -Dgemfire.launcher.registerSignalHandlers=true + -Djava.awt.headless=true + -Dsun.rmi.dgc.server.gcInterval=9223372036854775806 +Class-Path: /Users/you/geode/cluster/apache-geode-1.2.1/lib/geode-core-1.2.1.jar + :/Users/you/geode/cluster/apache-geode-1.2.1/lib/geode-dependencies.jar + +gfsh>list members + Name | Id +----------- | ---------------------------------------------- +Locator | 10.0.0.121(Locator:68173:locator):1024 +ServerOne | 10.0.0.121(ServerOne:68242):1025 +ServerTwo | 10.0.0.121(ServerTwo:68372):1026 +ServerThree | 10.0.0.121(ServerThree:68467):1027 + +gfsh>describe member --name=ServerThree +Name : ServerThree +Id : 10.0.0.121(ServerThree:68467):1027 +Host : 10.0.0.121 +Regions : Books +PID : 68467 +Groups : +Used Heap : 37M +Max Heap : 3641M +Working Dir : /Users/you/geode/cluster/ServerThree +Log file : /Users/you/geode/cluster/ServerThree/ServerThree.log +Locators : 10.0.0.121[10334] + +Cache Server Information +Server Bind : +Server Port : 41414 +Running : true +Client Connections : 0 +---- + +As you can see, "ServerThree" now has the "Books" Region. If the any or all of the server go down, they will have +the same configuration along with the "Books" Region when they come back up. + +On the client-side, many Book Store client application instances might be started to process books against +the Book Store online service. The "Books" Region might be 1 of many different Regions needed to implement +the Book Store application service. Rather than have to create and configure each Region individually, {sdg-acronym} +conveniently allows the client application Regions to be defined from the cluster, as follows: + +.Defining Client Regions from the Cluster with `@EnableClusterDefinedRegions` +[source,java] +---- +@ClientCacheApplication +@EnableClusterDefinedRegions +class BookStoreClientApplication { + + public static void main(String[] args) { + .... + } + + ... +} +---- + +NOTE: `@EnableClusterDefinedRegions` can only used on the client. + +TIP: You can use the `clientRegionShortcut` annotation attribute to control the type of Region created on the client. +By default, a client `PROXY` Region is created. Set `clientRegionShortcut` to `ClientRegionShortcut.CACHING_PROXY` +to implement "_near caching_". This setting applies to all client Regions created from Cluster-defined Regions. +If you want to control individual settings (like data policy) of the client Regions created from Regions defined +on the Cluster, then you can implement a +{sdg-javadoc}/org/springframework/data/gemfire/config/annotation/RegionConfigurer.html[`RegionConfigurer`] +with custom logic based on the Region name. + +Then, it becomes a simple matter to use the "Books" Region in your application. You can inject the "Books" Region +directly, as follows: + +.Using the "Books" Region +[source,java] +---- +@org.springframework.stereotype.Repository +class BooksDataAccessObject { + + @Resource(name = "Books") + private Region books; + + // implement CRUD and queries with the "Books" Region +} +---- + +Or, even define a Spring Data Repository definition based on the application domain type (entity), `Book`, +mapped to the "Books" Region, as follows: + +.Using the "Books" Region with a SD Repository +[source,java] +---- +interface BookRepository extends CrudRepository { + ... +} +---- + +You can then either inject your custom `BooksDataAccessObject` or the `BookRepository` into your application service +components to carry out whatever business function required. + [[bootstrap-annotation-config-region-eviction]] === Configuring Eviction @@ -1942,6 +2076,51 @@ with very little effort: See the https://docs.spring.io/spring-data/gemfire/docs/current/api/index.html?org/springframework/data/gemfire/config/annotation/EnableClusterConfiguration.html[`@EnableClusterConfiguration` annotation Javadoc] for more details. +[[bootstrap-annotation-config-ssl]] +== Configuring SSL + +Equally important to serializing data to be transferred over the wire is securing the data while in transit. +Of course, the common way to accomplish this in Java is by using the Secure Sockets Extension (SSE) +and Transport Layer Security (TLS). + +To enable SSL, annotate your application class with `@EnableSsl`, as follows: + +.Spring `ClientCache` application with SSL enabled +[source, java] +---- +@SpringBootApplication +@ClientCacheApplication +@EnableSsl +public class ClientApplication { .. } +---- + +Then you need to set the necessary SSL configuration attributes or properties: keystores, usernames/passwords, and so on. + +You can individually configure different {data-store-name} components (`GATEWAY`, `HTTP`, `JMX`, `LOCATOR`, and `SERVER`) +with SSL, or you can collectively configure them to use SSL by using the `CLUSTER` enumerated value. + +You can specify which {data-store-name} components the SSL configuration settings should applied by using +the nested `@EnableSsl` annotation, `components` attribute with enumerated values from the `Component` enum, +as follows: + +.Spring `ClientCache` application with SSL enabled by component +[source, java] +---- +@SpringBootApplication +@ClientCacheApplication +@EnableSsl(components = { GATEWAY, LOCATOR, SERVER }) +public class ClientApplication { .. } +---- + +In addition, you can also specify component-level SSL configuration (`ciphers`, `protocols` and `keystore`/`truststore` +information) by using the corresponding annotation attribute or associated configuration properties. + +See the https://docs.spring.io/spring-data/gemfire/docs/current/api/org/springframework/data/gemfire/config/annotation/EnableSsl.html[`@EnableSsl` annotation Javadoc] +for more details. + +More details on {data-store-name} SSL support can be found +{x-data-store-docs}/managing/security/ssl_overview.html[here]. + [[bootstrap-annotation-config-security]] == Configuring Security @@ -2178,9 +2357,10 @@ to accomplishing the function that the annotation provides: `@EnableSecurity` annotation, as described in "`<>`".) * `@EnableAutoRegionLookup`: Not recommended. Essentially, this annotation supports finding Regions defined in external configuration metadata (such as `cache.xml` or Cluster Configuration when applied to a server) and automatically -registers those Regions as beans in the Spring container. Users should generally prefer Spring configuration when -using Spring and {sdg-name}. See "`<>`" and "`<>`" -instead. +registers those Regions as beans in the Spring container. This annotation corresponds with the `` +element in SDG's XML namespace. More details can found <>. Users should generally +prefer Spring configuration when using Spring and {sdg-name}. See "`<>`" +and "`<>`" instead. * `@EnableBeanFactoryLocator`: Enables the {sdg-acronym} `GemfireBeanFactoryLocator` feature, which is only useful when using external configuration metadata (for example, `cache.xml`). For example, if you define a `CacheLoader` on a Region defined in `cache.xml`, you can still autowire this `CacheLoader` with, say, a relational database diff --git a/src/main/asciidoc/reference/bootstrap.adoc b/src/main/asciidoc/reference/bootstrap.adoc index 29639704..75d171e0 100644 --- a/src/main/asciidoc/reference/bootstrap.adoc +++ b/src/main/asciidoc/reference/bootstrap.adoc @@ -1,5 +1,5 @@ [[bootstrap]] -= Bootstrapping {data-store-name} with the Spring container += Bootstrapping {data-store-name} with the Spring Container {sdg-name} provides full configuration and initialization of the {data-store-name} In-Memory Data Grid (IMDG) using the Spring IoC container. The framework includes several classes to help simplify the configuration of diff --git a/src/main/asciidoc/reference/region.adoc b/src/main/asciidoc/reference/region.adoc index 35ae32db..d50892bd 100644 --- a/src/main/asciidoc/reference/region.adoc +++ b/src/main/asciidoc/reference/region.adoc @@ -61,7 +61,7 @@ or setup infrastructure. [[bootstrap:region:lookup:auto]] == Auto Region Lookup -"`auto-lookup`" lets you import all Regions defined in a {data-store-name} native `cache.xml` file into +`auto-region-lookup` lets you import all Regions defined in a {data-store-name} native `cache.xml` file into a Spring `ApplicationContext` when you use the `cache-xml-location` attribute on the `` element. For instance, consider the following `cache.xml` file: @@ -105,7 +105,7 @@ It is important to realize that {sdg-name} uses a Spring to post-process the cache after it is both created and initialized to determine the Regions defined in {data-store-name} to add as beans in the Spring `ApplicationContext`. -You may inject these "`auto-looked-up`" Regions as you would any other bean defined in the Spring `ApplicationContext`, +You may inject these "auto-looked-up" Regions as you would any other bean defined in the Spring `ApplicationContext`, with one exception: You may need to define a `depends-on` association with the '`gemfireCache`' bean, as follows: [source,java] @@ -138,7 +138,7 @@ If you declare your components by using Spring XML config, then you would do the ---- Doing so ensures that the {data-store-name} cache and all the Regions defined in `cache.xml` get created before -any components with auto-wire references when using the new `` element. +any components with auto-wire references when using the `` element. [[bootstrap:region:overview]] == Configuring Regions diff --git a/src/main/java/org/springframework/data/gemfire/client/GemfireDataSourcePostProcessor.java b/src/main/java/org/springframework/data/gemfire/client/GemfireDataSourcePostProcessor.java index 4b05eb7f..d119e5b5 100644 --- a/src/main/java/org/springframework/data/gemfire/client/GemfireDataSourcePostProcessor.java +++ b/src/main/java/org/springframework/data/gemfire/client/GemfireDataSourcePostProcessor.java @@ -15,7 +15,9 @@ package org.springframework.data.gemfire.client; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; +import org.apache.geode.cache.Region; import org.apache.geode.cache.client.ClientCache; import org.apache.geode.cache.client.ClientRegionFactory; import org.apache.geode.cache.client.ClientRegionShortcut; @@ -33,34 +35,42 @@ import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * A Spring {@link BeanFactoryPostProcessor} used to register a Client Region Proxy bean for each Region - * accessible to a GemFire DataSource. If the Region is already defined, the bean definition will not be overridden. + * A Spring {@link BeanFactoryPostProcessor} used to register a Client Region beans for each Region accessible to + * an Apache Geode or Pivotal GemFire DataSource. If the Region is already defined, the bean definition + * will not be overridden. * * @author David Turanski * @author John Blum - * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor - * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory - * @see org.springframework.data.gemfire.function.execution.GemfireOnServersFunctionTemplate - * @see ListRegionsOnServerFunction * @see org.apache.geode.cache.Region * @see org.apache.geode.cache.client.ClientCache * @see org.apache.geode.cache.client.ClientRegionFactory + * @see org.apache.geode.cache.client.ClientRegionShortcut * @see org.apache.geode.cache.execute.Function * @see org.apache.geode.management.internal.cli.functions.GetRegionsFunction + * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory + * @see org.springframework.data.gemfire.client.function.ListRegionsOnServerFunction + * @see org.springframework.data.gemfire.function.execution.GemfireOnServersFunctionTemplate + * @see ListRegionsOnServerFunction * @since 1.2.0 */ public class GemfireDataSourcePostProcessor implements BeanFactoryPostProcessor { - protected final Logger logger = LoggerFactory.getLogger(getClass()); + private static final ClientRegionShortcut DEFAULT_CLIENT_REGION_SHORTCUT = ClientRegionShortcut.PROXY; private final ClientCache clientCache; + private ClientRegionShortcut clientRegionShortcut = DEFAULT_CLIENT_REGION_SHORTCUT; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + /** - * Constructs an instance of the GemfireDataSourcePostProcessor BeanFactoryPostProcessor class initialized - * with the specified GemFire ClientCache instance for creating client PROXY Regions for all data Regions - * configured in the GemFire cluster. + * Constructs an instance of the {@link GemfireDataSourcePostProcessor} {@link BeanFactoryPostProcessor} class + * initialized * with the specified {@link ClientCache} instance for creating client {@link Region Regions} + * for all data {@link Region Regions} configured in the cluster. * - * @param clientCache the GemFire ClientCache instance. + * @param clientCache reference to the {@link ClientCache} instance. + * @throws IllegalArgumentException if {@link ClientCache} is {@literal null}. * @see org.apache.geode.cache.client.ClientCache */ public GemfireDataSourcePostProcessor(ClientCache clientCache) { @@ -70,13 +80,57 @@ public class GemfireDataSourcePostProcessor implements BeanFactoryPostProcessor this.clientCache = clientCache; } + /** + * Returns a reference to the {@link ClientCache}. + * + * @return a reference to the {@link ClientCache}. + * @see org.apache.geode.cache.client.ClientCache + */ + protected ClientCache getClientCache() { + return this.clientCache; + } + + /** + * Set the data policy used to configure the client {@link Region}. + * + * @param clientRegionShortcut {@link ClientRegionShortcut} used to define the data policy + * used by the client {@link Region}. + * @see org.apache.geode.cache.client.ClientRegionShortcut + */ + public void setClientRegionShortcut(ClientRegionShortcut clientRegionShortcut) { + this.clientRegionShortcut = clientRegionShortcut; + } + + /** + * Returns the data policy used to configure the client {@link Region}. + * + * @return the configured {@link ClientRegionShortcut} used to define the data policy + * used by the client {@link Region}. + * @see org.apache.geode.cache.client.ClientRegionShortcut + * @see java.util.Optional + */ + public Optional getClientRegionShortcut() { + return Optional.ofNullable(this.clientRegionShortcut); + } + + /** + * Returns a reference to the configured {@link Logger} used to log messages. + * + * @return a reference to the configured {@link Logger}. + * @see org.slf4j.Logger + */ + protected Logger getLogger() { + return this.logger; + } + /* * (non-Javadoc) - * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory) + * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor + * #postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory) */ @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - createClientRegionProxies(beanFactory, regionNames()); + createClientProxyRegions(beanFactory, regionNames()); } // TODO: remove this logic and delegate to o.s.d.g.config.remote.GemfireAdminOperations @@ -86,6 +140,7 @@ public class GemfireDataSourcePostProcessor implements BeanFactoryPostProcessor return execute(new ListRegionsOnServerFunction()); } catch (Exception ignore) { + try { Object results = execute(new GetRegionsFunction()); @@ -106,7 +161,7 @@ public class GemfireDataSourcePostProcessor implements BeanFactoryPostProcessor return regionNames; } catch (Exception cause) { - log("Failed to determine the Regions available on the Server: %n%1$s", cause); + logDebug("Failed to determine the Regions available on the Server: %n%1$s", cause); return Collections.emptyList(); } } @@ -114,51 +169,71 @@ public class GemfireDataSourcePostProcessor implements BeanFactoryPostProcessor @SuppressWarnings("unchecked") T execute(Function gemfireFunction, Object... arguments) { - return new GemfireOnServersFunctionTemplate(this.clientCache).executeAndExtract(gemfireFunction, arguments); + return new GemfireOnServersFunctionTemplate(getClientCache()).executeAndExtract(gemfireFunction, arguments); } boolean containsRegionInformation(Object results) { + return results instanceof Object[] && ((Object[]) results).length > 0 && ((Object[]) results)[0] instanceof RegionInformation; } - void createClientRegionProxies(ConfigurableListableBeanFactory beanFactory, Iterable regionNames) { + void createClientProxyRegions(ConfigurableListableBeanFactory beanFactory, Iterable regionNames) { if (regionNames.iterator().hasNext()) { + ClientRegionShortcut resolvedClientRegionShortcut = getClientRegionShortcut() + .orElse(DEFAULT_CLIENT_REGION_SHORTCUT); + ClientRegionFactory clientRegionFactory = - this.clientCache.createClientRegionFactory(ClientRegionShortcut.PROXY); + this.clientCache.createClientRegionFactory(resolvedClientRegionShortcut); for (String regionName : regionNames) { - boolean createRegion = true; - if (beanFactory.containsBean(regionName)) { - Object existingBean = beanFactory.getBean(regionName); + Object bean = beanFactory.getBean(regionName); - if (logger.isWarnEnabled()) { - logger.warn("Cannot create a client PROXY Region bean named {}; A bean with name {} having type {} already exists", - regionName, regionName, ObjectUtils.nullSafeClassName(existingBean)); - } - - createRegion = false; - } - - if (createRegion) { - log("Creating Region bean with name '%s'...", regionName); - beanFactory.registerSingleton(regionName, clientRegionFactory.create(regionName)); + logWarn("Cannot create a client {} Region bean named {}; A bean with name {} having type {} already exists", + resolvedClientRegionShortcut.name(), regionName, regionName, ObjectUtils.nullSafeClassName(bean)); } else { - log("A Region with name '%s' is already defined", regionName); + logInfo("Creating Region bean with name {}...", regionName); + beanFactory.registerSingleton(regionName, clientRegionFactory.create(regionName)); } } } } - void log(String message, Object... arguments) { + void logDebug(String message, Object... arguments) { + + Logger logger = getLogger(); + if (logger.isDebugEnabled()) { logger.debug(String.format(message, arguments)); } } + + void logInfo(String message, Object... arguments) { + + Logger logger = getLogger(); + + if (logger.isInfoEnabled()) { + logger.info(message, arguments); + } + } + + void logWarn(String message, Object... arguments) { + + Logger logger = getLogger(); + + if (logger.isWarnEnabled()) { + logger.warn(message, arguments); + } + } + + public GemfireDataSourcePostProcessor using(ClientRegionShortcut clientRegionShortcut) { + setClientRegionShortcut(clientRegionShortcut); + return this; + } } diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/AutoRegionLookupConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/AutoRegionLookupConfiguration.java index 79af900f..ebd388c7 100644 --- a/src/main/java/org/springframework/data/gemfire/config/annotation/AutoRegionLookupConfiguration.java +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/AutoRegionLookupConfiguration.java @@ -17,7 +17,7 @@ package org.springframework.data.gemfire.config.annotation; -import java.util.Map; +import java.lang.annotation.Annotation; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; @@ -30,7 +30,7 @@ import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.context.expression.BeanFactoryResolver; -import org.springframework.core.env.Environment; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport; import org.springframework.data.gemfire.config.support.AutoRegionLookupBeanPostProcessor; @@ -73,15 +73,12 @@ public class AutoRegionLookupConfiguration extends AbstractAnnotationConfigSuppo private static final AtomicBoolean AUTO_REGION_LOOKUP_BEAN_POST_PROCESSOR_REGISTERED = new AtomicBoolean(false); - private Environment environment; - private ExpressionParser spelParser = new SpelExpressionParser(); private StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); - /* (non-Javadoc) */ @Override - protected Class getAnnotationType() { + protected Class getAnnotationType() { return EnableAutoRegionLookup.class; } @@ -99,8 +96,7 @@ public class AutoRegionLookupConfiguration extends AbstractAnnotationConfigSuppo ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) beanFactory; - this.evaluationContext.setTypeLocator(new StandardTypeLocator( - configurableBeanFactory.getBeanClassLoader())); + this.evaluationContext.setTypeLocator(new StandardTypeLocator(configurableBeanFactory.getBeanClassLoader())); Optional.ofNullable(configurableBeanFactory.getConversionService()) .ifPresent(conversionService -> @@ -114,24 +110,23 @@ public class AutoRegionLookupConfiguration extends AbstractAnnotationConfigSuppo @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - Map enableAutoRegionLookupAttributes = - importingClassMetadata.getAnnotationAttributes(EnableAutoRegionLookup.class.getName()); + AnnotationAttributes enableAutoRegionLookupAttributes = getAnnotationAttributes(importingClassMetadata); - Optional.ofNullable(resolveProperty(propertyName("enable-auto-region-lookup"), - (Boolean) enableAutoRegionLookupAttributes.get("enabled"))) + Optional.ofNullable(resolveProperty(cacheProperty("enable-auto-region-lookup"), + resolveProperty(propertyName("enable-auto-region-lookup"), + enableAutoRegionLookupAttributes.getBoolean("enabled")))) .filter(Boolean.TRUE::equals) .ifPresent(enabled -> registerAutoRegionLookupBeanPostProcessor(registry)); } /** - * (non-Javadoc) - * - * This method was used to support Spring property placeholders and SpEL Expressions - * in the {@link EnableAutoRegionLookup#enabled()} attribute. However, this required the attribute to be - * of type {@link String}, which violates type-safety. We are favoring type-safety over configuration - * flexibility and offering alternative means to achieve flexible and dynamic configuration, e.g. properties + * This method is used to support Spring property placeholders and SpEL Expressions + * in the {@link EnableAutoRegionLookup#enabled()} attribute. However, this requires the attribute to be of type + * {@link String}, which violates type-safety. We are favoring type-safety over configuration flexibility + * and offering alternative means to achieve flexible and dynamic configuration, e.g. properties * from an {@literal application.properties} file. */ + @SuppressWarnings("unused") private boolean isEnabled(String enabled) { enabled = StringUtils.trimWhitespace(enabled); @@ -139,7 +134,8 @@ public class AutoRegionLookupConfiguration extends AbstractAnnotationConfigSuppo if (!Boolean.parseBoolean(enabled)) { try { // try parsing as a SpEL expression... - return this.spelParser.parseExpression(enabled).getValue(this.evaluationContext, Boolean.TYPE); + return Boolean.TRUE.equals(this.spelParser.parseExpression(enabled) + .getValue(this.evaluationContext, Boolean.TYPE)); } catch (EvaluationException ignore) { return false; @@ -153,10 +149,10 @@ public class AutoRegionLookupConfiguration extends AbstractAnnotationConfigSuppo return DEFAULT_ENABLED; } - /* (non-Javadoc) */ private void registerAutoRegionLookupBeanPostProcessor(BeanDefinitionRegistry registry) { if (AUTO_REGION_LOOKUP_BEAN_POST_PROCESSOR_REGISTERED.compareAndSet(false, true)) { + AbstractBeanDefinition autoRegionLookupBeanPostProcessor = BeanDefinitionBuilder .rootBeanDefinition(AutoRegionLookupBeanPostProcessor.class) .setRole(AbstractBeanDefinition.ROLE_INFRASTRUCTURE) diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/ClusterDefinedRegionsConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/ClusterDefinedRegionsConfiguration.java new file mode 100644 index 00000000..c4563682 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/ClusterDefinedRegionsConfiguration.java @@ -0,0 +1,92 @@ +/* + * 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.config.annotation; + +import java.lang.annotation.Annotation; +import java.util.Optional; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.client.ClientCache; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.apache.shiro.util.Assert; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.gemfire.client.GemfireDataSourcePostProcessor; +import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport; +import org.springframework.data.gemfire.util.CacheUtils; + +/** + * The {@link ClusterDefinedRegionsConfiguration} class configures client Proxy-based {@link Region Regions} + * for all {@link Region Regions} defined in the cluster to which the cache client is connected. + * + * @author John Blum + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.client.ClientCache + * @see org.apache.geode.cache.client.ClientRegionShortcut + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.ImportAware + * @see org.springframework.core.annotation.AnnotationAttributes + * @see org.springframework.core.type.AnnotationMetadata + * @see org.springframework.data.gemfire.client.GemfireDataSourcePostProcessor + * @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport + * @since 2.1.0 + */ +@Configuration +public class ClusterDefinedRegionsConfiguration extends AbstractAnnotationConfigSupport implements ImportAware { + + protected static final ClientRegionShortcut DEFAULT_CLIENT_REGION_SHORTCUT = ClientRegionShortcut.PROXY; + + private ClientRegionShortcut clientRegionShortcut = DEFAULT_CLIENT_REGION_SHORTCUT; + + @Override + protected Class getAnnotationType() { + return EnableClusterDefinedRegions.class; + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + + AnnotationAttributes enableClusterDefinedRegionsAttributes = getAnnotationAttributes(importMetadata); + + setClientRegionShortcut(enableClusterDefinedRegionsAttributes.getEnum("clientRegionShortcut")); + } + + protected void setClientRegionShortcut(ClientRegionShortcut clientRegionShortcut) { + this.clientRegionShortcut = clientRegionShortcut; + } + + protected Optional getClientRegionShortcut() { + return Optional.ofNullable(this.clientRegionShortcut); + } + + protected ClientRegionShortcut resolveClientRegionShortcut() { + return getClientRegionShortcut().orElse(DEFAULT_CLIENT_REGION_SHORTCUT); + } + + @Bean + public GemfireDataSourcePostProcessor gemfireDataSourcePostProcessor(GemFireCache gemfireCache) { + + Assert.isTrue(CacheUtils.isClient(gemfireCache), "GemFireCache must be an instance of ClientCache"); + + return new GemfireDataSourcePostProcessor((ClientCache) gemfireCache).using(resolveClientRegionShortcut()); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/EnableAutoRegionLookup.java b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableAutoRegionLookup.java index e72fbd65..85674c25 100644 --- a/src/main/java/org/springframework/data/gemfire/config/annotation/EnableAutoRegionLookup.java +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableAutoRegionLookup.java @@ -24,20 +24,19 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apache.geode.cache.Region; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.data.gemfire.PeerRegionFactoryBean; /** - * The {@link EnableAutoRegionLookup} annotation configures a Spring {@link org.springframework.context.annotation.Configuration} - * annotated class with the ability to automatically look up and register GemFire {@link org.apache.geode.cache.Region Regions} - * which may have be defined in {@literal cache.xml} or by using GemFire's Cluster Configuration Service. - * - * This annotation defines the {@code enabled} attribute to allow users to dynamically change the behavior - * of auto {@link org.apache.geode.cache.Region} lookup at application configuration time using either a SpEL - * expression or a Spring property placeholder. + * The {@link EnableAutoRegionLookup} annotation configures a Spring {@link Configuration} annotated class + * with the ability to automatically look up and register any Apache Geode or Pivotal GemFire {@link Region Regions} + * which may have be defined in {@literal cache.xml} or by using the Cluster Configuration Service. * * @author John Blum - * @see PeerRegionFactoryBean#setLookupEnabled(Boolean) + * @see org.apache.geode.cache.Region + * @see org.springframework.context.annotation.Import + * @see org.springframework.data.gemfire.ResolvableRegionFactoryBean#setLookupEnabled(Boolean) * @see org.springframework.data.gemfire.config.annotation.AutoRegionLookupConfiguration * @since 1.9.0 */ @@ -54,7 +53,7 @@ public @interface EnableAutoRegionLookup { * * Defaults to {@literal true}. * - * Use the {@literal spring.data.gemfire.enable-auto-region-lookup} in {@literal application.properties} + * Use the {@literal spring.data.gemfire.cache.enable-auto-region-lookup} in {@literal application.properties} * to dynamically customize this configuration setting. */ boolean enabled() default true; diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/EnableClusterDefinedRegions.java b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableClusterDefinedRegions.java new file mode 100644 index 00000000..322cc205 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableClusterDefinedRegions.java @@ -0,0 +1,62 @@ +/* + * 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.config.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.geode.cache.Region; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * The {@link EnableClusterDefinedRegions} annotation marks a Spring {@link Configuration @Configuration} application + * annotated class to enable the creation of client Proxy-based {@link Region Regions} for all {@link Region Regions} + * defined in an Apache Geode/Pivotal GemFire cluster. + * + * @author John Blum + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.client.ClientCache + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.Import + * @see org.springframework.data.gemfire.config.annotation.ClusterDefinedRegionsConfiguration + * @since 2.1.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Import(ClusterDefinedRegionsConfiguration.class) +@SuppressWarnings("unused") +public @interface EnableClusterDefinedRegions { + + /** + * Configures the client {@link Region} data management policy for all client {@link Region Regions} created from + * the corresponding server-side {@link Region}. + * + * Defaults to {@link ClientRegionShortcut#PROXY}. + * + * @see org.apache.geode.cache.client.ClientRegionShortcut + */ + ClientRegionShortcut clientRegionShortcut() default ClientRegionShortcut.PROXY; + +} diff --git a/src/main/java/org/springframework/data/gemfire/config/support/AutoRegionLookupBeanPostProcessor.java b/src/main/java/org/springframework/data/gemfire/config/support/AutoRegionLookupBeanPostProcessor.java index f73abbaa..9afa7586 100644 --- a/src/main/java/org/springframework/data/gemfire/config/support/AutoRegionLookupBeanPostProcessor.java +++ b/src/main/java/org/springframework/data/gemfire/config/support/AutoRegionLookupBeanPostProcessor.java @@ -47,6 +47,7 @@ import org.springframework.util.StringUtils; * @see org.springframework.beans.factory.BeanFactory * @see org.springframework.beans.factory.BeanFactoryAware * @see org.springframework.beans.factory.config.BeanPostProcessor + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory * @since 1.5.0 */ public class AutoRegionLookupBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { @@ -54,7 +55,13 @@ public class AutoRegionLookupBeanPostProcessor implements BeanPostProcessor, Bea private ConfigurableListableBeanFactory beanFactory; /** - * {@inheritDoc} + * Sets a reference to the configured Spring {@link BeanFactory}. + * + * @param beanFactory configured Spring {@link BeanFactory}. + * @throws IllegalArgumentException if the given {@link BeanFactory} is not an instance of + * {@link ConfigurableListableBeanFactory}. + * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.beans.factory.BeanFactory */ @Override @SuppressWarnings("all") @@ -67,15 +74,18 @@ public class AutoRegionLookupBeanPostProcessor implements BeanPostProcessor, Bea this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; } - /* (non-Javadoc) */ + /** + * Returns a reference to the containing Spring {@link BeanFactory}. + * + * @return a reference to the containing Spring {@link BeanFactory}. + * @throws IllegalStateException if the {@link BeanFactory} was not configured. + * @see org.springframework.beans.factory.BeanFactory + */ protected ConfigurableListableBeanFactory getBeanFactory() { - return Optional.ofNullable(this.beanFactory).orElseThrow(() -> - newIllegalStateException("BeanFactory was not properly configured")); + return Optional.ofNullable(this.beanFactory) + .orElseThrow(() -> newIllegalStateException("BeanFactory was not properly configured")); } - /** - * {@inheritDoc} - */ @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { @@ -86,12 +96,10 @@ public class AutoRegionLookupBeanPostProcessor implements BeanPostProcessor, Bea return bean; } - /* (non-Javadoc) */ void registerCacheRegionsAsBeans(GemFireCache cache) { cache.rootRegions().forEach(this::registerCacheRegionAsBean); } - /* (non-Javadoc) */ void registerCacheRegionAsBean(Region region) { if (region != null) { @@ -108,7 +116,6 @@ public class AutoRegionLookupBeanPostProcessor implements BeanPostProcessor, Bea } } - /* (non-Javadoc) */ String getBeanName(Region region) { return Optional.ofNullable(region.getFullPath()) @@ -117,7 +124,6 @@ public class AutoRegionLookupBeanPostProcessor implements BeanPostProcessor, Bea .orElseGet(region::getName); } - /* (non-Javadoc) */ Set> nullSafeSubregions(Region parentRegion) { return Optional.ofNullable(parentRegion.subregions(false)).orElse(Collections.emptySet()); } diff --git a/src/test/java/org/springframework/data/gemfire/client/GemfireDataSourcePostProcessorTest.java b/src/test/java/org/springframework/data/gemfire/client/GemfireDataSourcePostProcessorTest.java index 49ecb67a..358e2e73 100644 --- a/src/test/java/org/springframework/data/gemfire/client/GemfireDataSourcePostProcessorTest.java +++ b/src/test/java/org/springframework/data/gemfire/client/GemfireDataSourcePostProcessorTest.java @@ -16,19 +16,18 @@ package org.springframework.data.gemfire.client; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.CoreMatchers.sameInstance; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.same; +import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; 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.when; @@ -52,14 +51,10 @@ import org.apache.geode.cache.client.ClientRegionShortcut; import org.apache.geode.cache.execute.Function; import org.apache.geode.management.internal.cli.domain.RegionInformation; import org.apache.geode.management.internal.cli.functions.GetRegionsFunction; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.data.gemfire.client.function.ListRegionsOnServerFunction; import org.springframework.data.gemfire.util.RegionUtils; @@ -84,20 +79,13 @@ public class GemfireDataSourcePostProcessorTest { @Mock private ClientCache mockClientCache; - @Rule - public ExpectedException exception = ExpectedException.none(); - - private RegionInformation newRegionInformation(Region region) { - return new RegionInformation(region, false); - } - @SuppressWarnings("unchecked") private Region mockRegion(String name) { Region mockRegion = mock(Region.class, name); RegionAttributes mockRegionAttributes = - mock(RegionAttributes.class, String.format("%1$s-RegionAttributes", name)); + mock(RegionAttributes.class, String.format("%s-RegionAttributes", name)); when(mockRegion.getParentRegion()).thenReturn(null); when(mockRegion.getFullPath()).thenReturn(RegionUtils.toRegionPath(name)); @@ -108,6 +96,43 @@ public class GemfireDataSourcePostProcessorTest { return mockRegion; } + private RegionInformation newRegionInformation(Region region) { + return new RegionInformation(region, false); + } + + @Test + public void constructGemfireDataSourcePostProcessor() { + + GemfireDataSourcePostProcessor postProcessor = new GemfireDataSourcePostProcessor(mockClientCache); + + assertThat(postProcessor).isNotNull(); + assertThat(postProcessor.getClientCache()).isEqualTo(this.mockClientCache); + assertThat(postProcessor.getClientRegionShortcut().orElse(null)).isEqualTo(ClientRegionShortcut.PROXY); + assertThat(postProcessor.getLogger()).isNotNull(); + } + + @Test + public void setAndGetClientRegionShortcut() { + + GemfireDataSourcePostProcessor postProcessor = new GemfireDataSourcePostProcessor(this.mockClientCache); + + assertThat(postProcessor).isNotNull(); + assertThat(postProcessor.getClientRegionShortcut().orElse(null)).isEqualTo(ClientRegionShortcut.PROXY); + + postProcessor.setClientRegionShortcut(ClientRegionShortcut.CACHING_PROXY); + + assertThat(postProcessor.getClientRegionShortcut().orElse(null)) + .isEqualTo(ClientRegionShortcut.CACHING_PROXY); + + postProcessor.setClientRegionShortcut(ClientRegionShortcut.LOCAL); + + assertThat(postProcessor.getClientRegionShortcut().orElse(null)).isEqualTo(ClientRegionShortcut.LOCAL); + + postProcessor.setClientRegionShortcut(null); + + assertThat(postProcessor.getClientRegionShortcut().isPresent()).isFalse(); + } + @Test public void postProcessBeanFactoryCallsCreateClientRegionProxiesWithRegionNames() { @@ -116,166 +141,152 @@ public class GemfireDataSourcePostProcessorTest { ConfigurableListableBeanFactory mockBeanFactory = mock(ConfigurableListableBeanFactory.class, "MockSpringBeanFactory"); - List testRegionNames = Collections.singletonList("Test"); + String[] testRegionNames = { "Test" }; GemfireDataSourcePostProcessor postProcessor = new GemfireDataSourcePostProcessor(this.mockClientCache) { @Override Iterable regionNames() { - return testRegionNames; + return Arrays.asList(testRegionNames); } - @Override void createClientRegionProxies(ConfigurableListableBeanFactory beanFactory, Iterable regionNames) { - assertThat(beanFactory, is(sameInstance(mockBeanFactory))); - assertSame(testRegionNames, regionNames); + @Override + void createClientProxyRegions(ConfigurableListableBeanFactory beanFactory, Iterable regionNames) { + assertThat(beanFactory).isSameAs(mockBeanFactory); + assertThat(regionNames).containsExactly(testRegionNames); createClientRegionProxiesCalled.compareAndSet(false, true); } }; postProcessor.postProcessBeanFactory(mockBeanFactory); - assertThat(createClientRegionProxiesCalled.get(), is(true)); + assertThat(createClientRegionProxiesCalled.get()).isTrue(); } @Test public void regionNamesWithListRegionsOnServerFunction() { - List expectedRegionNames = Arrays.asList("ExampleOne", "ExampleTwo"); + String[] expectedRegionNames = { "ExampleOne", "ExampleTwo" }; - GemfireDataSourcePostProcessor postProcessor = new GemfireDataSourcePostProcessor(this.mockClientCache) { + GemfireDataSourcePostProcessor postProcessor = spy(new GemfireDataSourcePostProcessor(this.mockClientCache)); - @Override @SuppressWarnings("unchecked") - T execute(Function gemfireFunction, Object... arguments) { - assertThat(gemfireFunction, is(instanceOf(ListRegionsOnServerFunction.class))); - return (T) expectedRegionNames; - } - }; + doReturn(Arrays.asList(expectedRegionNames)).when(postProcessor) + .execute(isA(ListRegionsOnServerFunction.class), any()); Iterable actualRegionNames = postProcessor.regionNames(); - assertSame(expectedRegionNames, actualRegionNames); + assertThat(actualRegionNames).containsExactly(expectedRegionNames); + + verify(postProcessor, times(1)).execute(isA(ListRegionsOnServerFunction.class)); + verify(postProcessor, never()).execute(any(GetRegionsFunction.class)); } @Test @SuppressWarnings("unchecked") public void regionNamesWithGetRegionsFunction() { - List expectedRegionNames = Arrays.asList("ExampleOne", "ExampleTwo"); + String[] expectedRegionNames = { "ExampleOne", "ExampleTwo" }; - GemfireDataSourcePostProcessor postProcessor = new GemfireDataSourcePostProcessor(this.mockClientCache) { + GemfireDataSourcePostProcessor postProcessor = spy(new GemfireDataSourcePostProcessor(this.mockClientCache)); - @Override @SuppressWarnings("unchecked") - T execute(Function gemfireFunction, Object... arguments) { + doThrow(new RuntimeException("fail")).when(postProcessor).execute(isA(ListRegionsOnServerFunction.class), any()); - if (gemfireFunction instanceof ListRegionsOnServerFunction) { - throw new RuntimeException("fail"); - } - else if (gemfireFunction instanceof GetRegionsFunction) { - return (T) Arrays.asList(newRegionInformation(mockRegion(expectedRegionNames.get(0))), - newRegionInformation(mockRegion(expectedRegionNames.get(1)))).toArray(); - } - - throw new IllegalArgumentException(String.format("GemFire Function [%1$s] with ID [%2$s] not registered", - gemfireFunction.getClass().getName(), gemfireFunction.getId())); - } - }; + doAnswer(invocation -> + Arrays.stream(expectedRegionNames) + .map(this::mockRegion) + .map(this::newRegionInformation) + .collect(Collectors.toList()) + .toArray() + ).when(postProcessor).execute(isA(GetRegionsFunction.class), any()); List actualRegionNames = StreamSupport.stream(postProcessor.regionNames().spliterator(), false).collect(Collectors.toList()); - assertThat(actualRegionNames.containsAll(expectedRegionNames), is(true)); + assertThat(actualRegionNames).containsExactly(expectedRegionNames); + + verify(postProcessor, times(1)).execute(isA(ListRegionsOnServerFunction.class)); + verify(postProcessor, times(1)).execute(isA(GetRegionsFunction.class)); } @Test public void regionNamesWithGetRegionsFunctionReturningNoResults() { - GemfireDataSourcePostProcessor postProcessor = new GemfireDataSourcePostProcessor(this.mockClientCache) { + GemfireDataSourcePostProcessor postProcessor = spy(new GemfireDataSourcePostProcessor(this.mockClientCache)); - @Override @SuppressWarnings("unchecked") T - execute(Function gemfireFunction, Object... arguments) { - - if (gemfireFunction instanceof ListRegionsOnServerFunction) { - throw new RuntimeException("fail"); - } - else if (gemfireFunction instanceof GetRegionsFunction) { - return null; - } - - throw new IllegalArgumentException(String.format("GemFire Function (%1$s) with ID (%2$s) not registered", - gemfireFunction.getClass().getName(), gemfireFunction.getId())); - } - }; + doThrow(new RuntimeException("fail")).when(postProcessor).execute(isA(ListRegionsOnServerFunction.class), any()); + doReturn(null).when(postProcessor).execute(isA(GetRegionsFunction.class), any()); Iterable actualRegionNames = postProcessor.regionNames(); - assertThat(actualRegionNames, is(not(nullValue()))); - assertThat(actualRegionNames.iterator(), (is(not(nullValue())))); - assertThat(actualRegionNames.iterator().hasNext(), is(false)); + assertThat(actualRegionNames).isNotNull(); + assertThat(actualRegionNames).isEmpty(); + + verify(postProcessor, times(1)).execute(isA(ListRegionsOnServerFunction.class)); + verify(postProcessor, times(1)).execute(isA(GetRegionsFunction.class)); } @Test public void regionNamesWithGetRegionsFunctionThrowingException() { - AtomicBoolean logMethodCalled = new AtomicBoolean(false); + GemfireDataSourcePostProcessor postProcessor = spy(new GemfireDataSourcePostProcessor(this.mockClientCache)); - GemfireDataSourcePostProcessor postProcessor = new GemfireDataSourcePostProcessor(this.mockClientCache) { + doAnswer(invocation -> { - @Override @SuppressWarnings("unchecked") T - execute(Function gemfireFunction, Object... arguments) { - throw new IllegalArgumentException(String.format("GemFire Function (%1$s) with ID (%2$s) not registered", - gemfireFunction.getClass().getName(), gemfireFunction.getId())); - } + Function function = invocation.getArgument(0); - @Override - void log(final String message, final Object... arguments) { - assertThat(message.startsWith("Failed to determine the Regions available on the Server:"), is(true)); - logMethodCalled.compareAndSet(false, true); - } - }; + throw new IllegalArgumentException(String.format("Function [%1$s] with ID [%2$s] not registered", + function.getClass().getName(), function.getId())); + + }).when(postProcessor).execute(any(Function.class), any()); Iterable actualRegionNames = postProcessor.regionNames(); - assertThat(actualRegionNames, is(not(nullValue()))); - assertThat(actualRegionNames.iterator(), (is(not(nullValue())))); - assertThat(actualRegionNames.iterator().hasNext(), is(false)); - assertThat(logMethodCalled.get(), is(true)); + assertThat(actualRegionNames).isNotNull(); + assertThat(actualRegionNames).isEmpty(); + + verify(postProcessor, times(1)).execute(isA(ListRegionsOnServerFunction.class)); + verify(postProcessor, times(1)).execute(isA(GetRegionsFunction.class)); + verify(postProcessor, times(1)) + .logDebug(startsWith("Failed to determine the Regions available on the Server:"), any()); } @Test public void containsRegionInformationIsTrue() { assertThat(new GemfireDataSourcePostProcessor(this.mockClientCache) - .containsRegionInformation(new Object[] { newRegionInformation(mockRegion("Example")) }), - is(true)); - } - - @Test - public void containsRegionInformationWithListOfRegionInformationIsFalse() { - assertThat(new GemfireDataSourcePostProcessor(this.mockClientCache) - .containsRegionInformation(Collections.singletonList(newRegionInformation(mockRegion("Example")))), - is(false)); - } - - @Test - public void containsRegionInformationWithNonEmptyArrayContainingNonRegionInformationIsFalse() { - assertThat(new GemfireDataSourcePostProcessor(this.mockClientCache) - .containsRegionInformation(new Object[] { "test" }), is(false)); + .containsRegionInformation(new Object[] { newRegionInformation(mockRegion("Example")) })) + .isTrue(); } @Test public void containsRegionInformationWithEmptyArrayIsFalse() { assertThat(new GemfireDataSourcePostProcessor(this.mockClientCache) - .containsRegionInformation(new Object[0]), is(false)); + .containsRegionInformation(new Object[0])) + .isFalse(); + } + + @Test + public void containsRegionInformationWithListOfRegionInformationIsFalse() { + assertThat(new GemfireDataSourcePostProcessor(this.mockClientCache) + .containsRegionInformation(Collections.singletonList(newRegionInformation(mockRegion("Example"))))) + .isFalse(); + } + + @Test + public void containsRegionInformationWithNonEmptyArrayContainingNonRegionInformationIsFalse() { + assertThat(new GemfireDataSourcePostProcessor(this.mockClientCache) + .containsRegionInformation(new Object[] { "test" })) + .isFalse(); } @Test public void containsRegionInformationWithNullIsFalse() { - assertThat(new GemfireDataSourcePostProcessor(this.mockClientCache).containsRegionInformation(null), - is(false)); + assertThat(new GemfireDataSourcePostProcessor(this.mockClientCache).containsRegionInformation(null)) + .isFalse(); } @Test @SuppressWarnings("unchecked") - public void createClientRegionProxies() { + public void createClientProxyRegionsIsSuccessful() { ClientCache mockClientCache = mock(ClientCache.class, "MockGemFireClientCache"); @@ -287,17 +298,19 @@ public class GemfireDataSourcePostProcessorTest { Region mockRegionOne = mock(Region.class, "MockGemFireRegionOne"); Region mockRegionTwo = mock(Region.class, "MockGemFireRegionTwo"); - final Map> regionMap = new HashMap>(2); + Map> regionMap = new HashMap<>(2); regionMap.put("RegionOne", mockRegionOne); regionMap.put("RegionTwo", mockRegionTwo); - doAnswer(new Answer>() { - @Override public Region answer(final InvocationOnMock invocation) throws Throwable { - String regionName = invocation.getArgument(0); - assertThat(regionMap.containsKey(regionName), is(true)); - return regionMap.get(regionName); - } + doAnswer(invocation -> { + + String regionName = invocation.getArgument(0); + + assertThat(regionMap.containsKey(regionName)).isTrue(); + + return regionMap.get(regionName); + }).when(mockClientRegionFactory).create(any(String.class)); ConfigurableListableBeanFactory mockBeanFactory = mock(ConfigurableListableBeanFactory.class, "MockSpringBeanFactory"); @@ -306,7 +319,7 @@ public class GemfireDataSourcePostProcessorTest { GemfireDataSourcePostProcessor postProcessor = new GemfireDataSourcePostProcessor(mockClientCache); - postProcessor.createClientRegionProxies(mockBeanFactory, regionMap.keySet()); + postProcessor.createClientProxyRegions(mockBeanFactory, regionMap.keySet()); verify(mockClientCache, times(1)).createClientRegionFactory(eq(ClientRegionShortcut.PROXY)); verify(mockClientRegionFactory, times(1)).create(eq("RegionOne")); @@ -317,7 +330,7 @@ public class GemfireDataSourcePostProcessorTest { @Test @SuppressWarnings("unchecked") - public void createClientRegionProxiesWhenRegionBeanExists() { + public void createClientProxyRegionsWhenRegionBeanExists() { ClientCache mockClientCache = mock(ClientCache.class, "MockGemFireClientCache"); @@ -335,10 +348,25 @@ public class GemfireDataSourcePostProcessorTest { GemfireDataSourcePostProcessor postProcessor = new GemfireDataSourcePostProcessor(mockClientCache); - postProcessor.createClientRegionProxies(mockBeanFactory, Collections.singletonList("Example")); + postProcessor.createClientProxyRegions(mockBeanFactory, Collections.singletonList("Example")); verify(mockClientCache, times(1)).createClientRegionFactory(eq(ClientRegionShortcut.PROXY)); verify(mockClientRegionFactory, never()).create(any(String.class)); verify(mockBeanFactory, never()).registerSingleton(any(String.class), any(Region.class)); } + + @Test + public void usingIsCorrect() { + + GemfireDataSourcePostProcessor postProcessor = new GemfireDataSourcePostProcessor(this.mockClientCache) + .using(ClientRegionShortcut.LOCAL); + + assertThat(postProcessor).isNotNull(); + assertThat(postProcessor.getClientRegionShortcut().orElse(null)).isEqualTo(ClientRegionShortcut.LOCAL); + assertThat(postProcessor.using(ClientRegionShortcut.CACHING_PROXY)).isEqualTo(postProcessor); + assertThat(postProcessor.getClientRegionShortcut() + .orElse(null)).isEqualTo(ClientRegionShortcut.CACHING_PROXY); + assertThat(postProcessor.using(null)).isEqualTo(postProcessor); + assertThat(postProcessor.getClientRegionShortcut().isPresent()).isFalse(); + } } diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/EnableClusterDefinedRegionsIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableClusterDefinedRegionsIntegrationTests.java new file mode 100644 index 00000000..d7071623 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableClusterDefinedRegionsIntegrationTests.java @@ -0,0 +1,181 @@ +/* + * 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.config.annotation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; + +import javax.annotation.Resource; + +import org.apache.geode.cache.DataPolicy; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.Region; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.data.gemfire.LocalRegionFactoryBean; +import org.springframework.data.gemfire.PartitionedRegionFactoryBean; +import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; +import org.springframework.data.gemfire.process.ProcessWrapper; +import org.springframework.data.gemfire.support.ConnectionEndpoint; +import org.springframework.data.gemfire.test.support.ClientServerIntegrationTestsSupport; +import org.springframework.data.gemfire.util.RegionUtils; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link EnableClusterDefinedRegions} and {@link ClusterDefinedRegionsConfiguration}. + * + * @author John Blum + * @see org.junit.Test + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.cache.Region + * @see org.springframework.data.gemfire.config.annotation.ClusterDefinedRegionsConfiguration + * @see org.springframework.data.gemfire.config.annotation.EnableClusterDefinedRegions + * @see org.springframework.data.gemfire.process.ProcessWrapper + * @see org.springframework.data.gemfire.test.support.ClientServerIntegrationTestsSupport + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 2.1.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = EnableClusterDefinedRegionsIntegrationTests.ClientTestConfiguration.class) +@SuppressWarnings("unused") +public class EnableClusterDefinedRegionsIntegrationTests extends ClientServerIntegrationTestsSupport { + + private static final String GEMFIRE_LOG_LEVEL = "error"; + + private static ProcessWrapper gemfireServer; + + @BeforeClass + public static void startGemFireServer() throws Exception { + + int availablePort = findAvailablePort(); + + gemfireServer = run(ServerTestConfiguration.class, + String.format("-D%s=%d", GEMFIRE_CACHE_SERVER_PORT_PROPERTY, availablePort)); + + waitForServerToStart("localhost", availablePort); + + System.setProperty(GEMFIRE_CACHE_SERVER_PORT_PROPERTY, String.valueOf(availablePort)); + } + + @AfterClass + public static void stopGemFireServer() { + System.clearProperty(GEMFIRE_CACHE_SERVER_PORT_PROPERTY); + stop(gemfireServer); + } + + @Resource(name = "LocalRegion") + private Region localClientProxyRegion; + + @Resource(name = "PartitionRegion") + private Region partitionClientProxyRegion; + + @Resource(name = "ReplicateRegion") + private Region replicateClientProxyRegion; + + private void assertRegion(Region region, String expectedName) { + + assertThat(region).isNotNull(); + assertThat(region.getName()).isEqualTo(expectedName); + assertThat(region.getFullPath()).isEqualTo(RegionUtils.toRegionPath(expectedName)); + assertThat(region.getAttributes()).isNotNull(); + assertThat(region.getAttributes().getDataPolicy()).isEqualTo(DataPolicy.EMPTY); + } + + @Test + public void clusterRegionsExistOnClient() { + + assertRegion(this.localClientProxyRegion, "LocalRegion"); + assertRegion(this.partitionClientProxyRegion, "PartitionRegion"); + assertRegion(this.replicateClientProxyRegion, "ReplicateRegion"); + } + + @ClientCacheApplication(logLevel = GEMFIRE_LOG_LEVEL) + @EnableClusterDefinedRegions + static class ClientTestConfiguration { + + @Bean + static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + ClientCacheConfigurer clientCachePoolPortConfigurer( + @Value("${" + GEMFIRE_CACHE_SERVER_PORT_PROPERTY + ":40404}") int port) { + + return (bean, clientCacheFactoryBean) -> clientCacheFactoryBean.setServers( + Collections.singletonList(new ConnectionEndpoint("localhost", port))); + } + } + + @CacheServerApplication(name = "EnableClusterDefinedRegionsIntegrationTests", + logLevel = GEMFIRE_LOG_LEVEL) + static class ServerTestConfiguration { + + public static void main(String[] args) { + + AnnotationConfigApplicationContext applicationContext = + new AnnotationConfigApplicationContext(ServerTestConfiguration.class); + + applicationContext.registerShutdownHook(); + } + + @Bean("LocalRegion") + public LocalRegionFactoryBean localRegion(GemFireCache gemfireCache) { + + LocalRegionFactoryBean localRegion = new LocalRegionFactoryBean<>(); + + localRegion.setCache(gemfireCache); + localRegion.setClose(false); + localRegion.setPersistent(false); + + return localRegion; + } + + @Bean("PartitionRegion") + public PartitionedRegionFactoryBean partitionRegion(GemFireCache gemfireCache) { + + PartitionedRegionFactoryBean partitionRegion = new PartitionedRegionFactoryBean<>(); + + partitionRegion.setCache(gemfireCache); + partitionRegion.setClose(false); + partitionRegion.setPersistent(false); + + return partitionRegion; + } + + @Bean("ReplicateRegion") + public ReplicatedRegionFactoryBean replicateRegion(GemFireCache gemfireCache) { + + ReplicatedRegionFactoryBean replicateRegion = new ReplicatedRegionFactoryBean<>(); + + replicateRegion.setCache(gemfireCache); + replicateRegion.setClose(false); + replicateRegion.setPersistent(false); + + return replicateRegion; + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/EnableClusterDefinedRegionsUnitTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableClusterDefinedRegionsUnitTests.java new file mode 100644 index 00000000..7200172d --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableClusterDefinedRegionsUnitTests.java @@ -0,0 +1,138 @@ +/* + * 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.config.annotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Map; + +import org.apache.geode.cache.Cache; +import org.apache.geode.cache.client.ClientCache; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.junit.Test; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.gemfire.client.GemfireDataSourcePostProcessor; + +/** + * Unit tests for {@link EnableClusterDefinedRegions} and {@link ClusterDefinedRegionsConfiguration}. + * + * @author John Blum + * @see org.junit.Test + * @see org.apache.geode.cache.Cache + * @see org.apache.geode.cache.client.ClientCache + * @see org.apache.geode.cache.client.ClientRegionShortcut + * @see org.springframework.core.type.AnnotationMetadata + * @see org.springframework.data.gemfire.client.GemfireDataSourcePostProcessor + * @see org.springframework.data.gemfire.config.annotation.ClusterDefinedRegionsConfiguration + * @see org.springframework.data.gemfire.config.annotation.EnableClusterDefinedRegions + * @since 2.1.0 + */ +public class EnableClusterDefinedRegionsUnitTests { + + @Test + public void configuresClientRegionShortcutUsingAnnotationMetadata() { + + Map enableClusterDefinedRegionsAttributes = + Collections.singletonMap("clientRegionShortcut", ClientRegionShortcut.LOCAL); + + AnnotationMetadata mockAnnotationMetadata = mock(AnnotationMetadata.class); + + when(mockAnnotationMetadata.getAnnotationAttributes(EnableClusterDefinedRegions.class.getName())) + .thenReturn(enableClusterDefinedRegionsAttributes); + + ClusterDefinedRegionsConfiguration configuration = new ClusterDefinedRegionsConfiguration(); + + configuration.setImportMetadata(mockAnnotationMetadata); + + assertThat(configuration.resolveClientRegionShortcut()).isEqualTo(ClientRegionShortcut.LOCAL); + } + + @Test + public void setGetAndResolveClientRegionShortcut() { + + ClusterDefinedRegionsConfiguration configuration = new ClusterDefinedRegionsConfiguration(); + + assertThat(configuration.getClientRegionShortcut().orElse(null)).isEqualTo(ClientRegionShortcut.PROXY); + assertThat(configuration.resolveClientRegionShortcut()).isEqualTo(ClientRegionShortcut.PROXY); + + configuration.setClientRegionShortcut(ClientRegionShortcut.CACHING_PROXY); + + assertThat(configuration.getClientRegionShortcut().orElse(null)).isEqualTo(ClientRegionShortcut.CACHING_PROXY); + assertThat(configuration.resolveClientRegionShortcut()).isEqualTo(ClientRegionShortcut.CACHING_PROXY); + + configuration.setClientRegionShortcut(ClientRegionShortcut.LOCAL); + + assertThat(configuration.getClientRegionShortcut().orElse(null)).isEqualTo(ClientRegionShortcut.LOCAL); + assertThat(configuration.resolveClientRegionShortcut()).isEqualTo(ClientRegionShortcut.LOCAL); + + configuration.setClientRegionShortcut(null); + + assertThat(configuration.getClientRegionShortcut().orElse(null)).isNull(); + assertThat(configuration.resolveClientRegionShortcut()).isEqualTo(ClientRegionShortcut.PROXY); + } + + @Test + public void configureGemfireDataSourcePostProcessor() { + + ClientCache mockClientCache = mock(ClientCache.class); + + ClusterDefinedRegionsConfiguration configuration = new ClusterDefinedRegionsConfiguration(); + + configuration.setClientRegionShortcut(ClientRegionShortcut.CACHING_PROXY); + + GemfireDataSourcePostProcessor postProcessor = configuration.gemfireDataSourcePostProcessor(mockClientCache); + + assertThat(postProcessor.getClientRegionShortcut().orElse(null)) + .isEqualTo(ClientRegionShortcut.CACHING_PROXY); + } + + @Test(expected = IllegalArgumentException.class) + public void configureGemfireDataSourcePostProcessorWithNullCache() { + + try { + new ClusterDefinedRegionsConfiguration().gemfireDataSourcePostProcessor(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("GemFireCache must be an instance of ClientCache"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void configureGemfireDataSourcePostProcessorWithPeerCache() { + + try { + + Cache mockPeerCache = mock(Cache.class); + + new ClusterDefinedRegionsConfiguration().gemfireDataSourcePostProcessor(mockPeerCache); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("GemFireCache must be an instance of ClientCache"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } +}