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

com.netgrif.application.engine.elastic.service.ElasticCaseService Maven / Gradle / Ivy

package com.netgrif.application.engine.elastic.service;

import com.netgrif.application.engine.auth.domain.LoggedUser;
import com.netgrif.application.engine.configuration.properties.ElasticsearchProperties;
import com.netgrif.application.engine.elastic.domain.ElasticCase;
import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository;
import com.netgrif.application.engine.elastic.domain.ElasticQueryConstants;
import com.netgrif.application.engine.elastic.service.executors.Executor;
import com.netgrif.application.engine.elastic.service.interfaces.IElasticCasePrioritySearch;
import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService;
import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest;
import com.netgrif.application.engine.petrinet.domain.PetriNetSearch;
import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService;
import com.netgrif.application.engine.petrinet.web.responsebodies.PetriNetReference;
import com.netgrif.application.engine.utils.FullPageRequest;
import com.netgrif.application.engine.workflow.domain.Case;
import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.*;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHitSupport;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Order;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;

import static org.elasticsearch.index.query.QueryBuilders.*;

@Service
public class ElasticCaseService extends ElasticViewPermissionService implements IElasticCaseService {

    private static final Logger log = LoggerFactory.getLogger(ElasticCaseService.class);

    protected ElasticCaseRepository repository;

    protected IWorkflowService workflowService;

    @Value("${spring.data.elasticsearch.index.case}")
    protected String caseIndex;

    @Autowired
    protected ElasticsearchRestTemplate template;

    protected Executor executors;

    @Autowired
    protected ElasticsearchProperties elasticsearchProperties;

    @Autowired
    protected IPetriNetService petriNetService;

    @Autowired
    protected IElasticCasePrioritySearch iElasticCasePrioritySearch;

    @Autowired
    public ElasticCaseService(ElasticCaseRepository repository, ElasticsearchRestTemplate template, Executor executors) {
        this.repository = repository;
        this.template = template;
        this.executors = executors;
    }

    @Autowired
    @Lazy
    public void setWorkflowService(IWorkflowService workflowService) {
        this.workflowService = workflowService;
    }

    @Override
    public void remove(String caseId) {
        executors.execute(caseId, () -> {
            repository.deleteAllByStringId(caseId);
            log.info("[" + caseId + "]: Case \"" + caseId + "\" deleted");
        });
    }

    @Override
    public void removeByPetriNetId(String processId) {
        executors.execute(processId, () -> {
            repository.deleteAllByProcessId(processId);
            log.info("[" + processId + "]: All cases of Petri Net with id \"" + processId + "\" deleted");
        });
    }

    @Override
    public void index(ElasticCase useCase) {
        executors.execute(useCase.getStringId(), () -> {
            try {
                ElasticCase elasticCase = repository.findByStringId(useCase.getStringId());
                if (elasticCase == null) {
                    repository.save(useCase);
                } else {
                    elasticCase.update(useCase);
                    repository.save(elasticCase);
                }
                log.debug("[" + useCase.getStringId() + "]: Case \"" + useCase.getTitle() + "\" indexed");
            } catch (InvalidDataAccessApiUsageException ignored) {
                log.debug("[" + useCase.getStringId() + "]: Case \"" + useCase.getTitle() + "\" has duplicates, will be reindexed");
                repository.deleteAllByStringId(useCase.getStringId());
                repository.save(useCase);
                log.debug("[" + useCase.getStringId() + "]: Case \"" + useCase.getTitle() + "\" indexed");
            }
        });
    }

    @Override
    public void indexNow(ElasticCase useCase) {
        index(useCase);
    }

    @Override
    public Page search(List requests, LoggedUser user, Pageable pageable, Locale locale, Boolean isIntersection) {
        if (requests == null) {
            throw new IllegalArgumentException("Request can not be null!");
        }
        log.debug("Searching for query with logged user [{}]", user.getId());
        LoggedUser loggedOrImpersonated = user.getSelfOrImpersonated();
        pageable = resolveUnmappedSortAttributes(pageable);
        NativeSearchQuery query = buildQuery(requests, loggedOrImpersonated, pageable, locale, isIntersection);
        List casePage;
        long total;
        if (query != null) {
            SearchHits hits = template.search(query, ElasticCase.class, IndexCoordinates.of(caseIndex));
            Page indexedCases = (Page) SearchHitSupport.unwrapSearchHits(SearchHitSupport.searchPageFor(hits, query.getPageable()));
            casePage = workflowService.findAllById(indexedCases.get().map(ElasticCase::getStringId).collect(Collectors.toList()));
            total = indexedCases.getTotalElements();
            log.debug("Found [{}] total elements of page [{}]", casePage.size(), pageable.getPageNumber());
        } else {
            casePage = Collections.emptyList();
            total = 0;
        }

        return new PageImpl<>(casePage, pageable, total);
    }

