diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index f2b7542ce..6c03f1fbb 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -1,6 +1,30 @@ = Changelog -== v2026.1.0 (work in progress) +== v2026.3.0 (work in progress) + +=== Shapes + +=== Breaking changes + +=== Dependency update + +=== Bug fixes + +- https://github.com/eclipse-syson/syson/issues/1847[#1847] [export] Textual export duplicates "abstract" keyword for `OccurrenceUsage`. + +=== Improvements + +- https://github.com/eclipse-syson/syson/issues/1694[#1694] [syson] Customize Elasticsearch indices to work with SysML. +SysON now uses its own indexing logic instead of the default one provided by Sirius Web. +This allows to limit the size of the indices, and ensure information stored in the indices are useful to perform cross-project search. +You can find more information on how to setup Elasticsearch, how elements are mapped to index documents, and how to query them in the documentation. +- https://github.com/eclipse-syson/syson/issues/1649[#1649] [syson] Add support for publishing representations with libraries. +Publishing a project that contains representations now produces libraries that also contain the representations. + +=== New features + + +== v2026.1.0 === Shapes @@ -51,7 +75,6 @@ This fix ensure that imported models containing, for example, a top-level `Libra - https://github.com/eclipse-syson/syson/issues/1825[#1825] `LiteralRational` value wrongly rounded during textual export. - https://github.com/eclipse-syson/syson/issues/1784[#1784] Error while exporting `FeatureValue` using enumeration literals. - === Improvements - https://github.com/eclipse-syson/syson/issues/1666[#1666] [export] Export `SatisfyRequirementUsage` in textual format. diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/INestedIndexEntry.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/INestedIndexEntry.java new file mode 100644 index 000000000..475dfba97 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/INestedIndexEntry.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * 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.index; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.eclipse.sirius.web.application.index.services.api.IIndexEntry; + +/** + * A sub index entry contained in an {@link IIndexEntry}. + * + *

+ * Nested index entries are used inside regular {@link IIndexEntry} POJOs to represent elements connected to the elements represented by the {@link IIndexEntry}. In most cases, + * {@link INestedIndexEntry} should not contain other {@link INestedIndexEntry}, in order to avoid infinite nesting depth, which cannot be stored in the index. This constraint may be relaxed for + * {@link INestedIndexEntry} representing relationships, which can themselves contain an {@link INestedIndexEntry} representing the other end of the relationship. + *

+ *

+ * For example, an {@link IIndexEntry} representing a SysML element can contain {@link INestedIndexEntry} representing its owned elements, but these nested entries should not contain information + * related to their own nested elements. The same {@link IIndexEntry} can contain {@link INestedIndexEntry} representing its relationships, which themselves can contain {@link INestedIndexEntry} + * representing the other end of the relationship, but this second level of nested entries cannot contain another level of {@link INestedIndexEntry}. + *

+ *

+ * The fields {@code id}, {@code type}, and {@code label} should match the identifier of the semantic element, the name of its SysML type, and the label used to present it to the end user. Note that + * these fields are serialized with a {@code @} prefix because they represent technical information, and shouldn't clash with potential fields computed from attributes in the SysML metamodel. + * An extra field {@code @nestedIndexEntryType} is also produced during the serialization to store the concrete type of the POJO. This lets Sirius Web find the actual type to use when deserializing + * query results. + * {@link INestedIndexEntry} does not provide {@code iconURLs} nor {@code editingContextId}, because they are not directly manipulated by Sirius Web (their containing {@link IIndexEntry} are). + *

+ * + * @author gdaniel + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@nestedIndexEntryType") +public interface INestedIndexEntry { + + String NESTED_INDEX_ENTRY_TYPE_FIELD = "@nestedIndexEntryType"; + + @JsonProperty(IIndexEntry.ID_FIELD) + String id(); + + @JsonProperty(IIndexEntry.TYPE_FIELD) + String type(); + + @JsonProperty(IIndexEntry.LABEL_FIELD) + String label(); + +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/IndexEntrySwitch.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/IndexEntrySwitch.java new file mode 100644 index 000000000..5845d09d3 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/IndexEntrySwitch.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * 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.index; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EClassifier; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IIdentityService; +import org.eclipse.sirius.components.core.api.ILabelService; +import org.eclipse.sirius.web.application.index.services.api.IIndexEntry; +import org.eclipse.syson.sysml.Namespace; +import org.eclipse.syson.sysml.SysmlPackage; +import org.eclipse.syson.sysml.Type; +import org.eclipse.syson.sysml.util.SysmlSwitch; + +/** + * Provides {@link IIndexEntry} for SysML elements. + * + *

+ * {@link IIndexEntry} are top-level POJOs representing SysML elements that are stored in Elasticsearch indices. They can contain {@link INestedIndexEntry} to represent elements connected to the + * element they represent. + *

+ *

+ * SysON serializes SysML elements into entries that can contain nested entries with a maximum depth of: + *

+ *

