diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 285883e23..86ffb4b53 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -97,6 +97,7 @@ A new compartment named _satisfy requirements_ has also been added to `PartDefin - https://github.com/eclipse-syson/syson/issues/1818[#1818] [diagrams] On _Interconnection View_ diagrams, the top-level palette no longer proposes to create `Attributes` and `Ports`, which can not actually be displayed on the diagram's background. The tools are still available in the palette of e.g. `Parts`. - https://github.com/eclipse-syson/syson/issues/1819[#1819] [diagrams] In diagrams, when using _direct edit_ tool, you can now set a _multiplicity range part_ [n..n] after or before a _typing part_, _subsetting part_ or _redefinition part_. +- https://github.com/eclipse-syson/syson/issues/1861[#1861] [publication] Split SysONLibraryPublicationHandler in 2 so the publishing logic can be extended or re-used through the ISysMLLibraryPublisher API. === New features diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryPublicationHandler.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryPublicationHandler.java index f2ed57734..502883539 100644 --- a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryPublicationHandler.java +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryPublicationHandler.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 @@ -12,89 +12,48 @@ *******************************************************************************/ package org.eclipse.syson.application.publication; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; -import org.eclipse.emf.ecore.EObject; -import org.eclipse.emf.ecore.resource.Resource; -import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.sirius.components.core.api.ErrorPayload; import org.eclipse.sirius.components.core.api.IEditingContextSearchService; import org.eclipse.sirius.components.core.api.IPayload; -import org.eclipse.sirius.components.core.api.SuccessPayload; -import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; -import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; -import org.eclipse.sirius.components.events.ICause; import org.eclipse.sirius.components.representations.Message; import org.eclipse.sirius.components.representations.MessageLevel; -import org.eclipse.sirius.web.application.editingcontext.services.DocumentData; -import org.eclipse.sirius.web.application.editingcontext.services.EPackageEntry; -import org.eclipse.sirius.web.application.editingcontext.services.api.IResourceToDocumentService; import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; -import org.eclipse.sirius.web.application.library.services.LibraryMetadataAdapter; +import org.eclipse.sirius.web.application.library.services.api.ILibraryApplicationService; import org.eclipse.sirius.web.application.library.services.api.ILibraryPublicationHandler; -import org.eclipse.sirius.web.application.studio.services.library.api.DependencyGraph; -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.project.Project; import org.eclipse.sirius.web.domain.boundedcontexts.project.services.api.IProjectSearchService; import org.eclipse.sirius.web.domain.boundedcontexts.projectsemanticdata.services.api.IProjectSemanticDataSearchService; -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.services.api.ISemanticDataCreationService; -import org.eclipse.sirius.web.domain.services.IResult; -import org.eclipse.sirius.web.domain.services.Success; -import org.eclipse.syson.application.publication.api.ISysONLibraryDependencyCollector; -import org.eclipse.syson.sysml.SysmlPackage; -import org.eclipse.syson.sysml.util.ElementUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.syson.application.publication.api.ISysMLLibraryPublisher; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.stereotype.Service; /** * {@link ILibraryPublicationHandler} for publishing libraries in SysON. * + * @see ILibraryApplicationService * @see SysONLibraryPublicationListener * @author flatombe */ @Service public class SysONLibraryPublicationHandler implements ILibraryPublicationHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(SysONLibraryPublicationHandler.class); - private final IEditingContextSearchService editingContextSearchService; - private final ISemanticDataCreationService semanticDataCreationService; - - private final IResourceToDocumentService resourceToDocumentService; - private final IProjectSemanticDataSearchService projectSemanticDataSearchService; private final IProjectSearchService projectSearchService; - private final ILibrarySearchService librarySearchService; - - private final ISysONLibraryDependencyCollector sysonLibraryDependencyCollector; + private final ISysMLLibraryPublisher sysONSysMLLibraryPublisher; public SysONLibraryPublicationHandler(final IEditingContextSearchService editingContextSearchService, - final ISemanticDataCreationService semanticDataCreationService, - final IResourceToDocumentService resourceToDocumentService, - final IProjectSemanticDataSearchService projectSemanticDataSearchService, final IProjectSearchService projectSearchService, - final ILibrarySearchService librarySearchService, - final ISysONLibraryDependencyCollector sysonLibraryDependencyCollector) { - this.projectSearchService = Objects.requireNonNull(projectSearchService); + final IProjectSemanticDataSearchService projectSemanticDataSearchService, + final IProjectSearchService projectSearchService, + final ISysMLLibraryPublisher sysONSysMLLibraryPublisher) { this.editingContextSearchService = Objects.requireNonNull(editingContextSearchService); - this.semanticDataCreationService = Objects.requireNonNull(semanticDataCreationService); - this.resourceToDocumentService = Objects.requireNonNull(resourceToDocumentService); this.projectSemanticDataSearchService = Objects.requireNonNull(projectSemanticDataSearchService); - this.librarySearchService = Objects.requireNonNull(librarySearchService); - this.sysonLibraryDependencyCollector = Objects.requireNonNull(sysonLibraryDependencyCollector); + this.projectSearchService = Objects.requireNonNull(projectSearchService); + this.sysONSysMLLibraryPublisher = Objects.requireNonNull(sysONSysMLLibraryPublisher); } @Override @@ -107,147 +66,14 @@ public IPayload handle(final PublishLibrariesInput input) { return this.projectSearchService.findById(input.projectId()) .map(project -> this.projectSemanticDataSearchService.findByProjectId(AggregateReference.to(input.projectId())) .flatMap(projectSemanticData -> this.editingContextSearchService.findById(projectSemanticData.getSemanticData().getId().toString())) - .filter(IEMFEditingContext.class::isInstance) - .map(IEMFEditingContext.class::cast) - .map(editingContext -> this.handle(input, project, editingContext.getDomain().getResourceSet())) - .orElseGet(() -> new ErrorPayload(input.id(), - List.of(new Message("Could not find the editing context of project '%s'.".formatted(project.getName()), MessageLevel.ERROR))))) + .map(editingContext -> this.sysONSysMLLibraryPublisher.publish( + input, + editingContext, + project.getId(), + project.getName(), + input.version(), + input.description())) + .orElseGet(() -> new ErrorPayload(input.id(), List.of(new Message("Could not find the editing context of project '%s'.".formatted(project.getName()), MessageLevel.ERROR))))) .orElseGet(() -> new ErrorPayload(input.id(), List.of(new Message("Could not find project with ID '%s'.".formatted(input.projectId()), MessageLevel.ERROR)))); } - - protected IPayload handle(final PublishLibrariesInput input, final Project project, final ResourceSet resourceSet) { - final IPayload result; - - final String libraryName = project.getName(); - final String libraryVersion = input.version(); - - if (this.librarySearchService.findByNamespaceAndNameAndVersion(project.getId(), libraryName, libraryVersion).isPresent()) { - // There is already a Library from our namespace with that name and version. - result = new ErrorPayload(input.id(), - List.of(new Message("Library '%s' (version '%s') already exists in namespace '%s'.".formatted(libraryName, libraryVersion, project.getId()), MessageLevel.ERROR))); - } else { - final Set resourcesToPublish = this.getProperSysMLRootContents(resourceSet); - if (!resourcesToPublish.isEmpty()) { - DependencyGraph dependencyGraph = this.sysonLibraryDependencyCollector.collectDependencies(resourceSet); - - List> dependencies = this.getDependencies(dependencyGraph, resourcesToPublish); - - final Optional maybePublishedLibrarySemanticData = this.publishAsLibrary(input, resourcesToPublish, libraryName, libraryVersion, dependencies); - // After this transaction is done, SysONLibraryPublicationListener reacts by also creating the - // associated Library metadata. - - result = maybePublishedLibrarySemanticData - .map(publishedLibrary -> (IPayload) new SuccessPayload(input.id(), - List.of(new Message( - "Successfully published the SysML contents of project '%s' as version '%s' of library '%s'.".formatted(project.getName(), - libraryVersion, - libraryName), - MessageLevel.SUCCESS)))) - .orElseGet( - () -> new ErrorPayload(input.id(), - List.of(new Message("Failed to publish the SysML contents of project '%s' as a library.".formatted(project.getName(), project.getId()), - MessageLevel.ERROR)))); - } else { - result = new ErrorPayload(input.id(), - List.of(new Message("There are no SysML contents in project '%s' to publish as library.".formatted(project.getName()), MessageLevel.ERROR))); - } - } - return result; - } - - private List> getDependencies(DependencyGraph dependencyGraph, final Set resourcesToPublish) { - List> dependencies = new ArrayList<>(); - - for (Resource resourceToPublish : resourcesToPublish) { - for (Resource dependencyCandidate : dependencyGraph.getDependencies(resourceToPublish)) { - Optional optionalLibraryMetadata = dependencyCandidate.eAdapters().stream() - .filter(LibraryMetadataAdapter.class::isInstance) - .map(LibraryMetadataAdapter.class::cast) - .findFirst(); - if (optionalLibraryMetadata.isPresent()) { - LibraryMetadataAdapter libraryMetadataAdapter = optionalLibraryMetadata.get(); - this.librarySearchService.findByNamespaceAndNameAndVersion(libraryMetadataAdapter.getNamespace(), libraryMetadataAdapter.getName(), libraryMetadataAdapter.getVersion()) - .map(Library::getSemanticData) - .ifPresentOrElse(dependencies::add, () -> LOGGER.warn("Cannot retrieve library {}:{}:{}", libraryMetadataAdapter.getNamespace(), libraryMetadataAdapter.getName(), - libraryMetadataAdapter.getVersion())); - } - // Ignore the resource if it isn't a library: all non-library resources are published in a single - // library in SysON. - } - } - return dependencies.stream() - .distinct() - .toList(); - } - - protected Optional publishAsLibrary(final ICause parentCause, final Set resources, final String name, final String version, - final List> dependencies) { - final ICause cause = new SysONPublishedLibrarySemanticDataCreationRequested(parentCause, name); - // Remove the imported flag on the resource: the resource is now a library, and its read-only/read-write nature - // should be determined by its import kind (reference or copy), and not whether it was imported from a textual - // SysML file in the first place. - resources.forEach(resource -> ElementUtil.setIsImported(resource, false)); - return this.createSemanticData(cause, resources, dependencies); - } - - protected Optional createSemanticData(final ICause event, final Collection resources, final List> dependencies) { - final List documentDatas = resources.stream() - .map(resource -> this.resourceToDocumentService.toDocument(resource, false)) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - final List documents = documentDatas.stream() - .map(DocumentData::document) - .toList(); - final List domains = documentDatas.stream() - .map(DocumentData::ePackageEntries) - .flatMap(List::stream) - .map(EPackageEntry::nsURI) - .distinct() - .toList(); - - final IResult creationResult = this.semanticDataCreationService.create(event, documents, domains, dependencies); - if (creationResult instanceof Success creationSuccess) { - return Optional.ofNullable(creationSuccess.data()); - } else { - return Optional.empty(); - } - } - - protected Set getProperSysMLRootContents(final ResourceSet resourceSet) { - Objects.requireNonNull(resourceSet); - - final List dependencies = this.getDependenciesToPublishedLibraries(resourceSet); - return resourceSet.getResources().stream() - .filter(resource -> !dependencies.contains(resource)) - .collect(Collectors.toSet()); - } - - protected List getDependenciesToPublishedLibraries(ResourceSet resourceSet) { - return resourceSet.getResources().stream() - .filter(resource -> this.getLibraryMetadata(resource).isPresent() - || resource.getURI().scheme().equals(ElementUtil.KERML_LIBRARY_SCHEME) - || resource.getURI().scheme().equals(ElementUtil.SYSML_LIBRARY_SCHEME)) - .toList(); - } - - protected Optional getLibraryMetadata(final Resource resource) { - return resource.eAdapters().stream() - .filter(LibraryMetadataAdapter.class::isInstance) - .map(LibraryMetadataAdapter.class::cast) - .findFirst(); - } - - protected boolean isSysmlContent(final EObject rootEObject) { - return rootEObject.eClass().getEPackage() == SysmlPackage.eINSTANCE; - } - - protected String getResourceName(final Resource resource) { - return resource.eAdapters().stream() - .filter(ResourceMetadataAdapter.class::isInstance) - .map(ResourceMetadataAdapter.class::cast) - .map(ResourceMetadataAdapter::getName) - .findFirst() - .orElse(null); - } } diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryPublicationListener.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryPublicationListener.java index 10de51a24..ae0dfbb18 100644 --- a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryPublicationListener.java +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONLibraryPublicationListener.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 @@ -14,7 +14,6 @@ import java.util.Objects; -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.ILibraryCreationService; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; @@ -42,16 +41,15 @@ public SysONLibraryPublicationListener(final ILibraryCreationService libraryCrea @Transactional(propagation = Propagation.REQUIRES_NEW) @TransactionalEventListener public void onSemanticDataCreatedEvent(final SemanticDataCreatedEvent semanticDataCreatedEvent) { - if (semanticDataCreatedEvent.causedBy() instanceof SysONPublishedLibrarySemanticDataCreationRequested request - && request.causedBy() instanceof PublishLibrariesInput publishLibrariesInput) { + if (semanticDataCreatedEvent.causedBy() instanceof SysONPublishedLibrarySemanticDataCreationRequested request) { final SemanticData createdSemanticData = semanticDataCreatedEvent.semanticData(); final Library createdLibrary = Library.newLibrary() - .namespace(publishLibrariesInput.projectId()) + .namespace(request.libraryNamespace()) .name(request.libraryName()) + .version(request.libraryVersion()) + .description(request.libraryDescription()) .semanticData(AggregateReference.to(createdSemanticData.getId())) - .version(publishLibrariesInput.version()) - .description(publishLibrariesInput.description()) .build(semanticDataCreatedEvent); this.libraryCreationService.createLibrary(createdLibrary); } diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONPublishedLibrarySemanticDataCreationRequested.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONPublishedLibrarySemanticDataCreationRequested.java index bd330cb2b..b62f1241d 100644 --- a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONPublishedLibrarySemanticDataCreationRequested.java +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONPublishedLibrarySemanticDataCreationRequested.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 @@ -27,16 +27,23 @@ public record SysONPublishedLibrarySemanticDataCreationRequested( UUID id, ICause causedBy, - String libraryName) implements ICause { + String libraryNamespace, + String libraryName, + String libraryVersion, + String libraryDescription) implements ICause { - public SysONPublishedLibrarySemanticDataCreationRequested(final ICause cause, final String libraryName) { - this(UUID.randomUUID(), cause, libraryName); + public SysONPublishedLibrarySemanticDataCreationRequested(final ICause cause, final String libraryNamespace, final String libraryName, final String libraryVersion, + final String libraryDescription) { + this(UUID.randomUUID(), cause, libraryNamespace, libraryName, libraryVersion, libraryDescription); } public SysONPublishedLibrarySemanticDataCreationRequested { Objects.requireNonNull(id); Objects.requireNonNull(causedBy); + Objects.requireNonNull(libraryNamespace); Objects.requireNonNull(libraryName); + Objects.requireNonNull(libraryVersion); + Objects.requireNonNull(libraryDescription); } } diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONSysMLLibraryPublisher.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONSysMLLibraryPublisher.java new file mode 100644 index 000000000..bd5b0a317 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/SysONSysMLLibraryPublisher.java @@ -0,0 +1,243 @@ +/******************************************************************************* + * 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.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.core.api.SuccessPayload; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.components.events.ICause; +import org.eclipse.sirius.components.representations.Message; +import org.eclipse.sirius.components.representations.MessageLevel; +import org.eclipse.sirius.web.application.editingcontext.services.DocumentData; +import org.eclipse.sirius.web.application.editingcontext.services.EPackageEntry; +import org.eclipse.sirius.web.application.editingcontext.services.api.IResourceToDocumentService; +import org.eclipse.sirius.web.application.library.services.LibraryMetadataAdapter; +import org.eclipse.sirius.web.application.studio.services.library.api.DependencyGraph; +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.semanticdata.Document; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services.api.ISemanticDataCreationService; +import org.eclipse.sirius.web.domain.services.IResult; +import org.eclipse.sirius.web.domain.services.Success; +import org.eclipse.syson.application.omnibox.api.IPredicateCanEditingContextPublishSysMLProject; +import org.eclipse.syson.application.publication.api.ISysMLLibraryPublisher; +import org.eclipse.syson.application.publication.api.ISysONLibraryDependencyCollector; +import org.eclipse.syson.application.services.SysONResourceService; +import org.eclipse.syson.services.api.ISysONResourceService; +import org.eclipse.syson.sysml.util.ElementUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.stereotype.Service; + +/** + * SysON implementation of {@link ISysMLLibraryPublisher}. + *

+ * It publishes as one library all the {@link Resource resources} of the given {@link ResourceSet} matching the + * following criteria: + *

    + *
  • The {@link Resource} is not from a library + * ({@link SysONResourceService#isFromReferencedLibrary(IEditingContext, Resource) published} or + * {@link ElementUtil#isStandardLibraryResource(Resource) standard})
  • + *
  • The {@link Resource} is {@link SysONResourceService#isSysML(Resource) identified as a SysML resource}
  • + *
+ * Note: Attempting to publish a library that already exists (exact same {@code namespace}, {@code name} and + * {@code version}) will result in an error. + * + * @author flatombe + */ +@Service +public class SysONSysMLLibraryPublisher implements ISysMLLibraryPublisher { + private static final Logger LOGGER = LoggerFactory.getLogger(SysONSysMLLibraryPublisher.class); + + private final ILibrarySearchService librarySearchService; + + private final IPredicateCanEditingContextPublishSysMLProject predicateCanEditingContextPublishSysMLProject; + + private final IResourceToDocumentService resourceToDocumentService; + + private final ISemanticDataCreationService semanticDataCreationService; + + private final ISysONLibraryDependencyCollector sysONLibraryDependencyCollector; + + private final ISysONResourceService sysONResourceService; + + public SysONSysMLLibraryPublisher(final ILibrarySearchService librarySearchService, final IPredicateCanEditingContextPublishSysMLProject predicateCanEditingContextPublishSysMLProject, + final IResourceToDocumentService resourceToDocumentService, final ISemanticDataCreationService semanticDataCreationService, + final ISysONLibraryDependencyCollector sysonLibraryDependencyCollector, final ISysONResourceService sysONResourceService) { + this.librarySearchService = Objects.requireNonNull(librarySearchService); + this.predicateCanEditingContextPublishSysMLProject = Objects.requireNonNull(predicateCanEditingContextPublishSysMLProject); + this.resourceToDocumentService = Objects.requireNonNull(resourceToDocumentService); + this.semanticDataCreationService = Objects.requireNonNull(semanticDataCreationService); + this.sysONLibraryDependencyCollector = Objects.requireNonNull(sysonLibraryDependencyCollector); + this.sysONResourceService = Objects.requireNonNull(sysONResourceService); + } + + @Override + public IPayload publish(final ICause cause, final IEditingContext libraryAuthoringEditingContext, final String libraryNamespace, final String libraryName, + final String libraryVersion, final String libraryDescription) { + final IPayload result; + + if (!this.predicateCanEditingContextPublishSysMLProject.test(libraryAuthoringEditingContext.getId())) { + result = new ErrorPayload(cause.id(), + List.of(new Message("Cannot publish SysML library from editing context '%s'.".formatted(libraryAuthoringEditingContext.getId()), MessageLevel.ERROR))); + } else if (this.librarySearchService.findByNamespaceAndNameAndVersion(libraryNamespace, libraryName, libraryVersion).isPresent()) { + // The Sirius Web lifecycle for published libraries relies on their immutability, so we want to prevent the + // publication from happening. + result = new ErrorPayload(cause.id(), + List.of(new Message("Library '%s:%s@%s' already exists.".formatted(libraryNamespace, libraryName, libraryVersion), MessageLevel.ERROR))); + } else if (!(libraryAuthoringEditingContext instanceof IEMFEditingContext)) { + result = new ErrorPayload(cause.id(), + List.of(new Message( + "Editing context '%s' is of an unsupported type: %s".formatted(libraryAuthoringEditingContext.getId(), libraryAuthoringEditingContext.getClass().getCanonicalName()), + MessageLevel.ERROR))); + } else { + result = this.doPublish(cause, (IEMFEditingContext) libraryAuthoringEditingContext, libraryNamespace, libraryName, libraryVersion, libraryDescription); + } + return result; + } + + /** + * Publishes all the proper SysML contents of an editing context as a single library with the specified identity + * (namespace, name and version) and description. + * + * @param cause + * the (non-{@code null}) originating {@link ICause}. + * @param emfEditingContext + * the (non-{@code null}) {@link IEMFEditingContext} that authors the library. + * @param libraryNamespace + * the (non-{@code null}) desired {@link Library#getNamespace() namespace} for the library to publish. + * @param libraryName + * the (non-{@code null}) desired {@link Library#getName() name} for the library to publish. + * @param libraryVersion + * the (non-{@code null}) desired {@link Library#getVersion() version} for the library to publish. + * @param libraryDescription + * the (non-{@code null}) desired {@link Library#getDescription() description} for the library to + * publish. + * @return the (non-{@code null}) resulting {@link IPayload}. + */ + protected IPayload doPublish(final ICause cause, final IEMFEditingContext emfEditingContext, final String libraryNamespace, final String libraryName, + final String libraryVersion, final String libraryDescription) { + final Set resourcesToPublish = this.getResourcesToPublish(emfEditingContext); + final DependencyGraph dependencyGraph = this.sysONLibraryDependencyCollector.collectDependencies(emfEditingContext.getDomain().getResourceSet()); + + final List> dependencies = this.getDependencies(dependencyGraph, resourcesToPublish); + + final Optional maybePublishedLibrarySemanticData = this.publishAsLibrary(cause, resourcesToPublish, libraryNamespace, libraryName, libraryVersion, libraryDescription, + dependencies); + // After this transaction is done, SysONLibraryPublicationListener reacts by also creating the + // associated Library metadata. + + return maybePublishedLibrarySemanticData + .map(publishedLibrarySemanticData -> (IPayload) new SuccessPayload(cause.id(), + List.of(new Message("Successfully published library '%s:%s@%s'.".formatted(libraryNamespace, libraryName, libraryVersion), MessageLevel.SUCCESS)))) + .orElseGet( + () -> new ErrorPayload(cause.id(), + List.of(new Message("Failed to publish library '%s:%s@%s'.".formatted(libraryNamespace, libraryName, libraryVersion), MessageLevel.ERROR)))); + } + + protected List> getDependencies(final DependencyGraph dependencyGraph, final Set resourcesToPublish) { + final List> dependencies = new ArrayList<>(); + + for (final Resource resourceToPublish : resourcesToPublish) { + for (final Resource dependencyCandidate : dependencyGraph.getDependencies(resourceToPublish)) { + // This Resource is a "dependency" in the EMF sense of the word. + // It may be a proper Resource of the ResourceSet, or a Resource present in the ResourceSet because it + // originally belongs to a published library (a dependency in the Sirius Web sense of the word). + // Since this implementation publishes all proper Resources of the ResourceSet into a single library, we + // only need to look for the Resources which come from the published libraries we have in dependencies. + this.getLibraryMetadata(dependencyCandidate).ifPresent(libraryMetadataAdapter -> { + this.librarySearchService.findByNamespaceAndNameAndVersion( + libraryMetadataAdapter.getNamespace(), + libraryMetadataAdapter.getName(), + libraryMetadataAdapter.getVersion()) + .map(Library::getSemanticData) + .ifPresentOrElse(dependencies::add, + () -> LOGGER.warn("Cannot retrieve contents of library '%s:%s@%s'" + .formatted(libraryMetadataAdapter.getNamespace(), libraryMetadataAdapter.getName(), + libraryMetadataAdapter.getVersion()))); + }); + } + } + return dependencies.stream() + .distinct() + .toList(); + } + + protected Optional publishAsLibrary(final ICause parentCause, final Collection resources, final String libraryNamespace, final String libraryName, + final String libraryVersion, final String libraryDescription, final List> dependencies) { + // Remove the imported flag on the resource: the resource is now a library, and its read-only/read-write nature + // should be determined by its import kind (reference or copy), and not whether it was imported from a textual + // SysML file in the first place. + resources.forEach(resource -> ElementUtil.setIsImported(resource, false)); + + final ICause cause = new SysONPublishedLibrarySemanticDataCreationRequested(parentCause, libraryNamespace, libraryName, libraryVersion, libraryDescription); + return this.createSemanticData(cause, resources, dependencies); + } + + protected Optional createSemanticData(final ICause cause, final Collection resources, final List> dependencies) { + final List documentDatas = resources.stream() + .map(resource -> this.resourceToDocumentService.toDocument(resource, false)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + final List documents = documentDatas.stream() + .map(DocumentData::document) + .toList(); + final List domains = documentDatas.stream() + .map(DocumentData::ePackageEntries) + .flatMap(List::stream) + .map(EPackageEntry::nsURI) + .distinct() + .toList(); + + final IResult creationResult = this.semanticDataCreationService.create(cause, documents, domains, dependencies); + if (creationResult instanceof Success creationSuccess) { + return Optional.ofNullable(creationSuccess.data()); + } else { + return Optional.empty(); + } + } + + protected Set getResourcesToPublish(final IEMFEditingContext emfEditingContext) { + return emfEditingContext.getDomain().getResourceSet().getResources().stream() + .filter(Predicate.not(ElementUtil::isStandardLibraryResource)) + .filter(resource -> !this.sysONResourceService.isFromReferencedLibrary(emfEditingContext, resource)) + // Only the ".sysml" resources can be published. + .filter(this.sysONResourceService::isSysML) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + protected Optional getLibraryMetadata(final Resource resource) { + return resource.eAdapters().stream() + .filter(LibraryMetadataAdapter.class::isInstance) + .map(LibraryMetadataAdapter.class::cast) + .findFirst(); + } +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/api/ISysMLLibraryPublisher.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/api/ISysMLLibraryPublisher.java new file mode 100644 index 000000000..8dc4db8c4 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/publication/api/ISysMLLibraryPublisher.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * 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.api; + +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.events.ICause; +import org.eclipse.sirius.web.domain.boundedcontexts.library.Library; + +/** + * Publishes the proper SysML contents of an {@link IEditingContext} as a {@link Library}. + * + * @author flatombe + */ +public interface ISysMLLibraryPublisher { + IPayload publish(ICause cause, IEditingContext libraryAuthoringEditingContext, String libraryNamespace, String libraryName, String libraryVersion, String libraryDescription); +}