SGF-637 - Improve IndexFactoryBean's resilience and options for handling GemFire IndexExistsExceptions and IndexNameConflictExceptions.

Related JIRA: https://jira.spring.io/browse/DATAGEODE-14
This commit is contained in:
John Blum
2017-06-22 16:21:11 -07:00
parent 8e2689858b
commit 0db66018eb
19 changed files with 2528 additions and 917 deletions

View File

@@ -1,25 +1,238 @@
[[bootstrap:indexing]]
= Configuring an Index
Pivotal GemFire allows Indexes (or Indices) to be created to improve the performance of OQL queries.
Pivotal GemFire allows Indexes (or Indices) to be created on Region data to improve the performance of OQL queries.
In _Spring Data GemFire_, Indexes are declared with the `index` element:
In _Spring Data GemFire_ (SDG), Indexes are declared with the `index` element:
[source,xml]
----
<gfe:index id="myIndex" expression="someField" from="/SomeRegion"/>
<gfe:index id="myIndex" expression="someField" from="/SomeRegion" type="HASH"/>
----
Before creating the `Index`, _Spring Data GemFire_ will verify whether an `Index` with the same name already exists.
By default, SDG overrides an existing `Index` if an `Index` with the same name already exists. The existing Index
is overridden by removing the old `Index` first followed by creating a new `Index` with the same name defined by
the new bean definition, regardless if the old `Index` definition was the same or not.
In _Spring Data GemFire's_ XML schema (a.k.a. SDG namespace), `Index` bean declarations are not bound to a _Region_,
unlike GemFire's native `cache.xml`. Rather, they are top-level elements just like `&lt;gfe:cache&gt;`. This allows
a developer to declare any number of Indexes on any _Region_ whether they were just created or already exist,
a significant improvement over GemFire's native `cache.xml` format.
To prevent the named `Index` definition change, especially when the old and new `Index` definitions differ
in a significant way, then set the `override` attribute to *false*, which effectively returns the existing
`Index` definition given the same name.
An `Index` must have a name. A developer may give the `Index` an explicit name using the `name` attribute,
otherwise the _bean name_ (i.e. value of the `id` attribute) of the `Index` bean definition is used as
the `Index` name.
`Index` declarations are not bound to a Region but rather are top-level elements (just like `cache`).
This allows one to declare any number of Indices on any Region whether they are just created or already exist
- an improvement over GemFire's native `cache.xml`. By default, the `Index` relies on the default cache declaration
but one can customize it accordingly or use a Pool (if need be) - see the XML schema for a full set of options.
The `expression` and `from` clause form the main components of an `Index`, identifying the data to index
(i.e. the _Region_ identified in the `from` clause) along with what criteria (i.e. `expression`) is used
to index the data. The `expression` should be based on what application domain object fields are used
in the predicate of application-defined OQL queries used to query and lookup the objects stored
in the _Region_.
For example, if I have a `Customer` that has a `lastName` property...
[source,java]
----
@Region("Customers")
class Customer {
@Id
Long id;
String lastName;
String firstName;
...
}
----
And, I also have an application defined SD[G] _Repository_ to query for `Customers`...
[source,java]
----
interface CustomerRepository extends GemfireRepository<Customer, Long> {
Customer findByLastName(String lastName);
...
}
----
Then, the SD[G] _Repository_ finder/query method would result in the following OQL statement being executed...
[source,java]
----
SELECT * FROM /Customers c WHERE c.lastName = '$1'
----
Therefore, I might want to create an `Index` like so...
[source,xml]
----
<gfe:index id="myIndex" name="CustomersLastNameIndex" expression="lastName" from="/Customers" type="HASH"/>
----
The `from` clause must refer to a valid, existing _Region_ and is how an `Index` gets applied to a _Region_.
This is *not* _Sprig Data GemFire_ specific; this is a feature of Pivotal GemFire.
The `Index` `type` maybe 1 of 3 enumerated values defined by _Spring Data GemFire's_
http://docs.spring.io/spring-data-gemfire/docs/current/api/org/springframework/data/gemfire/IndexType.html[IndexType]
enumeration: `FUNCTIONAL`, `HASH` and `PRIMARY_KEY`.
Each of the enumerated values correspond to one of the http://gemfire-90-javadocs.docs.pivotal.io/org/apache/geode/cache/query/QueryService.html[QueryService]
`create[|Key|Hash]Index` methods invoked when the actual `Index` is to be created (or "defined"; more on "defining"
Indexes below). For instance, if the `IndexType` is `PRIMARY_KEY`, then the
http://gemfire-90-javadocs.docs.pivotal.io/org/apache/geode/cache/query/QueryService.html#createKeyIndex-java.lang.String-java.lang.String-java.lang.String-[QueryService.createKeyIndex(..)]
is invoked to create a `KEY` `Index`.
The default is `FUNCTIONAL` and results in one of the `QueryService.createIndex(..)` methods
being invoked.
See the _Spring Data GemFire_ XML schema for a full set of options.
For more information on Indexing in Pivotal GemFire, see http://gemfire90.docs.pivotal.io/geode/developing/query_index/query_index.html[Working with Indexes]
in Pivotal GemFire's User Guide.
== Defining Indexes
In addition to creating Indexes upfront as `Index` bean definitions are processed by _Spring Data GemFire_
on _Spring_ container initialization, you may also *define* all of your application Indexes prior to creating
them by using the `define` attribute, like so...
[source,xml]
----
<gfe:index id="myDefinedIndex" expression="someField" from="/SomeRegion" define="true"/>
----
When `define` is set to `true` (defaults to `false`), this will not actually create the `Index` right then and there.
All "defined" Indexes are created all at once, when the _Spring_ `ApplicationContext` is "refreshed", or, that is,
when a `ContextRefreshedEvent` is published by the _Spring_ container. _Spring Data GemFire_ registers itself as
an `ApplicationListener` listening for the `ContextRefreshedEvent`. When fired, _Spring Data GemFire_ will call
http://gemfire-90-javadocs.docs.pivotal.io/org/apache/geode/cache/query/QueryService.html#createDefinedIndexes--[QueryService.createDefinedIndexes()].
Defining Indexes and creating them all at once helps promote speed and efficiency when creating Indexes.
See http://gemfire90.docs.pivotal.io/geode/developing/query_index/create_multiple_indexes.html[Creating Multiple Indexes at Once]
for more details.
== `IgnoreIfExists` and `Override`
Two _Spring Data GemFire_ `Index` configuration options warrant special mention here: `ignoreIfExists` and `override`.
These options correspond to the `ignore-if-exists` and `override` attributes on the `&lt;gfe:index&gt;` element
in _Spring Data GemFire's_ XML schema, respectively.
WARNING: Make sure you absolutely understand what you are doing before using either of these options. These options can
affect the performance and/or resources (e.g. memory) consumed by your application at runtime. As such, both of
these options are disabled (i.e. set to `false`) in SDG by default.
NOTE: These options are only available in _Spring Data GemFire_ and exist to workaround known limitations
with Pivotal GemFire; there are no equivalent options or functionality available in GemFire itself.
Each option significantly differs in behavior and entirely depends on the type of GemFire `Index` _Exception_ thrown.
This also means that neither option has any effect if a GemFire Index-type _Exception_ is *not* thrown. These options
are meant to specifically handle GemFire `IndexExistsExceptions` and `IndexNameConflictExceptions`, which can occur
for various, sometimes obscure reasons. But, in general...
* An http://gemfire-90-javadocs.docs.pivotal.io/org/apache/geode/cache/query/IndexExistsException.html[IndexExistsException]
is thrown when there exists another `Index` with the same definition but different name when attempting to
create an `Index`.
* An http://gemfire-90-javadocs.docs.pivotal.io/org/apache/geode/cache/query/IndexNameConflictException.html[IndexNameConflictException]
is thrown when there exists another `Index` with the same name but possibly different definition when attempting to
create an `Index`.
_Spring Data GemFire's_ default behavior is to *_fail-fast_*, always! So, neither `Index` _Exception_ will be "handled"
by default; these `Index` _Exceptions_ are simply wrapped in a SDG `GemfireIndexException` and rethrown. If you wish
for _Spring Data GemFire_ to handle them for you, then you can set either of these `Index` bean definition options.
`IgnoreIfExists` always takes *precedence* over `Override`, primarily because it uses less resources given it returns
the "existing" `Index` in both exceptional cases.
=== `IgnoreIfExists` Behavior
When an `IndexExistsException` is thrown and `ignoreIfExists` is set to `true` (or `&lt;gfe:index ignore-if-exists="true"&gt;`),
then the `Index` that would have been created by this `Index` bean definition / declaration will be "*ignored*",
and the "existing" `Index` will be returned.
There is very little consequence in returning the "existing" `Index` since the `Index` "definition" is the same,
as deemed by GemFire itself, *not* SDG.
However, this also means that *no* `Index` with the "`name`" specified in your `Index` bean definition / declaration
will "actually" exist from GemFire's perspective either (i.e. with
http://gemfire-90-javadocs.docs.pivotal.io/org/apache/geode/cache/query/QueryService.html#getIndexes--[QueryService.getIndexes()]).
Therefore, you should be careful when writing OQL query statements that use _Query Hints_, especially _Hints_ that refer
to the application `Index` being "*ignored*". Those _Query Hints_ will need to be changed.
Now, when an `IndexNameConflictException` is thrown and `ignoreIfExists` is set to `true` (or `&lt;gfe:index ignore-if-exists="true"&gt;`),
then the `Index` that would have been created by this `Index` bean definition / declaration will also be "*ignored*",
and the "existing" Index will be returned, just like when an `IndexExistsException` is thrown.
However, there is more risk in returning the "existing" `Index` and "*ignoring*" the application's definition
of the `Index` when an `IndexNameConflictException` is thrown since, for a `IndexNameConflictException`, while the "names"
of the conflicting Indexes are the same, the "definitions" could very well be different! This obviously could have
implications for OQL queries specific to the application, where you would presume the Indexes were defined specifically
with the application data access patterns and queries in mind. However, if like named Indexes differ in definition,
this might not be the case. So, make sure you verify.
NOTE: SDG makes a best effort to inform the user when the `Index` being ignored is significantly different
in its definition from the "existing" `Index`. However, in order for SDG to accomplish this, it must be able to "find"
the existing `Index`, which is looked up using the GemFire API (the only means available).
=== `Override` Behavior
When an `IndexExistsException` is thrown and `override` is set to `true` (or `&lt;gfe:index override="true"&gt;`), then
the `Index` is effectively "_renamed_". Remember, `IndexExistsExceptions` are thrown when multiple Indexes exist,
all having the same "definition" but different "names".
_Spring Data GemFire_ can only accomplish this using GemFire's API, by first "_removing_" the "existing" `Index`
and then "_recreating_" the `Index` with the *new* name. It is possible that either the remove or subsequent
create invocation could fail. There is no way to execute both actions atomically and rollback this joint operation
if either fails.
However, if it succeeds, then you have the same problem as before with the "_ignoreIfExists_" option. Any existing OQL
query statement using "_Query Hints_" referring to the old `Index` by name must be changed.
Now, when an `IndexNameConflictException` is thrown and `override` is set to `true` (or `&lt;gfe:index override="true"&gt;`),
then potentially the "existing" `Index` will be "_re-defined_". I say "potentially", because it is possible for the
"like-named", "existing" `Index` to have exactly the same definition and name when an `IndexNameConflictException`
is thrown.
If so, SDG is *smart* and will just return the "existing" Index as is, even on `override`. There is no harm in this
since both the "name" and the "definition" are exactly the same. Of course, SDG can only accomplish this when
SDG is able to "find" the "existing" `Index`, which is dependent on GemFire's APIs. If it cannot find it,
nothing happens and a SDG `GemfireIndexException` is thrown wrapping the `IndexNameConflictException`.
However, when the "definition" of the "existing" `Index` is different, then SDG will attempt to "_recreate_" the `Index`
using the `Index` definition specified in the `Index` bean definition /declaration. Make sure this is what you want
and make sure the `Index` definition matches your expectations and application requirements.
=== How does `IndexNameConflictExceptions` actually happen?
It is probably not all that uncommon for `IndexExistsExceptions` to be thrown, especially when
multiple configuration sources are used to configure GemFire (e.g. _Spring Data GemFire_, GemFire _Cluster Config_,
maybe GemFire native `cache.xml`, the API, etc, etc). You should definitely prefer 1 configuration method here
and stick with it.
_However, when does an `IndexNameConflictException` get thrown?_
One particular case is an `Index` defined on a `PARTITION` _Region_ (PR). When an `Index` is defined on
a `PARTITION` _Region_ (e.g. "X"), GemFire distributes the `Index` definition (and name) to other peer members
in the cluster that also host the same `PARTITION` _Region_ (i.e. "X"). The distribution of this `Index` definition
to and subsequent creation of this `Index` by peer members on a "need-to-know" basis (i.e. those hosting the same PR)
is performed asynchronously.
During this window of time, it is possible that these "pending" PR `Indexes` will not be identifiable by GemFire,
such as with a call to http://gemfire-90-javadocs.docs.pivotal.io/org/apache/geode/cache/query/QueryService.html#getIndexes--[QueryService.getIndexes()]
or with http://gemfire-90-javadocs.docs.pivotal.io/org/apache/geode/cache/query/QueryService.html#getIndexes-org.apache.geode.cache.Region-[QueryService.getIndexes(:Region)],
or even with http://gemfire-90-javadocs.docs.pivotal.io/org/apache/geode/cache/query/QueryService.html#getIndex-org.apache.geode.cache.Region-java.lang.String-[QueryService.getIndex(:Region, indexName:String)].
As such, the only way for SDG or other GemFire cache client applications (not involving _Spring_) to know for sure,
is to just attempt to create the `Index`. If it fails with either an `IndexNameConflictException`,
or even an `IndexExistsException`, then you will know. This is because the `QueryService` `Index` creation waits on
"pending" `Index` definitions, where as the other GemFire API calls do not.
In any case, SDG makes a best effort and attempts to inform the user what has or is happening along with
the corrective action. Given all GemFire `QueryService.createIndex(..)` methods are synchronous, "blocking" operations,
then the state of GemFire should be consistent and accessible after either of these Index-type _Exceptions_ are thrown,
in which case, SDG can inspect the state of the system and respond/act accordingly, based on the user's
desired configuration.
In all other cases, SDG will simply *_fail-fast_*!

