diff --git a/src/main/asciidoc/reference/indexing.adoc b/src/main/asciidoc/reference/indexing.adoc new file mode 100644 index 00000000..9476782c --- /dev/null +++ b/src/main/asciidoc/reference/indexing.adoc @@ -0,0 +1,238 @@ +[[bootstrap:indexing]] += Configuring an Index + +Pivotal GemFire allows Indexes (or Indices) to be created on Region data to improve the performance of OQL queries. + +In _Spring Data GemFire_ (SDG), Indexes are declared with the `index` element: + +[source,xml] +---- + +---- + +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. + +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. + +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 b275452d..bf477a0b 100644 --- a/src/main/java/org/springframework/data/gemfire/GemfireIndexException.java +++ b/src/main/java/org/springframework/data/gemfire/GemfireIndexException.java @@ -38,6 +38,14 @@ import com.gemstone.gemfire.cache.query.IndexNameConflictException; @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); } @@ -77,5 +85,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/GemfireUtils.java b/src/main/java/org/springframework/data/gemfire/GemfireUtils.java index e7e87f0a..e63cf40d 100644 --- a/src/main/java/org/springframework/data/gemfire/GemfireUtils.java +++ b/src/main/java/org/springframework/data/gemfire/GemfireUtils.java @@ -18,12 +18,11 @@ package org.springframework.data.gemfire; import java.util.concurrent.ConcurrentMap; -import org.springframework.data.gemfire.util.DistributedSystemUtils; -import org.springframework.util.ClassUtils; - import com.gemstone.gemfire.cache.CacheFactory; import com.gemstone.gemfire.cache.Region; -import com.gemstone.gemfire.cache.client.ClientCacheFactory; + +import org.springframework.data.gemfire.util.CacheUtils; +import org.springframework.util.ClassUtils; /** * GemfireUtils is an abstract utility class encapsulating common functionality to access features and capabilities @@ -34,30 +33,10 @@ import com.gemstone.gemfire.cache.client.ClientCacheFactory; * @since 1.3.3 */ @SuppressWarnings("unused") -public abstract class GemfireUtils extends DistributedSystemUtils { +public abstract class GemfireUtils extends CacheUtils { public final static String GEMFIRE_VERSION = CacheFactory.getVersion(); - public static boolean closeCache() { - try { - CacheFactory.getAnyInstance().close(); - return true; - } - catch (Exception ignore) { - return false; - } - } - - public static boolean closeClientCache() { - try { - ClientCacheFactory.getAnyInstance().close(); - return true; - } - catch (Exception ignore) { - return false; - } - } - public static boolean isGemfireVersionGreaterThanEqual(double expectedVersion) { double actualVersion = Double.parseDouble(GEMFIRE_VERSION.substring(0, 3)); return actualVersion >= expectedVersion; @@ -103,5 +82,4 @@ public abstract class GemfireUtils extends DistributedSystemUtils { //System.out.printf("Is GemFire Version 6.5 of Above? %1$s%n", isGemfireVersion65OrAbove()); //System.out.printf("Is GemFire Version 7.0 of Above? %1$s%n", isGemfireVersion7OrAbove()); } - } diff --git a/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java b/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java index b94d2899..d66abe34 100644 --- a/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java @@ -16,14 +16,8 @@ package org.springframework.data.gemfire; -import java.util.Collection; - -import org.springframework.beans.factory.BeanNameAware; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.data.gemfire.util.CollectionUtils; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; +import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeCollection; +import static org.springframework.data.gemfire.util.SpringUtils.defaultIfNull; import com.gemstone.gemfire.cache.RegionService; import com.gemstone.gemfire.cache.client.ClientCache; @@ -32,23 +26,40 @@ import com.gemstone.gemfire.cache.query.IndexExistsException; import com.gemstone.gemfire.cache.query.IndexNameConflictException; import com.gemstone.gemfire.cache.query.QueryService; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.data.gemfire.support.AbstractFactoryBeanSupport; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + /** - * Spring FactoryBean for easy declarative creation of GemFire Indexes. - * + * Spring {@link FactoryBean} for simple and easy declarative creation of GemFire Indexes. + * * @author Costin Leau * @author David Turanski * @author John Blum - * @see org.springframework.beans.factory.InitializingBean - * @see org.springframework.beans.factory.BeanNameAware * @see org.springframework.beans.factory.FactoryBean + * @see org.springframework.beans.factory.InitializingBean + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory + * @see org.springframework.data.gemfire.support.AbstractFactoryBeanSupport + * @see com.gemstone.gemfire.cache.Region * @see com.gemstone.gemfire.cache.RegionService * @see com.gemstone.gemfire.cache.query.Index * @see com.gemstone.gemfire.cache.query.QueryService * @since 1.0.0 */ -public class IndexFactoryBean implements InitializingBean, FactoryBean, BeanNameAware { +public class IndexFactoryBean extends AbstractFactoryBeanSupport implements InitializingBean { - private boolean override = true; + 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 ignoreIfExists = false; + private boolean override = false; private Index index; @@ -58,53 +69,105 @@ public class IndexFactoryBean implements InitializingBean, FactoryBean, B private RegionService cache; - private String beanName; private String expression; private String from; private String imports; private String name; + @Override public void afterPropertiesSet() throws Exception { - Assert.notNull(cache, "The GemFire Cache reference must not be null!"); - queryService = lookupQueryService(); + this.cache = resolveCache(); - 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"); + String indexName = resolveIndexName(); - if (IndexType.isKey(indexType)) { - Assert.isNull(imports, "The 'imports' property is not supported for a Key Index."); - } + this.queryService = resolveQueryService(); - String indexName = (StringUtils.hasText(name) ? name : beanName); + assertIndexDefinitionConfiguration(); - Assert.hasText(indexName, "The Index bean id or name is required!"); + this.index = createIndex(this.queryService, indexName); - index = createIndex(queryService, indexName); + registerAlias(getBeanName(), indexName); } + /* (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() { + + RegionService resolvedCache = (this.cache != null ? this.cache : GemfireUtils.resolveGemFireCache()); + + Assert.state(resolvedCache != null, "Cache is required"); + + return resolvedCache; + } + + /* (non-Javadoc) */ + String resolveIndexName() { + + String resolvedIndexName = (StringUtils.hasText(this.name) ? this.name : getBeanName()); + + Assert.hasText(resolvedIndexName, "Index name is required"); + + return resolvedIndexName; + } + + /* (non-Javadoc) */ + QueryService resolveQueryService() { + + QueryService resolvedQueryService = (this.queryService != null ? this.queryService : lookupQueryService()); + + Assert.state(resolvedQueryService != null, "QueryService is required to create an Index"); + + return resolvedQueryService; + } + + /* (non-Javadoc) */ QueryService lookupQueryService() { + return (queryService != null ? queryService : (cache instanceof ClientCache ? ((ClientCache) cache).getLocalQueryService() : cache.getQueryService())); } - Index createIndex(QueryService queryService, String indexName) throws Exception { - Index existingIndex = getExistingIndex(queryService, indexName); + /* (non-Javadoc) */ + void registerAlias(String beanName, String indexName) { - if (existingIndex != null) { - if (override) { - queryService.removeIndex(existingIndex); - } - else { - return existingIndex; + BeanFactory beanFactory = getBeanFactory(); + + if (beanFactory instanceof ConfigurableBeanFactory) { + if (beanName != null && !beanName.equals(indexName)) { + ((ConfigurableBeanFactory) beanFactory).registerAlias(beanName, indexName); } } + } + + /* (non-Javadoc) */ + Index createIndex(QueryService queryService, String indexName) throws Exception { + return createIndex(queryService, indexName, false); + } + + /* (non-Javadoc) */ + private Index createIndex(QueryService queryService, String indexName, boolean retryAttempted) throws Exception { + + IndexType indexType = this.indexType; + + String expression = this.expression; + String from = this.from; + String imports = this.imports; try { if (IndexType.isKey(indexType)) { - return queryService.createKeyIndex(indexName, expression, from); + return createKeyIndex(queryService, indexName, expression, from); } else if (IndexType.isHash(indexType)) { return createHashIndex(queryService, indexName, expression, from, imports); @@ -113,36 +176,199 @@ public class IndexFactoryBean implements InitializingBean, FactoryBean, B 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 + + Index existingIndexByDefinition = + tryToFindExistingIndexByDefinition(queryService, expression, from, indexType); + + if (isIgnorable(existingIndexByDefinition)) { + + 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(), existingIndexByDefinition.getName()); + + return handleIgnore(existingIndexByDefinition); } + else if (isOverridable(existingIndexByDefinition, retryAttempted)) { - throw e; + // 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]", + existingIndexByDefinition.getName(), toBasicIndexDefinition(), indexName); + + return handleOverride(existingIndexByDefinition, queryService, indexName); + } + else { + + String existingIndexName = (existingIndexByDefinition != null + ? existingIndexByDefinition.getName() : "unknown"); + + throw 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 + + Index existingIndexByName = tryToFindExistingIndexByName(queryService, indexName); + + if (isIgnorable(existingIndexByName)) { + return handleIgnore(warnOnIndexDefinitionMismatch(existingIndexByName, indexName, "Returning")); + } + else if (isOverridable(existingIndexByName, retryAttempted)) { + return handleSmartOverride(warnOnIndexDefinitionMismatch(existingIndexByName, indexName, + "Overriding"), queryService, indexName); + } + else { + + String existingIndexDefinition = (existingIndexByName != null + ? String.format(DETAILED_INDEX_DEFINITION, existingIndexByName.getName(), + existingIndexByName.getIndexedExpression(), existingIndexByName.getFromClause(), "unknown", + existingIndexByName.getType()) + : "unknown"); + + throw 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) */ + private boolean isIgnorable(Index existingIndex) { + return (isIgnoreIfExists() && existingIndex != null); + } + + /* (non-Javadoc) */ + @SuppressWarnings("all") + private boolean isIndexDefinitionMatch(Index index) { + + boolean result = false; + + if (index != null) { + + IndexType thisIndexType = defaultIfNull(this.indexType, IndexType.FUNCTIONAL); + + result = ObjectUtils.nullSafeEquals(index.getIndexedExpression(), this.expression) + && ObjectUtils.nullSafeEquals(index.getFromClause(), this.from) + && ObjectUtils.nullSafeEquals(IndexType.valueOf(index.getType()), thisIndexType); + + } + + return result; + } + + /* (non-Javadoc) */ + private boolean isNotIndexDefinitionMatch(Index index) { + return !isIndexDefinitionMatch(index); + } + + /* (non-Javadoc) */ + private boolean isOverridable(Index existingIndex, boolean retryAttempted) { + return (isOverride() && existingIndex != null && !retryAttempted); + } + + /* (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 (existingIndex.getName().equalsIgnoreCase(indexName) && isIndexDefinitionMatch(existingIndex) + ? existingIndex + : 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) */ + Index createKeyIndex(QueryService queryService, String indexName, String expression, String from) throws Exception { + return queryService.createKeyIndex(indexName, expression, from); + } + + /* (non-Javadoc) */ + Index createHashIndex(QueryService queryService, String indexName, String expression, String from, String imports) + throws Exception { + + boolean hasImports = StringUtils.hasText(imports); + + if (hasImports) { + return queryService.createHashIndex(indexName, expression, from, imports); + } + else { + return queryService.createHashIndex(indexName, expression, from); + } + } + + /* (non-Javadoc) */ Index createFunctionalIndex(QueryService queryService, String indexName, String expression, String from, String imports) throws Exception { + if (StringUtils.hasText(imports)) { return queryService.createIndex(indexName, expression, from, imports); } @@ -151,18 +377,26 @@ public class IndexFactoryBean implements InitializingBean, FactoryBean, B } } - Index createHashIndex(QueryService queryService, String indexName, String expression, String from, - String imports) throws Exception { - if (StringUtils.hasText(imports)) { - return queryService.createHashIndex(indexName, expression, from, imports); - } - else { - return queryService.createHashIndex(indexName, expression, from); + /* (non-Javadoc) */ + Index tryToFindExistingIndexByDefinition(QueryService queryService, + String expression, String fromClause, IndexType indexType) { + + for (Index index : nullSafeCollection(queryService.getIndexes())) { + if (index.getIndexedExpression().equalsIgnoreCase(expression) + && index.getFromClause().equalsIgnoreCase(fromClause) + && indexType.equals(IndexType.valueOf(index.getType()))) { + + return index; + } } + + return null; } - Index getExistingIndex(QueryService queryService, String indexName) { - for (Index index : CollectionUtils.nullSafeCollection(queryService.getIndexes())) { + /* (non-Javadoc) */ + Index tryToFindExistingIndexByName(QueryService queryService, String indexName) { + + for (Index index : nullSafeCollection(queryService.getIndexes())) { if (index.getName().equalsIgnoreCase(indexName)) { return index; } @@ -171,91 +405,227 @@ public class IndexFactoryBean implements InitializingBean, FactoryBean, B return null; } - public Index getObject() { - return index; + /** + * 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 com.gemstone.gemfire.cache.query.Index + */ + public Index getIndex() { + return this.index; } + /* (non-Javadoc) */ + @Override + public Index getObject() { + + this.index = (this.index != null ? this.index + : tryToFindExistingIndexByName(resolveQueryService(), resolveIndexName())); + + return this.index; + } + + /* (non-Javadoc) */ + @Override public Class getObjectType() { + Index index = getIndex(); return (index != null ? index.getClass() : Index.class); } - public boolean isSingleton() { - return true; - } - /** - * Sets the underlying cache used for creating indexes. - * - * @param cache cache used for creating indexes. + * Sets a reference to the {@link RegionService}. + * + * @param cache reference to the {@link RegionService}. + * @see com.gemstone.gemfire.cache.RegionService */ public void setCache(RegionService cache) { this.cache = cache; } /** - * Sets the query service used for creating indexes. - * - * @param service query service used for creating indexes. - */ - public void setQueryService(QueryService service) { - this.queryService = service; - } - - public void setBeanName(String name) { - this.beanName = name; - } - - /** - * @param name the name to set + * Sets the name of the {@link Index}. + * + * @param name {@link String} containing the name given to the {@link Index}. */ public void setName(String name) { this.name = name; } /** - * @param expression the expression to set + * Sets the {@link QueryService} used to create the {@link Index}. + * + * @param service {@link QueryService} used to create the {@link Index}. + * @see com.gemstone.gemfire.cache.query.QueryService + */ + public void setQueryService(QueryService service) { + this.queryService = service; + } + + /** + * @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; } /** - * @param override the override to set + * 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; + } + + /** + * 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; } - } diff --git a/src/main/java/org/springframework/data/gemfire/IndexType.java b/src/main/java/org/springframework/data/gemfire/IndexType.java index d1a936eb..9acd503e 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(com.gemstone.gemfire.cache.query.IndexType.FUNCTIONAL), HASH(com.gemstone.gemfire.cache.query.IndexType.HASH), PRIMARY_KEY(com.gemstone.gemfire.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 com.gemstone.gemfire.cache.query.IndexType */ - public static IndexType valueOf(final com.gemstone.gemfire.cache.query.IndexType gemfireIndexType) { + public static IndexType valueOf(com.gemstone.gemfire.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/GemfireConstants.java b/src/main/java/org/springframework/data/gemfire/config/GemfireConstants.java index bcce9372..2b78219a 100644 --- a/src/main/java/org/springframework/data/gemfire/config/GemfireConstants.java +++ b/src/main/java/org/springframework/data/gemfire/config/GemfireConstants.java @@ -24,12 +24,12 @@ package org.springframework.data.gemfire.config; @SuppressWarnings("unused") public interface GemfireConstants { - static final String DEFAULT_GEMFIRE_CACHE_NAME = "gemfireCache"; - static final String DEFAULT_GEMFIRE_FUNCTION_SERVICE_NAME = "gemfireFunctionService"; - static final String DEFAULT_GEMFIRE_POOL_NAME = "DEFAULT"; - static final String DEFAULT_GEMFIRE_TRANSACTION_MANAGER_NAME = "gemfireTransactionManager"; + String DEFAULT_GEMFIRE_CACHE_NAME = "gemfireCache"; + String DEFAULT_GEMFIRE_FUNCTION_SERVICE_NAME = "gemfireFunctionService"; + String DEFAULT_GEMFIRE_POOL_NAME = "DEFAULT"; + String DEFAULT_GEMFIRE_TRANSACTION_MANAGER_NAME = "gemfireTransactionManager"; @Deprecated - static final String DEFAULT_GEMFIRE_TXMANAGER_NAME = DEFAULT_GEMFIRE_TRANSACTION_MANAGER_NAME; + String DEFAULT_GEMFIRE_TXMANAGER_NAME = DEFAULT_GEMFIRE_TRANSACTION_MANAGER_NAME; } diff --git a/src/main/java/org/springframework/data/gemfire/config/IndexParser.java b/src/main/java/org/springframework/data/gemfire/config/IndexParser.java index 33e05afe..2dc138fa 100644 --- a/src/main/java/org/springframework/data/gemfire/config/IndexParser.java +++ b/src/main/java/org/springframework/data/gemfire/config/IndexParser.java @@ -23,12 +23,13 @@ import org.springframework.data.gemfire.IndexFactoryBean; import org.w3c.dom.Element; /** - * Namespace parser for <index;gt; bean definitions. - * + * Namespace parser for <gfe:index;gt; bean definitions. + * * @author Costin Leau * @author John Blum * @see org.springframework.beans.factory.support.BeanDefinitionBuilder * @see org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser + * @see org.springframework.beans.factory.xml.ParserContext * @see org.springframework.data.gemfire.IndexFactoryBean * @since 1.1.0 */ @@ -48,5 +49,4 @@ class IndexParser extends AbstractSimpleBeanDefinitionParser { ParsingUtils.setPropertyReference(element, builder, "cache-ref", "cache"); super.doParse(element, parserContext, builder); } - } diff --git a/src/main/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupport.java b/src/main/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupport.java new file mode 100644 index 00000000..03da56b6 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupport.java @@ -0,0 +1,298 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.gemfire.support; + +import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.Assert; + +/** + * The {@link AbstractFactoryBeanSupport} class is an abstract Spring {@link FactoryBean} base class implementation + * encapsulating operations common to SDG's {@link FactoryBean} implementations. + * + * @author John Blum + * @see org.springframework.beans.factory.BeanClassLoaderAware + * @see org.springframework.beans.factory.BeanFactory + * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.beans.factory.BeanNameAware + * @see org.springframework.beans.factory.FactoryBean + * @since 1.0.0 + */ +public abstract class AbstractFactoryBeanSupport implements FactoryBean, + BeanClassLoaderAware, BeanFactoryAware, BeanNameAware { + + protected static final boolean DEFAULT_SINGLETON = true; + + private ClassLoader beanClassLoader; + + private BeanFactory beanFactory; + + private final Log log = newLog(); + + private String beanName; + + /** + * Constructs a new instance of {@link Log} to log statements printed by Spring Data GemFire/Geode. + * + * @return a new instance of {@link Log}. + * @see org.apache.commons.logging.LogFactory#getLog(Class) + * @see org.apache.commons.logging.Log + */ + protected Log newLog() { + return LogFactory.getLog(getClass()); + } + + /** + * Sets a reference to the {@link ClassLoader} used by the Spring container to load and create bean classes. + * + * @param classLoader {@link ClassLoader} used by the Spring container to load and create bean classes. + * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(ClassLoader) + * @see java.lang.ClassLoader + */ + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + /** + * Returns a reference to the {@link ClassLoader} used by the Spring container to load and create bean classes. + * + * @return the {@link ClassLoader} used by the Spring container to load and create bean classes. + * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(ClassLoader) + * @see java.lang.ClassLoader + */ + public ClassLoader getBeanClassLoader() { + return this.beanClassLoader; + } + + /** + * Sets a reference to the Spring {@link BeanFactory} in which this {@link FactoryBean} was declared. + * + * @param beanFactory reference to the declaring Spring {@link BeanFactory}. + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(BeanFactory) + * @see org.springframework.beans.factory.BeanFactory + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + /** + * Returns a reference to the Spring {@link BeanFactory} in which this {@link FactoryBean} was declared. + * + * @return a reference to the declaring Spring {@link BeanFactory}. + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(BeanFactory) + * @see org.springframework.beans.factory.BeanFactory + */ + public BeanFactory getBeanFactory() { + return this.beanFactory; + } + + /** + * Sets the {@link String bean name} assigned to this {@link FactoryBean} as declared in the Spring container. + * + * @param name {@link String bean name} assigned to this {@link FactoryBean} as declared in the Spring container. + * @see org.springframework.beans.factory.BeanNameAware#setBeanName(String) + * @see java.lang.String + */ + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + /** + * Returns the {@link String bean name} assigned to this {@link FactoryBean} as declared in the Spring container. + * + * @return the {@link String bean name} assigned to this {@link FactoryBean} as declared in the Spring container. + * @see org.springframework.beans.factory.BeanNameAware#setBeanName(String) + * @see java.lang.String + */ + public String getBeanName() { + return this.beanName; + } + + /** + * Returns a reference to the {@link Log} used by this {@link FactoryBean} to log {@link String messages}. + * + * @return a reference to the {@link Log} used by this {@link FactoryBean} to log {@link String messages}. + * @see org.apache.commons.logging.Log + */ + protected Log getLog() { + return this.log; + } + + /** + * Indicates that this {@link FactoryBean} produces a single bean instance. + * + * @return {@literal true} by default. + * @see org.springframework.beans.factory.FactoryBean#isSingleton() + */ + @Override + public boolean isSingleton() { + return DEFAULT_SINGLETON; + } + + /** + * Logs the {@link String message} formatted with the array of {@link Object arguments} at debug level. + * + * @param message {@link String} containing the message to log. + * @param args array of {@link Object arguments} used to format the {@code message}. + * @see #logDebug(Supplier) + */ + protected void logDebug(String message, Object... args) { + logDebug(StringFormatSupplier.of(message, args)); + } + + /** + * Logs the {@link String message} supplied by the given {@link Supplier} at debug level. + * + * @param message {@link Supplier} containing the {@link String message} and arguments to log. + * @see org.apache.commons.logging.Log#isDebugEnabled() + * @see org.apache.commons.logging.Log#debug(Object) + * @see #getLog() + */ + protected void logDebug(Supplier message) { + + Log log = getLog(); + + if (log.isDebugEnabled()) { + log.debug(message.get()); + } + } + + /** + * Logs the {@link String message} formatted with the array of {@link Object arguments} at info level. + * + * @param message {@link String} containing the message to log. + * @param args array of {@link Object arguments} used to format the {@code message}. + * @see #logInfo(Supplier) + */ + protected void logInfo(String message, Object... args) { + logInfo(StringFormatSupplier.of(message, args)); + } + + /** + * Logs the {@link String message} supplied by the given {@link Supplier} at info level. + * + * @param message {@link Supplier} containing the {@link String message} and arguments to log. + * @see org.apache.commons.logging.Log#isInfoEnabled() + * @see org.apache.commons.logging.Log#info(Object) + * @see #getLog() + */ + protected void logInfo(Supplier message) { + + Log log = getLog(); + + if (log.isInfoEnabled()) { + 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(StringFormatSupplier.of(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) { + + Log log = getLog(); + + if (log.isWarnEnabled()) { + 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(StringFormatSupplier.of(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) { + + Log log = getLog(); + + if (log.isErrorEnabled()) { + log.error(message.get()); + } + } + + protected interface Supplier { + + T get(); + + } + + protected static class StringFormatSupplier implements Supplier { + + private final Object[] args; + + private final String message; + + protected static StringFormatSupplier of(String message, Object... args) { + return new StringFormatSupplier(message, nullSafeArray(args)); + } + + private StringFormatSupplier(String message, Object... args) { + + Assert.hasText(message, "Message is required"); + + this.message = message; + this.args = args; + } + + @Override + public String get() { + return String.format(this.message, this.args); + } + } +} diff --git a/src/main/java/org/springframework/data/gemfire/util/CacheUtils.java b/src/main/java/org/springframework/data/gemfire/util/CacheUtils.java new file mode 100644 index 00000000..7dc617cd --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/util/CacheUtils.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.util; + +import com.gemstone.gemfire.cache.Cache; +import com.gemstone.gemfire.cache.CacheClosedException; +import com.gemstone.gemfire.cache.CacheFactory; +import com.gemstone.gemfire.cache.GemFireCache; +import com.gemstone.gemfire.cache.Region; +import com.gemstone.gemfire.cache.client.ClientCache; +import com.gemstone.gemfire.cache.client.ClientCacheFactory; +import com.gemstone.gemfire.internal.cache.GemFireCacheImpl; + +/** + * {@link CacheUtils} is an abstract utility class encapsulating common operations for working with + * GemFire {@link Cache} and {@link ClientCache} instances. + * + * @author John Blum + * @see org.springframework.data.gemfire.util.DistributedSystemUtils + * @see com.gemstone.gemfire.cache.Cache + * @see com.gemstone.gemfire.cache.CacheFactory + * @see com.gemstone.gemfire.cache.GemFireCache + * @see com.gemstone.gemfire.cache.Region + * @see com.gemstone.gemfire.cache.client.ClientCache + * @see com.gemstone.gemfire.cache.client.ClientCacheFactory + * @see com.gemstone.gemfire.distributed.DistributedSystem + * @see com.gemstone.gemfire.internal.cache.GemFireCacheImpl + * @since 1.8.0 + */ +@SuppressWarnings("unused") +public abstract class CacheUtils extends DistributedSystemUtils { + + public static final String DEFAULT_POOL_NAME = "DEFAULT"; + + /* (non-Javadoc) */ + @SuppressWarnings("all") + public static boolean isClient(GemFireCache cache) { + + boolean client = (cache instanceof ClientCache); + + if (cache instanceof GemFireCacheImpl) { + client &= ((GemFireCacheImpl) cache).isClient(); + } + + return client; + } + + /* (non-Javadoc) */ + @SuppressWarnings("all") + public static boolean isPeer(GemFireCache cache) { + + boolean peer = (cache instanceof Cache); + + if (cache instanceof GemFireCacheImpl) { + peer &= !((GemFireCacheImpl) cache).isClient(); + } + + return peer; + } + + /* (non-Javadoc) */ + public static boolean closeCache() { + try { + CacheFactory.getAnyInstance().close(); + return true; + } + catch (Exception ignore) { + return false; + } + } + + /* (non-Javadoc) */ + public static boolean closeClientCache() { + try { + ClientCacheFactory.getAnyInstance().close(); + return true; + } + catch (Exception ignore) { + return false; + } + } + + /* (non-Javadoc) */ + public static Cache getCache() { + try { + return CacheFactory.getAnyInstance(); + } + catch (CacheClosedException ignore) { + return null; + } + } + + /* (non-Javadoc) */ + public static ClientCache getClientCache() { + try { + return ClientCacheFactory.getAnyInstance(); + } + catch (CacheClosedException ignore) { + return null; + } + catch (IllegalStateException ignore) { + return null; + } + } + + /* (non-Javadoc) */ + public static GemFireCache resolveGemFireCache() { + + GemFireCache resolvedGemFireCache = getClientCache(); + + return (resolvedGemFireCache != null ? resolvedGemFireCache : getCache()); + } + + /* (non-Javadoc) */ + public static String toRegionPath(String regionName) { + return String.format("%1$s%2$s", Region.SEPARATOR, regionName); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java b/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java new file mode 100644 index 00000000..055db4ac --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.util; + +import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.util.StringUtils; + +/** + * SpringUtils is a utility class encapsulating common functionality on objects and other class types. + * + * @author John Blum + * @since 1.8.0 + */ +@SuppressWarnings("unused") +public abstract class SpringUtils { + + /* (non-Javadoc) */ + public static BeanDefinition addDependsOn(BeanDefinition bean, String... beanNames) { + + List dependsOnList = new ArrayList(); + + Collections.addAll(dependsOnList, nullSafeArray(bean.getDependsOn())); + dependsOnList.addAll(Arrays.asList(nullSafeArray(beanNames))); + bean.setDependsOn(dependsOnList.toArray(new String[dependsOnList.size()])); + + return bean; + } + + /* (non-Javadoc) */ + public static String defaultIfEmpty(String value, String defaultValue) { + return (StringUtils.hasText(value) ? value : defaultValue); + } + + /* (non-Javadoc) */ + public static T defaultIfNull(T value, T defaultValue) { + return (value != null ? value : defaultValue); + } + + /* (non-Javadoc) */ + public static String dereferenceBean(String beanName) { + return String.format("%1$s%2$s", BeanFactory.FACTORY_BEAN_PREFIX, beanName); + } + + /* (non-Javadoc) */ + public static boolean equalsIgnoreNull(Object obj1, Object obj2) { + return (obj1 == null ? obj2 == null : obj1.equals(obj2)); + } + + /* (non-Javadoc) */ + public static boolean nullOrEquals(Object obj1, Object obj2) { + return (obj1 == null || obj1.equals(obj2)); + } + + /* (non-Javadoc) */ + public static boolean nullSafeEquals(Object obj1, Object obj2) { + return (obj1 != null && obj1.equals(obj2)); + } +} diff --git a/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-1.6.xsd b/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-1.6.xsd index 4047f523..13df269c 100644 --- a/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-1.6.xsd +++ b/src/main/resources/org/springframework/data/gemfire/config/spring-gemfire-1.6.xsd @@ -2324,10 +2324,10 @@ The name of the index bean definition. If property 'name' is not set, it will be ]]> - + @@ -2346,18 +2346,27 @@ Corresponds to the regionPath parameter in createIndex methods. ]]> - - + - + + @@ -2368,6 +2377,13 @@ The name of the pool used by the index. Used usually in client scenarios. ]]> + + + + + diff --git a/src/test/java/org/springframework/data/gemfire/IndexConflictsIntegrationTest.java b/src/test/java/org/springframework/data/gemfire/IndexConflictsIntegrationTest.java index a9b9e2ce..0be8c08c 100644 --- a/src/test/java/org/springframework/data/gemfire/IndexConflictsIntegrationTest.java +++ b/src/test/java/org/springframework/data/gemfire/IndexConflictsIntegrationTest.java @@ -16,22 +16,22 @@ 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 com.gemstone.gemfire.cache.GemFireCache; +import com.gemstone.gemfire.cache.query.Index; +import com.gemstone.gemfire.cache.query.IndexExistsException; +import com.gemstone.gemfire.cache.query.IndexNameConflictException; +import com.gemstone.gemfire.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; @@ -39,31 +39,50 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Import; -import com.gemstone.gemfire.cache.Cache; -import com.gemstone.gemfire.cache.query.Index; -import com.gemstone.gemfire.cache.query.IndexExistsException; - /** - * 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.springframework.context.ConfigurableApplicationContext + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.Import + * @see com.gemstone.gemfire.cache.GemFireCache + * @see com.gemstone.gemfire.cache.query.Index + * @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()); @@ -72,185 +91,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 class 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(); @@ -258,7 +352,6 @@ public class IndexConflictsIntegrationTest { indexFactoryBean.setExpression("lastName"); indexFactoryBean.setFrom("/Customers"); indexFactoryBean.setName(INDEX_NAME); - indexFactoryBean.setOverride(override); indexFactoryBean.setType(IndexType.HASH); return indexFactoryBean; @@ -266,20 +359,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 60c941ad..67cbc1ce 100644 --- a/src/test/java/org/springframework/data/gemfire/IndexFactoryBeanTest.java +++ b/src/test/java/org/springframework/data/gemfire/IndexFactoryBeanTest.java @@ -16,38 +16,22 @@ 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.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -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.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.same; +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 org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import com.gemstone.gemfire.cache.Cache; import com.gemstone.gemfire.cache.client.ClientCache; @@ -56,12 +40,27 @@ import com.gemstone.gemfire.cache.query.IndexExistsException; import com.gemstone.gemfire.cache.query.IndexNameConflictException; import com.gemstone.gemfire.cache.query.QueryService; +import org.apache.commons.logging.Log; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; + /** - * The IndexFactoryBeanTest class is a test suite of test cases testing the contract and functionality - * of the IndexFactoryBean class. + * 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.runners.MockitoJUnitRunner * @see org.springframework.data.gemfire.IndexFactoryBean * @see com.gemstone.gemfire.cache.Cache * @see com.gemstone.gemfire.cache.client.ClientCache @@ -69,156 +68,205 @@ import com.gemstone.gemfire.cache.query.QueryService; * @see com.gemstone.gemfire.cache.query.QueryService * @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 QueryService mockQueryService = mock(QueryService.class, "IndexFactoryBeanTest.MockQueryService"); + @Mock + private Index mockIndex; - protected IndexFactoryBean newIndexFactoryBean() { - IndexFactoryBean indexFactoryBean = new IndexFactoryBean() { - @Override QueryService lookupQueryService() { - return mockQueryService; + @Spy + private IndexFactoryBean indexFactoryBean; + + @Mock + private Log mockLog; + + @Mock + private QueryService mockQueryService; + + @Before + public void setup() { + when(mockCache.getQueryService()).thenReturn(mockQueryService); + } + + @After + public void tearDown() { + indexFactoryBean.setBeanFactory(null); + indexFactoryBean.setCache(null); + indexFactoryBean.setExpression(null); + indexFactoryBean.setFrom(null); + indexFactoryBean.setImports(null); + indexFactoryBean.setQueryService(null); + indexFactoryBean.setType((IndexType) null); + } + + private Index mockIndex(String name) { + + Index mockIndex = mock(Index.class, name); + + when(mockIndex.getName()).thenReturn(name); + + return mockIndex; + } + + private Index mockIndexWithDefinition(String name, String expression, String fromClause, IndexType type) { + + 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 - public void testAfterPropertiesSet() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testAfterPropertiesSet.MockQueryService"); + @SuppressWarnings("all") + public void afterPropertiesSetIsSuccessful() throws Exception { - Cache mockCache = mock(Cache.class, "testAfterPropertiesSet.MockCache"); + ConfigurableBeanFactory mockConfigurableBeanFactory = mock(ConfigurableBeanFactory.class); - when(mockCache.getQueryService()).thenReturn(mockQueryService); + when(mockQueryService.createKeyIndex(eq("TestKeyIndex"), eq("id"), eq("/Example"))) + .thenReturn(mockIndex); - final Index mockIndex = mock(Index.class, "testAfterPropertiesSet.MockIndex"); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean() { - @Override Index createIndex(final QueryService queryService, final String indexName) throws Exception { - return mockIndex; - } - }; - - indexFactoryBean.setCache(mockCache); - indexFactoryBean.setName("TestIndex"); + indexFactoryBean.setBeanFactory(mockConfigurableBeanFactory); + indexFactoryBean.setBeanName("KeyIndexBean"); indexFactoryBean.setExpression("id"); indexFactoryBean.setFrom("/Example"); + indexFactoryBean.setName("TestKeyIndex"); indexFactoryBean.setType("key"); indexFactoryBean.afterPropertiesSet(); - assertEquals(mockIndex, indexFactoryBean.getObject()); - assertEquals(mockIndex, indexFactoryBean.getObject()); // assert Index really is a 'Singleton' - assertTrue(Index.class.isAssignableFrom(indexFactoryBean.getObjectType())); - assertTrue(indexFactoryBean.isSingleton()); + assertThat(indexFactoryBean.getIndex()).isEqualTo(mockIndex); + + 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 testAfterPropertiesSetWithNullCache() throws Exception { + 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 { new IndexFactoryBean().afterPropertiesSet(); } - catch (IllegalArgumentException expected) { - assertEquals("The GemFire Cache reference must not be null!", expected.getMessage()); + catch (IllegalStateException expected) { + assertThat(expected).hasMessage("Cache is required"); + assertThat(expected).hasNoCause(); + throw expected; } } - @Test(expected = IllegalArgumentException.class) - public void testAfterPropertiesSetWithNullQueryService() throws Exception { + @Test(expected = IllegalStateException.class) + public void afterPropertiesSetWithNullQueryService() throws Exception { + + IndexFactoryBean indexFactoryBean = spy(new IndexFactoryBean()); + + doReturn(null).when(indexFactoryBean).lookupQueryService(); + try { - Cache mockCache = mock(Cache.class, "testAfterPropertiesSetWithNullQueryService.MockCache"); - - when(mockCache.getQueryService()).thenReturn(null); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); - 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) public void testAfterPropertiesSetWithUnspecifiedExpression() throws Exception { try { - QueryService mockQueryService = mock(QueryService.class, - "testAfterPropertiesSetWithUnspecifiedExpression.MockQueryService"); - - Cache mockCache = mock(Cache.class, "testAfterPropertiesSetWithUnspecifiedExpression.MockCache"); - - when(mockCache.getQueryService()).thenReturn(mockQueryService); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); - - indexFactoryBean.setCache(mockCache); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + indexFactoryBean.setName("TestIndex"); 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 + @Test(expected = IllegalArgumentException.class) 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.afterPropertiesSet(); - } - - @Test(expected = IllegalArgumentException.class) - public void testAfterPropertiesSetWithUnspecifiedIndexName() throws Exception { try { - QueryService mockQueryService = mock(QueryService.class, - "testAfterPropertiesSetWithUnspecifiedIndexName.MockQueryService"); - - Cache mockCache = mock(Cache.class, "testAfterPropertiesSetWithUnspecifiedIndexName.MockCache"); - - when(mockCache.getQueryService()).thenReturn(mockQueryService); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); - - indexFactoryBean.setCache(mockCache); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + indexFactoryBean.setName("TestIndex"); indexFactoryBean.setExpression("id"); - indexFactoryBean.setFrom("/Example"); - indexFactoryBean.setImports("org.example.DomainType"); - indexFactoryBean.setType("hash"); indexFactoryBean.afterPropertiesSet(); } - catch (IllegalArgumentException expected) { - assertEquals("The Index bean id or name is required!", expected.getMessage()); + catch (Exception expected) { + assertThat(expected).hasMessage("Index from clause is required"); + assertThat(expected).hasNoCause(); + throw expected; } } @Test(expected = IllegalArgumentException.class) - public void testAfterPropertiesSetWithInvalidTypeUsingImports() throws Exception { + public void afterPropertiesSetUsingImportsWithInvalidIndexKeyType() throws Exception { try { - QueryService mockQueryService = mock(QueryService.class, - "testAfterPropertiesSetWithInvalidTypeUsingImports.MockQueryService"); - - Cache mockCache = mock(Cache.class, "testAfterPropertiesSetWithInvalidTypeUsingImports.MockCache"); - - when(mockCache.getQueryService()).thenReturn(mockQueryService); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); - - indexFactoryBean.setCache(mockCache); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + indexFactoryBean.setName("TestIndex"); indexFactoryBean.setExpression("id"); indexFactoryBean.setFrom("/Example"); indexFactoryBean.setImports("org.example.DomainType"); @@ -226,350 +274,839 @@ public class IndexFactoryBeanTest { indexFactoryBean.afterPropertiesSet(); } catch (IllegalArgumentException expected) { - assertEquals("The 'imports' property is not supported for a Key Index.", expected.getMessage()); + assertThat(expected).hasMessage("Imports are not supported with a KEY Index"); + assertThat(expected).hasNoCause(); + throw expected; } } @Test - public void testLookupQueryService() { - QueryService mockQueryServiceOne = mock(QueryService.class, "testLookupQueryService.MockQueryService.One"); - QueryService mockQueryServiceTwo = mock(QueryService.class, "testLookupQueryService.MockQueryService.Two"); + public void resolveCacheFromCacheProperty() { + assertThat(newIndexFactoryBean().resolveCache()).isSameAs(mockCache); + } - Cache mockCache = mock(Cache.class, "testLookupQueryService.MockCache"); + @Test(expected = IllegalStateException.class) + public void resolveCacheThrowsExceptionForUnresolvableCache() { + try { + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + indexFactoryBean.setCache(null); + indexFactoryBean.resolveCache(); + } + catch (IllegalStateException expected) { + assertThat(expected).hasMessage("Cache is required"); + assertThat(expected).hasNoCause(); - when(mockCache.getQueryService()).thenReturn(mockQueryServiceTwo); + throw expected; + } + } - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + @Test + public void resolveIndexNameFromBeanNameProperty() { - indexFactoryBean.setCache(mockCache); - indexFactoryBean.setQueryService(mockQueryServiceOne); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); - assertSame(mockQueryServiceOne, indexFactoryBean.lookupQueryService()); + 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 resolveIndexNameThrowsExceptionForUnresolvableIndexName() { + try { + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + indexFactoryBean.setBeanName(null); + indexFactoryBean.setName(null); + indexFactoryBean.resolveIndexName(); + } + catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("Index name is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void lookupQueryService() { + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.setQueryService(mockQueryService); + + assertThat(indexFactoryBean.lookupQueryService()).isSameAs(mockQueryService); verify(mockCache, never()).getQueryService(); } @Test - public void testLookupQueryServiceOnClientCache() { - QueryService mockQueryService = mock(QueryService.class, "testLookupQueryServiceOnClientCache.MockQueryService"); + public void lookupQueryServiceOnClientCache() { - ClientCache mockClientCache = mock(ClientCache.class, "testLookupQueryServiceOnClientCache.MockClientCache"); + ClientCache mockClientCache = mock(ClientCache.class); when(mockClientCache.getLocalQueryService()).thenReturn(mockQueryService); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); indexFactoryBean.setCache(mockClientCache); + indexFactoryBean.setQueryService(null); - assertSame(mockQueryService, indexFactoryBean.lookupQueryService()); + assertThat(indexFactoryBean.lookupQueryService()).isSameAs(mockQueryService); verify(mockClientCache, times(1)).getLocalQueryService(); } @Test - public void testLookupQueryServiceOnPeerCache() { - QueryService mockQueryService = mock(QueryService.class, "testLookupQueryServiceOnPeerCache.MockQueryService"); + public void lookupQueryServiceOnPeerCache() { - Cache mockCache = mock(Cache.class, "testLookupQueryServiceOnPeerCache.MockCache"); - - when(mockCache.getQueryService()).thenReturn(mockQueryService); - - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); indexFactoryBean.setCache(mockCache); indexFactoryBean.setQueryService(null); - assertSame(mockQueryService, indexFactoryBean.lookupQueryService()); + assertThat(indexFactoryBean.lookupQueryService()).isSameAs(mockQueryService); verify(mockCache, times(1)).getQueryService(); } @Test - public void testCreateIndexReturnsNewKeyIndex() throws Exception { - Index mockIndex = mock(Index.class, "testCreateIndexReturnsNewKeyIndex.MockIndex"); + public void resolveQueryServiceReturnsIndexFactoryBeanConfiguredQueryService() { + assertThat(newIndexFactoryBean().resolveQueryService()).isSameAs(mockQueryService); + } - QueryService mockQueryService = mock(QueryService.class, "testCreateIndexReturnsNewKeyIndex.MockQueryService"); + @Test + public void resolveQueryServiceReturnsQueryServiceFromLookup() { - when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); - when(mockQueryService.createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example"))).thenReturn(mockIndex); + QueryService mockQueryService = + mockQueryService("testResolveQueryServiceReturnsQueryServiceFromLookup.MockQueryService"); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + 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 registerAliasWhenBeanFactoryIsNull() { + + IndexFactoryBean indexFactoryBean = newIndexFactoryBean(); + + indexFactoryBean.setBeanFactory(null); + + 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")); } @Test - public void testCreateIndexReturnsNewHashIndex() throws Exception { - Index mockIndex = mock(Index.class, "testCreateIndexReturnsNewHashIndex.MockIndex"); + public void createIndexReturnsNewHashIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testCreateIndexReturnsNewHashIndex.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")); } @Test - public void testCreateIndexReturnsNewFunctionalIndex() throws Exception { - Index mockIndex = mock(Index.class, "testCreateIndexReturnsNewFunctionalIndex.MockIndex"); + public void createIndexReturnsNewFunctionalIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, - "testCreateIndexReturnsNewFunctionalIndex.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")); } - @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, "testCreateIndexReturnsExistingIndex.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)); - } - - @Test(expected = RuntimeException.class) - public void testCreateIndexThrowsException() throws Exception { - Index mockExistingIndex = mock(Index.class, "testCreateIndexThrowsException.MockIndex.Existing"); - - QueryService mockQueryService = mock(QueryService.class, "testCreateIndexThrowsException.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(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(String.class)); + + 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"), any(String.class)); + + 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(String.class)); + + 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"), any(String.class)); + + verifyZeroInteractions(mockLog); + + verify(mockQueryService, times(1)).getIndexes(); } } @Test - public void testCreateFunctionalIndex() throws Exception { - Index mockIndex = mock(Index.class, "testCreateFunctionalIndex.MockIndex"); + public void createIndexThrowsIndexNameConflictExceptionAndIgnoresThisIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testCreateFunctionalIndex.MockQueryService"); + 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(String.class)); + + 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"), any(String.class)); + + 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(String.class)); + + 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"), any(String.class)); + + 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(String.class)); + + 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"), any(String.class)); + + 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 { when(mockQueryService.createIndex(eq("FunctionalIndex"), eq("purchaseDate"), eq("/Orders"))) .thenReturn(mockIndex); @@ -577,77 +1114,323 @@ public class IndexFactoryBeanTest { Index actualIndex = new 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")); } @Test - public void testCreateFunctionalIndexWithImports() throws Exception { - Index mockIndex = mock(Index.class, "testCreateFunctionalIndexWithImports.MockIndex"); + public void createFunctionalIndexWithImports() throws Exception { - 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 = new IndexFactoryBean().createFunctionalIndex(mockQueryService, "FunctionalIndexWithImports", - "purchaseDate", "/Orders", "org.example.Order"); + Index actualIndex = indexFactoryBean.createFunctionalIndex(mockQueryService, "FunctionalIndexWithImports", + "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")); } @Test - public void testCreateHashIndex() throws Exception { - Index mockIndex = mock(Index.class, "testCreateHashIndex.MockIndex"); + public void createHashIndex() throws Exception { - 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 = new IndexFactoryBean().createHashIndex(mockQueryService, "HashIndex", + 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")); } @Test - public void testCreateHashIndexWithImports() throws Exception { - Index mockIndex = mock(Index.class, "testCreateHashIndexWithImports.MockIndex"); + public void createHashIndexWithImports() throws Exception { - 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 = new IndexFactoryBean().createHashIndex(mockQueryService, "HashIndexWithImports", - "name", "/People", "org.example.Person"); + Index actualIndex = indexFactoryBean.createHashIndex(mockQueryService, "HashIndexWithImports", + "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")); } @Test - public void testGetExistingIndex() { - 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 createKeyIndex() throws Exception { - QueryService mockQueryService = mock(QueryService.class, "testGetExistingIndex.MockQueryService"); + Index mockIndex = mockIndex("testCreateKeyIndex.MockKeyIndex"); - when(mockIndexOne.getName()).thenReturn("PrimaryIndex"); - when(mockIndexTwo.getName()).thenReturn("SecondaryIndex"); - when(mockIndexThree.getName()).thenReturn("TernaryIndex"); - when(mockQueryService.getIndexes()).thenReturn(Arrays.asList(mockIndexOne, mockIndexTwo, mockIndexThree)); + when(mockQueryService.createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example"))) + .thenReturn(mockIndex); - IndexFactoryBean indexFactoryBean = new IndexFactoryBean(); + Index actualIndex = indexFactoryBean.createKeyIndex(mockQueryService, "KeyIndex", + "id", "/Example"); - 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(actualIndex).isSameAs(mockIndex); + + verify(mockQueryService, times(1)) + .createKeyIndex(eq("KeyIndex"), eq("id"), eq("/Example")); } + @Test + public void tryToFindExistingIndexByDefinitionReturnsIndex() { + + 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(mockQueryService.getIndexes()).thenReturn(Arrays.asList(mockIndexOne, mockIndexTwo, mockIndexThree)); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/People", IndexType.PRIMARY_KEY)).isSameAs(mockIndexTwo); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "purchaseDate", "/Orders", IndexType.FUNCTIONAL)).isSameAs(mockIndexThree); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/People", IndexType.HASH)).isSameAs(mockIndexOne); + + verify(mockQueryService, times(3)).getIndexes(); + } + + @Test + public void tryToFindExistingIndexByDefinitionReturnsNull() { + + Index mockIndex = + mockIndexWithDefinition("PrimaryIndex", "id", "/People", IndexType.HASH); + + when(mockQueryService.getIndexes()).thenReturn(Collections.singletonList(mockIndex)); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "key", "/People", IndexType.HASH)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/Persons", IndexType.HASH)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/People", IndexType.KEY)).isNull(); + + verify(mockQueryService, times(3)).getIndexes(); + } + + @Test + public void tryToFindExistingIndexByDefinitionWithQueryServiceHavingNoIndexesReturnsNull() { + + when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/People", IndexType.KEY)).isNull(); + + verify(mockQueryService, times(1)).getIndexes(); + } + + @Test + public void tryToFindExistingIndexByDefinitionWithQueryServiceReturningNullIndexesReturnsNull() { + + when(mockQueryService.getIndexes()).thenReturn(null); + + assertThat(indexFactoryBean.tryToFindExistingIndexByDefinition(mockQueryService, + "id", "/People", IndexType.KEY)).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")) + .isSameAs(mockIndexOne); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "SecondaryIndex")) + .isSameAs(mockIndexTwo); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "ternaryindex")) + .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)).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "")).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, " ")).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "Primary Index")).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "Secondary_Index")).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "QuadIndex")).isNull(); + + verify(mockQueryService, times(6)).getIndexes(); + } + + @Test + public void tryToFindExistingIndexByNameWithQueryServiceHavingNoIndexesReturnsNull() { + + when(mockQueryService.getIndexes()).thenReturn(Collections.emptyList()); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "TestIndex")).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "KeyIndex")).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "IdIndex")).isNull(); + + verify(mockQueryService, times(3)).getIndexes(); + } + + @Test + public void tryToFindExistingIndexByNameWithQueryServiceReturningNullIndexesReturnsNull() { + + when(mockQueryService.getIndexes()).thenReturn(null); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "TestIndex")).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "KeyIndex")).isNull(); + + assertThat(indexFactoryBean.tryToFindExistingIndexByName(mockQueryService, "IdIndex")).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(); + } } diff --git a/src/test/java/org/springframework/data/gemfire/config/IndexNamespaceTest.java b/src/test/java/org/springframework/data/gemfire/config/IndexNamespaceTest.java index f3d9ecb6..72c1429e 100644 --- a/src/test/java/org/springframework/data/gemfire/config/IndexNamespaceTest.java +++ b/src/test/java/org/springframework/data/gemfire/config/IndexNamespaceTest.java @@ -16,22 +16,26 @@ package org.springframework.data.gemfire.config; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import javax.annotation.Resource; +import com.gemstone.gemfire.cache.Region; +import com.gemstone.gemfire.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 com.gemstone.gemfire.cache.Region; -import com.gemstone.gemfire.cache.query.Index; - /** - * 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 @@ -50,23 +54,35 @@ 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(com.gemstone.gemfire.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(com.gemstone.gemfire.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()); @@ -74,4 +90,13 @@ public class IndexNamespaceTest { assertEquals(com.gemstone.gemfire.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/support/AbstractFactoryBeanSupportUnitTests.java b/src/test/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupportUnitTests.java new file mode 100644 index 00000000..0f086965 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/support/AbstractFactoryBeanSupportUnitTests.java @@ -0,0 +1,208 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.gemfire.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.commons.logging.Log; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.factory.BeanFactory; + +/** + * Unit tests for {@link AbstractFactoryBeanSupport}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.Spy + * @see org.springframework.data.gemfire.support.AbstractFactoryBeanSupport + * @since 1.0.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class AbstractFactoryBeanSupportUnitTests { + + @Mock + private Log mockLog; + + @Spy + private TestFactoryBeanSupport factoryBeanSupport; + + @Before + public void setup() { + when(factoryBeanSupport.getLog()).thenReturn(mockLog); + } + + @Test + public void setAndGetBeanClassLoader() { + assertThat(factoryBeanSupport.getBeanClassLoader()).isNull(); + + ClassLoader mockClassLoader = mock(ClassLoader.class); + + factoryBeanSupport.setBeanClassLoader(mockClassLoader); + + assertThat(factoryBeanSupport.getBeanClassLoader()).isSameAs(mockClassLoader); + + ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + + factoryBeanSupport.setBeanClassLoader(systemClassLoader); + + assertThat(factoryBeanSupport.getBeanClassLoader()).isSameAs(systemClassLoader); + + factoryBeanSupport.setBeanClassLoader(null); + + assertThat(factoryBeanSupport.getBeanClassLoader()).isNull(); + } + + @Test + public void setAndGetBeanFactory() { + assertThat(factoryBeanSupport.getBeanFactory()).isNull(); + + BeanFactory mockBeanFactory = mock(BeanFactory.class); + + factoryBeanSupport.setBeanFactory(mockBeanFactory); + + assertThat(factoryBeanSupport.getBeanFactory()).isSameAs(mockBeanFactory); + + factoryBeanSupport.setBeanFactory(null); + + assertThat(factoryBeanSupport.getBeanFactory()).isNull(); + } + + @Test + public void setAndGetBeanName() { + assertThat(factoryBeanSupport.getBeanName()).isNullOrEmpty(); + + factoryBeanSupport.setBeanName("test"); + + assertThat(factoryBeanSupport.getBeanName()).isEqualTo("test"); + + factoryBeanSupport.setBeanName(null); + + assertThat(factoryBeanSupport.getBeanName()).isNullOrEmpty(); + } + + @Test + public void isSingletonDefaultsToTrue() { + assertThat(factoryBeanSupport.isSingleton()).isTrue(); + } + + @Test + public void logsDebugWhenDebugIsEnabled() { + when(mockLog.isDebugEnabled()).thenReturn(true); + + factoryBeanSupport.logDebug("%s log test", "debug"); + + verify(mockLog, times(1)).isDebugEnabled(); + verify(mockLog, times(1)).debug(eq("debug log test")); + } + + @Test + public void logsInfoWhenInfoIsEnabled() { + when(mockLog.isInfoEnabled()).thenReturn(true); + + factoryBeanSupport.logInfo("%s log test", "info"); + + verify(mockLog, times(1)).isInfoEnabled(); + 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); + + factoryBeanSupport.logDebug("test"); + + verify(mockLog, times(1)).isDebugEnabled(); + verify(mockLog, never()).debug(any()); + } + + @Test + public void suppressesInfoLoggingWhenInfoIsDisabled() { + when(mockLog.isInfoEnabled()).thenReturn(false); + + factoryBeanSupport.logInfo("test"); + + verify(mockLog, times(1)).isInfoEnabled(); + 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 + public T getObject() throws Exception { + return null; + } + + @Override + public Class getObjectType() { + return Object.class; + } + } +} diff --git a/src/test/resources/org/springframework/data/gemfire/config/index-ns.xml b/src/test/resources/org/springframework/data/gemfire/config/index-ns.xml index 0835a84c..9181829c 100644 --- a/src/test/resources/org/springframework/data/gemfire/config/index-ns.xml +++ b/src/test/resources/org/springframework/data/gemfire/config/index-ns.xml @@ -30,9 +30,12 @@ - + + +