+ * + * @author gdaniel + */ +public class IndexEntrySwitch extends SysmlSwitch> { + + private final IIdentityService identityService; + + private final ILabelService labelService; + + private final IEditingContext editingContext; + + private final NestedIndexEntrySwitch nestedIndexEntrySwitch; + + public IndexEntrySwitch(IIdentityService identityService, ILabelService labelService, IEditingContext editingContext) { + this.identityService = Objects.requireNonNull(identityService); + this.labelService = Objects.requireNonNull(labelService); + this.editingContext = Objects.requireNonNull(editingContext); + this.nestedIndexEntrySwitch = new NestedIndexEntrySwitch(identityService, labelService); + } + + @Override + public Optional doSwitch(EObject eObject) { + Optional result = Optional.empty(); + if (eObject != null) { + result = super.doSwitch(eObject); + } + return result; + } + + @Override + public Optional defaultCase(EObject object) { + return Optional.empty(); + } + + @Override + public Optional caseNamespace(Namespace namespace) { + String objectId = this.identityService.getId(namespace); + String type = this.getEClassifierName(namespace.eClass()); + String label = this.labelService.getStyledLabel(namespace).toString(); + List iconURLs = this.labelService.getImagePaths(namespace); + + String name = namespace.getName(); + String shortName = namespace.getShortName(); + String qualifiedName = namespace.getQualifiedName(); + + Optional ownerNestedIndexEntry = this.nestedIndexEntrySwitch.doSwitch(namespace.getOwner()); + List ownedElementNestedIndexEntries = namespace.getOwnedElement().stream() + .map(this.nestedIndexEntrySwitch::doSwitch) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + + return Optional.of(new NamespaceIndexEntry(this.editingContext.getId(), objectId, type, label, iconURLs, name, shortName, qualifiedName, ownerNestedIndexEntry.orElse(null), + ownedElementNestedIndexEntries)); + } + + @Override + public Optional caseType(Type type) { + String objectId = this.identityService.getId(type); + String objectType = this.getEClassifierName(type.eClass()); + String label = this.labelService.getStyledLabel(type).toString(); + List iconURLs = this.labelService.getImagePaths(type); + + String name = type.getName(); + String shortName = type.getShortName(); + String qualifiedName = type.getQualifiedName(); + + Optional ownerNestedIndexEntry = this.nestedIndexEntrySwitch.doSwitch(type.getOwner()); + List ownedElementNestedIndexEntries = type.getOwnedElement().stream() + .map(this.nestedIndexEntrySwitch::doSwitch) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + List ownedSpecializationNestedIndexEntries = type.getOwnedSpecialization().stream() + .map(this.nestedIndexEntrySwitch::doSwitch) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(NestedSpecializationIndexEntry.class::isInstance) + .map(NestedSpecializationIndexEntry.class::cast) + .toList(); + + return Optional.of(new TypeIndexEntry(this.editingContext.getId(), objectId, objectType, label, iconURLs, name, shortName, qualifiedName, ownerNestedIndexEntry.orElse(null), + ownedSpecializationNestedIndexEntries, ownedElementNestedIndexEntries)); + } + + private String getEClassifierName(EClassifier eClassifier) { + String name = eClassifier.getName(); + if (eClassifier instanceof EClass eClass) { + if (SysmlPackage.eINSTANCE.getUsage().isSuperTypeOf(eClass) + && !SysmlPackage.eINSTANCE.getConnectorAsUsage().equals(eClass) + && !SysmlPackage.eINSTANCE.getBindingConnectorAsUsage().equals(eClass) + && !SysmlPackage.eINSTANCE.getSuccessionAsUsage().equals(eClass)) { + if (eClass.getName().endsWith("Usage")) { + name = eClass.getName().substring(0, eClass.getName().length() - 5); + } + } + } + return name; + } +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NamespaceIndexEntry.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NamespaceIndexEntry.java new file mode 100644 index 000000000..cd0ba1e41 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NamespaceIndexEntry.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * 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.index; + +import java.util.List; + +import org.eclipse.sirius.web.application.index.services.api.IIndexEntry; + +/** + * An index entry representing a {@link org.eclipse.syson.sysml.Namespace}. + * + *

+ * This record contains relevant data related to a SysML Namespace (e.g. name, short name, qualified name), as well as technical information used by Sirius Web to present index entries to the end user + * (label, icons, type). + *

+ *

+ * This class contains {@link INestedIndexEntry}, which are additional objects serialized as sub-fields of this index entry, which allows to access information related to elements connected to the + * namespace (e.g. {@code owner.name} to access the name of the owner, or {@code ownedElement.name} to access the name of the owned elements). + *

+ * + * @param editingContextId the identifier of the editing context containing the namespace + * @param id the identifier of the namespace + * @param type the name of the concrete SysML type of the namespace + * @param label the label of the namespace + * @param iconURLs the URLs of the icons of the namespace + * @param name the name of the namespace + * @param shortName the short name of the namespace + * @param qualifiedName the qualified name of the namespace + * @param owner the nested entry holding information related to the namespace's owner + * @param ownedElement the nested entries holding information related to the namespace owned elements + * + * @author gdaniel + */ +public record NamespaceIndexEntry( + String editingContextId, + String id, + String type, + String label, + List iconURLs, + String name, + String shortName, + String qualifiedName, + INestedIndexEntry owner, + List ownedElement +) implements IIndexEntry { +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedElementIndexEntry.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedElementIndexEntry.java new file mode 100644 index 000000000..dfa240bc8 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedElementIndexEntry.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * 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.index; + +/** + * An index entry representing a nested {@link org.eclipse.syson.sysml.Element}. + * + *

+ * This record does not contain recursive {@link INestedIndexEntry} in order to avoid infinite entry nesting when converting elements into index entries. + * See {@link INestedIndexEntry} for more information. + *

+ * + * @param id the identifier of the element + * @param type the name of the concrete SysML type of the element + * @param label the label of the element + * @param name the name of the element + * @param shortName the short name of the element + * @param qualifiedName the qualified name of the element + * + * @author gdaniel + */ +public record NestedElementIndexEntry( + String id, + String type, + String label, + String name, + String shortName, + String qualifiedName +) implements INestedIndexEntry { +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedIndexEntrySwitch.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedIndexEntrySwitch.java new file mode 100644 index 000000000..df8a3bd14 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedIndexEntrySwitch.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * 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.index; + +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EClassifier; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.sirius.components.core.api.IIdentityService; +import org.eclipse.sirius.components.core.api.ILabelService; +import org.eclipse.syson.sysml.Element; +import org.eclipse.syson.sysml.Specialization; +import org.eclipse.syson.sysml.SysmlPackage; +import org.eclipse.syson.sysml.util.SysmlSwitch; + + +/** + * Provides {@link INestedIndexEntry} for SysML elements. + * + *

+ * {@link INestedIndexEntry} are used inside {@link org.eclipse.sirius.web.application.index.services.api.IIndexEntry} to represent elements associated to the element represented by their containing + * {@link org.eclipse.sirius.web.application.index.services.api.IIndexEntry}. They are usually not recursive, and may contain different information than their + * {@link org.eclipse.sirius.web.application.index.services.api.IIndexEntry} counterparts. + *

+ * + * @see INestedIndexEntry + * + * @author gdaniel + */ +public class NestedIndexEntrySwitch extends SysmlSwitch> { + + private final IIdentityService identityService; + + private final ILabelService labelService; + + public NestedIndexEntrySwitch(IIdentityService identityService, ILabelService labelService) { + this.identityService = Objects.requireNonNull(identityService); + this.labelService = Objects.requireNonNull(labelService); + } + + @Override + public Optional doSwitch(EObject eObject) { + Optional result = Optional.empty(); + if (eObject != null) { + result = super.doSwitch(eObject); + } + return result; + } + + @Override + public Optional defaultCase(EObject object) { + return Optional.empty(); + } + + + @Override + public Optional caseElement(Element element) { + String objectId = this.identityService.getId(element); + String type = this.getEClassifierName(element.eClass()); + String label = this.labelService.getStyledLabel(element).toString(); + + String name = element.getName(); + String shortName = element.getShortName(); + String qualifiedName = element.getQualifiedName(); + + return Optional.of(new NestedElementIndexEntry(objectId, type, label, name, shortName, qualifiedName)); + } + + @Override + public Optional caseSpecialization(Specialization specialization) { + String objectId = this.identityService.getId(specialization); + String type = this.getEClassifierName(specialization.eClass()); + String label = this.labelService.getStyledLabel(specialization).toString(); + + String name = specialization.getName(); + String shortName = specialization.getShortName(); + String qualifiedName = specialization.getQualifiedName(); + + Optional general = this.doSwitch(specialization.getGeneral()) + .filter(NestedElementIndexEntry.class::isInstance) + .map(NestedElementIndexEntry.class::cast); + + return Optional.of(new NestedSpecializationIndexEntry(objectId, type, label, name, shortName, qualifiedName, general.orElse(null))); + } + + private String getEClassifierName(EClassifier eClassifier) { + String name = eClassifier.getName(); + if (eClassifier instanceof EClass eClass) { + if (SysmlPackage.eINSTANCE.getUsage().isSuperTypeOf(eClass) + && !SysmlPackage.eINSTANCE.getConnectorAsUsage().equals(eClass) + && !SysmlPackage.eINSTANCE.getBindingConnectorAsUsage().equals(eClass) + && !SysmlPackage.eINSTANCE.getSuccessionAsUsage().equals(eClass)) { + if (eClass.getName().endsWith("Usage")) { + name = eClass.getName().substring(0, eClass.getName().length() - 5); + } + } + } + return name; + } +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedSpecializationIndexEntry.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedSpecializationIndexEntry.java new file mode 100644 index 000000000..2781e5658 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedSpecializationIndexEntry.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * 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.index; + +/** + * An index entry representing a nested {@link org.eclipse.syson.sysml.Specialization}. + * + *

+ * This record contains a {@link NestedElementIndexEntry} which allows to access information related to the {@code general} element of the specialization. Note that this nested element is not + * recursive, preventing infinite entry nesting when converting elements into index entries. See {@link INestedIndexEntry} for more information. + *

+ * + * @param id the identifier of the specialization + * @param type the name of the concrete SysML type of the specialization + * @param label the label of the specialization + * @param name the name of the specialization + * @param shortName the short name of the specialization + * @param qualifiedName the qualified name of the specialization + * + * @author gdaniel + */ +public record NestedSpecializationIndexEntry( + String id, + String type, + String label, + String name, + String shortName, + String qualifiedName, + NestedElementIndexEntry general +) implements INestedIndexEntry { +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexCreationService.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexCreationService.java new file mode 100644 index 000000000..415ea6e8d --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexCreationService.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * 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.index; + +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.sirius.web.application.index.services.api.IIndexEntry; +import org.eclipse.sirius.web.application.studio.services.api.IStudioCapableEditingContextPredicate; +import org.eclipse.sirius.web.infrastructure.elasticsearch.services.DefaultIndexCreationService; +import org.eclipse.sirius.web.infrastructure.elasticsearch.services.api.IIndexCreationServiceDelegate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import co.elastic.clients.elasticsearch._types.mapping.DynamicTemplate; +import co.elastic.clients.util.NamedValue; + +/** + * Creates indices to store SysML models. + * + *

+ * This service creates and configures the index associated to each editing context. The created index has a name matching the pattern {@code editing-context-}. + * This service is automatically called by Sirius Web when an editing context is created (e.g. when a project is created). + *

+ * + * @author gdaniel + */ +@Service +public class SysONIndexCreationService implements IIndexCreationServiceDelegate { + + private final IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate; + + private final Optional optionalElasticSearchClient; + + private final Logger logger = LoggerFactory.getLogger(SysONIndexCreationService.class); + + public SysONIndexCreationService(IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate, Optional optionalElasticSearchClient) { + this.studioCapableEditingContextPredicate = Objects.requireNonNull(studioCapableEditingContextPredicate); + this.optionalElasticSearchClient = Objects.requireNonNull(optionalElasticSearchClient); + } + + @Override + public boolean canHandle(String editingContextId) { + return !this.studioCapableEditingContextPredicate.test(editingContextId); + } + + @Override + public boolean createIndex(String editingContextId) { + boolean indexCreated = false; + if (this.optionalElasticSearchClient.isPresent()) { + ElasticsearchClient elasticSearchClient = this.optionalElasticSearchClient.get(); + try { + if (!elasticSearchClient.indices().exists(existsRequest -> existsRequest.index(DefaultIndexCreationService.EDITING_CONTEXT_INDEX_NAME_PREFIX + editingContextId)).value()) { + elasticSearchClient.indices().create(createIndexRequest -> createIndexRequest + .index(DefaultIndexCreationService.EDITING_CONTEXT_INDEX_NAME_PREFIX + editingContextId) + .settings(settingsBuilder -> settingsBuilder.mode("lookup")) + .mappings(mappingsBuilder -> mappingsBuilder + // Do not index nestedIndexEntryType field, this is technical information we don't want to search for. + // Note that we need to use a dynamic template here because multiple fields may exist containing "@nestedIndexEntryType", + // and we cannot use wildcards in field names when defining properties. + .dynamicTemplates(NamedValue.of("nestedIndexEntryTypeMapping", DynamicTemplate.of(dynamicTemplateBuilder -> + dynamicTemplateBuilder.matchMappingType("string") + .match("*" + INestedIndexEntry.NESTED_INDEX_ENTRY_TYPE_FIELD) + .mapping(mappingBuilder -> mappingBuilder + .text(textBuilder -> textBuilder.index(false))) + ))) + // Do not index iconURLs, editingContextId, or entryType, this is technical information we don't want to search for. + .properties(IIndexEntry.ICON_URLS_FIELD, propertyBuilder -> + propertyBuilder.text(textPropertyBuilder -> + textPropertyBuilder.index(false))) + .properties(IIndexEntry.EDITING_CONTEXT_ID_FIELD, propertyBuilder -> + propertyBuilder.text(textPropertyBuilder -> + textPropertyBuilder.index(false))) + .properties(IIndexEntry.INDEX_ENTRY_TYPE_FIELD, propertyBuilder -> + propertyBuilder.text(textPropertyBuilder -> + textPropertyBuilder.index(false))))); + indexCreated = true; + } + } catch (IOException | ElasticsearchException exception) { + this.logger.warn("An error occurred while creating the index", exception); + } + } + return indexCreated; + } +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexEntryProvider.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexEntryProvider.java new file mode 100644 index 000000000..4df15246f --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexEntryProvider.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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.index; + +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IIdentityService; +import org.eclipse.sirius.components.core.api.ILabelService; +import org.eclipse.sirius.web.application.index.services.api.IIndexEntry; +import org.eclipse.sirius.web.application.index.services.api.IIndexEntryProviderDelegate; +import org.eclipse.sirius.web.application.studio.services.api.IStudioCapableEditingContextPredicate; +import org.eclipse.syson.sysml.Element; +import org.springframework.stereotype.Service; + +/** + * Provides index entries for SysON. + * + *

+ * This service converts SysML {@link Element} into {@link IIndexEntry} that can be persisted in the Elasticsearch indices. + *

+ *

+ * Note that this service does not accept {@code editingContext} instances that contain Studio data. For these {@code editingContext}, the default implementation of + * {@link org.eclipse.sirius.web.application.index.services.api.IDefaultIndexEntryProvider} is used instead. + *

+ * + * @author gdaniel + */ +@Service +public class SysONIndexEntryProvider implements IIndexEntryProviderDelegate { + + private final IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate; + + private final IIdentityService identityService; + + private final ILabelService labelService; + + public SysONIndexEntryProvider(IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate, IIdentityService identityService, ILabelService labelService) { + this.studioCapableEditingContextPredicate = Objects.requireNonNull(studioCapableEditingContextPredicate); + this.identityService = Objects.requireNonNull(identityService); + this.labelService = Objects.requireNonNull(labelService); + } + + @Override + public boolean canHandle(IEditingContext editingContext, Object object) { + // Only produce SysON index entries for non-studio projects. + return !this.studioCapableEditingContextPredicate.test(editingContext.getId()); + } + + @Override + public Optional getIndexEntry(IEditingContext editingContext, Object object) { + Optional result = Optional.empty(); + if (object instanceof Element element) { + result = new IndexEntrySwitch(this.identityService, this.labelService, editingContext) + .doSwitch(element); + } + return result; + } +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexUpdateService.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexUpdateService.java new file mode 100644 index 000000000..04475832a --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexUpdateService.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * 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.index; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.StreamSupport; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.web.application.index.services.api.IIndexEntry; +import org.eclipse.sirius.web.application.index.services.api.IIndexEntryProvider; +import org.eclipse.sirius.web.application.studio.services.api.IStudioCapableEditingContextPredicate; +import org.eclipse.sirius.web.infrastructure.elasticsearch.services.DefaultIndexCreationService; +import org.eclipse.sirius.web.infrastructure.elasticsearch.services.api.IIndexCreationService; +import org.eclipse.sirius.web.infrastructure.elasticsearch.services.api.IIndexDeletionService; +import org.eclipse.sirius.web.infrastructure.elasticsearch.services.api.IIndexUpdateServiceDelegate; +import org.eclipse.syson.sysml.util.ElementUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._helpers.bulk.BulkIngester; + +/** + * A service to index the content of SysML model. + * + *

+ * This service is called by Sirius Web when changes are detected in an editing context, which may require to update the associated indices. This implementation ensures that all the resources in the + * editing context are indexed, excepted SysML and KerML standard libraries for performance reasons. + *

+ *

+ * Note that this service does not accept {@code editingContext} instances that contain Studio data. For these {@code editingContext}, the default implementation of + * {@link org.eclipse.sirius.web.infrastructure.elasticsearch.services.api.IDefaultIndexUpdateService} is used instead. + *

+ * + * + * @author gdaniel + */ +@Service +public class SysONIndexUpdateService implements IIndexUpdateServiceDelegate { + + private final IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate; + + private final IIndexCreationService indexCreationService; + + private final IIndexDeletionService indexDeletionService; + + private final IIndexEntryProvider indexEntryProvider; + + private final Optional optionalElasticSearchClient; + + private final Logger logger = LoggerFactory.getLogger(SysONIndexUpdateService.class); + + public SysONIndexUpdateService(IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate, IIndexCreationService indexCreationService, IIndexDeletionService indexDeletionService, + IIndexEntryProvider indexEntryProvider, Optional optionalElasticSearchClient) { + this.studioCapableEditingContextPredicate = Objects.requireNonNull(studioCapableEditingContextPredicate); + this.indexCreationService = Objects.requireNonNull(indexCreationService); + this.indexDeletionService = Objects.requireNonNull(indexDeletionService); + this.indexEntryProvider = Objects.requireNonNull(indexEntryProvider); + this.optionalElasticSearchClient = Objects.requireNonNull(optionalElasticSearchClient); + } + + @Override + public boolean canHandle(IEditingContext editingContext) { + return !this.studioCapableEditingContextPredicate.test(editingContext.getId()); + } + + @Override + public void updateIndex(IEditingContext editingContext) { + long start = System.nanoTime(); + if (this.optionalElasticSearchClient.isPresent()) { + if (editingContext instanceof IEMFEditingContext emfEditingContext) { + this.clearIndex(editingContext); + BulkIngester bulkIngester = BulkIngester.of(bulkIngesterBuilder -> bulkIngesterBuilder + .client(this.optionalElasticSearchClient.get()) + ); + // We don't want to index SysML/KerML standard libraries: they are duplicated in each project and pollutes the search. + List resourcesToIndex = emfEditingContext.getDomain().getResourceSet().getResources().stream() + .filter(resource -> !ElementUtil.isStandardLibraryResource(resource)) + .toList(); + for (Resource resourceToIndex : resourcesToIndex) { + StreamSupport.stream(Spliterators.spliteratorUnknownSize(resourceToIndex.getAllContents(), Spliterator.ORDERED), false) + .forEach(eObject -> { + Optional optionalIndexEntry = this.indexEntryProvider.getIndexEntry(editingContext, eObject); + if (optionalIndexEntry.isPresent()) { + IIndexEntry indexEntry = optionalIndexEntry.get(); + bulkIngester.add(bulkOperation -> bulkOperation + .index(indexOperation -> indexOperation + .index(DefaultIndexCreationService.EDITING_CONTEXT_INDEX_NAME_PREFIX + editingContext.getId()) + .id(indexEntry.id()) + .document(indexEntry) + ) + ); + } + }); + } + bulkIngester.close(); + } + } + Duration timeToIndexEditingContext = Duration.ofNanos(System.nanoTime() - start); + this.logger.trace("Indexed editing context {} in {}ms", editingContext.getId(), timeToIndexEditingContext.toMillis()); + } + + private void clearIndex(IEditingContext editingContext) { + /* + * In some cases (particularly when a new project is created), this line may throw an exception indicating that the index does not exist. This exception doesn't crash the application, it just + * prevents an update of the index, which is not critical (it will be updated later when the semantic data gets updated). + * This issue is linked to https://github.com/eclipse-sirius/sirius-web/issues/6044, and will disappear once it is fixed in Sirius Web. + */ + this.indexDeletionService.deleteIndex(editingContext.getId()); + this.indexCreationService.createIndex(editingContext.getId()); + } +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/TypeIndexEntry.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/TypeIndexEntry.java new file mode 100644 index 000000000..bd9a155e9 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/TypeIndexEntry.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * 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.index; + +import java.util.List; + +import org.eclipse.sirius.web.application.index.services.api.IIndexEntry; + +/** + * An index entry representing a {@link org.eclipse.syson.sysml.Type}. + * + *

+ * This record contains relevant data related to a SysML Type (e.g. name, short name, qualified name), as well as technical information used by Sirius Web to present index entries to the end user + * (label, icons, type). + *

+ *

+ * This class contains {@link INestedIndexEntry}, which are additional objects serialized as sub-fields of this index entry, which allows to access information related to elements connected to the + * type (e.g. {@code owner.name} to access the name of the owner, or {@code ownedSpecialization.general.name} to access the name of the element at the other end of an owned specialization). + *

+ * + * @param editingContextId the identifier of the editing context containing the type + * @param id the identifier of the type + * @param type the name of the concrete SysML type of the element + * @param label the label of the type + * @param iconURLs the URLs of the icons of the type + * @param name the name of the type + * @param shortName the short name of the type + * @param qualifiedName the qualified name of the type + * @param owner the nested entry holding information related to the type's owner + * @param ownedSpecialization the nested entries holding information related to the type owned specializations + * @param ownedElement the nested entries holding information related to the type owned elements + * + * @author gdaniel + */ +public record TypeIndexEntry( + String editingContextId, + String id, + String type, + String label, + List iconURLs, + String name, + String shortName, + String qualifiedName, + INestedIndexEntry owner, + List ownedSpecialization, + List ownedElement +) implements IIndexEntry { +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryRepresentationPublicationListener.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryRepresentationPublicationListener.java new file mode 100644 index 000000000..cac01689d --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryRepresentationPublicationListener.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * 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.publication; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; +import org.eclipse.sirius.web.application.project.services.api.IProjectEditingContextService; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.RepresentationContent; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.RepresentationMetadata; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.RepresentationCompositeIdProvider; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationContentCreationService; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationContentSearchService; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationMetadataCreationService; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationMetadataSearchService; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.events.SemanticDataCreatedEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * Creates the representations of a library once its semantic data are published. + * + * @author gdaniel + */ +@Service +public class SysONLibraryRepresentationPublicationListener { + + private final IRepresentationMetadataSearchService representationMetadataSearchService; + + private final IRepresentationMetadataCreationService representationMetadataCreationService; + + private final IRepresentationContentSearchService representationContentSearchService; + + private final IRepresentationContentCreationService representationContentCreationService; + + private final IProjectEditingContextService projectEditingContextService; + + private final Logger logger = LoggerFactory.getLogger(SysONLibraryRepresentationPublicationListener.class); + + public SysONLibraryRepresentationPublicationListener(IRepresentationMetadataSearchService representationMetadataSearchService, + IRepresentationMetadataCreationService representationMetadataCreationService, IRepresentationContentSearchService representationContentSearchService, + IRepresentationContentCreationService representationContentCreationService, + IProjectEditingContextService projectEditingContextService) { + this.representationMetadataSearchService = Objects.requireNonNull(representationMetadataSearchService); + this.representationMetadataCreationService = Objects.requireNonNull(representationMetadataCreationService); + this.representationContentSearchService = Objects.requireNonNull(representationContentSearchService); + this.representationContentCreationService = Objects.requireNonNull(representationContentCreationService); + this.projectEditingContextService = Objects.requireNonNull(projectEditingContextService); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener + public void onSemanticDataCreatedEvent(final SemanticDataCreatedEvent semanticDataCreatedEvent) { + if (semanticDataCreatedEvent.causedBy() instanceof SysONPublishedLibrarySemanticDataCreationRequested request + && request.causedBy() instanceof PublishLibrariesInput publishLibrariesInput) { + // TODO once we merge the PR in SW we should have direct access to the editing context, not the project id + Optional> optionalSemanticDataId = this.projectEditingContextService.getEditingContextId(publishLibrariesInput.projectId()) + .flatMap(id -> new UUIDParser().parse(id)) + .map(AggregateReference::to); + + if (optionalSemanticDataId.isPresent()) { + + SemanticData librarySemanticData = semanticDataCreatedEvent.semanticData(); + List representationMetadataOnSemanticData = this.representationMetadataSearchService.findAllRepresentationMetadataBySemanticData(optionalSemanticDataId.get()); + + for (RepresentationMetadata representationMetadata : representationMetadataOnSemanticData) { + + Optional optionalRepresentationContent = this.representationContentSearchService.findContentById(optionalSemanticDataId.get(), + AggregateReference.to(representationMetadata.getRepresentationMetadataId())); + + if (optionalRepresentationContent.isPresent()) { + + var duplicatedRepresentationId = UUID.randomUUID(); + var id = new RepresentationCompositeIdProvider().getId(librarySemanticData.getId(), duplicatedRepresentationId); + var duplicatedRepresentationMetadata = RepresentationMetadata.newRepresentationMetadata(id) + .representationMetadataId(duplicatedRepresentationId) + .semanticData(AggregateReference.to(librarySemanticData.getId())) + .targetObjectId(representationMetadata.getTargetObjectId()) // same target object id (ids don't change in libraries) + .descriptionId(representationMetadata.getDescriptionId()) + .label(representationMetadata.getLabel()) + .kind(representationMetadata.getKind()) + .documentation(representationMetadata.getDocumentation()) + .iconURLs(representationMetadata.getIconURLs()) + .build(semanticDataCreatedEvent); + this.representationMetadataCreationService.create(duplicatedRepresentationMetadata); + + var duplicatedContent = optionalRepresentationContent.get().getContent() + .replace(representationMetadata.getRepresentationMetadataId().toString(), duplicatedRepresentationId.toString()); + this.representationContentCreationService.create( + semanticDataCreatedEvent, + duplicatedRepresentationMetadata.getSemanticData(), + AggregateReference.to(duplicatedRepresentationMetadata.getRepresentationMetadataId()), + duplicatedContent, + optionalRepresentationContent.get().getLastMigrationPerformed(), + optionalRepresentationContent.get().getMigrationVersion() + ); + } else { + this.logger.warn("Cannot find representation content with id {}", representationMetadata.getRepresentationMetadataId()); + } + } + } else { + // TODO editingContextId once we get the SW update + this.logger.warn("Cannot find semantic datta witht id {}", publishLibrariesInput.projectId()); + } + } + } +} diff --git a/backend/application/syson-application/pom.xml b/backend/application/syson-application/pom.xml index 7af305466..5476fee91 100644 --- a/backend/application/syson-application/pom.xml +++ b/backend/application/syson-application/pom.xml @@ -185,6 +185,11 @@ postgresql test + + org.testcontainers + elasticsearch + test + org.testcontainers junit-jupiter diff --git a/backend/application/syson-application/src/main/resources/application.properties b/backend/application/syson-application/src/main/resources/application.properties index 41abb449e..1ab14f212 100644 --- a/backend/application/syson-application/src/main/resources/application.properties +++ b/backend/application/syson-application/src/main/resources/application.properties @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2023, 2024 Obeo. +# Copyright (c) 2023, 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 @@ -28,6 +28,9 @@ sirius.components.cors.allowedOriginPatterns=* sirius.components.cors.allowedCredentials=true logging.level.org.eclipse.sirius.web.diagrams.layout.LayoutService=OFF +# Prevent Elasticsearch from scanning SysON repositories +spring.data.elasticsearch.repositories.enabled=false + ################################################## # # SYSON CUSTOM IMAGES FOR IMAGES NODES diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/AbstractIntegrationTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/AbstractIntegrationTests.java index 2ab74b2cf..37cd57b3e 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/AbstractIntegrationTests.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/AbstractIntegrationTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024, 2025 Obeo. + * Copyright (c) 2024, 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 @@ -18,6 +18,7 @@ import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.elasticsearch.ElasticsearchContainer; /** * Superclass of all the integration tests used to setup the test environment. @@ -28,9 +29,17 @@ public abstract class AbstractIntegrationTests { public static final PostgreSQLContainer POSTGRESQL_CONTAINER; + public static final ElasticsearchContainer ELASTICSEARCH_CONTAINER; + + static { POSTGRESQL_CONTAINER = new PostgreSQLContainer<>("postgres:latest").withReuse(true); POSTGRESQL_CONTAINER.start(); + ELASTICSEARCH_CONTAINER = new ElasticsearchContainer("elasticsearch:9.2.1") + .withEnv("xpack.security.transport.ssl.enabled", "false") + .withEnv("xpack.security.http.ssl.enabled", "false") + .withReuse(true); + ELASTICSEARCH_CONTAINER.start(); } @DynamicPropertySource diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/ElasticsearchDynamicPropertyRegistrar.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/ElasticsearchDynamicPropertyRegistrar.java new file mode 100644 index 000000000..4e8f80fc5 --- /dev/null +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/ElasticsearchDynamicPropertyRegistrar.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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; + +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.DynamicPropertyRegistrar; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.testcontainers.elasticsearch.ElasticsearchContainer; + +/** + * Test configuration to register Elasticsearch properties for tests that require Elasticsearch. + *

+ * Use {@link SysONTestsProperties#ELASTICSEARCH_PROPERTY} in the test configuration to enable Elasticsearch. + *

+ * + * @author gdaniel + */ +@Configuration +@Conditional(OnElasticsearchEnabledTests.class) +public class ElasticsearchDynamicPropertyRegistrar implements DynamicPropertyRegistrar { + + @Override + public void accept(DynamicPropertyRegistry registry) { + registry.add("spring.elasticsearch.uris", AbstractIntegrationTests.ELASTICSEARCH_CONTAINER::getHttpHostAddress); + registry.add("spring.elasticsearch.username", () -> "elastic"); + registry.add("spring.elasticsearch.password", () -> ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD); + } +} diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/OnElasticsearchEnabledTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/OnElasticsearchEnabledTests.java new file mode 100644 index 000000000..af80951da --- /dev/null +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/OnElasticsearchEnabledTests.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * 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; + +import java.util.Arrays; + +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * Custom condition used to detect that a test requires Elasticsearch. + * + * @author gdaniel + */ +public class OnElasticsearchEnabledTests extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + var message = ConditionMessage.forCondition(this.getClass().getSimpleName()).notAvailable(SysONTestsProperties.SYSON_TEST_ENABLED); + ConditionOutcome outcome = ConditionOutcome.noMatch(message); + + var sysonTestEnabled = context.getEnvironment().getProperty(SysONTestsProperties.SYSON_TEST_ENABLED, ""); + var sysonTestEnabledFeatures = Arrays.stream(sysonTestEnabled.split(",")).map(String::trim).toList(); + + if (sysonTestEnabledFeatures.contains(SysONTestsProperties.ELASTICSEARCH)) { + message = ConditionMessage.forCondition(this.getClass().getSimpleName()).available(SysONTestsProperties.ELASTICSEARCH_PROPERTY); + outcome = ConditionOutcome.match(message); + } + + return outcome; + } + +} diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/SysONTestsProperties.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/SysONTestsProperties.java index fc7236f1d..f7897955e 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/SysONTestsProperties.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/SysONTestsProperties.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2025 Obeo. + * Copyright (c) 2025, 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 @@ -25,4 +25,8 @@ public class SysONTestsProperties { public static final String NO_DEFAULT_LIBRARIES_PROPERTY = SYSON_TEST_ENABLED + "=" + NO_DEFAULT_LIBRARIES; + public static final String ELASTICSEARCH = "elastic-search"; + + public static final String ELASTICSEARCH_PROPERTY = SYSON_TEST_ENABLED + "=" + ELASTICSEARCH; + } diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/omnibox/ProjectsOmniboxControllerIntegrationTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/omnibox/ProjectsOmniboxControllerIntegrationTests.java new file mode 100644 index 000000000..d52d7ac5d --- /dev/null +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/omnibox/ProjectsOmniboxControllerIntegrationTests.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * 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.omnibox; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.jayway.jsonpath.JsonPath; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.sirius.components.core.api.IEditingContextSearchService; +import org.eclipse.sirius.web.infrastructure.elasticsearch.services.api.IIndexUpdateService; +import org.eclipse.sirius.web.tests.graphql.ProjectsOmniboxSearchQueryRunner; +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; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; + +/** + * Integration tests of the projects omnibox controllers. + * + * @author gdaniel + */ +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { SysONTestsProperties.SYSON_TEST_ENABLED + "=" + SysONTestsProperties.NO_DEFAULT_LIBRARIES + ", " + SysONTestsProperties.ELASTICSEARCH }) +public class ProjectsOmniboxControllerIntegrationTests extends AbstractIntegrationTests { + + @Autowired + private ProjectsOmniboxSearchQueryRunner projectsOmniboxSearchQueryRunner; + + @Autowired + private IIndexUpdateService indexUpdateService; + + @Autowired + private Optional optionalElasticSearchClient; + + @Autowired + private IEditingContextSearchService editingContextSearchService; + + @Test + @DisplayName("GIVEN a query, WHEN the objects are searched in the projects omnibox, THEN the objects 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 givenQueryWhenObjectsAreSearchedInProjectsOmniboxThenObjectsAreReturned() { + assertThat(this.optionalElasticSearchClient.isPresent()); + this.editingContextSearchService.findById(SimpleProjectElementsTestProjectData.EDITING_CONTEXT_ID).ifPresent(this.indexUpdateService::updateIndex); + // Wait for Elasticsearch's refresh to ensure the indexed documents can be queried. + this.optionalElasticSearchClient.ifPresent(elasticSearchClient -> { + try { + elasticSearchClient.indices().refresh(); + } catch (IOException exception) { + fail(exception); + } + }); + + Map emptyQueryVariables = Map.of( + "query", "" + ); + + var emptyQueryResult = this.projectsOmniboxSearchQueryRunner.run(emptyQueryVariables); + List emptyQueryObjectLabels = JsonPath.read(emptyQueryResult.data(), "$.data.viewer.projectsOmniboxSearch.edges[*].node.label"); + assertThat(emptyQueryObjectLabels).isEmpty(); + + Map filterQueryVariables = Map.of( + "query", "name:Pack*" + ); + + var filterQueryResult = this.projectsOmniboxSearchQueryRunner.run(filterQueryVariables); + List filterQueryObjectLabels = JsonPath.read(filterQueryResult.data(), "$.data.viewer.projectsOmniboxSearch.edges[*].node.label"); + assertThat(filterQueryObjectLabels) + .hasSize(2) + .anySatisfy(label -> assertThat(label).contains("Package1", SimpleProjectElementsTestProjectData.PROJECT_NAME)) + .anySatisfy(label -> assertThat(label).contains("Package2", SimpleProjectElementsTestProjectData.PROJECT_NAME)); + + Map complexQueryVariables = Map.of( + "query", "@type:Part AND owner.name:Package1" + ); + + var complexQueryResult = this.projectsOmniboxSearchQueryRunner.run(complexQueryVariables); + List complexQueryObjectLabels = JsonPath.read(complexQueryResult.data(), "$.data.viewer.projectsOmniboxSearch.edges[*].node.label"); + assertThat(complexQueryObjectLabels).isNotEmpty() + .anySatisfy(label -> assertThat(label).contains("p", SimpleProjectElementsTestProjectData.PROJECT_NAME)); + } +} diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java index 9a4b823b5..73d48014c 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java @@ -901,6 +901,33 @@ public void checkConcernTest() throws IOException { this.checker.check(input, input); } + @Test + @DisplayName("GIVEN of a model with implicit referential usages, WHEN exporting that model, THEN the implicit referential element should no use the ref keyword") + // See org.eclipse.syson.application.imports.ImportSysMLModelTest.testReferentialUsages for test importing the same model + public void checkImplicitReferentialUsages() throws IOException { + var input = """ + package root { + part part1 { + part part11; + attribute attr; + in part part22; + ref part part13; + } + action a1 { + action a11; + action a12; + succession suc1 first a11 then a12; + } + use case def ucd_1 { + objective c; + subject subject_1; + actor partU_1; + } + }"""; + this.checker.check(input, input); + + } + /** * Test import/export on test file OccurrenceTest.sysml. * @@ -985,6 +1012,17 @@ public void checkReferencingWithShortName() throws IOException { this.checker.check(input, input); } + @Test + @DisplayName("GIVEN an abstract OccurrenceUsage, WHEN importing and exporting the model, THEN the abstract keyword should be correctly exported") + public void checkAbstractOccurrenceUsage() throws IOException { + var input = """ + package root { + abstract occurrence test; + abstract occurrence def Test; + }"""; + this.checker.check(input, input); + } + @Test @DisplayName("GIVEN a model with a references to elements, WHEN importing and exporting the model, THEN the references should privileged short name over declared name when possible") public void checkReferencingIdentifiers() throws IOException { diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/imports/ImportSysMLModelTest.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/imports/ImportSysMLModelTest.java index 1e0429da7..937177b71 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/imports/ImportSysMLModelTest.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/imports/ImportSysMLModelTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2025 Obeo. + * Copyright (c) 2025, 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 @@ -25,6 +25,7 @@ import java.util.Optional; import java.util.UUID; +import org.eclipse.emf.common.notify.Notifier; import org.eclipse.emf.common.util.EList; import org.eclipse.sirius.components.core.api.IEditingContext; import org.eclipse.sirius.components.core.api.IEditingContextSearchService; @@ -81,6 +82,7 @@ import org.eclipse.syson.sysml.TransitionFeatureMembership; import org.eclipse.syson.sysml.TransitionUsage; import org.eclipse.syson.sysml.Type; +import org.eclipse.syson.sysml.Usage; import org.eclipse.syson.sysml.helper.EMFUtils; import org.eclipse.syson.sysml.upload.SysMLExternalResourceLoaderService; import org.junit.jupiter.api.AfterEach; @@ -154,7 +156,71 @@ public void tearDown() { } @Test - @DisplayName("Given of model with redefinition depending on inherited memberships computation, WHEN importing the model, THEN redefined feature should resolve properly using inherited memberships") + @DisplayName("GIVEN a set of Usages, WHEN checking if the usage is referential, THEN the computation should be correct whether or not the ref keyword is present.") + public void testReferentialUsages() throws IOException { + var input = """ + package root { + // Referential because contained in package + part part1 { + part part11; // Composite because no "ref" keyword + attribute attr; // ReReferential because AttributeUsage + in part part22; // Referential because "directed" feature + ref part part13; // Referential since using "ref" keyword + } + // Referential because contained in package + action a1 { + action a11; // Composite because no "ref" keyword + action a12; // Composite because no "ref" keyword + succession suc1 first a11 then a12; // Referential because is a Connector + } + + use case def ucd_1 { + objective c; // Composite because no "ref" keyword + subject subject_1; // Ref because ReferenceUsage + actor partU_1; // Ref because in actor + } + } + """; + this.checker.checkImportedModel(resource -> { + assertThat(this.getByName(PartUsage.class, resource, "part1")) + .as("part1 should be referential since contained in a package") + .matches(Usage::isIsReference); + assertThat(this.getByName(PartUsage.class, resource, "part11")) + .as("part11 should be composite since contained in another type and not prefixed with ref") + .doesNotMatch(Usage::isIsReference); + assertThat(this.getByName(AttributeUsage.class, resource, "attr")) + .as("attr should be referential since it is a AttributeUsage") + .matches(Usage::isIsReference); + assertThat(this.getByName(PartUsage.class, resource, "part22")) + .as("part22 should be referential since it is a directed feature") + .matches(Usage::isIsReference); + assertThat(this.getByName(PartUsage.class, resource, "part13")) + .as("part13 should be referential using ref keyword") + .matches(Usage::isIsReference); + assertThat(this.getByName(ActionUsage.class, resource, "a1")) + .as("a1 should be referential since contained in a package") + .matches(Usage::isIsReference); + assertThat(this.getByName(ActionUsage.class, resource, "a11")) + .as("a11 should be composite since contained in another type and not prefixed with ref") + .doesNotMatch(Usage::isIsReference); + assertThat(this.getByName(ActionUsage.class, resource, "a12")) + .as("a12 should be composite since contained in another type and not prefixed with ref") + .doesNotMatch(Usage::isIsReference); + assertThat(this.getByName(SuccessionAsUsage.class, resource, "suc1")) + .as("suc1 should be referential since is a connector") + .matches(Usage::isIsReference); + assertThat(this.getByName(ReferenceUsage.class, resource, "subject_1")) + .as("subject_1 should be referential since ReferenceUsage") + .matches(Usage::isIsReference); + assertThat(this.getByName(PartUsage.class, resource, "partU_1")) + .as("partU_1 should be referential since stored in an ActorMembership") + .matches(Usage::isIsReference); + }); + } + + @Test + @DisplayName("GIVEN of model with redefinition depending on inherited memberships computation, WHEN importing the model, THEN redefined feature should resolve properly using inherited " + + "memberships") public void checkRedefinedFeatureToInheritedFields() throws IOException { var input = """ private import ISQBase::mass; @@ -285,7 +351,7 @@ public void checkMemberFeatureOfEndFeatureMembership() throws IOException { package pa1 { part pa1; part pa2; - + connect pa1 to pa2; } """; @@ -311,7 +377,7 @@ public void checkBasicMetadaUsage() throws IOException { attribute y : ScalarValues::String; :> annotatedElement : SysML::PartUsage; } - + #MD1 part p1; part p2 { @MD1 { @@ -322,7 +388,7 @@ public void checkBasicMetadaUsage() throws IOException { metadata MD1 about p3; part p4; metadata m1 : MD1 about p4; - + #MD2 part p5; }"""; this.checker.checkImportedModel(resource -> { @@ -375,17 +441,17 @@ public void checkBasicMetadaUsage() throws IOException { public void checkSemanticMetadataDefinition() throws IOException { var input = """ private import Metaobjects::SemanticMetadata; - + part def Functions { attribute x : ScalarValues::String; } - + part functions : Functions [*] nonunique; - + metadata def Function :> SemanticMetadata { :>> baseType = functions meta SysML::ActionUsage; } - + #Function action a0; action a1; metadata Function about a1; @@ -472,7 +538,7 @@ public void checkBindingConnectorWithFeatureChaine() throws IOException { package pk1 { action a1 { in item i1; - + action a11 { in item i11; } @@ -537,11 +603,11 @@ public void checkTextualRepresentationFeatures() throws IOException { public void checkFeatureChainExpressionNameResolution() throws IOException { var input = """ action def P1 { - + action def A2 { out pr2 : ScalarValues::Boolean; } - + action a2 : A2 { out pr2; } @@ -593,11 +659,11 @@ public void checkFeatureChainExpressionWithImplicitParameterRedefinitionNameReso part def P1 { isValid : ScalarValues::Boolean; } - + action def A2 { out prA2 : P1; } - + action a2 : A2 { out pra2; } @@ -898,7 +964,7 @@ public void checkSuccessionAsUsageWkithStartSourceTest() throws IOException { public void checkTransitionUsageWithAcceptActionUsageTest() throws IOException { var input = """ attribute def StartSignal; - + state myState { state off; accept StartSignal then on; @@ -910,9 +976,9 @@ public void checkTransitionUsageWithAcceptActionUsageTest() throws IOException { TransitionUsage transitionUsage = transitionUsages.get(0); assertThat(transitionUsage.getTriggerAction()).hasSize(1); Optional optionalTransitionFeatureMembership = transitionUsage.getOwnedRelationship().stream() - .filter(TransitionFeatureMembership.class::isInstance) - .map(TransitionFeatureMembership.class::cast) - .findFirst(); + .filter(TransitionFeatureMembership.class::isInstance) + .map(TransitionFeatureMembership.class::cast) + .findFirst(); assertThat(optionalTransitionFeatureMembership).isPresent(); assertThat(optionalTransitionFeatureMembership.get().getKind()).isEqualTo(TransitionFeatureKind.TRIGGER); }).check(input); @@ -923,7 +989,7 @@ public void checkTransitionUsageWithAcceptActionUsageTest() throws IOException { public void checkTransitionUsageWithAcceptActionUsageAndSendSignalActionTest() throws IOException { var input = """ attribute def StartSignal; - + state myState { state off; accept StartSignal @@ -1028,7 +1094,7 @@ public void checkPartUsageMultiplicityChainedFeatureReferenceExpressionBoundsTes attribute upper:ScalarValues::Integer = 2; } } - + part myPart[bounds::lower..bounds::upperBounds::upper]; """; this.checker.checkImportedModel(resource -> { @@ -1121,6 +1187,10 @@ private void checkMetadaUsage(MetadataUsage metadata, String expectedDefinitionN } } + private T getByName(Class type, Notifier root, String name) { + return EMFUtils.allContainedObjectOfType(root, type).filter(e -> name.equals(e.getDeclaredName())).findFirst().orElseThrow(); + } + private void assertStringValue(Feature f, String expectedValue) { FeatureValue valuation = new UtilService().getValuation(f); assertNotNull(valuation); diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/libraries/publication/SysONLibraryPublicationTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/libraries/publication/SysONLibraryPublicationTests.java index 7de3dda2d..21ec32cd7 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/libraries/publication/SysONLibraryPublicationTests.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/libraries/publication/SysONLibraryPublicationTests.java @@ -37,6 +37,8 @@ import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; import org.eclipse.sirius.web.domain.boundedcontexts.library.Library; import org.eclipse.sirius.web.domain.boundedcontexts.library.services.api.ILibrarySearchService; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.RepresentationMetadata; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationMetadataSearchService; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.Document; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticDataDependency; @@ -95,6 +97,9 @@ public class SysONLibraryPublicationTests extends AbstractIntegrationTests { @Autowired private ILibrarySearchService librarySearchService; + @Autowired + private IRepresentationMetadataSearchService representationMetadataSearchService; + @Autowired private PublishLibrariesMutationRunner publishLibrariesMutationRunner; @@ -176,12 +181,16 @@ public void givenProjectWhenLibraryIsPublishedThenContentOfLibraryMatchesContent TestTransaction.end(); TestTransaction.start(); - final Optional maybeLibraryEditingContext = this.librarySearchService + final Optional> maybeSemanticDataId = this.librarySearchService .findByNamespaceAndNameAndVersion(SimpleProjectElementsTestProjectData.PROJECT_ID, SimpleProjectElementsTestProjectData.PROJECT_NAME, LIBRARY_VERSION) - .map(Library::getSemanticData) - .map(AggregateReference::getId) - .map(UUID::toString) - .flatMap(this.editingContextSearchService::findById); + .map(Library::getSemanticData); + assertThat(maybeSemanticDataId).isPresent(); + + // Check that there is no representation associated to the library's semantic data + List libraryRepresentationMetadata = this.representationMetadataSearchService.findAllRepresentationMetadataBySemanticData(maybeSemanticDataId.get()); + assertThat(libraryRepresentationMetadata).isEmpty(); + + final Optional maybeLibraryEditingContext = this.editingContextSearchService.findById(maybeSemanticDataId.get().getId().toString()); assertThat(maybeLibraryEditingContext).isPresent().get().isInstanceOf(IEMFEditingContext.class); final ResourceSet libraryResourceSet = ((IEMFEditingContext) maybeLibraryEditingContext.get()).getDomain().getResourceSet(); @@ -251,6 +260,11 @@ public void givenProjectWithImportedResourceWhenLibraryIsPublishedThenLibraryDoe .flatMap(this.semanticDataSearchService::findById); assertThat(semanticData).isPresent(); + + // Check that there is no representation associated to the library's semantic data + List libraryRepresentationMetadata = this.representationMetadataSearchService.findAllRepresentationMetadataBySemanticData(AggregateReference.to(semanticData.get().getId())); + assertThat(libraryRepresentationMetadata).isEmpty(); + Set documents = semanticData.get().getDocuments(); assertThat(documents) .hasSize(1) @@ -285,6 +299,12 @@ public void givenProjectWithUsedDependencyToLibraryWhenLibraryIsPublishedThenItH assertThat(dependencyLibrary).isPresent(); assertThat(dependencyLibrary.get().getName()).isEqualTo("Batmobile"); + // Check that a representation is associated to the library's semantic data + List libraryRepresentationMetadata = this.representationMetadataSearchService.findAllRepresentationMetadataBySemanticData(AggregateReference.to(projectLibrarySemanticData.get().getId())); + assertThat(libraryRepresentationMetadata).hasSize(1) + .anySatisfy(representationMetadata -> assertThat(representationMetadata.getLabel()).isEqualTo("General View")); + + // Check that the library contains a single document (its proper content). Set documents = projectLibrarySemanticData.get().getDocuments(); assertThat(documents) @@ -316,6 +336,10 @@ public void givenProjectWithUnusedDependencyToLibraryWhenLibraryIsPublishedThenI assertThat(projectLibrarySemanticData).isPresent(); // Check that there is no dependency in the published library. assertThat(projectLibrarySemanticData.get().getDependencies()).isEmpty(); + // Check that a representation is associated to the library's semantic data + List libraryRepresentationMetadata = this.representationMetadataSearchService.findAllRepresentationMetadataBySemanticData(AggregateReference.to(projectLibrarySemanticData.get().getId())); + assertThat(libraryRepresentationMetadata).hasSize(1) + .anySatisfy(representationMetadata -> assertThat(representationMetadata.getLabel()).isEqualTo("General View")); // Check that the library contains a single document (its proper content). Set documents = projectLibrarySemanticData.get().getDocuments(); diff --git a/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/SysMLElementSerializer.java b/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/SysMLElementSerializer.java index f614eb926..338a00166 100644 --- a/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/SysMLElementSerializer.java +++ b/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/SysMLElementSerializer.java @@ -50,6 +50,7 @@ import org.eclipse.syson.sysml.ConjugatedPortDefinition; import org.eclipse.syson.sysml.ConjugatedPortTyping; import org.eclipse.syson.sysml.ConnectionUsage; +import org.eclipse.syson.sysml.Connector; import org.eclipse.syson.sysml.ConstraintUsage; import org.eclipse.syson.sysml.ControlNode; import org.eclipse.syson.sysml.DecisionNode; @@ -59,6 +60,7 @@ import org.eclipse.syson.sysml.EndFeatureMembership; import org.eclipse.syson.sysml.EnumerationDefinition; import org.eclipse.syson.sysml.EnumerationUsage; +import org.eclipse.syson.sysml.EventOccurrenceUsage; import org.eclipse.syson.sysml.Expose; import org.eclipse.syson.sysml.Expression; import org.eclipse.syson.sysml.Feature; @@ -182,9 +184,9 @@ public class SysMLElementSerializer extends SysmlSwitch { * Simple constructor. * * @param lineSeparator - * the string used to separate line + * the string used to separate line * @param indentation - * the string used to indent the file + * the string used to indent the file */ public SysMLElementSerializer(String lineSeparator, String indentation, INameDeresolver nameDeresolver, Consumer reportConsumer) { super(); @@ -748,7 +750,6 @@ public String caseObjectiveMembership(ObjectiveMembership objective) { @Override public String caseOccurrenceUsage(OccurrenceUsage occurrenceUsage) { Appender builder = new Appender(this.lineSeparator, this.indentation); - this.appendUsagePrefix(builder, occurrenceUsage); this.appendOccurrenceUsagePrefix(builder, occurrenceUsage); if (PortionKind.SNAPSHOT.equals(occurrenceUsage.getPortionKind())) { builder.appendWithSpaceIfNeeded("snapshot"); @@ -858,7 +859,7 @@ public String casePartDefinition(PartDefinition partDef) { @Override public String casePartUsage(PartUsage partUsage) { - return this.appendDefaultUsage(this.newAppender(), partUsage).toString(); + return this.appendDefaultUsage(this.newAppender(), partUsage); } @Override @@ -1267,7 +1268,7 @@ public String caseViewpointDefinition(ViewpointDefinition vp) { * Get a String representation of the "AcceptParameterPart" BNF rule to be used on {@link AcceptActionUsage}. * * @param acceptActionUsage - * a non null {@link AcceptActionUsage} + * a non null {@link AcceptActionUsage} * @return a String representation of the "AcceptParameterPart" */ public String getAcceptParameterPart(AcceptActionUsage acceptActionUsage) { @@ -1412,7 +1413,7 @@ private void appendDefinitionBody(Appender builder, Usage usage) { * Checks if the source feature define force the given {@link EndFeatureMembership} is implicit or not * * @param endFeatureMembership - * the element to test + * the element to test * @return true if the given EndFeatureMembership represent an implicit feature */ private boolean isSuccessionUsageImplicitSource(EndFeatureMembership endFeatureMembership) { @@ -2139,11 +2140,11 @@ private boolean isNotSuccessionWithSameSource(Membership m, Feature source) { * candidates {@link FeatureMembership} are filtered using a given predicate * * @param expectedFeature - * the expected feature + * the expected feature * @param sourceElement - * the source feature from which the previous elements will be searched + * the source feature from which the previous elements will be searched * @param candidatePredicate - * an optional predicate to filter among the previous elements + * an optional predicate to filter among the previous elements * @return true if the previous feature is the expected one */ private boolean isPreviousFeatureEqualsTo(Feature expectedFeature, Feature sourceElement, Predicate candidatePredicate) { @@ -2175,16 +2176,14 @@ private void reportUnhandledType(Element e) { * Returns true if the comment describes its direct owning namespace * * @param comment - * a comment + * a comment * @return true if described is direct owning namespace */ private boolean isSelfNamespaceDescribingComment(Comment comment) { EList annotatedElements = comment.getAnnotatedElement(); if (!annotatedElements.isEmpty()) { Element annotatedElement = annotatedElements.get(0); - if (annotatedElement instanceof Namespace owningNamespace && (owningNamespace == this.getDirectContainer(comment, Namespace.class))) { - return true; - } + return annotatedElement instanceof Namespace owningNamespace && (owningNamespace == this.getDirectContainer(comment, Namespace.class)); } return false; } @@ -2201,9 +2200,9 @@ private void appendAnnotatedElements(Appender builder, Comment comment, EListnull if no direct container of the expected type */ private T getDirectContainer(EObject element, Class expected) { @@ -2231,7 +2230,7 @@ private void appendLocale(Appender builder, String local) { private void appendControlNodePrefix(Appender builder, ControlNode controlNode) { final String isRef; - if (controlNode.isIsReference() && !this.isImplicitlyReferential(controlNode)) { + if (controlNode.isIsReference() && this.requireReferentialKeyword(controlNode)) { isRef = "ref"; } else { isRef = ""; @@ -2274,7 +2273,7 @@ private void appenDecisionTransition(TransitionUsage transitionUsage, Appender b if (sourceFeature != null // Skip this part for Transition with implicit source and no declared name && (!this.isPreviousFeatureEqualsTo(sourceFeature, transitionUsage, m -> this.isNotSuccessionWithSameSource(m, sourceFeature)) - || !declarionAppender.isEmpty())) { + || !declarionAppender.isEmpty())) { builder.appendWithSpaceIfNeeded("first ").append(this.getDeresolvableName(sourceFeature, transitionUsage)); } @@ -2351,9 +2350,9 @@ private void appendUsageDeclaration(Appender builder, Usage usage) { * Get a deresolvable name for a given element in a given context * * @param toDeresolve - * the object to deresolve + * the object to deresolve * @param context - * a context + * a context * @return a name */ private String getDeresolvableName(Element toDeresolve, Element context) { @@ -2436,15 +2435,6 @@ private void appendDefinitionPrefix(Appender builder, Definition def) { private void appendUsagePrefix(Appender builder, Usage usage) { this.appendBasicUsagePrefix(builder, usage); - final String isRef; - if (usage.isIsReference() && !this.isImplicitlyReferential(usage)) { - isRef = "ref"; - } else { - isRef = ""; - } - - builder.appendSpaceIfNeeded().append(isRef); - this.appendExtensionKeyword(builder, usage); } @@ -2462,10 +2452,48 @@ private void appendOccurrenceUsagePrefix(Appender builder, OccurrenceUsage occUs this.appendExtensionKeyword(builder, occUsage); } - private boolean isImplicitlyReferential(Usage usage) { - return usage.getOwningMembership() instanceof ActorMembership - || usage instanceof AttributeUsage - || usage instanceof ReferenceUsage; + /** + * Check if the given referential {@link Usage} needs to explicitly add the "ref" keyword in the textual format. + * + *

Note that this method should only be called if {@link Usage#isIsReference()} is true

+ * + * @param usage + * a non null usage + * @return {@code true} if the "ref" keyword can be omitted + */ + private boolean requireReferentialKeyword(Usage usage) { + // In some case the ref keyword can be omitted if the referential nature of the usage is implicit + return !this.isReferentialByNature(usage) && !this.isReferentialByFeature(usage) && !this.isReferentialByConstruct(usage); + } + + private boolean isReferentialByNature(Usage usage) { + // If is an AttributeUsage see SysML V2 specification : + // "AttributeUsages are also syntactically restricted by the validateAttributeUsageIsReference to be referential (non-composite)" + return usage instanceof AttributeUsage + // Is a ReferenceUsage see SysML V2 specification 7.6.4 Reference Usages: + // " However, a reference usage is always, by definition, referential. A reference usage is otherwise declared like any other usage, as given above." + || usage instanceof ReferenceUsage + // Is an EventOccurrenceUsage see cf SysML V2 8.4.5.3 Event Occurrence Usages : + // "An EventOccurrenceUsage is a kind of OccurrenceUsage that is required to always be referential" + || usage instanceof EventOccurrenceUsage + // Is a connector + || usage instanceof Connector; + } + + private boolean isReferentialByConstruct(Usage usage) { + // Feature not contained in type + // See https://groups.google.com/g/sysml-v2-release/c/BTwBJLCyozM/m/aNILfQylAQAJ + return !(usage.getOwningMembership() instanceof FeatureMembership && usage.getOwner() instanceof Type) + || usage.getOwningMembership() instanceof ActorMembership; + } + + private boolean isReferentialByFeature(Usage usage) { + // If is a directed feature see SysML v2 Section 7.6.3 Usages (Snippet 5) : + // "Note also that a directed usage is always referential, whether or not the keyword ref is also given explicitly in its declaration." + return usage.getDirection() != null + // Is end feature see SysML V2 specification 7.13.2 Connection Definitions and Usages + // "End features are always considered referential (non-composite), whether or not their declaration explicitly includes the ref keyword" + || usage.isIsEnd(); } private void appendExtensionKeyword(Appender builder, Type type) { @@ -2487,7 +2515,7 @@ private void appendPrefixMetadataMember(Appender builder, MetadataUsage metadata } } - + private void appendRequirementConstraintUsage(Appender builder, ConstraintUsage constraintUsage) { if (this.useRequirementConstraintUsageShortHandNotation(constraintUsage)) { this.appendRequirementConstraintUsageShorthandNotation(builder, constraintUsage); @@ -2567,6 +2595,9 @@ private void appendBasicUsagePrefix(Appender builder, Usage usage) { builder.appendSpaceIfNeeded(); builder.append("end"); } + if (usage.isIsReference() && this.requireReferentialKeyword(usage)) { + builder.appendWithSpaceIfNeeded(SysMLv2Keywords.REF); + } } private FeatureDirectionKind getDefaultDirection(Usage usage) { diff --git a/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/SysMLElementSerializerTest.java b/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/SysMLElementSerializerTest.java index d736e2ac5..867f9c82e 100644 --- a/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/SysMLElementSerializerTest.java +++ b/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/SysMLElementSerializerTest.java @@ -346,7 +346,7 @@ public void partUsage() { this.builder.createIn(Comment.class, partUsage).setBody(BODY); this.assertTextualFormEquals(""" - ref part PartUsage1 : 'Part Def1' { + part PartUsage1 : 'Part Def1' { /* A body */ }""", partUsage); } @@ -1402,7 +1402,9 @@ public void actionUsageWithSuccessionSimple() { this.assertTextualFormEquals("action a;", actionUsage); ActionUsage subAction1 = this.builder.createInWithName(ActionUsage.class, actionUsage, "a_1"); + subAction1.setIsComposite(true); ActionUsage subAction2 = this.builder.createInWithName(ActionUsage.class, actionUsage, "a_2"); + subAction2.setIsComposite(true); this.assertTextualFormEquals(""" action a { @@ -1457,6 +1459,7 @@ public void actionUsageWithActionDefinitionAndSuccession() { this.assertTextualFormEquals("action a : A;", actionUsage); ActionUsage subAction2 = this.builder.createInWithName(ActionUsage.class, actionUsage, "a_2"); + subAction2.setIsComposite(true); this.builder.createSuccessionAsUsage(SuccessionAsUsage.class, actionUsage, subAction1, subAction2); this.assertTextualFormEquals(""" @@ -1492,11 +1495,15 @@ public void successionUsageWithUnamedAction() { this.assertTextualFormEquals("action a;", actionUsage); ActionUsage subAction1 = this.builder.createInWithName(ActionUsage.class, actionUsage, "a_1"); + subAction1.setIsComposite(true); + this.builder.createInWithName(ActionUsage.class, actionUsage, "a_2"); ActionUsage unamedAction = this.builder.createIn(ActionUsage.class, actionUsage); + unamedAction.setIsComposite(true); this.assertTextualFormEquals(""" action a { action a_1; + ref action a_2; action; }""", actionUsage); @@ -1506,6 +1513,7 @@ public void successionUsageWithUnamedAction() { this.assertTextualFormEquals(""" action a { action a_1; + ref action a_2; action { /* This is an action with no name */ } @@ -1516,6 +1524,7 @@ public void successionUsageWithUnamedAction() { this.assertTextualFormEquals(""" action a { action a_1; + ref action a_2; action { /* This is an action with no name */ } @@ -1566,10 +1575,10 @@ public void perfomActionFullForm() { part def Camera { private import PictureTaking::*; perform action takePicture :> PictureTaking::takePicture [0..*]; - ref part focusingSubsystem { + part focusingSubsystem { perform takePicture.focus; } - ref part imagingSubsystem { + part imagingSubsystem { perform takePicture.shoot; } }""", cameraModel.getCamera()); @@ -1646,6 +1655,7 @@ public void portUsageFull() { PortUsage subPortUsage = this.builder.createInWithName(PortUsage.class, portUsage, "subPortUsage 1"); this.builder.setType(subPortUsage, portDefinition); + subPortUsage.setIsComposite(true); this.assertTextualFormEquals(""" package pack1 { @@ -1669,7 +1679,7 @@ public void documentation() { this.builder.createIn(Documentation.class, partUsage).setBody("A comment"); this.assertTextualFormEquals(""" - ref part PartUsage1 : 'Part Def1' { + part PartUsage1 : 'Part Def1' { doc /* A comment */ }""", partUsage); } @@ -1687,7 +1697,7 @@ public void documentationWithDeclaredName() { partUsage.getDocumentation().get(0).setDeclaredName(ANNOTATING1); this.assertTextualFormEquals(""" - ref part PartUsage1 : 'Part Def1' { + part PartUsage1 : 'Part Def1' { doc Annotating1 /* A body */ }""", partUsage); } diff --git a/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/models/sample/CameraModel.java b/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/models/sample/CameraModel.java index f51149914..3a1ca7a2e 100644 --- a/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/models/sample/CameraModel.java +++ b/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/models/sample/CameraModel.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024, 2025 Obeo. + * Copyright (c) 2024, 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 @@ -112,6 +112,7 @@ private void build() { this.builder.addSubsetting(this.takePicture, this.pictureTakingModel.getTakePicture()); this.focusingSubsystem = this.builder.createInWithName(PartUsage.class, this.camera, "focusingSubsystem"); + this.focusingSubsystem.setIsComposite(true); this.focusingSubsystemPerformAction = this.builder.createIn(PerformActionUsage.class, this.focusingSubsystem); @@ -120,6 +121,7 @@ private void build() { this.builder.addReferenceSubsetting(this.focusingSubsystemPerformAction, focusFeatureChain); this.imagingSubsystem = this.builder.createInWithName(PartUsage.class, this.camera, "imagingSubsystem"); + this.imagingSubsystem.setIsComposite(true); this.imagingSubsystemPerformAction = this.builder.createIn(PerformActionUsage.class, this.imagingSubsystem); diff --git a/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/models/sample/ItemTest.java b/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/models/sample/ItemTest.java index b354c8aec..04eb1e804 100644 --- a/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/models/sample/ItemTest.java +++ b/backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/textual/models/sample/ItemTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024, 2025 Obeo. + * Copyright (c) 2024, 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 @@ -101,6 +101,7 @@ private void build() { this.builder.setType(this.f, this.aDef); this.b = this.builder.createInWithName(ItemUsage.class, this.aDef, "b"); + this.b.setIsComposite(true); this.c = this.builder.createInWithName(PartUsage.class, this.aDef, "c"); this.c.getOwningMembership().setVisibility(VisibilityKind.PROTECTED); diff --git a/doc/content/modules/developer-guide/assets/images/elasticsearch-discover-view.png b/doc/content/modules/developer-guide/assets/images/elasticsearch-discover-view.png new file mode 100644 index 000000000..0aa0ecf5c Binary files /dev/null and b/doc/content/modules/developer-guide/assets/images/elasticsearch-discover-view.png differ diff --git a/doc/content/modules/developer-guide/assets/images/elasticsearch-shell.png b/doc/content/modules/developer-guide/assets/images/elasticsearch-shell.png new file mode 100644 index 000000000..b5e92208e Binary files /dev/null and b/doc/content/modules/developer-guide/assets/images/elasticsearch-shell.png differ diff --git a/doc/content/modules/developer-guide/pages/elasticsearch-integration.adoc b/doc/content/modules/developer-guide/pages/elasticsearch-integration.adoc new file mode 100644 index 000000000..8e470f061 --- /dev/null +++ b/doc/content/modules/developer-guide/pages/elasticsearch-integration.adoc @@ -0,0 +1,334 @@ += Elasticsearch integration + +include::user-manual:partial$before-you-start-experimental-all.adoc[] + +{product} can be configured to index models in Elasticsearch. +When configured, this feature allows to transparently index models created and modified by the users. +These indices can be queried through the xref:user-manual:features/cross-project-search.adoc[cross project search] to find an element regardless of its containing project. + +Let's consider that you are trying to debug an issue related to the indexing of models in Elasticsearch. +Elasticsearch comes with Kibana, a browser-based analytics and search dashboard that lets you visualize and manage your indices (among other features we won't cover here). + +== Install Elasticsearch + +You can run the following command to install a development instance of Elasticsearch on your computer. +This will install Elasticsearch (the index engine), and Kibana, a browser-based analytics and search dashboard that lets you visualize and manage your indices (among other features we won't cover here). + +[source, bash] +---- +curl -fsSL https://elastic.co/start-local | sh +---- + +If you don't want to install Elasticsearch directly on your computer, you can use the instructions below. + +Start by creating a folder for the data: + +[source, bash] +---- +mkdir -p VOLUME_PATH_ON_YOUR_COMPUTER +chmod 776 VOLUME_PATH_ON_YOUR_COMPUTER -R +---- + +Then start Elasticsearch: + +[source, bash] +---- +docker pull elasticsearch:VERSION +docker network create elastic +docker run -d --name elasticsearch -m 2g --net elastic -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -v VOLUME_PATH_ON_YOUR_COMPUTER elasticsearch:VERSION +---- + +Note the `-m 2g` parameter to limit the memory available to Elasticsearch; otherwise, at least under Linux, it tends to use a significant portion of the system's memory. +Of course, you can adjust the actual value depending on your needs, but 2G should be enough for simple local development and testing. + +Reset the password for the account `elastic`: + +[source, bash] +---- +docker exec -it elasticsearch /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic +---- + +The password will be written in the console. +Now retrieve the enrollment token for Kibana: + +[source, bash] +---- +docker exec -it elasticsearch /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana +---- + +Copy the Elasticsearch certificate from the Docker container: + +[source, bash] +---- +docker cp elasticsearch:/usr/share/elasticsearch/config/certs/http_ca.crt . +---- + +Put this certificate somewhere in your computer and use it to test the connexion to Elasticsearch + +[source, bash] +---- +curl --cacert PATH_TO_THE_CERTIFICATE/http_ca.crt -u "elastic:PASSWORD" https://localhost:9200 +---- + +Now let's start Kibana: + +[source, bash] +---- +docker pull kibana:VERSION +docker run -d --name kibana --net elastic -p 5601:5601 kibana:VERSION +---- + +Retrieve the Kibana verification code: + +[source, bash] +---- +docker exec -it kibana /usr/share/kibana/bin/kibana-verification-code +---- + +And go to `http://localhost:5601/` in a web browser to use both the enrollment token and the verification code to connect Kibana and Elasticsearch. +After all that, you may need to add the Elasticsearch certificate to the keystore of your JVM to allow Sirius Web to communicate with Elasticsearch: + +[source, bash] +---- +sudo keytool -J-Duser.language=en -import -trustcacerts -alias elasticsearch-siriusweb -keystore PATH_OF_THE_JVM/lib/security/cacerts -file PATH_TO_THE_CERTIFICATE/http_ca.crt +---- + +TIP: `keytool` will ask you for a password for the JVM's keystore. +If you never set a custom password, https://docs.oracle.com/en/java/javase/17/docs//specs/man/keytool.html[the default is `changeit`]. + +== Configure {product} to use Elasticsearch + +You can then configure your `application.properties` as follows + +[source, properties] +---- +spring.elasticsearch.uris=https://localhost:9200 +spring.elasticsearch.username=elastic +spring.elasticsearch.password= +---- + +Or add the following program arguments to your launch configuration: + +[source, bash] +---- +--spring.elasticsearch.uris=https://localhost:9200 +--spring.elasticsearch.username=elastic +--spring.elasticsearch.password= +---- + +Note that the Elasticsearch integration in Spring performs a scan of all the repositories on startup to detect Elasticsearch-compatible repositories. +This scan happens regardless of whether Elasticsearch is configured with the properties above. +{product} doesn't define such repositories, so the scan won't have an impact on the application, but it generates messages that can pollute the backend logs. +You can disable this scan with the following property: + +[source, properties] +---- +spring.data.elasticsearch.repositories.enabled=false +---- + +== How to see what is in an index + +Navigate to http://localhost:5601/ to open Kibana, open the left-side menu and select `Elasticsearch > Index Management`. + +The page displays the list of indexes currently managed by Elasticsearch. +Each {product}'s editing context (i.e. projectfootnote:[In some cases projects can have multiple editing contexts, but we won't cover it here.]) has its own index, named `editing-context-`. + +Click on an index to get high-level information (like the size of the index, the number of documents, etc). +Click on the _Discover index_ button to open the index and see the documents currently stored in it. + +image::elasticsearch-discover-view.png[Discover view in Elasticsearch] + +[NOTE] +==== +The discover view can only display 500 documents, use the fields on the left panel to filter the results, or check the next section to run queries on an index. +==== + +== How to run queries on an index + +On most of the pages you'll have access to the _Console_ at the bottom of the workbench. +Clicking on it opens a shell you can use to query your indices. + +The shell supports various types of queries, such as full-text search, a Json-based query DSL, or ES|QL. +The right panel displays the documents matching the last executed query. + +image::elasticsearch-shell.png[Elasticsearch Shell] + +For example, you can perform a full-text search on a given index with the following query: + +---- +// Get all the documents matching the term "Batmobile". +GET editing-context-/_search?q=Batmobile +---- + +You can use the query DSL to define more complex queries with a Json-based DSL, for example, you can get all the instances of of a given type that also have a name matching a given pattern: + +---- +// Get all the components with "Compo" at the beginning of their name +GET /editing-context-/_search +{ + "query": { + "bool": { + "must": [ + { + "wildcard": { + "name.keyword": "Batm*" + } + }, + { + "match": { + "@type": "Part" + } + } + ] + } + } +} +---- + +You can find more information on the https://www.elastic.co/docs/explore-analyze/query-filter/languages/querydsl[Query DSL documentation]. + + +Alternatively, you can define such complex queries with ES|QL, for example, you can get all the instances of a given type with the following query: + +---- +// Find all the instances of "Part", and show their name, id, and the index storing them. +POST /_query?format=txt +{ + "query": """ + FROM editing-context-* metadata _index + | where @type == "Part" + | keep name, @id, _index + """ +} +---- + +Note that query above uses a _wildcard_ in the name of the index it queries, meaning that all the indexes starting with "project-" are queried. + +ES|QL allows to define complex queries, including https://www.elastic.co/blog/esql-lookup-join-elasticsearch[joins]: + +---- +// Find the name of all the elements that are targeted by a ReferencingLink in a given editing context +POST /_query?format=txt +{ + "query": """ +FROM editing-context- + | where @type == "Subsetting" + // Rename because left and right columns in a join must have the same name. So we rename the column we want to lookup for. + | rename general.id.keyword as id.keyword + | lookup join editing-context- on id.keyword + | keep name + """ +} +---- + +Note that join queries do not support wildcards in their `FROM` clause. + +You can find more information on the https://www.elastic.co/docs/reference/query-languages/esql[ES|QL documentation]. + +[#data-structure] +== Data structure of indexed documents + +{product} indexes the following fields for each `Namespace` element contained in user models: + +- `@id` +- `@editingContextId` +- `@type` +- `@label` +- `@iconURLs` +- name +- shortName +- qualifiedName +- owner* +- ownedElement* +- ownedSpecialization* (only for `Type` elements, since `Namespace` elements can't have owned specializations) + +Note that some fields are prefixed with `@` to avoid clashes with specific SysML attributes we could index, but they can be used as any attribute in the queries. + +Fields with a * are _nested fields_: they contain sub-fields based on the actual element they represent. +Plain `Element` objects contain the following sub-fields: + +- `@id` +- `@type` +- `@label` +- name +- shortName +- qualifiedName + +`Specialization` objects contain the following sub-fields: + +- `@id` +- `@type` +- `@label` +- name +- shortName +- qualifiedName +- general* + +Since nested `Element` do not contain sub-fields for their own nested element, the maximum field depth of a given element is 2 for nested elements (e.g. `ownedElement.name`), and 3 for nested specializations (e.g. `ownedSpecialization.general.name`). + +The example below shows a document representing a {sysml} `Part` "part1" which is typed by a `PartDefinition`, and contains an `Attribute`. + +[source, json] +---- +{ + "@editingContextId": "13eee2c7-59b5-4ccd-a2b7-8ee5d93689eb", + "@id": "6b251d9b-d605-4ece-9870-fd43e0688a31", + "@type": "Part", + "@label": "part1", + "@iconURLs": [ + "/icons/full/obj16/PartUsage.svg" + ], + "name": "part1", + "qualifiedName": "Package1::part1", + "owner": { + "@id": "8c9499d1-c77f-435d-8d67-f26bed834c84", + "@type": "Package", + "@label": "Package1", + "name": "Package1", + "qualifiedName": "Package1" + }, + "ownedSpecialization": [ + { + "@id": "c1c2f2cc-5c4a-4a22-a6de-3188237ce4c2", + "@type": "FeatureTyping", + "@label": "FeatureTyping", + "general": { + "@id": "c010b813-dc52-4be0-b37f-9f3765f2aaa9", + "@type": "PartDefinition", + "@label": "PartDefinition1", + "name": "PartDefinition1", + "qualifiedName": "Package1::PartDefinition1" + } + }, + { + "@id": "1692b1a2-73a2-4d09-97ce-568f109a93f4", + "@type": "Subsetting", + "@label": "subsets (implicit)", + "name": "subsets (implicit)", + "qualifiedName": "'subsets (implicit)'", + "general": { + "@id": "0c6a9942-4bb9-58da-8344-57ecd220d4de", + "@type": "Part", + "@label": "parts", + "name": "parts", + "qualifiedName": "Parts::parts" + } + } + ], + "ownedElement": [ + { + "@id": "fd2f51a8-b868-41c1-95e4-818c206a2818", + "@type": "Attribute", + "@label": "attribute1", + "name": "attribute1", + "qualifiedName": "Package1::part1::attribute1" + } + ] +} +---- + + +[NOTE] +==== +{sysml} and {kerml} standard libraries are not indexed at the moment for performance reasons. +==== diff --git a/doc/content/modules/user-manual/assets/images/cross-project-search.png b/doc/content/modules/user-manual/assets/images/cross-project-search.png new file mode 100644 index 000000000..168e77341 Binary files /dev/null and b/doc/content/modules/user-manual/assets/images/cross-project-search.png differ diff --git a/doc/content/modules/user-manual/assets/images/homepage-toolbar.png b/doc/content/modules/user-manual/assets/images/homepage-toolbar.png index e74d0be8c..27dc3ec51 100644 Binary files a/doc/content/modules/user-manual/assets/images/homepage-toolbar.png and b/doc/content/modules/user-manual/assets/images/homepage-toolbar.png differ diff --git a/doc/content/modules/user-manual/assets/images/homepage.png b/doc/content/modules/user-manual/assets/images/homepage.png index 49dd28fb1..167d4acda 100644 Binary files a/doc/content/modules/user-manual/assets/images/homepage.png and b/doc/content/modules/user-manual/assets/images/homepage.png differ diff --git a/doc/content/modules/user-manual/pages/features/cross-project-search.adoc b/doc/content/modules/user-manual/pages/features/cross-project-search.adoc new file mode 100644 index 000000000..3c0b62d30 --- /dev/null +++ b/doc/content/modules/user-manual/pages/features/cross-project-search.adoc @@ -0,0 +1,37 @@ += Cross project search + +include::user-manual:partial$before-you-start-experimental-all.adoc[] + +[IMPORTANT] +==== +This feature requires {product} to be configured to xref:developer-guide:elasticsearch-integration.adoc[work with Elasticsearch]. +==== + +The _cross project search_ is a feature accessible in the Projects browser's command palette that allows to search for elements in any {product} project. +It is based on https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-query-string-query#query-string-syntax[Elasticsearch's query string], which allows to find elements by matching its indexed fields. + +image:cross-project-search.png[Cross project search dialog] + +{product} stores each element of a {sysml} model following a xref:developer-guide:elasticsearch-integration.adoc#data-structure[data structure]. +The fields defined in this data structure can be used to match elements in the cross project search, for example: + +---- +// Get all the elements with a name starting with "Batmo" +name:Batmo* + +// Get all the Part elements with a name starting with "part" followed by a digit +name:/part<0-9>/ AND @type:Part + +// Get all the elements owned by "Package1" +owner.name:Package1 + +// Get all the elements typed by "PartDefinition1" +ownedSpecialization.@type:FeatureTyping AND ownedSpecialization.general.name:PartDefinition1 +---- + +Note that the data structure only allows to query nested fields on 2 levels for `Element` instances (e.g. `owner.name`), and 3 levels for `Specialization` instances (e.g. `ownedSpecialization.general.name`). + +[NOTE] +==== +{sysml} and {kerml} standard libraries are not indexed at the moment for performance reasons. +==== \ No newline at end of file diff --git a/doc/content/modules/user-manual/pages/features/homepage.adoc b/doc/content/modules/user-manual/pages/features/homepage.adoc index 2ac85f7b2..bbe799def 100644 --- a/doc/content/modules/user-manual/pages/features/homepage.adoc +++ b/doc/content/modules/user-manual/pages/features/homepage.adoc @@ -20,12 +20,18 @@ image::homepage.png[{homepage}] === Toolbar -The toolbar consists of two groups: _Homepage_ and _Links_. +The toolbar consists of three groups: _Homepage_, _Command Palette_, and _Links_. image::homepage-toolbar.png[{homepage} Toolbar] include::user-manual:partial$homepage-action.adoc[leveloffset=+3] +[#command-palette] +==== Command Palette + +The _Command Palette_ in the {homepage} offers commands to perform actions on projects. +For example, it provides a command to create a _blank project_, or a xref:user-manual:features/cross-project-search.adoc[command to search elements across projects]. + include::user-manual:partial$projects-action.adoc[leveloffset=+3] include::user-manual:partial$libraries-action.adoc[leveloffset=+3] diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.1.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.1.0.adoc index 07af3faaf..a39929ef1 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2026.1.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2026.1.0.adoc @@ -1,4 +1,4 @@ -= 2026.1.0 (work in progress) += 2026.1.0 == Key highlights @@ -88,7 +88,6 @@ package root { ** New `Expose` elements now have `visibility` set to _protected_ and have `isImportAll` set to _true_, as required by the SysMLv2 specification. - == Improvements * In diagrams: diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.3.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.3.0.adoc new file mode 100644 index 000000000..454e84c02 --- /dev/null +++ b/doc/content/modules/user-manual/pages/release-notes/2026.3.0.adoc @@ -0,0 +1,28 @@ += 2026.3.0 (work in progress) + +== Key highlights + +== New features + +- SysON can now be connected with Elasticsearch to support cross-project search. +The Elasticsearch integration uses its own indexing logic instead of the default one provided by Sirius Web. +This allows to keep indices compact, and ensures information stored in the indices are useful to perform cross-project search. +You can find more information on how to setup Elasticsearch, how elements are mapped to index documents, and how to query them in the documentation. + +This feature is currently considered experimental. +Try it out and give feedback by reporting bugs and suggesting new features. +It's not recommended for production use. + +== Bug fixes + +* Fix the textual export of `OccurrenceUsage` to avoid duplication of the _abstract_ keyword. + +== Improvements + +* Add support for publishing representations with libraries. +Publishing a project that contains representations now produces libraries that also contain the representations. +Users can navigate to `/libraries/` to open a library and see its representations. + +== Technical details + +* For technical details on this {product} release (including breaking changes), please refer to https://github.com/eclipse-syson/syson/blob/main/CHANGELOG.adoc[changelog]. \ No newline at end of file diff --git a/doc/content/modules/user-manual/pages/release-notes/release-notes.adoc b/doc/content/modules/user-manual/pages/release-notes/release-notes.adoc index 04e9a4179..0f54b9cd5 100644 --- a/doc/content/modules/user-manual/pages/release-notes/release-notes.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/release-notes.adoc @@ -2,6 +2,7 @@ :sectnums!: +include::user-manual:release-notes/2026.3.0.adoc[leveloffset=+1] include::user-manual:release-notes/2026.1.0.adoc[leveloffset=+1] include::user-manual:release-notes/2025.12.0.adoc[leveloffset=+1] include::user-manual:release-notes/2025.10.0.adoc[leveloffset=+1] diff --git a/doc/content/modules/user-manual/partials/nav-features.adoc b/doc/content/modules/user-manual/partials/nav-features.adoc index 937f62f81..95039da14 100644 --- a/doc/content/modules/user-manual/partials/nav-features.adoc +++ b/doc/content/modules/user-manual/partials/nav-features.adoc @@ -8,6 +8,7 @@ * Diagramming Tools ** Projects Management *** xref:user-manual:features/homepage.adoc[] +*** xref:user-manual:features/cross-project-search.adoc[] ** Editor *** xref:user-manual:features/editor.adoc[] *** xref:user-manual:features/details.adoc[] @@ -42,4 +43,5 @@ ** xref:user-manual:features/scaling-limits.adoc[] ** xref:developer-guide:extend.adoc[] ** xref:user-manual:features/security.adoc[] +** xref:developer-guide:elasticsearch-integration.adoc[] diff --git a/doc/docs-site/antora-playbook.yml b/doc/docs-site/antora-playbook.yml index 97d6cce46..6fd3369eb 100644 --- a/doc/docs-site/antora-playbook.yml +++ b/doc/docs-site/antora-playbook.yml @@ -1,6 +1,6 @@ site: title: SysON Docs - start_page: v2025.12.0@syson::index.adoc + start_page: v2026.1.0@syson::index.adoc output: clean: true diff --git a/doc/shapes/2026.6.1/customize_elasticsearch_index_to_work_with_sysml.adoc b/doc/shapes/2026.6.1/customize_elasticsearch_index_to_work_with_sysml.adoc index 0c7110628..16f2b49fa 100644 --- a/doc/shapes/2026.6.1/customize_elasticsearch_index_to_work_with_sysml.adoc +++ b/doc/shapes/2026.6.1/customize_elasticsearch_index_to_work_with_sysml.adoc @@ -31,6 +31,7 @@ All the `Element` instances are stored in the index. The `name`, `shortName`, and `qualifiedName` attributes can be used to find elements. The following references can be navigated as part of the query: + - `Element#owner` - `Element#ownedElement` - `Type#ownedSpecialization` @@ -40,6 +41,7 @@ Navigation has a maximum depth of 1: for example, it is possible to access the o This limitation is necessary to limit the number of fields stored in the index (by default the maximum number of fields is 1000), and to avoid false positive matches on unbounded queries (e.g. an indexed document can match a term if its n-th owner's name contains the term). In addition, Sirius Web provides the following attributes that can be used to find elements: + - `@id`: the ID of the element - `@editingContextId`: the ID of the editing context containing the element (this is a technical field, it is usually not useful to write queries) - `@type`: the type of the element (by default its EClass)