    @Override
    public long count(List requests, LoggedUser user, Locale locale, Boolean isIntersection) {
        if (requests == null) {
            throw new IllegalArgumentException("Request can not be null!");
        }

        LoggedUser loggedOrImpersonated = user.getSelfOrImpersonated();
        NativeSearchQuery query = buildQuery(requests, loggedOrImpersonated, new FullPageRequest(), locale, isIntersection);
        if (query != null) {
            return template.count(query, ElasticCase.class);
        } else {
            return 0;
        }
    }

    public String findUriNodeId(Case aCase) {
        if (aCase == null) {
            return null;
        }
        ElasticCase elasticCase = repository.findByStringId(aCase.getStringId());
        if (elasticCase == null) {
            log.warn("[" + aCase.getStringId() + "] Case with id [" + aCase.getStringId() + "] is not indexed.");
            return null;
        }

        return elasticCase.getUriNodeId();
    }

    protected NativeSearchQuery buildQuery(List requests, LoggedUser user, Pageable pageable, Locale locale, Boolean isIntersection) {
        List singleQueries = requests.stream().map(request -> buildSingleQuery(request, user, locale)).collect(Collectors.toList());

        if (isIntersection && !singleQueries.stream().allMatch(Objects::nonNull)) {
            // one of the queries evaluates to empty set => the entire result is an empty set
            return null;
        } else if (!isIntersection) {
            singleQueries = singleQueries.stream().filter(Objects::nonNull).collect(Collectors.toList());
            if (singleQueries.size() == 0) {
                // all queries result in an empty set => the entire result is an empty set
                return null;
            }
        }

        BinaryOperator reductionOperator = isIntersection ? BoolQueryBuilder::must : BoolQueryBuilder::should;
        BoolQueryBuilder query = singleQueries.stream().reduce(new BoolQueryBuilder(), reductionOperator);

        NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
        return builder
                .withQuery(query)
                .withPageable(pageable)
                .build();
    }

    protected BoolQueryBuilder buildSingleQuery(CaseSearchRequest request, LoggedUser user, Locale locale) {
        BoolQueryBuilder query = boolQuery();

        buildViewPermissionQuery(query, user);
        buildPetriNetQuery(request, user, query);
        buildAuthorQuery(request, query);
        buildTaskQuery(request, query);
        buildRoleQuery(request, query);
        buildDataQuery(request, query);
        buildFullTextQuery(request, query);
        buildStringQuery(request, query, user);
        buildCaseIdQuery(request, query);
        buildUriNodeIdQuery(request, query);
        buildTagsQuery(request, query);
        boolean resultAlwaysEmpty = buildGroupQuery(request, user, locale, query);

        // TODO: filtered query https://stackoverflow.com/questions/28116404/filtered-query-using-nativesearchquerybuilder-in-spring-data-elasticsearch

        if (resultAlwaysEmpty)
            return null;
        else
            return query;
    }

    protected void buildPetriNetQuery(CaseSearchRequest request, LoggedUser user, BoolQueryBuilder query) {
        if (request.process == null || request.process.isEmpty()) {
            return;
        }

        BoolQueryBuilder petriNetQuery = boolQuery();

        for (CaseSearchRequest.PetriNet process : request.process) {
            if (process.identifier != null) {
                petriNetQuery.should(termQuery("processIdentifier", process.identifier));
            }
            if (process.processId != null) {
                petriNetQuery.should(termQuery("processId", process.processId));
            }
        }

        query.filter(petriNetQuery);
    }

