diff --git a/CHANGELOG.md b/CHANGELOG.md index 30cb5e20d78..2e31cb19e54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Full Changelog: [https://github.com/netgrif/application-engine/commits/v6.3.2](h - [NAE-1908] NAE-1906 Improvements - [NAE-1937] Fix the problem with empty string in filter - [NAE-1884] Improve execution of auto trigger tasks +- [NAE-1942] Authorization Bypass Process Download ### Added - [NAE-1901] Taskref list rendering update diff --git a/src/main/groovy/com/netgrif/application/engine/startup/ElasticsearchRunner.groovy b/src/main/groovy/com/netgrif/application/engine/startup/ElasticsearchRunner.groovy index 8c7613ef362..e98d1ad77ba 100644 --- a/src/main/groovy/com/netgrif/application/engine/startup/ElasticsearchRunner.groovy +++ b/src/main/groovy/com/netgrif/application/engine/startup/ElasticsearchRunner.groovy @@ -1,5 +1,6 @@ package com.netgrif.application.engine.startup +import com.netgrif.application.engine.configuration.ElasticsearchConfiguration import com.netgrif.application.engine.configuration.properties.UriProperties import com.netgrif.application.engine.elastic.domain.ElasticCase import com.netgrif.application.engine.elastic.domain.ElasticPetriNet @@ -42,20 +43,41 @@ class ElasticsearchRunner extends AbstractOrderedCommandLineRunner { @Autowired private IElasticIndexService template + @Autowired + private ElasticsearchConfiguration properties + @Override void run(String... args) throws Exception { if (drop) { - log.info("Dropping Elasticsearch database [${url}:${port}/${clusterName}]") - template.deleteIndex(ElasticPetriNet.class) - template.deleteIndex(ElasticCase.class) - template.deleteIndex(ElasticTask.class) - } - if (!template.indexExists(petriNetIndex)) { - log.info "Creating Elasticsearch case index [${petriNetIndex}]" - template.createIndex(ElasticPetriNet.class) - } else { - log.info "Elasticsearch case index exists [${caseIndex}]" + log.info("Dropping Elasticsearch database [${url}:${port}/${clusterName}]"); + if (template.indexExists(caseIndex)) { + template.deleteIndex(ElasticCase.class) + } + if (template.indexExists(taskIndex)) { + template.deleteIndex(ElasticTask.class) + } + if (!template.indexExists(petriNetIndex)) { + log.info "Creating Elasticsearch case index [${petriNetIndex}]" + template.createIndex(ElasticPetriNet.class) + } + try { + template.getAllDynamicIndexes().forEach(indexName -> { + if (template.indexExists(indexName)) { + log.info("Deleting dynamic index {}", indexName); + template.deleteIndex(indexName); + } else { + log.warn("Index {} does not exist, skipping deletion.", indexName); + } + }) + } catch (Exception e){ + log.warn("Index {} does not exist, skipping deletion.", e.message); + } + if (template.indexExists(uriProperties.index)) { + template.deleteIndex(UriNode.class) + } + template.evictAllCaches(); } + if (!template.indexExists(caseIndex)) { log.info "Creating Elasticsearch case index [${caseIndex}]" template.createIndex(ElasticCase.class) diff --git a/src/main/java/com/netgrif/application/engine/ApplicationEngine.java b/src/main/java/com/netgrif/application/engine/ApplicationEngine.java index c9688434e8b..c63f9ab42a7 100644 --- a/src/main/java/com/netgrif/application/engine/ApplicationEngine.java +++ b/src/main/java/com/netgrif/application/engine/ApplicationEngine.java @@ -23,14 +23,14 @@ import java.util.ArrayList; import java.util.List; +@Slf4j +@Aspect @EnableCaching -@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) -@EnableGlobalMethodSecurity(prePostEnabled = true) -@EnableAspectJAutoProxy -@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) @EnableMongoAuditing -@Aspect -@Slf4j +@EnableAspectJAutoProxy +@EnableGlobalMethodSecurity(prePostEnabled = true) +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) public class ApplicationEngine { @Around("execution(* com.netgrif.application.engine.startup.AbstractOrderedCommandLineRunner+.run(..))") diff --git a/src/main/java/com/netgrif/application/engine/configuration/ElasticServiceConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/ElasticServiceConfiguration.java index 02404db0b1d..e00e4f35287 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/ElasticServiceConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/ElasticServiceConfiguration.java @@ -4,9 +4,11 @@ import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository; import com.netgrif.application.engine.elastic.domain.ElasticTaskRepository; import com.netgrif.application.engine.elastic.service.ElasticCaseService; +import com.netgrif.application.engine.elastic.service.ElasticIndexService; import com.netgrif.application.engine.elastic.service.ElasticTaskService; import com.netgrif.application.engine.elastic.service.executors.Executor; import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -23,9 +25,6 @@ ) public class ElasticServiceConfiguration { - @Autowired - private ElasticCaseRepository caseRepository; - @Autowired private ElasticTaskRepository taskRepository; @@ -54,7 +53,7 @@ public Executor reindexingTaskTaskExecutor() { @Bean @Primary public IElasticCaseService elasticCaseService() { - return new ElasticCaseService(caseRepository, elasticsearchTemplate, executor()); + return new ElasticCaseService(elasticsearchTemplate, executor()); } @Bean @@ -62,10 +61,14 @@ public IElasticCaseService elasticCaseService() { public IElasticTaskService elasticTaskService() { return new ElasticTaskService(elasticsearchTemplate); } + @Bean + public IElasticIndexService elasticIndexService() { + return new ElasticIndexService(elasticsearchTemplate); + } @Bean public IElasticCaseService reindexingTaskElasticCaseService() { - return new ElasticCaseService(caseRepository, elasticsearchTemplate, reindexingTaskCaseExecutor()); + return new ElasticCaseService(elasticsearchTemplate, reindexingTaskCaseExecutor()); } diff --git a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java index dbd923e5a51..ba51c71bfc1 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/ElasticsearchConfiguration.java @@ -65,7 +65,6 @@ public String elasticUriIndex() { @Bean public RestHighLevelClient client() { - return new RestHighLevelClient( RestClient.builder(new HttpHost(url, port, "http"))); } diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/CacheProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/CacheProperties.java index d62b67be4db..c253f8b9ebf 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/properties/CacheProperties.java +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/CacheProperties.java @@ -19,11 +19,31 @@ public class CacheProperties { private String petriNetCache = "petriNetCache"; + private String caseIndexByNodeId = "caseIndexByNodeId"; + + private String caseIndexDynamic = "caseIndexDynamic"; + + private String caseIndexAll = "caseIndexAll"; + + private String caseIndexByMenuTaskId = "caseIndexByMenuTaskId"; + private List additional = new ArrayList<>(); public Set getAllCaches() { - Set caches = new LinkedHashSet<>(Arrays.asList(petriNetById, petriNetByIdentifier, petriNetNewest, petriNetCache)); + List cacheNames = Arrays.asList( + petriNetById, + petriNetByIdentifier, + petriNetNewest, + petriNetCache, + caseIndexByNodeId, + caseIndexDynamic, + caseIndexAll, + caseIndexByMenuTaskId + ); + + Set caches = new LinkedHashSet<>(cacheNames); caches.addAll(additional); return caches; } + } diff --git a/src/main/java/com/netgrif/application/engine/elastic/domain/IndexAwareElasticSearchRequest.java b/src/main/java/com/netgrif/application/engine/elastic/domain/IndexAwareElasticSearchRequest.java new file mode 100644 index 00000000000..a969fe77455 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/elastic/domain/IndexAwareElasticSearchRequest.java @@ -0,0 +1,57 @@ +package com.netgrif.application.engine.elastic.domain; + + +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.*; + +@NoArgsConstructor +public class IndexAwareElasticSearchRequest extends ArrayList implements List { + + /** + * taskIds of task "view" in preference_menu_item (by default part of URL) + */ + @Getter + @Setter + private List menuItemIds; + + /** + * actual index name in elasticsearch + */ + @Getter + @Setter + private List indexNames; + + @Getter + @Setter + private boolean allIndex; + + private IndexAwareElasticSearchRequest(List menuItemIds, List indexNames, Boolean allIndex) { + this.menuItemIds = menuItemIds; + this.indexNames = indexNames; + this.allIndex = Objects.requireNonNullElse(allIndex, false); + } + + public static IndexAwareElasticSearchRequest all() { + return new IndexAwareElasticSearchRequest(null, null, true); + } + + public static IndexAwareElasticSearchRequest ofIndex(String indexName) { + return new IndexAwareElasticSearchRequest(null, Collections.singletonList(indexName), false); + } + + public static IndexAwareElasticSearchRequest ofIndexes(List indexNames) { + return new IndexAwareElasticSearchRequest(null, Collections.unmodifiableList(indexNames), false); + } + + public static IndexAwareElasticSearchRequest ofMenuItems(List menuItemIds) { + return new IndexAwareElasticSearchRequest(Collections.unmodifiableList(Optional.ofNullable(menuItemIds).orElse(Collections.emptyList())), null, false); + } + + public boolean doQueryAll() { + return this.allIndex; + } +} diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java index 5c9e1cb9602..713f62945a7 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticCaseService.java @@ -2,40 +2,43 @@ import com.netgrif.application.engine.auth.domain.LoggedUser; 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.domain.IndexAwareElasticSearchRequest; 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.service.interfaces.IElasticIndexService; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; import com.netgrif.application.engine.petrinet.domain.PetriNetSearch; +import com.netgrif.application.engine.petrinet.domain.PetriNet; +import com.netgrif.application.engine.petrinet.domain.UriNode; import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService; import com.netgrif.application.engine.petrinet.web.responsebodies.PetriNetReference; +import com.netgrif.application.engine.startup.SystemUserRunner; 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.bson.types.ObjectId; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; 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.data.elasticsearch.core.query.*; import org.springframework.stereotype.Service; import java.util.*; import java.util.function.BinaryOperator; import java.util.stream.Collectors; +import static java.util.Map.entry; import static org.elasticsearch.index.query.QueryBuilders.*; @Service @@ -43,17 +46,15 @@ public class ElasticCaseService extends ElasticViewPermissionService implements private static final Logger log = LoggerFactory.getLogger(ElasticCaseService.class); - private ElasticCaseRepository repository; - private IWorkflowService workflowService; - @Value("${spring.data.elasticsearch.index.case}") - private String caseIndex; + private Executor executors; @Autowired private ElasticsearchRestTemplate template; - private Executor executors; + @Autowired + private IElasticIndexService indexService; @Autowired private IPetriNetService petriNetService; @@ -62,8 +63,13 @@ public class ElasticCaseService extends ElasticViewPermissionService implements private IElasticCasePrioritySearch iElasticCasePrioritySearch; @Autowired - public ElasticCaseService(ElasticCaseRepository repository, ElasticsearchRestTemplate template, Executor executors) { - this.repository = repository; + private SystemUserRunner systemUserRunner; + +// @Autowired +// private IImpersonationElasticFilterService impersonationElasticFilterService; + + @Autowired + public ElasticCaseService(ElasticsearchRestTemplate template, Executor executors) { this.template = template; this.executors = executors; } @@ -76,61 +82,92 @@ public void setWorkflowService(IWorkflowService workflowService) { @Override public void remove(String caseId) { + log.warn("calling remove(String caseId): Use remove(String caseId, String uriNodeId) instead"); executors.execute(caseId, () -> { - repository.deleteAllByStringId(caseId); + template.delete(getQueryForProperty("stringId", caseId), + ElasticCase.class, + IndexCoordinates.of(indexService.getAllDynamicIndexes().toArray(new String[0]))); log.info("[" + caseId + "]: Case \"" + caseId + "\" deleted"); }); } + @Override + public void remove(String caseId, String uriNodeId) { + executors.execute(caseId, () -> { + template.delete(caseId, IndexCoordinates.of(getIndex(uriNodeId))); + log.info("[" + caseId + "][" + uriNodeId + "]: 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"); + PetriNet net = petriNetService.get(new ObjectId(processId)); + template.delete(getQueryForProperty("processId", processId), ElasticCase.class, IndexCoordinates.of(getIndex(net.getUriNodeId()))); + log.info("[" + processId + "][" + net.getUriNodeId() + "]: All cases of Petri Net with id \"" + processId + "\" deleted"); }); } + @Override + public void removeByPetriNetId(String processId, String uriNodeId) { + executors.execute(processId, () -> { + template.delete(getQueryForProperty("processId", processId), ElasticCase.class, IndexCoordinates.of(getIndex(uriNodeId))); + log.info("[" + processId + "][" + uriNodeId + "]: All cases of Petri Net with id \"" + processId + "\" deleted"); + }); + } + + @Override + public void indexNow(ElasticCase useCase) { + index(useCase); + } + @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); + // stringId might not be indexed fast enough to prevent duplicity, + // we need to be able to search based on a case property that is indexed immediately: id +// useCase.setId(useCase.getStringId()); + String index = getIndex(useCase.getUriNodeId()); + IndexCoordinates allIndexes = IndexCoordinates.of(getAllIndexes().toArray(new String[0])); + log.debug("[" + useCase.getStringId() + "] Indexing case in " + index); + + List existing = findAllByStringIdOrId(useCase.getStringId(), useCase.getStringId(), allIndexes); + if (existing.size() == 1) { + ElasticCase oneExisting = existing.get(0); + String oneExistingIndex = getIndex(oneExisting.getUriNodeId()); + oneExisting.update(useCase); + if (!oneExistingIndex.equals(index)) { + template.delete(oneExisting.getId(), IndexCoordinates.of(oneExistingIndex)); } - 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"); + } else if (existing.size() > 1) { + // delete by id does not support multiple indexes in IndexCoordinates + existing.forEach(esCase -> template.delete(esCase.getId(), IndexCoordinates.of(getIndex(esCase.getUriNodeId())))); } + doIndex(useCase, index); + log.debug("[" + useCase.getStringId() + "] Indexed case in " + index); }); } - @Override - public void indexNow(ElasticCase useCase) { - index(useCase); + protected void doIndex(ElasticCase useCase, String index) { + IndexQuery indexQuery = new IndexQueryBuilder() +// .withId(useCase.getId()) //TODO: ON ? + .withObject(useCase) + .build(); + template.index(indexQuery, IndexCoordinates.of(index)); } + @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!"); - } + IndexCoordinates indexCoordinates = validateRequestAndExtractIndexCoords(requests); - LoggedUser loggedOrImpersonated = user.getSelfOrImpersonated(); - pageable = resolveUnmappedSortAttributes(pageable); - NativeSearchQuery query = buildQuery(requests, loggedOrImpersonated, pageable, locale, isIntersection); + NativeSearchQuery query = buildQuery(requests, user, pageable, locale, isIntersection); List casePage; long total; if (query != null) { - SearchHits hits = template.search(query, ElasticCase.class, IndexCoordinates.of(caseIndex)); + SearchHits hits = template.search(query, ElasticCase.class, indexCoordinates); Page indexedCases = (Page) SearchHitSupport.unwrapSearchHits(SearchHitSupport.searchPageFor(hits, query.getPageable())); - casePage = workflowService.findAllById(indexedCases.get().map(ElasticCase::getStringId).collect(Collectors.toList())); + casePage = workflowService.findAllById(indexedCases.get().map(ElasticCase::getStringId).distinct().collect(Collectors.toList())); total = indexedCases.getTotalElements(); } else { casePage = Collections.emptyList(); @@ -140,33 +177,71 @@ public Page search(List requests, LoggedUser user, Page return new PageImpl<>(casePage, pageable, total); } + @Override + public List findAllByStringIdOrId(String stringId, String elasticId, IndexCoordinates indexCoordinates) { + NativeSearchQuery query = getQueryForProperties(Map.ofEntries( + entry("stringId", stringId), + entry("_id", elasticId) + ), 0, 100, false); + SearchHits hits = template.search(query, ElasticCase.class, indexCoordinates); + Page indexedCases = (Page) SearchHitSupport.unwrapSearchHits(SearchHitSupport.searchPageFor(hits, query.getPageable())); + return indexedCases.getContent(); + } + + @Override + public long countByLastModified(Case useCase, long timestamp) { + IndexCoordinates index = IndexCoordinates.of(getIndex(useCase.getUriNodeId())); + NativeSearchQuery query = getQueryForProperties(Map.ofEntries( + entry("stringId", useCase.getStringId()), + entry("lastModified", timestamp) + ), 0, 100, true); + return template.count(query, ElasticCase.class, index); + } + + protected NativeSearchQuery getQueryForProperty(String property, String value) { + return getQueryForProperty(property, value, 0, 100); + } + + protected NativeSearchQuery getQueryForProperty(String property, String value, int page, int size) { + NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); + BoolQueryBuilder caseIdQuery = boolQuery(); + caseIdQuery.must(termQuery(property, value)); + return builder + .withQuery(caseIdQuery) + .withPageable(PageRequest.of(page, size)) + .build(); + } + + protected NativeSearchQuery getQueryForProperties(Map props, int page, int size, boolean intersection) { + NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); + List queries = props.entrySet().stream().map(entry -> { + BoolQueryBuilder query = boolQuery(); + query.must(termQuery(entry.getKey(), entry.getValue())); + return query; + }).collect(Collectors.toList()); + BinaryOperator reductionOperator = intersection ? BoolQueryBuilder::must : BoolQueryBuilder::should; + return builder + .withQuery(queries.stream().reduce(new BoolQueryBuilder(), reductionOperator)) + .withPageable(PageRequest.of(page, size)) + .build(); + } + @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); + IndexCoordinates indexCoordinates = validateRequestAndExtractIndexCoords(requests); + + NativeSearchQuery query = buildQuery(requests, user, new FullPageRequest(), locale, isIntersection); if (query != null) { - return template.count(query, ElasticCase.class); + return template.count(query, ElasticCase.class, indexCoordinates); } 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(); - } private 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()); @@ -192,21 +267,132 @@ private NativeSearchQuery buildQuery(List requests, LoggedUse .build(); } + + @Override + public void moveElasticIndex(List requests, String fromIndex, String toIndex) { + BoolQueryBuilder combinedQuery = QueryBuilders.boolQuery(); + for (CaseSearchRequest request : requests) { + BoolQueryBuilder queryForSingleRequest = QueryBuilders.boolQuery(); + buildPetriNetQuery(request, null, queryForSingleRequest); + combinedQuery.should(queryForSingleRequest); + } + + final int pageSize = 100; + long totalHits; + do { + Pageable pageable = PageRequest.of(0, pageSize); + + Query searchQuery = new NativeSearchQueryBuilder() + .withQuery(combinedQuery) + .withPageable(pageable) + .build(); + + SearchHits searchHits = template.search(searchQuery, ElasticCase.class, IndexCoordinates.of(fromIndex)); + totalHits = searchHits.getTotalHits(); + + List indexQueries = new ArrayList<>(); + List documentIdsToBeDeleted = new ArrayList<>(); + + searchHits.forEach(hit -> { + IndexQuery indexQuery = new IndexQueryBuilder() + .withId(hit.getId()) + .withObject(hit.getContent()) + .build(); + indexQueries.add(indexQuery); + documentIdsToBeDeleted.add(hit.getId()); + }); + + if (!indexQueries.isEmpty()) { + template.bulkIndex(indexQueries, IndexCoordinates.of(toIndex)); + documentIdsToBeDeleted.forEach(id -> template.delete(id, IndexCoordinates.of(fromIndex))); + log.debug("Moved " + indexQueries.size() + " documents from index '" + fromIndex + "' to '" + toIndex + "'."); + } + template.indexOps(IndexCoordinates.of(fromIndex)).refresh(); + template.indexOps(IndexCoordinates.of(toIndex)).refresh(); + + } while (totalHits != 0); + } + + @Override + public void moveElasticIndex(UriNode fromIndex, UriNode toIndex) { + String from = indexService.getIndex(fromIndex); + String to = indexService.getIndex(toIndex); + if (from.isEmpty() || to.isEmpty()) { + return; + } + moveElasticIndex(from, to); + } + + @Override + public void moveElasticIndex(String fromUriNodeId, String toUriNodeId) { + final int pageSize = 100; + long totalHits; + + String fromIndexPath = indexService.getIndex(fromUriNodeId); + String toIndexPath = indexService.getIndex(toUriNodeId); + + if (fromIndexPath.equalsIgnoreCase(toIndexPath)) { + return; + } + + do { + Pageable pageable = PageRequest.of(0, pageSize); + + Query searchQuery = new NativeSearchQueryBuilder() + .withQuery(QueryBuilders.matchAllQuery()) + .withPageable(pageable) + .build(); + + SearchHits searchHits = template.search(searchQuery, ElasticCase.class, IndexCoordinates.of(fromIndexPath)); + totalHits = searchHits.getTotalHits(); + + List indexQueries = searchHits.getSearchHits().stream().map(hit -> + new IndexQueryBuilder() + .withId(hit.getId()) + .withObject(hit.getContent()) + .build() + ).collect(Collectors.toList()); + + if (!indexQueries.isEmpty()) { + template.bulkIndex(indexQueries, IndexCoordinates.of(toIndexPath)); + searchHits.forEach(hit -> template.delete(hit.getId(), IndexCoordinates.of(fromIndexPath))); + } + + template.indexOps(IndexCoordinates.of(fromIndexPath)).refresh(); + template.indexOps(IndexCoordinates.of(toIndexPath)).refresh(); + + } while (totalHits > 0); + + long remainingDocs = template.count(new NativeSearchQueryBuilder().withQuery(QueryBuilders.matchAllQuery()).build(), ElasticCase.class, IndexCoordinates.of(fromIndexPath)); + if (remainingDocs == 0) { + template.indexOps(IndexCoordinates.of(fromIndexPath)).delete(); + log.info("Index '" + fromIndexPath + "' was empty after moving documents and has been deleted."); + } else { + log.warn("Index '" + fromIndexPath + "' is not empty after moving documents. Remaining documents: " + remainingDocs); + } + } + + private BoolQueryBuilder buildSingleQuery(CaseSearchRequest request, LoggedUser user, Locale locale) { BoolQueryBuilder query = boolQuery(); - - buildViewPermissionQuery(query, user); - buildPetriNetQuery(request, user, query); +// if (user.isImpersonating()) { +// addImpersonationAllowedProcessesConstraint(query, user); +// } + LoggedUser loggedOrImpersonated = user.getSelfOrImpersonated(); + if (!loggedOrImpersonated.getId().equals(systemUserRunner.getLoggedSystem().getId())) { + buildViewPermissionQuery(query, loggedOrImpersonated); + } + buildPetriNetQuery(request, loggedOrImpersonated, query); buildAuthorQuery(request, query); buildTaskQuery(request, query); buildRoleQuery(request, query); buildDataQuery(request, query); buildFullTextQuery(request, query); - buildStringQuery(request, query, user); + buildStringQuery(request, query, loggedOrImpersonated); buildCaseIdQuery(request, query); buildUriNodeIdQuery(request, query); buildTagsQuery(request, query); - boolean resultAlwaysEmpty = buildGroupQuery(request, user, locale, query); + boolean resultAlwaysEmpty = buildGroupQuery(request, loggedOrImpersonated, locale, query); // TODO: filtered query https://stackoverflow.com/questions/28116404/filtered-query-using-nativesearchquerybuilder-in-spring-data-elasticsearch @@ -216,6 +402,10 @@ private BoolQueryBuilder buildSingleQuery(CaseSearchRequest request, LoggedUser return query; } +// private void addImpersonationAllowedProcessesConstraint(BoolQueryBuilder query, LoggedUser user) { +// impersonationElasticFilterService.addImpersonationAllowedProcessesConstraint(query, user); +// } + private void buildPetriNetQuery(CaseSearchRequest request, LoggedUser user, BoolQueryBuilder query) { if (request.process == null || request.process.isEmpty()) { return; @@ -480,9 +670,52 @@ private boolean buildGroupQuery(CaseSearchRequest request, LoggedUser user, Loca return false; } - private Pageable resolveUnmappedSortAttributes(Pageable pageable) { + private Pageable resolveUnmappedSortAttributes(Pageable pageable) { //TODO: unuse ?? delete? 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)); } + + protected String getIndex(String uriNodeId) { + return indexService.getIndex(uriNodeId); + } + + protected List getAllIndexes() { + return indexService.getAllIndexes(); + } + + protected List getIndexes(IndexAwareElasticSearchRequest request) { + List result = new ArrayList<>(); + if (request.getIndexNames() != null && !request.getIndexNames().isEmpty()) { + result.addAll(request.getIndexNames()); + } + if (request.doQueryAll()) { + result.addAll(indexService.getAllIndexes()); + } + if (request.getMenuItemIds() != null) { + result.addAll(request.getMenuItemIds().stream().map(indexService::getIndexByMenuItemId).collect(Collectors.toList())); + } + return result; + } + + /** + * validates request for nullability, collects requested indexes into IndexCoordinates + * + * @param requests List | IndexAwareSearchRequest + * @return indexCoordinates + */ + protected IndexCoordinates validateRequestAndExtractIndexCoords(List requests) { + if (requests == null) { + throw new IllegalArgumentException("Request can not be null!"); + } + List indexes; + if (requests instanceof IndexAwareElasticSearchRequest) { + indexes = getIndexes((IndexAwareElasticSearchRequest) requests); + } else { + indexes = getAllIndexes(); + } + return IndexCoordinates.of(indexes.toArray(new String[0])); + } + + } \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java index 0b0b2f2bce9..4499ebd2b18 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticIndexService.java @@ -1,8 +1,15 @@ package com.netgrif.application.engine.elastic.service; +import com.netgrif.application.engine.configuration.properties.UriProperties; +import com.netgrif.application.engine.elastic.domain.ElasticCase; import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; -import lombok.extern.slf4j.Slf4j; +import com.netgrif.application.engine.petrinet.domain.UriNode; +import com.netgrif.application.engine.petrinet.service.interfaces.IUriService; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.domain.Task; +import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import org.elasticsearch.action.admin.indices.open.OpenIndexRequest; import org.elasticsearch.action.admin.indices.open.OpenIndexResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; @@ -12,6 +19,9 @@ import org.elasticsearch.client.indices.PutIndexTemplateRequest; import org.elasticsearch.xcontent.XContentType; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ApplicationContext; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Setting; @@ -26,25 +36,135 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.stereotype.Service; import org.springframework.util.Assert; +import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; @Slf4j @Service public class ElasticIndexService implements IElasticIndexService { private static final String PLACEHOLDERS = "petriNetIndex, caseIndex, taskIndex"; + + private static final int FIRST_LEVEL = 0; + + //index.mapping.total_fields.limit + @Value("${spring.data.elasticsearch.index.total-fields:1000}") + private int defaultTotalFields; + + @Value("${spring.data.elasticsearch.index.case}") + private String caseIndex; + @Autowired private ApplicationContext context; @Autowired - private ElasticsearchRestTemplate elasticsearchTemplate; + private ElasticsearchOperations operations; @Autowired - private ElasticsearchOperations operations; + private IUriService uriService; + + @Autowired + private CacheManager cacheManager; + + @Autowired + private UriProperties uriProperties; + + @Autowired + private ITaskService taskService; + + @Autowired + private IWorkflowService workflowService; + + protected ElasticsearchRestTemplate elasticsearchTemplate; + + public ElasticIndexService(ElasticsearchRestTemplate template) { + this.elasticsearchTemplate = template; + } + + @Override + @Cacheable("caseIndexByNodeId") + public String getIndex(String uriNodeId) { + return getIndex(uriService.findById(uriNodeId)); + } + + @Override + @Cacheable("caseIndexDynamic") + public List getAllDynamicIndexes() { + List root = uriService.findByLevel(FIRST_LEVEL); + if (root.isEmpty()) { + return Collections.emptyList(); + } + List indexNodes = uriService.findAllByParent(root.get(0).getStringId()); + return indexNodes.stream().map(this::makeName).collect(Collectors.toList()); + } + + @Override + @Cacheable("caseIndexAll") + public List getAllIndexes() { + List indexes = new ArrayList<>(getAllDynamicIndexes()); + indexes.add(getDefaultIndex()); + return Collections.unmodifiableList(indexes); + } + + @Override + public void evictAllCaches() { + Objects.requireNonNull(cacheManager.getCache("caseIndexByNodeId")).clear(); + Objects.requireNonNull(cacheManager.getCache("caseIndexByMenuTaskId")).clear(); + Objects.requireNonNull(cacheManager.getCache("caseIndexDynamic")).clear(); + Objects.requireNonNull(cacheManager.getCache("caseIndexAll")).clear(); + } + + @Override + public void evictCache(String uriNodeId) { + Objects.requireNonNull(cacheManager.getCache("caseIndexByNodeId")).evict(uriNodeId); + Objects.requireNonNull(cacheManager.getCache("caseIndexDynamic")).clear(); + Objects.requireNonNull(cacheManager.getCache("caseIndexAll")).clear(); + } + + @Override + public void evictCacheForMenuItem(String menuItemId) { + Objects.requireNonNull(cacheManager.getCache("caseIndexByMenuTaskId")).evict(menuItemId); + Objects.requireNonNull(cacheManager.getCache("caseIndexDynamic")).clear(); + Objects.requireNonNull(cacheManager.getCache("caseIndexAll")).clear(); + } + + @Override + public String makeName(UriNode node) { + int separatorIndex = node.getUriPath().lastIndexOf(uriProperties.getSeparator()); + String lastUriPart = node.getUriPath(); + if (separatorIndex != -1) { + lastUriPart = lastUriPart.substring(separatorIndex + 1); + } + return makeName(lastUriPart); + } + + @Override + public String makeName(String nodeName) { + return caseIndex + "_" + nodeName.toLowerCase(); + } + + + public String getIndex(UriNode node) { + UriNode root = uriService.getRoot(); + if (node.getStringId().equals(root.getStringId())) { + return getDefaultIndex(); + } + while (!node.getParentId().equals(root.getStringId())) { + node = uriService.findById(node.getParentId()); + } + return makeName(node); + } + + @Override + @Cacheable("caseIndexByMenuTaskId") + public String getIndexByMenuItemId(String menuItemId) { + Task task = taskService.findOne(menuItemId); + Case menuCase = workflowService.findOne(task.getCaseId()); + return getIndex(menuCase.getUriNodeId()); + } @Override public boolean indexExists(String indexName) { @@ -66,13 +186,17 @@ public String index(Class clazz, T source, String... placeholders) { @Override public boolean bulkIndex(List list, Class clazz, String... placeholders) { - String indexName = getIndexName(clazz, placeholders); try { if (list != null && !list.isEmpty()) { List indexQueries = new ArrayList<>(); - list.forEach(source -> - indexQueries.add(new IndexQueryBuilder().withId(getIdFromSource(source)).withObject(source).build())); - elasticsearchTemplate.bulkIndex(indexQueries, IndexCoordinates.of(indexName)); + list.forEach(source -> { + String indexName = getIndexName(clazz, placeholders); + if (source instanceof ElasticCase) { + indexName = getIndex(((ElasticCase) source).getUriNodeId()); + } + indexQueries.add(new IndexQueryBuilder().withId(getIdFromSource(source)).withObject(source).build()); + elasticsearchTemplate.bulkIndex(indexQueries, IndexCoordinates.of(indexName)); + }); } } catch (Exception e) { log.error("bulkIndex:", e); @@ -81,16 +205,43 @@ public boolean bulkIndex(List list, Class clazz, String... placeholders) { return true; } + @Override + public void createIndex(UriNode node) { + createIndex(makeName(node)); + } + + @Override + public void createIndex(String indexName) { + if (!indexExists(indexName)) { + log.info("Creating Elasticsearch case index " + indexName); + createElasticsearchIndex(indexName, ElasticCase.class); + } + } + @Override public boolean createIndex(Class clazz, String... placeholders) { try { String indexName = getIndexName(clazz, placeholders); if (!this.indexExists(indexName)) { - // https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html - HashMap settingMap = new HashMap<>(); - settingMap.put("number_of_shards", getShardsFromClass(clazz)); - settingMap.put("number_of_replicas", getReplicasFromClass(clazz)); - settingMap.put("max_result_window", 10000000); + log.info("Creating new index - {} ", indexName); + createElasticsearchIndex(indexName, clazz); + return true; + } else { + log.info("This index {} already exists", indexName); + } + } catch (Exception e) { + log.error("createIndex:", e); + } + return false; + } + + protected void createElasticsearchIndex(String indexName, Class clazz) { + HashMap settingMap = new HashMap<>(); + settingMap.put("number_of_shards", getShardsFromClass(clazz)); + settingMap.put("number_of_replicas", getReplicasFromClass(clazz)); + settingMap.put("max_result_window", 10000000); + settingMap.put("mapping.total_fields.limit", defaultTotalFields); + // https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html // HashMap analyzer = new HashMap<>(); //TODO: Analyzer // HashMap netgrif = new HashMap<>(); // HashMap filter = new HashMap<>(); @@ -100,16 +251,11 @@ public boolean createIndex(Class clazz, String... placeholders) { // filter.put("netgrif", netgrif); // analyzer.put("analyzer", filter); // settingMap.put("analysis", analyzer); - Document settings = Document.from(settingMap); - log.info("Creating new index - {} ", indexName); - return elasticsearchTemplate.indexOps(IndexCoordinates.of(indexName)).create(settings); - } else { - log.info("This index {} already exists", indexName); - } - } catch (Exception e) { - log.error("createIndex:", e); - } - return false; + Document settings = Document.from(settingMap); + elasticsearchTemplate.indexOps(IndexCoordinates.of(indexName)).create(settings); + + Document mapping = operations.indexOps(clazz).createMapping(); + elasticsearchTemplate.indexOps(IndexCoordinates.of(indexName)).putMapping(mapping); } @Override @@ -177,6 +323,22 @@ public SearchHits search(Query query, Class clazz, String... placeholders) return null; } + @Override + public void deleteIndex(UriNode node) { + deleteIndex(makeName(node)); + } + + @Override + public void deleteIndex(String index) { + elasticsearchTemplate.indexOps(IndexCoordinates.of(index)).delete(); + } + + + @Override + public String getDefaultIndex() { + return caseIndex; + } + @Override public boolean putMapping(Class clazz, String... placeholders) { try { @@ -297,5 +459,4 @@ private String getIndexName(Class clazz, String... placeholders) { } return indexName; } - -} +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticTaskService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticTaskService.java index cbb88b52feb..db5693d36d5 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticTaskService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticTaskService.java @@ -2,10 +2,7 @@ import com.google.common.collect.ImmutableMap; import com.netgrif.application.engine.auth.domain.LoggedUser; -import com.netgrif.application.engine.elastic.domain.ElasticJob; -import com.netgrif.application.engine.elastic.domain.ElasticQueryConstants; -import com.netgrif.application.engine.elastic.domain.ElasticTask; -import com.netgrif.application.engine.elastic.domain.ElasticTaskJob; +import com.netgrif.application.engine.elastic.domain.*; import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest; import com.netgrif.application.engine.petrinet.domain.PetriNetSearch; @@ -50,13 +47,14 @@ public class ElasticTaskService extends ElasticViewPermissionService implements private static final Logger log = LoggerFactory.getLogger(ElasticTaskService.class); protected ITaskService taskService; + protected ElasticsearchRestTemplate template; @Value("${spring.data.elasticsearch.index.task}") protected String taskIndex; @Autowired - protected ElasticsearchRestTemplate elasticsearchTemplate; + private ElasticTaskQueueManager elasticTaskQueueManager; @Autowired protected IPetriNetService petriNetService; @@ -70,8 +68,6 @@ public class ElasticTaskService extends ElasticViewPermissionService implements "caseTitle", 1f ); - @Autowired - private ElasticTaskQueueManager elasticTaskQueueManager; @Autowired public ElasticTaskService(ElasticsearchRestTemplate template) { @@ -128,7 +124,7 @@ public Page search(List requests, LoggedUser use List taskPage; long total; if (query != null) { - SearchHits hits = elasticsearchTemplate.search(query, ElasticTask.class, IndexCoordinates.of(taskIndex)); + SearchHits hits = template.search(query, ElasticTask.class, IndexCoordinates.of(taskIndex)); Page indexedTasks = (Page) SearchHitSupport.unwrapSearchHits(SearchHitSupport.searchPageFor(hits, query.getPageable())); taskPage = taskService.findAllById(indexedTasks.get().map(ElasticTask::getStringId).collect(Collectors.toList())); total = indexedTasks.getTotalElements(); diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java index ae21422527c..93cd978472e 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ReindexingTask.java @@ -110,7 +110,7 @@ private void reindexPage(Predicate predicate, int page, long numOfPages, boolean Page cases = this.workflowService.search(predicate, PageRequest.of(page, pageSize)); for (Case aCase : cases) { - if (forced || elasticCaseRepository.countByStringIdAndLastModified(aCase.getStringId(), Timestamp.valueOf(aCase.getLastModified()).getTime()) == 0) { + if (forced || elasticCaseService.countByLastModified(aCase, Timestamp.valueOf(aCase.getLastModified()).getTime()) == 0) { elasticCaseService.indexNow(this.caseMappingService.transform(aCase)); List tasks = taskRepository.findAllByCaseId(aCase.getStringId()); for (Task task : tasks) { diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java index 024f945aec1..6c86095b254 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticCaseService.java @@ -3,9 +3,11 @@ import com.netgrif.application.engine.auth.domain.LoggedUser; import com.netgrif.application.engine.elastic.domain.ElasticCase; import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.petrinet.domain.UriNode; import com.netgrif.application.engine.workflow.domain.Case; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.scheduling.annotation.Async; import java.util.List; @@ -26,5 +28,17 @@ public interface IElasticCaseService { void removeByPetriNetId(String processId); - String findUriNodeId(Case aCase); + void remove(String caseId, String uriNodeId); + + void removeByPetriNetId(String processId, String uriNodeId); + + List findAllByStringIdOrId(String stringId, String elasticId, IndexCoordinates indexCoordinates); + + long countByLastModified(Case useCase, long timestamp); + + void moveElasticIndex(UriNode fromIndex, UriNode toIndex); + + void moveElasticIndex(String fromIndex, String toIndex); + + void moveElasticIndex(List requests, String fromIndex, String toIndex); } \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java index 3e928d44cc8..a04de9436cf 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/interfaces/IElasticIndexService.java @@ -1,5 +1,6 @@ package com.netgrif.application.engine.elastic.service.interfaces; +import com.netgrif.application.engine.petrinet.domain.UriNode; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchScrollHits; import org.springframework.data.elasticsearch.core.query.Query; @@ -31,4 +32,34 @@ public interface IElasticIndexService { SearchScrollHits scroll(String scrollId, Class clazz, String... placeholders); SearchHits search(Query query, Class clazz, String... placeholders); + + void createIndex(UriNode node); + + void createIndex(String index); + + void deleteIndex(UriNode node); + + void deleteIndex(String index); + + String getDefaultIndex(); + + String getIndex(String uriNodeId); + + String getIndex(UriNode node); + + String getIndexByMenuItemId(String menuItemId); + + List getAllDynamicIndexes(); + + List getAllIndexes(); + + void evictAllCaches(); + + void evictCache(String uriNodeId); + + void evictCacheForMenuItem(String menuItemId); + + String makeName(UriNode node); + + String makeName(String nodeName); } diff --git a/src/main/java/com/netgrif/application/engine/orgstructure/groups/NextGroupService.java b/src/main/java/com/netgrif/application/engine/orgstructure/groups/NextGroupService.java index bb2a0639ad5..668841fb267 100644 --- a/src/main/java/com/netgrif/application/engine/orgstructure/groups/NextGroupService.java +++ b/src/main/java/com/netgrif/application/engine/orgstructure/groups/NextGroupService.java @@ -137,10 +137,7 @@ public Case findDefaultGroup() { @Override public Case findByName(String name) { - CaseSearchRequest request = new CaseSearchRequest(); - request.query = "title.keyword:\"" + name + "\""; - List result = elasticCaseService.search(Collections.singletonList(request), userService.getSystem().transformToLoggedUser(), PageRequest.of(0, 1), LocaleContextHolder.getLocale(), false).getContent(); - return !result.isEmpty() ? result.get(0) : null; + return workflowService.searchOne(groupCase().and(QCase.case$.title.eq(name))); } @Override diff --git a/src/main/java/com/netgrif/application/engine/petrinet/service/UriService.java b/src/main/java/com/netgrif/application/engine/petrinet/service/UriService.java index 2a6a3cff879..112a787cd90 100644 --- a/src/main/java/com/netgrif/application/engine/petrinet/service/UriService.java +++ b/src/main/java/com/netgrif/application/engine/petrinet/service/UriService.java @@ -1,10 +1,14 @@ package com.netgrif.application.engine.petrinet.service; import com.netgrif.application.engine.configuration.properties.UriProperties; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; +import com.netgrif.application.engine.petrinet.domain.PetriNet; import com.netgrif.application.engine.petrinet.domain.UriContentType; import com.netgrif.application.engine.petrinet.domain.UriNode; import com.netgrif.application.engine.petrinet.domain.repository.UriNodeRepository; import com.netgrif.application.engine.petrinet.service.interfaces.IUriService; +import org.springframework.beans.factory.annotation.Autowired; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Service; @@ -24,6 +28,13 @@ public class UriService implements IUriService { private final UriProperties uriProperties; + @Autowired + private IElasticIndexService indexService; + + @Autowired + private IElasticCaseService elasticCaseService; + + public UriService(UriNodeRepository uriNodeRepository, UriProperties uriProperties) { this.uriNodeRepository = uriNodeRepository; this.uriProperties = uriProperties; @@ -171,6 +182,9 @@ public UriNode move(UriNode node, String destUri) { uriNodeRepository.saveAll(List.of(oldParent, newParent, node)); uriNodeRepository.saveAll(childrenToSave); + +// elasticCaseService.moveElasticIndex(oldNodePath, newNodePath); //TODO: totok este + indexService.evictCache(node.getStringId()); return node; } @@ -253,7 +267,16 @@ public UriNode getOrCreate(String uri, UriContentType contentType) { uriNodeList.add(uriNode); parent = uriNode; } - return uriNodeList.getLast(); + + UriNode node = uriNodeList.getLast(); + if (node.getParentId() != null) { + UriNode root = getRoot(); + if (Objects.equals(node.getParentId(), root.getStringId())) { + indexService.createIndex(node); + indexService.evictAllCaches(); + } + } + return node; } /** diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseEventHandler.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseEventHandler.java index 89cb9b2b3df..8ac6b05ba90 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseEventHandler.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseEventHandler.java @@ -2,6 +2,7 @@ import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository; import org.bson.Document; import org.bson.types.ObjectId; import org.slf4j.Logger; @@ -9,8 +10,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent; +import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent; import org.springframework.stereotype.Component; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + @Component public class CaseEventHandler extends AbstractMongoEventListener { @@ -19,6 +24,32 @@ public class CaseEventHandler extends AbstractMongoEventListener { @Autowired private IElasticCaseService service; + @Autowired + private CaseRepository repository; + + private final Map caseUriNodes; + + + public CaseEventHandler() { + this.caseUriNodes = new ConcurrentHashMap<>();; + } + + @Override + public void onBeforeDelete(BeforeDeleteEvent event) { + Document document = event.getDocument(); + if (document == null) { + log.warn("Trying to delete null document!"); + return; + } + + ObjectId objectId = document.getObjectId("_id"); + if (objectId != null) { + Case useCase = repository.findById(event.getDocument().getObjectId("_id").toString()).get(); + caseUriNodes.put(useCase.getStringId(), useCase.getUriNodeId()); + } + } + + @Override public void onAfterDelete(AfterDeleteEvent event) { Document document = event.getDocument(); @@ -29,7 +60,8 @@ public void onAfterDelete(AfterDeleteEvent event) { ObjectId objectId = document.getObjectId("_id"); if (objectId != null) { - service.remove(objectId.toString()); + service.remove(objectId.toString(), caseUriNodes.get(objectId.toString())); + caseUriNodes.remove(objectId.toString()); return; } diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java b/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java index 23bfb683f97..989dd3ddce1 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java @@ -2,6 +2,7 @@ import com.netgrif.application.engine.auth.domain.LoggedUser; import com.netgrif.application.engine.elastic.domain.ElasticCase; +import com.netgrif.application.engine.elastic.domain.IndexAwareElasticSearchRequest; import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleCaseSearchRequestAsList; import com.netgrif.application.engine.eventoutcomes.LocalisedEventOutcomeFactory; @@ -16,6 +17,7 @@ import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.netgrif.application.engine.workflow.web.requestbodies.CreateCaseBody; +import com.netgrif.application.engine.workflow.web.requestbodies.IndexAwareApiCaseSearchRequest; import com.netgrif.application.engine.workflow.web.responsebodies.*; import com.querydsl.core.types.Predicate; import io.swagger.v3.oas.annotations.Operation; @@ -116,6 +118,27 @@ public PagedModel search2(@QuerydslPredicate(root = Case.class) Pr return resources; } + @Operation(summary = "Generic case search on Elasticsearch database", security = {@SecurityRequirement(name = "BasicAuth")}) + @PostMapping(value = "/case/search_index", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaTypes.HAL_JSON_VALUE) + public PagedModel searchByIndex(@RequestBody IndexAwareApiCaseSearchRequest searchBody, @RequestParam(defaultValue = "OR") MergeFilterOperation operation, Pageable pageable, PagedResourcesAssembler assembler, Authentication auth, Locale locale) { + LoggedUser user = (LoggedUser) auth.getPrincipal(); + IndexAwareElasticSearchRequest elasticRequest; + if (searchBody.getSearchAll() || searchBody.getMenuItemIds() == null || searchBody.getMenuItemIds().isEmpty()) { + elasticRequest = IndexAwareElasticSearchRequest.all(); + } else { + elasticRequest = IndexAwareElasticSearchRequest.ofMenuItems(searchBody.getMenuItemIds()); + } + elasticRequest.addAll(searchBody.getBody()); + Page cases = elasticCaseService.search(elasticRequest, user, pageable, locale, operation == MergeFilterOperation.AND); + + Link selfLink = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(WorkflowController.class) + .searchByIndex(searchBody, operation, pageable, assembler, auth, locale)).withRel("search_index"); + + PagedModel resources = assembler.toModel(cases, new CaseResourceAssembler(), selfLink); + ResourceLinkAssembler.addLinks(resources, ElasticCase.class, selfLink.getRel().toString()); + return resources; + } + @Operation(summary = "Generic case search on Elasticsearch database", security = {@SecurityRequirement(name = "BasicAuth")}) @PostMapping(value = "/case/search", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaTypes.HAL_JSON_VALUE) public PagedModel search(@RequestBody SingleCaseSearchRequestAsList searchBody, @RequestParam(defaultValue = "OR") MergeFilterOperation operation, Pageable pageable, PagedResourcesAssembler assembler, Authentication auth, Locale locale) { diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/IndexAwareApiCaseSearchRequest.java b/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/IndexAwareApiCaseSearchRequest.java new file mode 100644 index 00000000000..824bb3a7aa2 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/web/requestbodies/IndexAwareApiCaseSearchRequest.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.workflow.web.requestbodies; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IndexAwareApiCaseSearchRequest { + + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + protected List menuItemIds; + + protected Boolean searchAll = false; + + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + protected List body; +} diff --git a/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy b/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy index db7992527bc..166a932af90 100644 --- a/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy +++ b/src/test/groovy/com/netgrif/application/engine/TestHelper.groovy @@ -3,6 +3,7 @@ package com.netgrif.application.engine import com.netgrif.application.engine.auth.domain.repositories.UserRepository import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository import com.netgrif.application.engine.elastic.domain.ElasticTaskRepository +import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService import com.netgrif.application.engine.petrinet.domain.repository.UriNodeRepository import com.netgrif.application.engine.petrinet.domain.roles.ProcessRoleRepository import com.netgrif.application.engine.petrinet.service.ProcessRoleService @@ -18,46 +19,67 @@ class TestHelper { @Autowired private SuperCreator superCreator + @Autowired private MongoTemplate template + @Autowired private UserRepository userRepository + @Autowired private ProcessRoleRepository roleRepository + @Autowired private ProcessRoleService roleService + @Autowired private SystemUserRunner systemUserRunner + @Autowired private DefaultRoleRunner defaultRoleRunner + @Autowired private AnonymousRoleRunner anonymousRoleRunner + @Autowired private ElasticTaskRepository elasticTaskRepository + @Autowired private ElasticCaseRepository elasticCaseRepository + @Autowired private UriNodeRepository uriNodeRepository + @Autowired private GroupRunner groupRunner + @Autowired private IFieldActionsCacheService actionsCacheService + @Autowired private FilterRunner filterRunner + + @Autowired + private UriRunner uriRunner + @Autowired private FinisherRunner finisherRunner + @Autowired private ImpersonationRunner impersonationRunner - @Autowired - private UriRunner uriRunner + @Autowired private IPetriNetService petriNetService + @Autowired + private IElasticIndexService elasticIndexService + void truncateDbs() { template.db.drop() elasticTaskRepository.deleteAll() elasticCaseRepository.deleteAll() uriNodeRepository.deleteAll() + elasticIndexService.evictAllCaches(); userRepository.deleteAll() roleRepository.deleteAll() roleService.clearCache() @@ -66,6 +88,7 @@ class TestHelper { actionsCacheService.clearNamespaceFunctionCache() petriNetService.evictAllCaches() + defaultRoleRunner.run() anonymousRoleRunner.run() systemUserRunner.run() diff --git a/src/test/groovy/com/netgrif/application/engine/action/FilterApiTest.groovy b/src/test/groovy/com/netgrif/application/engine/action/FilterApiTest.groovy index fca6c5d1fbe..50c8fc2e99c 100644 --- a/src/test/groovy/com/netgrif/application/engine/action/FilterApiTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/action/FilterApiTest.groovy @@ -1,8 +1,10 @@ package com.netgrif.application.engine.action - +import com.netgrif.application.engine.ReindexRetryHelper import com.netgrif.application.engine.TestHelper import com.netgrif.application.engine.auth.service.interfaces.IUserService +import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest import com.netgrif.application.engine.orgstructure.groups.interfaces.INextGroupService import com.netgrif.application.engine.petrinet.domain.UriContentType import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate @@ -11,8 +13,11 @@ import com.netgrif.application.engine.startup.FilterRunner import com.netgrif.application.engine.startup.ImportHelper import com.netgrif.application.engine.workflow.domain.Case import com.netgrif.application.engine.workflow.domain.QCase +import com.netgrif.application.engine.workflow.domain.Task import com.netgrif.application.engine.workflow.service.interfaces.IDataService +import com.netgrif.application.engine.workflow.service.interfaces.ITaskService import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService +import groovy.util.logging.Slf4j import org.bson.types.ObjectId import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.BeforeEach @@ -20,10 +25,13 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.i18n.LocaleContextHolder +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit.jupiter.SpringExtension -@Disabled +@Slf4j @SpringBootTest @ActiveProfiles(["test"]) @ExtendWith(SpringExtension.class) @@ -47,12 +55,18 @@ class FilterApiTest { @Autowired private IDataService dataService + @Autowired + protected ITaskService taskService; + @Autowired private IUriService uriService @Autowired private INextGroupService nextGroupService + @Autowired + IElasticCaseService elasticCaseService + @BeforeEach void before() { testHelper.truncateDbs() @@ -90,12 +104,12 @@ class FilterApiTest { Case caze = createMenuItem() def newUri = uriService.getOrCreate("netgrif/test_new", UriContentType.DEFAULT) caze = setData(caze, [ - "uri": newUri.uriPath, - "title": "CHANGED FILTER", - "allowed_nets": "filter", - "query": "processIdentifier:filter", - "type": "Case", - "icon": "", + "uri" : newUri.uriPath, + "title" : "CHANGED FILTER", + "allowed_nets" : "filter", + "query" : "processIdentifier:filter", + "type" : "Case", + "icon" : "", "change_filter_and_menu": "0" ]) Case item = getMenuItem(caze) @@ -126,7 +140,7 @@ class FilterApiTest { List taskIds = (defGroup.dataSet[ActionDelegate.ORG_GROUP_FIELD_FILTER_TASKS].value ?: []) as List assert !taskIds - Thread.sleep(10000); + Thread.sleep(10000) assert workflowService.searchOne(QCase.case$._id.eq(new ObjectId(item.stringId))) == null assert workflowService.searchOne(QCase.case$._id.eq(new ObjectId(filter.stringId))) == null @@ -134,11 +148,37 @@ class FilterApiTest { @Test - @Disabled("Fix") + @Disabled void testFindFilter() { Case caze = createMenuItem() Case filter = getFilter(caze) + ReindexRetryHelper> caseSearchHelper = new ReindexRetryHelper<>(); + + def caseReq = new CaseSearchRequest() + caseReq.stringId = [filter.getStringId()] + Page cases = caseSearchHelper.execute( + () -> elasticCaseService.search([caseReq], userService.getLoggedOrSystem().transformToLoggedUser(), PageRequest.of(0, 1), LocaleContextHolder.locale, false), + resultList -> resultList.size() != 0 + ) + + assert cases.size() != 0 + + + CaseSearchRequest request = new CaseSearchRequest() + request.query = "processIdentifier:$FilterRunner.FILTER_PETRI_NET_IDENTIFIER AND title.keyword:\"FILTER\"" + cases = ReindexRetryHelper.execute( + () -> elasticCaseService.search([request], + userService.system.transformToLoggedUser(), + PageRequest.of(0, 10), + LocaleContextHolder.locale, + false + ), + resultList -> resultList.size() != 0 + ) + log.warn(cases.getContent().first().stringId) + log.warn(cases.getContent().first().title) + caze = setData(caze, [ "find_filter": "0" ]) @@ -149,14 +189,14 @@ class FilterApiTest { Case createMenuItem() { Case caze = getCase() caze = setData(caze, [ - "uri": "netgrif/test", - "title": "FILTER", - "allowed_nets": "filter,preference_filter_item", - "query": "processIdentifier:filter OR processIdentifier:preference_filter_item", - "type": "Case", - "group": null, - "identifier": "new_menu_item", - "icon": "device_hub", + "uri" : "netgrif/test", + "title" : "FILTER", + "allowed_nets" : "filter,preference_filter_item", + "query" : "processIdentifier:filter OR processIdentifier:preference_filter_item", + "type" : "Case", + "group" : null, + "identifier" : "new_menu_item", + "icon" : "device_hub", "create_filter_and_menu": "0" ]) return caze @@ -175,7 +215,18 @@ class FilterApiTest { } def setData(Case caze, Map dataSet) { - dataService.setData(caze.tasks[0].task, ImportHelper.populateDataset(dataSet.collectEntries { + ReindexRetryHelper caseHelper = new ReindexRetryHelper<>() + ReindexRetryHelper taskHelper = new ReindexRetryHelper<>() + caze = caseHelper.execute( + () -> workflowService.findOne(caze.stringId), + task -> caze.tasks[0].task != null + ) + String taskId = caze.tasks[0].task + Task task = taskHelper.execute( + () -> taskService.findOne(taskId), + task -> task != null + ) + dataService.setData(task, ImportHelper.populateDataset(dataSet.collectEntries { [(it.key): (["value": it.value, "type": "text"])] })) return workflowService.findOne(caze.stringId) diff --git a/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy b/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy index 3b5268c822f..dab7baa4356 100644 --- a/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/elastic/DataSearchRequestTest.groovy @@ -1,11 +1,10 @@ package com.netgrif.application.engine.elastic import com.netgrif.application.engine.MockService +import com.netgrif.application.engine.ReindexRetryHelper import com.netgrif.application.engine.TestHelper import com.netgrif.application.engine.auth.service.interfaces.IUserService -import com.netgrif.application.engine.elastic.domain.ElasticCase import com.netgrif.application.engine.elastic.domain.ElasticCaseRepository -import com.netgrif.application.engine.elastic.domain.ElasticTask import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest @@ -41,6 +40,8 @@ import java.sql.Timestamp import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime +import java.util.function.Predicate +import java.util.function.Supplier @SpringBootTest() @ActiveProfiles(["test"]) @@ -96,15 +97,6 @@ class DataSearchRequestTest { @BeforeEach void before() { testHelper.truncateDbs() -// template.deleteIndex(ElasticCase.class) - template.createIndex(ElasticCase.class) - template.putMapping(ElasticCase.class) - -// template.deleteIndex(ElasticTask.class) - template.createIndex(ElasticTask.class) - template.putMapping(ElasticTask.class) - - repository.deleteAll() def net = petriNetService.importPetriNet(new FileInputStream("src/test/resources/all_data.xml"), VersionType.MAJOR, superCreator.getLoggedSuper()) assert net.getNet() != null @@ -226,15 +218,19 @@ class DataSearchRequestTest { @Test void testDatSearchRequests() { + ReindexRetryHelper> helper = new ReindexRetryHelper<>() + testCases.each { testCase -> - CaseSearchRequest request = new CaseSearchRequest() - request.data = new HashMap<>() - request.data.put(testCase.getKey(), testCase.getValue()) + CaseSearchRequest request = new CaseSearchRequest(); + request.data = new HashMap<>(); + request.data.put(testCase.getKey(), testCase.getValue()); + + log.info(String.format("Testing %s == %s", testCase.getKey(), testCase.getValue())); - log.info(String.format("Testing %s == %s", testCase.getKey(), testCase.getValue())) + Supplier> searchOperation = () -> searchService.search([request] as List, mockService.mockLoggedUser(), PageRequest.of(0, 100), null, false) + Predicate> resultTest = result -> result.size() == 1 - Page result = searchService.search([request] as List, mockService.mockLoggedUser(), PageRequest.of(0, 100), null, false) - assert result + Page result = helper.execute(searchOperation, resultTest) assert result.size() == 1 } } diff --git a/src/test/groovy/com/netgrif/application/engine/export/service/ExportServiceTest.groovy b/src/test/groovy/com/netgrif/application/engine/export/service/ExportServiceTest.groovy index 31d5875ed97..1373fd51c0f 100644 --- a/src/test/groovy/com/netgrif/application/engine/export/service/ExportServiceTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/export/service/ExportServiceTest.groovy @@ -1,7 +1,9 @@ package com.netgrif.application.engine.export.service +import com.netgrif.application.engine.ReindexRetryHelper import com.netgrif.application.engine.TestHelper import com.netgrif.application.engine.auth.service.interfaces.IUserService +import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest import com.netgrif.application.engine.export.service.interfaces.IExportService import com.netgrif.application.engine.petrinet.domain.PetriNet @@ -16,12 +18,14 @@ import com.netgrif.application.engine.workflow.domain.repositories.TaskRepositor import com.netgrif.application.engine.workflow.service.interfaces.ITaskService import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.i18n.LocaleContextHolder +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -58,6 +62,9 @@ class ExportServiceTest { @Autowired private IExportService exportService + @Autowired + private IElasticTaskService elasticTaskService; + PetriNet testNet Case mainCase @@ -89,17 +96,35 @@ class ExportServiceTest { @Test @Order(3) - void testCaseElasticExport() { - Thread.sleep(5000) //Elastic wait - String exportTask = mainCase.tasks.find { it.transition == "t2" }.task + void testCaseElasticExport() throws InterruptedException { + ReindexRetryHelper helperForExportTask = new ReindexRetryHelper<>(); + ReindexRetryHelper helperForTask = new ReindexRetryHelper<>(); + + String exportTask = helperForExportTask.execute( + () -> mainCase.tasks.find { it.transition == "t2" }.task, + result -> result != null + ); + assert exportTask != null + + Task task = helperForTask.execute( + () -> taskService.findOne(exportTask), + result -> result != null + ); + assert task != null + + sleep(9000) + taskService.assignTask(userService.findByEmail("super@netgrif.com", false).transformToLoggedUser(), exportTask) + File csvFile = new File("src/test/resources/csv/case_elastic_export.csv") assert csvFile.readLines().size() == 11 + String[] headerSplit = csvFile.readLines()[0].split(",") assert (headerSplit.contains("text") && !headerSplit.contains("immediate_multichoice") - && !headerSplit.contains("immediate_number")) - taskService.cancelTask(userService.getLoggedOrSystem().transformToLoggedUser(), exportTask) + && !headerSplit.contains("immediate_number")); + + taskService.cancelTask(userService.getLoggedOrSystem().transformToLoggedUser(), exportTask); } @Test @@ -119,18 +144,35 @@ class ExportServiceTest { @Test @Order(1) - @Disabled("Github action") void testTaskElasticExport() { - Thread.sleep(10000) //Elastic wait - String exportTask = mainCase.tasks.find { it.transition == "t4" }.task + ReindexRetryHelper helperForExportTask = new ReindexRetryHelper<>() + ReindexRetryHelper helperForTask = new ReindexRetryHelper<>() + ReindexRetryHelper export = new ReindexRetryHelper<>() + + String exportTask = helperForExportTask.execute( + () -> mainCase.tasks.find { it.transition == "t4" }.task, + result -> result != null + ); + assert exportTask != null + + Task task = helperForTask.execute( + () -> taskService.findOne(exportTask), + result -> result != null + ); + assert task != null taskService.assignTask(userService.findByEmail("super@netgrif.com", false).transformToLoggedUser(), exportTask) - Thread.sleep(20000) //Elastic wait def processId = petriNetService.getNewestVersionByIdentifier("export_test").stringId def taskRequest = new ElasticTaskSearchRequest() taskRequest.process = [new com.netgrif.application.engine.workflow.web.requestbodies.taskSearch.PetriNet(processId)] as List taskRequest.transitionId = ["t4"] as List - actionDelegate.exportTasksToFile([taskRequest],"src/test/resources/csv/task_elastic_export.csv",null, userService.findByEmail("super@netgrif.com", false).transformToLoggedUser()) + + export.execute( + () -> elasticTaskService.count([taskRequest], userService.findByEmail("super@netgrif.com", false).transformToLoggedUser(), LocaleContextHolder.getLocale(), false), + result -> result == 10 + ) + + actionDelegate.exportTasksToFile([taskRequest], "src/test/resources/csv/task_elastic_export.csv", null, userService.findByEmail("super@netgrif.com", false).transformToLoggedUser()) File csvFile = new File("src/test/resources/csv/task_elastic_export.csv") int pocet = ((taskRepository.count(QTask.task.processId.eq(processId).and(QTask.task.transitionId.eq("t4"))) as int) + 1) assert csvFile.readLines().size() == pocet @@ -143,7 +185,7 @@ class ExportServiceTest { } @Test - void buildDefaultCsvTaskHeaderTest(){ + void buildDefaultCsvTaskHeaderTest() { def processId = petriNetService.getNewestVersionByIdentifier("export_test").stringId String exportTask = mainCase.tasks.find { it.transition == "t4" }.task taskService.assignTask(userService.findByEmail("super@netgrif.com", false).transformToLoggedUser(), exportTask) diff --git a/src/test/groovy/com/netgrif/application/engine/impersonation/ImpersonationServiceTest.groovy b/src/test/groovy/com/netgrif/application/engine/impersonation/ImpersonationServiceTest.groovy index aac173f5004..ef82dfca546 100644 --- a/src/test/groovy/com/netgrif/application/engine/impersonation/ImpersonationServiceTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/impersonation/ImpersonationServiceTest.groovy @@ -1,5 +1,6 @@ package com.netgrif.application.engine.impersonation +import com.netgrif.application.engine.ReindexRetryHelper import com.netgrif.application.engine.TestHelper import com.netgrif.application.engine.auth.domain.Authority import com.netgrif.application.engine.auth.domain.IUser @@ -9,6 +10,7 @@ import com.netgrif.application.engine.auth.service.interfaces.IAuthorityService import com.netgrif.application.engine.auth.service.interfaces.IUserService import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest +import com.netgrif.application.engine.impersonation.service.ImpersonationAuthorizationService import com.netgrif.application.engine.impersonation.service.interfaces.IImpersonationAuthorizationService import com.netgrif.application.engine.impersonation.service.interfaces.IImpersonationService import com.netgrif.application.engine.petrinet.domain.I18nString @@ -30,11 +32,13 @@ import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchReque import com.netgrif.application.engine.workflow.web.requestbodies.taskSearch.TaskSearchCaseRequest import groovy.json.JsonSlurper import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.context.i18n.LocaleContextHolder +import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.http.MediaType import org.springframework.mock.web.MockHttpServletRequest @@ -231,17 +235,34 @@ class ImpersonationServiceTest { } @Test + @Disabled("NOPE") void testAuthorization() { - def config = setup() - sleep(4000) // elastic + ReindexRetryHelper> caseSearchHelper = new ReindexRetryHelper<>(); + + Case config = setup() + def caseReq = new CaseSearchRequest() + caseReq.stringId = [config.getStringId()] + Page cases = caseSearchHelper.execute( + () -> elasticCaseService.search([caseReq], userService.loggedUser.transformToLoggedUser(), PageRequest.of(0, 1), LocaleContextHolder.locale, false), + resultList -> resultList.size() != 0 + ) + + assert cases.size() != 0 def logged = userService.loggedUser.transformToLoggedUser() assert impersonationAuthorizationService.canImpersonate(logged, config.stringId) assert impersonationAuthorizationService.canImpersonateUser(logged, user2.stringId) - config.dataSet["valid_to"].value = LocalDateTime.now().minusMinutes(1) + def date = LocalDateTime.now().minusMinutes(1) + config.dataSet["valid_to"].value = date workflowService.save(config) - sleep(4000) + + Page cases2 = caseSearchHelper.execute( + () -> elasticCaseService.search([caseReq], userService.loggedUser.transformToLoggedUser(), PageRequest.of(0, 1), LocaleContextHolder.locale, false), + resultList -> resultList.getContent().first().dataSet["valid_to"].value == date + ) + + assert cases2.size() != 0 assert !impersonationAuthorizationService.canImpersonate(logged, config.stringId) assert !impersonationAuthorizationService.canImpersonateUser(logged, user2.stringId) @@ -295,8 +316,8 @@ class ImpersonationServiceTest { assert json["impersonated"] == null } - def setup(List roles = null, List auths = null) { - def config = createConfigCase(user2, user1.stringId, roles, auths) + Case setup(List roles = null, List auths = null) { + Case config = createConfigCase(user2, user1.stringId, roles, auths) SecurityContextHolder.getContext().setAuthentication(auth1) return config } diff --git a/src/test/groovy/com/netgrif/application/engine/petrinet/service/UriServiceTest.groovy b/src/test/groovy/com/netgrif/application/engine/petrinet/service/UriServiceTest.groovy index d6c57f5f550..f0a52d23650 100644 --- a/src/test/groovy/com/netgrif/application/engine/petrinet/service/UriServiceTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/petrinet/service/UriServiceTest.groovy @@ -100,7 +100,7 @@ class UriServiceTest { uriNode = uriService.populateDirectRelatives(uriNode) assert uriNode.parent != null && uriNode.parent.stringId == uriNode.parentId - assert uriNode.children.size() == 1 && uriNode.children.find {it.stringId == uriNode.childrenId[0]} != null + assert uriNode.children.size() == 1 && uriNode.children.find { it.stringId == uriNode.childrenId[0] } != null } @Test @@ -135,7 +135,7 @@ class UriServiceTest { private prepareDatabase(List listOfUriPaths) { uriNodeRepository.deleteAll() - listOfUriPaths.each {path -> + listOfUriPaths.each { path -> uriService.getOrCreate(path, UriContentType.DEFAULT) } } @@ -161,5 +161,4 @@ class UriServiceTest { assert uriNode != null && uriNode.level == 0 } - } diff --git a/src/test/java/com/netgrif/application/engine/ReindexRetryHelper.java b/src/test/java/com/netgrif/application/engine/ReindexRetryHelper.java new file mode 100644 index 00000000000..678955c1071 --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/ReindexRetryHelper.java @@ -0,0 +1,53 @@ +package com.netgrif.application.engine; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; + +import java.util.function.Predicate; +import java.util.function.Supplier; + +@Slf4j +@Profile("test") +public class ReindexRetryHelper { + + private static final long DEFAULT_MAX_ATTEMPTS = 10; + private static final long DEFAULT_INITIAL_WAIT_MS = 5000; + private static final long DEFAULT_MAX_LIMIT_WAIT_MS = 120000; + + public ReindexRetryHelper() { + } + + public static T execute(Supplier op, Predicate test) throws InterruptedException { + return execute(op, test, DEFAULT_MAX_ATTEMPTS, DEFAULT_INITIAL_WAIT_MS, DEFAULT_MAX_LIMIT_WAIT_MS, 2); + } + + public static T execute(Supplier op, Predicate test, Long waitTime, Long attempts) throws InterruptedException { + return execute(op, test, attempts, waitTime, DEFAULT_MAX_LIMIT_WAIT_MS, 2); + } + + // exponentialWait(op,max,timeout,).until + + public static T execute(Supplier operation, Predicate resultTest, Long attempts, Long waitTimeMs, Long maxWaitTime, Integer waitTimeExponent) throws InterruptedException { + log.debug("Starting operation with max attempts: {}", attempts); + int attempt = 0; + long waitTime = waitTimeMs; + while (attempt < attempts) { + T result = operation.get(); + if (resultTest.test(result)) { + log.debug("Operation successful on attempt number: {}", attempt + 1); + return result; + } + if (attempt < attempts - 1) { + log.debug("Operation failed on attempt number {}. Retrying in {} ms", attempt + 1, waitTime); + Thread.sleep(waitTime); + waitTime *= waitTimeExponent; + if (waitTime > maxWaitTime) { + waitTime = maxWaitTime; + } + } + attempt++; + } + log.error("Failed to get expected result after {} attempts.", attempts); + throw new AssertionError(String.format("Failed to get expected result after %d attempts.", attempts)); + } +} diff --git a/src/test/java/com/netgrif/application/engine/elastic/ElasticIndexTest.java b/src/test/java/com/netgrif/application/engine/elastic/ElasticIndexTest.java new file mode 100644 index 00000000000..de60af27a4c --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/elastic/ElasticIndexTest.java @@ -0,0 +1,302 @@ +package com.netgrif.application.engine.elastic; + + +import com.netgrif.application.engine.TestHelper; +import com.netgrif.application.engine.auth.service.interfaces.IUserService; +import com.netgrif.application.engine.elastic.domain.IndexAwareElasticSearchRequest; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticIndexService; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.petrinet.domain.PetriNet; +import com.netgrif.application.engine.petrinet.domain.UriContentType; +import com.netgrif.application.engine.petrinet.domain.UriNode; +import com.netgrif.application.engine.petrinet.domain.VersionType; +import com.netgrif.application.engine.petrinet.service.interfaces.IUriService; +import com.netgrif.application.engine.startup.ImportHelper; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.service.interfaces.IDataService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; + +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static java.lang.Thread.sleep; +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class ElasticIndexTest { + + + @Autowired + private TestHelper testHelper; + + @Autowired + private IUserService userService; + + @Autowired + private IUriService uriService; + + @Autowired + private IElasticCaseService elasticCaseService; + + @Autowired + private IDataService dataService; + + @Autowired + private ImportHelper helper; + + @Autowired + private IWorkflowService workflowService; + + @Autowired + private IElasticIndexService elasticIndexService; + + private PetriNet aaaNet; + private PetriNet bbbNet; + private PetriNet cccNet; + private PetriNet xxxNet; + + + @BeforeEach + public void before() { + testHelper.truncateDbs(); + + UriNode aaa = uriService.getOrCreate("/aaa", UriContentType.PROCESS); + UriNode bbb = uriService.getOrCreate("/bbb", UriContentType.PROCESS); + UriNode xxx = uriService.getOrCreate("/xxx", UriContentType.PROCESS); + + this.aaaNet = helper.createNet("elasticIndex/aaa.xml", VersionType.MAJOR, userService.getSystem().transformToLoggedUser(), aaa.getStringId()).get(); + this.bbbNet = helper.createNet("elasticIndex/bbb.xml", VersionType.MAJOR, userService.getSystem().transformToLoggedUser(), bbb.getStringId()).get(); + this.xxxNet = helper.createNet("elasticIndex/xxx.xml", VersionType.MAJOR, userService.getSystem().transformToLoggedUser(), xxx.getStringId()).get(); + + this.cccNet = helper.createNet("elasticIndex/ccc.xml", VersionType.MAJOR, userService.getSystem().transformToLoggedUser(), uriService.getRoot().getStringId()).get(); + + Case aaaCase1 = helper.createCase("A1", aaaNet); + Case aaaCase2 = helper.createCase("A2", aaaNet); + Case aaaCase3 = helper.createCase("A3", aaaNet); + Case aaaCase4 = helper.createCase("A4", aaaNet); + + Case bbbCase1 = helper.createCase("B1", bbbNet); + Case bbbCase2 = helper.createCase("B2", bbbNet); + Case bbbCase3 = helper.createCase("B3", bbbNet); + Case bbbCase4 = helper.createCase("B4", bbbNet); + + Case cccCase1 = helper.createCase("C1", cccNet); + Case cccCase2 = helper.createCase("C2", cccNet); + Case cccCase3 = helper.createCase("C3", cccNet); + Case cccCase4 = helper.createCase("C4", cccNet); + + + Case xxxCase1 = helper.createCase("X1", xxxNet); + Case xxxCase2 = helper.createCase("X2", xxxNet); + Case xxxCase3 = helper.createCase("X3", xxxNet); + Case xxxCase4 = helper.createCase("X4", xxxNet); + + Case case_ = null; + + if (!aaaCase1.getTasks().isEmpty()) { + String task = aaaCase1.getTasks().stream().findFirst().orElse(null).getTask(); + Map> dataset = new HashMap<>(); + Map textAaaData = new HashMap<>(); + textAaaData.put("value", "aaa"); + textAaaData.put("type", "text"); + dataset.put("text_aaa", textAaaData); + + case_ = dataService.setData(task, ImportHelper.populateDataset(dataset)).getCase(); + workflowService.save(case_); + } + + if (!bbbCase1.getTasks().isEmpty()) { + String task = bbbCase1.getTasks().stream().findFirst().orElse(null).getTask(); + Map> dataset = new HashMap<>(); + Map textBbbData = new HashMap<>(); + textBbbData.put("value", "bbb"); + textBbbData.put("type", "text"); + dataset.put("text_bbb", textBbbData); + + case_ = dataService.setData(task, ImportHelper.populateDataset(dataset)).getCase(); + workflowService.save(case_); + } + + if (!cccCase1.getTasks().isEmpty()) { + String task = cccCase1.getTasks().stream().findFirst().orElse(null).getTask(); + Map> dataset = new HashMap<>(); + Map textCccData = new HashMap<>(); + textCccData.put("value", "ccc"); + textCccData.put("type", "text"); + dataset.put("text_ccc", textCccData); + + case_ = dataService.setData(task, ImportHelper.populateDataset(dataset)).getCase(); + workflowService.save(case_); + } + } + + @Test + public void getIndexTest(){ + List indexList_aaa = new ArrayList<>(Arrays.asList("nae_test_case_aaa")); + UriNode aaa = uriService.getOrCreate("aaa", UriContentType.PROCESS); + String elastic_aaa = elasticIndexService.getIndex(aaa); + assert indexList_aaa.get(0).equals(elastic_aaa); + + List indexList_bbb = new ArrayList<>(Arrays.asList("nae_test_case_bbb")); + UriNode bbb = uriService.getOrCreate("bbb", UriContentType.PROCESS); + String elastic_bbb = elasticIndexService.getIndex(bbb); + assert indexList_bbb.get(0).equals(elastic_bbb); + + List indexList_root = new ArrayList<>(Arrays.asList("nae_test_case")); + UriNode ccc = uriService.getRoot(); + String root_ccc = elasticIndexService.getIndex(ccc); + assert indexList_root.get(0).equals(root_ccc); + } + + @Test + public void elasticIndexTest() throws InterruptedException { + List indexList_aaa = new ArrayList<>(Arrays.asList("nae_test_case_aaa")); + List indexList_bbb = new ArrayList<>(Arrays.asList("nae_test_case_bbb")); + List indexList_ccc = new ArrayList<>(Arrays.asList("nae_test_case")); //ROOT + + List combinedList_aaa_bbb = Stream.concat(indexList_aaa.stream(), indexList_bbb.stream()) + .collect(Collectors.toList()); + + sleep(15000); + + List results_aaa_bbb = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", combinedList_aaa_bbb, PageRequest.of(0, 100)); + assertNotNull(results_aaa_bbb.get(0), "test0 should not be null"); + + List results_aaa = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_aaa, PageRequest.of(0, 100)); + assertNotNull(results_aaa.get(0), "test1 should not be null"); + + List results_bbb = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_bbb, PageRequest.of(0, 100)); + assertTrue(results_bbb.isEmpty(), "test2 should be null"); + + List results_ccc = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_ccc, PageRequest.of(0, 100)); + assertTrue(results_ccc.isEmpty(), "test3 should be null"); + } + + @Test + public void uriMoveTest() throws InterruptedException { + IntStream.range(0, 120).parallel().forEach(i -> { + helper.createCase("A" + i, aaaNet); + }); + + List indexList_aaa = new ArrayList<>(Arrays.asList("nae_test_case_aaa")); + List indexList_xxx = new ArrayList<>(Arrays.asList("nae_test_case_xxx")); + sleep(20000); + + List results_aaa = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_aaa, PageRequest.of(0, 100)); + assertNotNull(results_aaa.get(0), "test1 should not be null"); + + + List results_bbb = findCasesElastic("dataSet.text_1.textValue:\"xxx\"", indexList_xxx, PageRequest.of(0, 100)); + assertNotNull(results_bbb.get(0), "test1 should not be null"); + + UriNode rootNode = uriService.findByUri("/"); + + assert rootNode.getChildrenId().size() == 3; + + uriService.move("/xxx", "/aaa"); + + rootNode = uriService.findByUri("/"); + + assert rootNode.getChildrenId().size() == 2; + + sleep(25000); + + assertFalse(elasticIndexService.indexExists("nae_test_case_xxx"), "index remove!"); + + List results_bbb2 = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_xxx, PageRequest.of(0, 100)); + assertNotNull(results_bbb2.get(0), "test1 should not be null"); + } + + @Test + @Order(1) + public void moveIndexByProcessTest() throws InterruptedException { + IntStream.range(0, 10).parallel().forEach(i -> { + helper.createCase("A" + i, aaaNet); + }); + + List indexList_aaa = new ArrayList<>(Arrays.asList("nae_test_case_aaa")); + List indexList_bbb = new ArrayList<>(Arrays.asList("nae_test_case_bbb")); + sleep(10000); + + List results_aaa = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_aaa, PageRequest.of(0, 100)); + assertNotNull(results_aaa.get(0), "test1 should not be null"); + + + List results_bbb = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_bbb, PageRequest.of(0, 100)); + assertTrue(results_bbb.isEmpty(), "test2 should be null"); + + CaseSearchRequest request = new CaseSearchRequest(); + request.process = Collections.singletonList(new CaseSearchRequest.PetriNet(aaaNet.getIdentifier())); + IndexAwareElasticSearchRequest searchRequests = new IndexAwareElasticSearchRequest(); + searchRequests.add(request); + + elasticCaseService.moveElasticIndex(searchRequests, "nae_test_case_aaa", "nae_test_case_bbb"); + + sleep(15000); + + List results_aaa2 = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_aaa, PageRequest.of(0, 100)); + assertTrue(results_aaa2.isEmpty(), "test2 should be null"); + sleep(15000); + + List results_bbb2 = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_bbb, PageRequest.of(0, 100)); + assertFalse(results_bbb2.isEmpty(), "test1 should not be null"); + } + + + @Test + public void moveIndexTest() throws InterruptedException { + IntStream.range(0, 120).parallel().forEach(i -> { + helper.createCase("A" + i, aaaNet); + }); + + List indexList_aaa = new ArrayList<>(Arrays.asList("nae_test_case_aaa")); + List indexList_bbb = new ArrayList<>(Arrays.asList("nae_test_case_bbb")); + sleep(20000); + + List results_aaa = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_aaa, PageRequest.of(0, 100)); + assertNotNull(results_aaa.get(0), "test1 should not be null"); + + + List results_bbb = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_bbb, PageRequest.of(0, 100)); + assertTrue(results_bbb.isEmpty(), "test2 should be null"); + + + elasticCaseService.moveElasticIndex("nae_test_case_aaa", "nae_test_case_bbb"); + + sleep(15000); + + + assertFalse(elasticIndexService.indexExists("nae_test_case_aaa"), "index remove!"); + + List results_bbb2 = findCasesElastic("dataSet.text_1.textValue:\"aaa\"", indexList_bbb, PageRequest.of(0, 100)); + assertNotNull(results_bbb2.get(0), "test1 should not be null"); + } + + + protected List findCasesElastic(String query, List index, Pageable pageable) { + CaseSearchRequest request = new CaseSearchRequest(); + request.query = query; + IndexAwareElasticSearchRequest searchRequests = new IndexAwareElasticSearchRequest(); + searchRequests.setIndexNames(index); + searchRequests.add(request); + return elasticCaseService.search(searchRequests, userService.getSystem().transformToLoggedUser(), pageable, LocaleContextHolder.getLocale(), false).getContent(); + } +} diff --git a/src/test/java/com/netgrif/application/engine/rules/service/RuleEvaluationScheduleServiceTest.java b/src/test/java/com/netgrif/application/engine/rules/service/RuleEvaluationScheduleServiceTest.java index 52727d6759f..1ea79a094e7 100644 --- a/src/test/java/com/netgrif/application/engine/rules/service/RuleEvaluationScheduleServiceTest.java +++ b/src/test/java/com/netgrif/application/engine/rules/service/RuleEvaluationScheduleServiceTest.java @@ -1,5 +1,6 @@ package com.netgrif.application.engine.rules.service; +import com.netgrif.application.engine.ReindexRetryHelper; import com.netgrif.application.engine.TestHelper; import com.netgrif.application.engine.auth.domain.LoggedUser; import com.netgrif.application.engine.importer.service.throwable.MissingIconKeyException; @@ -26,6 +27,9 @@ import org.quartz.TriggerBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -67,6 +71,8 @@ public void before() { @Disabled void testScheduledRule() throws IOException, MissingPetriNetMetaDataException, RuleEvaluationScheduleException, InterruptedException, MissingIconKeyException { LoggedUser user = superCreator.getLoggedSuper(); + ReindexRetryHelper caseSearchHelper = new ReindexRetryHelper<>(); + ImportPetriNetEventOutcome importOutcome = petriNetService.importPetriNet(new FileInputStream("src/test/resources/rule_engine_test.xml"), VersionType.MAJOR, user); StoredRule rule = StoredRule.builder() @@ -87,7 +93,11 @@ void testScheduledRule() throws IOException, MissingPetriNetMetaDataException, R Thread.sleep(10000); String id = caseOutcome.getCase().getStringId(); assert id != null; - Case caze = workflowService.findOne(id); + + Case caze = caseSearchHelper.execute( + () -> workflowService.findOne(id), + cazeFinal -> cazeFinal != null + ); assert caze != null; assert caze.getDataSet().get("number_data").getValue().equals(5561.0); diff --git a/src/test/resources/application-test-ldap.properties b/src/test/resources/application-test-ldap.properties index 1e3afef7ccb..568c37afa56 100644 --- a/src/test/resources/application-test-ldap.properties +++ b/src/test/resources/application-test-ldap.properties @@ -8,9 +8,10 @@ spring.data.mongodb.drop=true # Elasticsearch spring.data.elasticsearch.drop=true -spring.data.elasticsearch.index.petriNet=${DATABASE_NAME:nae}_dev_test_petrinet +spring.data.elasticsearch.index.petriNet=${DATABASE_NAME:nae}_test_petrinet spring.data.elasticsearch.index.case=${DATABASE_NAME:nae}_test_case spring.data.elasticsearch.index.task=${DATABASE_NAME:nae}_test_task +nae.uri.index=${DATABASE_NAME:nae}_test_uri spring.data.elasticsearch.reindex=0 0 0 * * * nae.security.limits.login-attempts=3 diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 0c38d3eb36d..dfeed431f1b 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -11,6 +11,7 @@ spring.data.elasticsearch.drop=true spring.data.elasticsearch.index.petriNet=${DATABASE_NAME:nae}_test_petrinet spring.data.elasticsearch.index.case=${DATABASE_NAME:nae}_test_case spring.data.elasticsearch.index.task=${DATABASE_NAME:nae}_test_task +nae.uri.index=${DATABASE_NAME:nae}_test_uri spring.data.elasticsearch.reindex=0 0 0 * * * nae.security.limits.login-attempts=3 diff --git a/src/test/resources/petriNets/elasticIndex/aaa.xml b/src/test/resources/petriNets/elasticIndex/aaa.xml new file mode 100644 index 00000000000..04a8c929129 --- /dev/null +++ b/src/test/resources/petriNets/elasticIndex/aaa.xml @@ -0,0 +1,277 @@ + + aaa + aaa + aaa + device_hub + true + true + false + + text_0 + + <init>aaa</init> + </data> + <data type="text" immediate="true"> + <id>text_1</id> + <title/> + <init>aaa</init> + </data> + <data type="text" immediate="true"> + <id>text_2</id> + <title/> + <init>aaa</init> + </data> + <data type="text" immediate="true"> + <id>text_3</id> + <title/> + <init>aaa</init> + </data> + <data type="text" immediate="true"> + <id>text_4</id> + <title/> + <init>aaa</init> + </data> + <data type="text" immediate="true"> + <id>text_5</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_6</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_7</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_8</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_9</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_10</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_aaa</id> + <title/> + <init>aaa</init> + </data> + <transition> + <id>t1</id> + <x>260</x> + <y>100</y> + <label/> + <dataGroup> + <id>t1_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_1</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_2</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_3</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_4</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_5</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + <event type="assign"> + <id>t1_assign</id> + </event> + <event type="finish"> + <id>t1_finish</id> + </event> + <event type="cancel"> + <id>t1_cancel</id> + </event> + <event type="delegate"> + <id>t1_delegate</id> + </event> + </transition> + <transition> + <id>t2</id> + <x>380</x> + <y>100</y> + <label/> + <dataGroup> + <id>t2_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_6</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_7</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_8</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_9</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_10</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_aaa</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + <event type="assign"> + <id>t2_assign</id> + </event> + <event type="finish"> + <id>t2_finish</id> + </event> + <event type="cancel"> + <id>t2_cancel</id> + </event> + <event type="delegate"> + <id>t2_delegate</id> + </event> + </transition> +</document> \ No newline at end of file diff --git a/src/test/resources/petriNets/elasticIndex/bbb.xml b/src/test/resources/petriNets/elasticIndex/bbb.xml new file mode 100644 index 00000000000..e074c7de0e7 --- /dev/null +++ b/src/test/resources/petriNets/elasticIndex/bbb.xml @@ -0,0 +1,274 @@ +<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd"> + <id>bbb</id> + <initials>bbb</initials> + <title>bbb + device_hub + true + true + false + + text_0 + + <init>bbb</init> + </data> + <data type="text" immediate="true"> + <id>text_1</id> + <title/> + <init>bbb</init> + </data> + <data type="text" immediate="true"> + <id>text_2</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_3</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_4</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_5</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_6</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_7</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_8</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_9</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_10</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_bbb</id> + <title/> + <init>bbb</init> + </data> + <transition> + <id>t1</id> + <x>260</x> + <y>100</y> + <label/> + <dataGroup> + <id>t1_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_1</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_2</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_3</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_4</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_5</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + <event type="assign"> + <id>t1_assign</id> + </event> + <event type="finish"> + <id>t1_finish</id> + </event> + <event type="cancel"> + <id>t1_cancel</id> + </event> + <event type="delegate"> + <id>t1_delegate</id> + </event> + </transition> + <transition> + <id>t2</id> + <x>380</x> + <y>100</y> + <label/> + <dataGroup> + <id>t2_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_6</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_7</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_8</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_9</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_10</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_bbb</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + <event type="assign"> + <id>t2_assign</id> + </event> + <event type="finish"> + <id>t2_finish</id> + </event> + <event type="cancel"> + <id>t2_cancel</id> + </event> + <event type="delegate"> + <id>t2_delegate</id> + </event> + </transition> +</document> \ No newline at end of file diff --git a/src/test/resources/petriNets/elasticIndex/ccc.xml b/src/test/resources/petriNets/elasticIndex/ccc.xml new file mode 100644 index 00000000000..b5b8812323b --- /dev/null +++ b/src/test/resources/petriNets/elasticIndex/ccc.xml @@ -0,0 +1,274 @@ +<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd"> + <id>ccc</id> + <initials>ccc</initials> + <title>ccc + device_hub + true + true + false + + text_0 + + <init>ccc</init> + </data> + <data type="text" immediate="true"> + <id>text_1</id> + <title/> + <init>ccc</init> + </data> + <data type="text" immediate="true"> + <id>text_2</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_3</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_4</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_5</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_6</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_7</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_8</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_9</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_10</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_ccc</id> + <title/> + <init>ccc</init> + </data> + <transition> + <id>t1</id> + <x>260</x> + <y>100</y> + <label/> + <dataGroup> + <id>t1_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_1</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_2</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_3</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_4</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_5</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + <event type="assign"> + <id>t1_assign</id> + </event> + <event type="finish"> + <id>t1_finish</id> + </event> + <event type="cancel"> + <id>t1_cancel</id> + </event> + <event type="delegate"> + <id>t1_delegate</id> + </event> + </transition> + <transition> + <id>t2</id> + <x>380</x> + <y>100</y> + <label/> + <dataGroup> + <id>t2_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_6</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_7</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_8</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_9</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_10</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_ccc</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + <event type="assign"> + <id>t2_assign</id> + </event> + <event type="finish"> + <id>t2_finish</id> + </event> + <event type="cancel"> + <id>t2_cancel</id> + </event> + <event type="delegate"> + <id>t2_delegate</id> + </event> + </transition> +</document> \ No newline at end of file diff --git a/src/test/resources/petriNets/elasticIndex/xxx.xml b/src/test/resources/petriNets/elasticIndex/xxx.xml new file mode 100644 index 00000000000..ac2718ff0af --- /dev/null +++ b/src/test/resources/petriNets/elasticIndex/xxx.xml @@ -0,0 +1,274 @@ +<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd"> + <id>xxx</id> + <initials>xxx</initials> + <title>xxx + device_hub + true + true + false + + text_0 + + <init>xxx</init> + </data> + <data type="text" immediate="true"> + <id>text_1</id> + <title/> + <init>xxx</init> + </data> + <data type="text" immediate="true"> + <id>text_2</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_3</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_4</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_5</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_6</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_7</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_8</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_9</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_10</id> + <title/> + </data> + <data type="text" immediate="true"> + <id>text_xxx</id> + <title/> + <init>xxx</init> + </data> + <transition> + <id>t1</id> + <x>260</x> + <y>100</y> + <label/> + <dataGroup> + <id>t1_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_0</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_1</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_2</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_3</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_4</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_5</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + <event type="assign"> + <id>t1_assign</id> + </event> + <event type="finish"> + <id>t1_finish</id> + </event> + <event type="cancel"> + <id>t1_cancel</id> + </event> + <event type="delegate"> + <id>t1_delegate</id> + </event> + </transition> + <transition> + <id>t2</id> + <x>380</x> + <y>100</y> + <label/> + <dataGroup> + <id>t2_0</id> + <cols>4</cols> + <layout>grid</layout> + <dataRef> + <id>text_6</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_7</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>0</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_8</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_9</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>0</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_10</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>1</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + <dataRef> + <id>text_xxx</id> + <logic> + <behavior>editable</behavior> + </logic> + <layout> + <x>2</x> + <y>2</y> + <rows>1</rows> + <cols>2</cols> + <template>material</template> + <appearance>outline</appearance> + </layout> + </dataRef> + </dataGroup> + <event type="assign"> + <id>t2_assign</id> + </event> + <event type="finish"> + <id>t2_finish</id> + </event> + <event type="cancel"> + <id>t2_cancel</id> + </event> + <event type="delegate"> + <id>t2_delegate</id> + </event> + </transition> +</document> \ No newline at end of file