From 86de3c8f1e0dfa426161d88221fcd4eb3b751155 Mon Sep 17 00:00:00 2001 From: Gwendal Daniel Date: Fri, 13 Feb 2026 18:26:18 +0100 Subject: [PATCH] [1994] Add support for excluding standard libraries in search view Bug: https://github.com/eclipse-syson/syson/issues/1994 Signed-off-by: Gwendal Daniel --- .../search/SysONSearchService.java | 115 ++++++++++++++++++ .../search/SearchIntegrationTests.java | 90 ++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/search/SysONSearchService.java create mode 100644 backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/search/SearchIntegrationTests.java diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/search/SysONSearchService.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/search/SysONSearchService.java new file mode 100644 index 000000000..77705e529 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/search/SysONSearchService.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.syson.application.search; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.eclipse.emf.common.notify.Notifier; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.ILabelService; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.web.application.library.services.LibraryMetadataAdapter; +import org.eclipse.sirius.web.application.views.search.dto.SearchQuery; +import org.eclipse.sirius.web.application.views.search.services.api.ISearchService; +import org.eclipse.syson.sysml.util.ElementUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +/** + * Searches for elements inside a SysON editing context based on a user-supplied query. + * + * @author gdaniel + */ +@Service +@Primary // This class should be a delegate once https://github.com/eclipse-sirius/sirius-web/issues/5892 is fixed +public class SysONSearchService implements ISearchService { + + private static final int MAX_RESULT_SIZE = 1000_000; + + private final Logger logger = LoggerFactory.getLogger(SysONSearchService.class); + + private final ILabelService labelService; + + public SysONSearchService(ILabelService labelService) { + this.labelService = Objects.requireNonNull(labelService); + } + + @Override + public List search(IEditingContext editingContext, SearchQuery query) { + if (editingContext instanceof IEMFEditingContext emfEditingContext) { + long start = System.nanoTime(); + var textPredicate = this.toTextPredicate(query); + + Stream stream = emfEditingContext.getDomain().getResourceSet().getResources().stream() + .filter(resource -> query.searchInLibraries() + || (!ElementUtil.isStandardLibraryResource(resource) && resource.eAdapters().stream().noneMatch(LibraryMetadataAdapter.class::isInstance))) + .map(resource -> Stream.concat(Stream.of(resource), StreamSupport.stream(Spliterators.spliteratorUnknownSize(resource.getAllContents(), Spliterator.ORDERED), false))) + .reduce(Stream::concat) + .orElse(Stream.empty()); + + var result = stream.filter(obj -> this.matches(obj, query.searchInAttributes(), textPredicate)) + .limit(MAX_RESULT_SIZE) + .map(Object.class::cast).toList(); + var duration = Duration.ofNanos(System.nanoTime() - start); + this.logger.debug("Search found {} matches in {}s", result.size(), duration.toMillis()); + return result; + } + return List.of(); + } + + private Predicate toTextPredicate(SearchQuery query) { + StringBuilder patternText = new StringBuilder(); + if (query.matchWholeWord()) { + patternText.append("\\b"); + } + if (query.useRegularExpression()) { + patternText.append(query.text()); + } else { + patternText.append(Pattern.quote(query.text())); + } + if (query.matchWholeWord()) { + patternText.append("\\b"); + } + + int patternFlags = 0; + if (!query.matchCase()) { + patternFlags = Pattern.CASE_INSENSITIVE; + } + + return Pattern.compile(patternText.toString(), patternFlags).asPredicate(); + } + + private boolean matches(Object object, boolean searchInAttributes, Predicate predicate) { + boolean result = false; + String labelText = this.labelService.getStyledLabel(object).toString(); + boolean isLabelMatch = labelText != null && predicate.test(labelText); + if (isLabelMatch) { + result = true; + } else if (searchInAttributes && object instanceof EObject eObject) { + result = eObject.eClass().getEAllAttributes().stream() + .anyMatch(attribute -> predicate.test(String.valueOf(eObject.eGet(attribute)))); + } + return result; + } +} diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/search/SearchIntegrationTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/search/SearchIntegrationTests.java new file mode 100644 index 000000000..caff83771 --- /dev/null +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/search/SearchIntegrationTests.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.syson.application.controllers.search; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.jayway.jsonpath.JsonPath; + +import java.util.List; +import java.util.Map; + +import org.eclipse.sirius.web.application.views.search.dto.SearchResult; +import org.eclipse.sirius.web.application.views.search.dto.SearchSuccessPayload; +import org.eclipse.sirius.web.tests.graphql.SearchQueryRunner; +import org.eclipse.syson.AbstractIntegrationTests; +import org.eclipse.syson.SysONTestsProperties; +import org.eclipse.syson.application.data.SimpleProjectElementsTestProjectData; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests of the search controllers. + * + * @author gdaniel + */ +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { SysONTestsProperties.NO_DEFAULT_LIBRARIES_PROPERTY }) +public class SearchIntegrationTests extends AbstractIntegrationTests { + + @Autowired + private SearchQueryRunner searchQueryRunner; + + @Test + @DisplayName("GIVEN a SysML project, WHEN we execute a search including libraries, THEN all the matching semantic elements are returned") + @Sql(scripts = { SimpleProjectElementsTestProjectData.SCRIPT_PATH }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + @Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenSysMLProjectWhenWeExecuteSearchIncludingLibrariesThenAllMatchingElementsAreReturned() { + // TODO checked: p is part of the test data + List matches = this.search(SimpleProjectElementsTestProjectData.EDITING_CONTEXT_ID, "p", false, false, false, false, /* searchInLibraries */ false); + assertThat(matches).containsExactlyInAnyOrder( + "??" + ); + matches = this.search(SimpleProjectElementsTestProjectData.EDITING_CONTEXT_ID, "part", false, false, false, false, /* searchInLibraries */ true); + assertThat(matches).containsExactlyInAnyOrder( + // TODO, check how long it takes, may not work well with the CI + "??" + ); + } + + private List search(String editingContextId, String text, boolean matchCase, boolean matchWholeWord, boolean useRegularExpressions, boolean searchInAttributes, boolean searchInLibraries) { + // The SearchQuery object must be passed as a plain Map here + var queryMap = Map.of( + "text", text, + "matchCase", matchCase, + "matchWholeWord", matchWholeWord, + "useRegularExpression", useRegularExpressions, + "searchInAttributes", searchInAttributes, + "searchInLibraries", searchInLibraries + ); + Map variables = Map.of( + "editingContextId", editingContextId, + "query", queryMap + ); + var result = this.searchQueryRunner.run(variables); + + String payloadTypename = JsonPath.read(result.data(), "$.data.viewer.editingContext.search.__typename"); + assertThat(payloadTypename).isEqualTo(SearchSuccessPayload.class.getSimpleName()); + + String resultTypename = JsonPath.read(result.data(), "$.data.viewer.editingContext.search.result.__typename"); + assertThat(resultTypename).isEqualTo(SearchResult.class.getSimpleName()); + + return JsonPath.read(result.data(), "$.data.viewer.editingContext.search.result.matches[*].label"); + } +}