    /**
     * 
     * {
     *     "author": {
     *         "email": "[email protected]"
     *     }
     * }
     * 

*

* Cases with author with (id 1 AND email "[email protected]") OR (id 2) *

     * {
     *     "author": [{
     *         "id": 1
     *         "email": "[email protected]"
     *     }, {
     *         "id": 2
     *     }
     *     ]
     * }
     * 

*/ protected void buildAuthorQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.author == null || request.author.isEmpty()) { return; } BoolQueryBuilder authorsQuery = boolQuery(); for (CaseSearchRequest.Author author : request.author) { BoolQueryBuilder authorQuery = boolQuery(); if (author.email != null) { authorQuery.must(termQuery("authorEmail", author.email)); } if (author.id != null) { authorQuery.must(matchQuery("author", author.id)); } if (author.name != null) { authorQuery.must(termQuery("authorName", author.name)); } authorsQuery.should(authorQuery); } query.filter(authorsQuery); } /** * Cases with tasks with import Id "nova_uloha" *
     * {
     *     "transition": "nova_uloha"
     * }
     * 
*

* Cases with tasks with import Id "nova_uloha" OR "kontrola" *

     * {
     *     "transition": [
     *         "nova_uloha",
     *         "kontrola"
     *     ]
     * }
     * 
*/ protected void buildTaskQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.transition == null || request.transition.isEmpty()) { return; } BoolQueryBuilder taskQuery = boolQuery(); for (String taskImportId : request.transition) { taskQuery.should(termQuery("taskIds", taskImportId)); } query.filter(taskQuery); } /** * Cases with active role "5cb07b6ff05be15f0b972c36" *
     * {
     *     "role": "5cb07b6ff05be15f0b972c36"
     * }
     * 
*

* Cases with active role "5cb07b6ff05be15f0b972c36" OR "5cb07b6ff05be15f0b972c31" *

     * {
     *     "role" [
     *         "5cb07b6ff05be15f0b972c36",
     *         "5cb07b6ff05be15f0b972c31"
     *     ]
     * }
     * 
*/ protected void buildRoleQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.role == null || request.role.isEmpty()) { return; } BoolQueryBuilder roleQuery = boolQuery(); for (String roleId : request.role) { roleQuery.should(termQuery("enabledRoles", roleId)); } query.filter(roleQuery); } /** * Cases where "text_field" has value EXACTLY "text" AND "number_field" has value EXACTLY "125".
*
     * {
     *     "data": {
     *         "text_field": "text",
     *         "number_field": "125"
     *     }
     * }
     * 
*/ protected void buildDataQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.data == null || request.data.isEmpty()) { return; } BoolQueryBuilder dataQuery = boolQuery(); for (Map.Entry field : request.data.entrySet()) { if (field.getKey().contains(".")) dataQuery.must(termQuery("dataSet." + field.getKey(), field.getValue())); else dataQuery.must(termQuery("dataSet." + field.getKey() + ".fulltextValue.keyword", field.getValue())); } query.filter(dataQuery); } protected void buildTagsQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.tags == null || request.tags.isEmpty()) { return; } BoolQueryBuilder tagsQuery = boolQuery(); for (Map.Entry field : request.tags.entrySet()) { tagsQuery.must(termQuery("tags." + field.getKey(), field.getValue())); } query.filter(tagsQuery); } protected void buildFullTextQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.fullText == null || request.fullText.isEmpty()) { return; } // TODO: improvement? wildcard does not scale good //String searchText = elasticsearchProperties.isAnalyzerEnabled() ? request.fullText : "*" + request.fullText + "*"; String searchText = "*" + request.fullText + "*"; QueryBuilder fullTextQuery = queryStringQuery(searchText).fields(iElasticCasePrioritySearch.fullTextFields()); query.must(fullTextQuery); } /** * See Query String Query */ protected void buildStringQuery(CaseSearchRequest request, BoolQueryBuilder query, LoggedUser user) { if (request.query == null || request.query.isEmpty()) { return; } String populatedQuery = request.query.replaceAll(ElasticQueryConstants.USER_ID_TEMPLATE, user.getId().toString()); query.must(queryStringQuery(populatedQuery).allowLeadingWildcard(true).analyzeWildcard(true)); } /** * Case with stringId "5cb07b6ff05be15f0b972c36" *
     * {
     *     "stringId": "5cb07b6ff05be15f0b972c36"
     * }
     * 
*

* Cases with stringId "5cb07b6ff05be15f0b972c36" OR "5cb07b6ff05be15f0b972c31" *

     * {
     *     "stringId" [
     *         "5cb07b6ff05be15f0b972c36",
     *         "5cb07b6ff05be15f0b972c31"
     *     ]
     * }
     * 
*/ protected void buildCaseIdQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.stringId == null || request.stringId.isEmpty()) { return; } BoolQueryBuilder caseIdQuery = boolQuery(); request.stringId.forEach(caseId -> caseIdQuery.should(termQuery("stringId", caseId))); query.filter(caseIdQuery); } protected void buildUriNodeIdQuery(CaseSearchRequest request, BoolQueryBuilder query) { if (request.uriNodeId == null || request.uriNodeId.isEmpty()) { return; } BoolQueryBuilder caseIdQuery = boolQuery(); caseIdQuery.should(termQuery("uriNodeId", request.uriNodeId)); query.filter(caseIdQuery); } /** * Cases that are instances of processes of group "5cb07b6ff05be15f0b972c36" *
     * {
     *     "group": "5cb07b6ff05be15f0b972c36"
     * }
     * 
*

* Cases that are instances of processes of group "5cb07b6ff05be15f0b972c36" OR "5cb07b6ff05be15f0b972c31" *

     * {
     *     "group" [
     *         "5cb07b6ff05be15f0b972c36",
     *         "5cb07b6ff05be15f0b972c31"
     *     ]
     * }
     * 
*/ protected boolean buildGroupQuery(CaseSearchRequest request, LoggedUser user, Locale locale, BoolQueryBuilder query) { if (request.group == null || request.group.isEmpty()) { return false; } PetriNetSearch processQuery = new PetriNetSearch(); processQuery.setGroup(request.group); List groupProcesses = this.petriNetService.search(processQuery, user, new FullPageRequest(), locale).getContent(); if (groupProcesses.size() == 0) return true; BoolQueryBuilder groupQuery = boolQuery(); groupProcesses.stream().map(PetriNetReference::getIdentifier) .map(netIdentifier -> termQuery("processIdentifier", netIdentifier)) .forEach(groupQuery::should); query.filter(groupQuery); return false; } protected Pageable resolveUnmappedSortAttributes(Pageable pageable) { List modifiedOrders = new ArrayList<>(); pageable.getSort().iterator().forEachRemaining(order -> modifiedOrders.add(new Order(order.getDirection(), order.getProperty()).withUnmappedType("keyword"))); return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize()).withSort(Sort.by(modifiedOrders)); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy