From 993d1a4a45fd14ca06a9c55da80643b0986eaf56 Mon Sep 17 00:00:00 2001 From: Gwendal Daniel Date: Fri, 28 Feb 2025 15:19:25 +0100 Subject: [PATCH 1/2] [4652] Add support for loading editing contexts with dependencies Bug: https://github.com/eclipse-sirius/sirius-web/issues/4652 Signed-off-by: Gwendal Daniel --- CHANGELOG.adoc | 4 + .../EditingContextDependencyLoader.java | 73 +++++++++++++++++++ .../services/EditingContextLoader.java | 8 +- .../api/IEditingContextDependencyLoader.java | 28 +++++++ .../ComposedReadOnlyObjectPredicate.java | 52 +++++++++++++ .../DefaultReadOnlyObjectPredicate.java | 48 ++++++++++++ .../api/IDefaultReadOnlyObjectPredicate.java | 24 ++++++ .../api/IReadOnlyObjectPredicate.java | 24 ++++++ .../api/IReadOnlyObjectPredicateDelegate.java | 26 +++++++ .../explorer/services/ExplorerServices.java | 22 ++++-- .../semanticdata/SemanticData.java | 37 ++++++++++ .../SemanticDataDependencyAddedEvent.java | 35 +++++++++ .../SemanticDataDependencyRemovedEvent.java | 35 +++++++++ .../services/SemanticDataSearchService.java | 29 ++++++++ .../services/SemanticDataUpdateService.java | 43 ++++++++++- .../api/ISemanticDataSearchService.java | 3 + .../api/ISemanticDataUpdateService.java | 6 ++ .../LibraryControllerIntegrationTests.java | 4 - .../EditingContextLifecycleTests.java | 70 ++++++++++++++++++ 19 files changed, 557 insertions(+), 14 deletions(-) create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/EditingContextDependencyLoader.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/api/IEditingContextDependencyLoader.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/ComposedReadOnlyObjectPredicate.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/DefaultReadOnlyObjectPredicate.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IDefaultReadOnlyObjectPredicate.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IReadOnlyObjectPredicate.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IReadOnlyObjectPredicateDelegate.java create mode 100644 packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/events/SemanticDataDependencyAddedEvent.java create mode 100644 packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/events/SemanticDataDependencyRemovedEvent.java diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 9f3cfc94aa..13e7e4edd8 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -57,6 +57,10 @@ See the new interface `org.eclipse.sirius.components.emf.services.api.IRepresent - https://github.com/eclipse-sirius/sirius-web/issues/4597[#4597] [sirius-web] Add a command to publish libraries from studios The command creates a library for each _RepresentationDescription_ and _Domain_ in the studio, and creates the dependencies between them. A _shared components_ library can be created in the process to store elements that are needed by other libraries but are not stored in libraries themselves. +- https://github.com/eclipse-sirius/sirius-web/issues/4652[#4652] [sirius-web] Add support for loading projects with dependencies +Semantic data can have dependencies between them, which are transitively loaded when an editing context is created. +A new predicate `IReadOnlyObjectPredicate` has been added with a default implementation that returns `true` if the object belongs to semantic data loaded as a dependency. +This predicate is used in Sirius Web's explorer to prevent users from deleting or renaming elements from dependencies. === Improvements diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/EditingContextDependencyLoader.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/EditingContextDependencyLoader.java new file mode 100644 index 0000000000..f29f5796eb --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/EditingContextDependencyLoader.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.editingcontext.services; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.editingcontext.EditingContext; +import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextDependencyLoader; +import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextMigrationParticipantPredicate; +import org.eclipse.sirius.web.application.editingcontext.services.api.IResourceLoader; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services.api.ISemanticDataSearchService; +import org.springframework.stereotype.Service; + +/** + * Loads dependencies into the editing context. + * + * @author gdaniel + */ +@Service +public class EditingContextDependencyLoader implements IEditingContextDependencyLoader { + + private final ISemanticDataSearchService semanticDataSearchService; + + private final IResourceLoader resourceLoader; + + private final List migrationParticipantPredicates; + + public EditingContextDependencyLoader(ISemanticDataSearchService semanticDataSearchService, IResourceLoader resourceLoader, List migrationParticipantPredicates) { + this.semanticDataSearchService = Objects.requireNonNull(semanticDataSearchService); + this.resourceLoader = Objects.requireNonNull(resourceLoader); + this.migrationParticipantPredicates = Objects.requireNonNull(migrationParticipantPredicates); + } + + @Override + public void loadDependencies(IEditingContext editingContext) { + if (editingContext instanceof EditingContext siriusWebEditingContext) { + Set dependenciesSemanticData = new UUIDParser().parse(siriusWebEditingContext.getId()) + .map(this.semanticDataSearchService::findAllTransitiveSemanticDataById) + .orElse(Set.of()); + for (SemanticData semanticData : dependenciesSemanticData) { + semanticData.getDocuments().forEach(document -> { + URI dependencyResourceURI = URI.createURI(DEPENDENCY_SCHEME + ":///" + document.getId().toString()); + if (siriusWebEditingContext.getDomain().getResourceSet().getResource(dependencyResourceURI, false) == null) { + Optional resource = this.resourceLoader.toResource(siriusWebEditingContext.getDomain().getResourceSet(), document.getId().toString(), document.getName(), document.getContent(), + this.migrationParticipantPredicates.stream().anyMatch(predicate -> predicate.test(editingContext.getId()))); + resource.ifPresent(r -> { + siriusWebEditingContext.getDomain().getResourceSet().getURIConverter().getURIMap().put(r.getURI(), dependencyResourceURI); + r.setURI(dependencyResourceURI); + }); + } + }); + } + } + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/EditingContextLoader.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/EditingContextLoader.java index 9cfa9b15ae..3c5f66457e 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/EditingContextLoader.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/EditingContextLoader.java @@ -21,6 +21,7 @@ import org.eclipse.sirius.components.emf.services.EditingContextCrossReferenceAdapter; import org.eclipse.sirius.emfjson.resource.JsonResource; import org.eclipse.sirius.web.application.editingcontext.EditingContext; +import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextDependencyLoader; import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextLoader; import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextMigrationParticipantPredicate; import org.eclipse.sirius.web.application.editingcontext.services.api.IResourceLoader; @@ -41,22 +42,27 @@ public class EditingContextLoader implements IEditingContextLoader { private final IResourceLoader resourceLoader; + private final IEditingContextDependencyLoader editingContextDependencyLoader; + private final List representationDescriptionProviders; private final List editingContextProcessors; private final List migrationParticipantPredicates; - public EditingContextLoader(IResourceLoader resourceLoader, List representationDescriptionProviders, List editingContextProcessors, List migrationParticipantPredicates) { + public EditingContextLoader(IResourceLoader resourceLoader, IEditingContextDependencyLoader editingContextDependencyLoader, List representationDescriptionProviders, List editingContextProcessors, List migrationParticipantPredicates) { this.resourceLoader = Objects.requireNonNull(resourceLoader); + this.editingContextDependencyLoader = Objects.requireNonNull(editingContextDependencyLoader); this.representationDescriptionProviders = Objects.requireNonNull(representationDescriptionProviders); this.editingContextProcessors = Objects.requireNonNull(editingContextProcessors); this.migrationParticipantPredicates = Objects.requireNonNull(migrationParticipantPredicates); } + @Override public void load(EditingContext editingContext, SemanticData semanticData) { this.editingContextProcessors.forEach(processor -> processor.preProcess(editingContext)); + this.editingContextDependencyLoader.loadDependencies(editingContext); this.loadSemanticData(editingContext, semanticData); this.representationDescriptionProviders.forEach(representationDescriptionProvider -> { diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/api/IEditingContextDependencyLoader.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/api/IEditingContextDependencyLoader.java new file mode 100644 index 0000000000..90d4524b9f --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/api/IEditingContextDependencyLoader.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.editingcontext.services.api; + +import org.eclipse.sirius.components.core.api.IEditingContext; + +/** + * Loads dependencies into the editing context. + * + * @author gdaniel + */ +public interface IEditingContextDependencyLoader { + + String DEPENDENCY_SCHEME = "dependency"; + + void loadDependencies(IEditingContext editingContext); + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/ComposedReadOnlyObjectPredicate.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/ComposedReadOnlyObjectPredicate.java new file mode 100644 index 0000000000..b14d6849cd --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/ComposedReadOnlyObjectPredicate.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.object.services; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.sirius.web.application.object.services.api.IDefaultReadOnlyObjectPredicate; +import org.eclipse.sirius.web.application.object.services.api.IReadOnlyObjectPredicate; +import org.eclipse.sirius.web.application.object.services.api.IReadOnlyObjectPredicateDelegate; +import org.springframework.stereotype.Service; + +/** + * Implementation of {@link IReadOnlyObjectPredicate} which delegates to {@link IReadOnlyObjectPredicateDelegate} or fallback to + * {@link IDefaultReadOnlyObjectPredicate}. + * + * @author gdaniel + */ +@Service +public class ComposedReadOnlyObjectPredicate implements IReadOnlyObjectPredicate { + + private final List readOnlyObjectPredicateDelegate; + + private final IDefaultReadOnlyObjectPredicate defaultReadOnlyObjectPredicate; + + public ComposedReadOnlyObjectPredicate(List readOnlyObjectPredicateDelegate, IDefaultReadOnlyObjectPredicate defaultReadOnlyObjectPredicate) { + this.readOnlyObjectPredicateDelegate = Objects.requireNonNull(readOnlyObjectPredicateDelegate); + this.defaultReadOnlyObjectPredicate = Objects.requireNonNull(defaultReadOnlyObjectPredicate); + } + + @Override + public boolean test(Object object) { + var optionalDelegate = this.readOnlyObjectPredicateDelegate.stream() + .filter(delegate -> delegate.canHandle(object)) + .findFirst(); + if (optionalDelegate.isPresent()) { + return optionalDelegate.get().test(object); + } + return this.defaultReadOnlyObjectPredicate.test(object); + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/DefaultReadOnlyObjectPredicate.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/DefaultReadOnlyObjectPredicate.java new file mode 100644 index 0000000000..e8a45805b7 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/DefaultReadOnlyObjectPredicate.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.object.services; + +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextDependencyLoader; +import org.eclipse.sirius.web.application.object.services.api.IDefaultReadOnlyObjectPredicate; +import org.springframework.stereotype.Service; + +/** + * The default service used to test if an object is read-only. + * + * @author gdaniel + */ +@Service +public class DefaultReadOnlyObjectPredicate implements IDefaultReadOnlyObjectPredicate { + + @Override + public boolean test(Object object) { + boolean result = false; + if (object instanceof EObject eObject) { + result = Optional.ofNullable(eObject.eResource()) + .map(Resource::getURI) + .map(URI::scheme) + .filter(scheme -> Objects.equals(scheme, IEditingContextDependencyLoader.DEPENDENCY_SCHEME)) + .isPresent(); + } else if (object instanceof Resource resource) { + result = Objects.equals(resource.getURI().scheme(), IEditingContextDependencyLoader.DEPENDENCY_SCHEME); + } + return result; + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IDefaultReadOnlyObjectPredicate.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IDefaultReadOnlyObjectPredicate.java new file mode 100644 index 0000000000..532ddba284 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IDefaultReadOnlyObjectPredicate.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.object.services.api; + +import java.util.function.Predicate; + +/** + * The default service used to test if an object is read-only. + * + * @author gdaniel + */ +public interface IDefaultReadOnlyObjectPredicate extends Predicate { + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IReadOnlyObjectPredicate.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IReadOnlyObjectPredicate.java new file mode 100644 index 0000000000..c16a18bea5 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IReadOnlyObjectPredicate.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.object.services.api; + +import java.util.function.Predicate; + +/** + * Used to test if an object is read-only. + * + * @author gdaniel + */ +public interface IReadOnlyObjectPredicate extends Predicate { + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IReadOnlyObjectPredicateDelegate.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IReadOnlyObjectPredicateDelegate.java new file mode 100644 index 0000000000..2562fd1417 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/object/services/api/IReadOnlyObjectPredicateDelegate.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.object.services.api; + +import java.util.function.Predicate; + +/** + * Used to test if an object is read-only. + * + * @author gdaniel + */ +public interface IReadOnlyObjectPredicateDelegate extends Predicate { + + boolean canHandle(Object object); + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerServices.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerServices.java index 83a7135d64..a73c3a8753 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerServices.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerServices.java @@ -33,6 +33,7 @@ import org.eclipse.sirius.components.emf.services.JSONResourceFactory; import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.object.services.api.IReadOnlyObjectPredicate; import org.eclipse.sirius.web.application.views.explorer.services.api.IExplorerServices; import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.RepresentationIconURL; import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.RepresentationMetadata; @@ -56,11 +57,14 @@ public class ExplorerServices implements IExplorerServices { private final IRepresentationMetadataSearchService representationMetadataSearchService; - public ExplorerServices(IObjectService objectService, IURLParser urlParser, List representationImageProviders, IRepresentationMetadataSearchService representationMetadataSearchService) { + private final IReadOnlyObjectPredicate readOnlyObjectPredicate; + + public ExplorerServices(IObjectService objectService, IURLParser urlParser, List representationImageProviders, IRepresentationMetadataSearchService representationMetadataSearchService, IReadOnlyObjectPredicate readOnlyObjectPredicate) { this.objectService = Objects.requireNonNull(objectService); this.urlParser = Objects.requireNonNull(urlParser); this.representationImageProviders = Objects.requireNonNull(representationImageProviders); this.representationMetadataSearchService = Objects.requireNonNull(representationMetadataSearchService); + this.readOnlyObjectPredicate = Objects.requireNonNull(readOnlyObjectPredicate); } @Override @@ -114,19 +118,21 @@ private String getResourceLabel(Resource resource) { @Override public boolean isEditable(Object self) { boolean editable = false; - if (self instanceof RepresentationMetadata) { - editable = true; - } else if (self instanceof Resource) { - editable = true; - } else if (self instanceof EObject) { - editable = this.objectService.isLabelEditable(self); + if (!this.readOnlyObjectPredicate.test(self)) { + if (self instanceof RepresentationMetadata) { + editable = true; + } else if (self instanceof Resource) { + editable = true; + } else if (self instanceof EObject) { + editable = this.objectService.isLabelEditable(self); + } } return editable; } @Override public boolean isDeletable(Object self) { - return true; + return !this.readOnlyObjectPredicate.test(self); } @Override diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/SemanticData.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/SemanticData.java index 80cfb823a5..afcfaf068b 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/SemanticData.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/SemanticData.java @@ -25,6 +25,8 @@ import org.eclipse.sirius.components.events.ICause; import org.eclipse.sirius.web.domain.boundedcontexts.AbstractValidatingAggregateRoot; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.events.SemanticDataCreatedEvent; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.events.SemanticDataDependencyAddedEvent; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.events.SemanticDataDependencyRemovedEvent; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.events.SemanticDataUpdatedEvent; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient; @@ -77,6 +79,27 @@ public List getDependencies() { return Collections.unmodifiableList(this.dependencies); } + public void addDependency(ICause cause, UUID semanticDataId) { + var newDependency = new SemanticDataDependency(AggregateReference.to(semanticDataId)); + this.dependencies.add(newDependency); + this.lastModifiedOn = Instant.now(); + + this.registerEvent(new SemanticDataDependencyAddedEvent(UUID.randomUUID(), this.lastModifiedOn, cause, this, newDependency)); + + } + + public void removeDependency(ICause cause, UUID semanticDataId) { + this.dependencies.stream() + .filter(dependency -> dependency.dependencySemanticDataId().getId().equals(semanticDataId)) + .findFirst() + .ifPresent(dependency -> { + this.dependencies.remove(dependency); + this.lastModifiedOn = Instant.now(); + + this.registerEvent(new SemanticDataDependencyRemovedEvent(UUID.randomUUID(), this.lastModifiedOn, cause, this, dependency)); + }); + } + public Instant getCreatedOn() { return this.createdOn; } @@ -151,6 +174,20 @@ public static Builder newSemanticData() { return new Builder(); } + @Override + public boolean equals(Object obj) { + boolean result = false; + if (obj instanceof SemanticData semanticData) { + result = Objects.equals(this.id, semanticData.getId()); + } + return result; + } + + @Override + public int hashCode() { + return this.id.hashCode(); + } + /** * Used to create new semantic data. * diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/events/SemanticDataDependencyAddedEvent.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/events/SemanticDataDependencyAddedEvent.java new file mode 100644 index 0000000000..c64830b1ab --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/events/SemanticDataDependencyAddedEvent.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.domain.boundedcontexts.semanticdata.events; + +import java.time.Instant; +import java.util.UUID; + +import org.eclipse.sirius.components.events.ICause; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticDataDependency; + +import jakarta.validation.constraints.NotNull; + +/** + * Event fired when a dependency is added. + * + * @author gdaniel + */ +public record SemanticDataDependencyAddedEvent( + @NotNull UUID id, + @NotNull Instant createdOn, + @NotNull ICause causedBy, + @NotNull SemanticData semanticData, + @NotNull SemanticDataDependency dependency) implements ISemanticDataEvent { +} diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/events/SemanticDataDependencyRemovedEvent.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/events/SemanticDataDependencyRemovedEvent.java new file mode 100644 index 0000000000..f6a897f0a0 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/events/SemanticDataDependencyRemovedEvent.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.domain.boundedcontexts.semanticdata.events; + +import java.time.Instant; +import java.util.UUID; + +import org.eclipse.sirius.components.events.ICause; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticDataDependency; + +import jakarta.validation.constraints.NotNull; + +/** + * Event fired when a dependency is removed. + * + * @author gdaniel + */ +public record SemanticDataDependencyRemovedEvent( + @NotNull UUID id, + @NotNull Instant createdOn, + @NotNull ICause causedBy, + @NotNull SemanticData semanticData, + @NotNull SemanticDataDependency dependency) implements ISemanticDataEvent { +} diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataSearchService.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataSearchService.java index 65a334264f..aebb6ad711 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataSearchService.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataSearchService.java @@ -12,14 +12,18 @@ *******************************************************************************/ package org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services; +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 org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticDataDependency; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.repositories.ISemanticDataRepository; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services.api.ISemanticDataSearchService; +import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.stereotype.Service; /** @@ -50,4 +54,29 @@ public Optional findById(UUID id) { public boolean existsById(UUID id) { return this.semanticDataRepository.existsById(id); } + + @Override + public Set findAllTransitiveSemanticDataById(UUID id) { + Set collectedSemanticData = new LinkedHashSet<>(); + Optional semanticData = this.findById(id); + if (semanticData.isPresent()) { + this.collectTransitiveSemanticData(semanticData.get(), collectedSemanticData); + } + return collectedSemanticData; + } + + private void collectTransitiveSemanticData(SemanticData semanticData, Set collectedSemanticData) { + semanticData.getDependencies().stream() + .map(SemanticDataDependency::dependencySemanticDataId) + .map(AggregateReference::getId) + .map(this::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(dependencySemanticData -> { + if (!collectedSemanticData.contains(dependencySemanticData)) { + collectedSemanticData.add(dependencySemanticData); + this.collectTransitiveSemanticData(dependencySemanticData, collectedSemanticData); + } + }); + } } diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataUpdateService.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataUpdateService.java index 846e321c9c..fe0c6e33bc 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataUpdateService.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataUpdateService.java @@ -21,6 +21,10 @@ import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.repositories.ISemanticDataRepository; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services.api.ISemanticDataUpdateService; +import org.eclipse.sirius.web.domain.services.Failure; +import org.eclipse.sirius.web.domain.services.IResult; +import org.eclipse.sirius.web.domain.services.Success; +import org.eclipse.sirius.web.domain.services.api.IMessageService; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.stereotype.Service; @@ -34,8 +38,11 @@ public class SemanticDataUpdateService implements ISemanticDataUpdateService { private final ISemanticDataRepository semanticDataRepository; - public SemanticDataUpdateService(ISemanticDataRepository semanticDataRepository) { + private final IMessageService messageService; + + public SemanticDataUpdateService(ISemanticDataRepository semanticDataRepository, IMessageService messageService) { this.semanticDataRepository = Objects.requireNonNull(semanticDataRepository); + this.messageService = Objects.requireNonNull(messageService); } @Override @@ -45,4 +52,38 @@ public void updateDocuments(ICause cause, AggregateReference this.semanticDataRepository.save(semanticData); }); } + + @Override + public IResult addDependency(ICause cause, UUID semanticDataId, UUID dependencySemanticDataId) { + IResult result = null; + + var optionalSemanticData = this.semanticDataRepository.findById(semanticDataId); + if (optionalSemanticData.isEmpty()) { + result = new Failure<>(this.messageService.notFound()); + } else { + var semanticData = optionalSemanticData.get(); + semanticData.addDependency(cause, dependencySemanticDataId); + this.semanticDataRepository.save(semanticData); + result = new Success<>(null); + } + + return result; + } + + @Override + public IResult removeDependency(ICause cause, UUID semanticDataId, UUID dependencySemanticDataId) { + IResult result = null; + + var optionalSemanticData = this.semanticDataRepository.findById(semanticDataId); + if (optionalSemanticData.isEmpty()) { + result = new Failure<>(this.messageService.notFound()); + } else { + var semanticData = optionalSemanticData.get(); + semanticData.removeDependency(cause, dependencySemanticDataId); + this.semanticDataRepository.save(semanticData); + result = new Success<>(null); + } + + return result; + } } diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataSearchService.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataSearchService.java index a85eb7ba00..4f8fe07b29 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataSearchService.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataSearchService.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; @@ -30,4 +31,6 @@ public interface ISemanticDataSearchService { Optional findById(UUID id); boolean existsById(UUID id); + + Set findAllTransitiveSemanticDataById(UUID id); } diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataUpdateService.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataUpdateService.java index 4af5151bf1..6150001e6d 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataUpdateService.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataUpdateService.java @@ -18,6 +18,7 @@ import org.eclipse.sirius.components.events.ICause; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.Document; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; +import org.eclipse.sirius.web.domain.services.IResult; import org.springframework.data.jdbc.core.mapping.AggregateReference; /** @@ -27,4 +28,9 @@ */ public interface ISemanticDataUpdateService { void updateDocuments(ICause cause, AggregateReference semanticData, Set documents, Set domainUris); + + IResult addDependency(ICause cause, UUID semanticDataId, UUID dependencySemanticDataId); + + IResult removeDependency(ICause cause, UUID semanticDataID, UUID dependencySemanticDataId); + } diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java index f295252767..f3bf575ca8 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java @@ -29,7 +29,6 @@ import org.eclipse.sirius.web.domain.boundedcontexts.library.services.api.ILibrarySearchService; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticDataDependency; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services.api.ISemanticDataSearchService; -import org.eclipse.sirius.web.papaya.services.library.api.IStandardLibrarySemanticDataInitializer; import org.eclipse.sirius.web.tests.data.GivenSiriusWebServer; import org.eclipse.sirius.web.tests.graphql.LibrariesQueryRunner; import org.eclipse.sirius.web.tests.graphql.PublishLibrariesMutationRunner; @@ -63,9 +62,6 @@ public class LibraryControllerIntegrationTests extends AbstractIntegrationTests @Autowired private ISemanticDataSearchService semanticDataSearchService; - @Autowired - private IStandardLibrarySemanticDataInitializer standardLibrarySemanticDataInitializer; - @Test @GivenSiriusWebServer @DisplayName("Given a set of libraries, when a query is performed, then the libraries are returned") diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/services/EditingContextLifecycleTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/services/EditingContextLifecycleTests.java index ed0cf107e0..9973d584b9 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/services/EditingContextLifecycleTests.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/services/EditingContextLifecycleTests.java @@ -15,15 +15,30 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + import org.eclipse.emf.ecore.EPackage; import org.eclipse.sirius.components.core.api.IEditingContextPersistenceService; import org.eclipse.sirius.components.core.api.IEditingContextSearchService; +import org.eclipse.sirius.components.core.api.ILabelService; import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; import org.eclipse.sirius.components.events.ICause; import org.eclipse.sirius.web.AbstractIntegrationTests; import org.eclipse.sirius.web.application.editingcontext.EditingContext; +import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextDependencyLoader; +import org.eclipse.sirius.web.application.object.services.api.IReadOnlyObjectPredicate; +import org.eclipse.sirius.web.data.PapayaIdentifiers; import org.eclipse.sirius.web.data.TestIdentifiers; +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.services.api.ISemanticDataUpdateService; +import org.eclipse.sirius.web.domain.services.Success; +import org.eclipse.sirius.web.papaya.services.library.InitializeStandardLibraryEvent; +import org.eclipse.sirius.web.papaya.services.library.api.IStandardLibrarySemanticDataInitializer; import org.eclipse.sirius.web.tests.data.GivenSiriusWebServer; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +52,7 @@ * @author sbegaudeau */ @Transactional +@SuppressWarnings("checkstyle:MultipleStringLiterals") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class EditingContextLifecycleTests extends AbstractIntegrationTests { @Autowired @@ -45,6 +61,30 @@ public class EditingContextLifecycleTests extends AbstractIntegrationTests { @Autowired private IEditingContextPersistenceService editingContextPersistenceService; + @Autowired + private ILibrarySearchService librarySearchService; + + @Autowired + private ISemanticDataUpdateService semanticDataUpdateService; + + @Autowired + private ILabelService labelService; + + @Autowired + private IReadOnlyObjectPredicate readOnlyObjectPredicate; + + @Autowired + private IStandardLibrarySemanticDataInitializer standardLibrarySemanticDataInitializer; + + @BeforeEach + public void beforeEach() { + var initializeJavaStandardLibraryEvent = new InitializeStandardLibraryEvent(UUID.randomUUID(), "java", "Java Standard Library", "17.0.0", "The standard library of the Java programming language"); + this.standardLibrarySemanticDataInitializer.initializeStandardLibrary(initializeJavaStandardLibraryEvent); + TestTransaction.flagForCommit(); + TestTransaction.end(); + TestTransaction.start(); + } + @Test @GivenSiriusWebServer @DisplayName("Given semantic data using static metamodels, when the loading is performed, then the semantic data are available in the editing context") @@ -115,4 +155,34 @@ public void givenEditingContextProperlyLoadedWhenItIsModifiedAndPersistedThenThe assertThat(ePackage.getName()).isEqualTo("Sample Updated"); } } + + @Test + @GivenSiriusWebServer + @DisplayName("Given semantic data with a dependency, when the loading is performed, then the semantic data are available in the editing context") + public void givenSemanticDataWithDependencyWhenLoadingIsPerformedThenSemanticDataAvailableInEditingContext() { + Optional library = this.librarySearchService.findByNamespaceAndNameAndVersion("java", "Java Standard Library", "17.0.0"); + assertThat(library).isPresent(); + var result = this.semanticDataUpdateService.addDependency(null, PapayaIdentifiers.PAPAYA_EDITING_CONTEXT_ID, library.get().getSemanticData().getId()); + assertThat(result).isInstanceOf(Success.class); + TestTransaction.flagForCommit(); + TestTransaction.end(); + TestTransaction.start(); + + var optionalEditingContext = this.editingContextSearchService.findById(PapayaIdentifiers.PAPAYA_EDITING_CONTEXT_ID.toString()); + assertThat(optionalEditingContext).isPresent(); + + var editingContext = optionalEditingContext.get(); + assertThat(editingContext.getId()).isEqualTo(PapayaIdentifiers.PAPAYA_EDITING_CONTEXT_ID.toString()); + if (editingContext instanceof EditingContext siriusWebEditingContext) { + var resourceSet = siriusWebEditingContext.getDomain().getResourceSet(); + assertThat(resourceSet.getResources()) + .hasSize(3) + .anyMatch(resource -> + this.labelService.getLabel(resource).equals("Java Standard Library") + && Objects.equals(resource.getURI().scheme(), IEditingContextDependencyLoader.DEPENDENCY_SCHEME) + && this.readOnlyObjectPredicate.test(resource)); + } else { + fail("Invalid editing context"); + } + } } From c46ba8c66282072a3d09560c98b38a91d74b1252 Mon Sep 17 00:00:00 2001 From: Gwendal Daniel Date: Fri, 28 Feb 2025 17:38:10 +0100 Subject: [PATCH 2/2] [4509] Add a command to import libraries into a studio Bug: https://github.com/eclipse-sirius/sirius-web/issues/4509 Signed-off-by: Gwendal Daniel --- CHANGELOG.adoc | 4 + .../MutationImportLibrariesDataFetcher.java | 55 +++++ .../library/dto/ImportLibrariesInput.java | 29 +++ .../application/library/dto/LibraryDTO.java | 2 + .../library/services/LibraryMapper.java | 1 + .../StudioImportLibraryCommandProvider.java | 49 +++++ .../library/CopyLibraryImportHandler.java | 129 ++++++++++++ .../ReferenceLibraryImportHandler.java | 118 +++++++++++ .../StudioLibraryPublicationHandler.java | 61 ++++-- .../src/main/resources/omnibox/import.svg | 1 + .../main/resources/schema/libraries.graphqls | 12 ++ .../services/LibrarySearchService.java | 12 ++ .../services/api/ILibrarySearchService.java | 6 + .../repositories/ISemanticDataRepository.java | 11 + .../services/SemanticDataSearchService.java | 5 + .../api/ISemanticDataSearchService.java | 2 + .../ImportLibrariesMutationRunner.java | 62 ++++++ .../LibraryControllerIntegrationTests.java | 104 +++++++++- .../extension/DefaultExtensionRegistry.tsx | 7 + .../src/omnibox/ImportLibraryCommand.tsx | 41 ++++ .../src/omnibox/ImportLibraryCommand.types.ts | 16 ++ .../src/omnibox/ImportLibraryDialog.tsx | 191 ++++++++++++++++++ .../src/omnibox/ImportLibraryDialog.types.ts | 41 ++++ .../src/omnibox/LibrariesImportTable.tsx | 120 +++++++++++ .../src/omnibox/LibrariesImportTable.types.ts | 21 ++ .../src/omnibox/useImportLibraries.ts | 92 +++++++++ .../src/omnibox/useImportLibraries.types.ts | 47 +++++ .../library-browser/useLibraries.fragments.ts | 1 + .../library-browser/useLibraries.types.ts | 1 + 29 files changed, 1224 insertions(+), 17 deletions(-) create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/controllers/MutationImportLibrariesDataFetcher.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/ImportLibrariesInput.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/StudioImportLibraryCommandProvider.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/CopyLibraryImportHandler.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/ReferenceLibraryImportHandler.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/resources/omnibox/import.svg create mode 100644 packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/ImportLibrariesMutationRunner.java create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryCommand.tsx create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryCommand.types.ts create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryDialog.tsx create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryDialog.types.ts create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/omnibox/LibrariesImportTable.tsx create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/omnibox/LibrariesImportTable.types.ts create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/omnibox/useImportLibraries.ts create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/omnibox/useImportLibraries.types.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 13e7e4edd8..d44b678ea6 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -61,6 +61,10 @@ A _shared components_ library can be created in the process to store elements th Semantic data can have dependencies between them, which are transitively loaded when an editing context is created. A new predicate `IReadOnlyObjectPredicate` has been added with a default implementation that returns `true` if the object belongs to semantic data loaded as a dependency. This predicate is used in Sirius Web's explorer to prevent users from deleting or renaming elements from dependencies. +- https://github.com/eclipse-sirius/sirius-web/issues/4509[#4509] [sirius-web] Add a command to import libraries into a studio +The command is only present in studio projects, but the corresponding mutation can work on any project. +Downstream applications can provide their own command to import libraries into their projects, and rely on Sirius Web's import mechanism to load libraries. +The publication of a studio that references elements from imported libraries now produces libraries with the appropriate dependencies. === Improvements diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/controllers/MutationImportLibrariesDataFetcher.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/controllers/MutationImportLibrariesDataFetcher.java new file mode 100644 index 0000000000..6860cd31ac --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/controllers/MutationImportLibrariesDataFetcher.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.library.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.sirius.components.annotations.spring.graphql.QueryDataFetcher; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates; +import org.eclipse.sirius.components.graphql.api.IEditingContextDispatcher; +import org.eclipse.sirius.web.application.library.dto.ImportLibrariesInput; + +import graphql.schema.DataFetchingEnvironment; + +/** + * Data fetcher for the field Mutation#importLibraries. + * + * @author gdaniel + */ +@QueryDataFetcher(type = "Mutation", field = "importLibraries") +public class MutationImportLibrariesDataFetcher implements IDataFetcherWithFieldCoordinates> { + + private static final String INPUT_ARGUMENT = "input"; + + private final ObjectMapper objectMapper; + + private final IEditingContextDispatcher editingContextDispatcher; + + public MutationImportLibrariesDataFetcher(ObjectMapper objectMapper, IEditingContextDispatcher editingContextDispatcher) { + this.objectMapper = Objects.requireNonNull(objectMapper); + this.editingContextDispatcher = Objects.requireNonNull(editingContextDispatcher); + } + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + Object argument = environment.getArgument(INPUT_ARGUMENT); + var input = this.objectMapper.convertValue(argument, ImportLibrariesInput.class); + + return this.editingContextDispatcher.dispatchMutation(input.editingContextId(), input) + .toFuture(); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/ImportLibrariesInput.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/ImportLibrariesInput.java new file mode 100644 index 0000000000..e44d8db8ba --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/ImportLibrariesInput.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.library.dto; + +import java.util.List; +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IInput; + +import jakarta.validation.constraints.NotNull; + +/** + * Input used to import libraries. + * + * @author gdaniel + */ +public record ImportLibrariesInput(@NotNull UUID id, @NotNull String editingContextId, @NotNull String type, @NotNull List libraryIds) implements IInput { + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/LibraryDTO.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/LibraryDTO.java index 8b2865894d..4bf7842482 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/LibraryDTO.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/LibraryDTO.java @@ -13,6 +13,7 @@ package org.eclipse.sirius.web.application.library.dto; import java.time.Instant; +import java.util.UUID; import jakarta.validation.constraints.NotNull; @@ -22,6 +23,7 @@ * @author gdaniel */ public record LibraryDTO( + @NotNull UUID id, @NotNull String namespace, @NotNull String name, @NotNull String version, diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/LibraryMapper.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/LibraryMapper.java index 588b2fb0db..fc931f703a 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/LibraryMapper.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/LibraryMapper.java @@ -28,6 +28,7 @@ public class LibraryMapper implements ILibraryMapper { @Override public LibraryDTO toDTO(Library library) { return new LibraryDTO( + library.getId(), library.getNamespace(), library.getName(), library.getVersion(), diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/StudioImportLibraryCommandProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/StudioImportLibraryCommandProvider.java new file mode 100644 index 0000000000..67e626c4a6 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/StudioImportLibraryCommandProvider.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.studio.services; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.eclipse.sirius.components.collaborative.omnibox.api.IOmniboxCommandProvider; +import org.eclipse.sirius.components.collaborative.omnibox.dto.OmniboxCommand; +import org.eclipse.sirius.web.application.studio.services.api.IStudioCapableEditingContextPredicate; +import org.springframework.stereotype.Service; + +/** + * Provides the import studio command in the omnibox. + * + * @author gdaniel + */ +@Service +public class StudioImportLibraryCommandProvider implements IOmniboxCommandProvider { + + public static final String IMPORT_LIBRARY_COMMAND_ID = "importLibrary"; + + private final IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate; + + public StudioImportLibraryCommandProvider(IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate) { + this.studioCapableEditingContextPredicate = Objects.requireNonNull(studioCapableEditingContextPredicate); + } + + @Override + public List getCommands(String editingContextId, List selectedObjectIds, String query) { + List result = new ArrayList<>(); + if (this.studioCapableEditingContextPredicate.test(editingContextId)) { + result.add(new OmniboxCommand(IMPORT_LIBRARY_COMMAND_ID, "Import studio libraries", List.of("/omnibox/import.svg"), "Import studio libraries in the project")); + } + return result; + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/CopyLibraryImportHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/CopyLibraryImportHandler.java new file mode 100644 index 0000000000..31849033ff --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/CopyLibraryImportHandler.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.studio.services.library; + +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 org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventHandler; +import org.eclipse.sirius.components.collaborative.api.Monitoring; +import org.eclipse.sirius.components.collaborative.messages.ICollaborativeMessageService; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IInput; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.core.api.SuccessPayload; +import org.eclipse.sirius.components.emf.services.JSONResourceFactory; +import org.eclipse.sirius.web.application.editingcontext.EditingContext; +import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextMigrationParticipantPredicate; +import org.eclipse.sirius.web.application.editingcontext.services.api.IResourceLoader; +import org.eclipse.sirius.web.application.library.dto.ImportLibrariesInput; +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.ISemanticDataSearchService; +import org.springframework.stereotype.Service; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import reactor.core.publisher.Sinks.Many; +import reactor.core.publisher.Sinks.One; + +/** + * Handles the import of a library as a copy. + * + * @author gdaniel + */ +@Service +public class CopyLibraryImportHandler implements IEditingContextEventHandler { + + private final ILibrarySearchService librarySearchService; + + private final ISemanticDataSearchService semanticDataSearchService; + + private final IResourceLoader resourceLoader; + + private final List migrationParticipantPredicates; + + private final ICollaborativeMessageService messageService; + + private final Counter counter; + + public CopyLibraryImportHandler(ILibrarySearchService librarySearchService, ISemanticDataSearchService semanticDataSearchService, IResourceLoader resourceLoader, List migrationParticipantPredicates, ICollaborativeMessageService messageService, MeterRegistry meterRegistry) { + this.librarySearchService = Objects.requireNonNull(librarySearchService); + this.semanticDataSearchService = Objects.requireNonNull(semanticDataSearchService); + this.resourceLoader = Objects.requireNonNull(resourceLoader); + this.migrationParticipantPredicates = Objects.requireNonNull(migrationParticipantPredicates); + this.messageService = Objects.requireNonNull(messageService); + this.counter = Counter.builder(Monitoring.EVENT_HANDLER) + .tag(Monitoring.NAME, this.getClass().getSimpleName()) + .register(meterRegistry); + } + + @Override + public boolean canHandle(IEditingContext editingContext, IInput input) { + return input instanceof ImportLibrariesInput importLibrariesInput + && Objects.equals(importLibrariesInput.type(), "copy"); + } + + @Override + public void handle(One payloadSink, Many changeDescriptionSink, IEditingContext editingContext, IInput input) { + this.counter.increment(); + + String message = this.messageService.invalidInput(input.getClass().getSimpleName(), ImportLibrariesInput.class.getSimpleName()); + IPayload payload = new ErrorPayload(input.id(), message); + ChangeDescription changeDescription = new ChangeDescription(ChangeKind.NOTHING, editingContext.getId(), input); + + if (input instanceof ImportLibrariesInput importLibrariesInput && Objects.equals(importLibrariesInput.type(), "copy")) { + if (editingContext instanceof EditingContext siriusWebEditingContext) { + List libraries = this.librarySearchService.findAllById(importLibrariesInput.libraryIds().stream().map(UUID::fromString).toList()); + + Set semanticDataToCopy = new LinkedHashSet<>(); + for (Library library : libraries) { + this.semanticDataSearchService.findById(library.getSemanticData().getId()) + .ifPresent(semanticData -> { + semanticDataToCopy.add(semanticData); + semanticDataToCopy.addAll(this.semanticDataSearchService.findAllTransitiveSemanticDataById(semanticData.getId())); + }); + } + + for (SemanticData semanticData: semanticDataToCopy) { + for (Document document : semanticData.getDocuments()) { + Optional optResource = this.resourceLoader.toResource(siriusWebEditingContext.getDomain().getResourceSet(), document.getId().toString(), document.getName(), document.getContent(), + this.migrationParticipantPredicates.stream().anyMatch(predicate -> predicate.test(editingContext.getId()))); + optResource.ifPresent(resource -> { + URI newResourceURI = new JSONResourceFactory().createResourceURI(UUID.randomUUID().toString()); + URI oldResourceURI = resource.getURI(); + siriusWebEditingContext.getDomain().getResourceSet().getURIConverter().getURIMap().put(oldResourceURI, newResourceURI); + resource.setURI(newResourceURI); + }); + } + } + + payload = new SuccessPayload(input.id()); + changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), importLibrariesInput); + } + } + payloadSink.tryEmitValue(payload); + changeDescriptionSink.tryEmitNext(changeDescription); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/ReferenceLibraryImportHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/ReferenceLibraryImportHandler.java new file mode 100644 index 0000000000..0370bc5d79 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/ReferenceLibraryImportHandler.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.application.studio.services.library; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventHandler; +import org.eclipse.sirius.components.collaborative.api.Monitoring; +import org.eclipse.sirius.components.collaborative.messages.ICollaborativeMessageService; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IInput; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.core.api.SuccessPayload; +import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.editingcontext.EditingContext; +import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextDependencyLoader; +import org.eclipse.sirius.web.application.library.dto.ImportLibrariesInput; +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.SemanticData; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services.api.ISemanticDataSearchService; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services.api.ISemanticDataUpdateService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import reactor.core.publisher.Sinks.Many; +import reactor.core.publisher.Sinks.One; + +/** + * Handles the import of a library as a reference. + * + * @author gdaniel + */ +@Service +public class ReferenceLibraryImportHandler implements IEditingContextEventHandler { + + private final Logger logger = LoggerFactory.getLogger(ReferenceLibraryImportHandler.class); + + private final ILibrarySearchService librarySearchService; + + private final ISemanticDataSearchService semanticDataSearchService; + + private final ISemanticDataUpdateService semanticDataUpdateService; + + private final IEditingContextDependencyLoader editingContextDependencyLoader; + + private final ICollaborativeMessageService messageService; + + private final Counter counter; + + public ReferenceLibraryImportHandler(ILibrarySearchService librarySearchService, ISemanticDataSearchService semanticDataSearchService, ISemanticDataUpdateService semanticDataUpdateService, IEditingContextDependencyLoader editingContextDependencyLoader, ICollaborativeMessageService messageService, MeterRegistry meterRegistry) { + this.librarySearchService = Objects.requireNonNull(librarySearchService); + this.semanticDataSearchService = Objects.requireNonNull(semanticDataSearchService); + this.semanticDataUpdateService = Objects.requireNonNull(semanticDataUpdateService); + this.editingContextDependencyLoader = Objects.requireNonNull(editingContextDependencyLoader); + this.messageService = Objects.requireNonNull(messageService); + this.counter = Counter.builder(Monitoring.EVENT_HANDLER) + .tag(Monitoring.NAME, this.getClass().getSimpleName()) + .register(meterRegistry); + } + + @Override + public boolean canHandle(IEditingContext editingContext, IInput input) { + return input instanceof ImportLibrariesInput importLibrariesInput + && importLibrariesInput.type().equals("import"); + } + + @Override + public void handle(One payloadSink, Many changeDescriptionSink, IEditingContext editingContext, IInput input) { + this.counter.increment(); + + String message = this.messageService.invalidInput(input.getClass().getSimpleName(), ImportLibrariesInput.class.getSimpleName()); + IPayload payload = new ErrorPayload(input.id(), message); + ChangeDescription changeDescription = new ChangeDescription(ChangeKind.NOTHING, editingContext.getId(), input); + if (input instanceof ImportLibrariesInput importLibrariesInput && Objects.equals(importLibrariesInput.type(), "import")) { + if (editingContext instanceof EditingContext siriusWebEditingContext) { + List libraries = this.librarySearchService.findAllById(importLibrariesInput.libraryIds().stream().map(UUID::fromString).toList()); + for (Library library : libraries) { + Optional optionalSemanticData = new UUIDParser().parse(siriusWebEditingContext.getId()) + .flatMap(this.semanticDataSearchService::findById); + if (optionalSemanticData.isPresent()) { + if (optionalSemanticData.get().getDependencies().stream().anyMatch(dependency -> dependency.dependencySemanticDataId().getId().equals(library.getSemanticData().getId()))) { + this.logger.warn("Cannot add the dependency to library " + library.getNamespace() + ":" + library.getName() + ":" + library.getVersion() + ": the dependency already exists"); + } + else { + this.semanticDataUpdateService.addDependency(input, optionalSemanticData.get().getId(), library.getSemanticData().getId()); + } + } + } + this.editingContextDependencyLoader.loadDependencies(editingContext); + payload = new SuccessPayload(input.id()); + changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input); + } + } + payloadSink.tryEmitValue(payload); + changeDescriptionSink.tryEmitNext(changeDescription); + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/StudioLibraryPublicationHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/StudioLibraryPublicationHandler.java index 6cad11dff7..996a23a0b6 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/StudioLibraryPublicationHandler.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/StudioLibraryPublicationHandler.java @@ -12,6 +12,7 @@ *******************************************************************************/ package org.eclipse.sirius.web.application.studio.services.library; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,16 +36,19 @@ import org.eclipse.sirius.components.representations.Message; import org.eclipse.sirius.components.representations.MessageLevel; import org.eclipse.sirius.components.view.RepresentationDescription; +import org.eclipse.sirius.web.application.UUIDParser; 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.IEditingContextDependencyLoader; 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.studio.services.library.api.DependencyGraph; 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.application.studio.services.library.api.IStudioLibraryDependencyCollector; import org.eclipse.sirius.web.domain.boundedcontexts.projectsemanticdata.services.api.IProjectSemanticDataSearchService; 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.boundedcontexts.semanticdata.services.api.ISemanticDataSearchService; import org.eclipse.sirius.web.domain.services.IResult; import org.eclipse.sirius.web.domain.services.Success; import org.eclipse.sirius.web.domain.services.api.IMessageService; @@ -69,14 +73,17 @@ public class StudioLibraryPublicationHandler implements ILibraryPublicationHandl private final IProjectSemanticDataSearchService projectSemanticDataSearchService; + private final ISemanticDataSearchService semanticDataSearchService; + private final IMessageService messageService; - public StudioLibraryPublicationHandler(IEditingContextSearchService editingContextSearchService, ISemanticDataCreationService semanticDataCreationService, IResourceToDocumentService resourceToDocumentService, IStudioLibraryDependencyCollector studioLibraryDependencyCollector, IProjectSemanticDataSearchService projectSemanticDataSearchService, IMessageService messageService) { + public StudioLibraryPublicationHandler(IEditingContextSearchService editingContextSearchService, ISemanticDataCreationService semanticDataCreationService, IResourceToDocumentService resourceToDocumentService, IStudioLibraryDependencyCollector studioLibraryDependencyCollector, IProjectSemanticDataSearchService projectSemanticDataSearchService, ISemanticDataSearchService semanticDataSearchService, IMessageService messageService) { this.editingContextSearchService = Objects.requireNonNull(editingContextSearchService); this.semanticDataCreationService = Objects.requireNonNull(semanticDataCreationService); this.resourceToDocumentService = Objects.requireNonNull(resourceToDocumentService); this.studioLibraryDependencyCollector = Objects.requireNonNull(studioLibraryDependencyCollector); this.projectSemanticDataSearchService = Objects.requireNonNull(projectSemanticDataSearchService); + this.semanticDataSearchService = Objects.requireNonNull(semanticDataSearchService); this.messageService = Objects.requireNonNull(messageService); } @@ -105,27 +112,26 @@ public IPayload handle(PublishLibrariesInput input) { List libraryCandidates = dependencyGraph.computeTopologicalOrdering(); for (EObject libraryCandidate : libraryCandidates) { - if (libraryCandidate instanceof Domain || libraryCandidate instanceof RepresentationDescription) { - Optional optionalLibraryName = this.getUniqueLibraryName(libraryCandidate, libraryCandidates); - if (optionalLibraryName.isPresent()) { - Resource libraryResource = this.getOrCreateLibraryResource(input.projectId(), optionalLibraryName.get(), input.version(), resourceSet); - libraryResource.getContents().add(libraryCandidate); + if (!libraryCandidate.eResource().getURI().scheme().equals(IEditingContextDependencyLoader.DEPENDENCY_SCHEME)) { + if (libraryCandidate instanceof Domain || libraryCandidate instanceof RepresentationDescription) { + Optional optionalLibraryName = this.getUniqueLibraryName(libraryCandidate, libraryCandidates); + if (optionalLibraryName.isPresent()) { + Resource libraryResource = this.getOrCreateLibraryResource(input.projectId(), optionalLibraryName.get(), input.version(), resourceSet); + libraryResource.getContents().add(libraryCandidate); + } + } else { + Resource sharedComponentsResource = this.getOrCreateLibraryResource(input.projectId(), "shared_components", input.version(), resourceSet); + sharedComponentsResource.getContents().add(libraryCandidate); } - } else { - Resource sharedComponentsResource = this.getOrCreateLibraryResource(input.projectId(), "shared_components", input.version(), resourceSet); - sharedComponentsResource.getContents().add(libraryCandidate); } } Map createdSemanticData = new HashMap<>(); for (EObject libraryCandidate : libraryCandidates) { String libraryName = this.getResourceName(libraryCandidate.eResource()); - if (!createdSemanticData.containsKey(libraryName)) { - List> dependencies = dependencyGraph.getDependencies(libraryCandidate).stream() - .map(dependency -> createdSemanticData.get(this.getResourceName(dependency.eResource())).getId()) - .distinct() - .map(AggregateReference::to) - .toList(); + if (!createdSemanticData.containsKey(libraryName) + && !libraryCandidate.eResource().getURI().scheme().equals(IEditingContextDependencyLoader.DEPENDENCY_SCHEME)) { + List> dependencies = this.getDependencies(dependencyGraph, libraryCandidate, createdSemanticData); ICause cause = new StudioLibrarySemanticDataCreationRequested(UUID.randomUUID(), input, libraryName); Optional optionalSemanticData = this.createSemanticData(cause, libraryCandidate.eResource(), dependencies); if (optionalSemanticData.isPresent()) { @@ -201,4 +207,27 @@ private Resource getOrCreateLibraryResource(String projectId, String name, Strin } return resource; } + + private List> getDependencies(DependencyGraph dependencyGraph, EObject libraryCandidate, Map createdSemanticData) { + List dependencies = new ArrayList<>(); + for (EObject dependencyCandidate : dependencyGraph.getDependencies(libraryCandidate)) { + if (dependencyCandidate.eResource().getURI().scheme().equals(IEditingContextDependencyLoader.DEPENDENCY_SCHEME)) { + Optional optExistingSemanticData = new UUIDParser().parse(dependencyCandidate.eResource().getURI().path().substring(1)) + .flatMap(this.semanticDataSearchService::findByDocumentId); + if (optExistingSemanticData.isPresent()) { + dependencies.add(optExistingSemanticData.get()); + } + } else { + SemanticData existingSemanticData = createdSemanticData.get(this.getResourceName(dependencyCandidate.eResource())); + if (existingSemanticData != null) { + dependencies.add(existingSemanticData); + } + } + } + return dependencies.stream() + .map(SemanticData::getId) + .distinct() + .map(AggregateReference::to) + .toList(); + } } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/resources/omnibox/import.svg b/packages/sirius-web/backend/sirius-web-application/src/main/resources/omnibox/import.svg new file mode 100644 index 0000000000..e450370861 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/resources/omnibox/import.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/libraries.graphqls b/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/libraries.graphqls index cc4c9705a5..a5262d60ed 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/libraries.graphqls +++ b/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/libraries.graphqls @@ -13,6 +13,7 @@ type ViewerLibrariesEdge { } type Library { + id: ID! namespace: String! name: String! version: String! @@ -23,6 +24,8 @@ type Library { extend type Mutation { publishLibraries(input: PublishLibrariesInput!): PublishLibrariesPayload! + importLibraries(input: ImportLibrariesInput!): ImportLibrariesPayload! + } input PublishLibrariesInput { @@ -34,3 +37,12 @@ input PublishLibrariesInput { } union PublishLibrariesPayload = ErrorPayload | SuccessPayload + +input ImportLibrariesInput { + id: ID! + editingContextId: ID! + type: String! + libraryIds: [String!]! +} + +union ImportLibrariesPayload = ErrorPayload | SuccessPayload \ No newline at end of file diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/services/LibrarySearchService.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/services/LibrarySearchService.java index 78059cacb0..e79addb84d 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/services/LibrarySearchService.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/services/LibrarySearchService.java @@ -12,8 +12,10 @@ *******************************************************************************/ package org.eclipse.sirius.web.domain.boundedcontexts.library.services; +import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.UUID; import org.eclipse.sirius.web.domain.boundedcontexts.library.Library; import org.eclipse.sirius.web.domain.boundedcontexts.library.repositories.ILibraryRepository; @@ -50,4 +52,14 @@ public boolean existsByNamespaceAndNameAndVersion(String namespace, String name, public Optional findByNamespaceAndNameAndVersion(String namespace, String name, String version) { return this.libraryRepository.findByNamespaceAndNameAndVersion(namespace, name, version); } + + @Override + public Optional findById(UUID id) { + return this.libraryRepository.findById(id); + } + + @Override + public List findAllById(Iterable ids) { + return this.libraryRepository.findAllById(ids); + } } diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/services/api/ILibrarySearchService.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/services/api/ILibrarySearchService.java index 54325a900d..b5f3b3dd68 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/services/api/ILibrarySearchService.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/services/api/ILibrarySearchService.java @@ -12,7 +12,9 @@ *******************************************************************************/ package org.eclipse.sirius.web.domain.boundedcontexts.library.services.api; +import java.util.List; import java.util.Optional; +import java.util.UUID; import org.eclipse.sirius.web.domain.boundedcontexts.library.Library; import org.springframework.data.domain.Page; @@ -30,4 +32,8 @@ public interface ILibrarySearchService { boolean existsByNamespaceAndNameAndVersion(String namespace, String name, String version); Optional findByNamespaceAndNameAndVersion(String namespace, String name, String version); + + Optional findById(UUID id); + + List findAllById(Iterable ids); } diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/repositories/ISemanticDataRepository.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/repositories/ISemanticDataRepository.java index 60a8882941..91481a5a9d 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/repositories/ISemanticDataRepository.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/repositories/ISemanticDataRepository.java @@ -13,6 +13,7 @@ package org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.repositories; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; @@ -38,4 +39,14 @@ WHERE semanticDataDomain.uri IN (:domainUris) GROUP BY semanticData.id """) List findAllByDomains(List domainUris); + + @Query(""" + SELECT semanticData.* + FROM semantic_data semanticData + JOIN document + ON semanticData.id = document.semantic_data_id + WHERE document.id = :documentId + GROUP BY semanticData.id + """) + Optional findByDocumentId(UUID documentId); } diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataSearchService.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataSearchService.java index aebb6ad711..c0c3a607ed 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataSearchService.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/SemanticDataSearchService.java @@ -65,6 +65,11 @@ public Set findAllTransitiveSemanticDataById(UUID id) { return collectedSemanticData; } + @Override + public Optional findByDocumentId(UUID documentId) { + return this.semanticDataRepository.findByDocumentId(documentId); + } + private void collectTransitiveSemanticData(SemanticData semanticData, Set collectedSemanticData) { semanticData.getDependencies().stream() .map(SemanticDataDependency::dependencySemanticDataId) diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataSearchService.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataSearchService.java index 4f8fe07b29..1a8877613d 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataSearchService.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/semanticdata/services/api/ISemanticDataSearchService.java @@ -33,4 +33,6 @@ public interface ISemanticDataSearchService { boolean existsById(UUID id); Set findAllTransitiveSemanticDataById(UUID id); + + Optional findByDocumentId(UUID documentId); } diff --git a/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/ImportLibrariesMutationRunner.java b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/ImportLibrariesMutationRunner.java new file mode 100644 index 0000000000..d7d082067a --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/ImportLibrariesMutationRunner.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2025 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.sirius.web.tests.graphql; + +import java.util.Objects; + +import org.eclipse.sirius.components.graphql.tests.api.IGraphQLRequestor; +import org.eclipse.sirius.components.graphql.tests.api.IMutationRunner; +import org.eclipse.sirius.web.application.library.dto.ImportLibrariesInput; +import org.springframework.stereotype.Service; + +/** + * Used to import libraries with the GraphQL API. + * + * @author gdaniel + */ +@Service +public class ImportLibrariesMutationRunner implements IMutationRunner { + + private static final String IMPORT_LIBRARIES = """ + mutation importLibraries($input: ImportLibrariesInput!) { + importLibraries(input: $input) { + __typename + ... on SuccessPayload { + messages { + level + body + } + } + ... on ErrorPayload { + message + messages { + level + body + } + } + } + } + """; + + private final IGraphQLRequestor graphQLRequestor; + + public ImportLibrariesMutationRunner(IGraphQLRequestor graphQLRequestor) { + this.graphQLRequestor = Objects.requireNonNull(graphQLRequestor); + } + + @Override + public String run(ImportLibrariesInput input) { + return this.graphQLRequestor.execute(IMPORT_LIBRARIES, input); + } + +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java index f3bf575ca8..d53f2eb229 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java @@ -18,20 +18,37 @@ import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.function.BiFunction; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IInput; +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.graphql.tests.ExecuteEditingContextFunctionInput; +import org.eclipse.sirius.components.graphql.tests.api.IExecuteEditingContextFunctionRunner; import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextDependencyLoader; +import org.eclipse.sirius.web.application.library.dto.ImportLibrariesInput; import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; +import org.eclipse.sirius.web.data.PapayaIdentifiers; import org.eclipse.sirius.web.data.StudioIdentifiers; 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.SemanticDataDependency; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services.api.ISemanticDataSearchService; +import org.eclipse.sirius.web.papaya.services.library.InitializeStandardLibraryEvent; +import org.eclipse.sirius.web.papaya.services.library.api.IStandardLibrarySemanticDataInitializer; import org.eclipse.sirius.web.tests.data.GivenSiriusWebServer; +import org.eclipse.sirius.web.tests.graphql.ImportLibrariesMutationRunner; import org.eclipse.sirius.web.tests.graphql.LibrariesQueryRunner; import org.eclipse.sirius.web.tests.graphql.PublishLibrariesMutationRunner; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -42,11 +59,12 @@ import org.springframework.transaction.annotation.Transactional; /** - * Used to get libraries from the GraphQL API. + * Integration tests of the library controllers. * * @author gdaniel */ @Transactional +@SuppressWarnings("checkstyle:MultipleStringLiterals") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class LibraryControllerIntegrationTests extends AbstractIntegrationTests { @@ -56,12 +74,30 @@ public class LibraryControllerIntegrationTests extends AbstractIntegrationTests @Autowired private PublishLibrariesMutationRunner publishLibrariesMutationRunner; + @Autowired + private ImportLibrariesMutationRunner importLibrariesMutationRunner; + @Autowired private ILibrarySearchService librarySearchService; @Autowired private ISemanticDataSearchService semanticDataSearchService; + @Autowired + private IExecuteEditingContextFunctionRunner executeEditingContextFunctionRunner; + + @Autowired + private IStandardLibrarySemanticDataInitializer standardLibrarySemanticDataInitializer; + + @BeforeEach + public void beforeEach() { + var initializeJavaStandardLibraryEvent = new InitializeStandardLibraryEvent(UUID.randomUUID(), "java", "Java Standard Library", "17.0.0", "The standard library of the Java programming language"); + this.standardLibrarySemanticDataInitializer.initializeStandardLibrary(initializeJavaStandardLibraryEvent); + TestTransaction.flagForCommit(); + TestTransaction.end(); + TestTransaction.start(); + } + @Test @GivenSiriusWebServer @DisplayName("Given a set of libraries, when a query is performed, then the libraries are returned") @@ -141,6 +177,72 @@ public void givenValidStudioProjectIdWhenPublicationMutationIsPerformedThenLibra this.assertThatLibraryHasCorrectDescriptionAndDependencies(rootDiagram1DescriptionLibrary.get(), description, List.of(buckLibrary, sharedComponentsLibrary)); } + @Test + @GivenSiriusWebServer + @DisplayName("Given a project, when a library is imported as a copy, then the editing context contains the copy of the library") + public void givenProjectWhenLibraryIsImportedAsCopyThenEditingContextContainsTheLibraryCopy() { + Optional library = this.librarySearchService.findByNamespaceAndNameAndVersion("java", "Java Standard Library", "17.0.0"); + assertThat(library).isPresent(); + var importLibrariesInput = new ImportLibrariesInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_EDITING_CONTEXT_ID.toString(), "copy", List.of(library.get().getId().toString())); + this.importLibrariesMutationRunner.run(importLibrariesInput); + + BiFunction function = (editingContext, executeEditingContextFunctionInput) -> { + if (editingContext instanceof IEMFEditingContext emfEditingContext) { + assertThat(emfEditingContext.getDomain().getResourceSet().getResources()) + .anyMatch(resource -> { + Optional optName = resource.eAdapters().stream() + .filter(ResourceMetadataAdapter.class::isInstance) + .map(ResourceMetadataAdapter.class::cast) + .map(ResourceMetadataAdapter::getName) + .findFirst(); + return optName.isPresent() + && optName.get().equals("Java Standard Library") + // The library should be in a RESOURCE_SCHEME resource: it has been copied and is a regular Sirius resource. + && Objects.equals(resource.getURI().scheme(), IEMFEditingContext.RESOURCE_SCHEME); + }); + return new SuccessPayload(executeEditingContextFunctionInput.id()); + } + return new ErrorPayload(executeEditingContextFunctionInput.id(), "Invalid editing context"); + }; + + var input = new ExecuteEditingContextFunctionInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_EDITING_CONTEXT_ID.toString(), function); + var payload = this.executeEditingContextFunctionRunner.execute(input).block(); + assertThat(payload).isInstanceOf(SuccessPayload.class); + } + + @Test + @GivenSiriusWebServer + @DisplayName("Given a project, when a library is imported as a reference, then the editing context contains the reference to the library") + public void givenProjectWhenLibraryIsImportedAsReferenceThenEditingContextContainsTheLibraryDependency() { + Optional library = this.librarySearchService.findByNamespaceAndNameAndVersion("java", "Java Standard Library", "17.0.0"); + assertThat(library).isPresent(); + var importLibrariesInput = new ImportLibrariesInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_EDITING_CONTEXT_ID.toString(), "import", List.of(library.get().getId().toString())); + this.importLibrariesMutationRunner.run(importLibrariesInput); + + BiFunction function = (editingContext, executeEditingContextFunctionInput) -> { + if (editingContext instanceof IEMFEditingContext emfEditingContext) { + assertThat(emfEditingContext.getDomain().getResourceSet().getResources()) + .anyMatch(resource -> { + Optional optName = resource.eAdapters().stream() + .filter(ResourceMetadataAdapter.class::isInstance) + .map(ResourceMetadataAdapter.class::cast) + .map(ResourceMetadataAdapter::getName) + .findFirst(); + return optName.isPresent() + && optName.get().equals("Java Standard Library") + // The library should be in a DEPENDENCY_SCHEME resource: it is referenced by the editing context. + && Objects.equals(resource.getURI().scheme(), IEditingContextDependencyLoader.DEPENDENCY_SCHEME); + }); + return new SuccessPayload(executeEditingContextFunctionInput.id()); + } + return new ErrorPayload(executeEditingContextFunctionInput.id(), "Invalid editing context"); + }; + + var input = new ExecuteEditingContextFunctionInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_EDITING_CONTEXT_ID.toString(), function); + var payload = this.executeEditingContextFunctionRunner.execute(input).block(); + assertThat(payload).isInstanceOf(SuccessPayload.class); + } + private void assertThatLibraryHasCorrectDescriptionAndDependencies(Library library, String description, List dependencyLibraries) { var optionalLibrarySemanticData = this.semanticDataSearchService.findById(library.getSemanticData().getId()); diff --git a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx index a594349d8d..16266f4599 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx +++ b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx @@ -102,6 +102,7 @@ import { projectSettingsTabExtensionPoint } from '../views/project-settings/Proj import { ellipseNodeStyleDocumentTransform } from './EllipseNodeDocumentTransform'; import { referenceWidgetDocumentTransform } from './ReferenceWidgetDocumentTransform'; import { tableWidgetDocumentTransform } from './TableWidgetDocumentTransform'; +import { ImportLibraryCommand } from '../omnibox/ImportLibraryCommand'; const getType = (representation: RepresentationMetadata): string | null => { const query = representation.kind.substring(representation.kind.indexOf('?') + 1, representation.kind.length); @@ -486,6 +487,12 @@ const omniboxCommandOverrides: OmniboxCommandOverrideContribution[] = [ }, component: PublishStudioLibraryCommand, }, + { + canHandle: (action: GQLOmniboxCommand) => { + return action.id === 'importLibrary'; + }, + component: ImportLibraryCommand, + }, ]; defaultExtensionRegistry.putData( diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryCommand.tsx b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryCommand.tsx new file mode 100644 index 0000000000..9859be74e2 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryCommand.tsx @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2025 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 + *******************************************************************************/ + +import { OmniboxCommandComponentProps } from '@eclipse-sirius/sirius-components-omnibox'; +import { useState } from 'react'; +import { ImportLibraryCommandState } from './ImportLibraryCommand.types'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import { IconOverlay } from '@eclipse-sirius/sirius-components-core'; +import { ImportLibraryDialog } from './ImportLibraryDialog'; + +export const ImportLibraryCommand = ({ command, onKeyDown, onClose }: OmniboxCommandComponentProps) => { + const [state, setState] = useState({ + open: false, + }); + + const handleClick = () => setState((prevState) => ({ ...prevState, open: true })); + + return ( + <> + + + + + {command.label} + + {state.open && } + + ); +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryCommand.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryCommand.types.ts new file mode 100644 index 0000000000..b6b456edba --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryCommand.types.ts @@ -0,0 +1,16 @@ +/******************************************************************************* + * Copyright (c) 2025 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 + *******************************************************************************/ + +export interface ImportLibraryCommandState { + open: boolean; +} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryDialog.tsx b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryDialog.tsx new file mode 100644 index 0000000000..3d83960bdf --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryDialog.tsx @@ -0,0 +1,191 @@ +/******************************************************************************* + * Copyright (c) 2025 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 + *******************************************************************************/ +import DialogTitle from '@mui/material/DialogTitle'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import Button from '@mui/material/Button'; +import DialogActions from '@mui/material/DialogActions'; +import { LibrariesImportTable } from './LibrariesImportTable'; +import { useEffect, useRef, useState } from 'react'; +import Typography from '@mui/material/Typography'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { useImportLibraries } from './useImportLibraries'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Grow from '@mui/material/Grow'; +import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; +import { + ImportLibraryAction, + ImportLibraryDialogProps, + ImportLibraryDialogState, + ImportStudioSplitButtonProps, + ImportStudioSplitButtonState, +} from './ImportLibraryDialog.types'; +import { useCurrentProject } from '../views/edit-project/useCurrentProject'; + +export const ImportLibraryDialog = ({ open, title, onClose }: ImportLibraryDialogProps) => { + const [state, setState] = useState({ + selectedLibraries: [], + actions: [ + { id: 'import', label: 'Import' }, + { id: 'copy', label: 'Copy' }, + ], + }); + + const onSelectionChange = (selection) => { + setState((prevState) => ({ + ...prevState, + selectedLibraries: selection, + })); + }; + + const { importLibraries, data } = useImportLibraries(); + useEffect(() => { + if (data) { + onClose(); + } + }, [data]); + + const { project } = useCurrentProject(); + + const onImportLibraries = (action: ImportLibraryAction) => { + importLibraries(project.currentEditingContext.id, action.id, state.selectedLibraries); + }; + + return ( + + {title} + + + + + {state.actions.length > 0 ? ( + Apply to {state.selectedLibraries.length} libraries: + ) : ( + + No import action found for the selected libraries + + )} + + + + + ); +}; + +export const ImportStudioSplitButton = ({ actions, onClick }: ImportStudioSplitButtonProps) => { + const initialState: ImportStudioSplitButtonState = { + selected: false, + open: false, + selectedIndex: 0, + message: '', + actions, + }; + + const [state, setState] = useState(initialState); + + const buttonGroupRef = useRef(null); + const widgetRef = useRef(null); + + const handleMenuItemClick = (_event, index) => { + setState((prevState) => ({ ...prevState, open: false, selectedIndex: index })); + }; + + const handleToggle = () => { + setState((prevState) => ({ ...prevState, open: !prevState.open })); + }; + + const handleClose = (event) => { + event.preventDefault(); + setState((prevState) => ({ ...prevState, open: false })); + }; + + const handleClick = () => { + const selectedAction = state.actions[state.selectedIndex]; + onClick(selectedAction); + }; + + return ( +
+ + setState((prevState) => { + return { + ...prevState, + selected: true, + }; + }) + } + onBlur={() => + setState((prevState) => { + return { + ...prevState, + selected: false, + }; + }) + }> + + + + + {({ TransitionProps, placement }) => ( + + + + + {state.actions.map((action, index) => ( + handleMenuItemClick(event, index)}> + {action.label} + + ))} + + + + + )} + +
+ ); +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryDialog.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryDialog.types.ts new file mode 100644 index 0000000000..a1e37fb9a1 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/ImportLibraryDialog.types.ts @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2025 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 + *******************************************************************************/ + +export interface ImportLibraryDialogProps { + open: boolean; + title: string; + onClose: () => void; +} + +export interface ImportLibraryDialogState { + selectedLibraries: string[]; + actions: ImportLibraryAction[]; +} + +export interface ImportLibraryAction { + id: string; + label: string; +} + +export interface ImportStudioSplitButtonProps { + actions: ImportLibraryAction[]; + onClick: (action: ImportLibraryAction) => void; +} + +export interface ImportStudioSplitButtonState { + actions: ImportLibraryAction[]; + selected: boolean; + open: boolean; + selectedIndex: number; + message: string; +} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/LibrariesImportTable.tsx b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/LibrariesImportTable.tsx new file mode 100644 index 0000000000..d04f5ab68d --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/LibrariesImportTable.tsx @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2025 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 + *******************************************************************************/ +import Typography from '@mui/material/Typography'; +import { + MaterialReactTable, + MRT_ColumnDef, + MRT_PaginationState, + MRT_RowSelectionState, + useMaterialReactTable, +} from 'material-react-table'; +import { useState, useEffect, useMemo } from 'react'; +import { useLibraries } from '../views/library-browser/useLibraries'; +import { GQLLibrary } from '../views/library-browser/useLibraries.types'; +import { LibrariesImportTableProps, LibrariesImportTableState } from './LibrariesImportTable.types'; + +export const LibrariesImportTable = ({ onSelectionChange }: LibrariesImportTableProps) => { + const [state, setState] = useState({ + data: null, + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 20, + }); + + const rows: GQLLibrary[] = state.data?.viewer.libraries.edges.map((edge) => edge.node) || []; + const count: number = state.data?.viewer.libraries.pageInfo.count ?? 0; + + const { data } = useLibraries(pagination.pageIndex, pagination.pageSize); + useEffect(() => { + if (data) { + setState((prevState) => ({ ...prevState, data })); + } + }, [data]); + + const [rowSelection, setRowSelection] = useState({}); + + useEffect(() => { + onSelectionChange(Object.keys(rowSelection)); + }, [rowSelection]); + + const columns = useMemo[]>( + () => [ + { + accessorFn: (row) => row.name, + header: 'Name', + size: 200, + Cell: ({ renderedCellValue }) => {renderedCellValue}, + }, + { + accessorFn: (row) => row.version, + header: 'Version', + size: 50, + Cell: ({ renderedCellValue }) => {renderedCellValue}, + }, + { + accessorFn: (row) => new Date(row.createdOn).toISOString().split('T')[0], + header: 'Created On', + size: 50, + Cell: ({ renderedCellValue }) => {renderedCellValue}, + }, + { + accessorFn: (row) => row.namespace, + header: 'Namespace', + size: 150, + Cell: ({ renderedCellValue }) => ( + + {renderedCellValue} + + ), + }, + { + accessorFn: (row) => row.description, + header: 'Description', + size: 200, + grow: true, + Cell: ({ renderedCellValue }) => {renderedCellValue}, + }, + ], + [] + ); + + const table = useMaterialReactTable({ + // Data + columns, + data: rows, + + // Disable some unnecessary features (overkill here) + enableColumnActions: false, + enableColumnFilters: false, + enableFullScreenToggle: false, + enableDensityToggle: false, + enableHiding: false, + enableSorting: false, + + // Configure selection + enableRowSelection: true, + getRowId: (originalRow) => originalRow.id, + onRowSelectionChange: setRowSelection, + + // Configure pagination + enablePagination: true, + manualPagination: true, + onPaginationChange: setPagination, + rowCount: count, + + state: { pagination, rowSelection }, + }); + + return ; +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/LibrariesImportTable.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/LibrariesImportTable.types.ts new file mode 100644 index 0000000000..7aafe317a6 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/LibrariesImportTable.types.ts @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2025 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 + *******************************************************************************/ +import { GQLGetLibrariesQueryData } from '../views/library-browser/useLibraries.types'; + +export interface LibrariesImportTableProps { + onSelectionChange(selection: string[]); +} + +export interface LibrariesImportTableState { + data: GQLGetLibrariesQueryData | null; +} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/useImportLibraries.ts b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/useImportLibraries.ts new file mode 100644 index 0000000000..6ca61f3da8 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/useImportLibraries.ts @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2025 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 + *******************************************************************************/ + +import { gql, useMutation } from '@apollo/client'; +import { useMultiToast } from '@eclipse-sirius/sirius-components-core'; +import { useEffect } from 'react'; +import { + GQLErrorPayload, + GQLImportLibrariesMutationData, + GQLImportLibrariesMutationVariables, + GQLImportLibrariesPayload, + GQLSuccessPayload, + UseImportLibrariesValue, +} from './useImportLibraries.types'; + +const importLibrariesMutation = gql` + mutation importLibraries($input: ImportLibrariesInput!) { + importLibraries(input: $input) { + __typename + ... on SuccessPayload { + messages { + level + body + } + } + ... on ErrorPayload { + message + messages { + level + body + } + } + } + } +`; + +const isErrorPayload = (payload: GQLImportLibrariesPayload): payload is GQLErrorPayload => + payload.__typename === 'ErrorPayload'; + +const isSuccessPayload = (payload: GQLImportLibrariesPayload): payload is GQLSuccessPayload => + payload.__typename === 'SuccessPayload'; + +export const useImportLibraries = (): UseImportLibrariesValue => { + const { addErrorMessage, addMessages } = useMultiToast(); + const [performImportLibraries, { loading, data, error }] = useMutation< + GQLImportLibrariesMutationData, + GQLImportLibrariesMutationVariables + >(importLibrariesMutation); + + useEffect(() => { + if (data) { + const { importLibraries } = data; + if (isErrorPayload(importLibraries)) { + addMessages(importLibraries.messages); + } + if (isSuccessPayload(importLibraries)) { + addMessages(importLibraries.messages); + } + } + if (error) { + addErrorMessage('An unexpected error has occurred, please refresh the page'); + } + }, [error, data]); + + const importLibraries = (editingContextId: string, type: string, libraryIds: string[]) => { + const variables: GQLImportLibrariesMutationVariables = { + input: { + id: crypto.randomUUID(), + editingContextId, + type, + libraryIds, + }, + }; + performImportLibraries({ variables }); + }; + + return { + importLibraries, + loading, + data: data?.importLibraries ?? null, + }; +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/useImportLibraries.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/useImportLibraries.types.ts new file mode 100644 index 0000000000..e39d9e318d --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/useImportLibraries.types.ts @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2025 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 + *******************************************************************************/ + +import { GQLMessage } from '@eclipse-sirius/sirius-components-core'; + +export interface UseImportLibrariesValue { + importLibraries: (projectId: string, type: string, libraryIds: string[]) => void; + loading: boolean; + data: GQLImportLibrariesPayload | null; +} + +export interface GQLImportLibrariesMutationVariables { + input: GQLImportLibrariesMutationInput; +} + +export interface GQLImportLibrariesMutationInput { + id: string; + editingContextId: string; + type: string; + libraryIds: string[]; +} + +export interface GQLImportLibrariesMutationData { + importLibraries: GQLImportLibrariesPayload; +} + +export interface GQLImportLibrariesPayload { + __typename: string; +} + +export interface GQLSuccessPayload extends GQLImportLibrariesPayload { + messages: GQLMessage[]; +} + +export interface GQLErrorPayload extends GQLImportLibrariesPayload { + messages: GQLMessage[]; +} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/library-browser/useLibraries.fragments.ts b/packages/sirius-web/frontend/sirius-web-application/src/views/library-browser/useLibraries.fragments.ts index 1650233886..0aa68af7f5 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/views/library-browser/useLibraries.fragments.ts +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/library-browser/useLibraries.fragments.ts @@ -34,6 +34,7 @@ export const ViewerLibrariesFragment = gql` export const LibraryFragment = gql` fragment Library on Library { + id namespace name version diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/library-browser/useLibraries.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/views/library-browser/useLibraries.types.ts index 2a35acb7fc..832124f6f7 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/views/library-browser/useLibraries.types.ts +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/library-browser/useLibraries.types.ts @@ -40,6 +40,7 @@ export interface GQLViewerLibraryEdge { } export interface GQLLibrary { + id: string; namespace: string; name: string; version: string;