diff --git a/src/main/asciidoc/reference/indexing.adoc b/src/main/asciidoc/reference/indexing.adoc index 7206ab97..9476782c 100644 --- a/src/main/asciidoc/reference/indexing.adoc +++ b/src/main/asciidoc/reference/indexing.adoc @@ -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] ---- - + ---- -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 `<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 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 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] +---- + +---- + +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] +---- + +---- + +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 `<gfe:index>` 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 `<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 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 `<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 GemFire 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 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 `<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 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_*! diff --git a/src/main/java/org/springframework/data/gemfire/GemfireIndexException.java b/src/main/java/org/springframework/data/gemfire/GemfireIndexException.java index e3d36a80..029ba012 100644 --- a/src/main/java/org/springframework/data/gemfire/GemfireIndexException.java +++ b/src/main/java/org/springframework/data/gemfire/GemfireIndexException.java @@ -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); } - } diff --git a/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java b/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java index 17cb7fbb..35245f4d 100644 --- a/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java @@ -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 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 implemen //@Autowired(required = false) private List 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 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 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 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 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 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 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 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 implemen } /* (non-Javadoc) */ - Index getExistingIndex(QueryService queryService, String indexName) { + Optional 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 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 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 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 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 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 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 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); } } } diff --git a/src/main/java/org/springframework/data/gemfire/IndexType.java b/src/main/java/org/springframework/data/gemfire/IndexType.java index 904c35d9..ec51d6af 100644 --- a/src/main/java/org/springframework/data/gemfire/IndexType.java +++ b/src/main/java/org/springframework/data/gemfire/IndexType.java @@ -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)); } - } diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/IndexConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/IndexConfiguration.java index 3fe29742..4f0f38a4 100644 --- a/src/main/java/org/springframework/data/gemfire/config/annotation/IndexConfiguration.java +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/IndexConfiguration.java @@ -136,6 +136,7 @@ public class IndexConfiguration extends EntityDefinedRegionsConfiguration { getAnnotationAttributes(importingClassMetadata, getEnableIndexingAnnotationTypeName()); localPersistentEntity.doWithProperties((PropertyHandler) 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, diff --git a/src/main/java/org/springframework/data/gemfire/config/xml/IndexParser.java b/src/main/java/org/springframework/data/gemfire/config/xml/IndexParser.java index 8e92573e..0681e0c2 100644 --- a/src/main/java/org/springframework/data/gemfire/config/xml/IndexParser.java +++ b/src/main/java/org/springframework/data/gemfire/config/xml/IndexParser.java @@ -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) diff --git a/src/main/java/org/springframework/data/gemfire/mapping/annotation/Indexed.java b/src/main/java/org/springframework/data/gemfire/mapping/annotation/Indexed.java index 1d5a7184..043c7b21 100644 --- a/src/main/java/org/springframework/data/gemfire/mapping/annotation/Indexed.java +++ b/src/main/java/org/springframework/data/gemfire/mapping/annotation/Indexed.java @@ -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. * diff --git a/src/main/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupport.java b/src/main/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupport.java index 0a1ca91f..d1b444f9 100644 --- a/src/main/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupport.java @@ -203,4 +203,54 @@ public abstract class AbstractFactoryBeanSupport implements FactoryBean, .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 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 message) { + Optional.ofNullable(getLog()) + .filter(Log::isErrorEnabled) + .ifPresent(log -> log.error(message.get())); + } } diff --git a/src/main/java/org/springframework/data/gemfire/util/CacheUtils.java b/src/main/java/org/springframework/data/gemfire/util/CacheUtils.java index adda07e7..73393781 100644 --- a/src/main/java/org/springframework/data/gemfire/util/CacheUtils.java +++ b/src/main/java/org/springframework/data/gemfire/util/CacheUtils.java @@ -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.ofNullable(getCache()).orElseGet(CacheUtils::getClientCache); + return Optional.ofNullable(getClientCache()).orElseGet(CacheUtils::getCache); } /* (non-Javadoc) */ diff --git a/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-2.0.xsd b/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-2.0.xsd index 45e5f990..f909cbcc 100644 --- a/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-2.0.xsd +++ b/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-2.0.xsd @@ -2256,11 +2256,27 @@ Corresponds to the regionPath parameter in createIndex methods. ]]> - - + + + + + + + diff --git a/src/main/resources/org/springframework/data/gemfire/config/spring-geode-1.1.xsd b/src/main/resources/org/springframework/data/gemfire/config/spring-geode-1.1.xsd index 4f987903..46656697 100644 --- a/src/main/resources/org/springframework/data/gemfire/config/spring-geode-1.1.xsd +++ b/src/main/resources/org/springframework/data/gemfire/config/spring-geode-1.1.xsd @@ -2358,11 +2358,27 @@ Corresponds to the regionPath parameter in createIndex methods. ]]> - - + + + + + + + diff --git a/src/test/java/org/springframework/data/gemfire/IndexConflictsIntegrationTest.java b/src/test/java/org/springframework/data/gemfire/IndexConflictsIntegrationTest.java index 1602db3e..46c17190 100644 --- a/src/test/java/org/springframework/data/gemfire/IndexConflictsIntegrationTest.java +++ b/src/test/java/org/springframework/data/gemfire/IndexConflictsIntegrationTest.java @@ -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 IndexFactoryBean traps IndexExistsException instead of IndexNameConflictException + * @see Improve IndexFactoryBean's resilience and options for handling GemFire IndexExistsExceptions and IndexNameConflictExceptions * @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; } } - } diff --git a/src/test/java/org/springframework/data/gemfire/IndexFactoryBeanTest.java b/src/test/java/org/springframework/data/gemfire/IndexFactoryBeanTest.java index 1140f97b..9fe52273 100644 --- a/src/test/java/org/springframework/data/gemfire/IndexFactoryBeanTest.java +++ b/src/test/java/org/springframework/data/gemfire/IndexFactoryBeanTest.java @@ -16,90 +16,84 @@ package org.springframework.data.gemfire; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.isA; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.CoreMatchers.sameInstance; -import static org.hamcrest.Matchers.startsWith; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.logging.Log; import org.apache.geode.cache.Cache; 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; import org.junit.After; -import org.junit.Rule; +import org.junit.Before; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.data.gemfire.config.xml.GemfireConstants; -import org.springframework.data.util.ReflectionUtils; /** * Unit tests for {@link IndexFactoryBean}. * * @author John Blum * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.Spy + * @see org.mockito.junit.MockitoJUnitRunner * @see org.apache.geode.cache.Cache * @see org.apache.geode.cache.client.ClientCache * @see org.apache.geode.cache.query.Index + * @see org.apache.geode.cache.query.QueryService * @see org.springframework.data.gemfire.IndexFactoryBean * @since 1.5.2 */ +@RunWith(MockitoJUnitRunner.class) public class IndexFactoryBeanTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); + @Mock + private BeanFactory mockBeanFactory; - private Cache mockCache = mock(Cache.class, "IndexFactoryBeanTest.MockCache"); + @Mock + private Cache mockCache; - private IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + @Spy + private IndexFactoryBean indexFactoryBean; - private QueryService mockQueryService = mock(QueryService.class, "IndexFactoryBeanTest.MockQueryService"); + @Mock + private Log mockLog; - protected IndexFactoryBean newIndexFactoryBean() { - IndexFactoryBean indexFactoryBean = new IndexFactoryBean() { - @Override QueryService lookupQueryService() { - return mockQueryService; - } - }; + @Mock + private QueryService mockQueryService; - indexFactoryBean.setCache(mockCache); - - return indexFactoryBean; + @Before + public void setup() { + when(mockCache.getQueryService()).thenReturn(mockQueryService); } @After @@ -107,26 +101,67 @@ public class IndexFactoryBeanTest { indexFactoryBean.setBeanFactory(null); indexFactoryBean.setCache(null); indexFactoryBean.setDefine(false); + indexFactoryBean.setExpression(null); + indexFactoryBean.setFrom(null); + indexFactoryBean.setImports(null); + indexFactoryBean.setQueryService(null); + indexFactoryBean.setType((IndexType) null); } - @Test - public void afterPropertiesSetIsSuccessful() throws Exception { - BeanFactory mockBeanFactory = mock(BeanFactory.class, "testAfterPropertiesSetIsSuccessful.MockBeanFactory"); + private Index mockIndex(String name) { - Cache mockCache = mock(Cache.class, "testAfterPropertiesSetIsSuccessful.MockCache"); + Index mockIndex = mock(Index.class, name); - Index mockIndex = mock(Index.class, "testAfterPropertiesSetIsSuccessful.MockIndex"); + when(mockIndex.getName()).thenReturn(name); - QueryService mockQueryService = mock(QueryService.class, "testAfterPropertiesSetIsSuccessful.MockQueryService"); + return mockIndex; + } - when(mockBeanFactory.containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE))).thenReturn(false); - when(mockCache.getQueryService()).thenReturn(mockQueryService); - when(mockQueryService.createKeyIndex(eq("TestKeyIndex"), eq("id"), eq("/Example"))).thenReturn(mockIndex); + private Index mockIndexWithDefinition(String name, String expression, String fromClause, IndexType type) { - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + Index mockIndex = mockIndex(name); + + when(mockIndex.getIndexedExpression()).thenReturn(expression); + when(mockIndex.getFromClause()).thenReturn(fromClause); + when(mockIndex.getType()).thenReturn(type.getGemfireIndexType()); + + return mockIndex; + } + + private QueryService mockQueryService(String name) { + return mock(QueryService.class, name); + } + + private IndexFactoryBean newIndexFactoryBean() { + + IndexFactoryBean indexFactoryBean = spy(new IndexFactoryBean() { + @Override + protected Log newLog() { + return mockLog; + } + }); indexFactoryBean.setBeanFactory(mockBeanFactory); indexFactoryBean.setCache(mockCache); + indexFactoryBean.setQueryService(mockQueryService); + + return indexFactoryBean; + } + + @Test + @SuppressWarnings("all") + public void afterPropertiesSetIsSuccessful() throws Exception { + + ConfigurableBeanFactory mockConfigurableBeanFactory = mock(ConfigurableBeanFactory.class); + + Index mockIndex = mock(Index.class, "testAfterPropertiesSetIsSuccessful.MockIndex"); + + when(mockQueryService.createKeyIndex(eq("TestKeyIndex"), eq("id"), eq("/Example"))).thenReturn(mockIndex); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.setBeanFactory(mockConfigurableBeanFactory); + indexFactoryBean.setBeanName("KeyIndexBean"); indexFactoryBean.setDefine(false); indexFactoryBean.setExpression("id"); indexFactoryBean.setFrom("/Example"); @@ -134,47 +169,73 @@ public class IndexFactoryBeanTest { indexFactoryBean.setType("key"); indexFactoryBean.afterPropertiesSet(); - assertEquals(mockIndex, indexFactoryBean.getObject()); - assertSame(mockIndex, indexFactoryBean.getObject()); // assert Index really is a 'Singleton' - assertTrue(Index.class.isAssignableFrom(indexFactoryBean.getObjectType())); - assertTrue(indexFactoryBean.isSingleton()); + assertThat(indexFactoryBean.getIndex()).isEqualTo(mockIndex); - verify(mockBeanFactory, times(1)).containsBean( - eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE)); - verify(mockCache, times(1)).getQueryService(); - verify(mockQueryService, times(1)).createKeyIndex(eq("TestKeyIndex"), eq("id"), eq("/Example")); + Index actualIndex = indexFactoryBean.getObject(); + + assertThat(actualIndex).isEqualTo(mockIndex); + assertThat(indexFactoryBean.getObject()).isSameAs(actualIndex); + assertThat(Index.class).isAssignableFrom(indexFactoryBean.getObjectType()); + assertThat(indexFactoryBean.isSingleton()).isTrue(); + + verify(indexFactoryBean, times(1)).getBeanName(); + verify(indexFactoryBean, never()).lookupQueryService(); + verify(mockConfigurableBeanFactory, times(1)) + .registerAlias(eq("KeyIndexBean"), eq("TestKeyIndex")); + verify(mockQueryService, times(1)) + .createKeyIndex(eq("TestKeyIndex"), eq("id"), eq("/Example")); + verifyZeroInteractions(mockCache); } @Test(expected = IllegalArgumentException.class) + public void afterPropertiesSetWithNoIndexName() throws Exception { + try { + newIndexFactoryBean().afterPropertiesSet(); + } + catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("Index name is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalStateException.class) public void afterPropertiesSetWithNullCache() throws Exception { try { IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); indexFactoryBean.setName("TestIndex"); indexFactoryBean.afterPropertiesSet(); } - catch (IllegalArgumentException expected) { - assertEquals("Cache is required", expected.getMessage()); + catch (IllegalStateException expected) { + assertThat(expected).hasMessage("Cache is required"); + assertThat(expected).hasNoCause(); + throw expected; } } - @Test(expected = IllegalArgumentException.class) + @Test(expected = IllegalStateException.class) public void afterPropertiesSetWithNullQueryService() throws Exception { - try { - IndexFactoryBean indexFactoryBean = new IndexFactoryBean() { - @Override QueryService lookupQueryService() { - return null; - } - }; + IndexFactoryBean indexFactoryBean = spy(new IndexFactoryBean()); + + doReturn(null).when(indexFactoryBean).lookupQueryService(); + + try { indexFactoryBean.setCache(mockCache); indexFactoryBean.setName("TestIndex"); indexFactoryBean.afterPropertiesSet(); } - catch (IllegalArgumentException expected) { - assertEquals("QueryService is required to create an Index", expected.getMessage()); + catch (IllegalStateException expected) { + assertThat(expected).hasMessage("QueryService is required to create an Index"); + assertThat(expected).hasNoCause(); + throw expected; } + finally { + verify(indexFactoryBean, times(1)).lookupQueryService(); + } } @Test(expected = IllegalArgumentException.class) @@ -185,88 +246,134 @@ public class IndexFactoryBeanTest { indexFactoryBean.afterPropertiesSet(); } catch (IllegalArgumentException expected) { - assertEquals("Index expression is required", expected.getMessage()); + assertThat(expected).hasMessage("Index expression is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void afterPropertiesSetWithUnspecifiedFromClause() throws Exception { + try { + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setExpression("id"); + indexFactoryBean.afterPropertiesSet(); + } + catch (Exception expected) { + assertThat(expected).hasMessage("Index from clause is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void afterPropertiesSetUsingImportsWithInvalidIndexKeyType() throws Exception { + try { + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setExpression("id"); + indexFactoryBean.setFrom("/Example"); + indexFactoryBean.setImports("org.example.DomainType"); + indexFactoryBean.setType("PriMary_Key"); + indexFactoryBean.afterPropertiesSet(); + } + catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("Imports are not supported with a KEY Index"); + assertThat(expected).hasNoCause(); + throw expected; } } @Test - public void afterPropertiesSetWithUnspecifiedFromClause() throws Exception { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectCause(is(nullValue(Throwable.class))); - expectedException.expectMessage("Index from clause is required"); - - IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - - indexFactoryBean.setExpression("id"); - indexFactoryBean.setName("TestIndex"); - indexFactoryBean.afterPropertiesSet(); + public void resolveCacheFromCacheProperty() { + assertThat(newIndexFactoryBean().resolveCache()).isSameAs(mockCache); } - @Test(expected = IllegalArgumentException.class) - public void afterPropertiesSetWithUnspecifiedIndexName() throws Exception { + @Test(expected = IllegalStateException.class) + public void resolveCacheThrowsExceptionForUnresolvableCache() { try { IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - indexFactoryBean.setExpression("id"); - indexFactoryBean.setFrom("/Example"); - indexFactoryBean.afterPropertiesSet(); + indexFactoryBean.setCache(null); + indexFactoryBean.resolveCache(); } - catch (IllegalArgumentException expected) { - assertEquals("Index name is required", expected.getMessage()); + catch (IllegalStateException expected) { + assertThat(expected).hasMessage("Cache is required"); + assertThat(expected).hasNoCause(); + throw expected; } } + @Test + public void resolveIndexNameFromBeanNameProperty() { + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.setBeanName("TestIndexBeanName"); + indexFactoryBean.setName(null); + + assertThat(indexFactoryBean.resolveIndexName()).isEqualTo("TestIndexBeanName"); + } + + @Test + public void resolveIndexNameFromNameProperty() { + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.setBeanName("TestIndexBeanName"); + indexFactoryBean.setName("TestIndex"); + + assertThat(indexFactoryBean.resolveIndexName()).isEqualTo("TestIndex"); + } + @Test(expected = IllegalArgumentException.class) - public void afterPropertiesSetUsingImportsWithInvalidType() throws Exception { + public void resolveIndexNameThrowsExceptionForUnresolvableIndexName() { try { IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - indexFactoryBean.setExpression("id"); - indexFactoryBean.setFrom("/Example"); - indexFactoryBean.setImports("org.example.DomainType"); - indexFactoryBean.setName("TestIndex"); - indexFactoryBean.setType("PriMary_Key"); - indexFactoryBean.afterPropertiesSet(); + indexFactoryBean.setBeanName(null); + indexFactoryBean.setName(null); + indexFactoryBean.resolveIndexName(); } catch (IllegalArgumentException expected) { - assertEquals("imports are not supported with a KEY Index", expected.getMessage()); + assertThat(expected).hasMessage("Index name is required"); + assertThat(expected).hasNoCause(); + throw expected; } } @Test public void doLookupQueryService() { - QueryService mockQueryServiceOne = mock(QueryService.class, "testLookupQueryService.MockQueryService.One"); - QueryService mockQueryServiceTwo = mock(QueryService.class, "testLookupQueryService.MockQueryService.Two"); - Cache mockCache = mock(Cache.class, "testDoLookupQueryService.MockCache"); + QueryService mockQueryServiceOne = mockQueryService("testDoLookupQueryService.MockQueryService.One"); - when(mockCache.getQueryService()).thenReturn(mockQueryServiceTwo); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); - - indexFactoryBean.setCache(mockCache); indexFactoryBean.setQueryService(mockQueryServiceOne); - assertSame(mockQueryServiceOne, indexFactoryBean.doLookupQueryService()); + assertThat(indexFactoryBean.doLookupQueryService()).isSameAs(mockQueryServiceOne); verify(mockCache, never()).getQueryService(); } @Test public void doLookupQueryServiceOnClientCache() { - ClientCache mockClientCache = mock(ClientCache.class, "testDoLookupQueryServiceOnClientCache.MockClientCache"); - QueryService mockQueryService = mock(QueryService.class, "testDoLookupQueryServiceOnClientCache.MockQueryService"); + ClientCache mockClientCache = mock(ClientCache.class); + QueryService mockQueryService = mockQueryService("testDoLookupQueryServiceOnClientCache.MockQueryService"); when(mockClientCache.getLocalQueryService()).thenReturn(mockQueryService); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); indexFactoryBean.setCache(mockClientCache); indexFactoryBean.setQueryService(null); - assertSame(mockQueryService, indexFactoryBean.doLookupQueryService()); + assertThat(indexFactoryBean.doLookupQueryService()).isSameAs(mockQueryService); verify(mockClientCache, times(1)).getLocalQueryService(); verify(mockClientCache, never()).getQueryService(); @@ -274,399 +381,824 @@ public class IndexFactoryBeanTest { @Test public void doLookupQueryServiceOnPeerCache() { - Cache mockCache = mock(Cache.class, "testDoLookupQueryServiceOnPeerCache.MockCache"); - QueryService mockQueryService = mock(QueryService.class, "testDoLookupQueryServiceOnPeerCache.MockQueryService"); - - when(mockCache.getQueryService()).thenReturn(mockQueryService); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); indexFactoryBean.setCache(mockCache); indexFactoryBean.setQueryService(null); - assertSame(mockQueryService, indexFactoryBean.doLookupQueryService()); + assertThat(indexFactoryBean.doLookupQueryService()).isSameAs(mockQueryService); verify(mockCache, times(1)).getQueryService(); } @Test public void lookupQueryServiceFromBeanFactory() { - BeanFactory mockBeanFactory = mock(BeanFactory.class, "testLookupQueryServiceFromBeanFactory.MockBeanFactory"); - QueryService mockQueryService = mock(QueryService.class, "testLookupQueryServiceFromBeanFactory.MockQueryService"); + QueryService mockQueryService = + mockQueryService("testLookupQueryServiceFromBeanFactory.MockQueryService"); when(mockBeanFactory.containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE))) .thenReturn(true); when(mockBeanFactory.getBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), eq(QueryService.class))).thenReturn(mockQueryService); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - indexFactoryBean.setBeanFactory(mockBeanFactory); + assertThat(indexFactoryBean.lookupQueryService()).isSameAs(mockQueryService); - QueryService actualQueryService = indexFactoryBean.lookupQueryService(); - - assertSame(mockQueryService, actualQueryService); - - verify(mockBeanFactory, times(1)).containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE)); - verify(mockBeanFactory, times(1)).getBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), - eq(QueryService.class)); + verify(mockBeanFactory, times(1)).containsBean( + eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE)); + verify(mockBeanFactory, times(1)).getBean( + eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), eq(QueryService.class)); + verify(mockCache, never()).getQueryService(); } @Test public void lookupQueryServiceFromCache() { - BeanFactory mockBeanFactory = mock(BeanFactory.class, "testLookupQueryServiceFromCache.MockBeanFactory"); + + QueryService mockQueryService = mockQueryService("testLookupQueryServiceFromCache.MockQueryService"); when(mockBeanFactory.containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE))) .thenReturn(false); + when(mockCache.getQueryService()).thenReturn(mockQueryService); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - indexFactoryBean.setBeanFactory(mockBeanFactory); indexFactoryBean.setDefine(false); - indexFactoryBean.setQueryService(mockQueryService); + indexFactoryBean.setQueryService(null); - QueryService actualQueryService = indexFactoryBean.lookupQueryService(); + assertThat(indexFactoryBean.lookupQueryService()).isSameAs(mockQueryService); - assertSame(mockQueryService, actualQueryService); + verify(mockBeanFactory, times(1)).containsBean( + eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE)); + verify(mockBeanFactory, never()).getBean( + eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), eq(QueryService.class)); + verify(mockCache, times(1)).getQueryService(); + } - verify(mockBeanFactory, times(1)).containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE)); - verify(mockBeanFactory, never()).getBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), - eq(QueryService.class)); + @Test + public void resolveQueryServiceReturnsIndexFactoryBeanConfiguredQueryService() { + assertThat(newIndexFactoryBean().resolveQueryService()).isSameAs(mockQueryService); + } + + @Test + public void resolveQueryServiceReturnsQueryServiceFromLookup() { + + QueryService mockQueryService = + mockQueryService("testResolveQueryServiceReturnsQueryServiceFromLookup.MockQueryService"); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.setQueryService(null); + + doReturn(mockQueryService).when(indexFactoryBean).lookupQueryService(); + + assertThat(indexFactoryBean.resolveQueryService()).isSameAs(mockQueryService); + + verify(indexFactoryBean, times(1)).lookupQueryService(); + } + + @Test(expected = IllegalStateException.class) + public void resolveQueryServiceThrowsExceptionForUnresolvableQueryService() { + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + try { + indexFactoryBean.setQueryService(null); + doReturn(null).when(indexFactoryBean).lookupQueryService(); + indexFactoryBean.resolveQueryService(); + } + catch (IllegalStateException expected) { + assertThat(expected).hasMessage("QueryService is required to create an Index"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(indexFactoryBean, times(1)).lookupQueryService(); + } } @Test public void registerQueryServiceBeanWhenIndexIsCreated() { + ConfigurableBeanFactory mockBeanFactory = mock(ConfigurableBeanFactory.class, "testRegisterQueryServiceBeanWhenIndexIsCreated.MockBeanFactory"); - QueryService mockQueryService = mock(QueryService.class, "testRegisterQueryServiceBeanWhenIndexIsCreated.MockQueryService"); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); indexFactoryBean.setBeanFactory(mockBeanFactory); indexFactoryBean.setDefine(false); - assertSame(mockQueryService, indexFactoryBean.registerQueryServiceBean(mockQueryService)); + assertThat(indexFactoryBean.registerQueryServiceBean("queryServiceBeanName", mockQueryService)) + .isSameAs(mockQueryService); - verify(mockBeanFactory, never()).registerSingleton( - eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), same(mockQueryService)); + verify(mockBeanFactory, never()).registerSingleton(eq("queryServiceBeanName"), same(mockQueryService)); } @Test public void registerQueryServiceBeanWhenIndexIsDefined() { + ConfigurableBeanFactory mockBeanFactory = mock(ConfigurableBeanFactory.class, "testRegisterQueryServiceBeanWhenIndexIsDefined.MockBeanFactory"); - QueryService mockQueryService = mock(QueryService.class, "testRegisterQueryServiceBeanWhenIndexIsDefined.MockQueryService"); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); indexFactoryBean.setBeanFactory(mockBeanFactory); indexFactoryBean.setDefine(true); - assertSame(mockQueryService, indexFactoryBean.registerQueryServiceBean(mockQueryService)); + assertThat(indexFactoryBean.registerQueryServiceBean("queryServiceBeanName", mockQueryService)) + .isSameAs(mockQueryService); - verify(mockBeanFactory, times(1)).registerSingleton( - eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), same(mockQueryService)); + verify(mockBeanFactory, times(1)) + .registerSingleton(eq("queryServiceBeanName"), same(mockQueryService)); } @Test - public void createIndexReturningNewKeyIndex() throws Exception { - Index mockIndex = mock(Index.class, "createIndexReturningNewKeyIndex.MockIndex"); + public void registerAliasWhenBeanFactoryIsNull() { - QueryService mockQueryService = mock(QueryService.class, "createIndexReturningNewKeyIndex.MockQueryService"); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); - when(mockQueryService.createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example"))).thenReturn(mockIndex); + indexFactoryBean.setBeanFactory(null); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + indexFactoryBean.registerAlias("IndexBean", "TestIndex"); + } + + @Test + public void registerAliasWhenBeanFactoryIsNotConfigurable() { + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.registerAlias("IndexBean", "TestIndex"); + + verifyZeroInteractions(mockBeanFactory); + } + + @Test + public void registerAliasWhenBeanNameAndIndexNameMatch() { + + ConfigurableBeanFactory mockConfigurableBeanFactory = mock(ConfigurableBeanFactory.class); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.setBeanFactory(mockConfigurableBeanFactory); + indexFactoryBean.registerAlias("TestIndex", "TestIndex"); + + assertThat(indexFactoryBean.getBeanFactory()).isSameAs(mockConfigurableBeanFactory); + + verifyZeroInteractions(mockConfigurableBeanFactory); + } + + @Test + public void registerAliasWhenBeanNameAndIndexNameAreDifferent() { + + ConfigurableBeanFactory mockConfigurableBeanFactory = mock(ConfigurableBeanFactory.class); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.setBeanFactory(mockConfigurableBeanFactory); + indexFactoryBean.registerAlias("IndexBean", "TestIndex"); + + assertThat(indexFactoryBean.getBeanFactory()).isSameAs(mockConfigurableBeanFactory); + + verify(mockConfigurableBeanFactory, times(1)) + .registerAlias(eq("IndexBean"), eq("TestIndex")); + } + + @Test + public void createIndexReturnsNewKeyIndex() throws Exception { + + Index mockIndex = mockIndex("testCreateIndexReturnsNewKeyIndex.MockIndex"); + + when(mockQueryService.createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example"))) + .thenReturn(mockIndex); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); indexFactoryBean.setExpression("id"); indexFactoryBean.setFrom("/Example"); indexFactoryBean.setName("KeyIndex"); - indexFactoryBean.setOverride(false); indexFactoryBean.setType("key"); Index actualIndex = indexFactoryBean.createIndex(mockQueryService, "KeyIndex"); - assertSame(mockIndex, actualIndex); + assertThat(actualIndex).isSameAs(mockIndex); - verify(mockQueryService, times(1)).createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example")); + verify(mockQueryService, times(1)) + .createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example")); } @Test - public void createIndexReturningNewHashIndex() throws Exception { - Index mockIndex = mock(Index.class, "createIndexReturningNewHashIndex.MockIndex"); + public void createIndexReturnsNewHashIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "createIndexReturningNewHashIndex.MockQueryService"); + Index mockIndex = mockIndex("testCreateIndexReturnsNewHashIndex.MockIndex"); - when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); when(mockQueryService.createHashIndex(eq("HashIndex"), eq("name"), eq("/Animals"), eq("org.example.Dog"))) .thenReturn(mockIndex); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); indexFactoryBean.setExpression("name"); indexFactoryBean.setFrom("/Animals"); indexFactoryBean.setImports("org.example.Dog"); indexFactoryBean.setName("HashIndex"); - indexFactoryBean.setOverride(false); indexFactoryBean.setType("HasH"); Index actualIndex = indexFactoryBean.createIndex(mockQueryService, "HashIndex"); - assertSame(mockIndex, actualIndex); + assertThat(actualIndex).isSameAs(mockIndex); - verify(mockQueryService, times(1)).createHashIndex(eq("HashIndex"), eq("name"), eq("/Animals"), - eq("org.example.Dog")); + verify(mockQueryService, times(1)) + .createHashIndex(eq("HashIndex"), eq("name"), eq("/Animals"), eq("org.example.Dog")); } @Test - public void createIndexReturningNewFunctionalIndex() throws Exception { - Index mockIndex = mock(Index.class, "createIndexReturningNewFunctionalIndex.MockIndex"); + public void createIndexReturnsNewFunctionalIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, - "createIndexReturningNewFunctionalIndex.MockQueryService"); + Index mockIndex = mockIndex("testCreateIndexReturnsNewFunctionalIndex.MockIndex"); - when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); when(mockQueryService.createIndex(eq("FunctionalIndex"), eq("someField"), eq("/Example"))) .thenReturn(mockIndex); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); indexFactoryBean.setExpression("someField"); indexFactoryBean.setFrom("/Example"); indexFactoryBean.setImports(" "); indexFactoryBean.setName("FunctionalIndex"); - indexFactoryBean.setOverride(false); indexFactoryBean.setType((String) null); Index actualIndex = indexFactoryBean.createIndex(mockQueryService, "FunctionalIndex"); - assertSame(mockIndex, actualIndex); + assertThat(actualIndex).isSameAs(mockIndex); - verify(mockQueryService, times(1)).createIndex(eq("FunctionalIndex"), eq("someField"), eq("/Example")); + verify(mockQueryService, times(1)) + .createIndex(eq("FunctionalIndex"), eq("someField"), eq("/Example")); } - @Test - public void createIndexOverridesExistingIndex() throws Exception { - Index mockExistingIndex = mock(Index.class, "createIndexOverridesExistingIndex.MockExistingIndex"); - Index mockOverridingIndex = mock(Index.class, "createIndexOverridesExistingIndex.MockOverridingIndex"); + @Test(expected = GemfireIndexException.class) + public void createIndexThrowsIndexExistsException() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "createIndexOverridesExistingIndex.MockQueryService"); + Index mockIndex = + mockIndexWithDefinition("MockIndex", "id", "/Example", IndexType.PRIMARY_KEY); - when(mockExistingIndex.getName()).thenReturn("ExistingIndex"); - when(mockOverridingIndex.getName()).thenReturn("OverridingIndex"); - when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockExistingIndex)); - when(mockQueryService.createHashIndex(eq("ExistingIndex"), eq("someField"), eq("/Example"), - eq("example.DomainType"))).thenReturn(mockOverridingIndex); + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - indexFactoryBean.setExpression("someField"); + doThrow(new IndexExistsException("TEST")).when(indexFactoryBean) + .createKeyIndex(any(QueryService.class), anyString(), anyString(), anyString()); + + indexFactoryBean.setExpression("id"); indexFactoryBean.setFrom("/Example"); - indexFactoryBean.setImports("example.DomainType"); - indexFactoryBean.setName("OverridingIndex"); - indexFactoryBean.setType("HASH"); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setType(IndexType.PRIMARY_KEY); - Index actualIndex = indexFactoryBean.createIndex(mockQueryService, "ExistingIndex"); + assertThat(indexFactoryBean.isIgnoreIfExists()).isFalse(); + assertThat(indexFactoryBean.isOverride()).isFalse(); - assertSame(mockOverridingIndex, actualIndex); + try { + indexFactoryBean.createIndex(mockQueryService, "TestIndex"); + } + catch (GemfireIndexException expected) { - verifyZeroInteractions(mockOverridingIndex); - verify(mockExistingIndex, times(1)).getName(); - verify(mockQueryService, times(1)).removeIndex(same(mockExistingIndex)); - verify(mockQueryService, times(1)).createHashIndex(eq("ExistingIndex"), eq("someField"), eq("/Example"), - eq("example.DomainType")); + assertThat(expected).hasMessageStartingWith(String.format( + "An Index with a different name [MockIndex] having the same definition [%s] already exists;" + + " You may attempt to override the existing Index [MockIndex] with the new name [TestIndex]" + + " by setting the 'override' property to 'true'", indexFactoryBean.toBasicIndexDefinition())); + + assertThat(expected).hasCauseInstanceOf(IndexExistsException.class); + assertThat(expected.getCause()).hasMessage("TEST"); + assertThat(expected.getCause()).hasNoCause(); + + throw expected; + } + finally { + verify(mockQueryService, times(1)).getIndexes(); + + verify(indexFactoryBean, times(1)) + .createKeyIndex(eq(mockQueryService), eq("TestIndex"), eq("id"), eq("/Example")); + } + } + + @Test(expected = GemfireIndexException.class) + public void createIndexThrowsIndexExistsExceptionAndCannotFindExistingIndexByDefinition() throws Exception { + + when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doThrow(new IndexExistsException("TEST")).when(indexFactoryBean) + .createKeyIndex(any(QueryService.class), anyString(), anyString(), anyString()); + + indexFactoryBean.setExpression("id"); + indexFactoryBean.setFrom("/Example"); + indexFactoryBean.setIgnoreIfExists(true); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setOverride(true); + indexFactoryBean.setType(IndexType.KEY); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isTrue(); + assertThat(indexFactoryBean.isOverride()).isTrue(); + + try { + indexFactoryBean.createIndex(mockQueryService, "TestIndex"); + } + catch (GemfireIndexException expected) { + + assertThat(expected).hasMessageStartingWith(String.format( + "An Index with a different name [unknown] having the same definition [%s] already exists;" + + " You may attempt to override the existing Index [unknown] with the new name [TestIndex]" + + " by setting the 'override' property to 'true'", indexFactoryBean.toBasicIndexDefinition())); + + assertThat(expected).hasCauseInstanceOf(IndexExistsException.class); + assertThat(expected.getCause()).hasMessage("TEST"); + assertThat(expected.getCause()).hasNoCause(); + + throw expected; + } + finally { + verify(mockQueryService, times(1)).getIndexes(); + + verify(indexFactoryBean, times(1)) + .createKeyIndex(eq(mockQueryService), eq("TestIndex"), eq("id"), eq("/Example")); + } } @Test - public void createIndexReturnsExistingIndex() throws Exception { - Index mockExistingIndex = mock(Index.class, "createIndexReturnsExistingIndex.MockExistingIndex"); - Index mockNewIndex = mock(Index.class, "createIndexReturnsExistingIndex.MockNewIndex"); + public void createIndexThrowsIndexExistsExceptionAndIgnoresThisIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "createIndexReturnsExistingIndex.MockQueryService"); + Index mockIndex = + mockIndexWithDefinition("MockIndex", "id", "/Example", IndexType.PRIMARY_KEY); - when(mockExistingIndex.getName()).thenReturn("ExistingIndex"); - when(mockNewIndex.getName()).thenReturn("NewIndex"); - when(mockQueryService.getIndexes()).thenReturn(Arrays.asList(mockExistingIndex, mockNewIndex)); + when(mockLog.isWarnEnabled()).thenReturn(true); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); - indexFactoryBean.setName("ExistingIndex"); - indexFactoryBean.setOverride(false); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - Index actualIndex = indexFactoryBean.createIndex(mockQueryService, "ExistingIndex"); + doThrow(new IndexExistsException("TEST")).when(indexFactoryBean) + .createKeyIndex(any(QueryService.class), anyString(), anyString(), anyString()); - assertSame(mockExistingIndex, actualIndex); + indexFactoryBean.setExpression("id"); + indexFactoryBean.setFrom("/Example"); + indexFactoryBean.setIgnoreIfExists(true); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setOverride(true); + indexFactoryBean.setType(IndexType.PRIMARY_KEY); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isTrue(); + assertThat(indexFactoryBean.isOverride()).isTrue(); + assertThat(indexFactoryBean.createIndex(mockQueryService, "TestIndex")).isEqualTo(mockIndex); + + verify(indexFactoryBean, times(1)) + .createKeyIndex(eq(mockQueryService), eq("TestIndex"), eq("id"), eq("/Example")); + + verify(mockLog, times(1)).warn( + eq(String.format("WARNING! You are choosing to ignore this Index [TestIndex] and return the existing Index" + + " having the same basic definition [%s] but with a different name [MockIndex];" + + " Make sure no OQL Query Hints refer to this Index by name [TestIndex]", + indexFactoryBean.toBasicIndexDefinition()))); - verify(mockExistingIndex, times(1)).getName(); - verify(mockNewIndex, never()).getName(); verify(mockQueryService, times(1)).getIndexes(); } @Test - public void createIndexThrowsIndexNameConflictExceptionOnOverride() throws Exception { - Index mockExistingIndex = mock(Index.class, - "createIndexThrowsIndexNameConflictExceptionOnOverride.MockExistingIndex"); + public void createIndexThrowsIndexExistsExceptionAndOverridesExistingIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, - "createIndexThrowsIndexNameConflictExceptionOnOverride.MockQueryService"); + Index mockIndex = + mockIndexWithDefinition("MockIndex", "id", "/Example", IndexType.PRIMARY_KEY); - when(mockExistingIndex.getName()).thenReturn("ExistingIndex"); - when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockExistingIndex)); - when(mockQueryService.createIndex(any(String.class), any(String.class), any(String.class))) - .thenThrow(new IndexNameConflictException("TEST")); + Index testIndex = + mockIndexWithDefinition("TestIndex", "id", "/Example", IndexType.PRIMARY_KEY); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + when(mockLog.isWarnEnabled()).thenReturn(true); - indexFactoryBean.setExpression("someField"); + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doThrow(new IndexExistsException("TEST")).doReturn(testIndex).when(indexFactoryBean) + .createKeyIndex(any(QueryService.class), anyString(), anyString(), anyString()); + + indexFactoryBean.setExpression("id"); indexFactoryBean.setFrom("/Example"); - indexFactoryBean.setName("ExistingIndex"); - indexFactoryBean.setType("Functional"); - - try { - expectedException.expect(GemfireIndexException.class); - expectedException.expectCause(isA(IndexNameConflictException.class)); - expectedException.expectMessage( - "Failed to remove the existing Index on override before re-creating Index with name (ExistingIndex)"); - - indexFactoryBean.createIndex(mockQueryService, "ExistingIndex"); - } - finally { - verify(mockExistingIndex, times(1)).getName(); - verify(mockQueryService, times(1)).getIndexes(); - verify(mockQueryService, times(1)).createIndex(eq("ExistingIndex"), eq("someField"), eq("/Example")); - } - } - - @Test - public void createIndexThrowsIndexExistsException() throws Exception { - QueryService mockQueryService = mock(QueryService.class, - "createIndexThrowsIndexExistsException.MockQueryService"); - - when(mockQueryService.getIndexes()).thenReturn(null); - when(mockQueryService.createKeyIndex(eq("NewIndex"), eq("someField"), eq("/Example"))) - .thenThrow(new IndexExistsException("TEST")); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); - - indexFactoryBean.setExpression("someField"); - indexFactoryBean.setFrom("/Example"); - indexFactoryBean.setName("NewIndex"); + indexFactoryBean.setIgnoreIfExists(false); + indexFactoryBean.setName("TestIndex"); indexFactoryBean.setOverride(true); - indexFactoryBean.setType("PRIMARY_KEY"); + indexFactoryBean.setType(IndexType.PRIMARY_KEY); - try { - expectedException.expect(GemfireIndexException.class); - expectedException.expectCause(isA(IndexExistsException.class)); - expectedException.expectMessage( - "An Index with a different name having the same definition as this Index (NewIndex) already exists"); + assertThat(indexFactoryBean.isIgnoreIfExists()).isFalse(); + assertThat(indexFactoryBean.isOverride()).isTrue(); + assertThat(indexFactoryBean.createIndex(mockQueryService, "TestIndex")).isEqualTo(testIndex); - indexFactoryBean.createIndex(mockQueryService, "NewIndex"); - } - finally { - verify(mockQueryService, times(1)).getIndexes(); - verify(mockQueryService, never()).removeIndex(any(Index.class)); - verify(mockQueryService, times(1)).createKeyIndex(eq("NewIndex"), eq("someField"), eq("/Example")); - } + verify(indexFactoryBean, times(2)) + .createKeyIndex(eq(mockQueryService), eq("TestIndex"), eq("id"), eq("/Example")); + + verify(mockLog, times(1)).warn( + eq(String.format("WARNING! You are attempting to 'override' an existing Index [MockIndex]" + + " having the same basic definition [%s] as the Index that will be created by this" + + " IndexFactoryBean [TestIndex]; 'Override' effectively 'renames' the existing Index [MockIndex]" + + " by removing it then recreating it under the new name [TestIndex] with the same definition;" + + " You should be careful to update any existing OQL Query Hints referring to the old" + + " Index name [MockIndex] to now use the new name [TestIndex]", + indexFactoryBean.toBasicIndexDefinition()))); + + verify(mockQueryService, times(1)).getIndexes(); } - @Test - public void createIndexAddsExistingIndexOnAnyException() throws Exception { - final Index mockExistingIndex = mock(Index.class, "createIndexAddsExistingIndexOnAnyException.MockExistingIndex"); - final Index mockIndexTwo = mock(Index.class, "createIndexAddsExistingIndexOnException.MockIndexTwo"); + @Test(expected = GemfireIndexException.class) + public void createIndexThrowsIndexExistsExceptionAndOverrideThrowsRuntimeException() throws Exception { - final List indexes = new ArrayList(1); + Index mockIndex = + mockIndexWithDefinition("MockIndex", "id", "/Example", IndexType.PRIMARY_KEY); - indexes.add(mockIndexTwo); + when(mockLog.isWarnEnabled()).thenReturn(true); - QueryService mockQueryService = mock(QueryService.class, - "createIndexAddsExistingIndexOnAnyException.MockQueryService"); + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)) + .thenReturn(Collections.emptyList()); - when(mockExistingIndex.getName()).thenReturn("ExistingIndex"); - when(mockIndexTwo.getName()).thenReturn("NewIndex"); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - when(mockQueryService.getIndexes()).then(new Answer>() { - private boolean called = false; + doThrow(new IndexExistsException("TEST")).doThrow(new RuntimeException("RETRY")).when(indexFactoryBean) + .createKeyIndex(any(QueryService.class), anyString(), anyString(), anyString()); - private synchronized boolean wasCalledOnce() { - boolean localCalled = this.called; - this.called = true; - return localCalled; - } - - @Override - public Collection answer(final InvocationOnMock invocationOnMock) throws Throwable { - return (wasCalledOnce() ? indexes : Collections.singletonList(mockExistingIndex)); - } - }); - - when(mockQueryService.createIndex(eq("ExistingIndex"), eq("someField"), eq("/Example"))) - .thenThrow(new RuntimeException("TEST")); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); - - indexFactoryBean.setExpression("someField"); + indexFactoryBean.setExpression("id"); indexFactoryBean.setFrom("/Example"); - indexFactoryBean.setName("ExistingIndex"); + indexFactoryBean.setIgnoreIfExists(false); + indexFactoryBean.setName("TestIndex"); indexFactoryBean.setOverride(true); - indexFactoryBean.setType("FUNCTIONAL"); + indexFactoryBean.setType(IndexType.PRIMARY_KEY); - assertEquals(1, indexes.size()); - assertFalse(indexes.contains(mockExistingIndex)); - - Index actualIndex = indexFactoryBean.createIndex(mockQueryService, "ExistingIndex"); - - assertThat(actualIndex, is(sameInstance(mockExistingIndex))); - assertThat(indexes.size(), is(equalTo(2))); - assertThat(indexes.contains(mockExistingIndex), is(true)); - assertThat(indexes.contains(mockIndexTwo), is(true)); - - verify(mockQueryService, times(3)).getIndexes(); - verify(mockQueryService, times(1)).removeIndex(same(mockExistingIndex)); - verify(mockQueryService, times(1)).createIndex(eq("ExistingIndex"), eq("someField"), eq("/Example")); - } - - @Test(expected = RuntimeException.class) - public void createIndexThrowsExceptionOnAbsoluteFailure() throws Exception { - Index mockExistingIndex = mock(Index.class, "createIndexThrowsExceptionOnAbsoluteFailure.MockIndex.Existing"); - - QueryService mockQueryService = mock(QueryService.class, - "createIndexThrowsExceptionOnAbsoluteFailure.MockQueryService"); - - when(mockExistingIndex.getName()).thenReturn("ExistingIndex"); - when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockExistingIndex)); - when(mockQueryService.createIndex(eq("ExistingIndex"), eq("someField"), eq("/Example"))) - .thenThrow(new RuntimeException("test")); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); - - indexFactoryBean.setExpression("someField"); - indexFactoryBean.setFrom("/Example"); - indexFactoryBean.setName("ExistingIndex"); + assertThat(indexFactoryBean.isIgnoreIfExists()).isFalse(); + assertThat(indexFactoryBean.isOverride()).isTrue(); try { - indexFactoryBean.createIndex(mockQueryService, "ExistingIndex"); + indexFactoryBean.createIndex(mockQueryService, "TestIndex"); } - catch (RuntimeException expected) { - assertEquals("test", expected.getMessage()); + catch (GemfireIndexException expected) { + + assertThat(expected).hasMessageStartingWith( + "Attempt to 'override' existing Index [MockIndex] with the Index that would be created by this" + + " IndexFactoryBean [TestIndex] failed; you should verify the state of your system" + + " and make sure the previously existing Index [MockIndex] still exits"); + + assertThat(expected).hasCauseInstanceOf(GemfireIndexException.class); + assertThat(expected.getCause()).hasMessageStartingWith( + String.format("Failed to create Index [%s]", indexFactoryBean.toDetailedIndexDefinition())); + assertThat(expected.getCause()).hasCauseInstanceOf(RuntimeException.class); + assertThat(expected.getCause().getCause()).hasMessageStartingWith("RETRY"); + assertThat(expected.getCause().getCause()).hasNoCause(); + throw expected; } finally { - verify(mockQueryService, times(2)).getIndexes(); - verify(mockQueryService, times(1)).removeIndex(same(mockExistingIndex)); - verify(mockQueryService, times(1)).createIndex(eq("ExistingIndex"), eq("someField"), eq("/Example")); + + verify(indexFactoryBean, times(2)) + .createKeyIndex(eq(mockQueryService), eq("TestIndex"), eq("id"), eq("/Example")); + + verify(mockLog, times(1)).warn( + eq(String.format("WARNING! You are attempting to 'override' an existing Index [MockIndex]" + + " having the same basic definition [%s] as the Index that will be created by this" + + " IndexFactoryBean [TestIndex]; 'Override' effectively 'renames' the existing Index [MockIndex]" + + " by removing it then recreating it under the new name [TestIndex] with the same definition;" + + " You should be careful to update any existing OQL Query Hints referring to the old" + + " Index name [MockIndex] to now use the new name [TestIndex]", + indexFactoryBean.toBasicIndexDefinition()))); + + verify(mockQueryService, times(1)).getIndexes(); + } + } + + @Test(expected = GemfireIndexException.class) + public void createIndexThrowsIndexNameConflictException() throws Exception { + + Index mockIndex = + mockIndexWithDefinition("TestIndex", "purchaseDate", "/Orders", IndexType.HASH); + + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doThrow(new IndexNameConflictException("TEST")).when(indexFactoryBean) + .createFunctionalIndex(eq(mockQueryService), anyString(), anyString(), anyString(), any()); + + indexFactoryBean.setExpression("id"); + indexFactoryBean.setFrom("/Example"); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setType(IndexType.FUNCTIONAL); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isFalse(); + assertThat(indexFactoryBean.isOverride()).isFalse(); + + try { + indexFactoryBean.createIndex(mockQueryService, "TestIndex"); + } + catch (GemfireIndexException expected) { + + String existingIndexDefinition = String.format(IndexFactoryBean.DETAILED_INDEX_DEFINITION, + mockIndex.getName(), mockIndex.getIndexedExpression(), mockIndex.getFromClause(), "unknown", + IndexType.valueOf(mockIndex.getType())); + + assertThat(expected).hasMessageStartingWith(String.format( + "An Index with the same name [TestIndex] having possibly a different definition already exists;" + + " you may choose to ignore this Index definition [%1$s] and use the existing Index" + + " definition [%2$s] by setting the 'ignoreIfExists' property to 'true'", + indexFactoryBean.toDetailedIndexDefinition(), existingIndexDefinition)); + + assertThat(expected).hasCauseInstanceOf(IndexNameConflictException.class); + assertThat(expected.getCause()).hasMessage("TEST"); + assertThat(expected.getCause()).hasNoCause(); + + throw expected; + } + finally { + verify(indexFactoryBean, times(1)) + .createFunctionalIndex(eq(mockQueryService), eq("TestIndex"), + eq("id"), eq("/Example"), eq(null)); + + verifyZeroInteractions(mockLog); + + verify(mockQueryService, times(1)).getIndexes(); + } + } + + @Test(expected = GemfireIndexException.class) + public void createIndexThrowsIndexNameConflictExceptionAndCannotFindExistingIndexByName() throws Exception { + + when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doThrow(new IndexNameConflictException("TEST")).when(indexFactoryBean) + .createFunctionalIndex(eq(mockQueryService), anyString(), anyString(), anyString(), any()); + + indexFactoryBean.setExpression("id"); + indexFactoryBean.setFrom("/Example"); + indexFactoryBean.setIgnoreIfExists(true); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setOverride(true); + indexFactoryBean.setType(IndexType.FUNCTIONAL); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isTrue(); + assertThat(indexFactoryBean.isOverride()).isTrue(); + + try { + indexFactoryBean.createIndex(mockQueryService, "TestIndex"); + } + catch (GemfireIndexException expected) { + + assertThat(expected).hasMessageStartingWith(String.format( + "An Index with the same name [TestIndex] having possibly a different definition already exists;" + + " you may choose to ignore this Index definition [%s] and use the existing Index" + + " definition [unknown] by setting the 'ignoreIfExists' property to 'true'", + indexFactoryBean.toDetailedIndexDefinition())); + + assertThat(expected).hasCauseInstanceOf(IndexNameConflictException.class); + assertThat(expected.getCause()).hasMessage("TEST"); + assertThat(expected.getCause()).hasNoCause(); + + throw expected; + } + finally { + verify(indexFactoryBean, times(1)) + .createFunctionalIndex(eq(mockQueryService), eq("TestIndex"), + eq("id"), eq("/Example"), eq(null)); + + verifyZeroInteractions(mockLog); + + verify(mockQueryService, times(1)).getIndexes(); + } + } + + @Test + public void createIndexThrowsIndexNameConflictExceptionAndIgnoresThisIndex() throws Exception { + + Index mockIndex = + mockIndexWithDefinition("TestIndex", "price", "/Orders", IndexType.FUNCTIONAL); + + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doThrow(new IndexNameConflictException("TEST")).when(indexFactoryBean) + .createFunctionalIndex(eq(mockQueryService), anyString(), anyString(), anyString(), any()); + + indexFactoryBean.setExpression("price"); + indexFactoryBean.setFrom("/Orders"); + indexFactoryBean.setIgnoreIfExists(true); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setOverride(true); + indexFactoryBean.setType(IndexType.FUNCTIONAL); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isTrue(); + assertThat(indexFactoryBean.isOverride()).isTrue(); + assertThat(indexFactoryBean.createIndex(mockQueryService, "TestIndex")).isEqualTo(mockIndex); + + verify(indexFactoryBean, times(1)) + .createFunctionalIndex(eq(mockQueryService), eq("TestIndex"), eq("price"), + eq("/Orders"), eq(null)); + + verifyZeroInteractions(mockLog); + + verify(mockQueryService, times(1)).getIndexes(); + } + + @Test + public void createIndexThrowsIndexNameConflictExceptionAndIgnoresThisIndexWithWarning() throws Exception { + + Index mockIndex = + mockIndexWithDefinition("TestIndex", "id", "/Orders", IndexType.PRIMARY_KEY); + + when(mockLog.isWarnEnabled()).thenReturn(true); + + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doThrow(new IndexNameConflictException("TEST")).when(indexFactoryBean) + .createFunctionalIndex(eq(mockQueryService), anyString(), anyString(), anyString(), any()); + + indexFactoryBean.setExpression("price"); + indexFactoryBean.setFrom("/Orders"); + indexFactoryBean.setIgnoreIfExists(true); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setOverride(true); + indexFactoryBean.setType(IndexType.FUNCTIONAL); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isTrue(); + assertThat(indexFactoryBean.isOverride()).isTrue(); + assertThat(indexFactoryBean.createIndex(mockQueryService, "TestIndex")).isEqualTo(mockIndex); + + String existingIndexDefinition = String.format(IndexFactoryBean.BASIC_INDEX_DEFINITION, + "id", "/Orders", IndexType.PRIMARY_KEY); + + verify(indexFactoryBean, times(1)) + .createFunctionalIndex(eq(mockQueryService), eq("TestIndex"), eq("price"), + eq("/Orders"), eq(null)); + + verify(mockLog, times(1)).warn(String.format( + "WARNING! Returning existing Index [TestIndex] having a definition [%1$s] that does not match" + + " the Index defined [%2$s] by this IndexFactoryBean [TestIndex]", + existingIndexDefinition, indexFactoryBean.toBasicIndexDefinition())); + + verify(mockQueryService, times(1)).getIndexes(); + } + + @Test + public void createIndexThrowsIndexNameConflictExceptionAndOverridesExistingIndex() throws Exception { + + Index mockIndex = + mockIndexWithDefinition("TestIndex", "id", "/Example", IndexType.PRIMARY_KEY); + + Index testIndex = + mockIndexWithDefinition("TestIndex", "id", "/Example", IndexType.PRIMARY_KEY); + + assertThat(mockIndex).isNotSameAs(testIndex); + + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doThrow(new IndexNameConflictException("TEST")).doReturn(testIndex).when(indexFactoryBean) + .createKeyIndex(any(QueryService.class), anyString(), anyString(), anyString()); + + indexFactoryBean.setExpression("id"); + indexFactoryBean.setFrom("/Example"); + indexFactoryBean.setIgnoreIfExists(false); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setOverride(true); + indexFactoryBean.setType(IndexType.PRIMARY_KEY); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isFalse(); + assertThat(indexFactoryBean.isOverride()).isTrue(); + + // NOTE: this test case is also testing the smart "override" handling logic, which just + // returns the existing Index if both the name and definition are the same on when an + // IndexNameConflictException is thrown. + assertThat(indexFactoryBean.createIndex(mockQueryService, "TestIndex")).isEqualTo(mockIndex); + + verify(indexFactoryBean, times(1)) + .createKeyIndex(eq(mockQueryService), eq("TestIndex"), eq("id"), eq("/Example")); + + verifyZeroInteractions(mockLog); + + verify(mockQueryService, times(1)).getIndexes(); + } + + @Test + public void createIndexThrowsIndexNameConflictExceptionAndOverridesExistingIndexWithWarning() throws Exception { + + Index mockIndex = + mockIndexWithDefinition("TestIndex", "price", "/Orders", IndexType.FUNCTIONAL); + + Index testIndex = + mockIndexWithDefinition("TestIndex", "purchaseDate", "/Orders", IndexType.HASH); + + assertThat(mockIndex).isNotSameAs(testIndex); + + when(mockLog.isWarnEnabled()).thenReturn(true); + + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doThrow(new IndexNameConflictException("TEST")).doReturn(testIndex).when(indexFactoryBean) + .createHashIndex(any(QueryService.class), anyString(), anyString(), anyString(), any()); + + indexFactoryBean.setExpression("purchaseDate"); + indexFactoryBean.setFrom("/Orders"); + indexFactoryBean.setIgnoreIfExists(false); + indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setOverride(true); + indexFactoryBean.setType(IndexType.HASH); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isFalse(); + assertThat(indexFactoryBean.isOverride()).isTrue(); + assertThat(indexFactoryBean.createIndex(mockQueryService, "TestIndex")).isEqualTo(testIndex); + + String existingIndexDefinition = String.format(IndexFactoryBean.BASIC_INDEX_DEFINITION, + mockIndex.getIndexedExpression(), mockIndex.getFromClause(), IndexType.valueOf(mockIndex.getType())); + + verify(indexFactoryBean, times(2)).createHashIndex(eq(mockQueryService), + eq("TestIndex"), eq("purchaseDate"), eq("/Orders"), eq(null)); + + verify(mockLog, times(1)).warn(eq(String.format( + "WARNING! Overriding existing Index [TestIndex] having a definition [%1$s] that does not match" + + " the Index defined [%2$s] by this IndexFactoryBean [TestIndex]", + existingIndexDefinition, indexFactoryBean.toBasicIndexDefinition()))); + + verify(mockQueryService, times(1)).getIndexes(); + } + + @Test(expected = GemfireIndexException.class) + public void createIndexThrowsIndexNameConflictExceptionAndOverrideThrowsRuntimeException() throws Exception { + + Index mockIndex = + mockIndexWithDefinition("MockIndex", "purchaseDate", "/Example", + IndexType.HASH); + + when(mockLog.isWarnEnabled()).thenReturn(true); + + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)) + .thenReturn(Collections.emptyList()); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doThrow(new IndexNameConflictException("TEST")).doThrow(new RuntimeException("RETRY")).when(indexFactoryBean) + .createKeyIndex(any(QueryService.class), anyString(), anyString(), anyString()); + + indexFactoryBean.setExpression("id"); + indexFactoryBean.setFrom("/Example"); + indexFactoryBean.setIgnoreIfExists(false); + indexFactoryBean.setName("MockIndex"); + indexFactoryBean.setOverride(true); + indexFactoryBean.setType(IndexType.PRIMARY_KEY); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isFalse(); + assertThat(indexFactoryBean.isOverride()).isTrue(); + + try { + indexFactoryBean.createIndex(mockQueryService, "MockIndex"); + } + catch (GemfireIndexException expected) { + + assertThat(expected).hasMessageStartingWith( + "Attempt to 'override' existing Index [MockIndex] with the Index that would be created by this" + + " IndexFactoryBean [MockIndex] failed; you should verify the state of your system" + + " and make sure the previously existing Index [MockIndex] still exits"); + + assertThat(expected).hasCauseInstanceOf(GemfireIndexException.class); + assertThat(expected.getCause()).hasMessageStartingWith( + String.format("Failed to create Index [%s]", indexFactoryBean.toDetailedIndexDefinition())); + assertThat(expected.getCause()).hasCauseInstanceOf(RuntimeException.class); + assertThat(expected.getCause().getCause()).hasMessageStartingWith("RETRY"); + assertThat(expected.getCause().getCause()).hasNoCause(); + + throw expected; + } + finally { + + String existingIndexDefinition = String.format(IndexFactoryBean.BASIC_INDEX_DEFINITION, + mockIndex.getIndexedExpression(), mockIndex.getFromClause(), IndexType.valueOf(mockIndex.getType())); + + verify(indexFactoryBean, times(2)) + .createKeyIndex(eq(mockQueryService), eq("MockIndex"), eq("id"), eq("/Example")); + + verify(mockLog, times(1)).warn(eq(String.format( + "WARNING! Overriding existing Index [MockIndex] having a definition [%1$s] that does not match" + + " the Index defined [%2$s] by this IndexFactoryBean [MockIndex]", + existingIndexDefinition, indexFactoryBean.toBasicIndexDefinition()))); + + verify(mockQueryService, times(1)).getIndexes(); } } @Test public void createFunctionalIndex() throws Exception { - Index mockIndex = mock(Index.class, "testCreateFunctionalIndex.MockIndex"); - QueryService mockQueryService = mock(QueryService.class, "testCreateFunctionalIndex.MockQueryService"); + Index mockIndex = mockIndex("testCreateFunctionalIndex.MockIndex"); when(mockQueryService.createIndex(eq("FunctionalIndex"), eq("purchaseDate"), eq("/Orders"))) .thenReturn(mockIndex); @@ -674,298 +1206,464 @@ public class IndexFactoryBeanTest { Index actualIndex = indexFactoryBean.createFunctionalIndex(mockQueryService, "FunctionalIndex", "purchaseDate", "/Orders", null); - assertSame(mockIndex, actualIndex); + assertThat(actualIndex).isSameAs(mockIndex); - verify(mockQueryService, times(1)).createIndex(eq("FunctionalIndex"), eq("purchaseDate"), eq("/Orders")); + verify(mockQueryService, times(1)) + .createIndex(eq("FunctionalIndex"), eq("purchaseDate"), eq("/Orders")); verify(mockQueryService, never()).defineIndex(anyString(), anyString(), anyString()); } @Test public void createFunctionalIndexWithImports() throws Exception { - Index mockIndex = mock(Index.class, "testCreateFunctionalIndexWithImports.MockIndex"); - QueryService mockQueryService = mock(QueryService.class, "testCreateFunctionalIndexWithImports.MockQueryService"); + Index mockIndex = mockIndex("testCreateFunctionalIndexWithImports.MockIndex"); - when(mockQueryService.createIndex(eq("FunctionalIndexWithImports"), eq("purchaseDate"), eq("/Orders"), - eq("org.example.Order"))).thenReturn(mockIndex); + when(mockQueryService.createIndex(eq("FunctionalIndexWithImports"), eq("purchaseDate"), + eq("/Orders"), eq("example.Order"))).thenReturn(mockIndex); Index actualIndex = indexFactoryBean.createFunctionalIndex(mockQueryService, "FunctionalIndexWithImports", - "purchaseDate", "/Orders", "org.example.Order"); + "purchaseDate", "/Orders", "example.Order"); - assertSame(mockIndex, actualIndex); + assertThat(actualIndex).isSameAs(mockIndex); + + verify(mockQueryService, times(1)) + .createIndex(eq("FunctionalIndexWithImports"), eq("purchaseDate"), eq("/Orders"), + eq("example.Order")); - verify(mockQueryService, times(1)).createIndex(eq("FunctionalIndexWithImports"), eq("purchaseDate"), - eq("/Orders"), eq("org.example.Order")); verify(mockQueryService, never()).defineIndex(anyString(), anyString(), anyString(), anyString()); } @Test public void defineFunctionalIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testDefineFunctionalIndex.MockQueryService"); - - doAnswer(new Answer() { - @Override public Void answer(final InvocationOnMock invocation) throws Throwable { - return null; - } - }).when(mockQueryService).defineIndex(eq("FunctionalIndex"), eq("purchaseDate"), eq("/Orders")); indexFactoryBean.setDefine(true); Index actualIndex = indexFactoryBean.createFunctionalIndex(mockQueryService, "FunctionalIndex", "purchaseDate", "/Orders", null); - assertTrue(actualIndex instanceof IndexFactoryBean.IndexWrapper); - assertSame(mockQueryService, ((IndexFactoryBean.IndexWrapper) actualIndex).getQueryService()); - assertEquals("FunctionalIndex", ((IndexFactoryBean.IndexWrapper) actualIndex).getIndexName()); + assertThat(actualIndex).isInstanceOf(IndexFactoryBean.IndexWrapper.class); + assertThat(((IndexFactoryBean.IndexWrapper) actualIndex).getIndexName()).isEqualTo("FunctionalIndex"); + assertThat(((IndexFactoryBean.IndexWrapper) actualIndex).getQueryService()).isSameAs(mockQueryService); verify(mockQueryService, never()).createIndex(anyString(), anyString(), anyString()); - verify(mockQueryService, times(1)).defineIndex(eq("FunctionalIndex"), eq("purchaseDate"), eq("/Orders")); + verify(mockQueryService, times(1)) + .defineIndex(eq("FunctionalIndex"), eq("purchaseDate"), eq("/Orders")); } @Test public void defineFunctionalIndexWithImports() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testDefineFunctionalIndexWithImports.MockQueryService"); - - doAnswer(new Answer() { - @Override public Void answer(final InvocationOnMock invocation) throws Throwable { - return null; - } - }).when(mockQueryService).defineIndex(eq("FunctionalIndex"), eq("purchaseDate"), eq("/Orders"), - eq("org.example.Order")); indexFactoryBean.setDefine(true); - Index actualIndex = indexFactoryBean.createFunctionalIndex(mockQueryService, "FunctionalIndexWithImports", - "purchaseDate", "/Orders", "org.example.Order"); + Index actualIndex = indexFactoryBean.createFunctionalIndex(mockQueryService, + "FunctionalIndexWithImports", "purchaseDate", "/Orders", "example.Order"); - assertTrue(actualIndex instanceof IndexFactoryBean.IndexWrapper); - assertSame(mockQueryService, ((IndexFactoryBean.IndexWrapper) actualIndex).getQueryService()); - assertEquals("FunctionalIndexWithImports", ((IndexFactoryBean.IndexWrapper) actualIndex).getIndexName()); + assertThat(actualIndex).isInstanceOf(IndexFactoryBean.IndexWrapper.class); + assertThat(((IndexFactoryBean.IndexWrapper) actualIndex).getIndexName()).isEqualTo("FunctionalIndexWithImports"); + assertThat(((IndexFactoryBean.IndexWrapper) actualIndex).getQueryService()).isSameAs(mockQueryService); verify(mockQueryService, never()).createIndex(anyString(), anyString(), anyString(), anyString()); - verify(mockQueryService, times(1)).defineIndex(eq("FunctionalIndexWithImports"), eq("purchaseDate"), - eq("/Orders"), eq("org.example.Order")); + verify(mockQueryService, times(1)).defineIndex(eq("FunctionalIndexWithImports"), + eq("purchaseDate"), eq("/Orders"), eq("example.Order")); } @Test public void createHashIndex() throws Exception { - Index mockIndex = mock(Index.class, "testCreateHashIndex.MockIndex"); - QueryService mockQueryService = mock(QueryService.class, "testCreateHashIndex.MockQueryService"); + Index mockIndex = mockIndex("testCreateHashIndex.MockIndex"); - when(mockQueryService.createHashIndex(eq("HashIndex"), eq("name"), eq("/People"))).thenReturn(mockIndex); + when(mockQueryService.createHashIndex(eq("HashIndex"), eq("name"), eq("/People"))) + .thenReturn(mockIndex); - Index actualIndex = indexFactoryBean.createHashIndex(mockQueryService, "HashIndex", "name", "/People", " "); + Index actualIndex = indexFactoryBean.createHashIndex(mockQueryService, "HashIndex", + "name", "/People", " "); - assertSame(mockIndex, actualIndex); + assertThat(actualIndex).isSameAs(mockIndex); + + verify(mockQueryService, times(1)) + .createHashIndex(eq("HashIndex"), eq("name"), eq("/People")); - verify(mockQueryService, times(1)).createHashIndex(eq("HashIndex"), eq("name"), eq("/People")); verify(mockQueryService, never()).defineHashIndex(anyString(), anyString(), anyString()); } @Test public void createHashIndexWithImports() throws Exception { - Index mockIndex = mock(Index.class, "testCreateHashIndexWithImports.MockIndex"); - QueryService mockQueryService = mock(QueryService.class, "testCreateHashIndexWithImports.MockQueryService"); + Index mockIndex = mockIndex("testCreateHashIndexWithImports.MockIndex"); when(mockQueryService.createHashIndex(eq("HashIndexWithImports"), eq("name"), eq("/People"), - eq("org.example.Person"))).thenReturn(mockIndex); + eq("example.Person"))).thenReturn(mockIndex); Index actualIndex = indexFactoryBean.createHashIndex(mockQueryService, "HashIndexWithImports", - "name", "/People", "org.example.Person"); + "name", "/People", "example.Person"); - assertSame(mockIndex, actualIndex); + assertThat(actualIndex).isSameAs(mockIndex); + + verify(mockQueryService, times(1)).createHashIndex(eq("HashIndexWithImports"), + eq("name"), eq("/People"), eq("example.Person")); - verify(mockQueryService, times(1)).createHashIndex(eq("HashIndexWithImports"), eq("name"), eq("/People"), - eq("org.example.Person")); verify(mockQueryService, never()).defineHashIndex(anyString(), anyString(), anyString(), anyString()); } @Test public void defineHashIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testDefineHashIndex.MockQueryService"); - - doAnswer(new Answer() { - @Override public Void answer(final InvocationOnMock invocation) throws Throwable { - return null; - } - }).when(mockQueryService).defineHashIndex(eq("HashIndex"), eq("name"), eq("/People")); indexFactoryBean.setDefine(true); Index actualIndex = indexFactoryBean.createHashIndex(mockQueryService, "HashIndex", "name", "/People", " "); - assertTrue(actualIndex instanceof IndexFactoryBean.IndexWrapper); - assertSame(mockQueryService, ((IndexFactoryBean.IndexWrapper) actualIndex).getQueryService()); - assertEquals("HashIndex", ((IndexFactoryBean.IndexWrapper) actualIndex).getIndexName()); + assertThat(actualIndex).isInstanceOf(IndexFactoryBean.IndexWrapper.class); + assertThat(((IndexFactoryBean.IndexWrapper) actualIndex).getIndexName()).isEqualTo("HashIndex"); + assertThat(((IndexFactoryBean.IndexWrapper) actualIndex).getQueryService()).isSameAs(mockQueryService); verify(mockQueryService, never()).createHashIndex(anyString(), anyString(), anyString()); - verify(mockQueryService, times(1)).defineHashIndex(eq("HashIndex"), eq("name"), eq("/People")); + verify(mockQueryService, times(1)) + .defineHashIndex(eq("HashIndex"), eq("name"), eq("/People")); } @Test - public void defineHashIndexWithImports() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testDefineHashIndexWithImports.MockQueryService"); - - doAnswer(new Answer() { - @Override public Void answer(final InvocationOnMock invocation) throws Throwable { - return null; - } - }).when(mockQueryService).defineHashIndex(eq("HashIndexWithImports"), eq("name"), eq("/People"), - eq("org.example.Person")); + public void defineHashIndexWithImport() throws Exception { indexFactoryBean.setDefine(true); Index actualIndex = indexFactoryBean.createHashIndex(mockQueryService, "HashIndexWithImports", - "name", "/People", "org.example.Person"); + "name", "/People", "example.Person"); - assertTrue(actualIndex instanceof IndexFactoryBean.IndexWrapper); - assertSame(mockQueryService, ((IndexFactoryBean.IndexWrapper) actualIndex).getQueryService()); - assertEquals("HashIndexWithImports", ((IndexFactoryBean.IndexWrapper) actualIndex).getIndexName()); + assertThat(actualIndex).isInstanceOf(IndexFactoryBean.IndexWrapper.class); + assertThat(((IndexFactoryBean.IndexWrapper) actualIndex).getIndexName()).isEqualTo("HashIndexWithImports"); + assertThat(((IndexFactoryBean.IndexWrapper) actualIndex).getQueryService()).isSameAs(mockQueryService); verify(mockQueryService, never()).createHashIndex(anyString(), anyString(), anyString(), anyString()); - verify(mockQueryService, times(1)).defineHashIndex(eq("HashIndexWithImports"), eq("name"), eq("/People"), - eq("org.example.Person")); + verify(mockQueryService, times(1)).defineHashIndex(eq("HashIndexWithImports"), + eq("name"), eq("/People"), eq("example.Person")); } @Test public void createKeyIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testCreateKeyIndex.MockQueryService"); - Index mockIndex = mock(Index.class, "testCreateKeyIndex.MockKeyIndex"); + Index mockIndex = mockIndex("testCreateKeyIndex.MockKeyIndex"); - when(mockQueryService.createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example"))).thenReturn(mockIndex); + when(mockQueryService.createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example"))) + .thenReturn(mockIndex); - indexFactoryBean.setDefine(false); + Index actualIndex = indexFactoryBean.createKeyIndex(mockQueryService, "KeyIndex", + "id", "/Example"); - Index actualIndex = indexFactoryBean.createKeyIndex(mockQueryService, "KeyIndex", "id", "/Example"); + assertThat(actualIndex).isSameAs(mockIndex); - assertSame(mockIndex, actualIndex); + verify(mockQueryService, times(1)) + .createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example")); - verify(mockQueryService, times(1)).createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example")); verify(mockQueryService, never()).defineKeyIndex(anyString(), anyString(), anyString()); } @Test public void defineKeyIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testDefineKeyIndex.MockQueryService"); indexFactoryBean.setDefine(true); - Index actualIndex = indexFactoryBean.createKeyIndex(mockQueryService, "KeyIndex", "id", "/Example"); + Index actualIndex = indexFactoryBean.createKeyIndex(mockQueryService, "KeyIndex", + "id", "/Example"); - assertTrue(actualIndex instanceof IndexFactoryBean.IndexWrapper); - - IndexFactoryBean.IndexWrapper indexWrapper = (IndexFactoryBean.IndexWrapper) actualIndex; - - assertSame(mockQueryService, indexWrapper.getQueryService()); - assertEquals("KeyIndex", indexWrapper.getIndexName()); + assertThat(actualIndex).isInstanceOf(IndexFactoryBean.IndexWrapper.class); + assertThat(((IndexFactoryBean.IndexWrapper) actualIndex).getIndexName()).isEqualTo("KeyIndex"); + assertThat(((IndexFactoryBean.IndexWrapper) actualIndex).getQueryService()).isSameAs(mockQueryService); verify(mockQueryService, never()).createKeyIndex(anyString(), anyString(), anyString()); - verify(mockQueryService, times(1)).defineKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example")); + verify(mockQueryService, times(1)).defineKeyIndex(eq("KeyIndex"), + eq("id"), eq("/Example")); } @Test - public void getExistingIndex() { - Index mockIndexOne = mock(Index.class, "testGetExistingIndex.MockIndex.One"); - Index mockIndexTwo = mock(Index.class, "testGetExistingIndex.MockIndex.Two"); - Index mockIndexThree = mock(Index.class, "testGetExistingIndex.MockIndex.Two"); + public void tryToFindExistingIndexByDefinitionReturnsIndex() { - QueryService mockQueryService = mock(QueryService.class, "testGetExistingIndex.MockQueryService"); + Index mockIndexOne = + mockIndexWithDefinition("PrimaryIndex", "id", "/People", IndexType.HASH); + + Index mockIndexTwo = + mockIndexWithDefinition("SecondaryIndex", "id", "/People", IndexType.PRIMARY_KEY); + + Index mockIndexThree = + mockIndexWithDefinition("TernaryIndex", "purchaseDate", "/Orders", + IndexType.FUNCTIONAL); - when(mockIndexOne.getName()).thenReturn("PrimaryIndex"); - when(mockIndexTwo.getName()).thenReturn("SecondaryIndex"); - when(mockIndexThree.getName()).thenReturn("TernaryIndex"); when(mockQueryService.getIndexes()).thenReturn(Arrays.asList(mockIndexOne, mockIndexTwo, mockIndexThree)); - assertNull(indexFactoryBean.getExistingIndex(mockQueryService, null)); - assertNull(indexFactoryBean.getExistingIndex(mockQueryService, "")); - assertNull(indexFactoryBean.getExistingIndex(mockQueryService, " ")); - assertNull(indexFactoryBean.getExistingIndex(mockQueryService, "Primary Index")); - assertNull(indexFactoryBean.getExistingIndex(mockQueryService, "Secondary_Index")); - assertNull(indexFactoryBean.getExistingIndex(mockQueryService, "QuadIndex")); - assertSame(mockIndexOne, indexFactoryBean.getExistingIndex(mockQueryService, "PRIMARYINDEX")); - assertSame(mockIndexTwo, indexFactoryBean.getExistingIndex(mockQueryService, "SecondaryIndex")); - assertSame(mockIndexThree, indexFactoryBean.getExistingIndex(mockQueryService, "ternaryindex")); + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/People", IndexType.PRIMARY_KEY).orElse(null)).isSameAs(mockIndexTwo); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "purchaseDate", "/Orders", IndexType.FUNCTIONAL).orElse(null)).isSameAs(mockIndexThree); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/People", IndexType.HASH).orElse(null)).isSameAs(mockIndexOne); + + verify(mockQueryService, times(3)).getIndexes(); } @Test - public void getObjectLooksUpIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testGetObjectLooksUpIndex.MockQueryService"); + public void tryToFindExistingIndexByDefinitionReturnsNull() { - when(mockQueryService.getIndexes()).then(new Answer>() { - private final AtomicInteger counter = new AtomicInteger(1); - @Override public Collection answer(final InvocationOnMock invocation) throws Throwable { - Index mockIndex = mock(Index.class, "getObjectLooksUpIndex.MockIndex"); - when(mockIndex.getName()).thenReturn(String.format("MockIndex%1$s", counter.getAndIncrement())); - return Collections.singletonList(mockIndex); - } - }); + Index mockIndex = + mockIndexWithDefinition("PrimaryIndex", "id", "/People", IndexType.HASH); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); - indexFactoryBean.setQueryService(mockQueryService); - indexFactoryBean.setName("MockIndex1"); - ReflectionUtils.setField(IndexFactoryBean.class.getDeclaredField("indexName"), indexFactoryBean, "MockIndex1"); + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "key", "/People", IndexType.HASH).orElse(null)).isNull(); - Index actualIndex = indexFactoryBean.getObject(); + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/Persons", IndexType.HASH).orElse(null)).isNull(); - assertNotNull(actualIndex); - assertEquals("MockIndex1", actualIndex.getName()); + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/People", IndexType.KEY).orElse(null)).isNull(); - Index sameIndex = indexFactoryBean.getObject(); - - assertSame(actualIndex, sameIndex); + verify(mockQueryService, times(3)).getIndexes(); } @Test - public void indexFactoryBeanCreatesSingleIndex() { - assertTrue(indexFactoryBean.isSingleton()); + public void tryToFindExistingIndexByDefinitionWithQueryServiceHavingNoIndexesReturnsNull() { + + when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/People", IndexType.KEY).orElse(null)).isNull(); + + verify(mockQueryService, times(1)).getIndexes(); + } + + @Test + public void tryToFindExistingIndexByDefinitionWithQueryServiceReturningNullIndexesReturnsNull() { + + when(mockQueryService.getIndexes()).thenReturn(null); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/People", IndexType.KEY).orElse(null)).isNull(); + + verify(mockQueryService, times(1)).getIndexes(); + } + + @Test + public void tryToFindExistingIndexByNameReturnsIndex() { + + Index mockIndexOne = mockIndex("PrimaryIndex"); + Index mockIndexTwo = mockIndex("SecondaryIndex"); + Index mockIndexThree = mockIndex("TernaryIndex"); + + when(mockQueryService.getIndexes()).thenReturn(Arrays.asList(mockIndexOne, mockIndexTwo, mockIndexThree)); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "PRIMARYINDEX") + .orElse(null)).isSameAs(mockIndexOne); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "SecondaryIndex") + .orElse(null)).isSameAs(mockIndexTwo); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "ternaryindex") + .orElse(null)).isSameAs(mockIndexThree); + + verify(mockQueryService, times(3)).getIndexes(); + } + + @Test + public void tryToFindExistingIndexByNameReturnsNull() { + + Index mockIndexOne = mockIndex("PrimaryIndex"); + Index mockIndexTwo = mockIndex("SecondaryIndex"); + Index mockIndexThree = mockIndex("TernaryIndex"); + + when(mockQueryService.getIndexes()).thenReturn(Arrays.asList(mockIndexOne, mockIndexTwo, mockIndexThree)); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, null) + .orElse(null)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "") + .orElse(null)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, " ") + .orElse(null)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "Primary Index") + .orElse(null)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "Secondary_Index") + .orElse(null)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "QuadIndex") + .orElse(null)).isNull(); + + verify(mockQueryService, times(6)).getIndexes(); + } + + @Test + public void tryToFindExistingIndexByNameWithQueryServiceHavingNoIndexesReturnsNull() { + + when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "TestIndex") + .orElse(null)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "KeyIndex") + .orElse(null)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "IdIndex") + .orElse(null)).isNull(); + + verify(mockQueryService, times(3)).getIndexes(); + } + + @Test + public void tryToFindExistingIndexByNameWithQueryServiceReturningNullIndexesReturnsNull() { + + when(mockQueryService.getIndexes()).thenReturn(null); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "TestIndex") + .orElse(null)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "KeyIndex") + .orElse(null)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "IdIndex") + .orElse(null)).isNull(); + + verify(mockQueryService, times(3)).getIndexes(); + } + + @Test + public void getObjectReturnsFactoryIndex() throws Exception { + + Index mockIndex = mockIndex("testGetObjectReturnsFactoryIndex.MockIndex"); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doReturn(mockIndex).when(indexFactoryBean).createIndex(eq(mockQueryService), + eq("testGetObjectReturnsFactoryIndex.MockIndex")); + + indexFactoryBean.setName("testGetObjectReturnsFactoryIndex.MockIndex"); + indexFactoryBean.setExpression("id"); + indexFactoryBean.setFrom("/Mock"); + indexFactoryBean.afterPropertiesSet(); + + assertThat(indexFactoryBean.getIndex()).isEqualTo(mockIndex); + assertThat(indexFactoryBean.getObject()).isEqualTo(mockIndex); + assertThat(indexFactoryBean.getObject()).isEqualTo(mockIndex); + + verify(indexFactoryBean, times(1)) + .createIndex(eq(mockQueryService), eq("testGetObjectReturnsFactoryIndex.MockIndex")); + } + + @Test + public void getObjectReturnsLookedUpIndex() throws Exception { + + Index mockIndexOne = mockIndex("MockIndexOne"); + Index mockIndexTwo = mockIndex("MockIndexTwo"); + + when(mockQueryService.getIndexes()).thenReturn(Arrays.asList(mockIndexOne, mockIndexTwo)); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.setName("MockIndexTwo"); + + assertThat(indexFactoryBean.getObject()).isEqualTo(mockIndexTwo); + } + + @Test + public void getObjectReturnsNull() { + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.setName("TestIndex"); + + assertThat(indexFactoryBean.getObject()).isNull(); + } + + @Test + public void getObjectTypeReturnsIndexClassType() { + assertThat(newIndexFactoryBean().getObjectType()).isEqualTo(Index.class); + } + + @Test + @SuppressWarnings("all") + public void getObjectTypeReturnsMockIndexClassType() throws Exception { + + Index mockIndex = mockIndex("MockIndex"); + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + doReturn(mockIndex).when(indexFactoryBean).getIndex(); + + assertThat(indexFactoryBean.getObjectType()).isEqualTo(mockIndex.getClass()); + } + + @Test + public void isSingletonReturnsTrue() { + assertThat(indexFactoryBean.isSingleton()).isTrue(); + } + + @Test + public void setAndIsIgnoreIfExists() { + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isFalse(); + + indexFactoryBean.setIgnoreIfExists(true); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isTrue(); + + indexFactoryBean.setIgnoreIfExists(false); + + assertThat(indexFactoryBean.isIgnoreIfExists()).isFalse(); + } + + @Test + public void setAndIsOverride() { + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + assertThat(indexFactoryBean.isOverride()).isFalse(); + + indexFactoryBean.setOverride(true); + + assertThat(indexFactoryBean.isOverride()).isTrue(); + + indexFactoryBean.setOverride(false); + + assertThat(indexFactoryBean.isOverride()).isFalse(); } @Test public void defineMultipleIndexesWithSeparateIndexFactoryBeansSameSpringContext() throws Exception { + ConfigurableBeanFactory mockBeanFactory = mock(ConfigurableBeanFactory.class, "testDefineMultipleIndexesWithSeparateIndexFactoryBeansSameSpringContext.MockBeanFactory"); - Cache mockCacheOne = mock(Cache.class, "testDefineMultipleIndexesWithSeparateIndexFactoryBeansSameSpringContext.MockCacheOne"); - Cache mockCacheTwo = mock(Cache.class, "testDefineMultipleIndexesWithSeparateIndexFactoryBeansSameSpringContext.MockCacheTwo"); + Cache mockCacheOne = + mock(Cache.class, "testDefineMultipleIndexesWithSeparateIndexFactoryBeansSameSpringContext.MockCacheOne"); - QueryService mockQueryServiceOne = mock(QueryService.class, - "testDefineMultipleIndexesWithSeparateIndexFactoryBeansSameSpringContext.MockQueryServiceOne"); + Cache mockCacheTwo = + mock(Cache.class, "testDefineMultipleIndexesWithSeparateIndexFactoryBeansSameSpringContext.MockCacheTwo"); - QueryService mockQueryServiceTwo = mock(QueryService.class, - "testDefineMultipleIndexesWithSeparateIndexFactoryBeansSameSpringContext.MockQueryServiceTwo"); + AtomicReference queryServiceReference = new AtomicReference<>(null); - final AtomicReference queryServiceReference = new AtomicReference(null); + doAnswer(invocation -> (queryServiceReference.get() != null)).when(mockBeanFactory) + .containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE)); - doAnswer(new Answer() { - @Override public Boolean answer(final InvocationOnMock invocation) throws Throwable { - return (queryServiceReference.get() != null); - } - }).when(mockBeanFactory).containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE)); + doAnswer(invocation -> queryServiceReference.get()).when(mockBeanFactory) + .getBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), eq(QueryService.class)); - doAnswer(new Answer() { - @Override public QueryService answer(final InvocationOnMock invocation) throws Throwable { - return queryServiceReference.get(); - } - }).when(mockBeanFactory).getBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), - eq(QueryService.class)); + doAnswer(invocation -> { - doAnswer(new Answer() { - @Override public Void answer(final InvocationOnMock invocation) throws Throwable { - assertEquals(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE, - invocation.getArgument(0)); - queryServiceReference.compareAndSet(null, invocation.getArgument(1)); - return null; - } + assertEquals(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE, + invocation.getArgument(0)); + + queryServiceReference.compareAndSet(null, invocation.getArgument(1)); + + return null; }).when(mockBeanFactory).registerSingleton(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), any(QueryService.class)); - when(mockCacheOne.getQueryService()).thenReturn(mockQueryServiceOne); - when(mockCacheTwo.getQueryService()).thenReturn(mockQueryServiceTwo); + when(mockCacheOne.getQueryService()).thenReturn(mockQueryService); IndexFactoryBean indexFactoryBeanOne = new IndexFactoryBean(); @@ -990,31 +1688,36 @@ public class IndexFactoryBeanTest { indexFactoryBeanTwo.setType("HASH"); indexFactoryBeanTwo.afterPropertiesSet(); - verify(mockBeanFactory, times(2)).containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE)); - verify(mockBeanFactory, times(1)).getBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), - eq(QueryService.class)); - verify(mockBeanFactory, times(1)).registerSingleton(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), - same(mockQueryServiceOne)); + verify(mockBeanFactory, times(2)) + .containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE)); + + verify(mockBeanFactory, times(1)) + .getBean(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), eq(QueryService.class)); + + verify(mockBeanFactory, times(1)) + .registerSingleton(eq(GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE), + same(mockQueryService)); + verify(mockCacheOne, times(1)).getQueryService(); verify(mockCacheTwo, never()).getQueryService(); - verify(mockQueryServiceOne, times(1)).defineKeyIndex(eq("PersonIdIndex"), eq("id"), eq("/People")); - verify(mockQueryServiceOne, times(1)).defineHashIndex(eq("PurchaseDateIndex"), eq("purchaseDate"), eq("/Orders"), - eq("org.example.Order")); - verify(mockQueryServiceTwo, never()).defineHashIndex(anyString(), anyString(), anyString()); - verify(mockQueryServiceTwo, never()).defineHashIndex(anyString(), anyString(), anyString()); - verify(mockQueryServiceTwo, never()).defineIndex(anyString(), anyString(), anyString()); - verify(mockQueryServiceTwo, never()).defineIndex(anyString(), anyString(), anyString(), anyString()); - verify(mockQueryServiceTwo, never()).defineKeyIndex(anyString(), anyString(), anyString()); + + verify(mockQueryService, times(1)) + .defineKeyIndex(eq("PersonIdIndex"), eq("id"), eq("/People")); + + verify(mockQueryService, times(1)) + .defineHashIndex(eq("PurchaseDateIndex"), eq("purchaseDate"), eq("/Orders"), + eq("org.example.Order")); } @Test @SuppressWarnings("deprecation") public void indexWrapperDelegation() { - Index mockIndex = mock(Index.class, "indexWrapperDelegation.MockIndex"); - IndexStatistics mockIndexStats = mock(IndexStatistics.class, "indexWrapperDelegation.MockIndexStats"); + Index mockIndex = mockIndex("testIndexWrapperDelegation.MockIndex"); - QueryService mockQueryService = mock(QueryService.class, "indexWrapperDelegation.MockQueryService"); + IndexStatistics mockIndexStats = mock(IndexStatistics.class, "testIndexWrapperDelegation.MockIndexStats"); + + QueryService mockQueryService = mock(QueryService.class, "testIndexWrapperDelegation.MockQueryService"); when(mockIndex.getCanonicalizedFromClause()).thenReturn("/Example"); when(mockIndex.getCanonicalizedIndexedExpression()).thenReturn("ID"); @@ -1027,28 +1730,30 @@ public class IndexFactoryBeanTest { when(mockIndex.getType()).thenReturn(org.apache.geode.cache.query.IndexType.HASH); when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); - IndexFactoryBean.IndexWrapper indexWrapper = new IndexFactoryBean.IndexWrapper(mockQueryService, "MockIndex"); + IndexFactoryBean.IndexWrapper indexWrapper = + new IndexFactoryBean.IndexWrapper(mockQueryService, "MockIndex"); - assertNotNull(indexWrapper); - assertEquals("MockIndex", indexWrapper.getIndexName()); - assertSame(mockQueryService, indexWrapper.getQueryService()); + assertThat(indexWrapper).isNotNull(); + assertThat(indexWrapper.getIndexName()).isEqualTo("MockIndex"); + assertThat(indexWrapper.getQueryService()).isEqualTo(mockQueryService); - Index actualIndex = indexWrapper.getIndex(); + Index actualIndex = indexWrapper.resolveIndex(); - assertSame(mockIndex, actualIndex); - assertEquals("/Example", indexWrapper.getCanonicalizedFromClause()); - assertEquals("ID", indexWrapper.getCanonicalizedIndexedExpression()); - assertEquals("identifier", indexWrapper.getCanonicalizedProjectionAttributes()); - assertEquals("Example", indexWrapper.getFromClause()); - assertEquals("id", indexWrapper.getIndexedExpression()); - assertEquals("MockIndex", indexWrapper.getName()); - assertEquals("id", indexWrapper.getProjectionAttributes()); - assertSame(mockIndexStats, indexWrapper.getStatistics()); - assertEquals(org.apache.geode.cache.query.IndexType.HASH, indexWrapper.getType()); + assertThat(actualIndex).isSameAs(mockIndex); - Index sameIndex = indexWrapper.getIndex(); + assertThat(indexWrapper.getCanonicalizedFromClause()).isEqualTo("/Example"); + assertThat(indexWrapper.getCanonicalizedIndexedExpression()).isEqualTo("ID"); + assertThat(indexWrapper.getCanonicalizedProjectionAttributes()).isEqualTo("identifier"); + assertThat(indexWrapper.getFromClause()).isEqualTo("Example"); + assertThat(indexWrapper.getIndexedExpression()).isEqualTo("id"); + assertThat(indexWrapper.getName()).isEqualTo("MockIndex"); + assertThat(indexWrapper.getProjectionAttributes()).isEqualTo("id"); + assertThat(indexWrapper.getStatistics()).isSameAs(mockIndexStats); + assertThat(indexWrapper.getType()).isEqualTo(org.apache.geode.cache.query.IndexType.HASH); - assertSame(actualIndex, sameIndex); + Index sameIndex = indexWrapper.resolveIndex(); + + assertThat(sameIndex).isSameAs(actualIndex); } @Test(expected = IllegalArgumentException.class) @@ -1057,40 +1762,45 @@ public class IndexFactoryBeanTest { new IndexFactoryBean.IndexWrapper(null, "TestIndex"); } catch (IllegalArgumentException expected) { - assertEquals("QueryService must not be null", expected.getMessage()); + assertThat(expected).hasMessage("QueryService is required"); + assertThat(expected).hasNoCause(); + throw expected; } } @Test(expected = IllegalArgumentException.class) - public void createIndexWrapperWithUnspecifiedIndexName() { + public void createIndexWrapperWithNoIndexName() { try { - new IndexFactoryBean.IndexWrapper(mock(QueryService.class), " "); + new IndexFactoryBean.IndexWrapper(mockQueryService, " "); } catch (IllegalArgumentException expected) { - assertEquals("The name of the Index must be specified!", expected.getMessage()); + assertThat(expected).hasMessage("Name of Index is required"); + assertThat(expected).hasNoCause(); + throw expected; } } @Test(expected = GemfireIndexException.class) - public void indexWrapperGetIndexWhenIndexNotFound() { - QueryService mockQueryService = mock(QueryService.class, "indexWrapperGetIndexWhenIndexNotFound.MockQueryService"); + public void indexWrapperGetIndexThrowsExceptionWhenIndexNotFound() { - when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); + when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); - IndexFactoryBean.IndexWrapper indexWrapper = new IndexFactoryBean.IndexWrapper(mockQueryService, "NonExistingIndex"); + IndexFactoryBean.IndexWrapper indexWrapper = + new IndexFactoryBean.IndexWrapper(mockQueryService, "NonExistingIndex"); - assertNotNull(indexWrapper); - assertEquals("NonExistingIndex", indexWrapper.getIndexName()); - assertSame(mockQueryService, indexWrapper.getQueryService()); + assertThat(indexWrapper).isNotNull(); + assertThat(indexWrapper.getIndexName()).isEqualTo("NonExistingIndex"); + assertThat(indexWrapper.getQueryService()).isSameAs(mockQueryService); try { - indexWrapper.getIndex(); + indexWrapper.resolveIndex(); } catch (GemfireIndexException expected) { - assertThat(expected.getMessage(), startsWith("index with name (NonExistingIndex) was not found")); - assertTrue(expected.getCause() instanceof IndexInvalidException); + assertThat(expected).hasMessage("Index with name [NonExistingIndex] was not found"); + assertThat(expected).hasNoCause(); + throw expected; } } diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/EnableIndexingConfigurationUnitTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableIndexingConfigurationUnitTests.java index 2a5e90a0..866e1159 100644 --- a/src/test/java/org/springframework/data/gemfire/config/annotation/EnableIndexingConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableIndexingConfigurationUnitTests.java @@ -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 indexes = new HashSet<>(); + private static final Set 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 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.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; - } - } } diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/test/entities/PartitionRegionEntity.java b/src/test/java/org/springframework/data/gemfire/config/annotation/test/entities/PartitionRegionEntity.java index a88dc4ec..dce72e06 100644 --- a/src/test/java/org/springframework/data/gemfire/config/annotation/test/entities/PartitionRegionEntity.java +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/test/entities/PartitionRegionEntity.java @@ -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") diff --git a/src/test/java/org/springframework/data/gemfire/config/xml/IndexNamespaceTest.java b/src/test/java/org/springframework/data/gemfire/config/xml/IndexNamespaceTest.java index 0ccf759b..a492c04b 100644 --- a/src/test/java/org/springframework/data/gemfire/config/xml/IndexNamespaceTest.java +++ b/src/test/java/org/springframework/data/gemfire/config/xml/IndexNamespaceTest.java @@ -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(); + } } diff --git a/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneOperationsIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneOperationsIntegrationTests.java index 43b85b3c..7632a6ca 100644 --- a/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneOperationsIntegrationTests.java @@ -129,6 +129,7 @@ public class LuceneOperationsIntegrationTests { @Test public void findsDoctorDoesAsTypePersonSuccessfully() { + Collection doctorDoes = template.queryForValues("title: Doctor*", "title"); assertThat(doctorDoes).isNotNull(); @@ -139,6 +140,7 @@ public class LuceneOperationsIntegrationTests { @Test @SuppressWarnings("all") public void findsMasterDoesAsTypeUserSuccessfully() { + List masterDoes = template.query("title: Master*", "title", User.class); assertThat(masterDoes).isNotNull(); diff --git a/src/test/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupportUnitTests.java b/src/test/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupportUnitTests.java index 1e600e74..648da8b9 100644 --- a/src/test/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupportUnitTests.java +++ b/src/test/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupportUnitTests.java @@ -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 extends AbstractFactoryBeanSupport { @Override diff --git a/src/test/resources/org/springframework/data/gemfire/config/xml/index-ns.xml b/src/test/resources/org/springframework/data/gemfire/config/xml/index-ns.xml index 0835a84c..9181829c 100644 --- a/src/test/resources/org/springframework/data/gemfire/config/xml/index-ns.xml +++ b/src/test/resources/org/springframework/data/gemfire/config/xml/index-ns.xml @@ -30,9 +30,12 @@ - + + +