From ef0ead5e51a9b349264541dbc434be512bd5cf2e Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:22:16 +0300 Subject: [PATCH 01/16] feat: added mcp trigger --- .../ConfigurationPropertiesConstants.java | 1 + .../builders/McpTriggerPropertiesBuilder.java | 37 +++++++++ .../elements/mcp-trigger/description.yml | 77 +++++++++++++++++++ .../elements/mcp-trigger/template.hbs | 5 ++ 4 files changed, 120 insertions(+) create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java create mode 100644 src/main/resources/elements/mcp-trigger/description.yml create mode 100644 src/main/resources/elements/mcp-trigger/template.hbs diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/consul/ConfigurationPropertiesConstants.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/consul/ConfigurationPropertiesConstants.java index ab868032..5242f000 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/consul/ConfigurationPropertiesConstants.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/consul/ConfigurationPropertiesConstants.java @@ -50,6 +50,7 @@ public class ConfigurationPropertiesConstants { public static final String GRPC_SENDER_ELEMENT = "grpc-sender"; public static final String CHAIN_CALL_2_ELEMENT = "chain-call-2"; public static final String HTTP_TRIGGER_ELEMENT = "http-trigger"; + public static final String MCP_TRIGGER_ELEMENT = "mcp-trigger"; public static final String SERVICE_CALL_ELEMENT = "service-call"; public static final String CHAIN_CALL_PROPERTY_OPTION = "chain-call"; public static final String HTTP_TRIGGER_FAILURE_HANDLER_ACTION = "handleChainFailureAction"; diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java new file mode 100644 index 00000000..5de3c04b --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java @@ -0,0 +1,37 @@ +package org.qubership.integration.platform.runtime.catalog.service.deployment.properties.builders; + +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.chain.element.ChainElement; +import org.qubership.integration.platform.runtime.catalog.service.deployment.properties.ElementPropertiesBuilder; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.qubership.integration.platform.runtime.catalog.consul.ConfigurationPropertiesConstants.*; + +@Component +public class McpTriggerPropertiesBuilder implements ElementPropertiesBuilder { + @Override + public boolean applicableTo(ChainElement element) { + String type = element.getType(); + return MCP_TRIGGER_ELEMENT.equals(type); + } + + @Override + public Map build(ChainElement element) { + return Stream.of( + "name", + "title", + "description", + "inputSchema", + "outputSchema", + "readOnly", + "destructive", + "idempotent", + "openWorld", + "requiresLocal" + ).collect(Collectors.toMap(Function.identity(), element::getPropertyAsString)); + } +} diff --git a/src/main/resources/elements/mcp-trigger/description.yml b/src/main/resources/elements/mcp-trigger/description.yml new file mode 100644 index 00000000..7d132e03 --- /dev/null +++ b/src/main/resources/elements/mcp-trigger/description.yml @@ -0,0 +1,77 @@ +# Copyright 2024-2025 NetCracker Technology Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: mcp-trigger +title: MCP Trigger +description: Expose integration chain as a MCP tool or resource. +type: trigger +folder: triggers +inputEnabled: false +colorType: trigger +outputEnabled: true +container: false +customTabs: + - name: Idempotency + uiComponent: idempotency-parameters +properties: + common: + - name: name + title: Identifier + type: string + mandatory: true + - name: title + title: Title + description: Human-readable name of the tool for display purposes + type: string + mandatory: false + - name: description + title: Description + description: Human-readable description of functionality + type: string + mandatory: true + - name: inputSchema + title: Input schema + description: JSON Schema defining expected parameters + type: string + mandatory: true + - name: outputSchema + title: Output schema + description: Optional JSON Schema defining expected output structure + type: string + mandatory: false + - name: readOnly + title: Tool doesn't modify its environment + description: Indicates the tool only performs read operations and does not change any external state. + type: boolean + default: false + - name: destructive + type: boolean + default: false + title: Tool may perform destructive updates (delete/overwrite data) + description: Signals that the tool will modify or delete data, often used to trigger a "human-in-the-loop" (HITL) approval step. + - name: idempotent + type: boolean + default: false + title: Calling repeatedly with same args has no additional effect + description: Confirms that running the tool multiple times with the same inputs will produce the same result without unintended side effects. + - name: openWorld + type: boolean + default: false + title: Tool may interact with external entities beyond its local environment + description: Defines if the tool interacts with an unpredictable external environment (like a web search) or a closed, well-defined system. + - name: requiresLocal + type: boolean + default: false + title: Tool needs local resources or execution + description: A specialized hint (common in Azure MCP) indicating the tool needs local resources or execution. diff --git a/src/main/resources/elements/mcp-trigger/template.hbs b/src/main/resources/elements/mcp-trigger/template.hbs new file mode 100644 index 00000000..80481dc2 --- /dev/null +++ b/src/main/resources/elements/mcp-trigger/template.hbs @@ -0,0 +1,5 @@ + + + + {{> idempotency}} + From 61422ea25d72837d21a07ae091474c3cfda0c45b Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:48:48 +0300 Subject: [PATCH 02/16] fix: handled null values --- .../properties/builders/McpTriggerPropertiesBuilder.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java index 5de3c04b..2644580c 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Component; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -32,6 +33,10 @@ public Map build(ChainElement element) { "idempotent", "openWorld", "requiresLocal" - ).collect(Collectors.toMap(Function.identity(), element::getPropertyAsString)); + ).collect(Collectors.toMap( + Function.identity(), + key -> Optional.ofNullable(element.getProperties().get(key)) + .map(String::valueOf) + .orElse(""))); } } From c03534fe513a5095be677571fcbb21aa7f102192 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:55:21 +0300 Subject: [PATCH 03/16] feat: added a default value for the input schema --- src/main/resources/elements/mcp-trigger/description.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/elements/mcp-trigger/description.yml b/src/main/resources/elements/mcp-trigger/description.yml index 7d132e03..05d8e579 100644 --- a/src/main/resources/elements/mcp-trigger/description.yml +++ b/src/main/resources/elements/mcp-trigger/description.yml @@ -45,6 +45,7 @@ properties: description: JSON Schema defining expected parameters type: string mandatory: true + default: '{ "type": "object", "additionalProperties": false }' - name: outputSchema title: Output schema description: Optional JSON Schema defining expected output structure From 7f1e81204896a6340fdf5b18a0778edcd6073c36 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 2 Apr 2026 19:24:40 +0300 Subject: [PATCH 04/16] feat: added a mcp system --- .../catalog/model/constant/CamelNames.java | 2 + .../GeneralImportInstructionsConfig.java | 3 + .../system/MCPServiceContentDto.java | 34 ++ .../exportimport/system/MCPServiceDto.java | 27 ++ .../catalog/model/filter/FilterFeature.java | 4 +- .../system/exportimport/ExportableObject.java | 2 + .../exportimport/ExportedMCPSystemObject.java | 18 + .../configs/entity/actionlog/EntityType.java | 1 + .../configs/entity/mcp/MCPSystem.java | 61 +++ .../configs/entity/mcp/MCPSystemLabel.java | 54 +++ .../repository/chain/ChainRepository.java | 12 + .../repository/mcp/MCPSystemRepository.java | 9 + .../v1/controller/MCPSystemController.java | 174 +++++++ .../system/mcp/MCPSystemCreateRequestDTO.java | 26 + .../dto/system/mcp/MCPSystemResponseDTO.java | 27 ++ .../system/mcp/MCPSystemUpdateRequestDTO.java | 26 + .../rest/v1/mapper/MCPSystemMapper.java | 38 ++ .../exportimport/ImportPreviewResponse.java | 3 + .../catalog/service/MCPSystemService.java | 151 ++++++ .../builders/McpTriggerPropertiesBuilder.java | 1 + .../ContextExportImportService.java | 12 +- .../exportimport/ExportImportConstants.java | 3 + .../exportimport/GeneralImportService.java | 8 +- .../MCPSystemImportExportService.java | 452 ++++++++++++++++++ .../SystemExportImportService.java | 6 +- .../deserializer/MCPSystemDeserializer.java | 53 ++ .../mapper/services/MCPServiceDtoMapper.java | 74 +++ .../mcp/MCPServiceImportFileMigration.java | 6 + .../V100MCPServiceImportFileMigration.java | 21 + .../serializer/ArchiveWriter.java | 37 ++ .../serializer/ContextServiceSerializer.java | 35 +- .../ExportableObjectWriterVisitor.java | 6 + .../serializer/MCPSystemSerializer.java | 38 ++ .../serializer/ServiceSerializer.java | 23 - .../MCPSystemFilterSpecificationBuilder.java | 87 ++++ .../catalog/util/ExportImportUtils.java | 6 + src/main/resources/application.yml | 1 + .../configs/V110_000__add-mcp-systems.sql | 31 ++ 38 files changed, 1513 insertions(+), 59 deletions(-) create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/system/MCPServiceContentDto.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/system/MCPServiceDto.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/model/system/exportimport/ExportedMCPSystemObject.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/mcp/MCPSystem.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/mcp/MCPSystemLabel.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/mcp/MCPSystemRepository.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemCreateRequestDTO.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemResponseDTO.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemUpdateRequestDTO.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportService.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/deserializer/MCPSystemDeserializer.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/mapper/services/MCPServiceDtoMapper.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/migrations/mcp/MCPServiceImportFileMigration.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/migrations/mcp/V100MCPServiceImportFileMigration.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ArchiveWriter.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/MCPSystemSerializer.java create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java create mode 100644 src/main/resources/db/migration/postgresql/configs/V110_000__add-mcp-systems.sql diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/constant/CamelNames.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/constant/CamelNames.java index fd7ce340..eca90c6f 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/constant/CamelNames.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/constant/CamelNames.java @@ -129,6 +129,8 @@ public final class CamelNames { public static final String CHAIN_CALL_ELEMENT_ID = "elementId"; public static final String REUSE_ESTABLISHED_CONN = "reuseEstablishedConnection"; + public static final String MCP_SERVICE_ID = "mcpServiceId"; + private CamelNames() { } diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/instructions/GeneralImportInstructionsConfig.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/instructions/GeneralImportInstructionsConfig.java index 5d65e9a5..74988cbc 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/instructions/GeneralImportInstructionsConfig.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/instructions/GeneralImportInstructionsConfig.java @@ -47,6 +47,9 @@ public class GeneralImportInstructionsConfig { private ImportInstructionsConfig contextServices = new ImportInstructionsConfig(); @Valid @Builder.Default + private ImportInstructionsConfig mcpServices = new ImportInstructionsConfig(); + @Valid + @Builder.Default @JsonIgnoreProperties(value = "ignore") private ImportInstructionsConfig specificationGroups = new ImportInstructionsConfig(); @Valid diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/system/MCPServiceContentDto.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/system/MCPServiceContentDto.java new file mode 100644 index 00000000..93046160 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/system/MCPServiceContentDto.java @@ -0,0 +1,34 @@ +package org.qubership.integration.platform.runtime.catalog.model.exportimport.system; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.User; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@SuperBuilder +@Jacksonized +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +public class MCPServiceContentDto { + private String description; + private String identifier; + private String instructions; + private Timestamp createdWhen; + private Timestamp modifiedWhen; + private User createdBy; + private User modifiedBy; + private String migrations; + + @Builder.Default + private List labels = new ArrayList<>(); +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/system/MCPServiceDto.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/system/MCPServiceDto.java new file mode 100644 index 00000000..4c29f79a --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/exportimport/system/MCPServiceDto.java @@ -0,0 +1,27 @@ +package org.qubership.integration.platform.runtime.catalog.model.exportimport.system; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.net.URI; + +@Getter +@Setter +@SuperBuilder +@Jacksonized +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "id", "schema", "name", "content" }) +public class MCPServiceDto { + @JsonProperty(value = "$schema", index = 0) + private URI schema; + private String id; + private String name; + MCPServiceContentDto content; +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/filter/FilterFeature.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/filter/FilterFeature.java index fb67390a..746ab99e 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/filter/FilterFeature.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/filter/FilterFeature.java @@ -58,5 +58,7 @@ public enum FilterFeature { SESSION_DURATION, EXCHANGE_DURATION, MAIN_THREAD, - POD_IP + POD_IP, + IDENTIFIER, + INSTRUCTIONS, } diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/system/exportimport/ExportableObject.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/system/exportimport/ExportableObject.java index 82d4af8c..e2a1337d 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/system/exportimport/ExportableObject.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/system/exportimport/ExportableObject.java @@ -22,5 +22,7 @@ import java.util.zip.ZipOutputStream; public interface ExportableObject { + String getId(); + void accept(ExportableObjectWriterVisitor visitor, ZipOutputStream zipOut, String entryPath) throws IOException; } diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/system/exportimport/ExportedMCPSystemObject.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/system/exportimport/ExportedMCPSystemObject.java new file mode 100644 index 00000000..feb33e53 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/system/exportimport/ExportedMCPSystemObject.java @@ -0,0 +1,18 @@ +package org.qubership.integration.platform.runtime.catalog.model.system.exportimport; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer.ExportableObjectWriterVisitor; + +import java.io.IOException; +import java.util.zip.ZipOutputStream; + +public class ExportedMCPSystemObject extends ExportedSystemObject { + public ExportedMCPSystemObject(String id, ObjectNode objectNode) { + super(id, objectNode); + } + + @Override + public void accept(ExportableObjectWriterVisitor visitor, ZipOutputStream zipOut, String entryPath) throws IOException { + visitor.visit(this, zipOut, entryPath); + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/actionlog/EntityType.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/actionlog/EntityType.java index 5bc256d1..b716ea22 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/actionlog/EntityType.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/actionlog/EntityType.java @@ -33,6 +33,7 @@ public enum EntityType { CHAIN_RUNTIME_PROPERTIES, DATABASE_SYSTEM, //removed databases in 24.3 CONTEXT_SYSTEM, + MCP_SYSTEM, DATABASE_SCRIPT, //This types remained to avoid error with old actions(in action log) with databases SERVICE_DISCOVERY, EXTERNAL_SERVICE, diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/mcp/MCPSystem.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/mcp/MCPSystem.java new file mode 100644 index 00000000..2850a749 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/mcp/MCPSystem.java @@ -0,0 +1,61 @@ +package org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Transient; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldNameConstants; +import lombok.experimental.SuperBuilder; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.chain.Chain; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.system.AbstractSystemEntity; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static jakarta.persistence.CascadeType.*; + +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@FieldNameConstants +@Entity(name = "mcp_systems") +public class MCPSystem extends AbstractSystemEntity { + @Column + private String identifier; + + @Column + private String instructions; + + @Builder.Default + @OneToMany(mappedBy = "system", + orphanRemoval = true, + cascade = {PERSIST, REMOVE, MERGE} + ) + private Set labels = new LinkedHashSet<>(); + + @Transient + private List chains; + + @Override + public boolean equals(Object o) { + if (!super.equals(o)) { + return false; + } + if (!(o instanceof MCPSystem mcpSystem)) { + return false; + } + return Objects.equals(identifier, mcpSystem.identifier) && Objects.equals(instructions, mcpSystem.instructions); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), getIdentifier(), getInstructions(), getChains()); + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/mcp/MCPSystemLabel.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/mcp/MCPSystemLabel.java new file mode 100644 index 00000000..ea622656 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/entity/mcp/MCPSystemLabel.java @@ -0,0 +1,54 @@ +package org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import lombok.extern.slf4j.Slf4j; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.AbstractLabel; + +import java.util.Objects; + +@Getter +@Setter +@Slf4j +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@Entity(name = "mcp_system_labels") +public class MCPSystemLabel extends AbstractLabel { + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "mcp_system_id") + private MCPSystem system; + + public MCPSystemLabel(String name, MCPSystem system) { + super(name); + this.system = system; + } + + public MCPSystemLabel(String name, boolean technical, MCPSystem system) { + super(name, technical); + this.system = system; + } + + @Override + public boolean equals(Object o) { + if (!super.equals(o)) { + return false; + } + MCPSystemLabel that = (MCPSystemLabel) o; + return Objects.equals(getSystem(), that.getSystem()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), getSystem()); + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/chain/ChainRepository.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/chain/ChainRepository.java index 10f4476d..41b04a45 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/chain/ChainRepository.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/chain/ChainRepository.java @@ -86,6 +86,18 @@ WITH RECURSIVE parent_folders AS ( ) List findAllChainsInFolders(List folderIds); + @Query( + nativeQuery = true, + value = """ + select distinct on (chain.id) chain.* + from catalog.chains chain + inner join catalog.elements element + on element.chain_id = chain.id + where jsonb_extract_path_text(element.properties, :property) = :value + """ + ) + List findChainsWithElementPropertyValue(String property, String value); + @Query( nativeQuery = true, value = """ diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/mcp/MCPSystemRepository.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/mcp/MCPSystemRepository.java new file mode 100644 index 00000000..9405e086 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/mcp/MCPSystemRepository.java @@ -0,0 +1,9 @@ +package org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.mcp; + +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface MCPSystemRepository extends JpaRepository, JpaSpecificationExecutor { + +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java new file mode 100644 index 00000000..ee2c4630 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java @@ -0,0 +1,174 @@ +package org.qubership.integration.platform.runtime.catalog.rest.v1.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.extensions.Extension; +import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.system.ImportSystemResult; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.FilterRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemSearchRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.imports.ImportSystemStatus; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemCreateRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemResponseDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemUpdateRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.mapper.MCPSystemMapper; +import org.qubership.integration.platform.runtime.catalog.service.MCPSystemService; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.MCPSystemImportExportService; +import org.qubership.integration.platform.runtime.catalog.util.ExportImportUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static java.util.Objects.isNull; + +@Slf4j +@ComponentScan +@RestController +@CrossOrigin(origins = "*") +@RequestMapping(value = "/v1/catalog/mcp-system", produces = MediaType.APPLICATION_JSON_VALUE) +@Tag(name = "mcp-system-controller", description = "MCP System Controller") +public class MCPSystemController { + private final MCPSystemService mcpSystemService; + private final MCPSystemMapper mcpSystemMapper; + private final MCPSystemImportExportService mcpSystemImportExportService; + + @Autowired + public MCPSystemController( + MCPSystemService mcpSystemService, + MCPSystemMapper mcpSystemMapper, + MCPSystemImportExportService mcpSystemImportExportService + ) { + this.mcpSystemService = mcpSystemService; + this.mcpSystemMapper = mcpSystemMapper; + this.mcpSystemImportExportService = mcpSystemImportExportService; + } + + @GetMapping + @Operation(description = "Get all MCP systems") + public ResponseEntity> getAll( + @RequestParam(name = "withChains", defaultValue = "false") + boolean withChains + ) { + log.debug("Request to get all MCP systems"); + List systems = mcpSystemService.findAll(withChains); + List dtos = mcpSystemMapper.toResponseDtos(systems); + return ResponseEntity.ok(dtos); + } + + @PostMapping + @Operation(description = "Create MCP system") + public ResponseEntity create( + @RequestBody MCPSystemCreateRequestDTO requestDTO + ) { + log.debug("Request to create MCP system: {}", requestDTO); + MCPSystem system = mcpSystemService.create(requestDTO); + MCPSystemResponseDTO dto = mcpSystemMapper.toResponseDto(system); + return ResponseEntity.status(HttpStatus.CREATED).body(dto); + } + + @PutMapping("/{id}") + @Operation(description = "Update MCP system") + public ResponseEntity update( + @PathVariable String id, + @RequestBody MCPSystemUpdateRequestDTO requestDTO + ) { + log.debug("Request to update MCP system: {}", requestDTO); + MCPSystem system = mcpSystemService.update(id, requestDTO); + MCPSystemResponseDTO dto = mcpSystemMapper.toResponseDto(system); + return ResponseEntity.ok(dto); + } + + @DeleteMapping("/{id}") + @Operation(description = "Delete MCP system") + public ResponseEntity delete(@PathVariable String id) { + log.debug("Request to delete MCP system: {}", id); + mcpSystemService.deleteById(id); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/search") + @Operation(description = "Search MCP systems") + public ResponseEntity> searchSystems( + @RequestBody SystemSearchRequestDTO systemSearchRequestDTO + ) { + log.debug("Request to search MCP systems: {}", systemSearchRequestDTO); + List systems = mcpSystemService.searchSystems(systemSearchRequestDTO); + List dtos = mcpSystemMapper.toResponseDtos(systems); + return ResponseEntity.ok(dtos); + } + + @PostMapping("/filter") + @Operation(description = "Filter MCP systems") + public ResponseEntity> filter( + @RequestBody List filters + ) { + log.debug("Request to filter MCP systems: {}", filters); + List systems = mcpSystemService.filter(filters); + List dtos = mcpSystemMapper.toResponseDtos(systems); + return ResponseEntity.ok(dtos); + } + + @PostMapping(value = "/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + @Operation(description = "Export MCP services") + public ResponseEntity export( + @RequestParam(required = false) + @Parameter(description = "List of system IDs") + List ids + ) { + byte[] data = mcpSystemImportExportService.export(ids); + if (isNull(data)) { + return ResponseEntity.noContent().build(); + } + + return ExportImportUtils.convertFileToResponse(data, ExportImportUtils.generateArchiveExportName()); + } + + @PostMapping("/import") + @Operation( + extensions = @Extension( + properties = {@ExtensionProperty(name = "x-api-kind", value = "bwc")} + ), + description = "Import MCP services from a file" + ) + public ResponseEntity> importSystems( + @RequestParam("file") + @Parameter(description = "File") + MultipartFile file, + + @RequestParam(required = false) + @Parameter(description = "List of system IDs") + List ids + ) { + List result = mcpSystemImportExportService.importSystems(file, ids); + if (result.isEmpty()) { + return ResponseEntity.noContent().build(); + } else { + HttpStatus responseCode = result.stream().anyMatch(dto -> dto.getStatus().equals(ImportSystemStatus.ERROR)) + ? HttpStatus.MULTI_STATUS + : HttpStatus.OK; + return ResponseEntity.status(responseCode).body(result); + } + } + + @PostMapping("/import/preview") + @Operation(description = "Get preview on what will be imported from file") + public ResponseEntity> getImportPreview( + @RequestParam("file") + @Parameter(description = "File") + MultipartFile file + ) { + List result = mcpSystemImportExportService.getImportPreview(file); + return result.isEmpty() + ? ResponseEntity.noContent().build() + : ResponseEntity.ok().body(result); + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemCreateRequestDTO.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemCreateRequestDTO.java new file mode 100644 index 00000000..0ab2a390 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemCreateRequestDTO.java @@ -0,0 +1,26 @@ +package org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemLabelDTO; + +import java.util.List; + +@Data +@Schema(description = "Create MCP Service request object") +public class MCPSystemCreateRequestDTO { + @Schema(description = "Name") + private String name; + + @Schema(description = "Description") + private String description; + + @Schema(description = "MCP server name") + private String identifier; + + @Schema(description = "MCP server instructions") + private String instructions; + + @Schema(description = "Labels assigned to the service") + private List labels; +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemResponseDTO.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemResponseDTO.java new file mode 100644 index 00000000..21add3de --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemResponseDTO.java @@ -0,0 +1,27 @@ +package org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.qubership.integration.platform.runtime.catalog.model.dto.BaseResponse; +import org.qubership.integration.platform.runtime.catalog.model.dto.user.UserDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemLabelDTO; + +import java.util.List; + +@Data +@Schema(description = "Create MCP Service request object") +public class MCPSystemResponseDTO { + private String id; + private String name; + private String description; + private String identifier; + private String instructions; + private Long createdWhen; + private UserDTO createdBy; + private Long modifiedWhen; + private UserDTO modifiedBy; + private List labels; + + @Schema(description = "List of chains that is using current service") + private List chains; +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemUpdateRequestDTO.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemUpdateRequestDTO.java new file mode 100644 index 00000000..05ee42a4 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemUpdateRequestDTO.java @@ -0,0 +1,26 @@ +package org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemLabelDTO; + +import java.util.List; + +@Data +@Schema(description = "Update MCP Service request object") +public class MCPSystemUpdateRequestDTO { + @Schema(description = "Name") + private String name; + + @Schema(description = "description") + private String description; + + @Schema(description = "MCP server name") + private String identifier; + + @Schema(description = "MCP server instructions") + private String instructions; + + @Schema(description = "Labels assigned to the service") + private List labels; +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java new file mode 100644 index 00000000..427e1f79 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java @@ -0,0 +1,38 @@ +package org.qubership.integration.platform.runtime.catalog.rest.v1.mapper; + +import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystemLabel; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemLabelDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemResponseDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemUpdateRequestDTO; +import org.qubership.integration.platform.runtime.catalog.util.MapperUtils; + +import java.util.List; + +@Mapper( + componentModel = "spring", + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, + collectionMappingStrategy = CollectionMappingStrategy.SETTER_PREFERRED, + uses = { + MapperUtils.class + } +) +public interface MCPSystemMapper { + List toResponseDtos(List systems); + + MCPSystemResponseDTO toResponseDto(MCPSystem system); + + MCPSystem update(@MappingTarget MCPSystem contextSystem, MCPSystemUpdateRequestDTO request); + + MCPSystemLabel asLabel(SystemLabelDTO labelDTO); + + List asLabels(List labelDTOs); + + SystemLabelDTO asLabelDTO(MCPSystemLabel label); + + List asLabelDTOs(List labels); +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v3/dto/exportimport/ImportPreviewResponse.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v3/dto/exportimport/ImportPreviewResponse.java index 43af252e..26b1e8dc 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v3/dto/exportimport/ImportPreviewResponse.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v3/dto/exportimport/ImportPreviewResponse.java @@ -49,6 +49,9 @@ public class ImportPreviewResponse { @Schema(description = "List of results by each context service") private List contextService = new ArrayList<>(); @Builder.Default + @Schema(description = "List of results by each MCP service") + private List mcpService = new ArrayList<>(); + @Builder.Default @Schema(description = "List of results by each variable") private List variables = new ArrayList<>(); @Schema(description = "Import instructions preview") diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java new file mode 100644 index 00000000..d09ad2ce --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java @@ -0,0 +1,151 @@ +package org.qubership.integration.platform.runtime.catalog.service; + +import jakarta.persistence.EntityNotFoundException; +import org.qubership.integration.platform.runtime.catalog.exception.exceptions.SystemDeleteException; +import org.qubership.integration.platform.runtime.catalog.model.constant.CamelNames; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.actionlog.ActionLog; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.actionlog.EntityType; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.actionlog.LogOperation; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.chain.Chain; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.chain.ChainRepository; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.mcp.MCPSystemRepository; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.FilterRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemSearchRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemCreateRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemUpdateRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.mapper.MCPSystemMapper; +import org.qubership.integration.platform.runtime.catalog.service.filter.MCPSystemFilterSpecificationBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional +public class MCPSystemService { + private final MCPSystemRepository mcpSystemRepository; + private final MCPSystemMapper mcpSystemMapper; + private final ActionsLogService actionLogger; + private final MCPSystemFilterSpecificationBuilder mcpSystemFilterSpecificationBuilder; + private final ChainRepository chainRepository; + + @Autowired + public MCPSystemService( + MCPSystemRepository mcpSystemRepository, + MCPSystemMapper mcpSystemMapper, + ActionsLogService actionLogger, + MCPSystemFilterSpecificationBuilder mcpSystemFilterSpecificationBuilder, + ChainRepository chainRepository + ) { + this.mcpSystemRepository = mcpSystemRepository; + this.mcpSystemMapper = mcpSystemMapper; + this.actionLogger = actionLogger; + this.mcpSystemFilterSpecificationBuilder = mcpSystemFilterSpecificationBuilder; + this.chainRepository = chainRepository; + } + + public List findAll(boolean withChains) { + List systems = mcpSystemRepository.findAll(); + return withChains + ? enrichWithChains(systems) + : systems; + } + + public List findAll() { + return findAll(false); + } + + public List findAllById(List ids) { + return mcpSystemRepository.findAllById(ids); + } + + public Optional findById(String id) { + return mcpSystemRepository.findById(id); + } + + public MCPSystem create(MCPSystemCreateRequestDTO request) { + MCPSystem mcpSystem = new MCPSystem(); + mcpSystem.setName(request.getName()); + mcpSystem.setDescription(request.getDescription()); + mcpSystem.setIdentifier(request.getIdentifier()); + mcpSystem.setInstructions(request.getInstructions()); + return mcpSystemRepository.save(mcpSystem); + } + + public MCPSystem create(MCPSystem system, boolean isImport) { + system = mcpSystemRepository.save(system); + logAction(system, isImport ? LogOperation.IMPORT : LogOperation.CREATE); + return system; + } + + public MCPSystem update(String id, MCPSystemUpdateRequestDTO request) { + Optional mcpSystem = mcpSystemRepository.findById(id); + if (mcpSystem.isEmpty()) { + throw new EntityNotFoundException(String.format("MCP system with id %s not found", id)); + } + MCPSystem system = mcpSystem.get(); + system = mcpSystemMapper.update(system, request); + return update(system); + } + + public MCPSystem update(MCPSystem system) { + system = mcpSystemRepository.save(system); + logAction(system, LogOperation.UPDATE); + return system; + } + + public void deleteById(String id) { + if (isUsedByChain(id)) { + throw new SystemDeleteException("Service used by one or more chains"); + } + mcpSystemRepository.findById(id).ifPresent(system -> { + mcpSystemRepository.delete(system); + logAction(system, LogOperation.DELETE); + }); + } + + public List searchSystems(SystemSearchRequestDTO request) { + return searchSystems(request.getSearchCondition()); + } + + public List searchSystems(String searchCondition) { + Specification specification = mcpSystemFilterSpecificationBuilder.buildSearch(searchCondition); + return enrichWithChains(mcpSystemRepository.findAll(specification)); + } + + public List filter(List filters) { + Specification specification = mcpSystemFilterSpecificationBuilder.buildFilters(filters); + return enrichWithChains(mcpSystemRepository.findAll(specification)); + } + + private void logAction(MCPSystem system, LogOperation operation) { + actionLogger.logAction(ActionLog.builder() + .entityType(EntityType.MCP_SYSTEM) + .entityId(system.getId()) + .entityName(system.getName()) + .parentId(null) + .operation(operation) + .build()); + } + + public boolean isUsedByChain(String id) { + return !findChainsByMcpSystemId(id).isEmpty(); + } + + private void enrichWithChains(MCPSystem system) { + system.setChains(findChainsByMcpSystemId(system.getId())); + } + + private List enrichWithChains(List systems) { + systems.forEach(this::enrichWithChains); + return systems; + } + + private List findChainsByMcpSystemId(String mcpSystemId) { + return chainRepository.findChainsWithElementPropertyValue(CamelNames.MCP_SERVICE_ID, mcpSystemId); + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java index 2644580c..1e1c669a 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java @@ -23,6 +23,7 @@ public boolean applicableTo(ChainElement element) { @Override public Map build(ChainElement element) { return Stream.of( + "mcpServiceId", "name", "title", "description", diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/ContextExportImportService.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/ContextExportImportService.java index 3e494d0d..42b7ca38 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/ContextExportImportService.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/ContextExportImportService.java @@ -44,6 +44,7 @@ import org.qubership.integration.platform.runtime.catalog.service.ContextBaseService; import org.qubership.integration.platform.runtime.catalog.service.exportimport.deserializer.ContextServiceDeserializer; import org.qubership.integration.platform.runtime.catalog.service.exportimport.instructions.ImportInstructionsService; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer.ArchiveWriter; import org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer.ContextServiceSerializer; import org.qubership.integration.platform.runtime.catalog.util.ExportImportUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -83,6 +84,7 @@ public class ContextExportImportService { protected final ActionsLogService actionLogger; private final ContextServiceSerializer contextServiceSerializer; private final ContextServiceDeserializer contextServiceDeserializer; + private final ArchiveWriter archiveWriter; private final ImportSessionService importProgressService; private final ImportInstructionsService importInstructionsService; @@ -95,15 +97,19 @@ public ContextExportImportService( YAMLMapper yamlExportImportMapper, ActionsLogService actionLogger, ContextServiceSerializer contextServiceSerializer, - ContextServiceDeserializer contextServiceDeserializer, ImportSessionService importProgressService, + ContextServiceDeserializer contextServiceDeserializer, + ArchiveWriter archiveWriter, + ImportSessionService importProgressService, ImportInstructionsService importInstructionsService, - @Value("${qip.json.schemas.context-service:http://qubership.org/schemas/product/qip/context-service}") URI contextServiceSchemaUri) { + @Value("${qip.json.schemas.context-service:http://qubership.org/schemas/product/qip/context-service}") URI contextServiceSchemaUri + ) { this.transactionTemplate = transactionTemplate; this.contextBaseService = contextBaseService; this.yamlMapper = yamlExportImportMapper; this.actionLogger = actionLogger; this.contextServiceSerializer = contextServiceSerializer; this.contextServiceDeserializer = contextServiceDeserializer; + this.archiveWriter = archiveWriter; this.importProgressService = importProgressService; this.importInstructionsService = importInstructionsService; this.contextServiceSchemaUri = contextServiceSchemaUri; @@ -145,7 +151,7 @@ public byte[] exportSystemsRequest(List systemIds) { } List exportedSystems = exportSystems(systems); - byte[] archive = contextServiceSerializer.writeSerializedArchive(exportedSystems); + byte[] archive = archiveWriter.writeArchive(exportedSystems); for (ContextSystem system : systems) { logSystemExportImport(system, null, LogOperation.EXPORT); } diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/ExportImportConstants.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/ExportImportConstants.java index 03919d29..40436b52 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/ExportImportConstants.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/ExportImportConstants.java @@ -38,6 +38,9 @@ public class ExportImportConstants { @Deprecated public static final String CONTEXT_SERVICE_YAML_NAME_PREFIX = "context-service-"; public static final String CONTEXT_SERVICE_YAML_NAME_POSTFIX = ".context-service."; + @Deprecated + public static final String MCP_SERVICE_YAML_NAME_PREFIX = "mcp-service-"; + public static final String MCP_SERVICE_YAML_NAME_POSTFIX = ".mcp-service."; public static final String SOURCE_YAML_NAME_PREFIX = "source-"; public static final String EXPORT_FILE_NAME_PREFIX = "export-"; @Deprecated diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/GeneralImportService.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/GeneralImportService.java index 9373f4a2..80ee344a 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/GeneralImportService.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/GeneralImportService.java @@ -58,6 +58,7 @@ public class GeneralImportService { private final CommonVariablesService commonVariablesService; private final SystemExportImportService systemExportImportService; private final ContextExportImportService contextExportImportService; + private final MCPSystemImportExportService mcpSystemImportExportService; private final ChainImportService chainImportService; private final ImportSessionService importSessionService; private final ActionsLogService actionsLogService; @@ -68,7 +69,9 @@ public class GeneralImportService { public GeneralImportService( CommonVariablesService commonVariablesService, SystemExportImportService systemExportImportService, - ContextExportImportService contextExportImportService, ChainImportService chainImportService, + ContextExportImportService contextExportImportService, + MCPSystemImportExportService mcpSystemImportExportService, + ChainImportService chainImportService, ImportSessionService importSessionService, ActionsLogService actionsLogService, ImportInstructionsService importInstructionsService, @@ -77,6 +80,7 @@ public GeneralImportService( this.commonVariablesService = commonVariablesService; this.systemExportImportService = systemExportImportService; this.contextExportImportService = contextExportImportService; + this.mcpSystemImportExportService = mcpSystemImportExportService; this.chainImportService = chainImportService; this.importSessionService = importSessionService; this.actionsLogService = actionsLogService; @@ -113,6 +117,7 @@ public ImportPreviewResponse getImportPreview(MultipartFile file) { .chains(chainImportService.getChainsImportPreview(unpackedDirectory, instructionsConfig.getChains())) .systems(systemExportImportService.getSystemsImportPreview(unpackedDirectory, instructionsConfig.getServices())) .contextService(contextExportImportService.getContextServiceImportPreview(unpackedDirectory, instructionsConfig.getContextServices())) + .mcpService(mcpSystemImportExportService.getImportPreview(unpackedDirectory, instructionsConfig.getMcpServices())) .instructions(generalInstructionsMapper.asDTO(importInstructions)) .build(); } finally { @@ -162,6 +167,7 @@ public String importFileAsync(MultipartFile file, ImportRequest importRequest, S ImportSystemsAndInstructionsResult importSystemsAndInstructionsResult = systemExportImportService .importSystems(unpackedDirectory, importRequest.getSystemsCommitRequest(), importId, technicalLabels); ImportContextServiceAndInstructionsResult importChainsAndContextInstructionsResult = contextExportImportService.importContextService(unpackedDirectory, importRequest.getSystemsCommitRequest(), importId); + ImportSystemsAndInstructionsResult importMcpSystemsAndInstructionsResult = mcpSystemImportExportService.importSystems(unpackedDirectory, importRequest.getSystemsCommitRequest(), importId); ImportChainsAndInstructionsResult importChainsAndInstructionsResult = chainImportService .importChains(unpackedDirectory, importRequest.getChainCommitRequests(), importId, technicalLabels, validateByHash); diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportService.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportService.java new file mode 100644 index 00000000..aafd9bb9 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportService.java @@ -0,0 +1,452 @@ +package org.qubership.integration.platform.runtime.catalog.service.exportimport; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.qubership.integration.platform.runtime.catalog.exception.exceptions.ServicesNotFoundException; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.chain.ImportSystemsAndInstructionsResult; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.instructions.IgnoreResult; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.instructions.ImportInstructionAction; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.instructions.ImportInstructionsConfig; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.system.ImportSystemResult; +import org.qubership.integration.platform.runtime.catalog.model.system.exportimport.ExportedSystemObject; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.actionlog.ActionLog; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.actionlog.EntityType; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.actionlog.LogOperation; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.system.AbstractSystemEntity; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.imports.ImportSystemStatus; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.imports.remote.SystemCompareAction; +import org.qubership.integration.platform.runtime.catalog.rest.v3.dto.exportimport.ImportMode; +import org.qubership.integration.platform.runtime.catalog.rest.v3.dto.exportimport.system.SystemsCommitRequest; +import org.qubership.integration.platform.runtime.catalog.service.ActionsLogService; +import org.qubership.integration.platform.runtime.catalog.service.MCPSystemService; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.deserializer.MCPSystemDeserializer; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.instructions.ImportInstructionsService; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer.ArchiveWriter; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer.MCPSystemSerializer; +import org.qubership.integration.platform.runtime.catalog.util.ExportImportUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.CollectionUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Paths; +import java.sql.Timestamp; +import java.util.*; +import java.util.stream.Collectors; + +import static java.util.Objects.isNull; +import static org.qubership.integration.platform.runtime.catalog.service.exportimport.ExportImportConstants.MCP_SERVICE_YAML_NAME_POSTFIX; +import static org.qubership.integration.platform.runtime.catalog.service.exportimport.ExportImportConstants.ZIP_EXTENSION; +import static org.qubership.integration.platform.runtime.catalog.util.ExportImportUtils.*; +import static org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED; + +@Slf4j +@Service +@Transactional +public class MCPSystemImportExportService { + private final TransactionTemplate transactionTemplate; + private final YAMLMapper yamlMapper; + private final MCPSystemService mcpSystemService; + private final ActionsLogService actionLogger; + private final MCPSystemSerializer mcpSystemSerializer; + private final MCPSystemDeserializer mcpSystemDeserializer; + private final ArchiveWriter archiveWriter; + private final ImportInstructionsService importInstructionsService; + private final ImportSessionService importProgressService; + private final URI mcpServiceSchemaUri; + + @Autowired + public MCPSystemImportExportService( + TransactionTemplate transactionTemplate, + @Qualifier("yamlExportImportMapper") YAMLMapper yamlMapper, + MCPSystemService mcpSystemService, + ActionsLogService actionLogger, + MCPSystemSerializer mcpSystemSerializer, + MCPSystemDeserializer mcpSystemDeserializer, + ArchiveWriter archiveWriter, + ImportInstructionsService importInstructionsService, + ImportSessionService importProgressService, + @Value("${qip.json.schemas.mcp-service:http://qubership.org/schemas/product/qip/mcp-service}") URI mcpServiceSchemaUri + ) { + this.transactionTemplate = transactionTemplate; + this.yamlMapper = yamlMapper; + this.mcpSystemService = mcpSystemService; + this.actionLogger = actionLogger; + this.mcpSystemSerializer = mcpSystemSerializer; + this.mcpSystemDeserializer = mcpSystemDeserializer; + this.archiveWriter = archiveWriter; + this.importInstructionsService = importInstructionsService; + this.importProgressService = importProgressService; + this.mcpServiceSchemaUri = mcpServiceSchemaUri; + } + + public byte[] export(List ids) { + List systems = isNull(ids) + ? mcpSystemService.findAll() + : mcpSystemService.findAllById(ids); + if (systems.isEmpty()) { + return null; + } + + List exportedSystems = exportSystems(systems); + byte[] data = archiveWriter.writeArchive(exportedSystems); + systems.forEach(this::logExport); + return data; + } + + private List exportSystems(List systems) { + return systems.stream().map(this::exportSystem).toList(); + } + + private ExportedSystemObject exportSystem(MCPSystem system) { + try { + return mcpSystemSerializer.serialize(system); + } catch (JsonProcessingException e) { + String message = String.format("Failed to export system: %s (%s)", + system.getName(), system.getId()); + throw new RuntimeException(message, e); + } + } + + @Transactional(propagation = NOT_SUPPORTED) + public List importSystems( + MultipartFile file, + List ids + ) { + List response = new ArrayList<>(); + String fileExtension = FilenameUtils.getExtension(file.getOriginalFilename()); + logArchiveImport(file.getOriginalFilename()); + if (ZIP_EXTENSION.equalsIgnoreCase(fileExtension)) { + String exportDirectory = Paths.get(FileUtils.getTempDirectory().getAbsolutePath(), + UUID.randomUUID().toString()).toString(); + List extractedSystemFiles; + + try (InputStream fs = file.getInputStream()) { + extractedSystemFiles = extractSystemsFromZip(fs, exportDirectory, MCP_SERVICE_YAML_NAME_POSTFIX); + } catch (IOException e) { + deleteFile(exportDirectory); + throw new RuntimeException("Unexpected error while archive unpacking: " + e.getMessage(), e); + } catch (RuntimeException e) { + deleteFile(exportDirectory); + throw e; + } + + Set servicesToImport = importInstructionsService.performServiceIgnoreInstructions( + extractedSystemFiles.stream() + .map(ExportImportUtils::extractSystemIdFromFileName) + .collect(Collectors.toSet()), + false) + .idsToImport(); + for (File singleSystemFile : extractedSystemFiles) { + String serviceId = extractSystemIdFromFileName(singleSystemFile); + if (!servicesToImport.contains(serviceId)) { + addIgnoredServiceResult(response, serviceId); + continue; + } + + ImportSystemResult result = importOneSystemInTransaction(singleSystemFile, ids); + if (result != null) { + response.add(result); + } + } + + deleteFile(exportDirectory); + } else { + throw new RuntimeException("Unsupported file extension: " + fileExtension); + } + + return response; + } + + @Transactional(propagation = NOT_SUPPORTED) + public ImportSystemsAndInstructionsResult importSystems( + File importDirectory, + SystemsCommitRequest systemCommitRequest, + String importId + ) { + if (systemCommitRequest.getImportMode() == ImportMode.NONE) { + return new ImportSystemsAndInstructionsResult(); + } + + List systemsFiles = extractMcpServiceFilesFromDirectory(importDirectory); + + List systemIds = systemCommitRequest.getImportMode() == ImportMode.FULL + ? Collections.emptyList() + : systemCommitRequest.getSystemIds(); + + IgnoreResult ignoreResult = importInstructionsService.performServiceIgnoreInstructions( + systemsFiles.stream() + .map(ExportImportUtils::extractSystemIdFromFileName) + .collect(Collectors.toSet()), + true); + int total = systemsFiles.size(); + int counter = 0; + List response = new ArrayList<>(); + for (File systemFile : systemsFiles) { + String serviceId = extractSystemIdFromFileName(systemFile); + if (!ignoreResult.idsToImport().contains(serviceId)) { + addIgnoredServiceResult(response, serviceId); + continue; + } + + importProgressService.calculateImportStatus( + importId, total, counter, ImportSessionService.COMMON_VARIABLES_IMPORT_PERCENTAGE_THRESHOLD, + ImportSessionService.SERVICE_IMPORT_PERCENTAGE_THRESHOLD); + counter++; + + ImportSystemResult result = importOneSystemInTransaction(systemFile, systemIds); + + if (result != null) { + response.add(result); + } + } + + return new ImportSystemsAndInstructionsResult(response, ignoreResult.importInstructionResults()); + } + + public List getImportPreview(MultipartFile file) { + List result = new ArrayList<>(); + String fileExtension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (ZIP_EXTENSION.equalsIgnoreCase(fileExtension)) { + String dataDirectory = Paths.get(FileUtils.getTempDirectory().getAbsolutePath(), + UUID.randomUUID().toString()).toString(); + List extractedSystemFiles = new ArrayList<>(); + + try (InputStream fs = file.getInputStream()) { + extractedSystemFiles = extractSystemsFromZip(fs, dataDirectory, MCP_SERVICE_YAML_NAME_POSTFIX); + } catch (ServicesNotFoundException e) { + deleteFile(dataDirectory); + } catch (IOException e) { + deleteFile(dataDirectory); + throw new RuntimeException("Unexpected error while archive unpacking: " + e.getMessage(), e); + } catch (RuntimeException e) { + deleteFile(dataDirectory); + throw e; + } + + ImportInstructionsConfig instructionsConfig = importInstructionsService + .getServiceImportInstructionsConfig(Set.of(ImportInstructionAction.IGNORE)); + for (File singleSystemFile : extractedSystemFiles) { + result.add(getSystemChanges(singleSystemFile, instructionsConfig)); + } + deleteFile(dataDirectory); + } else { + throw new RuntimeException("Unsupported file extension: " + fileExtension); + } + + return result; + } + + public List getImportPreview( + File importDirectory, + ImportInstructionsConfig instructionsConfig + ) { + List contextServiceFiles = extractMcpServiceFilesFromDirectory(importDirectory); + + List importSystemResults = new ArrayList<>(); + for (File systemFile : contextServiceFiles) { + importSystemResults.add(getSystemChanges(systemFile, instructionsConfig)); + } + + return importSystemResults; + } + + private void addIgnoredServiceResult(List response, String serviceId) { + response.add(ImportSystemResult.builder() + .id(serviceId) + .name(serviceId) + .status(ImportSystemStatus.IGNORED) + .build()); + log.info("Service {} ignored as a part of import exclusion list", serviceId); + } + + private List extractMcpServiceFilesFromDirectory(File importDirectory) { + List systemsFiles; + try { + systemsFiles = extractSystemsFromImportDirectory(importDirectory.getAbsolutePath(), + MCP_SERVICE_YAML_NAME_POSTFIX); + } catch (IOException e) { + throw new RuntimeException("Error while extracting MCP service files", e); + } + return systemsFiles.stream() + .filter(this::isMCPServiceFile) + .collect(Collectors.toList()); + } + + private ObjectNode getFileNode(File file) throws IOException { + return (ObjectNode) yamlMapper.readTree(file); + } + + private boolean isMCPServiceFile(File file) { + try { + ObjectNode node = getFileNode(file); + JsonNode schemaNode = node.get("$schema"); + if (schemaNode != null && schemaNode.isTextual()) { + String fileSchema = schemaNode.asText(); + return mcpServiceSchemaUri.toString().equals(fileSchema); + } + return false; + } catch (Exception e) { + log.warn("Failed to check schema for file {}: {}", file.getName(), e.getMessage()); + return false; + } + } + + private ImportSystemResult getSystemChanges(File mainSystemFile, ImportInstructionsConfig instructionsConfig) { + ImportSystemResult resultSystemCompareDTO; + + String systemId = null; + String systemName = null; + + try { + ObjectNode serviceNode = getFileNode(mainSystemFile); + MCPSystem baseSystem = getBaseSystemDeserializationResult(serviceNode); + systemId = baseSystem.getId(); + systemName = baseSystem.getName(); + Long systemModifiedWhen = baseSystem.getModifiedWhen() != null ? baseSystem.getModifiedWhen().getTime() : 0; + ImportInstructionAction instructionAction = instructionsConfig.getIgnore().contains(systemId) + ? ImportInstructionAction.IGNORE + : null; + + resultSystemCompareDTO = ImportSystemResult.builder() + .id(systemId) + .modified(systemModifiedWhen) + .instructionAction(instructionAction) + .build(); + setCompareSystemResult(baseSystem, resultSystemCompareDTO); + } catch (RuntimeException | IOException e) { + log.error("Exception while system compare: ", e); + resultSystemCompareDTO = ImportSystemResult.builder() + .id(systemId) + .name(systemName) + .requiredAction(SystemCompareAction.ERROR) + .message("Exception while system compare: " + e.getMessage()) + .build(); + } + return resultSystemCompareDTO; + } + + private MCPSystem getBaseSystemDeserializationResult(JsonNode serviceNode) + throws JsonProcessingException { + MCPSystem result = new MCPSystem(); + + JsonNode idNode = serviceNode.get(AbstractSystemEntity.Fields.id); + String systemId = (idNode == null || idNode.isNull()) ? null : idNode.asText(); + if (systemId == null) { + throw new RuntimeException("Missing id field in system file"); + } + + JsonNode nameNode = serviceNode.get(AbstractSystemEntity.Fields.name); + String systemName = (nameNode == null || nameNode.isNull()) ? "" : nameNode.asText(); + + Timestamp modifiedWhen = serviceNode.get(AbstractSystemEntity.Fields.modifiedWhen) != null + ? new Timestamp(serviceNode.get(AbstractSystemEntity.Fields.modifiedWhen).asLong()) + : null; + + result.setId(systemId); + result.setName(systemName); + result.setModifiedWhen(modifiedWhen); + + return result; + } + + private void setCompareSystemResult(MCPSystem system, ImportSystemResult result) { + mcpSystemService.findById(system.getId()).ifPresentOrElse( + oldSystem -> { + result.setName(oldSystem.getName()); + result.setRequiredAction(SystemCompareAction.UPDATE); + }, + () -> { + result.setName(system.getName()); + result.setRequiredAction(SystemCompareAction.CREATE); + } + ); + } + + private synchronized ImportSystemResult importOneSystemInTransaction( + File mainServiceFile, + List systemIds + ) { + ImportSystemResult result; + Optional baseSystemOptional = Optional.empty(); + + try { + ObjectNode serviceNode = getFileNode(mainServiceFile); + MCPSystem system = getBaseSystemDeserializationResult(serviceNode); + baseSystemOptional = Optional.of(system); + if (!CollectionUtils.isEmpty(systemIds) && !systemIds.contains(system.getId())) { + return null; + } + result = transactionTemplate.execute((status) -> { + MCPSystem mcpSystem = system; + try { + mcpSystem = mcpSystemDeserializer.deserialize(mainServiceFile); + } catch (Exception e) { + throw new RuntimeException(e); + } + + ImportSystemStatus importStatus = enrichAndSaveSystem(mcpSystem); + return ImportSystemResult.builder() + .id(mcpSystem.getId()) + .name(mcpSystem.getName()) + .status(importStatus) + .build(); + }); + } catch (Exception e) { + result = ImportSystemResult.builder() + .id(baseSystemOptional.map(MCPSystem::getId).orElse(null)) + .name(baseSystemOptional.map(MCPSystem::getName).orElse("")) + .status(ImportSystemStatus.ERROR) + .message(e.getMessage()) + .build(); + log.warn("Exception when importing MCP system {} ({})", result.getName(), result.getId(), e); + } + return result; + } + + private ImportSystemStatus enrichAndSaveSystem(MCPSystem system) { + ImportSystemStatus status; + Optional oldSystem = mcpSystemService.findById(system.getId()); + + if (oldSystem.isPresent()) { + mcpSystemService.update(system); + return ImportSystemStatus.UPDATED; + } else { + mcpSystemService.create(system, true); + return ImportSystemStatus.CREATED; + } + } + + private void logExport(MCPSystem system) { + actionLogger.logAction(ActionLog.builder() + .entityType(EntityType.MCP_SYSTEM) + .entityId(system.getId()) + .entityName(system.getName()) + .operation(LogOperation.EXPORT) + .build() + ); + } + + private void logArchiveImport(String archiveName) { + actionLogger.logAction(ActionLog.builder() + .entityType(EntityType.SERVICES) + .entityId(null) + .entityName(archiveName) + .operation(LogOperation.IMPORT) + .build()); + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/SystemExportImportService.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/SystemExportImportService.java index c485a51c..57699e2e 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/SystemExportImportService.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/SystemExportImportService.java @@ -47,6 +47,7 @@ import org.qubership.integration.platform.runtime.catalog.service.*; import org.qubership.integration.platform.runtime.catalog.service.exportimport.deserializer.ServiceDeserializer; import org.qubership.integration.platform.runtime.catalog.service.exportimport.instructions.ImportInstructionsService; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer.ArchiveWriter; import org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer.ServiceSerializer; import org.qubership.integration.platform.runtime.catalog.service.helpers.ElementHelperService; import org.qubership.integration.platform.runtime.catalog.util.ExportImportUtils; @@ -98,6 +99,7 @@ public class SystemExportImportService { private final AuditingHandler auditingHandler; private final ServiceSerializer serviceSerializer; private final ServiceDeserializer serviceDeserializer; + private final ArchiveWriter archiveWriter; private final ImportSessionService importProgressService; private final ImportInstructionsService importInstructionsService; private final ElementHelperService elementHelperService; @@ -118,6 +120,7 @@ public SystemExportImportService( AuditingHandler jpaAuditingHandler, ServiceSerializer serviceSerializer, ServiceDeserializer serviceDeserializer, + ArchiveWriter archiveWriter, ImportSessionService importProgressService, ImportInstructionsService importInstructionsService, ElementHelperService elementHelperService, @@ -133,6 +136,7 @@ public SystemExportImportService( this.auditingHandler = jpaAuditingHandler; this.serviceSerializer = serviceSerializer; this.serviceDeserializer = serviceDeserializer; + this.archiveWriter = archiveWriter; this.importProgressService = importProgressService; this.importInstructionsService = importInstructionsService; this.elementHelperService = elementHelperService; @@ -200,7 +204,7 @@ public byte[] exportSystemsRequest(List systemIds, List usedSyst } List exportedSystems = exportSystems(systems, usedSystemModelIds); - byte[] archive = serviceSerializer.writeSerializedArchive(exportedSystems); + byte[] archive = archiveWriter.writeArchive(exportedSystems); for (IntegrationSystem system : systems) { logSystemExportImport(system, null, LogOperation.EXPORT); } diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/deserializer/MCPSystemDeserializer.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/deserializer/MCPSystemDeserializer.java new file mode 100644 index 00000000..fe16f1af --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/deserializer/MCPSystemDeserializer.java @@ -0,0 +1,53 @@ +package org.qubership.integration.platform.runtime.catalog.service.exportimport.deserializer; + +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import org.qubership.integration.platform.runtime.catalog.exception.exceptions.ServiceImportException; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.system.MCPServiceDto; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.mapper.services.MCPServiceDtoMapper; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.migrations.FileMigrationService; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.migrations.ImportFileMigration; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.migrations.mcp.MCPServiceImportFileMigration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.nio.file.Files; +import java.util.Collection; + +@Component +public class MCPSystemDeserializer { + private final YAMLMapper yamlMapper; + private final FileMigrationService fileMigrationService; + private final Collection importFileMigrations; + private final MCPServiceDtoMapper mcpServiceDtoMapper; + + @Autowired + public MCPSystemDeserializer( + @Qualifier("yamlExportImportMapper") YAMLMapper yamlMapper, + FileMigrationService fileMigrationService, + Collection importFileMigrations, + MCPServiceDtoMapper mcpServiceDtoMapper + ) { + this.yamlMapper = yamlMapper; + this.fileMigrationService = fileMigrationService; + this.importFileMigrations = importFileMigrations; + this.mcpServiceDtoMapper = mcpServiceDtoMapper; + } + + public MCPSystem deserialize(File serviceFile) { + try { + String serviceData = fileMigrationService.migrate( + Files.readString(serviceFile.toPath()), + importFileMigrations.stream().map(ImportFileMigration.class::cast).toList() + ); + MCPServiceDto mcpServiceDto = yamlMapper.readValue(serviceData, MCPServiceDto.class); + return mcpServiceDtoMapper.toInternalEntity(mcpServiceDto); + } catch (ServiceImportException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/mapper/services/MCPServiceDtoMapper.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/mapper/services/MCPServiceDtoMapper.java new file mode 100644 index 00000000..163126a9 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/mapper/services/MCPServiceDtoMapper.java @@ -0,0 +1,74 @@ +package org.qubership.integration.platform.runtime.catalog.service.exportimport.mapper.services; + +import org.qubership.integration.platform.runtime.catalog.model.exportimport.system.MCPServiceContentDto; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.system.MCPServiceDto; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystemLabel; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.mapper.ExternalEntityMapper; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.migrations.ImportFileMigration; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.migrations.mcp.MCPServiceImportFileMigration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class MCPServiceDtoMapper implements ExternalEntityMapper { + private final URI schemaUri; + private final List migrations; + + @Autowired + public MCPServiceDtoMapper( + @Value("${qip.json.schemas.mcp-service:http://qubership.org/schemas/product/qip/mcp-service}") URI schemaUri, + List migrations + ) { + this.schemaUri = schemaUri; + this.migrations = migrations; + } + + @Override + public MCPSystem toInternalEntity(MCPServiceDto mcpServiceDto) { + MCPSystem system = MCPSystem.builder() + .id(mcpServiceDto.getId()) + .name(mcpServiceDto.getName()) + .description(mcpServiceDto.getContent().getDescription()) + .identifier(mcpServiceDto.getContent().getIdentifier()) + .instructions(mcpServiceDto.getContent().getInstructions()) + .createdBy(mcpServiceDto.getContent().getCreatedBy()) + .createdWhen(mcpServiceDto.getContent().getCreatedWhen()) + .modifiedBy(mcpServiceDto.getContent().getModifiedBy()) + .modifiedWhen(mcpServiceDto.getContent().getModifiedWhen()) + .build(); + system.setLabels(mcpServiceDto + .getContent() + .getLabels() + .stream() + .map(name -> new MCPSystemLabel(name, system)) + .collect(Collectors.toSet())); + return system; + } + + @Override + public MCPServiceDto toExternalEntity(MCPSystem system) { + return MCPServiceDto.builder() + .id(system.getId()) + .name(system.getName()) + .schema(schemaUri) + .content(MCPServiceContentDto.builder() + .description(system.getDescription()) + .identifier(system.getIdentifier()) + .instructions(system.getInstructions()) + .labels(system.getLabels().stream().map(MCPSystemLabel::getName).toList()) + .migrations(migrations + .stream() + .map(ImportFileMigration::getVersion) + .sorted() + .toList() + .toString()) + .build()) + .build(); + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/migrations/mcp/MCPServiceImportFileMigration.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/migrations/mcp/MCPServiceImportFileMigration.java new file mode 100644 index 00000000..5aa9a135 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/migrations/mcp/MCPServiceImportFileMigration.java @@ -0,0 +1,6 @@ +package org.qubership.integration.platform.runtime.catalog.service.exportimport.migrations.mcp; + +import org.qubership.integration.platform.runtime.catalog.service.exportimport.migrations.ImportFileMigration; + +public interface MCPServiceImportFileMigration extends ImportFileMigration { +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/migrations/mcp/V100MCPServiceImportFileMigration.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/migrations/mcp/V100MCPServiceImportFileMigration.java new file mode 100644 index 00000000..a95cb57f --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/migrations/mcp/V100MCPServiceImportFileMigration.java @@ -0,0 +1,21 @@ +package org.qubership.integration.platform.runtime.catalog.service.exportimport.migrations.mcp; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class V100MCPServiceImportFileMigration implements MCPServiceImportFileMigration { + @Override + public int getVersion() { + return 100; + } + + @Override + public ObjectNode makeMigration(ObjectNode fileNode) throws JsonProcessingException { + log.debug("Initial MCP service migration V100"); + return fileNode; + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ArchiveWriter.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ArchiveWriter.java new file mode 100644 index 00000000..681043c2 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ArchiveWriter.java @@ -0,0 +1,37 @@ +package org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer; + +import org.qubership.integration.platform.runtime.catalog.model.system.exportimport.ExportableObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.zip.ZipOutputStream; + +import static org.qubership.integration.platform.runtime.catalog.service.exportimport.ExportImportConstants.ARCH_PARENT_DIR; + +@Component +public class ArchiveWriter { + private final ExportableObjectWriterVisitor exportableObjectWriterVisitor; + + @Autowired + public ArchiveWriter(ExportableObjectWriterVisitor exportableObjectWriterVisitor) { + this.exportableObjectWriterVisitor = exportableObjectWriterVisitor; + } + + public byte[] writeArchive(List exportedSystems) { + try (ByteArrayOutputStream fos = new ByteArrayOutputStream()) { + try (ZipOutputStream zipOut = new ZipOutputStream(fos)) { + for (ExportableObject exportedSystem : exportedSystems) { + String entryPath = ARCH_PARENT_DIR + File.separator + exportedSystem.getId() + File.separator; + exportedSystem.accept(exportableObjectWriterVisitor, zipOut, entryPath); + } + } + return fos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Failed to create archive: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ContextServiceSerializer.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ContextServiceSerializer.java index 45492f8c..7e7b0590 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ContextServiceSerializer.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ContextServiceSerializer.java @@ -27,29 +27,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.zip.ZipOutputStream; - -import static org.qubership.integration.platform.runtime.catalog.service.exportimport.ExportImportConstants.ARCH_PARENT_DIR; - @Component public class ContextServiceSerializer { private final YAMLMapper yamlMapper; - private final ExportableObjectWriterVisitor exportableObjectWriterVisitor; private final ContextServiceDtoMapper contextServiceDtoMapper; private final FileMigrationService fileMigrationService; @Autowired - public ContextServiceSerializer(YAMLMapper yamlExportImportMapper, - ExportableObjectWriterVisitor exportableObjectWriterVisitor, - ContextServiceDtoMapper contextServiceDtoMapper, - FileMigrationService fileMigrationService) { + public ContextServiceSerializer( + YAMLMapper yamlExportImportMapper, + ContextServiceDtoMapper contextServiceDtoMapper, + FileMigrationService fileMigrationService + ) { this.yamlMapper = yamlExportImportMapper; - this.exportableObjectWriterVisitor = exportableObjectWriterVisitor; this.contextServiceDtoMapper = contextServiceDtoMapper; this.fileMigrationService = fileMigrationService; } @@ -60,20 +51,4 @@ public ExportedSystemObject serialize(ContextSystem system) throws JsonProcessin return new ExportedContextService(system.getId(), systemNode); } - - - public byte[] writeSerializedArchive(List exportedSystems) { - try (ByteArrayOutputStream fos = new ByteArrayOutputStream()) { - try (ZipOutputStream zipOut = new ZipOutputStream(fos)) { - for (ExportedSystemObject exportedSystem : exportedSystems) { - String entryPath = ARCH_PARENT_DIR + File.separator + exportedSystem.getId() + File.separator; - exportedSystem.accept(exportableObjectWriterVisitor, zipOut, entryPath); - } - } - return fos.toByteArray(); - } catch (IOException e) { - throw new RuntimeException("Unknown exception while archive creation: " + e.getMessage(), e); - } - } - } diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ExportableObjectWriterVisitor.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ExportableObjectWriterVisitor.java index 77295643..f16652d1 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ExportableObjectWriterVisitor.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ExportableObjectWriterVisitor.java @@ -84,4 +84,10 @@ public void visit(ExportedContextService exportedContextService, ZipOutputStream entryPath + ExportImportUtils.generateMainContextServiceFileExportName(exportedContextService.getId(), appName, isLegacyExport), yamlMapper.writeValueAsString(exportedContextService.getObjectNode())); } + + public void visit(ExportedMCPSystemObject exportedMCPSystemObject, ZipOutputStream zipOut, String entryPath) throws IOException { + ExportImportUtils.writeSystemObject(zipOut, + entryPath + ExportImportUtils.generateMCPServiceFileExportName(exportedMCPSystemObject.getId(), appName, isLegacyExport), + yamlMapper.writeValueAsString(exportedMCPSystemObject.getObjectNode())); + } } diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/MCPSystemSerializer.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/MCPSystemSerializer.java new file mode 100644 index 00000000..3c9d6f00 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/MCPSystemSerializer.java @@ -0,0 +1,38 @@ +package org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.system.MCPServiceDto; +import org.qubership.integration.platform.runtime.catalog.model.system.exportimport.ExportedMCPSystemObject; +import org.qubership.integration.platform.runtime.catalog.model.system.exportimport.ExportedSystemObject; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.mapper.services.MCPServiceDtoMapper; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.migrations.FileMigrationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +@Component +public class MCPSystemSerializer { + private final YAMLMapper yamlMapper; + private final MCPServiceDtoMapper mcpServiceDtoMapper; + private final FileMigrationService fileMigrationService; + + @Autowired + public MCPSystemSerializer( + @Qualifier("yamlExportImportMapper") YAMLMapper yamlExportImportMapper, + MCPServiceDtoMapper mcpServiceDtoMapper, + FileMigrationService fileMigrationService + ) { + this.yamlMapper = yamlExportImportMapper; + this.mcpServiceDtoMapper = mcpServiceDtoMapper; + this.fileMigrationService = fileMigrationService; + } + + public ExportedSystemObject serialize(MCPSystem system) throws JsonProcessingException { + MCPServiceDto mcpServiceDto = mcpServiceDtoMapper.toExternalEntity(system); + ObjectNode systemNode = fileMigrationService.revertMigrationIfNeeded(yamlMapper.valueToTree(mcpServiceDto)); + return new ExportedMCPSystemObject(system.getId(), systemNode); + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ServiceSerializer.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ServiceSerializer.java index ef7a6527..88892095 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ServiceSerializer.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ServiceSerializer.java @@ -31,19 +31,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; import java.util.List; -import java.util.zip.ZipOutputStream; - -import static org.qubership.integration.platform.runtime.catalog.service.exportimport.ExportImportConstants.ARCH_PARENT_DIR; @Component public class ServiceSerializer { private final YAMLMapper yamlMapper; - private final ExportableObjectWriterVisitor exportableObjectWriterVisitor; private final IntegrationSystemDtoMapper integrationSystemDtoMapper; private final SpecificationGroupDtoMapper specificationGroupDtoMapper; private final SystemModelDtoMapper systemModelDtoMapper; @@ -52,14 +45,12 @@ public class ServiceSerializer { @Autowired public ServiceSerializer( YAMLMapper yamlExportImportMapper, - ExportableObjectWriterVisitor exportableObjectWriterVisitor, IntegrationSystemDtoMapper integrationSystemDtoMapper, SpecificationGroupDtoMapper specificationGroupDtoMapper, SystemModelDtoMapper systemModelDtoMapper, FileMigrationService fileMigrationService ) { this.yamlMapper = yamlExportImportMapper; - this.exportableObjectWriterVisitor = exportableObjectWriterVisitor; this.integrationSystemDtoMapper = integrationSystemDtoMapper; this.specificationGroupDtoMapper = specificationGroupDtoMapper; this.systemModelDtoMapper = systemModelDtoMapper; @@ -103,18 +94,4 @@ public ExportedSpecification serialize(SystemModel specification) { return new ExportedSpecification(specification.getId(), node, exportedSpecificationSources); } - - public byte[] writeSerializedArchive(List exportedSystems) { - try (ByteArrayOutputStream fos = new ByteArrayOutputStream()) { - try (ZipOutputStream zipOut = new ZipOutputStream(fos)) { - for (ExportedSystemObject exportedSystem : exportedSystems) { - String entryPath = ARCH_PARENT_DIR + File.separator + exportedSystem.getId() + File.separator; - exportedSystem.accept(exportableObjectWriterVisitor, zipOut, entryPath); - } - } - return fos.toByteArray(); - } catch (IOException e) { - throw new RuntimeException("Unknown exception while archive creation: " + e.getMessage(), e); - } - } } diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java new file mode 100644 index 00000000..54550d69 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java @@ -0,0 +1,87 @@ +package org.qubership.integration.platform.runtime.catalog.service.filter; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import org.qubership.integration.platform.runtime.catalog.model.filter.FilterCondition; +import org.qubership.integration.platform.runtime.catalog.model.filter.FilterFeature; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.FilterRequestDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +@Component +public class MCPSystemFilterSpecificationBuilder { + private final FilterConditionPredicateBuilderFactory filterConditionPredicateBuilderFactory; + + @Autowired + public MCPSystemFilterSpecificationBuilder( + FilterConditionPredicateBuilderFactory filterConditionPredicateBuilderFactory + ) { + this.filterConditionPredicateBuilderFactory = filterConditionPredicateBuilderFactory; + } + + public Specification buildSearch(String searchString) { + Collection filters = buildFiltersFromSearchString(searchString); + return build(filters, CriteriaBuilder::or, true); + } + + public Specification buildFilters(Collection filters) { + return build(filters, CriteriaBuilder::and, false); + } + + private List buildFiltersFromSearchString(String searchString) { + return Stream.of( + FilterFeature.ID, + FilterFeature.NAME, + FilterFeature.DESCRIPTION, + FilterFeature.IDENTIFIER, + FilterFeature.INSTRUCTIONS, + FilterFeature.LABELS + ).map(feature -> FilterRequestDTO + .builder() + .feature(feature) + .value(searchString) + .condition(FilterCondition.CONTAINS) + .build() + ).toList(); + } + + public Specification build( + Collection filters, + BiFunction predicateAccumulator, + boolean searchMode + ) { + return (root, query, criteriaBuilder) -> { + Predicate[] predicates = filters.stream() + .map(filter -> buildPredicate(root, criteriaBuilder, filter)) + .toArray(Predicate[]::new); + return predicateAccumulator.apply(criteriaBuilder, predicates); + }; + } + + private Predicate buildPredicate( + Root root, + CriteriaBuilder criteriaBuilder, + FilterRequestDTO filter + ) { + boolean isNegativeElementFilter = FilterCondition.IS_NOT.equals(filter.getCondition()) + || FilterCondition.NOT_IN.equals(filter.getCondition()); + var conditionPredicateBuilder = filterConditionPredicateBuilderFactory + .getPredicateBuilder(criteriaBuilder, filter.getCondition()); + String value = filter.getValue(); + return switch (filter.getFeature()) { + case ID -> conditionPredicateBuilder.apply(root.get("id"), value); + case NAME -> conditionPredicateBuilder.apply(root.get("name"), value); + case DESCRIPTION -> conditionPredicateBuilder.apply(root.get("description"), value); + case INSTRUCTIONS -> conditionPredicateBuilder.apply(root.get("assumptions"), value); + case IDENTIFIER -> conditionPredicateBuilder.apply(root.get("businessDescription"), value); + default -> throw new IllegalStateException("Unexpected filter feature: " + filter.getFeature()); + }; + } +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/util/ExportImportUtils.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/util/ExportImportUtils.java index 197bcfc4..7fbe2a01 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/util/ExportImportUtils.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/util/ExportImportUtils.java @@ -229,6 +229,12 @@ public static String generateMainContextServiceFileExportName(String id, String : id + CONTEXT_SERVICE_YAML_NAME_POSTFIX + appName + YAML_FILE_NAME_POSTFIX; } + public static String generateMCPServiceFileExportName(String id, String appName, boolean isLegacyExport) { + return isLegacyExport + ? MCP_SERVICE_YAML_NAME_PREFIX + id + "." + YAML_EXTENSION + : id + MCP_SERVICE_YAML_NAME_POSTFIX + appName + YAML_FILE_NAME_POSTFIX; + } + public static String generateSourceExportDir(String id) { return SOURCE_YAML_NAME_PREFIX + id; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 16a0654c..923a765d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -182,6 +182,7 @@ qip: chain: ${CHAIN_JSON_SCHEMA_URI:http://qubership.org/schemas/product/qip/chain} service: ${SERVICE_JSON_SCHEMA_URI:http://qubership.org/schemas/product/qip/service} context-service: ${CONTEXT_SERVICE_JSON_SCHEMA_URI:http://qubership.org/schemas/product/qip/context-service} + mcp-service: ${MCP_SERVICE_JSON_SCHEMA_URI:http://qubership.org/schemas/product/qip/mcp-service} specification-group: ${SPECIFICATION_GROUP_JSON_SCHEMA_URI:http://qubership.org/schemas/product/qip/specification-group} specification: ${SPECIFICATION_JSON_SCHEMA_URI:http://qubership.org/schemas/product/qip/specification} element-descriptors: diff --git a/src/main/resources/db/migration/postgresql/configs/V110_000__add-mcp-systems.sql b/src/main/resources/db/migration/postgresql/configs/V110_000__add-mcp-systems.sql new file mode 100644 index 00000000..2b5cbbbb --- /dev/null +++ b/src/main/resources/db/migration/postgresql/configs/V110_000__add-mcp-systems.sql @@ -0,0 +1,31 @@ +create table mcp_systems +( + id varchar(255) not null + constraint pk_mcp_system primary key, + name text, + description text, + created_when timestamptz, + modified_when timestamptz, + created_by_id text, + created_by_name text, + modified_by_id text, + modified_by_name text, + + identifier text unique, + instructions text +); + +create table mcp_system_labels +( + id varchar(255) not null, + name text not null, + mcp_system_id varchar(255) not null + references mcp_systems on delete cascade, + technical boolean default false +); + +create index idx_mcp_system_labels_mcp_system_id + ON mcp_system_labels (mcp_system_id); + +create unique index uk_mcp_system_labels + ON mcp_system_labels (name, mcp_system_id, technical); From 51a9c356a94960a62cc477cce0fd6463fbe1c382 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:53:11 +0300 Subject: [PATCH 05/16] feat: fixed setting up labels --- .../v1/controller/MCPSystemController.java | 24 ++-------- .../system/mcp/MCPSystemFilterRequestDTO.java | 17 +++++++ .../rest/v1/mapper/MCPSystemMapper.java | 10 ++-- .../catalog/service/MCPSystemService.java | 41 +++++++++------- .../MCPSystemFilterSpecificationBuilder.java | 48 +++++++++++++------ 5 files changed, 86 insertions(+), 54 deletions(-) create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemFilterRequestDTO.java diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java index ee2c4630..9c26a9b5 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java @@ -8,10 +8,9 @@ import lombok.extern.slf4j.Slf4j; import org.qubership.integration.platform.runtime.catalog.model.exportimport.system.ImportSystemResult; import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; -import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.FilterRequestDTO; -import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemSearchRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.imports.ImportSystemStatus; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemCreateRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemFilterRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemResponseDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemUpdateRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.mapper.MCPSystemMapper; @@ -95,24 +94,13 @@ public ResponseEntity delete(@PathVariable String id) { return ResponseEntity.noContent().build(); } - @PostMapping("/search") - @Operation(description = "Search MCP systems") - public ResponseEntity> searchSystems( - @RequestBody SystemSearchRequestDTO systemSearchRequestDTO - ) { - log.debug("Request to search MCP systems: {}", systemSearchRequestDTO); - List systems = mcpSystemService.searchSystems(systemSearchRequestDTO); - List dtos = mcpSystemMapper.toResponseDtos(systems); - return ResponseEntity.ok(dtos); - } - @PostMapping("/filter") @Operation(description = "Filter MCP systems") public ResponseEntity> filter( - @RequestBody List filters + @RequestBody MCPSystemFilterRequestDTO requestDTO ) { - log.debug("Request to filter MCP systems: {}", filters); - List systems = mcpSystemService.filter(filters); + log.debug("Request to filter MCP systems: {}", requestDTO); + List systems = mcpSystemService.filter(requestDTO.getSearchString(), requestDTO.getFilters()); List dtos = mcpSystemMapper.toResponseDtos(systems); return ResponseEntity.ok(dtos); } @@ -120,9 +108,7 @@ public ResponseEntity> filter( @PostMapping(value = "/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @Operation(description = "Export MCP services") public ResponseEntity export( - @RequestParam(required = false) - @Parameter(description = "List of system IDs") - List ids + @RequestBody List ids ) { byte[] data = mcpSystemImportExportService.export(ids); if (isNull(data)) { diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemFilterRequestDTO.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemFilterRequestDTO.java new file mode 100644 index 00000000..06230bf5 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemFilterRequestDTO.java @@ -0,0 +1,17 @@ +package org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.FilterRequestDTO; + +import java.util.List; + +@Data +@Schema(description = "MCP system filter request object") +public class MCPSystemFilterRequestDTO { + @Schema(description = "Search string") + private String searchString; + + @Schema(description = "Filters") + private List filters; +} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java index 427e1f79..911ad23f 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java @@ -1,9 +1,6 @@ package org.qubership.integration.platform.runtime.catalog.rest.v1.mapper; -import org.mapstruct.CollectionMappingStrategy; -import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; -import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.*; import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystemLabel; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemLabelDTO; @@ -26,7 +23,10 @@ public interface MCPSystemMapper { MCPSystemResponseDTO toResponseDto(MCPSystem system); - MCPSystem update(@MappingTarget MCPSystem contextSystem, MCPSystemUpdateRequestDTO request); + @Mapping(target = "labels", ignore = true) + MCPSystem updateWithoutLabels(@MappingTarget MCPSystem contextSystem, MCPSystemUpdateRequestDTO request); + + MCPSystemLabel updateLabel(@MappingTarget MCPSystemLabel label, SystemLabelDTO labelDTO); MCPSystemLabel asLabel(SystemLabelDTO labelDTO); diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java index d09ad2ce..8ff113e2 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java @@ -8,10 +8,11 @@ import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.actionlog.LogOperation; import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.chain.Chain; import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystemLabel; import org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.chain.ChainRepository; import org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.mcp.MCPSystemRepository; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.FilterRequestDTO; -import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemSearchRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemLabelDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemCreateRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemUpdateRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.mapper.MCPSystemMapper; @@ -22,7 +23,10 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; @Service @Transactional @@ -51,8 +55,8 @@ public MCPSystemService( public List findAll(boolean withChains) { List systems = mcpSystemRepository.findAll(); return withChains - ? enrichWithChains(systems) - : systems; + ? enrichWithChains(systems) + : systems; } public List findAll() { @@ -88,8 +92,22 @@ public MCPSystem update(String id, MCPSystemUpdateRequestDTO request) { throw new EntityNotFoundException(String.format("MCP system with id %s not found", id)); } MCPSystem system = mcpSystem.get(); - system = mcpSystemMapper.update(system, request); - return update(system); + MCPSystem updatedSystem = mcpSystemMapper.updateWithoutLabels(system, request); + + Map labelMap = request.getLabels().stream() + .collect(Collectors.toMap(SystemLabelDTO::getName, Function.identity())); + + updatedSystem.getLabels().removeIf(label -> !labelMap.containsKey(label.getName())); + updatedSystem.getLabels().forEach(label -> { + SystemLabelDTO labelDTO = labelMap.remove(label.getName()); + mcpSystemMapper.updateLabel(label, labelDTO); + }); + updatedSystem.getLabels().addAll(labelMap.values().stream().map(l -> { + MCPSystemLabel label = mcpSystemMapper.asLabel(l); + label.setSystem(updatedSystem); + return label; + }).toList()); + return update(updatedSystem); } public MCPSystem update(MCPSystem system) { @@ -108,17 +126,8 @@ public void deleteById(String id) { }); } - public List searchSystems(SystemSearchRequestDTO request) { - return searchSystems(request.getSearchCondition()); - } - - public List searchSystems(String searchCondition) { - Specification specification = mcpSystemFilterSpecificationBuilder.buildSearch(searchCondition); - return enrichWithChains(mcpSystemRepository.findAll(specification)); - } - - public List filter(List filters) { - Specification specification = mcpSystemFilterSpecificationBuilder.buildFilters(filters); + public List filter(String searchString, List filters) { + Specification specification = mcpSystemFilterSpecificationBuilder.buildSearchAndFilters(searchString, filters); return enrichWithChains(mcpSystemRepository.findAll(specification)); } diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java index 54550d69..bc74efdf 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java @@ -3,6 +3,7 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import org.apache.poi.util.StringUtil; import org.qubership.integration.platform.runtime.catalog.model.filter.FilterCondition; import org.qubership.integration.platform.runtime.catalog.model.filter.FilterFeature; import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; @@ -15,6 +16,8 @@ import java.util.function.BiFunction; import java.util.stream.Stream; +import static java.util.Objects.isNull; + @Component public class MCPSystemFilterSpecificationBuilder { private final FilterConditionPredicateBuilderFactory filterConditionPredicateBuilderFactory; @@ -26,13 +29,33 @@ public MCPSystemFilterSpecificationBuilder( this.filterConditionPredicateBuilderFactory = filterConditionPredicateBuilderFactory; } - public Specification buildSearch(String searchString) { + public Specification buildSearchAndFilters(String searchString, Collection filters) { + return (root, query, criteriaBuilder) -> { + Predicate searchPredicate = StringUtil.isBlank(searchString) + ? criteriaBuilder.conjunction() + : buildSearch(searchString, root, criteriaBuilder); + Predicate filterPredicate = isNull(filters) || filters.isEmpty() + ? criteriaBuilder.conjunction() + : buildFilters(filters, root, criteriaBuilder); + return criteriaBuilder.and(searchPredicate, filterPredicate); + }; + } + + private Predicate buildSearch( + String searchString, + Root root, + CriteriaBuilder criteriaBuilder + ) { Collection filters = buildFiltersFromSearchString(searchString); - return build(filters, CriteriaBuilder::or, true); + return build(filters, CriteriaBuilder::or, root, criteriaBuilder); } - public Specification buildFilters(Collection filters) { - return build(filters, CriteriaBuilder::and, false); + private Predicate buildFilters( + Collection filters, + Root root, + CriteriaBuilder criteriaBuilder + ) { + return build(filters, CriteriaBuilder::and, root, criteriaBuilder); } private List buildFiltersFromSearchString(String searchString) { @@ -52,17 +75,16 @@ private List buildFiltersFromSearchString(String searchString) ).toList(); } - public Specification build( + public Predicate build( Collection filters, BiFunction predicateAccumulator, - boolean searchMode + Root root, + CriteriaBuilder criteriaBuilder ) { - return (root, query, criteriaBuilder) -> { - Predicate[] predicates = filters.stream() - .map(filter -> buildPredicate(root, criteriaBuilder, filter)) - .toArray(Predicate[]::new); - return predicateAccumulator.apply(criteriaBuilder, predicates); - }; + Predicate[] predicates = filters.stream() + .map(filter -> buildPredicate(root, criteriaBuilder, filter)) + .toArray(Predicate[]::new); + return predicateAccumulator.apply(criteriaBuilder, predicates); } private Predicate buildPredicate( @@ -70,8 +92,6 @@ private Predicate buildPredicate( CriteriaBuilder criteriaBuilder, FilterRequestDTO filter ) { - boolean isNegativeElementFilter = FilterCondition.IS_NOT.equals(filter.getCondition()) - || FilterCondition.NOT_IN.equals(filter.getCondition()); var conditionPredicateBuilder = filterConditionPredicateBuilderFactory .getPredicateBuilder(criteriaBuilder, filter.getCondition()); String value = filter.getValue(); From 7c5e64eecc2c68efab42eee8f30f61404da4e205 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:22:24 +0300 Subject: [PATCH 06/16] feat: implemented endpoint to get MCP service by ID --- .../rest/v1/controller/MCPSystemController.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java index 9c26a9b5..ed0f9ae1 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java @@ -63,6 +63,18 @@ public ResponseEntity> getAll( return ResponseEntity.ok(dtos); } + @GetMapping("/{id}") + @Operation(description = "Get MCP system by ID") + public ResponseEntity getById( + @PathVariable String id + ) { + log.debug("Request to get MCP system by ID: {}", id); + return mcpSystemService.findById(id).map(system -> { + MCPSystemResponseDTO dto = mcpSystemMapper.toResponseDto(system); + return ResponseEntity.ok(dto); + }).orElse(ResponseEntity.notFound().build()); + } + @PostMapping @Operation(description = "Create MCP system") public ResponseEntity create( From 68e5685d63051f6156c57a605ca441c8e2c52879 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:11:23 +0300 Subject: [PATCH 07/16] fix: fixed identifier filter --- .../service/filter/MCPSystemFilterSpecificationBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java index bc74efdf..2d6d9d04 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java @@ -100,7 +100,7 @@ private Predicate buildPredicate( case NAME -> conditionPredicateBuilder.apply(root.get("name"), value); case DESCRIPTION -> conditionPredicateBuilder.apply(root.get("description"), value); case INSTRUCTIONS -> conditionPredicateBuilder.apply(root.get("assumptions"), value); - case IDENTIFIER -> conditionPredicateBuilder.apply(root.get("businessDescription"), value); + case IDENTIFIER -> conditionPredicateBuilder.apply(root.get("identifier"), value); default -> throw new IllegalStateException("Unexpected filter feature: " + filter.getFeature()); }; } From c60606985390a7dd2c3f891128a5d8c7c1dac2a6 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:14:37 +0300 Subject: [PATCH 08/16] fix: fixed labels filter --- .../MCPSystemFilterSpecificationBuilder.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java index 2d6d9d04..f8be154e 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/filter/MCPSystemFilterSpecificationBuilder.java @@ -1,8 +1,6 @@ package org.qubership.integration.platform.runtime.catalog.service.filter; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.*; import org.apache.poi.util.StringUtil; import org.qubership.integration.platform.runtime.catalog.model.filter.FilterCondition; import org.qubership.integration.platform.runtime.catalog.model.filter.FilterFeature; @@ -99,9 +97,26 @@ private Predicate buildPredicate( case ID -> conditionPredicateBuilder.apply(root.get("id"), value); case NAME -> conditionPredicateBuilder.apply(root.get("name"), value); case DESCRIPTION -> conditionPredicateBuilder.apply(root.get("description"), value); - case INSTRUCTIONS -> conditionPredicateBuilder.apply(root.get("assumptions"), value); + case INSTRUCTIONS -> conditionPredicateBuilder.apply(root.get("instructions"), value); case IDENTIFIER -> conditionPredicateBuilder.apply(root.get("identifier"), value); + case LABELS -> { + Predicate predicate = conditionPredicateBuilder.apply(getJoin(root, "labels").get("name"), value); + boolean negativeLabelFilter = + filter.getCondition() == FilterCondition.IS_NOT + || filter.getCondition() == FilterCondition.DOES_NOT_CONTAIN; + + yield negativeLabelFilter + ? criteriaBuilder.or(predicate, criteriaBuilder.isNull(getJoin(root, "labels").get("name"))) + : predicate; + } default -> throw new IllegalStateException("Unexpected filter feature: " + filter.getFeature()); }; } + + private Join getJoin(Root root, String attributeName) { + return root.getJoins().stream() + .filter(join -> join.getAttribute().getName().equals(attributeName)) + .findAny() + .orElseGet(() -> root.join(attributeName, JoinType.LEFT)); + } } From 94e0daaa3d072beb4d96ce751f36ba8d4a3da822 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:05:25 +0300 Subject: [PATCH 09/16] fix: fixed MCP system import --- .../catalog/service/MCPSystemService.java | 34 +++++++++++-------- .../MCPSystemImportExportService.java | 3 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java index 8ff113e2..f6ad8cef 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java @@ -12,7 +12,6 @@ import org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.chain.ChainRepository; import org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.mcp.MCPSystemRepository; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.FilterRequestDTO; -import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemLabelDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemCreateRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemUpdateRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.mapper.MCPSystemMapper; @@ -22,6 +21,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; @@ -94,19 +94,8 @@ public MCPSystem update(String id, MCPSystemUpdateRequestDTO request) { MCPSystem system = mcpSystem.get(); MCPSystem updatedSystem = mcpSystemMapper.updateWithoutLabels(system, request); - Map labelMap = request.getLabels().stream() - .collect(Collectors.toMap(SystemLabelDTO::getName, Function.identity())); - - updatedSystem.getLabels().removeIf(label -> !labelMap.containsKey(label.getName())); - updatedSystem.getLabels().forEach(label -> { - SystemLabelDTO labelDTO = labelMap.remove(label.getName()); - mcpSystemMapper.updateLabel(label, labelDTO); - }); - updatedSystem.getLabels().addAll(labelMap.values().stream().map(l -> { - MCPSystemLabel label = mcpSystemMapper.asLabel(l); - label.setSystem(updatedSystem); - return label; - }).toList()); + List newLabels = request.getLabels().stream().map(mcpSystemMapper::asLabel).toList(); + updateLabels(updatedSystem, updatedSystem.getLabels(), newLabels); return update(updatedSystem); } @@ -116,6 +105,23 @@ public MCPSystem update(MCPSystem system) { return system; } + public void updateLabels( + MCPSystem system, + Collection currentLabels, + Collection newLabels + ) { + Map labelMap = newLabels.stream() + .collect(Collectors.toMap(MCPSystemLabel::getName, Function.identity())); + + currentLabels.removeIf(label -> !labelMap.containsKey(label.getName())); + currentLabels.forEach(label -> { + MCPSystemLabel updatedLabel = labelMap.remove(label.getName()); + label.setTechnical(updatedLabel.isTechnical()); + }); + labelMap.values().forEach(label -> label.setSystem(system)); + currentLabels.addAll(labelMap.values()); + } + public void deleteById(String id) { if (isUsedByChain(id)) { throw new SystemDeleteException("Service used by one or more chains"); diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportService.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportService.java index aafd9bb9..baabf5fb 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportService.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportService.java @@ -419,10 +419,11 @@ private synchronized ImportSystemResult importOneSystemInTransaction( } private ImportSystemStatus enrichAndSaveSystem(MCPSystem system) { - ImportSystemStatus status; Optional oldSystem = mcpSystemService.findById(system.getId()); if (oldSystem.isPresent()) { + mcpSystemService.updateLabels(system, oldSystem.get().getLabels(), system.getLabels()); + system.setLabels(oldSystem.get().getLabels()); mcpSystemService.update(system); return ImportSystemStatus.UPDATED; } else { From 615c60393bc2ce2868edea7e13e37295ae4cc5fd Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:05:37 +0300 Subject: [PATCH 10/16] feat: one can refer multiple MCP services in MCP trigger --- .../runtime/catalog/model/constant/CamelNames.java | 2 +- .../configs/repository/chain/ChainRepository.java | 12 ++++++++++++ .../runtime/catalog/service/MCPSystemService.java | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/constant/CamelNames.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/constant/CamelNames.java index eca90c6f..ac1f0f7b 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/model/constant/CamelNames.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/model/constant/CamelNames.java @@ -129,7 +129,7 @@ public final class CamelNames { public static final String CHAIN_CALL_ELEMENT_ID = "elementId"; public static final String REUSE_ESTABLISHED_CONN = "reuseEstablishedConnection"; - public static final String MCP_SERVICE_ID = "mcpServiceId"; + public static final String MCP_SERVICE_IDS = "mcpServiceIds"; private CamelNames() { } diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/chain/ChainRepository.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/chain/ChainRepository.java index 41b04a45..a2736c6f 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/chain/ChainRepository.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/persistence/configs/repository/chain/ChainRepository.java @@ -98,6 +98,18 @@ where jsonb_extract_path_text(element.properties, :property) = :value ) List findChainsWithElementPropertyValue(String property, String value); + @Query( + nativeQuery = true, + value = """ + select distinct on (chain.id) chain.* + from catalog.chains chain + inner join catalog.elements element + on element.chain_id = chain.id + where jsonb_exists(jsonb_extract_path(element.properties, :property), :value) + """ + ) + List findChainsWithElementPropertyContainsValue(String property, String value); + @Query( nativeQuery = true, value = """ diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java index f6ad8cef..1677348f 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java @@ -161,6 +161,6 @@ private List enrichWithChains(List systems) { } private List findChainsByMcpSystemId(String mcpSystemId) { - return chainRepository.findChainsWithElementPropertyValue(CamelNames.MCP_SERVICE_ID, mcpSystemId); + return chainRepository.findChainsWithElementPropertyContainsValue(CamelNames.MCP_SERVICE_IDS, mcpSystemId); } } From 016bf74fd72b0bd5d0d8a29db0f23f2fba2d4b22 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:20:47 +0300 Subject: [PATCH 11/16] feat: removed duplicated code --- .../v1/controller/MCPSystemController.java | 7 +++-- .../system/mcp/MCPSystemCreateRequestDTO.java | 26 ------------------- ...questDTO.java => MCPSystemRequestDTO.java} | 4 +-- .../rest/v1/mapper/MCPSystemMapper.java | 4 +-- .../catalog/service/MCPSystemService.java | 7 +++-- 5 files changed, 10 insertions(+), 38 deletions(-) delete mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemCreateRequestDTO.java rename src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/{MCPSystemUpdateRequestDTO.java => MCPSystemRequestDTO.java} (87%) diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java index ed0f9ae1..ea5c35a8 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemController.java @@ -9,10 +9,9 @@ import org.qubership.integration.platform.runtime.catalog.model.exportimport.system.ImportSystemResult; import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.imports.ImportSystemStatus; -import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemCreateRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemFilterRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemResponseDTO; -import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemUpdateRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.mapper.MCPSystemMapper; import org.qubership.integration.platform.runtime.catalog.service.MCPSystemService; import org.qubership.integration.platform.runtime.catalog.service.exportimport.MCPSystemImportExportService; @@ -78,7 +77,7 @@ public ResponseEntity getById( @PostMapping @Operation(description = "Create MCP system") public ResponseEntity create( - @RequestBody MCPSystemCreateRequestDTO requestDTO + @RequestBody MCPSystemRequestDTO requestDTO ) { log.debug("Request to create MCP system: {}", requestDTO); MCPSystem system = mcpSystemService.create(requestDTO); @@ -90,7 +89,7 @@ public ResponseEntity create( @Operation(description = "Update MCP system") public ResponseEntity update( @PathVariable String id, - @RequestBody MCPSystemUpdateRequestDTO requestDTO + @RequestBody MCPSystemRequestDTO requestDTO ) { log.debug("Request to update MCP system: {}", requestDTO); MCPSystem system = mcpSystemService.update(id, requestDTO); diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemCreateRequestDTO.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemCreateRequestDTO.java deleted file mode 100644 index 0ab2a390..00000000 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemCreateRequestDTO.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemLabelDTO; - -import java.util.List; - -@Data -@Schema(description = "Create MCP Service request object") -public class MCPSystemCreateRequestDTO { - @Schema(description = "Name") - private String name; - - @Schema(description = "Description") - private String description; - - @Schema(description = "MCP server name") - private String identifier; - - @Schema(description = "MCP server instructions") - private String instructions; - - @Schema(description = "Labels assigned to the service") - private List labels; -} diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemUpdateRequestDTO.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemRequestDTO.java similarity index 87% rename from src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemUpdateRequestDTO.java rename to src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemRequestDTO.java index 05ee42a4..795bb25d 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemUpdateRequestDTO.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/dto/system/mcp/MCPSystemRequestDTO.java @@ -7,8 +7,8 @@ import java.util.List; @Data -@Schema(description = "Update MCP Service request object") -public class MCPSystemUpdateRequestDTO { +@Schema(description = "MCP Service request object") +public class MCPSystemRequestDTO { @Schema(description = "Name") private String name; diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java index 911ad23f..aca7ee1f 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/rest/v1/mapper/MCPSystemMapper.java @@ -4,8 +4,8 @@ import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystemLabel; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.SystemLabelDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemResponseDTO; -import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemUpdateRequestDTO; import org.qubership.integration.platform.runtime.catalog.util.MapperUtils; import java.util.List; @@ -24,7 +24,7 @@ public interface MCPSystemMapper { MCPSystemResponseDTO toResponseDto(MCPSystem system); @Mapping(target = "labels", ignore = true) - MCPSystem updateWithoutLabels(@MappingTarget MCPSystem contextSystem, MCPSystemUpdateRequestDTO request); + MCPSystem updateWithoutLabels(@MappingTarget MCPSystem contextSystem, MCPSystemRequestDTO request); MCPSystemLabel updateLabel(@MappingTarget MCPSystemLabel label, SystemLabelDTO labelDTO); diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java index 1677348f..ddd707d7 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemService.java @@ -12,8 +12,7 @@ import org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.chain.ChainRepository; import org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.mcp.MCPSystemRepository; import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.FilterRequestDTO; -import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemCreateRequestDTO; -import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemUpdateRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemRequestDTO; import org.qubership.integration.platform.runtime.catalog.rest.v1.mapper.MCPSystemMapper; import org.qubership.integration.platform.runtime.catalog.service.filter.MCPSystemFilterSpecificationBuilder; import org.springframework.beans.factory.annotation.Autowired; @@ -71,7 +70,7 @@ public Optional findById(String id) { return mcpSystemRepository.findById(id); } - public MCPSystem create(MCPSystemCreateRequestDTO request) { + public MCPSystem create(MCPSystemRequestDTO request) { MCPSystem mcpSystem = new MCPSystem(); mcpSystem.setName(request.getName()); mcpSystem.setDescription(request.getDescription()); @@ -86,7 +85,7 @@ public MCPSystem create(MCPSystem system, boolean isImport) { return system; } - public MCPSystem update(String id, MCPSystemUpdateRequestDTO request) { + public MCPSystem update(String id, MCPSystemRequestDTO request) { Optional mcpSystem = mcpSystemRepository.findById(id); if (mcpSystem.isEmpty()) { throw new EntityNotFoundException(String.format("MCP system with id %s not found", id)); From dc50b842ecc8a243c25a4f570b3481b2f9df2c3e Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:15:21 +0300 Subject: [PATCH 12/16] feat: added some unit-tests --- .../controller/MCPSystemControllerTest.java | 327 +++++++++++++++ .../catalog/service/MCPSystemServiceTest.java | 396 ++++++++++++++++++ .../McpTriggerPropertiesBuilderTest.java | 168 ++++++++ .../serializer/ArchiveWriterTest.java | 179 ++++++++ 4 files changed, 1070 insertions(+) create mode 100644 src/test/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemControllerTest.java create mode 100644 src/test/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemServiceTest.java create mode 100644 src/test/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilderTest.java create mode 100644 src/test/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ArchiveWriterTest.java diff --git a/src/test/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemControllerTest.java b/src/test/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemControllerTest.java new file mode 100644 index 00000000..020fc27b --- /dev/null +++ b/src/test/java/org/qubership/integration/platform/runtime/catalog/rest/v1/controller/MCPSystemControllerTest.java @@ -0,0 +1,327 @@ +/* + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.qubership.integration.platform.runtime.catalog.rest.v1.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.system.ImportSystemResult; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.imports.ImportSystemStatus; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemFilterRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemResponseDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.mapper.MCPSystemMapper; +import org.qubership.integration.platform.runtime.catalog.service.MCPSystemService; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.MCPSystemImportExportService; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ExtendWith(MockitoExtension.class) +class MCPSystemControllerTest { + + private static final String BASE_URL = "/v1/catalog/mcp-system"; + private static final String SYSTEM_ID = "system-id-1"; + + @Mock + MCPSystemService mcpSystemService; + + @Mock + MCPSystemMapper mcpSystemMapper; + + @Mock + MCPSystemImportExportService mcpSystemImportExportService; + + @InjectMocks + MCPSystemController mcpSystemController; + + MockMvc mockMvc; + ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(mcpSystemController).build(); + objectMapper = new ObjectMapper(); + } + + // GET /v1/catalog/mcp-system + + @Test + @DisplayName("GET /v1/catalog/mcp-system returns 200 with list of systems") + void getAllReturns200WithDtos() throws Exception { + MCPSystem system = MCPSystem.builder().id(SYSTEM_ID).name("s1").build(); + MCPSystemResponseDTO dto = new MCPSystemResponseDTO(); + dto.setId(SYSTEM_ID); + dto.setName("s1"); + + when(mcpSystemService.findAll(false)).thenReturn(List.of(system)); + when(mcpSystemMapper.toResponseDtos(List.of(system))).thenReturn(List.of(dto)); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].id", is(SYSTEM_ID))); + } + + @Test + @DisplayName("GET /v1/catalog/mcp-system?withChains=true passes withChains=true to service") + void getAllWithChainsFlagPassesTrueToService() throws Exception { + when(mcpSystemService.findAll(true)).thenReturn(List.of()); + when(mcpSystemMapper.toResponseDtos(any())).thenReturn(List.of()); + + mockMvc.perform(get(BASE_URL).param("withChains", "true")) + .andExpect(status().isOk()); + + verify(mcpSystemService).findAll(true); + } + + // GET /v1/catalog/mcp-system/{id} + + @Test + @DisplayName("GET /v1/catalog/mcp-system/{id} returns 200 when system found") + void getByIdReturns200WhenFound() throws Exception { + MCPSystem system = MCPSystem.builder().id(SYSTEM_ID).build(); + MCPSystemResponseDTO dto = new MCPSystemResponseDTO(); + dto.setId(SYSTEM_ID); + + when(mcpSystemService.findById(SYSTEM_ID)).thenReturn(Optional.of(system)); + when(mcpSystemMapper.toResponseDto(system)).thenReturn(dto); + + mockMvc.perform(get(BASE_URL + "/{id}", SYSTEM_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(SYSTEM_ID))); + } + + @Test + @DisplayName("GET /v1/catalog/mcp-system/{id} returns 404 when system not found") + void getByIdReturns404WhenNotFound() throws Exception { + when(mcpSystemService.findById(SYSTEM_ID)).thenReturn(Optional.empty()); + + mockMvc.perform(get(BASE_URL + "/{id}", SYSTEM_ID)) + .andExpect(status().isNotFound()); + } + + // POST /v1/catalog/mcp-system + + @Test + @DisplayName("POST /v1/catalog/mcp-system returns 201 with created system DTO") + void createReturns201WithDto() throws Exception { + MCPSystemRequestDTO request = new MCPSystemRequestDTO(); + request.setName("new-system"); + + MCPSystem created = MCPSystem.builder().id(SYSTEM_ID).name("new-system").build(); + MCPSystemResponseDTO dto = new MCPSystemResponseDTO(); + dto.setId(SYSTEM_ID); + dto.setName("new-system"); + + when(mcpSystemService.create(any(MCPSystemRequestDTO.class))).thenReturn(created); + when(mcpSystemMapper.toResponseDto(created)).thenReturn(dto); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(SYSTEM_ID))) + .andExpect(jsonPath("$.name", is("new-system"))); + } + + // PUT /v1/catalog/mcp-system/{id} + + @Test + @DisplayName("PUT /v1/catalog/mcp-system/{id} returns 200 with updated system DTO") + void updateReturns200WithDto() throws Exception { + MCPSystemRequestDTO request = new MCPSystemRequestDTO(); + request.setName("updated-name"); + + MCPSystem updated = MCPSystem.builder().id(SYSTEM_ID).name("updated-name").build(); + MCPSystemResponseDTO dto = new MCPSystemResponseDTO(); + dto.setId(SYSTEM_ID); + dto.setName("updated-name"); + + when(mcpSystemService.update(eq(SYSTEM_ID), any(MCPSystemRequestDTO.class))).thenReturn(updated); + when(mcpSystemMapper.toResponseDto(updated)).thenReturn(dto); + + mockMvc.perform(put(BASE_URL + "/{id}", SYSTEM_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(SYSTEM_ID))) + .andExpect(jsonPath("$.name", is("updated-name"))); + } + + // DELETE /v1/catalog/mcp-system/{id} + + @Test + @DisplayName("DELETE /v1/catalog/mcp-system/{id} returns 204") + void deleteReturns204() throws Exception { + doNothing().when(mcpSystemService).deleteById(SYSTEM_ID); + + mockMvc.perform(delete(BASE_URL + "/{id}", SYSTEM_ID)) + .andExpect(status().isNoContent()); + + verify(mcpSystemService).deleteById(SYSTEM_ID); + } + + // POST /v1/catalog/mcp-system/filter + + @Test + @DisplayName("POST /v1/catalog/mcp-system/filter returns 200 with filtered results") + void filterReturns200WithResults() throws Exception { + MCPSystemFilterRequestDTO filterRequest = new MCPSystemFilterRequestDTO(); + filterRequest.setSearchString("test"); + filterRequest.setFilters(List.of()); + + MCPSystem system = MCPSystem.builder().id(SYSTEM_ID).build(); + MCPSystemResponseDTO dto = new MCPSystemResponseDTO(); + dto.setId(SYSTEM_ID); + + when(mcpSystemService.filter(eq("test"), any())).thenReturn(List.of(system)); + when(mcpSystemMapper.toResponseDtos(List.of(system))).thenReturn(List.of(dto)); + + mockMvc.perform(post(BASE_URL + "/filter") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(filterRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].id", is(SYSTEM_ID))); + } + + // POST /v1/catalog/mcp-system/export + + @Test + @DisplayName("POST /v1/catalog/mcp-system/export returns 200 with attachment when data present") + void exportReturns200WithAttachmentWhenDataPresent() throws Exception { + byte[] data = new byte[]{1, 2, 3}; + when(mcpSystemImportExportService.export(any())).thenReturn(data); + + mockMvc.perform(post(BASE_URL + "/export") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(List.of(SYSTEM_ID)))) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Disposition", containsString("attachment"))); + } + + @Test + @DisplayName("POST /v1/catalog/mcp-system/export returns 204 when no data") + void exportReturns204WhenNoData() throws Exception { + when(mcpSystemImportExportService.export(any())).thenReturn(null); + + mockMvc.perform(post(BASE_URL + "/export") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(List.of()))) + .andExpect(status().isNoContent()); + } + + // POST /v1/catalog/mcp-system/import + + @Test + @DisplayName("POST /v1/catalog/mcp-system/import returns 200 when all results are OK") + void importSystemsReturns200WhenAllOk() throws Exception { + ImportSystemResult result = ImportSystemResult.builder() + .id(SYSTEM_ID) + .status(ImportSystemStatus.CREATED) + .build(); + + when(mcpSystemImportExportService.importSystems(any(), any())).thenReturn(List.of(result)); + + MockMultipartFile file = new MockMultipartFile("file", "systems.zip", + MediaType.APPLICATION_OCTET_STREAM_VALUE, new byte[]{1, 2, 3}); + + mockMvc.perform(multipart(BASE_URL + "/import").file(file)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].id", is(SYSTEM_ID))); + } + + @Test + @DisplayName("POST /v1/catalog/mcp-system/import returns 207 when some results have errors") + void importSystemsReturns207WhenSomeErrors() throws Exception { + ImportSystemResult ok = ImportSystemResult.builder() + .id("id-1").status(ImportSystemStatus.CREATED).build(); + ImportSystemResult error = ImportSystemResult.builder() + .id("id-2").status(ImportSystemStatus.ERROR).build(); + + when(mcpSystemImportExportService.importSystems(any(), any())).thenReturn(List.of(ok, error)); + + MockMultipartFile file = new MockMultipartFile("file", "systems.zip", + MediaType.APPLICATION_OCTET_STREAM_VALUE, new byte[]{1, 2, 3}); + + mockMvc.perform(multipart(BASE_URL + "/import").file(file)) + .andExpect(status().isMultiStatus()); + } + + @Test + @DisplayName("POST /v1/catalog/mcp-system/import returns 204 when result list is empty") + void importSystemsReturns204WhenEmpty() throws Exception { + when(mcpSystemImportExportService.importSystems(any(), any())).thenReturn(List.of()); + + MockMultipartFile file = new MockMultipartFile("file", "systems.zip", + MediaType.APPLICATION_OCTET_STREAM_VALUE, new byte[]{1, 2, 3}); + + mockMvc.perform(multipart(BASE_URL + "/import").file(file)) + .andExpect(status().isNoContent()); + } + + // POST /v1/catalog/mcp-system/import/preview + + @Test + @DisplayName("POST /v1/catalog/mcp-system/import/preview returns 200 with preview results") + void getImportPreviewReturns200WhenResultsPresent() throws Exception { + ImportSystemResult result = ImportSystemResult.builder() + .id(SYSTEM_ID).status(ImportSystemStatus.CREATED).build(); + + when(mcpSystemImportExportService.getImportPreview(any())).thenReturn(List.of(result)); + + MockMultipartFile file = new MockMultipartFile("file", "systems.zip", + MediaType.APPLICATION_OCTET_STREAM_VALUE, new byte[]{1, 2, 3}); + + mockMvc.perform(multipart(BASE_URL + "/import/preview").file(file)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].id", is(SYSTEM_ID))); + } + + @Test + @DisplayName("POST /v1/catalog/mcp-system/import/preview returns 204 when no preview results") + void getImportPreviewReturns204WhenEmpty() throws Exception { + when(mcpSystemImportExportService.getImportPreview(any())).thenReturn(List.of()); + + MockMultipartFile file = new MockMultipartFile("file", "systems.zip", + MediaType.APPLICATION_OCTET_STREAM_VALUE, new byte[]{1, 2, 3}); + + mockMvc.perform(multipart(BASE_URL + "/import/preview").file(file)) + .andExpect(status().isNoContent()); + } +} diff --git a/src/test/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemServiceTest.java b/src/test/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemServiceTest.java new file mode 100644 index 00000000..22061cf3 --- /dev/null +++ b/src/test/java/org/qubership/integration/platform/runtime/catalog/service/MCPSystemServiceTest.java @@ -0,0 +1,396 @@ +/* + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.qubership.integration.platform.runtime.catalog.service; + +import jakarta.persistence.EntityNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.junit.jupiter.MockitoExtension; +import org.qubership.integration.platform.runtime.catalog.exception.exceptions.SystemDeleteException; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.actionlog.ActionLog; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.actionlog.EntityType; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.actionlog.LogOperation; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.chain.Chain; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystemLabel; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.chain.ChainRepository; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.repository.mcp.MCPSystemRepository; +import org.qubership.integration.platform.runtime.catalog.rest.v1.dto.system.mcp.MCPSystemRequestDTO; +import org.qubership.integration.platform.runtime.catalog.rest.v1.mapper.MCPSystemMapper; +import org.qubership.integration.platform.runtime.catalog.service.filter.MCPSystemFilterSpecificationBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = MCPSystemService.class) +@ExtendWith(SpringExtension.class) +@ExtendWith(MockitoExtension.class) +public class MCPSystemServiceTest { + + private static final String SYSTEM_ID = "system-id-1"; + private static final String SYSTEM_NAME = "Test MCP System"; + private static final String MCP_SERVICE_IDS_PROPERTY = "mcpServiceIds"; + + @MockitoBean + MCPSystemRepository mcpSystemRepository; + + @MockitoBean + MCPSystemMapper mcpSystemMapper; + + @MockitoBean + ActionsLogService actionLogger; + + @MockitoBean + MCPSystemFilterSpecificationBuilder mcpSystemFilterSpecificationBuilder; + + @MockitoBean + ChainRepository chainRepository; + + @Captor + ArgumentCaptor actionLogCaptor; + + @Autowired + MCPSystemService mcpSystemService; + + private MCPSystem mcpSystem; + + @BeforeEach + void setUp() { + mcpSystem = MCPSystem.builder() + .id(SYSTEM_ID) + .name(SYSTEM_NAME) + .build(); + } + + // findAll tests + + @Test + @DisplayName("findAll without chains returns repository result") + void findAllWithoutChainsReturnsRepositoryResult() { + List systems = List.of(mcpSystem); + when(mcpSystemRepository.findAll()).thenReturn(systems); + + List result = mcpSystemService.findAll(false); + + assertThat(result, equalTo(systems)); + verifyNoInteractions(chainRepository); + } + + @Test + @DisplayName("findAll with chains enriches each system with chain list") + void findAllWithChainsEnrichesWithChains() { + List systems = List.of(mcpSystem); + List chains = List.of(Chain.builder().id("chain-1").build()); + when(mcpSystemRepository.findAll()).thenReturn(systems); + when(chainRepository.findChainsWithElementPropertyContainsValue(MCP_SERVICE_IDS_PROPERTY, SYSTEM_ID)) + .thenReturn(chains); + + List result = mcpSystemService.findAll(true); + + assertThat(result.get(0).getChains(), equalTo(chains)); + } + + @Test + @DisplayName("findAll default overload delegates to findAll(false)") + void findAllDefaultDelegatesToFindAllWithoutChains() { + when(mcpSystemRepository.findAll()).thenReturn(List.of()); + + mcpSystemService.findAll(); + + verify(mcpSystemRepository).findAll(); + verifyNoInteractions(chainRepository); + } + + // findAllById tests + + @Test + @DisplayName("findAllById delegates to repository") + void findAllByIdDelegatesToRepository() { + List ids = List.of(SYSTEM_ID); + List systems = List.of(mcpSystem); + when(mcpSystemRepository.findAllById(ids)).thenReturn(systems); + + List result = mcpSystemService.findAllById(ids); + + assertThat(result, equalTo(systems)); + } + + // findById tests + + @Test + @DisplayName("findById returns optional from repository") + void findByIdReturnsOptionalFromRepository() { + when(mcpSystemRepository.findById(SYSTEM_ID)).thenReturn(Optional.of(mcpSystem)); + + Optional result = mcpSystemService.findById(SYSTEM_ID); + + assertTrue(result.isPresent()); + assertThat(result.get(), equalTo(mcpSystem)); + } + + @Test + @DisplayName("findById returns empty optional when not found") + void findByIdReturnsEmptyWhenNotFound() { + when(mcpSystemRepository.findById(SYSTEM_ID)).thenReturn(Optional.empty()); + + Optional result = mcpSystemService.findById(SYSTEM_ID); + + assertFalse(result.isPresent()); + } + + // create(MCPSystemRequestDTO) tests + + @Test + @DisplayName("create from request saves system with mapped fields") + void createFromRequestSavesSystemWithMappedFields() { + MCPSystemRequestDTO request = new MCPSystemRequestDTO(); + request.setName("name"); + request.setDescription("desc"); + request.setIdentifier("ident"); + request.setInstructions("instruc"); + + when(mcpSystemRepository.save(any(MCPSystem.class))).thenAnswer(inv -> inv.getArgument(0)); + + MCPSystem result = mcpSystemService.create(request); + + assertThat(result.getName(), equalTo("name")); + assertThat(result.getDescription(), equalTo("desc")); + assertThat(result.getIdentifier(), equalTo("ident")); + assertThat(result.getInstructions(), equalTo("instruc")); + } + + // create(MCPSystem, boolean) tests + + @Test + @DisplayName("create with isImport=false logs CREATE operation") + void createWithIsImportFalseLogsCreate() { + when(mcpSystemRepository.save(mcpSystem)).thenReturn(mcpSystem); + + mcpSystemService.create(mcpSystem, false); + + verify(actionLogger).logAction(actionLogCaptor.capture()); + ActionLog log = actionLogCaptor.getValue(); + assertThat(log.getOperation(), equalTo(LogOperation.CREATE)); + assertThat(log.getEntityType(), equalTo(EntityType.MCP_SYSTEM)); + assertThat(log.getEntityId(), equalTo(SYSTEM_ID)); + assertThat(log.getEntityName(), equalTo(SYSTEM_NAME)); + } + + @Test + @DisplayName("create with isImport=true logs IMPORT operation") + void createWithIsImportTrueLogsImport() { + when(mcpSystemRepository.save(mcpSystem)).thenReturn(mcpSystem); + + mcpSystemService.create(mcpSystem, true); + + verify(actionLogger).logAction(actionLogCaptor.capture()); + assertThat(actionLogCaptor.getValue().getOperation(), equalTo(LogOperation.IMPORT)); + } + + // update(String, MCPSystemRequestDTO) tests + + @Test + @DisplayName("update throws EntityNotFoundException when system not found") + void updateThrowsEntityNotFoundWhenSystemNotFound() { + when(mcpSystemRepository.findById(SYSTEM_ID)).thenReturn(Optional.empty()); + + MCPSystemRequestDTO request = new MCPSystemRequestDTO(); + request.setLabels(List.of()); + + assertThrows(EntityNotFoundException.class, () -> mcpSystemService.update(SYSTEM_ID, request)); + } + + @Test + @DisplayName("update saves system and logs UPDATE operation") + void updateSavesAndLogsUpdate() { + MCPSystemRequestDTO request = new MCPSystemRequestDTO(); + request.setLabels(List.of()); + + when(mcpSystemRepository.findById(SYSTEM_ID)).thenReturn(Optional.of(mcpSystem)); + when(mcpSystemMapper.updateWithoutLabels(eq(mcpSystem), eq(request))).thenReturn(mcpSystem); + when(mcpSystemRepository.save(mcpSystem)).thenReturn(mcpSystem); + + MCPSystem result = mcpSystemService.update(SYSTEM_ID, request); + + assertThat(result, equalTo(mcpSystem)); + verify(actionLogger).logAction(actionLogCaptor.capture()); + assertThat(actionLogCaptor.getValue().getOperation(), equalTo(LogOperation.UPDATE)); + } + + // update(MCPSystem) tests + + @Test + @DisplayName("update(MCPSystem) saves system and logs UPDATE") + void updateMcpSystemSavesAndLogsUpdate() { + when(mcpSystemRepository.save(mcpSystem)).thenReturn(mcpSystem); + + MCPSystem result = mcpSystemService.update(mcpSystem); + + assertThat(result, equalTo(mcpSystem)); + verify(actionLogger).logAction(actionLogCaptor.capture()); + assertThat(actionLogCaptor.getValue().getOperation(), equalTo(LogOperation.UPDATE)); + } + + // updateLabels tests + + @Test + @DisplayName("updateLabels adds new labels not present in current set") + void updateLabelsAddsNewLabels() { + Set currentLabels = new LinkedHashSet<>(); + MCPSystemLabel newLabel = new MCPSystemLabel("new-label", false, mcpSystem); + List newLabels = List.of(newLabel); + + mcpSystemService.updateLabels(mcpSystem, currentLabels, newLabels); + + assertThat(currentLabels, hasSize(1)); + assertThat(currentLabels.iterator().next().getName(), equalTo("new-label")); + } + + @Test + @DisplayName("updateLabels removes labels absent from new list") + void updateLabelsRemovesStaleLabels() { + MCPSystemLabel existing = new MCPSystemLabel("old-label", false, mcpSystem); + Set currentLabels = new LinkedHashSet<>(List.of(existing)); + + mcpSystemService.updateLabels(mcpSystem, currentLabels, List.of()); + + assertThat(currentLabels, empty()); + } + + @Test + @DisplayName("updateLabels updates technical flag of existing labels") + void updateLabelsUpdatesTechnicalFlag() { + MCPSystemLabel existing = new MCPSystemLabel("label", false, mcpSystem); + Set currentLabels = new LinkedHashSet<>(List.of(existing)); + + MCPSystemLabel updated = new MCPSystemLabel("label", true, mcpSystem); + mcpSystemService.updateLabels(mcpSystem, currentLabels, List.of(updated)); + + assertThat(currentLabels, hasSize(1)); + assertTrue(currentLabels.iterator().next().isTechnical()); + } + + @Test + @DisplayName("updateLabels sets system reference on newly added labels") + void updateLabelsSetsSystemOnNewLabels() { + Set currentLabels = new LinkedHashSet<>(); + MCPSystemLabel newLabel = new MCPSystemLabel(); + newLabel.setName("brand-new"); + + mcpSystemService.updateLabels(mcpSystem, currentLabels, List.of(newLabel)); + + assertThat(currentLabels.iterator().next().getSystem(), equalTo(mcpSystem)); + } + + // deleteById tests + + @Test + @DisplayName("deleteById throws SystemDeleteException when system is used by a chain") + void deleteByIdThrowsWhenUsedByChain() { + when(chainRepository.findChainsWithElementPropertyContainsValue(MCP_SERVICE_IDS_PROPERTY, SYSTEM_ID)) + .thenReturn(List.of(Chain.builder().id("chain-1").build())); + + assertThrows(SystemDeleteException.class, () -> mcpSystemService.deleteById(SYSTEM_ID)); + verify(mcpSystemRepository, never()).delete(any(MCPSystem.class)); + } + + @Test + @DisplayName("deleteById deletes system and logs DELETE when not used by chains") + void deleteByIdDeletesAndLogsWhenNotUsedByChain() { + when(chainRepository.findChainsWithElementPropertyContainsValue(MCP_SERVICE_IDS_PROPERTY, SYSTEM_ID)) + .thenReturn(List.of()); + when(mcpSystemRepository.findById(SYSTEM_ID)).thenReturn(Optional.of(mcpSystem)); + + mcpSystemService.deleteById(SYSTEM_ID); + + verify(mcpSystemRepository).delete(mcpSystem); + verify(actionLogger).logAction(actionLogCaptor.capture()); + assertThat(actionLogCaptor.getValue().getOperation(), equalTo(LogOperation.DELETE)); + } + + @Test + @DisplayName("deleteById does nothing when system not found in repository") + void deleteByIdDoesNothingWhenNotFound() { + when(chainRepository.findChainsWithElementPropertyContainsValue(MCP_SERVICE_IDS_PROPERTY, SYSTEM_ID)) + .thenReturn(List.of()); + when(mcpSystemRepository.findById(SYSTEM_ID)).thenReturn(Optional.empty()); + + mcpSystemService.deleteById(SYSTEM_ID); + + verify(mcpSystemRepository, never()).delete(any(MCPSystem.class)); + verifyNoInteractions(actionLogger); + } + + // filter tests + + @Test + @DisplayName("filter builds specification and enriches result with chains") + void filterBuildsSpecificationAndEnrichesWithChains() { + Specification spec = mock(Specification.class); + List systems = new ArrayList<>(List.of(mcpSystem)); + + when(mcpSystemFilterSpecificationBuilder.buildSearchAndFilters(eq("search"), any())) + .thenReturn(spec); + when(mcpSystemRepository.findAll(spec)).thenReturn(systems); + when(chainRepository.findChainsWithElementPropertyContainsValue(MCP_SERVICE_IDS_PROPERTY, SYSTEM_ID)) + .thenReturn(List.of()); + + List result = mcpSystemService.filter("search", List.of()); + + assertThat(result, hasSize(1)); + verify(mcpSystemFilterSpecificationBuilder).buildSearchAndFilters(eq("search"), any()); + } + + // isUsedByChain tests + + @Test + @DisplayName("isUsedByChain returns true when chains reference the system") + void isUsedByChainReturnsTrueWhenChainExists() { + when(chainRepository.findChainsWithElementPropertyContainsValue(MCP_SERVICE_IDS_PROPERTY, SYSTEM_ID)) + .thenReturn(List.of(Chain.builder().id("chain-1").build())); + + assertTrue(mcpSystemService.isUsedByChain(SYSTEM_ID)); + } + + @Test + @DisplayName("isUsedByChain returns false when no chains reference the system") + void isUsedByChainReturnsFalseWhenNoChains() { + when(chainRepository.findChainsWithElementPropertyContainsValue(MCP_SERVICE_IDS_PROPERTY, SYSTEM_ID)) + .thenReturn(List.of()); + + assertFalse(mcpSystemService.isUsedByChain(SYSTEM_ID)); + } +} diff --git a/src/test/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilderTest.java b/src/test/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilderTest.java new file mode 100644 index 00000000..fcbebc3c --- /dev/null +++ b/src/test/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilderTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.qubership.integration.platform.runtime.catalog.service.deployment.properties.builders; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.chain.element.ChainElement; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; + +class McpTriggerPropertiesBuilderTest { + + private McpTriggerPropertiesBuilder builder; + + @BeforeEach + void setUp() { + builder = new McpTriggerPropertiesBuilder(); + } + + // applicableTo tests + + @Test + @DisplayName("applicableTo returns true for mcp-trigger element type") + void applicableToMcpTriggerTypeReturnsTrue() { + ChainElement element = ChainElement.builder().type("mcp-trigger").build(); + assertTrue(builder.applicableTo(element)); + } + + @Test + @DisplayName("applicableTo returns false for non-mcp-trigger element type") + void applicableToOtherTypeReturnsFalse() { + ChainElement element = ChainElement.builder().type("http-trigger").build(); + assertFalse(builder.applicableTo(element)); + } + + @Test + @DisplayName("applicableTo returns false for null element type") + void applicableToNullTypeReturnsFalse() { + ChainElement element = ChainElement.builder().build(); + assertFalse(builder.applicableTo(element)); + } + + // build tests + + @Test + @DisplayName("build returns all expected keys") + void buildAllExpectedKeysPresent() { + ChainElement element = ChainElement.builder().type("mcp-trigger").build(); + + Map result = builder.build(element); + + assertThat(result.keySet(), containsInAnyOrder( + "mcpServiceId", "name", "title", "description", + "inputSchema", "outputSchema", "readOnly", "destructive", + "idempotent", "openWorld", "requiresLocal" + )); + } + + @Test + @DisplayName("build maps element properties to string values") + void buildWithAllPropertiesMapsCorrectly() { + Map properties = new HashMap<>(); + properties.put("mcpServiceId", "service-123"); + properties.put("name", "my-tool"); + properties.put("title", "My Tool"); + properties.put("description", "Does something"); + properties.put("inputSchema", "{\"type\":\"object\"}"); + properties.put("outputSchema", "{\"type\":\"string\"}"); + properties.put("readOnly", true); + properties.put("destructive", false); + properties.put("idempotent", true); + properties.put("openWorld", false); + properties.put("requiresLocal", true); + + ChainElement element = ChainElement.builder() + .type("mcp-trigger") + .properties(properties) + .build(); + + Map result = builder.build(element); + + assertThat(result, allOf( + hasEntry("mcpServiceId", "service-123"), + hasEntry("name", "my-tool"), + hasEntry("title", "My Tool"), + hasEntry("description", "Does something"), + hasEntry("inputSchema", "{\"type\":\"object\"}"), + hasEntry("outputSchema", "{\"type\":\"string\"}"), + hasEntry("readOnly", "true"), + hasEntry("destructive", "false"), + hasEntry("idempotent", "true"), + hasEntry("openWorld", "false"), + hasEntry("requiresLocal", "true") + )); + } + + @Test + @DisplayName("build returns empty string for missing properties") + void buildWithNoPropertiesReturnsEmptyStrings() { + ChainElement element = ChainElement.builder().type("mcp-trigger").build(); + + Map result = builder.build(element); + + assertThat(result.values(), everyItem(equalTo(""))); + } + + @Test + @DisplayName("build returns empty string for null property value") + void buildWithNullPropertyValueReturnsEmptyString() { + Map properties = new HashMap<>(); + properties.put("name", null); + + ChainElement element = ChainElement.builder() + .type("mcp-trigger") + .properties(properties) + .build(); + + Map result = builder.build(element); + + assertThat(result, hasEntry("name", "")); + } + + @Test + @DisplayName("build converts non-string property values using toString") + void buildWithIntegerPropertyValueConvertsToString() { + Map properties = new HashMap<>(); + properties.put("mcpServiceId", 42); + + ChainElement element = ChainElement.builder() + .type("mcp-trigger") + .properties(properties) + .build(); + + Map result = builder.build(element); + + assertThat(result, hasEntry("mcpServiceId", "42")); + } + + @Test + @DisplayName("build returns exactly 11 entries") + void buildReturnsExactly11Entries() { + ChainElement element = ChainElement.builder().type("mcp-trigger").build(); + + Map result = builder.build(element); + + assertThat(result.size(), equalTo(11)); + } +} diff --git a/src/test/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ArchiveWriterTest.java b/src/test/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ArchiveWriterTest.java new file mode 100644 index 00000000..b705a9b2 --- /dev/null +++ b/src/test/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/serializer/ArchiveWriterTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.qubership.integration.platform.runtime.catalog.model.system.exportimport.ExportableObject; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.qubership.integration.platform.runtime.catalog.service.exportimport.ExportImportConstants.ARCH_PARENT_DIR; + +@ExtendWith(MockitoExtension.class) +class ArchiveWriterTest { + + @Mock + ExportableObjectWriterVisitor exportableObjectWriterVisitor; + + @Captor + ArgumentCaptor entryPathCaptor; + + ArchiveWriter archiveWriter; + + @BeforeEach + void setUp() { + archiveWriter = new ArchiveWriter(exportableObjectWriterVisitor); + } + + @Test + @DisplayName("writeArchive with empty list returns valid empty zip bytes") + void writeArchiveEmptyListReturnsValidZip() throws IOException { + byte[] result = archiveWriter.writeArchive(List.of()); + + assertNotNull(result); + assertTrue(result.length > 0, "Result should contain zip header bytes"); + + // verify the bytes form a valid (empty) zip + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(result))) { + assertNull(zis.getNextEntry(), "Empty archive should have no entries"); + } + } + + @Test + @DisplayName("writeArchive calls accept on single object with correct entry path") + void writeArchiveSingleObjectCallsAcceptWithCorrectPath() throws IOException { + ExportableObject obj = mock(ExportableObject.class); + when(obj.getId()).thenReturn("obj-1"); + + archiveWriter.writeArchive(List.of(obj)); + + verify(obj).accept(eq(exportableObjectWriterVisitor), any(ZipOutputStream.class), entryPathCaptor.capture()); + + String expectedPath = ARCH_PARENT_DIR + File.separator + "obj-1" + File.separator; + assertThat(entryPathCaptor.getValue(), equalTo(expectedPath)); + } + + @Test + @DisplayName("writeArchive calls accept on each object in the list") + void writeArchiveMultipleObjectsCallsAcceptForEach() throws IOException { + ExportableObject obj1 = mock(ExportableObject.class); + ExportableObject obj2 = mock(ExportableObject.class); + ExportableObject obj3 = mock(ExportableObject.class); + when(obj1.getId()).thenReturn("id-1"); + when(obj2.getId()).thenReturn("id-2"); + when(obj3.getId()).thenReturn("id-3"); + + archiveWriter.writeArchive(List.of(obj1, obj2, obj3)); + + verify(obj1).accept(eq(exportableObjectWriterVisitor), any(ZipOutputStream.class), entryPathCaptor.capture()); + verify(obj2).accept(eq(exportableObjectWriterVisitor), any(ZipOutputStream.class), entryPathCaptor.capture()); + verify(obj3).accept(eq(exportableObjectWriterVisitor), any(ZipOutputStream.class), entryPathCaptor.capture()); + + List capturedPaths = entryPathCaptor.getAllValues(); + assertThat(capturedPaths, containsInAnyOrder( + ARCH_PARENT_DIR + File.separator + "id-1" + File.separator, + ARCH_PARENT_DIR + File.separator + "id-2" + File.separator, + ARCH_PARENT_DIR + File.separator + "id-3" + File.separator + )); + } + + @Test + @DisplayName("writeArchive wraps IOException in RuntimeException") + void writeArchiveWrapsIOExceptionInRuntimeException() throws IOException { + ExportableObject obj = mock(ExportableObject.class); + when(obj.getId()).thenReturn("obj-fail"); + doThrow(new IOException("disk error")) + .when(obj).accept(any(), any(), any()); + + RuntimeException ex = assertThrows(RuntimeException.class, + () -> archiveWriter.writeArchive(List.of(obj))); + + assertThat(ex.getMessage(), containsString("Failed to create archive")); + assertThat(ex.getCause(), instanceOf(IOException.class)); + } + + @Test + @DisplayName("writeArchive result contains a zip entry written by the visitor") + void writeArchiveResultContainsEntryWrittenByVisitor() throws IOException { + String entryName = "services/obj-1/system.yaml"; + String entryContent = "name: test"; + + ExportableObject obj = mock(ExportableObject.class); + when(obj.getId()).thenReturn("obj-1"); + doAnswer(invocation -> { + ZipOutputStream zos = invocation.getArgument(1); + zos.putNextEntry(new ZipEntry(entryName)); + zos.write(entryContent.getBytes()); + zos.closeEntry(); + return null; + }).when(obj).accept(any(), any(ZipOutputStream.class), any()); + + byte[] result = archiveWriter.writeArchive(List.of(obj)); + + List entryNames = new ArrayList<>(); + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(result))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + entryNames.add(entry.getName()); + } + } + + assertThat(entryNames, hasSize(1)); + assertThat(entryNames, hasItem(entryName)); + } + + @Test + @DisplayName("writeArchive passes the same ZipOutputStream instance to all accept calls") + void writeArchivePassesSameZipOutputStreamToAllObjects() throws IOException { + ExportableObject obj1 = mock(ExportableObject.class); + ExportableObject obj2 = mock(ExportableObject.class); + when(obj1.getId()).thenReturn("id-1"); + when(obj2.getId()).thenReturn("id-2"); + + ArgumentCaptor zipCaptor = ArgumentCaptor.forClass(ZipOutputStream.class); + + archiveWriter.writeArchive(List.of(obj1, obj2)); + + verify(obj1).accept(any(), zipCaptor.capture(), any()); + ZipOutputStream zip1 = zipCaptor.getValue(); + + verify(obj2).accept(any(), zipCaptor.capture(), any()); + ZipOutputStream zip2 = zipCaptor.getValue(); + + assertSame(zip1, zip2, "All objects must receive the same ZipOutputStream instance"); + } +} From 9f8df659ab2bd842d7e24fcf493938054e12a97a Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:35:04 +0300 Subject: [PATCH 13/16] feat: added some unit-tests --- .../MCPSystemImportExportServiceTest.java | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 src/test/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportServiceTest.java diff --git a/src/test/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportServiceTest.java b/src/test/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportServiceTest.java new file mode 100644 index 00000000..05bc8789 --- /dev/null +++ b/src/test/java/org/qubership/integration/platform/runtime/catalog/service/exportimport/MCPSystemImportExportServiceTest.java @@ -0,0 +1,289 @@ +/* + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.qubership.integration.platform.runtime.catalog.service.exportimport; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.qubership.integration.platform.runtime.catalog.model.exportimport.system.ImportSystemResult; +import org.qubership.integration.platform.runtime.catalog.model.system.exportimport.ExportedSystemObject; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.mcp.MCPSystem; +import org.qubership.integration.platform.runtime.catalog.service.ActionsLogService; +import org.qubership.integration.platform.runtime.catalog.service.MCPSystemService; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.deserializer.MCPSystemDeserializer; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.instructions.ImportInstructionsService; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer.ArchiveWriter; +import org.qubership.integration.platform.runtime.catalog.service.exportimport.serializer.MCPSystemSerializer; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.transaction.support.TransactionTemplate; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MCPSystemImportExportServiceTest { + + private static final String SYSTEM_ID = "system-id-1"; + private static final String SYSTEM_NAME = "Test System"; + private static final byte[] ARCHIVE_BYTES = new byte[]{1, 2, 3}; + + @Mock TransactionTemplate transactionTemplate; + @Mock YAMLMapper yamlMapper; + @Mock MCPSystemService mcpSystemService; + @Mock ActionsLogService actionLogger; + @Mock MCPSystemSerializer mcpSystemSerializer; + @Mock MCPSystemDeserializer mcpSystemDeserializer; + @Mock ArchiveWriter archiveWriter; + @Mock ImportInstructionsService importInstructionsService; + @Mock ImportSessionService importProgressService; + + @Captor ArgumentCaptor> exportedSystemsCaptor; + + MCPSystemImportExportService service; + + @BeforeEach + void setUp() throws Exception { + service = new MCPSystemImportExportService( + transactionTemplate, + yamlMapper, + mcpSystemService, + actionLogger, + mcpSystemSerializer, + mcpSystemDeserializer, + archiveWriter, + importInstructionsService, + importProgressService, + new URI("http://qubership.org/schemas/product/qip/mcp-service") + ); + } + + // export tests + + @Test + @DisplayName("export with null ids calls findAll()") + void exportWithNullIdsFindAll() throws JsonProcessingException { + MCPSystem system = buildSystem(); + ExportedSystemObject exported = mock(ExportedSystemObject.class); + when(mcpSystemService.findAll()).thenReturn(List.of(system)); + when(mcpSystemSerializer.serialize(system)).thenReturn(exported); + when(archiveWriter.writeArchive(anyList())).thenReturn(ARCHIVE_BYTES); + + service.export(null); + + verify(mcpSystemService).findAll(); + verify(mcpSystemService, never()).findAllById(any()); + } + + @Test + @DisplayName("export with ids list calls findAllById()") + void exportWithIdsCallsFindAllById() throws JsonProcessingException { + List ids = List.of(SYSTEM_ID); + MCPSystem system = buildSystem(); + ExportedSystemObject exported = mock(ExportedSystemObject.class); + when(mcpSystemService.findAllById(ids)).thenReturn(List.of(system)); + when(mcpSystemSerializer.serialize(system)).thenReturn(exported); + when(archiveWriter.writeArchive(anyList())).thenReturn(ARCHIVE_BYTES); + + service.export(ids); + + verify(mcpSystemService).findAllById(ids); + verify(mcpSystemService, never()).findAll(); + } + + @Test + @DisplayName("export returns null when no systems found") + void exportReturnsNullWhenNoSystems() { + when(mcpSystemService.findAll()).thenReturn(List.of()); + + byte[] result = service.export(null); + + assertNull(result); + verifyNoInteractions(mcpSystemSerializer, archiveWriter); + } + + @Test + @DisplayName("export returns archive bytes when systems found") + void exportReturnsBytesFromArchiveWriter() throws JsonProcessingException { + MCPSystem system = buildSystem(); + ExportedSystemObject exported = mock(ExportedSystemObject.class); + when(mcpSystemService.findAll()).thenReturn(List.of(system)); + when(mcpSystemSerializer.serialize(system)).thenReturn(exported); + when(archiveWriter.writeArchive(anyList())).thenReturn(ARCHIVE_BYTES); + + byte[] result = service.export(null); + + assertThat(result, equalTo(ARCHIVE_BYTES)); + } + + @Test + @DisplayName("export serializes all systems and passes them to archiveWriter") + void exportSerializesAllSystems() throws JsonProcessingException { + MCPSystem s1 = buildSystem("id-1", "sys-1"); + MCPSystem s2 = buildSystem("id-2", "sys-2"); + ExportedSystemObject e1 = mock(ExportedSystemObject.class); + ExportedSystemObject e2 = mock(ExportedSystemObject.class); + when(mcpSystemService.findAll()).thenReturn(List.of(s1, s2)); + when(mcpSystemSerializer.serialize(s1)).thenReturn(e1); + when(mcpSystemSerializer.serialize(s2)).thenReturn(e2); + when(archiveWriter.writeArchive(exportedSystemsCaptor.capture())).thenReturn(ARCHIVE_BYTES); + + service.export(null); + + List passed = exportedSystemsCaptor.getValue(); + assertThat(passed, containsInAnyOrder(e1, e2)); + } + + @Test + @DisplayName("export logs EXPORT action for each exported system") + void exportLogsExportForEachSystem() throws JsonProcessingException { + MCPSystem s1 = buildSystem("id-1", "sys-1"); + MCPSystem s2 = buildSystem("id-2", "sys-2"); + ExportedSystemObject e1 = mock(ExportedSystemObject.class); + ExportedSystemObject e2 = mock(ExportedSystemObject.class); + when(mcpSystemService.findAll()).thenReturn(List.of(s1, s2)); + when(mcpSystemSerializer.serialize(s1)).thenReturn(e1); + when(mcpSystemSerializer.serialize(s2)).thenReturn(e2); + when(archiveWriter.writeArchive(anyList())).thenReturn(ARCHIVE_BYTES); + + service.export(null); + + verify(actionLogger, times(2)).logAction(any()); + } + + @Test + @DisplayName("export throws RuntimeException when serialization fails") + void exportThrowsWhenSerializationFails() throws JsonProcessingException { + MCPSystem system = buildSystem(); + when(mcpSystemService.findAll()).thenReturn(List.of(system)); + when(mcpSystemSerializer.serialize(system)).thenThrow(mock(JsonProcessingException.class)); + + RuntimeException ex = assertThrows(RuntimeException.class, () -> service.export(null)); + + assertThat(ex.getMessage(), containsString("Failed to export system")); + assertThat(ex.getMessage(), containsString(SYSTEM_NAME)); + assertThat(ex.getMessage(), containsString(SYSTEM_ID)); + } + + // importSystems(MultipartFile, List) error paths + + @Test + @DisplayName("importSystems throws RuntimeException for non-zip file extension") + void importSystemsThrowsForNonZipExtension() { + MockMultipartFile file = new MockMultipartFile( + "file", "systems.yaml", "application/octet-stream", new byte[]{1}); + + RuntimeException ex = assertThrows(RuntimeException.class, + () -> service.importSystems(file, null)); + + assertThat(ex.getMessage(), containsString("Unsupported file extension")); + assertThat(ex.getMessage(), containsString("yaml")); + } + + @Test + @DisplayName("importSystems throws RuntimeException for file with no extension") + void importSystemsThrowsForMissingExtension() { + MockMultipartFile file = new MockMultipartFile( + "file", "systems", "application/octet-stream", new byte[]{1}); + + RuntimeException ex = assertThrows(RuntimeException.class, + () -> service.importSystems(file, null)); + + assertThat(ex.getMessage(), containsString("Unsupported file extension")); + } + + @Test + @DisplayName("importSystems accepts zip file (case-insensitive extension)") + void importSystemsAcceptsZipCaseInsensitive() throws Exception { + byte[] zipBytes = buildEmptyZip(); + MockMultipartFile file = new MockMultipartFile( + "file", "systems.ZIP", "application/octet-stream", zipBytes); + + when(importInstructionsService.performServiceIgnoreInstructions(any(), anyBoolean())) + .thenReturn(new org.qubership.integration.platform.runtime.catalog.model.exportimport.instructions.IgnoreResult( + java.util.Set.of(), List.of())); + + List result = service.importSystems(file, null); + + assertThat(result, empty()); + } + + // getImportPreview(MultipartFile) error paths + + @Test + @DisplayName("getImportPreview throws RuntimeException for non-zip file extension") + void getImportPreviewThrowsForNonZipExtension() { + MockMultipartFile file = new MockMultipartFile( + "file", "systems.yaml", "application/octet-stream", new byte[]{1}); + + RuntimeException ex = assertThrows(RuntimeException.class, + () -> service.getImportPreview(file)); + + assertThat(ex.getMessage(), containsString("Unsupported file extension")); + } + + @Test + @DisplayName("getImportPreview returns empty list for empty zip") + void getImportPreviewReturnsEmptyListForEmptyZip() throws Exception { + byte[] zipBytes = buildEmptyZip(); + MockMultipartFile file = new MockMultipartFile( + "file", "systems.zip", "application/octet-stream", zipBytes); + + when(importInstructionsService.getServiceImportInstructionsConfig(any())) + .thenReturn(new org.qubership.integration.platform.runtime.catalog.model.exportimport.instructions.ImportInstructionsConfig()); + + List result = service.getImportPreview(file); + + assertThat(result, empty()); + } + + // helpers + + private MCPSystem buildSystem() { + return buildSystem(SYSTEM_ID, SYSTEM_NAME); + } + + private MCPSystem buildSystem(String id, String name) { + return MCPSystem.builder().id(id).name(name).build(); + } + + private byte[] buildEmptyZip() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos)) { + zos.putNextEntry(new ZipEntry("placeholder/")); + zos.closeEntry(); + } + return baos.toByteArray(); + } +} From 0382ad3e92034f6fb022c12562a194f121c08eab Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:59:11 +0300 Subject: [PATCH 14/16] feat: fixed property name --- .../builders/McpTriggerPropertiesBuilder.java | 2 +- .../builders/McpTriggerPropertiesBuilderTest.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java index 1e1c669a..0e7d83e6 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilder.java @@ -23,7 +23,7 @@ public boolean applicableTo(ChainElement element) { @Override public Map build(ChainElement element) { return Stream.of( - "mcpServiceId", + "mcpServiceIds", "name", "title", "description", diff --git a/src/test/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilderTest.java b/src/test/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilderTest.java index fcbebc3c..9b3c1101 100644 --- a/src/test/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilderTest.java +++ b/src/test/java/org/qubership/integration/platform/runtime/catalog/service/deployment/properties/builders/McpTriggerPropertiesBuilderTest.java @@ -70,7 +70,7 @@ void buildAllExpectedKeysPresent() { Map result = builder.build(element); assertThat(result.keySet(), containsInAnyOrder( - "mcpServiceId", "name", "title", "description", + "mcpServiceIds", "name", "title", "description", "inputSchema", "outputSchema", "readOnly", "destructive", "idempotent", "openWorld", "requiresLocal" )); @@ -80,7 +80,7 @@ void buildAllExpectedKeysPresent() { @DisplayName("build maps element properties to string values") void buildWithAllPropertiesMapsCorrectly() { Map properties = new HashMap<>(); - properties.put("mcpServiceId", "service-123"); + properties.put("mcpServiceIds", "service-123"); properties.put("name", "my-tool"); properties.put("title", "My Tool"); properties.put("description", "Does something"); @@ -100,7 +100,7 @@ void buildWithAllPropertiesMapsCorrectly() { Map result = builder.build(element); assertThat(result, allOf( - hasEntry("mcpServiceId", "service-123"), + hasEntry("mcpServiceIds", "service-123"), hasEntry("name", "my-tool"), hasEntry("title", "My Tool"), hasEntry("description", "Does something"), @@ -144,7 +144,7 @@ void buildWithNullPropertyValueReturnsEmptyString() { @DisplayName("build converts non-string property values using toString") void buildWithIntegerPropertyValueConvertsToString() { Map properties = new HashMap<>(); - properties.put("mcpServiceId", 42); + properties.put("mcpServiceIds", 42); ChainElement element = ChainElement.builder() .type("mcp-trigger") @@ -153,7 +153,7 @@ void buildWithIntegerPropertyValueConvertsToString() { Map result = builder.build(element); - assertThat(result, hasEntry("mcpServiceId", "42")); + assertThat(result, hasEntry("mcpServiceIds", "42")); } @Test From d02102d2b8cd3b3d08a8cab8f66832d71ae82576 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Mon, 18 May 2026 16:44:32 +0300 Subject: [PATCH 15/16] fix: Added mcp trigger beans builder for micro deployment --- .../element/McpTriggerBeansBuilder.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/main/java/org/qubership/integration/platform/runtime/catalog/cr/sources/builders/xml/beans/builders/element/McpTriggerBeansBuilder.java diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/cr/sources/builders/xml/beans/builders/element/McpTriggerBeansBuilder.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/cr/sources/builders/xml/beans/builders/element/McpTriggerBeansBuilder.java new file mode 100644 index 00000000..a5f32315 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/cr/sources/builders/xml/beans/builders/element/McpTriggerBeansBuilder.java @@ -0,0 +1,55 @@ +package org.qubership.integration.platform.runtime.catalog.cr.sources.builders.xml.beans.builders.element; + +import org.codehaus.stax2.XMLStreamWriter2; +import org.qubership.integration.platform.runtime.catalog.cr.sources.SourceBuilderContext; +import org.qubership.integration.platform.runtime.catalog.cr.sources.builders.xml.beans.ElementBeansBuilder; +import org.qubership.integration.platform.runtime.catalog.persistence.configs.entity.chain.element.ChainElement; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.qubership.integration.platform.runtime.catalog.consul.ConfigurationPropertiesConstants.MCP_TRIGGER_ELEMENT; + +@Component +public class McpTriggerBeansBuilder implements ElementBeansBuilder { + @Override + public boolean applicableTo(ChainElement element) { + String type = element.getType(); + return MCP_TRIGGER_ELEMENT.equals(type); + } + + @Override + public void build(XMLStreamWriter2 streamWriter, ChainElement element, SourceBuilderContext context) throws Exception { + streamWriter.writeStartElement("bean"); + streamWriter.writeAttribute("name", "McpTriggerInfo-" + element.getId()); + streamWriter.writeAttribute("type", "org.qubership.integration.platform.engine.metadata.McpTriggerInfo"); + + streamWriter.writeStartElement("properties"); + + Collection propertyNames = List.of( + "name", + "title", + "description", + "inputSchema", + "outputSchema", + "readOnly", + "destructive", + "idempotent", + "openWorld", + "requiresLocal" + ); + + for (String propertyName : propertyNames) { + streamWriter.writeEmptyElement("property"); + streamWriter.writeAttribute("key", propertyName); + streamWriter.writeAttribute("value", Optional.ofNullable(element.getProperties().get(propertyName)) + .map(String::valueOf) + .orElse("")); + } + + streamWriter.writeEndElement(); + streamWriter.writeEndElement(); + } +} From 2f4bbf24d76b9015b468ec39d8e4ce3996325b51 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Tue, 19 May 2026 11:21:52 +0300 Subject: [PATCH 16/16] fix: Added snapshot element id to element info --- .../xml/beans/builders/element/CommonBeansBuilder.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/qubership/integration/platform/runtime/catalog/cr/sources/builders/xml/beans/builders/element/CommonBeansBuilder.java b/src/main/java/org/qubership/integration/platform/runtime/catalog/cr/sources/builders/xml/beans/builders/element/CommonBeansBuilder.java index bd6c45e7..4427eff9 100644 --- a/src/main/java/org/qubership/integration/platform/runtime/catalog/cr/sources/builders/xml/beans/builders/element/CommonBeansBuilder.java +++ b/src/main/java/org/qubership/integration/platform/runtime/catalog/cr/sources/builders/xml/beans/builders/element/CommonBeansBuilder.java @@ -46,6 +46,10 @@ public void build( streamWriter.writeAttribute("key", "id"); streamWriter.writeAttribute("value", element.getOriginalId()); + streamWriter.writeEmptyElement("property"); + streamWriter.writeAttribute("key", "snapshotElementId"); + streamWriter.writeAttribute("value", element.getId()); + streamWriter.writeEmptyElement("property"); streamWriter.writeAttribute("key", "name"); streamWriter.writeAttribute("value", element.getName());