DATAGEODE-14 - Improve IndexFactoryBean's resilience and options for handling GemFire IndexExistsExceptions and IndexNameConflictExceptions.
Related JIRA: https://jira.spring.io/browse/SGF-637 (cherry picked from commit e868c58db9d245adab005b6a5ae1cc0a94275cf3) Signed-off-by: John Blum <jblum@pivotal.io>
This commit is contained in:
@@ -1,25 +1,237 @@
|
||||
[[bootstrap:indexing]]
|
||||
= Configuring an Index
|
||||
|
||||
Apache Geode allows Indexes (or Indices) to be created to improve the performance of OQL queries.
|
||||
Apache Geode allows Indexes (or Indices) to be created on Region data to improve the performance of OQL queries.
|
||||
|
||||
In _Spring Data Geode_, Indexes are declared with the `index` element:
|
||||
In _Spring Data Geode_ (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 Geode_ 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 Geode's_ XML schema (a.k.a. SDG namespace), `Index` bean declarations are not bound to a _Region_,
|
||||
unlike Geode's native `cache.xml`. Rather, they are top-level elements just like `<gfe:cache>`. 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 Geode'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 Geode'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 Geode_ specific; this is a feature of Apache Geode.
|
||||
|
||||
The `Index` `type` maybe 1 of 3 enumerated values defined by _Spring Data Geode'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://geode.apache.org/releases/latest/javadoc/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://geode.apache.org/releases/latest/javadoc/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 Geode_ XML schema for a full set of options.
|
||||
|
||||
For more information on Indexing in Apache Geode, see http://geode.apache.org/docs/guide/11/developing/query_index/query_index.html[Working with Indexes]
|
||||
in Apache Geode's User Guide.
|
||||
|
||||
== Defining Indexes
|
||||
|
||||
In addition to creating Indexes upfront as `Index` bean definitions are processed by _Spring Data Geode_
|
||||
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 Geode_ registers itself as
|
||||
an `ApplicationListener` listening for the `ContextRefreshedEvent`. When fired, _Spring Data Geode_ will call
|
||||
http://geode.apache.org/releases/latest/javadoc/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://geode.apache.org/docs/guide/11/developing/query_index/create_multiple_indexes.html[Creating Multiple Indexes at Once]
|
||||
for more details.
|
||||
|
||||
== `IgnoreIfExists` and `Override`
|
||||
|
||||
Two _Spring Data Geode_ `Index` configuration options warrant special mention here: `ignoreIfExists` and `override`.
|
||||
|
||||
These options correspond to the `ignore-if-exists` and `override` attributes on the `<gfe:index>` element
|
||||
in _Spring Data Geode'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 Geode_ and exist to workaround known limitations
|
||||
with Apache Geode; there are no equivalent options or functionality available in Geode itself.
|
||||
|
||||
Each option significantly differs in behavior and entirely depends on the type of Geode `Index` _Exception_ thrown.
|
||||
This also means that neither option has any effect if a Geode Index-type _Exception_ is *not* thrown. These options
|
||||
are meant to specifically handle Geode `IndexExistsExceptions` and `IndexNameConflictExceptions`, which can occur
|
||||
for various, sometimes obscure reasons. But, in general...
|
||||
|
||||
* An http://geode.apache.org/releases/latest/javadoc/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://geode.apache.org/releases/latest/javadoc/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 Geode'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 Geode_ 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 `<gfe:index ignore-if-exists="true">`),
|
||||
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 Geode itself, *not* SDG.
|
||||
|
||||
However, this also means that *no* `Index` with the "`name`" specified in your `Index` bean definition / declaration
|
||||
will "actually" exist from Geode's perspective either (i.e. with
|
||||
http://geode.apache.org/releases/latest/javadoc/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 `<gfe:index ignore-if-exists="true">`),
|
||||
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 Geode API (the only means available).
|
||||
|
||||
|
||||
=== `Override` Behavior
|
||||
|
||||
When an `IndexExistsException` is thrown and `override` is set to `true` (or `<gfe:index override="true">`), then
|
||||
the `Index` is effectively "_renamed_". Remember, `IndexExistsExceptions` are thrown when multiple Indexes exist,
|
||||
all having the same "definition" but different "names".
|
||||
|
||||
_Spring Data Geode_ can only accomplish this using Geode'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 `<gfe:index override="true">`),
|
||||
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 Geode'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 Geode (e.g. _Spring Data Geode_, Geode _Cluster Config_,
|
||||
maybe Geode 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"), Geode 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 Geode,
|
||||
such as with a call to http://geode.apache.org/releases/latest/javadoc/org/apache/geode/cache/query/QueryService.html#getIndexes--[QueryService.getIndexes()]
|
||||
or with http://geode.apache.org/releases/latest/javadoc/org/apache/geode/cache/query/QueryService.html#getIndexes-org.apache.geode.cache.Region-[QueryService.getIndexes(:Region)].
|
||||
|
||||
As such, the only way for SDG or other Geode 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 Geode 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 Geode `QueryService.createIndex(..)` methods are synchronous, "blocking" operations,
|
||||
then the state of Geode 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_*!
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ public class IndexConfiguration extends EntityDefinedRegionsConfiguration {
|
||||
getAnnotationAttributes(importingClassMetadata, getEnableIndexingAnnotationTypeName());
|
||||
|
||||
localPersistentEntity.doWithProperties((PropertyHandler<GemfirePersistentProperty>) persistentProperty -> {
|
||||
|
||||
Optional<Id> idAnnotation = persistentProperty.findAnnotation(Id.class);
|
||||
|
||||
idAnnotation.ifPresent(id ->
|
||||
@@ -201,12 +202,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());
|
||||
@@ -331,15 +333,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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user