org.springframework.data.gemfire.IndexFactoryBean.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.data.gemfire.IndexFactoryBean.java

Source

/*
 * Copyright 2011-2019 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
 *
 *      https://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;

import static java.util.stream.StreamSupport.stream;
import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeCollection;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeIterable;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionService;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.query.Index;
import org.apache.geode.cache.query.IndexExistsException;
import org.apache.geode.cache.query.IndexNameConflictException;
import org.apache.geode.cache.query.IndexStatistics;
import org.apache.geode.cache.query.QueryService;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.data.gemfire.config.annotation.IndexConfigurer;
import org.springframework.data.gemfire.config.xml.GemfireConstants;
import org.springframework.data.gemfire.support.AbstractFactoryBeanSupport;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * Spring {@link FactoryBean} used to construct, configure and initialize {@link Index Indexes}
 * using a declarative approach.
 *
 * @author Costin Leau
 * @author David Turanski
 * @author John Blum
 * @see org.apache.geode.cache.Region
 * @see org.apache.geode.cache.RegionService
 * @see org.apache.geode.cache.query.Index
 * @see org.apache.geode.cache.query.QueryService
 * @see org.springframework.beans.factory.FactoryBean
 * @see org.springframework.beans.factory.InitializingBean
 * @see org.springframework.beans.factory.config.ConfigurableBeanFactory
 * @see org.springframework.data.gemfire.IndexFactoryBean.IndexWrapper
 * @see org.springframework.data.gemfire.config.annotation.IndexConfigurer
 * @see org.springframework.data.gemfire.support.AbstractFactoryBeanSupport
 * @since 1.0.0
 */
