All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bonitasoft.engine.search.AbstractSearchEntity Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (C) 2019 Bonitasoft S.A.
 * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
 * This library is free software; you can redistribute it and/or modify it under the terms
 * of the GNU Lesser General Public License as published by the Free Software Foundation
 * version 2.1 of the License.
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
 * Floor, Boston, MA 02110-1301, USA.
 **/
package org.bonitasoft.engine.search;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import lombok.extern.slf4j.Slf4j;
import org.bonitasoft.engine.commons.exceptions.SBonitaException;
import org.bonitasoft.engine.commons.transaction.TransactionContentWithResult;
import org.bonitasoft.engine.exception.SearchException;
import org.bonitasoft.engine.persistence.FilterOption;
import org.bonitasoft.engine.persistence.OrderByOption;
import org.bonitasoft.engine.persistence.PersistentObject;
import org.bonitasoft.engine.persistence.QueryOptions;
import org.bonitasoft.engine.persistence.SBonitaReadException;
import org.bonitasoft.engine.persistence.SearchFields;
import org.bonitasoft.engine.search.descriptor.SearchEntityDescriptor;
import org.bonitasoft.engine.search.impl.SearchFilter;
import org.bonitasoft.engine.search.impl.SearchResultImpl;

/**
 * Abstract class to allow to search server object and convert them to client object
 *
 * @author Matthieu Chaffotte
 * @author Baptiste Mesta
 * @author Celine Souchet
 * @param 
 *        The client object
 * @param 
 *        The server object
 */
@Slf4j
public abstract class AbstractSearchEntity
        implements TransactionContentWithResult> {

    private final SearchOptions options;

    private final SearchEntityDescriptor searchDescriptor;

    private long count;

    private List clientObjects;

    /**
     * @param searchDescriptor
     *        The search descriptor of the searched entity
     * @param options
     *        The options of the search
     */
    public AbstractSearchEntity(final SearchEntityDescriptor searchDescriptor, final SearchOptions options) {
        this.searchDescriptor = searchDescriptor;
        this.options = options;
    }

    protected void validateQuery(SearchOptions options) throws SBonitaException {
        /*
         * Used to validate the query before execution,
         * in order to throw exceptions with meaningful message.
         */
    }

    @Override
    public void execute() throws SBonitaException {
        validateQuery(options);
        List serverObjects;
        if (options == null) {
            throw new SBonitaReadException("SearchOptions cannot be null");
        }
        final int numberOfResults = options.getMaxResults();
        final int fromIndex = options.getStartIndex();
        final List filters = options.getFilters();
        final List filterOptions = new ArrayList<>(filters.size());
        for (final SearchFilter filter : filters) {
            final FilterOption option = searchDescriptor.getEntityFilter(filter);
            if (option != null) {// in case of a unknown filter on state
                filterOptions.add(option);
            }
        }
        final String searchTerm = options.getSearchTerm();
        SearchFields userSearchTerm = null;
        if (searchTerm != null) {
            userSearchTerm = searchDescriptor.getEntitySearchTerm(searchTerm);
        }
        final List orderOptions = new ArrayList<>();
        final List sorts = options.getSorts();
        for (final Sort sort : sorts) {
            final OrderByOption order = searchDescriptor.getEntityOrder(sort);
            orderOptions.add(order);
        }
        final QueryOptions countOptions = new QueryOptions(0, QueryOptions.UNLIMITED_NUMBER_OF_RESULTS, null,
                filterOptions, userSearchTerm);
        count = executeCount(countOptions);
        if (count > 0 && numberOfResults != 0) {
            final QueryOptions searchOptions = new QueryOptions(fromIndex, numberOfResults, orderOptions, filterOptions,
                    userSearchTerm);
            serverObjects = executeSearch(searchOptions);
            detectPotentialTransactionIsolationIssue(serverObjects, numberOfResults, countOptions);
        } else {
            serverObjects = Collections.emptyList();
        }
        clientObjects = convertToClientObjects(serverObjects);
    }

    private void detectPotentialTransactionIsolationIssue(List serverObjects, int numberOfResults,
            QueryOptions countOptions) throws SBonitaReadException {
        // If there are at most 1 page of result AND the count does not detect as many objects as the search:
        if (count <= numberOfResults && count != serverObjects.size()) {
            long doubleCheck = executeCount(countOptions);
            if (count != doubleCheck) {
                log.error("Double checking the same query within the same transaction did NOT bring the same"
                        + " result. You DO have a database transaction isolation problem. Please fix it ASAP. See" +
                        " https://documentation.bonitasoft.com/bonita/latest/runtime/database-configuration#customize-rdbms"
                        + " for details.");
            } else {
                log.warn("Within the same transaction, the Search count & page results are not consistent." +
                        " Please see https://documentation.bonitasoft.com/bonita/latest/runtime/performance-troubleshooting#monitor-transaction-isolation");
            }
        }
    }

    /**
     * execute this search and return the result
     *
     * @return the result of the search
     */
    public SearchResult search() throws SearchException {
        try {
            execute();
        } catch (SBonitaException e) {
            throw new SearchException(e);
        }
        return getResult();
    }

    /**
     * Execute the count here
     *
     * @param queryOptions
     *        The query options to execute the count with
     * @return The number of result on the server
     * @throws SBonitaReadException when the search failed to retrieve the count number
     */
    public abstract long executeCount(QueryOptions queryOptions) throws SBonitaReadException;

    /**
     * Execute the search here
     *
     * @param queryOptions
     *        The query options to execute the search with
     * @return The list of searched server objects
     * @throws SBonitaReadException when the search failed to retrieve the results
     */
    public abstract List executeSearch(QueryOptions queryOptions) throws SBonitaReadException;

    /**
     * Must convert server objects in client objects here
     *
     * @param serverObjects
     *        The server object to convert
     * @return The list of the client objects corresponding to the server objects
     */
    public abstract List convertToClientObjects(List serverObjects) throws SBonitaException;

    @Override
    public SearchResult getResult() {
        return new SearchResultImpl<>(count, clientObjects);
    }

    protected SearchFilter getSearchFilter(final SearchOptions searchOptions, final String searchedKey) {
        return searchOptions.getFilters().stream().filter(searchFilter -> searchedKey.equals(searchFilter.getField()))
                .findFirst().orElse(null);
    }

    public static  SearchResult search(
            SearchEntityDescriptor searchDescriptor,
            SearchOptions options,
            BonitaReadFunction, List> converter,
            BonitaReadFunction count,
            BonitaReadFunction> search) throws SearchException {
        return new AbstractSearchEntity(searchDescriptor, options) {

            @Override
            public long executeCount(QueryOptions queryOptions) throws SBonitaReadException {
                return count.apply(queryOptions);
            }

            @Override
            public List executeSearch(QueryOptions queryOptions) throws SBonitaReadException {
                return search.apply(queryOptions);
            }

            @Override
            public List convertToClientObjects(List serverObjects) throws SBonitaException {
                return converter.apply(serverObjects);
            }
        }.search();
    }

}