Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Object> search(IEditingContext editingContext, SearchQuery query) {
if (editingContext instanceof IEMFEditingContext emfEditingContext) {
long start = System.nanoTime();
var textPredicate = this.toTextPredicate(query);

Stream<Notifier> 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<String> 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<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String, Object> 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");
}
}
Loading