public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implements InitializingBean {

    public static final String BASIC_INDEX_DEFINITION = "{ expression = '%1$s', from = '%2$s', type = %3$s }";

    public static final String DETAILED_INDEX_DEFINITION = "{ name = '%1$s', expression = '%2$s', from = '%3$s', imports = '%4$s', type = %5$s }";

    private boolean define = false;
    private boolean ignoreIfExists = false;
    private boolean override = false;

    private Index index;

    private IndexType indexType;

    //@Autowired(required = false)
    private List<IndexConfigurer> indexConfigurers = Collections.emptyList();

    private final IndexConfigurer compositeIndexConfigurer = new IndexConfigurer() {

        @Override
        public void configure(String beanName, IndexFactoryBean bean) {
            nullSafeCollection(indexConfigurers)
                    .forEach(indexConfigurer -> indexConfigurer.configure(beanName, bean));
        }
    };

    private QueryService queryService;

    private RegionService cache;

    private String expression;
    private String from;
    private String imports;
    private String indexName;
    private String name;

    @Override
    public void afterPropertiesSet() throws Exception {

        this.indexName = resolveIndexName();

        applyIndexConfigurers(this.indexName);

        this.cache = resolveCache();
        this.queryService = resolveQueryService();

        assertIndexDefinitionConfiguration();

        this.index = createIndex(this.queryService, this.indexName);

        registerAlias(getBeanName(), this.indexName);
    }

    /* (non-Javadoc) */
    private void applyIndexConfigurers(String indexName) {
        applyIndexConfigurers(indexName, getCompositeRegionConfigurer());
    }

    /**
     * Null-safe operation to apply the given array of {@link IndexConfigurer IndexConfigurers}
     * to this {@link IndexFactoryBean}.
     *
     * @param indexName {@link String} containing the name of the {@link Index}.
     * @param indexConfigurers array of {@link IndexConfigurer IndexConfigurers} applied
     * to this {@link IndexFactoryBean}.
     * @see org.springframework.data.gemfire.config.annotation.RegionConfigurer
     * @see #applyIndexConfigurers(String, Iterable)
     */
    protected void applyIndexConfigurers(String indexName, IndexConfigurer... indexConfigurers) {
        applyIndexConfigurers(indexName, Arrays.asList(nullSafeArray(indexConfigurers, IndexConfigurer.class)));
    }

    /**
     * Null-safe operation to apply the given {@link Iterable} of {@link IndexConfigurer IndexConfigurers}
     * to this {@link IndexFactoryBean}.
     *
     * @param indexName {@link String} containing the name of the {@link Index}.
     * @param indexConfigurers {@link Iterable} of {@link IndexConfigurer IndexConfigurers} applied
     * to this {@link IndexFactoryBean}.
     * @see org.springframework.data.gemfire.config.annotation.RegionConfigurer
     */
    protected void applyIndexConfigurers(String indexName, Iterable<IndexConfigurer> indexConfigurers) {
        stream(nullSafeIterable(indexConfigurers).spliterator(), false)
                .forEach(indexConfigurer -> indexConfigurer.configure(indexName, this));
    }

    /* (non-Javadoc) */
    private void assertIndexDefinitionConfiguration() {

        Assert.hasText(this.expression, "Index expression is required");
        Assert.hasText(this.from, "Index from clause is required");

        if (IndexType.isKey(this.indexType)) {
            Assert.isTrue(StringUtils.isEmpty(this.imports), "Imports are not supported with a KEY Index");
        }
    }

    /* (non-Javadoc) */
    RegionService resolveCache() {

        return Optional.ofNullable(this.cache)
                .orElseGet(() -> Optional.ofNullable(GemfireUtils.resolveGemFireCache())
                        .orElseThrow(() -> newIllegalStateException("Cache is required")));
    }

    /* (non-Javadoc) */
    String resolveIndexName() {

        return Optional.ofNullable(this.name).filter(StringUtils::hasText)
                .orElseGet(() -> Optional.ofNullable(getBeanName()).filter(StringUtils::hasText)
                        .orElseThrow(() -> newIllegalArgumentException("Index name is required")));
    }

    /* (non-Javadoc) */
    QueryService resolveQueryService() {

        return Optional.ofNullable(this.queryService).orElseGet(() -> Optional.ofNullable(lookupQueryService())
                .orElseThrow(() -> newIllegalStateException("QueryService is required to create an Index")));
    }

    /* (non-Javadoc) */
    QueryService lookupQueryService() {

        String queryServiceBeanName = GemfireConstants.DEFAULT_GEMFIRE_INDEX_DEFINITION_QUERY_SERVICE;

        return Optional.ofNullable(getBeanFactory())
                .filter(beanFactory -> beanFactory.containsBean(queryServiceBeanName))
                .map(beanFactory -> beanFactory.getBean(queryServiceBeanName, QueryService.class))
                .orElseGet(() -> registerQueryServiceBean(queryServiceBeanName, doLookupQueryService()));
    }

    /* (non-Javadoc) */
    QueryService doLookupQueryService() {

        return Optional.ofNullable(this.queryService).orElseGet(
                () -> (this.cache instanceof ClientCache ? ((ClientCache) this.cache).getLocalQueryService()
                        : this.cache.getQueryService()));
    }

    /* (non-Javadoc) */
    QueryService registerQueryServiceBean(String beanName, QueryService queryService) {

        if (isDefine()) {
            ((ConfigurableBeanFactory) getBeanFactory()).registerSingleton(beanName, queryService);
        }

        return queryService;
    }

    /* (non-Javadoc) */
    void registerAlias(String beanName, String indexName) {

        Optional.ofNullable(getBeanFactory()).filter(it -> it instanceof ConfigurableBeanFactory)
                .filter(it -> (beanName != null && !beanName.equals(indexName)))
                .ifPresent(it -> ((ConfigurableBeanFactory) it).registerAlias(beanName, indexName));
    }

    /* (non-Javadoc) */
    Index createIndex(QueryService queryService, String indexName) throws Exception {
        return createIndex(queryService, indexName, false);
    }

    /* (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 createKeyIndex(queryService, indexName, expression, from);
            } else if (IndexType.isHash(indexType)) {
                return createHashIndex(queryService, indexName, expression, from, imports);
            } else {
                return createFunctionalIndex(queryService, indexName, expression, from, imports);
            }
        } catch (IndexExistsException cause) {

            // Same definition, different name

            Optional<Index> existingIndexByDefinition = tryToFindExistingIndexByDefinition(queryService, expression,
                    from, indexType);

            return existingIndexByDefinition.filter(existingIndex -> isIgnoreIfExists()).map(existingIndex -> {

                logWarning(
                        "WARNING! You are choosing to ignore this Index [%1$s] and return the existing"
                                + " Index having the same basic definition [%2$s] but with a different name [%3$s];"
                                + " Make sure no OQL Query Hints refer to this Index by name [%1$s]",
                        indexName, toBasicIndexDefinition(), existingIndex.getName());

                return handleIgnore(existingIndex);

            }).orElseGet(() ->

            existingIndexByDefinition.filter(it -> !retryAttempted && isOverride()).map(existingIndex -> {

                // Log an informational warning to caution the user about using the override
                logWarning(
                        "WARNING! You are attempting to 'override' an existing Index [%1$s]"
                                + " having the same basic definition [%2$s] as the Index that will be created"
                                + " by this IndexFactoryBean [%3$s]; 'Override' effectively 'renames' the existing"
                                + " Index [%1$s] by removing it then recreating it under the new name [%3$s] with"
                                + " the same definition; You should be careful to update any existing OQL Query"
                                + " Hints referring to the old Index name [%1$s] to now use the new name [%3$s]",
                        existingIndex.getName(), toBasicIndexDefinition(), indexName);

                return handleOverride(existingIndex, queryService, indexName);

            }).orElseThrow(() -> {

                String existingIndexName = existingIndexByDefinition.map(Index::getName).orElse("unknown");

                return new GemfireIndexException(String.format(
                        "An Index with a different name [%1$s] having the same definition [%2$s] already exists;"
                                + " You may attempt to override the existing Index [%1$s] with the new name [%3$s]"
                                + " by setting the 'override' property to 'true'",
                        existingIndexName, toBasicIndexDefinition(), indexName), cause);

            }));
        } catch (IndexNameConflictException cause) {

            // Same name; possibly different definition

            Optional<Index> existingIndexByName = tryToFindExistingIndexByName(queryService, indexName);

            return existingIndexByName.filter(existingIndex -> isIgnoreIfExists()).map(existingIndex ->

            handleIgnore(warnOnIndexDefinitionMismatch(existingIndex, indexName, "Returning"))

            ).orElseGet(() ->

            existingIndexByName.filter(it -> !retryAttempted && isOverride()).map(existingIndex ->

            handleSmartOverride(warnOnIndexDefinitionMismatch(existingIndex, indexName, "Overriding"), queryService,
                    indexName)

            ).orElseThrow(() -> {

                String existingIndexDefinition = existingIndexByName
                        .map(it -> String.format(DETAILED_INDEX_DEFINITION, it.getName(), it.getIndexedExpression(),
                                it.getFromClause(), "unknown", it.getType()))
                        .orElse("unknown");

                return new GemfireIndexException(String.format(
                        "An Index with the same name [%1$s] having possibly a different definition already exists;"
                                + " you may choose to ignore this Index definition [%2$s] and use the existing Index"
                                + " definition [%3$s] by setting the 'ignoreIfExists' property to 'true'",
                        indexName, toDetailedIndexDefinition(), existingIndexDefinition), cause);

            }));
        } catch (Exception cause) {
            throw new GemfireIndexException(
                    String.format("Failed to create Index [%s]", toDetailedIndexDefinition()), cause);
        }
    }

    /* (non-Javadoc) */
    @SuppressWarnings("all")
    private boolean isIndexDefinitionMatch(Index index) {

        return Optional.ofNullable(index).map(it -> {

            IndexType thisIndexType = Optional.ofNullable(this.indexType).orElse(IndexType.FUNCTIONAL);

            boolean result = ObjectUtils.nullSafeEquals(it.getIndexedExpression(), this.expression)
                    && ObjectUtils.nullSafeEquals(it.getFromClause(), this.from)
                    && ObjectUtils.nullSafeEquals(IndexType.valueOf(it.getType()), thisIndexType);

            return result;
        }).orElse(false);
    }

    /* (non-Javadoc) */
    private boolean isNotIndexDefinitionMatch(Index index) {
        return !isIndexDefinitionMatch(index);
    }

    /* (non-Javadoc) */
    private Index warnOnIndexDefinitionMismatch(Index existingIndex, String indexName, String action) {

        if (isNotIndexDefinitionMatch(existingIndex)) {

            String existingIndexDefinition = String.format(BASIC_INDEX_DEFINITION,
                    existingIndex.getIndexedExpression(), existingIndex.getFromClause(),
                    IndexType.valueOf(existingIndex.getType()));

            logWarning(
                    "WARNING! %1$s existing Index [%2$s] having a definition [%3$s]"
                            + " that does not match the Index defined [%4$s] by this IndexFactoryBean [%5$s]",
                    action, existingIndex.getName(), existingIndexDefinition, toBasicIndexDefinition(), indexName);
        }

        return existingIndex;
    }

    /* (non-Javadoc) */
    private Index handleIgnore(Index existingIndex) {

        registerAlias(getBeanName(), existingIndex.getName());

        return existingIndex;
    }

    /* (non-Javadoc) */
    private Index handleOverride(Index existingIndex, QueryService queryService, String indexName) {
        try {
            // No way to tell whether the QueryService.remove(:Index) was successful or not! o.O
            // Should return a boolean! Does it throw an RuntimeException? Javadoc is useless; #sigh
            queryService.removeIndex(existingIndex);

            return createIndex(queryService, indexName, true);
        } catch (Exception cause) {
            throw new GemfireIndexException(String.format(
                    "Attempt to 'override' existing Index [%1$s] with the Index that would be created"
                            + " by this IndexFactoryBean [%2$s] failed; you should verify the state of"
                            + " your system and make sure the previously existing Index [%1$s] still exits",
                    existingIndex.getName(), indexName), cause);
        }
    }

    /* (non-Javadoc) */
    private Index handleSmartOverride(Index existingIndex, QueryService queryService, String indexName) {

        return Optional.of(existingIndex).filter(it -> it.getName().equalsIgnoreCase(indexName))
                .filter(it -> isIndexDefinitionMatch(existingIndex))
                .orElseGet(() -> handleOverride(existingIndex, queryService, indexName));
    }

    /* (non-Javadoc) */
    String toBasicIndexDefinition() {
        return String.format(BASIC_INDEX_DEFINITION, this.expression, this.from, this.indexType);
    }

    /* (non-Javadoc) */
    String toDetailedIndexDefinition() {
        return String.format(DETAILED_INDEX_DEFINITION, this.name, this.expression, this.from, this.imports,
                this.indexType);
    }

    /* (non-Javadoc) */
    Index createKeyIndex(QueryService queryService, String indexName, String expression, String from)
            throws Exception {

        if (isDefine()) {
            queryService.defineKeyIndex(indexName, expression, from);
            return new IndexWrapper(queryService, indexName);
        } else {
            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 (isDefine()) {
            if (hasImports) {
                queryService.defineHashIndex(indexName, expression, from, imports);
            } else {
                queryService.defineHashIndex(indexName, expression, from);
            }

            return new IndexWrapper(queryService, indexName);
        } else {
            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 {

        boolean hasImports = StringUtils.hasText(imports);

        if (isDefine()) {
            if (hasImports) {
                queryService.defineIndex(indexName, expression, from, imports);
            } else {
                queryService.defineIndex(indexName, expression, from);
            }

            return new IndexWrapper(queryService, indexName);
        } else {
            if (hasImports) {
                return queryService.createIndex(indexName, expression, from, imports);
            } else {
                return queryService.createIndex(indexName, expression, from);
            }
        }
    }

    /* (non-Javadoc) */
    Optional<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 Optional.of(index);
            }
        }

        return Optional.empty();
    }

    /* (non-Javadoc) */
    Optional<Index> tryToFindExistingIndexByName(QueryService queryService, String indexName) {

        for (Index index : nullSafeCollection(queryService.getIndexes())) {
            if (index.getName().equalsIgnoreCase(indexName)) {
                return Optional.of(index);
            }
        }

        return Optional.empty();
    }

    /**
     * Returns a reference to the Composite {@link IndexConfigurer} used to apply additional configuration
     * to this {@link IndexFactoryBean} on Spring container initialization.
     *
     * @return the Composite {@link IndexConfigurer}.
     * @see org.springframework.data.gemfire.config.annotation.IndexConfigurer
     */
    protected IndexConfigurer getCompositeRegionConfigurer() {
        return this.compositeIndexConfigurer;
    }

    /**
     * Returns a reference to the {@link Index} created by this {@link IndexFactoryBean}.
     *
     * @return a reference to the {@link Index} created by this {@link IndexFactoryBean}.
     * @see org.apache.geode.cache.query.Index
     */
    public Index getIndex() {
        return this.index;
    }

    /**
     * @inheritDoc
     */
    @Override
    public Index getObject() {
        return Optional.ofNullable(getIndex()).orElseGet(
                () -> this.index = tryToFindExistingIndexByName(resolveQueryService(), resolveIndexName())
                        .orElse(null));
    }

    /**
     * @inheritDoc
     */
    @Override
    @SuppressWarnings("unchecked")
    public Class<?> getObjectType() {
        return Optional.ofNullable(getIndex()).map(Index::getClass).orElse((Class) Index.class);
    }

    /**
     * Sets a reference to the {@link RegionService}.
     *
     * @param cache reference to the {@link RegionService}.
     * @see org.apache.geode.cache.RegionService
     */
    public void setCache(RegionService cache) {
        this.cache = cache;
    }

    /**
     * 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;
    }

    /**
     * Sets the {@link QueryService} used to create the {@link Index}.
     *
     * @param service {@link QueryService} used to create the {@link Index}.
     * @see org.apache.geode.cache.query.QueryService
     */
    public void setQueryService(QueryService service) {
        this.queryService = service;
    }

    /**
     * Sets a boolean condition to indicate whether the {@link Index} declared and defined by this
     * {@link IndexFactoryBean} will only be defined initially, or defined and created.  If defined-only,
     * the {@link IndexFactoryBean} will receive a callback at the end of the Spring container lifecycle
     * to subsequently "create" all "defined-only" {@link Index Indexes} once, in a single operation.
     *
     * @param define a boolean value indicating the define or define/create status.  If {@literal true},
     * the {@link Index} declared by this {@link IndexFactoryBean} will only be defined initially
     * and subsequently created when this bean receives an appropriate callback from the Spring container;
     * if {@literal false}, the {@link Index} will be created immediately.
     */
    public void setDefine(boolean define) {
        this.define = define;
    }

    /**
     * Returns a boolean indicating whether the {@link Index} declared and defined by this {@link IndexFactoryBean}
     * will only be defined initially, or defined and created.  If defined-only, the {@link IndexFactoryBean}
     * will receive a callback at the end of the Spring container lifecycle to subsequently "create" all "defined-only"
     * {@link Index Indexes} once, in a single operation.
     *
     * @return a boolean value indicating the define or define/create status.  If {@literal true}, the {@link Index}
     * declared by this {@link IndexFactoryBean} will only be defined initially and subsequently created when this bean
     * receives an appropriate callback from the Spring container; if {@literal false}, the {@link Index}
     * will be created immediately.
     */
    protected boolean isDefine() {
        return define;
    }

    /**
     * @param expression Index expression to set
     */
    public void setExpression(String expression) {
        this.expression = expression;
    }

    /**
     * @param from Index from clause to set
     */
    public void setFrom(String from) {
        this.from = from;
    }

    /**
     * @param imports Index imports to set
     */
    public void setImports(String imports) {
        this.imports = imports;
    }

    /**
     * Configures whether to ignore the {@link Index} defined by this {@link IndexFactoryBean}
     * when an {@link IndexExistsException} or {@link IndexNameConflictException} is thrown.
     *
     * An {@link IndexExistsException} is thrown when there exists another {@link Index} with the same definition
     * but with another name.
     *
     * An {@link IndexNameConflictException} is thrown when there exists another {@link Index} with the same name
     * but possibly a different definition.
     *
     * When {@literal ignoreIfExists} is set to {@literal true} and an {@link IndexExistsException} is thrown,
     * then the existing {@link Index} will be returned as the object of this {@link IndexFactoryBean} creation
     * and the name of the existing {@link Index} is added as an alias for this bean.
     *
     * When {@literal ignoreIfExists} is set to {@literal true} and {@link IndexNameConflictException} is thrown,
     * then the existing {@link Index} will be returned as the object of this {@link IndexFactoryBean} creation.
     * A warning is logged if the definition of this {@link IndexFactoryBean} and the existing {@link Index}
     * are different.
     *
     * {@literal ignoreIfExists} takes precedence over {@link #isOverride() override}.
     *
     * Defaults to {@literal false}.
     *
     * @param ignore boolean value indicating whether to ignore the {@link Index} defined by
     * this {@link IndexFactoryBean}. Default is {@literal false}.
     * @see #setOverride(boolean)
     */
    public void setIgnoreIfExists(boolean ignore) {
        this.ignoreIfExists = ignore;
    }

    /**
     * Determines whether to ignore the {@link Index} defined by this {@link IndexFactoryBean}
     * when an {@link IndexExistsException} or {@link IndexNameConflictException} is thrown.
     *
     * An {@link IndexExistsException} is thrown when there exists another {@link Index} with the same definition
     * but with another name.
     *
     * An {@link IndexNameConflictException} is thrown when there exists another {@link Index} with the same name
     * but possibly a different definition.
     *
     * When {@literal ignoreIfExists} is set to {@literal true} and an {@link IndexExistsException} is thrown,
     * then the existing {@link Index} will be returned as the object of this {@link IndexFactoryBean} creation
     * and the name of the existing {@link Index} is added as an alias for this bean.
     *
     * When {@literal ignoreIfExists} is set to {@literal true} and {@link IndexNameConflictException} is thrown,
     * then the existing {@link Index} will be returned as the object of this {@link IndexFactoryBean} creation.
     * A warning is logged if the definition of this {@link IndexFactoryBean} and the existing {@link Index}
     * are different.
     *
     * {@literal ignoreIfExists} takes precedence over {@link #isOverride() override}.
     *
     * Defaults to {@literal false}.
     *
     * @return a boolean value indicating whether to ignore the {@link Index} defined by this {@link IndexFactoryBean}.
     * Default is {@literal false}.
     * @see #setIgnoreIfExists(boolean)
     */
    public boolean isIgnoreIfExists() {
        return this.ignoreIfExists;
    }

    /**
     * Null-safe operation to set an array of {@link IndexConfigurer IndexConfigurers} used to apply
     * additional configuration to this {@link IndexFactoryBean} when using Annotation-based configuration.
     *
     * @param indexConfigurers array of {@link IndexConfigurer IndexConfigurers} used to apply
     * additional configuration to this {@link IndexFactoryBean}.
     * @see org.springframework.data.gemfire.config.annotation.IndexConfigurer
     * @see #setIndexConfigurers(List)
     */
    public void setIndexConfigurers(IndexConfigurer... indexConfigurers) {
        setIndexConfigurers(Arrays.asList(nullSafeArray(indexConfigurers, IndexConfigurer.class)));
    }

    /**
     * Null-safe operation to set an {@link Iterable} of {@link IndexConfigurer IndexConfigurers} used to apply
     * additional configuration to this {@link IndexFactoryBean} when using Annotation-based configuration.
     *
     * @param indexConfigurers {@link Iterable } of {@link IndexConfigurer IndexConfigurers} used to apply
     * additional configuration to this {@link IndexFactoryBean}.
     * @see org.springframework.data.gemfire.config.annotation.IndexConfigurer
     */
    public void setIndexConfigurers(List<IndexConfigurer> indexConfigurers) {
        this.indexConfigurers = Optional.ofNullable(indexConfigurers).orElseGet(Collections::emptyList);
    }

    /**
     * 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;
    }

    /**
     * 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));
    }

    /**
     * Set the {@link IndexType type} of the {@link Index}.
     *
     * @param type {@link IndexType} indicating the type of the {@link Index}.
     * @see org.springframework.data.gemfire.IndexType
     */
    public void setType(IndexType type) {
        this.indexType = type;
    }

    /* (non-Javadoc) */
    protected static final class IndexWrapper implements Index {

        private Index index;

        private final QueryService queryService;

        private final String indexName;

        protected IndexWrapper(QueryService queryService, String indexName) {

            Assert.notNull(queryService, "QueryService is required");
            Assert.hasText(indexName, "Name of Index is required");

            this.queryService = queryService;
            this.indexName = indexName;
        }

        /* (non-Javadoc) */
        protected synchronized Index resolveIndex() {

            String indexName = getIndexName();

            return Optional.ofNullable(this.index).orElseGet(() -> {

                AtomicReference<Index> searchResult = new AtomicReference<>();

                nullSafeCollection(getQueryService().getIndexes()).forEach(index -> {
                    if (index.getName().equalsIgnoreCase(indexName)) {
                        searchResult.set(index);
                    }
                });

                return Optional.of(searchResult).map(it -> {
                    this.index = it.get();
                    return this.index;
                }).orElseThrow(() -> new GemfireIndexException(
                        String.format("Index with name [%s] was not found", indexName), (Exception) null));
            });
        }

        /* (non-Javadoc) */
        protected Index getIndex() {
            return this.index;
        }

        /* (non-Javadoc) */
        protected String getIndexName() {
            return Optional.ofNullable(this.indexName).filter(StringUtils::hasText)
                    .orElseThrow(() -> newIllegalStateException("Index name is required"));
        }

        /* (non-Javadoc) */
        protected QueryService getQueryService() {
            return this.queryService;
        }

        @Override
        public String getName() {
            return resolveIndex().getName();
        }

        @Override
        public String getCanonicalizedFromClause() {
            return resolveIndex().getCanonicalizedFromClause();
        }

        @Override
        public String getCanonicalizedIndexedExpression() {
            return resolveIndex().getCanonicalizedIndexedExpression();
        }

        @Override
        public String getCanonicalizedProjectionAttributes() {
            return resolveIndex().getCanonicalizedProjectionAttributes();
        }

        @Override
        public String getFromClause() {
            return resolveIndex().getFromClause();
        }

        @Override
        public String getIndexedExpression() {
            return resolveIndex().getIndexedExpression();
        }

        @Override
        public String getProjectionAttributes() {
            return resolveIndex().getProjectionAttributes();
        }

        @Override
        public Region<?, ?> getRegion() {
            return resolveIndex().getRegion();
        }

        @Override
        public IndexStatistics getStatistics() {
            return resolveIndex().getStatistics();
        }

        @Override
        @SuppressWarnings("deprecation")
        public org.apache.geode.cache.query.IndexType getType() {
            return resolveIndex().getType();
        }

        @Override
        public boolean equals(Object obj) {

            if (this == obj) {
                return true;
            }

            if (!(obj instanceof IndexWrapper || obj instanceof Index)) {
                return false;
            }

            if (obj instanceof IndexWrapper) {
                return (getIndexName().equals(((IndexWrapper) obj).getIndexName()));
            }

            return resolveIndex().equals(obj);
        }

        @Override
        public int hashCode() {

            int hashValue = 37;

            hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getIndexName());
            hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(index);

            return hashValue;
        }

        @Override
        public String toString() {

            return Optional.ofNullable(getIndex()).map(String::valueOf).orElseGet(this::getIndexName);
        }
    }
}