View File

@@ -37,6 +37,14 @@ import org.springframework.dao.DataIntegrityViolationException;
@SuppressWarnings({ "serial", "unused" })
public class GemfireIndexException extends DataIntegrityViolationException {
public GemfireIndexException(Exception cause) {
this(cause.getMessage(), cause);
}
public GemfireIndexException(String message, Exception cause) {
super(message, cause);
}
public GemfireIndexException(IndexCreationException cause) {
this(cause.getMessage(), cause);
}
@@ -76,5 +84,4 @@ public class GemfireIndexException extends DataIntegrityViolationException {
public GemfireIndexException(String message, IndexNameConflictException cause) {
super(message, cause);
}
}

View File

@@ -20,19 +20,20 @@ import static java.util.stream.StreamSupport.stream;
import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeCollection;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeIterable;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionService;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.query.Index;
import org.apache.geode.cache.query.IndexExistsException;
import org.apache.geode.cache.query.IndexInvalidException;
import org.apache.geode.cache.query.IndexNameConflictException;
import org.apache.geode.cache.query.IndexStatistics;
import org.apache.geode.cache.query.QueryService;
@@ -42,7 +43,6 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.data.gemfire.config.annotation.IndexConfigurer;
import org.springframework.data.gemfire.config.xml.GemfireConstants;
import org.springframework.data.gemfire.support.AbstractFactoryBeanSupport;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -60,6 +60,7 @@ import org.springframework.util.StringUtils;
* @see org.apache.geode.cache.query.QueryService
* @see org.springframework.beans.factory.FactoryBean
* @see org.springframework.beans.factory.InitializingBean
* @see org.springframework.beans.factory.config.ConfigurableBeanFactory
* @see org.springframework.data.gemfire.IndexFactoryBean.IndexWrapper
* @see org.springframework.data.gemfire.config.annotation.IndexConfigurer
* @see org.springframework.data.gemfire.support.AbstractFactoryBeanSupport
@@ -67,8 +68,14 @@ import org.springframework.util.StringUtils;
*/
public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implements InitializingBean {
public static final String BASIC_INDEX_DEFINITION = "{ expression = '%1$s', from = '%2$s', type = %3$s }";
public static final String DETAILED_INDEX_DEFINITION =
"{ name = '%1$s', expression = '%2$s', from = '%3$s', imports = '%4$s', type = %5$s }";
private boolean define = false;
private boolean override = true;
private boolean ignoreIfExists = false;
private boolean override = false;
private Index index;
@@ -77,7 +84,7 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
//@Autowired(required = false)
private List<IndexConfigurer> indexConfigurers = Collections.emptyList();
private IndexConfigurer compositeIndexConfigurer = new IndexConfigurer() {
private final IndexConfigurer compositeIndexConfigurer = new IndexConfigurer() {
@Override
public void configure(String beanName, IndexFactoryBean bean) {
@@ -101,25 +108,18 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
@Override
public void afterPropertiesSet() throws Exception {
this.indexName = Optional.ofNullable(this.name).filter(StringUtils::hasText).orElse(getBeanName());
Assert.hasText(this.indexName, "Index name is required");
this.indexName = resolveIndexName();
applyIndexConfigurers(this.indexName);
Assert.notNull(cache, "Cache is required");
this.cache = resolveCache();
this.queryService = resolveQueryService();
this.queryService = lookupQueryService();
assertIndexDefinitionConfiguration();
Assert.notNull(queryService, "QueryService is required to create an Index");
Assert.hasText(expression, "Index expression is required");
Assert.hasText(from, "Index from clause is required");
this.index = createIndex(this.queryService, this.indexName);
if (IndexType.isKey(indexType)) {
Assert.isNull(imports, "imports are not supported with a KEY Index");
}
this.index = createIndex(queryService, indexName);
registerAlias(getBeanName(), this.indexName);
}
/* (non-Javadoc) */
@@ -155,47 +155,93 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
.forEach(indexConfigurer -> indexConfigurer.configure(indexName, this));
}
/* (non-Javadoc) */
private void assertIndexDefinitionConfiguration() {
Assert.hasText(this.expression, "Index expression is required");
Assert.hasText(this.from, "Index from clause is required");
if (IndexType.isKey(this.indexType)) {
Assert.isTrue(StringUtils.isEmpty(this.imports), "Imports are not supported with a KEY Index");
}
}
/* (non-Javadoc) */
RegionService resolveCache() {
return Optional.ofNullable(this.cache)
.orElseGet(() -> Optional.ofNullable(GemfireUtils.resolveGemFireCache())
.orElseThrow(() -> newIllegalStateException("Cache is required")));
}
/* (non-Javadoc) */
String resolveIndexName() {
return Optional.ofNullable(this.name).filter(StringUtils::hasText)
.orElseGet(() -> Optional.ofNullable(getBeanName()).filter(StringUtils::hasText)
.orElseThrow(() -> newIllegalArgumentException("Index name is required")));
}
/* (non-Javadoc) */
QueryService resolveQueryService() {
return Optional.ofNullable(this.queryService)
.orElseGet(() -> Optional.ofNullable(lookupQueryService())
.orElseThrow(() -> newIllegalStateException("QueryService is required to create an Index")));
}
/* (non-Javadoc) */
QueryService lookupQueryService() {
String queryServiceBeanName = GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE;
return Optional.ofNullable(getBeanFactory())
.filter(beanFactory -> beanFactory.containsBean(queryServiceBeanName))
.map(beanFactory -> beanFactory.getBean(queryServiceBeanName, QueryService.class))
.orElseGet(() -> registerQueryServiceBean(queryServiceBeanName, doLookupQueryService()));
}
/* (non-Javadoc) */
QueryService doLookupQueryService() {
return Optional.ofNullable(this.queryService).orElseGet(() ->
(this.cache instanceof ClientCache ? ((ClientCache) this.cache).getLocalQueryService()
: this.cache.getQueryService()));
}
/* (non-Javadoc) */
QueryService lookupQueryService() {
if (getBeanFactory().containsBean(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE)) {
return getBeanFactory().getBean(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE,
QueryService.class);
}
else {
return registerQueryServiceBean(doLookupQueryService());
}
}
/* (non-Javadoc) */
QueryService registerQueryServiceBean(QueryService queryService) {
QueryService registerQueryServiceBean(String beanName, QueryService queryService) {
if (isDefine()) {
((ConfigurableBeanFactory) getBeanFactory()).registerSingleton(
GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE, queryService);
((ConfigurableBeanFactory) getBeanFactory()).registerSingleton(beanName, queryService);
}
return queryService;
}
/* (non-Javadoc) */
void registerAlias(String beanName, String indexName) {
Optional.ofNullable(getBeanFactory()).filter(it -> it instanceof ConfigurableBeanFactory)
.filter(it -> (beanName != null && !beanName.equals(indexName)))
.ifPresent(it -> ((ConfigurableBeanFactory) it).registerAlias(beanName, indexName));
}
/* (non-Javadoc) */
Index createIndex(QueryService queryService, String indexName) throws Exception {
return createIndex(queryService, indexName, false);
}
Index existingIndex = getExistingIndex(queryService, indexName);
/* (non-Javadoc) */
private Index createIndex(QueryService queryService, String indexName, boolean retryAttempted) throws Exception {
if (existingIndex != null) {
if (override) {
queryService.removeIndex(existingIndex);
}
else {
return existingIndex;
}
}
IndexType indexType = this.indexType;
String expression = this.expression;
String from = this.from;
String imports = this.imports;
try {
if (IndexType.isKey(indexType)) {
@@ -208,32 +254,177 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
return createFunctionalIndex(queryService, indexName, expression, from, imports);
}
}
catch (IndexExistsException e) {
throw new GemfireIndexException(String.format(
"An Index with a different name having the same definition as this Index (%1$s) already exists",
indexName), e);
}
catch (IndexNameConflictException e) {
// NOTE technically, the only way for an IndexNameConflictException to be thrown is if
// queryService.remove(existingIndex) above silently fails, since otherwise, when override is 'false',
// the existingIndex is already being returned. Given this state of affairs, an Index with the provided
// name is unresolvable based on what the user intended to happen, so just rethrow an Exception.
throw new GemfireIndexException(String.format(
"Failed to remove the existing Index%1$sbefore re-creating Index with name (%2$s)",
(override ? " on override " : " "), indexName), e);
}
catch (Exception e) {
if (existingIndex != null) {
Collection<Index> indexes = queryService.getIndexes();
catch (IndexExistsException cause) {
if (CollectionUtils.isEmpty(indexes) || !indexes.contains(existingIndex)) {
queryService.getIndexes().add(existingIndex);
return existingIndex;
}
}
// Same definition, different name
throw e;
Optional<Index> existingIndexByDefinition =
tryToFindExistingIndexByDefinition(queryService, expression, from, indexType);
return existingIndexByDefinition.filter(existingIndex -> isIgnoreIfExists())
.map(existingIndex -> {
logWarning("WARNING! You are choosing to ignore this Index [%1$s] and return the existing"
+ " Index having the same basic definition [%2$s] but with a different name [%3$s];"
+ " Make sure no OQL Query Hints refer to this Index by name [%1$s]",
indexName, toBasicIndexDefinition(), existingIndex.getName());
return handleIgnore(existingIndex);
}).orElseGet(() ->
existingIndexByDefinition.filter(it -> !retryAttempted && isOverride())
.map(existingIndex -> {
// Log an informational warning to caution the user about using the override
logWarning("WARNING! You are attempting to 'override' an existing Index [%1$s]"
+ " having the same basic definition [%2$s] as the Index that will be created"
+ " by this IndexFactoryBean [%3$s]; 'Override' effectively 'renames' the existing"
+ " Index [%1$s] by removing it then recreating it under the new name [%3$s] with"
+ " the same definition; You should be careful to update any existing OQL Query"
+ " Hints referring to the old Index name [%1$s] to now use the new name [%3$s]",
existingIndex.getName(), toBasicIndexDefinition(), indexName);
return handleOverride(existingIndex, queryService, indexName);
}).orElseThrow(() -> {
String existingIndexName = existingIndexByDefinition.map(Index::getName)
.orElse("unknown");
return new GemfireIndexException(String.format(
"An Index with a different name [%1$s] having the same definition [%2$s] already exists;"
+ " You may attempt to override the existing Index [%1$s] with the new name [%3$s]"
+ " by setting the 'override' property to 'true'",
existingIndexName, toBasicIndexDefinition(), indexName), cause);
})
);
}
catch (IndexNameConflictException cause) {
// Same name; possibly different definition
Optional<Index> existingIndexByName = tryToFindExistingIndexByName(queryService, indexName);
return existingIndexByName.filter(existingIndex -> isIgnoreIfExists())
.map(existingIndex ->
handleIgnore(warnOnIndexDefinitionMismatch(existingIndex, indexName, "Returning"))
).orElseGet(() ->
existingIndexByName.filter(it -> !retryAttempted && isOverride())
.map(existingIndex ->
handleSmartOverride(warnOnIndexDefinitionMismatch(existingIndex, indexName,
"Overriding"), queryService, indexName)
).orElseThrow(() -> {
String existingIndexDefinition = existingIndexByName
.map(it -> String.format(DETAILED_INDEX_DEFINITION, it.getName(),
it.getIndexedExpression(), it.getFromClause(), "unknown", it.getType()))
.orElse("unknown");
return new GemfireIndexException(String.format(
"An Index with the same name [%1$s] having possibly a different definition already exists;"
+ " you may choose to ignore this Index definition [%2$s] and use the existing Index"
+ " definition [%3$s] by setting the 'ignoreIfExists' property to 'true'",
indexName, toDetailedIndexDefinition(), existingIndexDefinition), cause);
})
);
}
catch (Exception cause) {
throw new GemfireIndexException(String.format("Failed to create Index [%s]",
toDetailedIndexDefinition()), cause);
}
}
/* (non-Javadoc) */
@SuppressWarnings("all")
private boolean isIndexDefinitionMatch(Index index) {
return Optional.ofNullable(index)
.map(it -> {
IndexType thisIndexType = Optional.ofNullable(this.indexType).orElse(IndexType.FUNCTIONAL);
boolean result = ObjectUtils.nullSafeEquals(it.getIndexedExpression(), this.expression)
&& ObjectUtils.nullSafeEquals(it.getFromClause(), this.from)
&& ObjectUtils.nullSafeEquals(IndexType.valueOf(it.getType()), thisIndexType);
return result;
})
.orElse(false);
}
/* (non-Javadoc) */
private boolean isNotIndexDefinitionMatch(Index index) {
return !isIndexDefinitionMatch(index);
}
/* (non-Javadoc) */
private Index warnOnIndexDefinitionMismatch(Index existingIndex, String indexName, String action) {
if (isNotIndexDefinitionMatch(existingIndex)) {
String existingIndexDefinition = String.format(BASIC_INDEX_DEFINITION, existingIndex.getIndexedExpression(),
existingIndex.getFromClause(), IndexType.valueOf(existingIndex.getType()));
logWarning("WARNING! %1$s existing Index [%2$s] having a definition [%3$s]"
+ " that does not match the Index defined [%4$s] by this IndexFactoryBean [%5$s]",
action, existingIndex.getName(), existingIndexDefinition, toBasicIndexDefinition(), indexName);
}
return existingIndex;
}
/* (non-Javadoc) */
private Index handleIgnore(Index existingIndex) {
registerAlias(getBeanName(), existingIndex.getName());
return existingIndex;
}
/* (non-Javadoc) */
private Index handleOverride(Index existingIndex, QueryService queryService, String indexName) {
try {
// No way to tell whether the QueryService.remove(:Index) was successful or not! o.O
// Should return a boolean! Does it throw an RuntimeException? Javadoc is useless; #sigh
queryService.removeIndex(existingIndex);
return createIndex(queryService, indexName, true);
}
catch (Exception cause) {
throw new GemfireIndexException(String.format(
"Attempt to 'override' existing Index [%1$s] with the Index that would be created"
+ " by this IndexFactoryBean [%2$s] failed; you should verify the state of"
+ " your system and make sure the previously existing Index [%1$s] still exits",
existingIndex.getName(), indexName), cause);
}
}
/* (non-Javadoc) */
private Index handleSmartOverride(Index existingIndex, QueryService queryService, String indexName) {
return Optional.of(existingIndex)
.filter(it -> it.getName().equalsIgnoreCase(indexName))
.filter(it -> isIndexDefinitionMatch(existingIndex))
.orElseGet(() -> handleOverride(existingIndex, queryService, indexName));
}
/* (non-Javadoc) */
String toBasicIndexDefinition() {
return String.format(BASIC_INDEX_DEFINITION, this.expression, this.from, this.indexType);
}
/* (non-Javadoc) */
String toDetailedIndexDefinition() {
return String.format(DETAILED_INDEX_DEFINITION,
this.name, this.expression, this.from, this.imports, this.indexType);
}
/* (non-Javadoc) */
@@ -249,8 +440,8 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
}
/* (non-Javadoc) */
Index createHashIndex(QueryService queryService, String indexName, String expression, String from,
String imports) throws Exception {
Index createHashIndex(QueryService queryService, String indexName, String expression, String from, String imports)
throws Exception {
boolean hasImports = StringUtils.hasText(imports);
@@ -301,15 +492,31 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
}
/* (non-Javadoc) */
Index getExistingIndex(QueryService queryService, String indexName) {
Optional<Index> tryToFindExistingIndexByDefinition(QueryService queryService,
String expression, String fromClause, IndexType indexType) {
for (Index index : nullSafeCollection(queryService.getIndexes())) {
if (index.getName().equalsIgnoreCase(indexName)) {
return index;
if (index.getIndexedExpression().equalsIgnoreCase(expression)
&& index.getFromClause().equalsIgnoreCase(fromClause)
&& indexType.equals(IndexType.valueOf(index.getType()))) {
return Optional.of(index);
}
}
return null;
return Optional.empty();
}
/* (non-Javadoc) */
Optional<Index> tryToFindExistingIndexByName(QueryService queryService, String indexName) {
for (Index index : nullSafeCollection(queryService.getIndexes())) {
if (index.getName().equalsIgnoreCase(indexName)) {
return Optional.of(index);
}
}
return Optional.empty();
}
/**
@@ -323,13 +530,23 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
return this.compositeIndexConfigurer;
}
/**
* Returns a reference to the {@link Index} created by this {@link IndexFactoryBean}.
*
* @return a reference to the {@link Index} created by this {@link IndexFactoryBean}.
* @see org.apache.geode.cache.query.Index
*/
public Index getIndex() {
return this.index;
}
/**
* @inheritDoc
*/
@Override
public Index getObject() {
return Optional.ofNullable(this.index)
.orElseGet(() -> this.index = getExistingIndex(queryService, indexName));
return Optional.ofNullable(getIndex()).orElseGet(() ->
this.index = tryToFindExistingIndexByName(resolveQueryService(), resolveIndexName()).orElse(null));
}
/**
@@ -338,74 +555,151 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
@Override
@SuppressWarnings("unchecked")
public Class<?> getObjectType() {
return Optional.ofNullable(this.index).map(Index::getClass).orElse((Class) Index.class);
return Optional.ofNullable(getIndex()).map(Index::getClass).orElse((Class) Index.class);
}
/**
* Sets the underlying cache used for creating indexes.
* Sets a reference to the {@link RegionService}.
*
* @param cache cache used for creating indexes.
* @param cache reference to the {@link RegionService}.
* @see org.apache.geode.cache.RegionService
*/
public void setCache(RegionService cache) {
this.cache = cache;
}
/**
* Sets the query service used for creating indexes.
* Sets the name of the {@link Index}.
*
* @param service query service used for creating indexes.
*/
public void setQueryService(QueryService service) {
this.queryService = service;
}
/**
* @param name the name to set
* @param name {@link String} containing the name given to the {@link Index}.
*/
public void setName(String name) {
this.name = name;
}
/**
* Sets a boolean condition to indicate whether the Index declared and defined by this FactoryBean will only be
* defined initially, or defined and created. If defined-only, the IndexFactoryBean will receive a callback at
* the end of the Spring container lifecycle to subsequently "create" all "defined-only" Indexes once, in a
* single operation.
* Sets the {@link QueryService} used to create the {@link Index}.
*
* @param define a boolean value indicating the define or define/create status. If true, the Index declared
* by this FactoryBean will only be defined initially and subsequently created when this SmartLifecycle bean
* receives an appropriate callback from the Spring container; if false, the Index will be created immediately.
* @param service {@link QueryService} used to create the {@link Index}.
* @see org.apache.geode.cache.query.QueryService
*/
public void setQueryService(QueryService service) {
this.queryService = service;
}
/**
* Sets a boolean condition to indicate whether the {@link Index} declared and defined by this
* {@link IndexFactoryBean} will only be defined initially, or defined and created. If defined-only,
* the {@link IndexFactoryBean} will receive a callback at the end of the Spring container lifecycle
* to subsequently "create" all "defined-only" {@link Index Indexes} once, in a single operation.
*
* @param define a boolean value indicating the define or define/create status. If {@literal true},
* the {@link Index} declared by this {@link IndexFactoryBean} will only be defined initially
* and subsequently created when this bean receives an appropriate callback from the Spring container;
* if {@literal false}, the {@link Index} will be created immediately.
*/
public void setDefine(boolean define) {
this.define = define;
}
/* (non-Javadoc) */
/**
* Returns a boolean indicating whether the {@link Index} declared and defined by this {@link IndexFactoryBean}
* will only be defined initially, or defined and created. If defined-only, the {@link IndexFactoryBean}
* will receive a callback at the end of the Spring container lifecycle to subsequently "create" all "defined-only"
* {@link Index Indexes} once, in a single operation.
*
* @return a boolean value indicating the define or define/create status. If {@literal true}, the {@link Index}
* declared by this {@link IndexFactoryBean} will only be defined initially and subsequently created when this bean
* receives an appropriate callback from the Spring container; if {@literal false}, the {@link Index}
* will be created immediately.
*/
protected boolean isDefine() {
return define;
}
/**
* @param expression the expression to set
* @param expression Index expression to set
*/
public void setExpression(String expression) {
this.expression = expression;
}
/**
* @param from the from to set
* @param from Index from clause to set
*/
public void setFrom(String from) {
this.from = from;
}
/**
* @param imports the imports to set
* @param imports Index imports to set
*/
public void setImports(String imports) {
this.imports = imports;
}
/**
* Configures whether to ignore the {@link Index} defined by this {@link IndexFactoryBean}
* when an {@link IndexExistsException} or {@link IndexNameConflictException} is thrown.
*
* An {@link IndexExistsException} is thrown when there exists another {@link Index} with the same definition
* but with another name.
*
* An {@link IndexNameConflictException} is thrown when there exists another {@link Index} with the same name
* but possibly a different definition.
*
* When {@literal ignoreIfExists} is set to {@literal true} and an {@link IndexExistsException} is thrown,
* then the existing {@link Index} will be returned as the object of this {@link IndexFactoryBean} creation
* and the name of the existing {@link Index} is added as an alias for this bean.
*
* When {@literal ignoreIfExists} is set to {@literal true} and {@link IndexNameConflictException} is thrown,
* then the existing {@link Index} will be returned as the object of this {@link IndexFactoryBean} creation.
* A warning is logged if the definition of this {@link IndexFactoryBean} and the existing {@link Index}
* are different.
*
* {@literal ignoreIfExists} takes precedence over {@link #isOverride() override}.
*
* Defaults to {@literal false}.
*
* @param ignore boolean value indicating whether to ignore the {@link Index} defined by
* this {@link IndexFactoryBean}. Default is {@literal false}.
* @see #setOverride(boolean)
*/
public void setIgnoreIfExists(boolean ignore) {
this.ignoreIfExists = ignore;
}
/**
* Determines whether to ignore the {@link Index} defined by this {@link IndexFactoryBean}
* when an {@link IndexExistsException} or {@link IndexNameConflictException} is thrown.
*
* An {@link IndexExistsException} is thrown when there exists another {@link Index} with the same definition
* but with another name.
*
* An {@link IndexNameConflictException} is thrown when there exists another {@link Index} with the same name
* but possibly a different definition.
*
* When {@literal ignoreIfExists} is set to {@literal true} and an {@link IndexExistsException} is thrown,
* then the existing {@link Index} will be returned as the object of this {@link IndexFactoryBean} creation
* and the name of the existing {@link Index} is added as an alias for this bean.
*
* When {@literal ignoreIfExists} is set to {@literal true} and {@link IndexNameConflictException} is thrown,
* then the existing {@link Index} will be returned as the object of this {@link IndexFactoryBean} creation.
* A warning is logged if the definition of this {@link IndexFactoryBean} and the existing {@link Index}
* are different.
*
* {@literal ignoreIfExists} takes precedence over {@link #isOverride() override}.
*
* Defaults to {@literal false}.
*
* @return a boolean value indicating whether to ignore the {@link Index} defined by this {@link IndexFactoryBean}.
* Default is {@literal false}.
* @see #setIgnoreIfExists(boolean)
*/
public boolean isIgnoreIfExists() {
return this.ignoreIfExists;
}
/**
* Null-safe operation to set an array of {@link IndexConfigurer IndexConfigurers} used to apply
* additional configuration to this {@link IndexFactoryBean} when using Annotation-based configuration.
@@ -432,28 +726,88 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
}
/**
* @param override the override to set
* Configures whether to override an existing {@link Index} having the same definition but different name
* as the {@link Index} that would be created by this {@link IndexFactoryBean}.
*
* An {@link IndexExistsException} is thrown when there exists another {@link Index} with the same definition
* but with another name.
*
* An {@link IndexNameConflictException} is thrown when there exists another {@link Index} with the same name
* but possibly a different definition.
*
* With {@literal override} set to {@literal true} when an {@link IndexExistsException} is thrown, then override
* is effectively the same as "renaming" the existing {@link Index}. In other words, the existing {@link Index}
* will be {@link QueryService#removeIndex(Index) removed} and recreated by this {@link IndexFactoryBean}
* under the new {@link #resolveIndexName() name} having the same definition.
*
* With {@literal override} set to {@literal true} when an {@link IndexNameConflictException} is thrown,
* then overriding the existing {@link Index} is equivalent to changing the existing {@link Index} definition.
* When this happens, a warning is logged. If the existing {@link Index} definition is the same then overriding
* effectively just rebuilds the {@link Index}.
*
* {@literal ignoreIfExists} takes precedence over {@literal override}.
*
* Defaults to {@literal false}.
*
* @param override boolean value indicating whether an existing {@link Index} will be removed and recreated
* by this {@link IndexFactoryBean}. Default is {@literal false}.
* @see #setIgnoreIfExists(boolean)
*/
public void setOverride(boolean override) {
this.override = override;
}
/**
* @param type the type to set
* Determines whether to override an existing {@link Index} having the same definition but different name
* as the {@link Index} that would be created by this {@link IndexFactoryBean}.
*
* An {@link IndexExistsException} is thrown when there exists another {@link Index} with the same definition
* but with another name.
*
* An {@link IndexNameConflictException} is thrown when there exists another {@link Index} with the same name
* but possibly a different definition.
*
* With {@literal override} set to {@literal true} when an {@link IndexExistsException} is thrown, then override
* is effectively the same as "renaming" the existing {@link Index}. In other words, the existing {@link Index}
* will be {@link QueryService#removeIndex(Index) removed} and recreated by this {@link IndexFactoryBean}
* under the new {@link #resolveIndexName() name} having the same definition.
*
* With {@literal override} set to {@literal true} when an {@link IndexNameConflictException} is thrown,
* then overriding the existing {@link Index} is equivalent to changing the existing {@link Index} definition.
* When this happens, a warning is logged. If the existing {@link Index} definition is the same then overriding
* effectively just rebuilds the {@link Index}.
*
* {@literal ignoreIfExists} takes precedence over {@literal override}.
*
* Defaults to {@literal false}.
*
* @return a boolean value indicating whether an existing {@link Index} will be removed and recreated
* by this {@link IndexFactoryBean}. Default is {@literal false}.
* @see #setOverride(boolean)
*/
public boolean isOverride() {
return this.override;
}
/**
* Set the {@link IndexType type} of the {@link Index} as a {@link String}.
*
* @param type {@link String} specifying the {@link IndexType type} of the {@link Index}.
* @see org.springframework.data.gemfire.IndexType#valueOf(String)
* @see #setType(IndexType)
*/
public void setType(String type) {
setType(IndexType.valueOfIgnoreCase(type));
}
/**
* Sets the type of GemFire Index to create.
* Set the {@link IndexType type} of the {@link Index}.
*
* @param indexType the IndexType enumerated value indicating the type of GemFire Index
* that will be created by this Spring FactoryBean.
* @param type {@link IndexType} indicating the type of the {@link Index}.
* @see org.springframework.data.gemfire.IndexType
*/
public void setType(IndexType indexType) {
this.indexType = indexType;
public void setType(IndexType type) {
this.indexType = type;
}
/* (non-Javadoc) */
@@ -466,96 +820,109 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
private final String indexName;
protected IndexWrapper(QueryService queryService, String indexName) {
Assert.notNull(queryService, "QueryService must not be null");
Assert.hasText(indexName, "The name of the Index must be specified!");
Assert.notNull(queryService, "QueryService is required");
Assert.hasText(indexName, "Name of Index is required");
this.queryService = queryService;
this.indexName = indexName;
}
protected synchronized Index getIndex() {
if (this.index == null) {
String localIndexName = getIndexName();
/* (non-Javadoc) */
protected synchronized Index resolveIndex() {
for (Index localIndex : getQueryService().getIndexes()) {
if (localIndex.getName().equals(localIndexName)) {
this.index = localIndex;
break;
}
}
String indexName = getIndexName();
if (this.index == null) {
throw new GemfireIndexException(new IndexInvalidException(String.format(
"index with name (%1$s) was not found", localIndexName)));
}
}
return Optional.ofNullable(this.index)
.orElseGet(() -> {
return index;
AtomicReference<Index> searchResult = new AtomicReference<>();
nullSafeCollection(getQueryService().getIndexes()).forEach(index -> {
if (index.getName().equalsIgnoreCase(indexName)) {
searchResult.set(index);
}
});
return Optional.of(searchResult).map(it -> {
this.index = it.get();
return this.index;
}).orElseThrow(() -> new GemfireIndexException(
String.format("Index with name [%s] was not found", indexName), (Exception) null));
});
}
/* (non-Javadoc) */
protected Index getIndex() {
return this.index;
}
/* (non-Javadoc) */
protected String getIndexName() {
Assert.state(StringUtils.hasText(indexName), "The Index 'name' was not properly initialized!");
return indexName;
return Optional.ofNullable(this.indexName).filter(StringUtils::hasText).orElseThrow(() ->
newIllegalStateException("Index name is required"));
}
/* (non-Javadoc) */
protected QueryService getQueryService() {
return queryService;
return this.queryService;
}
@Override
public String getName() {
return getIndex().getName();
return resolveIndex().getName();
}
@Override
public String getCanonicalizedFromClause() {
return getIndex().getCanonicalizedFromClause();
return resolveIndex().getCanonicalizedFromClause();
}
@Override
public String getCanonicalizedIndexedExpression() {
return getIndex().getCanonicalizedIndexedExpression();
return resolveIndex().getCanonicalizedIndexedExpression();
}
@Override
public String getCanonicalizedProjectionAttributes() {
return getIndex().getCanonicalizedProjectionAttributes();
return resolveIndex().getCanonicalizedProjectionAttributes();
}
@Override
public String getFromClause() {
return getIndex().getFromClause();
return resolveIndex().getFromClause();
}
@Override
public String getIndexedExpression() {
return getIndex().getIndexedExpression();
return resolveIndex().getIndexedExpression();
}
@Override
public String getProjectionAttributes() {
return getIndex().getProjectionAttributes();
return resolveIndex().getProjectionAttributes();
}
@Override
public Region<?, ?> getRegion() {
return getIndex().getRegion();
return resolveIndex().getRegion();
}
@Override
public IndexStatistics getStatistics() {
return getIndex().getStatistics();
return resolveIndex().getStatistics();
}
@Override
@SuppressWarnings("deprecation")
public org.apache.geode.cache.query.IndexType getType() {
return getIndex().getType();
return resolveIndex().getType();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
if (this == obj) {
return true;
}
@@ -567,20 +934,25 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
return (getIndexName().equals(((IndexWrapper) obj).getIndexName()));
}
return getIndex().equals(obj);
return resolveIndex().equals(obj);
}
@Override
public int hashCode() {
int hashValue = 37;
hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getIndexName());
hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(index);
return hashValue;
}
@Override
public String toString() {
return (index != null ? String.valueOf(index) : getIndexName());
return Optional.ofNullable(getIndex()).map(String::valueOf)
.orElseGet(this::getIndexName);
}
}
}

View File

@@ -25,6 +25,7 @@ package org.springframework.data.gemfire;
*/
@SuppressWarnings({ "deprecation", "unused" })
public enum IndexType {
FUNCTIONAL(org.apache.geode.cache.query.IndexType.FUNCTIONAL),
HASH(org.apache.geode.cache.query.IndexType.HASH),
PRIMARY_KEY(org.apache.geode.cache.query.IndexType.PRIMARY_KEY),
@@ -49,7 +50,7 @@ public enum IndexType {
* @return a boolean value indicating whether the IndexType is a "FUNCTIONAL" Index.
* @see #isFunctional()
*/
public static boolean isFunctional(final IndexType indexType) {
public static boolean isFunctional(IndexType indexType) {
return (indexType != null && indexType.isFunctional());
}
@@ -60,7 +61,7 @@ public enum IndexType {
* @return a boolean value indicating whether the IndexType is a "HASH" Index.
* @see #isHash()
*/
public static boolean isHash(final IndexType indexType) {
public static boolean isHash(IndexType indexType) {
return (indexType != null && indexType.isHash());
}
@@ -71,7 +72,7 @@ public enum IndexType {
* @return a boolean value indicating whether the IndexType is a "KEY" Index.
* @see #isFunctional()
*/
public static boolean isKey(final IndexType indexType) {
public static boolean isKey(IndexType indexType) {
return (indexType != null && indexType.isKey());
}
@@ -84,7 +85,8 @@ public enum IndexType {
* any IndexType in this enumeration.
* @see org.apache.geode.cache.query.IndexType
*/
public static IndexType valueOf(final org.apache.geode.cache.query.IndexType gemfireIndexType) {
public static IndexType valueOf(org.apache.geode.cache.query.IndexType gemfireIndexType) {
for (IndexType indexType : values()) {
if (indexType.getGemfireIndexType().equals(gemfireIndexType)) {
return indexType;
@@ -101,7 +103,8 @@ public enum IndexType {
* @return an IndexType matching the given String.
* @see java.lang.String#equalsIgnoreCase(String)
*/
public static IndexType valueOfIgnoreCase(final String value) {
public static IndexType valueOfIgnoreCase(String value) {
for (IndexType indexType : values()) {
if (indexType.name().equalsIgnoreCase(value)) {
return indexType;
@@ -147,5 +150,4 @@ public enum IndexType {
public boolean isKey() {
return (this.equals(KEY) || this.equals(PRIMARY_KEY));
}
}

View File

@@ -136,6 +136,7 @@ public class IndexConfiguration extends EntityDefinedRegionsConfiguration {
getAnnotationAttributes(importingClassMetadata, getEnableIndexingAnnotationTypeName());
localPersistentEntity.doWithProperties((PropertyHandler<GemfirePersistentProperty>) persistentProperty -> {
persistentProperty.findAnnotation(Id.class).ifPresent(idAnnotation ->
registerIndexBeanDefinition(enableIndexingAttributes, localPersistentEntity, persistentProperty,
IndexType.KEY, idAnnotation, registry));
@@ -147,6 +148,7 @@ public class IndexConfiguration extends EntityDefinedRegionsConfiguration {
persistentProperty.findAnnotation(LuceneIndexed.class).ifPresent( luceneIndexAnnotation ->
registerLuceneIndexBeanDefinition(enableIndexingAttributes, localPersistentEntity,
persistentProperty, luceneIndexAnnotation, registry));
});
}
@@ -195,12 +197,13 @@ public class IndexConfiguration extends EntityDefinedRegionsConfiguration {
indexFactoryBeanBuilder.addPropertyValue("from",
resolveFrom(persistentEntity, persistentProperty, indexedAttributes));
indexFactoryBeanBuilder.addPropertyValue("ignoreIfExists", Boolean.TRUE);
indexFactoryBeanBuilder.addPropertyValue("indexConfigurers", resolveIndexConfigurers());
indexFactoryBeanBuilder.addPropertyValue("name", indexName);
indexFactoryBeanBuilder.addPropertyValue("override",
resolveOverride(persistentEntity, persistentProperty, indexedAttributes));
indexFactoryBeanBuilder.addPropertyValue("override", Boolean.FALSE);
indexFactoryBeanBuilder.addPropertyValue("type",
resolveType(persistentEntity, persistentProperty, indexedAttributes, indexType).toString());
@@ -325,15 +328,6 @@ public class IndexConfiguration extends EntityDefinedRegionsConfiguration {
StringUtils.capitalize(indexType.name().toLowerCase()));
}
/* (non-Javadoc) */
@SuppressWarnings("unused")
private boolean resolveOverride(GemfirePersistentEntity persistentEntity,
GemfirePersistentProperty persistentProperty, AnnotationAttributes indexedAttributes) {
return (indexedAttributes.containsKey("override")
&& indexedAttributes.getBoolean("override"));
}
/* (non-Javadoc) */
@SuppressWarnings("unused")
private IndexType resolveType(GemfirePersistentEntity<?> persistentEntity,

View File

@@ -42,8 +42,10 @@ class IndexParser extends AbstractSimpleBeanDefinitionParser {
private static final AtomicBoolean REGISTERED = new AtomicBoolean(false);
/* (non-Javadoc) */
protected static void registerDefinedIndexesApplicationListener(ParserContext parserContext) {
private static void registerDefinedIndexesApplicationListener(ParserContext parserContext) {
if (REGISTERED.compareAndSet(false, true)) {
AbstractBeanDefinition createDefinedIndexesApplicationListener = BeanDefinitionBuilder
.rootBeanDefinition(DefinedIndexesApplicationListener.class)
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE)

View File

@@ -70,13 +70,6 @@ public @interface Indexed {
*/
String from() default "";
/**
* Determines whether this given {@link Index} definition should override any existing {@link Index} definition.
*
* Defaults to {@literal false}.
*/
boolean override() default false;
/**
* Type of Index to create.
*

View File

@@ -203,4 +203,54 @@ public abstract class AbstractFactoryBeanSupport<T> implements FactoryBean<T>,
.filter(Log::isInfoEnabled)
.ifPresent(log -> log.info(message.get()));
}
/**
* Logs the {@link String message} formatted with the array of {@link Object arguments} at warn level.
*
* @param message {@link String} containing the message to log.
* @param args array of {@link Object arguments} used to format the {@code message}.
* @see #logWarning(Supplier)
*/
protected void logWarning(String message, Object... args) {
logWarning(() -> String.format(message, args));
}
/**
* Logs the {@link String message} supplied by the given {@link Supplier} at warn level.
*
* @param message {@link Supplier} containing the {@link String message} and arguments to log.
* @see org.apache.commons.logging.Log#isWarnEnabled()
* @see org.apache.commons.logging.Log#warn(Object)
* @see #getLog()
*/
protected void logWarning(Supplier<String> message) {
Optional.ofNullable(getLog())
.filter(Log::isWarnEnabled)
.ifPresent(log -> log.warn(message.get()));
}
/**
* Logs the {@link String message} formatted with the array of {@link Object arguments} at error level.
*
* @param message {@link String} containing the message to log.
* @param args array of {@link Object arguments} used to format the {@code message}.
* @see #logError(Supplier)
*/
protected void logError(String message, Object... args) {
logError(() -> String.format(message, args));
}
/**
* Logs the {@link String message} supplied by the given {@link Supplier} at error level.
*
* @param message {@link Supplier} containing the {@link String message} and arguments to log.
* @see org.apache.commons.logging.Log#isErrorEnabled()
* @see org.apache.commons.logging.Log#error(Object)
* @see #getLog()
*/
protected void logError(Supplier<String> message) {
Optional.ofNullable(getLog())
.filter(Log::isErrorEnabled)
.ifPresent(log -> log.error(message.get()));
}
}

View File

@@ -126,14 +126,14 @@ public abstract class CacheUtils extends DistributedSystemUtils {
try {
return ClientCacheFactory.getAnyInstance();
}
catch (CacheClosedException ignore) {
catch (CacheClosedException | IllegalStateException ignore) {
return null;
}
}
/* (non-Javadoc) */
public static GemFireCache resolveGemFireCache() {
return Optional.<GemFireCache>ofNullable(getCache()).orElseGet(CacheUtils::getClientCache);
return Optional.<GemFireCache>ofNullable(getClientCache()).orElseGet(CacheUtils::getCache);
}
/* (non-Javadoc) */

View File

@@ -2256,11 +2256,27 @@ Corresponds to the regionPath parameter in createIndex methods.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="imports" type="xsd:string" use="optional" />
<xsd:attribute name="override" type="xsd:string" use="optional" default="true">
<xsd:attribute name="ignore-if-exists" type="xsd:string" use="optional" default="false">
<xsd:annotation>
<xsd:documentation><![CDATA[
Indicates whether the index is created even if there is an index with the same name (default) or not.
Indicates whether this Index is ignored when there exists an Index with the same name, but possibly
different definition.
You should use this setting with care. See Javadoc for more details.
Defaults to false.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="imports" type="xsd:string" use="optional" />
<xsd:attribute name="override" type="xsd:string" use="optional" default="false">
<xsd:annotation>
<xsd:documentation><![CDATA[
Indicates whether the Index is created even if there exists an Index with the same definition, different name.
You should use this setting with care. See Javadoc for more details.
Defaults to false.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>

View File

@@ -2358,11 +2358,27 @@ Corresponds to the regionPath parameter in createIndex methods.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="imports" type="xsd:string" use="optional" />
<xsd:attribute name="override" type="xsd:string" use="optional" default="true">
<xsd:attribute name="ignore-if-exists" type="xsd:string" use="optional" default="false">
<xsd:annotation>
<xsd:documentation><![CDATA[
Indicates whether the index is created even if there is an index with the same name (default) or not.
Indicates whether this Index is ignored when there exists an Index with the same name, but possibly
different definition.
You should use this setting with care. See Javadoc for more details.
Defaults to false.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="imports" type="xsd:string" use="optional" />
<xsd:attribute name="override" type="xsd:string" use="optional" default="false">
<xsd:annotation>
<xsd:documentation><![CDATA[
Indicates whether the Index is created even if there exists an Index with the same definition, different name.
You should use this setting with care. See Javadoc for more details.
Defaults to false.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>

View File

@@ -16,25 +16,21 @@
package org.springframework.data.gemfire;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeCollection;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.GemFireCache;
import org.apache.geode.cache.query.Index;
import org.apache.geode.cache.query.IndexExistsException;
import org.apache.geode.cache.query.IndexNameConflictException;
import org.apache.geode.cache.query.QueryService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -43,26 +39,49 @@ import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Import;
/**
* The IndexConflictsIntegrationTest class...
* Integration tests with test cases testing the numerous conflicting {@link Index} configurations.
*
* An {@link IndexExistsException} is thrown when 2 or more {@link Index Indexes} share the same definition
* but have different names.
*
* An {@link IndexNameConflictException} is thrown when 2 or more {@link Index Indexes} share the same name
* but have potentially different definitions.
*
* @author John Blum
* @see org.junit.Test
* @link https://jira.spring.io/browse/SGF-432
* @see org.apache.geode.cache.GemFireCache
* @see org.apache.geode.cache.query.Index
* @see org.springframework.context.ConfigurableApplicationContext
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.Import
* @see <a href="https://jira.spring.io/browse/SGF-432">IndexFactoryBean traps IndexExistsException instead of IndexNameConflictException</a>
* @see <a href="https://jira.spring.io/browse/SGF-637">Improve IndexFactoryBean's resilience and options for handling GemFire IndexExistsExceptions and IndexNameConflictExceptions</a>
* @since 1.6.3
*/
public class IndexConflictsIntegrationTest {
protected void assertIndex(Index index, String expectedName, String expectedExpression, String expectedFromClause,
private static final AtomicBoolean IGNORE = new AtomicBoolean(false);
private static final AtomicBoolean OVERRIDE = new AtomicBoolean(false);
private ConfigurableApplicationContext applicationContext;
private void assertIndex(Index index, String expectedName, String expectedExpression, String expectedFromClause,
IndexType expectedType) {
assertThat(index, is(notNullValue()));
assertThat(index.getName(), is(equalTo(expectedName)));
assertThat(index.getIndexedExpression(), is(equalTo(expectedExpression)));
assertThat(index.getFromClause(), is(equalTo(expectedFromClause)));
assertThat(IndexType.valueOf(index.getType()), is(equalTo(expectedType)));
assertThat(index).isNotNull();
assertThat(index.getName()).isEqualTo(expectedName);
assertThat(index.getIndexedExpression()).isEqualTo(expectedExpression);
assertThat(index.getFromClause()).isEqualTo(expectedFromClause);
assertThat(IndexType.valueOf(index.getType())).isEqualTo(expectedType);
}
protected boolean close(ConfigurableApplicationContext applicationContext) {
private void assertIndexCount(int count) {
assertThat(getIndexCount()).isEqualTo(count);
}
private boolean close(ConfigurableApplicationContext applicationContext) {
if (applicationContext != null) {
applicationContext.close();
return !(applicationContext.isActive() || applicationContext.isRunning());
@@ -71,185 +90,260 @@ public class IndexConflictsIntegrationTest {
return true;
}
private Index getIndex(String indexName) {
for (Index index : nullSafeCollection(getQueryService().getIndexes())) {
if (index.getName().equalsIgnoreCase(indexName)) {
return index;
}
}
return null;
}
private int getIndexCount() {
return nullSafeCollection(getQueryService().getIndexes()).size();
}
private QueryService getQueryService() {
return this.applicationContext.getBean("gemfireCache", GemFireCache.class).getQueryService();
}
private boolean hasIndex(String indexName) {
return (getIndex(indexName) != null);
}
private ConfigurableApplicationContext newApplicationContext(Class<?>... annotatedClasses) {
return new AnnotationConfigApplicationContext(annotatedClasses);
}
@Before
public void setup() {
System.getProperties().remove("gemfire.cache.region.index.override");
assertThat(System.getProperties().containsKey("gemfire.cache.region.index.override"), is(false));
assertThat(IGNORE.get()).isFalse();
assertThat(OVERRIDE.get()).isFalse();
}
@Test(expected = BeanCreationException.class)
public void indexDefinitionConflictThrowsException() {
ConfigurableApplicationContext applicationContext = null;
@After
public void tearDown() {
OVERRIDE.set(false);
IGNORE.set(false);
assertThat(close(this.applicationContext)).isTrue();
}
@Test
public void indexDefinitionConflictIgnoresIndex() {
assertThat(IGNORE.compareAndSet(false, true)).isTrue();
this.applicationContext = newApplicationContext(IndexDefinitionConflictConfiguration.class);
assertThat(this.applicationContext.containsBean("customerIdIndex")).isTrue();
assertThat(this.applicationContext.containsBean("customerIdentifierIndex")).isTrue();
assertIndexCount(1);
assertThat(hasIndex("customerIdIndex")).isTrue();
assertThat(hasIndex("customerIdentifierIndex")).isFalse();
Index customersIdIndex = getIndex("customerIdIndex");
assertIndex(customersIdIndex, "customerIdIndex",
"id", "/Customers", IndexType.PRIMARY_KEY);
}
@Test
public void indexDefinitionConflictOverridesIndex() {
assertThat(OVERRIDE.compareAndSet(false, true)).isTrue();
this.applicationContext = newApplicationContext(IndexDefinitionConflictConfiguration.class);
assertThat(this.applicationContext.containsBean("customerIdIndex")).isTrue();
assertThat(this.applicationContext.containsBean("customerIdentifierIndex")).isTrue();
assertIndexCount(1);
assertThat(hasIndex("customerIdIndex")).isFalse();
assertThat(hasIndex("customerIdentifierIndex")).isTrue();
Index customersIdentifierIndex = getIndex("customerIdentifierIndex");
assertIndex(customersIdentifierIndex, "customerIdentifierIndex",
"id", "/Customers", IndexType.PRIMARY_KEY);
}
@Test(expected = IndexExistsException.class)
public void indexDefinitionConflictThrowsIndexExistsException() throws Throwable {
try {
applicationContext = new AnnotationConfigApplicationContext(
IndexDefinitionConflictGemFireConfiguration.class);
this.applicationContext = newApplicationContext(IndexDefinitionConflictConfiguration.class);
}
catch (BeanCreationException expected) {
assertThat(expected.getMessage(), containsString("Error creating bean with name 'customerIdentityIndex'"
+ " defined in org.springframework.data.gemfire.IndexConflictsIntegrationTest$IndexDefinitionConflictGemFireConfiguration:"
+ " Invocation of init method failed"));
assertThat(expected.getCause(), is(instanceOf(GemfireIndexException.class)));
assertThat(expected.getCause().getMessage(),
containsString("An Index with a different name having the same definition"
+ " as this Index (customerIdentityIndex) already exists"));
assertThat(expected.getCause().getCause(), is(instanceOf(IndexExistsException.class)));
assertThat(expected.getCause().getCause().getMessage(), is(equalTo("Similar Index Exists")));
throw expected;
}
finally {
assertThat(close(applicationContext), is(true));
assertThat(expected).hasMessageStartingWith("Error creating bean with name 'customerIdentifierIndex'");
assertThat(expected).hasCauseInstanceOf(GemfireIndexException.class);
String existingIndexDefinition = String.format(IndexFactoryBean.BASIC_INDEX_DEFINITION,
"id", "/Customers", IndexType.PRIMARY_KEY);
assertThat(expected.getCause()).hasMessageStartingWith(String.format(
"An Index with a different name [customerIdIndex] having the same definition [%s] already exists",
existingIndexDefinition));
assertThat(expected.getCause()).hasCauseInstanceOf(IndexExistsException.class);
assertThat(expected.getCause().getCause()).hasMessage("Similar Index Exists");
assertThat(expected.getCause().getCause()).hasNoCause();
throw expected.getCause().getCause();
}
}
@Test
public void indexNameConflictOverridesExistingIndex() {
ConfigurableApplicationContext applicationContext = null;
public void indexNameConflictIgnoresIndex() {
try {
applicationContext = new AnnotationConfigApplicationContext(IndexNameConflictGemFireConfiguration.class);
assertThat(IGNORE.compareAndSet(false, true)).isTrue();
assertThat(applicationContext.getBeansOfType(Index.class).size(), is(equalTo(2)));
this.applicationContext = newApplicationContext(IndexNameConflictConfiguration.class);
Cache gemfireCache = applicationContext.getBean("gemfireCache", Cache.class);
assertThat(this.applicationContext.containsBean("customerLastNameIndex")).isTrue();
assertThat(this.applicationContext.containsBean("customerFirstNameIndex")).isTrue();
assertIndexCount(1);
assertThat(hasIndex(IndexNameConflictConfiguration.INDEX_NAME)).isTrue();
assertThat(gemfireCache.getQueryService().getIndexes().size(), is(equalTo(1)));
Index customerLastNameIndex = getIndex(IndexNameConflictConfiguration.INDEX_NAME);
Index customerLastNameIndex = applicationContext.getBean("customerLastNameIndex", Index.class);
assertIndex(customerLastNameIndex, IndexNameConflictGemFireConfiguration.INDEX_NAME,
"lastName", "/Customers", IndexType.HASH);
Index customerFirstNameIndex = applicationContext.getBean("customerFirstNameIndex", Index.class);
assertIndex(customerFirstNameIndex, IndexNameConflictGemFireConfiguration.INDEX_NAME,
"firstName", "/Customers", IndexType.FUNCTIONAL);
assertThat(customerFirstNameIndex, is(not(sameInstance(customerLastNameIndex))));
assertThat(gemfireCache.getQueryService().getIndexes().iterator().next(),
is(sameInstance(customerFirstNameIndex)));
}
finally {
assertThat(close(applicationContext), is(true));
}
assertIndex(customerLastNameIndex, IndexNameConflictConfiguration.INDEX_NAME,
"lastName", "/Customers", IndexType.HASH);
}
@Test
public void indexNameConflictReturnsExistingIndex() {
ConfigurableApplicationContext applicationContext = null;
public void indexNameConflictOverridesIndex() {
assertThat(OVERRIDE.compareAndSet(false, true)).isTrue();
this.applicationContext = newApplicationContext(IndexNameConflictConfiguration.class);
assertThat(this.applicationContext.getBeansOfType(Index.class)).hasSize(2);
assertThat(this.applicationContext.containsBean("customerLastNameIndex")).isTrue();
assertThat(this.applicationContext.containsBean("customerFirstNameIndex")).isTrue();
assertIndexCount(1);
assertThat(hasIndex(IndexNameConflictConfiguration.INDEX_NAME)).isTrue();
Index customerFirstNameIndex = getIndex(IndexNameConflictConfiguration.INDEX_NAME);
assertIndex(customerFirstNameIndex, IndexNameConflictConfiguration.INDEX_NAME,
"firstName", "/Customers", IndexType.FUNCTIONAL);
}
@Test(expected = IndexNameConflictException.class)
public void indexNameConflictThrowsIndexNameConflictException() throws Throwable {
try {
System.setProperty("gemfire.cache.region.index.override", Boolean.FALSE.toString());
assertThat(System.getProperty("gemfire.cache.region.index.override", "true"),
is(equalTo(Boolean.FALSE.toString())));
applicationContext = new AnnotationConfigApplicationContext(IndexNameConflictGemFireConfiguration.class);
assertThat(applicationContext.getBeansOfType(Index.class).size(), is(equalTo(2)));
Cache gemfireCache = applicationContext.getBean("gemfireCache", Cache.class);
assertThat(gemfireCache.getQueryService().getIndexes().size(), is(equalTo(1)));
Index customerLastNameIndex = applicationContext.getBean("customerLastNameIndex", Index.class);
assertIndex(customerLastNameIndex, IndexNameConflictGemFireConfiguration.INDEX_NAME,
"lastName", "/Customers", IndexType.HASH);
Index customerFirstNameIndex = applicationContext.getBean("customerFirstNameIndex", Index.class);
assertIndex(customerFirstNameIndex, IndexNameConflictGemFireConfiguration.INDEX_NAME,
"lastName", "/Customers", IndexType.HASH);
assertThat(customerFirstNameIndex, is(sameInstance(customerLastNameIndex)));
assertThat(gemfireCache.getQueryService().getIndexes().iterator().next(),
is(sameInstance(customerLastNameIndex)));
this.applicationContext = newApplicationContext(IndexNameConflictConfiguration.class);
}
finally {
System.getProperties().remove("gemfire.cache.region.index.override");
catch (BeanCreationException expected) {
if (applicationContext != null) {
applicationContext.close();
}
assertThat(expected).hasMessageStartingWith("Error creating bean with name 'customerFirstNameIndex'");
assertThat(expected).hasCauseInstanceOf(GemfireIndexException.class);
assertThat(expected.getCause()).hasMessageStartingWith(String.format(
"An Index with the same name [%s] having possibly a different definition already exists;",
IndexNameConflictConfiguration.INDEX_NAME));
assertThat(expected.getCause()).hasCauseInstanceOf(IndexNameConflictException.class);
assertThat(expected.getCause().getCause()).hasMessage(String.format("Index named ' %s ' already exists.",
IndexNameConflictConfiguration.INDEX_NAME));
assertThat(expected.getCause().getCause()).hasNoCause();
throw expected.getCause().getCause();
}
}
@Configuration
@SuppressWarnings("unused")
public static class BaseGemFireConfiguration {
public static class GemFireConfiguration {
@Bean
public Properties gemfireProperties() {
Properties gemfireProperties = new Properties();
gemfireProperties.setProperty("name", IndexConflictsIntegrationTest.class.getSimpleName());
gemfireProperties.setProperty("mcast-port", "0");
gemfireProperties.setProperty("log-level", "warning");
return gemfireProperties;
}
@Bean
public CacheFactoryBean gemfireCache() {
CacheFactoryBean cacheFactoryBean = new CacheFactoryBean();
cacheFactoryBean.setClose(true);
cacheFactoryBean.setProperties(gemfireProperties());
cacheFactoryBean.setUseBeanFactoryLocator(false);
return cacheFactoryBean;
}
@Bean(name = "Customers")
public ReplicatedRegionFactoryBean customersRegion(Cache gemfireCache) {
public ReplicatedRegionFactoryBean customersRegion(GemFireCache gemfireCache) {
ReplicatedRegionFactoryBean customersRegionFactory = new ReplicatedRegionFactoryBean();
customersRegionFactory.setCache(gemfireCache);
customersRegionFactory.setName("Customers");
customersRegionFactory.setClose(false);
customersRegionFactory.setPersistent(false);
return customersRegionFactory;
}
}
@Configuration
@Import(BaseGemFireConfiguration.class)
@Import(GemFireConfiguration.class)
@SuppressWarnings("unused")
public static class IndexDefinitionConflictGemFireConfiguration {
public static class IndexDefinitionConflictConfiguration {
@Bean
public IndexFactoryBean customerIdIndex(Cache gemfireCache) {
public IndexFactoryBean customerIdIndex(GemFireCache gemfireCache) {
IndexFactoryBean indexFactoryBean = new IndexFactoryBean();
indexFactoryBean.setCache(gemfireCache);
indexFactoryBean.setExpression("id");
indexFactoryBean.setFrom("/Customers");
indexFactoryBean.setType(IndexType.PRIMARY_KEY);
return indexFactoryBean;
}
@Bean
@DependsOn("customerIdIndex")
public IndexFactoryBean customerIdentityIndex(Cache gemfireCache) {
public IndexFactoryBean customerIdentifierIndex(GemFireCache gemfireCache) {
IndexFactoryBean indexFactoryBean = new IndexFactoryBean();
indexFactoryBean.setCache(gemfireCache);
indexFactoryBean.setExpression("id");
indexFactoryBean.setIgnoreIfExists(IGNORE.get());
indexFactoryBean.setFrom("/Customers");
indexFactoryBean.setOverride(OVERRIDE.get());
indexFactoryBean.setType(IndexType.PRIMARY_KEY);
return indexFactoryBean;
}
}
@Configuration
@Import(BaseGemFireConfiguration.class)
@Import(GemFireConfiguration.class)
@SuppressWarnings("unused")
public static class IndexNameConflictGemFireConfiguration {
public static class IndexNameConflictConfiguration {
protected static final String INDEX_NAME = "CustomerNameIdx";
@Bean
public PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() {
return new PropertyPlaceholderConfigurer();
}
@Bean
public IndexFactoryBean customerLastNameIndex(Cache gemfireCache,
@Value("${gemfire.cache.region.index.override:true}") boolean override) {
public IndexFactoryBean customerLastNameIndex(GemFireCache gemfireCache) {
IndexFactoryBean indexFactoryBean = new IndexFactoryBean();
@@ -257,7 +351,6 @@ public class IndexConflictsIntegrationTest {
indexFactoryBean.setExpression("lastName");
indexFactoryBean.setFrom("/Customers");
indexFactoryBean.setName(INDEX_NAME);
indexFactoryBean.setOverride(override);
indexFactoryBean.setType(IndexType.HASH);
return indexFactoryBean;
@@ -265,20 +358,19 @@ public class IndexConflictsIntegrationTest {
@Bean
@DependsOn("customerLastNameIndex")
public IndexFactoryBean customerFirstNameIndex(Cache gemfireCache,
@Value("${gemfire.cache.region.index.override:true}") boolean override) {
public IndexFactoryBean customerFirstNameIndex(GemFireCache gemfireCache) {
IndexFactoryBean indexFactoryBean = new IndexFactoryBean();
indexFactoryBean.setCache(gemfireCache);
indexFactoryBean.setExpression("firstName");
indexFactoryBean.setFrom("/Customers");
indexFactoryBean.setIgnoreIfExists(IGNORE.get());
indexFactoryBean.setName(INDEX_NAME);
indexFactoryBean.setOverride(override);
indexFactoryBean.setOverride(OVERRIDE.get());
indexFactoryBean.setType(IndexType.FUNCTIONAL);
return indexFactoryBean;
}
}
}

View File

@@ -25,7 +25,6 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -38,7 +37,10 @@ import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.lucene.LuceneIndex;
import org.apache.geode.cache.lucene.LuceneService;
import org.apache.geode.cache.query.Index;
import org.apache.geode.cache.query.IndexExistsException;
import org.apache.geode.cache.query.IndexNameConflictException;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.internal.concurrent.ConcurrentHashSet;
import org.junit.After;
import org.junit.Test;
import org.mockito.Matchers;
@@ -71,7 +73,7 @@ import org.springframework.data.gemfire.config.annotation.test.entities.Replicat
*/
public class EnableIndexingConfigurationUnitTests {
private static final Set<Index> indexes = new HashSet<>();
private static final Set<Index> indexes = new ConcurrentHashSet<>();
private ConfigurableApplicationContext applicationContext;
@@ -82,16 +84,19 @@ public class EnableIndexingConfigurationUnitTests {
}
/* (non-Javadoc) */
protected void assertIndex(Index index, String name, String expression, String from, IndexType indexType) {
assertThat(index).isNotNull();
assertThat(index.getName()).isEqualTo(name);
assertThat(index.getIndexedExpression()).isEqualTo(expression);
assertThat(index.getFromClause()).isEqualTo(from);
assertThat(index.getType()).isEqualTo(indexType.getGemfireIndexType());
private static Index findIndexByName(String indexName) {
for (Index index : indexes) {
if (index.getName().equalsIgnoreCase(indexName)) {
return index;
}
}
return null;
}
/* (non-Javadoc) */
protected void assertLuceneIndex(LuceneIndex index, String name, String regionPath, String... fields) {
private void assertLuceneIndex(LuceneIndex index, String name, String regionPath, String... fields) {
assertThat(index).isNotNull();
assertThat(index.getName()).isEqualTo(name);
assertThat(index.getRegionPath()).isEqualTo(regionPath);
@@ -100,68 +105,87 @@ public class EnableIndexingConfigurationUnitTests {
}
/* (non-Javadoc) */
protected ConfigurableApplicationContext newApplicationContext(Class<?>... annotatedClasses) {
private void assertOqlIndex(Index index, String name, String expression, String from, IndexType indexType) {
assertThat(index).isNotNull();
assertThat(index.getName()).isEqualTo(name);
assertThat(index.getIndexedExpression()).isEqualTo(expression);
assertThat(index.getFromClause()).isEqualTo(from);
assertThat(index.getType()).isEqualTo(indexType.getGemfireIndexType());
}
/* (non-Javadoc) */
private ConfigurableApplicationContext newApplicationContext(Class<?>... annotatedClasses) {
ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(annotatedClasses);
applicationContext.registerShutdownHook();
return applicationContext;
}
@Test
public void persistentEntityIndexesCreatedSuccessfully() {
applicationContext = newApplicationContext(IndexedPersistentEntityConfiguration.class);
public void persistentEntityIndexesAreCreated() {
applicationContext = newApplicationContext(IndexingEnabledWithIndexedPersistentEntityConfiguration.class);
assertLuceneIndexes(applicationContext);
assertOqlIndexes(applicationContext);
}
private void assertLuceneIndexes(ConfigurableApplicationContext applicationContext) {
LuceneIndex luceneIndex = applicationContext.getBean("TitleLuceneIdx", LuceneIndex.class);
assertLuceneIndex(luceneIndex, "TitleLuceneIdx", "Customers", "title");
}
private void assertOqlIndexes(ConfigurableApplicationContext applicationContext) {
Index customersIdIdx = applicationContext.getBean("CustomersIdKeyIdx", Index.class);
assertIndex(customersIdIdx, "CustomersIdKeyIdx", "id", "Customers", IndexType.KEY);
Index customersIdIndex = applicationContext.getBean("CustomersIdKeyIdx", Index.class);
Index customersFirstNameIdx = applicationContext.getBean("CustomersFirstNameFunctionalIdx", Index.class);
assertOqlIndex(customersIdIndex, "CustomersIdKeyIdx", "id", "Customers", IndexType.KEY);
assertIndex(customersFirstNameIdx, "CustomersFirstNameFunctionalIdx", "first_name",
Index customersFirstNameIndex = applicationContext.getBean("CustomersFirstNameFunctionalIdx", Index.class);
assertOqlIndex(customersFirstNameIndex, "CustomersFirstNameFunctionalIdx", "first_name",
"/LoyalCustomers", IndexType.FUNCTIONAL);
Index lastNameIdx = applicationContext.getBean("LastNameIdx", Index.class);
Index lastNameIndex = applicationContext.getBean("LastNameIdx", Index.class);
assertIndex(lastNameIdx, "LastNameIdx", "surname", "Customers", IndexType.HASH);
assertOqlIndex(lastNameIndex, "LastNameIdx", "surname", "Customers", IndexType.HASH);
}
@Test
public void persistentEntityIndexesWillNotBeCreated() {
applicationContext = newApplicationContext(NoIndexesCreatedForIndexedPersistentEntityConfiguration.class);
public void persistentEntityIndexesAreNotCreated() {
applicationContext = newApplicationContext(IndexingNotEnabledWithIndexedPersistentEntityConfiguration.class);
Map<String, Index> indexes = applicationContext.getBeansOfType(Index.class);
assertThat(indexes).isNotNull();
assertThat(indexes.isEmpty()).isTrue();
assertThat(indexes).isEmpty();
}
@Test
public void indexAnnotatedEntityPropertyDoesNotOverrideIndexBeanDefinition() {
applicationContext = newApplicationContext(IndexAnnotatedEntityPropertyDoesNotOverrideBeanDefinitionConfiguration.class);
public void indexAnnotatedEntityPropertyIsIgnoredWithExistingIndexHavingSameDefinition() {
Index lastNameIdx = applicationContext.getBean("LastNameIdx", Index.class);
applicationContext = newApplicationContext(
IndexAnnotatedEntityPropertyIsIgnoredWithExistingIndexHavingSameDefinitionConfiguration.class);
assertIndex(lastNameIdx, "LastNameIdx", "last_name", "/People", IndexType.HASH);
}
Index firstNameIndex = applicationContext.getBean("LoyalCustomersFirstNameFunctionalIdx", Index.class);
@Test
public void indexAnnotatedEntityPropertyOverridesIndexBeanDefinition() {
applicationContext = newApplicationContext(IndexAnnotatedEntityPropertyOverridesIndexBeanDefinitionConfiguration.class);
Index customersFirstNameIdx = applicationContext.getBean("CustomersFirstNameFunctionalIdx", Index.class);
assertIndex(customersFirstNameIdx, "CustomersFirstNameFunctionalIdx",
assertOqlIndex(firstNameIndex, "LoyalCustomersFirstNameFunctionalIdx",
"first_name", "/LoyalCustomers", IndexType.FUNCTIONAL);
assertThat(findIndexByName("CustomersFirstNameFunctionalIdx")).isNull();
}
@Test
public void indexAnnotatedEntityPropertyIsIgnoredWithExistingIndexHavingSameName() {
applicationContext = newApplicationContext(
IndexAnnotatedEntityPropertyIsIgnoredWithExistingIndexHavingSameNameConfiguration.class);
Index lastNameIndex = applicationContext.getBean("LastNameIdx", Index.class);
assertOqlIndex(lastNameIndex, "LastNameIdx", "last_name", "/People", IndexType.HASH);
}
@Configuration
@@ -175,10 +199,10 @@ public class EnableIndexingConfigurationUnitTests {
}
Cache mockQueryService(Cache mockCache) throws Exception {
QueryService mockQueryService = mock(QueryService.class);
when(mockCache.getQueryService()).thenReturn(mockQueryService);
when(mockQueryService.getIndexes()).thenReturn(indexes);
when(mockQueryService.createHashIndex(anyString(), anyString(), anyString()))
.thenAnswer(new HashIndexAnswer());
@@ -189,11 +213,24 @@ public class EnableIndexingConfigurationUnitTests {
when(mockQueryService.createKeyIndex(anyString(), anyString(), anyString()))
.thenAnswer(new KeyIndexAnswer());
when(mockQueryService.getIndexes()).thenReturn(indexes);
doAnswer(invocation -> {
Index indexToRemove = invocation.getArgument(0);
indexes.remove(findIndexByName(indexToRemove.getName()));
return null;
}).when(mockQueryService).removeIndex(any(Index.class));
return mockCache;
}
@SuppressWarnings("unchecked")
Cache mockRegionFactory(Cache mockCache) {
RegionFactory mockRegionFactory = mock(RegionFactory.class);
when(mockCache.createRegionFactory()).thenReturn(mockRegionFactory);
@@ -206,9 +243,11 @@ public class EnableIndexingConfigurationUnitTests {
@Bean
LuceneService luceneService() {
LuceneService mockLuceneService = mock(LuceneService.class);
doAnswer(invocation -> {
LuceneIndex mockLuceneIndex = mock(LuceneIndex.class);
String indexName = invocation.getArgument(0);
@@ -220,6 +259,7 @@ public class EnableIndexingConfigurationUnitTests {
when(mockLuceneService.getIndex(eq(indexName), eq(regionPath))).thenReturn(mockLuceneIndex);
return mockLuceneIndex;
}).when(mockLuceneService).createIndex(anyString(), anyString(), Matchers.<String[]>anyVararg());
return mockLuceneService;
@@ -237,16 +277,22 @@ public class EnableIndexingConfigurationUnitTests {
@Override
public Index answer(InvocationOnMock invocation) throws Throwable {
IndexType indexType = getType();
String name = invocation.getArgument(0);
String expression = invocation.getArgument(1);
String from = invocation.getArgument(2);
validateIndexDefinition(name, expression, from, indexType);
validateIndexName(name);
Index mockIndex = mock(Index.class, name);
when(mockIndex.getName()).thenReturn(name);
when(mockIndex.getIndexedExpression()).thenReturn(expression);
when(mockIndex.getFromClause()).thenReturn(from);
when(mockIndex.getType()).thenReturn(getType().getGemfireIndexType());
when(mockIndex.getType()).thenReturn(indexType.getGemfireIndexType());
indexes.add(mockIndex);
@@ -255,6 +301,30 @@ public class EnableIndexingConfigurationUnitTests {
abstract IndexType getType();
private void validateIndexDefinition(String name, String expression, String fromClause, IndexType type)
throws IndexExistsException {
for (Index index : indexes) {
if (index.getIndexedExpression().equalsIgnoreCase(expression)
&& index.getFromClause().equalsIgnoreCase(fromClause)
&& index.getType().equals(type.getGemfireIndexType())) {
throw new IndexExistsException(String.format(
"Index [%1$s] has the same definition as existing Index [%2$s]",
name, index.getName()));
}
}
}
private void validateIndexName(String name) throws IndexNameConflictException {
for (Index index : indexes) {
if (index.getName().equalsIgnoreCase(name)) {
throw new IndexNameConflictException(String.format("Index with name [%s] already exists", name));
}
}
}
}
static class FunctionalIndexAnswer extends AbstractIndexAnswer {
@@ -286,7 +356,7 @@ public class EnableIndexingConfigurationUnitTests {
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {
ClientRegionEntity.class, CollocatedPartitionRegionEntity.class, GenericRegionEntity.class,
LocalRegionEntity.class, ReplicateRegionEntity.class }))
static class IndexedPersistentEntityConfiguration extends GemFireConfiguration {
private static class IndexingEnabledWithIndexedPersistentEntityConfiguration extends GemFireConfiguration {
}
@@ -294,7 +364,7 @@ public class EnableIndexingConfigurationUnitTests {
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {
ClientRegionEntity.class, CollocatedPartitionRegionEntity.class, GenericRegionEntity.class,
LocalRegionEntity.class, ReplicateRegionEntity.class }))
static class NoIndexesCreatedForIndexedPersistentEntityConfiguration extends GemFireConfiguration {
private static class IndexingNotEnabledWithIndexedPersistentEntityConfiguration extends GemFireConfiguration {
}
@@ -303,42 +373,46 @@ public class EnableIndexingConfigurationUnitTests {
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {
ClientRegionEntity.class, CollocatedPartitionRegionEntity.class, GenericRegionEntity.class,
LocalRegionEntity.class, ReplicateRegionEntity.class }))
static class IndexAnnotatedEntityPropertyDoesNotOverrideBeanDefinitionConfiguration extends GemFireConfiguration {
static class IndexAnnotatedEntityPropertyIsIgnoredWithExistingIndexHavingSameDefinitionConfiguration
extends GemFireConfiguration {
@Bean
@SuppressWarnings("unused")
IndexFactoryBean firstNameIndex(GemFireCache gemfireCache) {
IndexFactoryBean firstNameIndex = new IndexFactoryBean();
firstNameIndex.setCache(gemfireCache);
firstNameIndex.setName("LoyalCustomersFirstNameFunctionalIdx");
firstNameIndex.setExpression("first_name");
firstNameIndex.setFrom("/LoyalCustomers");
firstNameIndex.setType(IndexType.FUNCTIONAL);
return firstNameIndex;
}
}
@EnableIndexing
@EnableEntityDefinedRegions(basePackageClasses = NonEntity.class,
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {
ClientRegionEntity.class, CollocatedPartitionRegionEntity.class, GenericRegionEntity.class,
LocalRegionEntity.class, ReplicateRegionEntity.class }))
static class IndexAnnotatedEntityPropertyIsIgnoredWithExistingIndexHavingSameNameConfiguration
extends GemFireConfiguration {
@Bean
@SuppressWarnings("unused")
IndexFactoryBean lastNameIndex(GemFireCache gemfireCache) {
IndexFactoryBean lastNameIndex = new IndexFactoryBean();
lastNameIndex.setCache(gemfireCache);
lastNameIndex.setName("LastNameIdx");
lastNameIndex.setExpression("last_name");
lastNameIndex.setFrom("/People");
lastNameIndex.setName("LastNameIdx");
lastNameIndex.setType(IndexType.HASH);
return lastNameIndex;
}
}
@EnableIndexing
@EnableEntityDefinedRegions(basePackageClasses = NonEntity.class,
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {
ClientRegionEntity.class, CollocatedPartitionRegionEntity.class, GenericRegionEntity.class,
LocalRegionEntity.class, ReplicateRegionEntity.class }))
static class IndexAnnotatedEntityPropertyOverridesIndexBeanDefinitionConfiguration extends GemFireConfiguration {
@Bean
@SuppressWarnings("unused")
IndexFactoryBean firstNameIndex(GemFireCache gemfireCache) {
IndexFactoryBean firstNameIndex = new IndexFactoryBean();
firstNameIndex.setCache(gemfireCache);
firstNameIndex.setExpression("given_name");
firstNameIndex.setFrom("/ProspectiveCustomers");
firstNameIndex.setName("CustomersFirstNameFunctionalIdx");
firstNameIndex.setType(IndexType.HASH);
return firstNameIndex;
}
}
}

View File

@@ -42,7 +42,7 @@ public class PartitionRegionEntity {
@Id
private Long id;
@Indexed(expression = "first_name", from = "/LoyalCustomers", override = true, type = IndexType.FUNCTIONAL)
@Indexed(expression = "first_name", from = "/LoyalCustomers", type = IndexType.FUNCTIONAL)
private String firstName;
@Indexed(name = "LastNameIdx", expression = "surname")

View File

@@ -16,6 +16,7 @@
package org.springframework.data.gemfire.config.xml;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import javax.annotation.Resource;
@@ -24,13 +25,16 @@ import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.Index;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.gemfire.IndexFactoryBean;
import org.springframework.data.gemfire.test.GemfireTestApplicationContextInitializer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
/**
* The IndexNamespaceTest is a test suite of test cases testing the functionality of GemFire Index creation using
* the Spring Data GemFire XML namespace (XSD).
* Integration tests with test cases testing the functionality of GemFire Index creation using
* the Spring Data GemFire XML namespace (XSD) and the {@link IndexParser}.
*
* @author Costin Leau
* @author David Turanski
@@ -42,30 +46,42 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
* @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner
* @since 1.1.0
*/
@RunWith(SpringJUnit4ClassRunner.class)
@RunWith(SpringRunner.class)
@ContextConfiguration(locations="index-ns.xml", initializers=GemfireTestApplicationContextInitializer.class)
@SuppressWarnings({ "deprecation", "unused" })
public class IndexNamespaceTest {
private static final String TEST_REGION_NAME = "IndexedRegion";
@Resource(name = "simple")
private Index simple;
@Autowired
private ApplicationContext applicationContext;
@Resource(name = "basic")
private Index basic;
@Resource(name = "complex")
private Index complex;
@Test
public void testBasicIndex() throws Exception {
assertEquals("simple", simple.getName());
assertEquals("status", simple.getIndexedExpression());
assertEquals(Region.SEPARATOR + TEST_REGION_NAME, simple.getFromClause());
assertEquals(TEST_REGION_NAME, simple.getRegion().getName());
assertEquals(org.apache.geode.cache.query.IndexType.FUNCTIONAL, simple.getType());
public void basicIndexIsCorrect() throws Exception {
assertEquals("basic", basic.getName());
assertEquals("status", basic.getIndexedExpression());
assertEquals(Region.SEPARATOR + TEST_REGION_NAME, basic.getFromClause());
assertEquals(TEST_REGION_NAME, basic.getRegion().getName());
assertEquals(org.apache.geode.cache.query.IndexType.FUNCTIONAL, basic.getType());
}
@Test
public void testComplexIndex() throws Exception {
public void basicIndexFactoryBeanIsCorrect() {
IndexFactoryBean basicIndexFactoryBean = applicationContext.getBean("&basic", IndexFactoryBean.class);
assertThat(basicIndexFactoryBean.isIgnoreIfExists()).isFalse();
assertThat(basicIndexFactoryBean.isOverride()).isFalse();
}
@Test
public void complexIndexIsCorrect() throws Exception {
assertEquals("complex-index", complex.getName());
assertEquals("tsi.name", complex.getIndexedExpression());
assertEquals(Region.SEPARATOR + TEST_REGION_NAME + " tsi", complex.getFromClause());
@@ -73,4 +89,13 @@ public class IndexNamespaceTest {
assertEquals(org.apache.geode.cache.query.IndexType.HASH, complex.getType());
}
@Test
public void indexWithIgnoreAndOverrideIsCorrect() {
IndexFactoryBean indexFactoryBean =
applicationContext.getBean("&index-with-ignore-and-override", IndexFactoryBean.class);
assertThat(indexFactoryBean.isIgnoreIfExists()).isTrue();
assertThat(indexFactoryBean.isOverride()).isTrue();
}
}

View File

@@ -129,6 +129,7 @@ public class LuceneOperationsIntegrationTests {
@Test
public void findsDoctorDoesAsTypePersonSuccessfully() {
Collection<Person> doctorDoes = template.queryForValues("title: Doctor*", "title");
assertThat(doctorDoes).isNotNull();
@@ -139,6 +140,7 @@ public class LuceneOperationsIntegrationTests {
@Test
@SuppressWarnings("all")
public void findsMasterDoesAsTypeUserSuccessfully() {
List<User> masterDoes = template.query("title: Master*", "title", User.class);
assertThat(masterDoes).isNotNull();

View File

@@ -133,6 +133,26 @@ public class AbstractFactoryBeanSupportUnitTests {
verify(mockLog, times(1)).info(eq("info log test"));
}
@Test
public void logsWarningWhenWarnIsEnabled() {
when(mockLog.isWarnEnabled()).thenReturn(true);
factoryBeanSupport.logWarning("%s log test", "warn");
verify(mockLog, times(1)).isWarnEnabled();
verify(mockLog, times(1)).warn(eq("warn log test"));
}
@Test
public void logsWarningWhenErrorIsEnabled() {
when(mockLog.isErrorEnabled()).thenReturn(true);
factoryBeanSupport.logError("%s log test", "error");
verify(mockLog, times(1)).isErrorEnabled();
verify(mockLog, times(1)).error(eq("error log test"));
}
@Test
public void suppressesDebugLoggingWhenDebugIsDisabled() {
when(mockLog.isDebugEnabled()).thenReturn(false);
@@ -153,6 +173,26 @@ public class AbstractFactoryBeanSupportUnitTests {
verify(mockLog, never()).info(any());
}
@Test
public void suppressesWarnLoggingWhenWarnIsDisabled() {
when(mockLog.isWarnEnabled()).thenReturn(false);
factoryBeanSupport.logWarning(() -> "test");
verify(mockLog, times(1)).isWarnEnabled();
verify(mockLog, never()).warn(any());
}
@Test
public void suppressesErrorLoggingWhenInfoIsDisabled() {
when(mockLog.isErrorEnabled()).thenReturn(false);
factoryBeanSupport.logError(() -> "test");
verify(mockLog, times(1)).isErrorEnabled();
verify(mockLog, never()).error(any());
}
private static class TestFactoryBeanSupport<T> extends AbstractFactoryBeanSupport<T> {
@Override

View File

@@ -30,9 +30,12 @@
<gfe:replicated-region id="IndexedRegion" persistent="false"/>
<gfe:index id="simple" expression="status" from="/IndexedRegion" type="functional"/>
<gfe:index id="basic" expression="status" from="/IndexedRegion" type="functional"/>
<gfe:index id="complex" name="complex-index" expression="tsi.name" from="/IndexedRegion tsi"
imports="import java.util" type="${index.complex.type}"/>
<gfe:index id="index-with-ignore-and-override" expression="id" from="/IndexedRegion"
ignore-if-exists="true" override="true" type="PRIMARY_KEY"/>
</beans>