diff --git a/step-automation-packages/pom.xml b/step-automation-packages/pom.xml
index a5210c3099..c0af7bb484 100644
--- a/step-automation-packages/pom.xml
+++ b/step-automation-packages/pom.xml
@@ -42,9 +42,11 @@
step-automation-packages-manager
step-automation-packages-client
step-automation-packages-controller
+ step-automation-packages-ide
+
diff --git a/step-automation-packages/step-automation-packages-ide/pom.xml b/step-automation-packages/step-automation-packages-ide/pom.xml
new file mode 100644
index 0000000000..db6d0906c9
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+
+ ch.exense.step
+ step-automation-packages
+ 0.0.0-SNAPSHOT
+
+
+ step-automation-packages-ide
+
+
+ ch.exense.step
+ 21
+ 21
+ UTF-8
+
+
+
+
+ ch.exense.step
+ step-plans-base-artefacts
+ ${project.version}
+
+
+ ch.exense.step
+ step-automation-packages-yaml
+ ${project.version}
+
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ ch.exense.step
+ step-plans-core
+ ${project.version}
+ test
+
+
+ ch.exense.step
+ step-functions-plugins-jmeter-def
+ ${project.version}
+ test
+
+
+ ch.exense.step
+ step-functions-plugins-node-def
+ ${project.version}
+ test
+
+
+ ch.exense.step
+ step-automation-packages-controller
+ ${project.version}
+ test
+
+
+
+
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java b/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java
new file mode 100644
index 0000000000..e3be980294
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import java.io.IOException;
+import java.util.Properties;
+
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.core.collections.inmemory.InMemoryCollectionFactory;
+import step.core.plans.Plan;
+
+public class AutomationPackageCollectionFactory implements CollectionFactory {
+
+ private final InMemoryCollectionFactory baseFactory;
+ private final AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackageCollectionFactory(Properties properties, AutomationPackageYamlFragmentManager fragmentManager) {
+ this.fragmentManager = fragmentManager;
+ this.baseFactory = new InMemoryCollectionFactory(properties);
+ }
+
+ @Override
+ public Collection getCollection(String name, Class entityClass) {
+
+ if (entityClass == Plan.class) {
+ return (Collection) new AutomationPackagePlanCollection(fragmentManager);
+ }
+
+ return baseFactory.getCollection(name, entityClass);
+ }
+
+ @Override
+ public Collection getVersionedCollection(String name) {
+ Collection baseCollection = baseFactory.getCollection(name, EntityVersion.class);
+ return baseCollection;
+ }
+
+ @Override
+ public void close() throws IOException {
+ baseFactory.close();
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackagePlanCollection.java b/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackagePlanCollection.java
new file mode 100644
index 0000000000..4ba860257f
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackagePlanCollection.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.core.collections.inmemory.InMemoryCollection;
+import step.core.plans.Plan;
+
+public class AutomationPackagePlanCollection extends InMemoryCollection implements Collection {
+
+
+ private final AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackagePlanCollection(AutomationPackageYamlFragmentManager fragmentManager) {
+ super(true, "plans");
+ this.fragmentManager = fragmentManager;
+ fragmentManager.getPlans().forEach(super::save);
+ }
+
+ @Override
+ public Plan save(Plan p){
+ return super.save(fragmentManager.savePlan(p));
+ }
+
+ @Override
+ public void save(Iterable iterable) {
+ for (Plan p : iterable) {
+ save(p);
+ }
+ }
+
+ @Override
+ public void remove(Filter filter) {
+ find(filter, null, null, null, Integer.MAX_VALUE).forEach(fragmentManager::removePlan);
+ super.remove(filter);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/java/step/core/collections/AutomationPackageCollectionTest.java b/step-automation-packages/step-automation-packages-ide/src/test/java/step/core/collections/AutomationPackageCollectionTest.java
new file mode 100644
index 0000000000..df3a96df65
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/java/step/core/collections/AutomationPackageCollectionTest.java
@@ -0,0 +1,303 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import ch.exense.commons.app.Configuration;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import step.artefacts.Echo;
+import step.artefacts.Sequence;
+import step.automation.packages.AutomationPackageHookRegistry;
+import step.automation.packages.AutomationPackageReadingException;
+import step.automation.packages.JavaAutomationPackageReader;
+import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
+import step.automation.packages.yaml.AutomationPackageConcurrentEditException;
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.automation.packages.yaml.YamlAutomationPackageVersions;
+import step.core.dynamicbeans.DynamicValue;
+import step.core.plans.Plan;
+import step.parameter.ParameterManager;
+import step.parameter.automation.AutomationPackageParametersRegistration;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.*;
+
+public class AutomationPackageCollectionTest {
+
+ private final JavaAutomationPackageReader reader;
+
+ private final File sourceDirectory = new File("src/test/resources/samples/step-automation-packages-sample1");
+ private File destinationDirectory;
+ private Collection planCollection;
+ private final Path expectedFilesPath = sourceDirectory.toPath().resolve("expected");
+ private AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackageCollectionTest() {
+ AutomationPackageSerializationRegistry serializationRegistry = new AutomationPackageSerializationRegistry();
+ AutomationPackageHookRegistry hookRegistry = new AutomationPackageHookRegistry();
+
+ // accessor is not required in this test - we only read the yaml and don't store the result anywhere
+ AutomationPackageParametersRegistration.registerParametersHooks(hookRegistry, serializationRegistry, Mockito.mock(ParameterManager.class));
+
+ this.reader = new JavaAutomationPackageReader(YamlAutomationPackageVersions.ACTUAL_JSON_SCHEMA_PATH, hookRegistry, serializationRegistry, new Configuration());
+ }
+
+ @Before
+ public void setUp() throws IOException, AutomationPackageReadingException {
+ Properties properties = new Properties();
+ destinationDirectory = Files.createTempDirectory("automationPackageCollectionTest").toFile();
+ FileUtils.copyDirectory(sourceDirectory, destinationDirectory);
+
+ fragmentManager = reader.provideAutomationPackageYamlFragmentManager(destinationDirectory);
+ AutomationPackageCollectionFactory collectionFactory = new AutomationPackageCollectionFactory(properties, fragmentManager);
+ planCollection = collectionFactory.getCollection("plan", Plan.class);
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ FileUtils.deleteDirectory(destinationDirectory);
+ }
+
+ @Test
+ public void testReadAllPlans() {
+ long count = planCollection.count(Filters.empty(), 100);
+ List plans = planCollection.find(Filters.empty(), null, null, null, 100).collect(Collectors.toList());
+
+ assertEquals(2, count);
+ Set names = plans.stream().map(p -> p.getAttributes().get("name")).collect(Collectors.toUnmodifiableSet());
+
+ assertEquals(2, names.size());
+
+ assertTrue(names.contains("Test Plan"));
+ assertTrue(names.contains("Test Plan with Composite"));
+ }
+
+ @Test
+ public void testPlanModify() throws IOException {
+ Optional optionalPlan = planCollection.find(Filters.equals("attributes.name", "Test Plan"), null, null, null, 100).findFirst();
+
+ assertTrue(optionalPlan.isPresent());
+
+ Plan plan = optionalPlan.get();
+
+ Echo firstEcho = (Echo) plan.getRoot().getChildren().get(0);
+ DynamicValue text = firstEcho.getText();
+ text.setDynamic(true);
+ text.setExpression("new Date().toString();");
+
+ planCollection.save(plan);
+
+ assertFilesEqual(expectedFilesPath.resolve("plan1AfterModification.yml"), destinationDirectory.toPath().resolve("plans").resolve("plan1.yml"));
+ }
+
+
+ @Test
+ public void testPlanModifyWithConcurrentEdit() throws IOException {
+ Optional optionalPlan = planCollection.find(Filters.equals("attributes.name", "Test Plan"), null, null, null, 100).findFirst();
+
+ assertTrue(optionalPlan.isPresent());
+
+ Plan plan = optionalPlan.get();
+
+ Echo firstEcho = (Echo) plan.getRoot().getChildren().get(0);
+ DynamicValue text = firstEcho.getText();
+ text.setDynamic(true);
+ text.setExpression("new Date().toString();");
+
+ Files.writeString(destinationDirectory.toPath().resolve("plans").resolve("plan1.yml"), "This file was edited", StandardCharsets.UTF_8);
+
+ assertThrows(AutomationPackageConcurrentEditException.class, () -> planCollection.save(plan));
+
+ }
+
+
+ @Test
+ public void testPlanRenameExisting() throws IOException {
+ Optional optionalPlan = planCollection.find(Filters.equals("attributes.name", "Test Plan"), null, null, null, 100).findFirst();
+
+ assertTrue(optionalPlan.isPresent());
+
+ Plan plan = optionalPlan.get();
+
+ plan.getAttributes().put("name", "New Plan Name");
+
+ planCollection.save(plan);
+
+ assertFilesEqual(expectedFilesPath.resolve("plan1AfterRename.yml"), destinationDirectory.toPath().resolve("plans").resolve("plan1.yml"));
+ }
+
+
+ @Test
+ public void testPlanRemoveExisting() throws IOException {
+ planCollection.remove(Filters.equals("attributes.name", "Test Plan"));
+
+ assertFilesEqual(expectedFilesPath.resolve("plan1AfterRemove.yml"), destinationDirectory.toPath().resolve("plans").resolve("plan1.yml"));
+ }
+
+ @Test
+ public void testAddPlanToExistingFragmentWithExistingPlans() throws IOException {
+
+ Sequence sequence = new Sequence();
+ Echo echo = new Echo();
+ echo.setText(new DynamicValue<>("Hello World"));
+ sequence.addChild(echo);
+
+ Plan plan = new Plan(sequence);
+ Map attributes = new HashMap<>();
+ attributes.put("name", "New Name");
+ plan.setAttributes(attributes);
+
+ Properties properties = new Properties();
+ properties.setProperty(AutomationPackageYamlFragmentManager.PROPERTY_NEW_PLAN_FRAGMENT_PATH, "plans/plan1.yml");
+ fragmentManager.setProperties(properties);
+ planCollection.save(plan);
+
+ assertFilesEqual(expectedFilesPath.resolve("plan1AfterAdd.yml"), destinationDirectory.toPath().resolve("plans").resolve("plan1.yml"));
+ }
+
+ @Test
+ public void testPlanModifyAndAdd() throws IOException {
+ Optional optionalPlan = planCollection.find(Filters.equals("attributes.name", "Test Plan"), null, null, null, 100).findFirst();
+
+ assertTrue(optionalPlan.isPresent());
+
+ Plan plan = optionalPlan.get();
+
+ Echo firstEcho = (Echo) plan.getRoot().getChildren().get(0);
+ DynamicValue text = firstEcho.getText();
+ text.setDynamic(true);
+ text.setExpression("new Date().toString();");
+
+ planCollection.save(plan);
+
+ Sequence sequence = new Sequence();
+ Echo echo = new Echo();
+ echo.setText(new DynamicValue<>("Hello World"));
+ sequence.addChild(echo);
+
+ plan = new Plan(sequence);
+ Map attributes = new HashMap<>();
+ attributes.put("name", "New Name");
+ plan.setAttributes(attributes);
+
+ Properties properties = new Properties();
+ properties.setProperty(AutomationPackageYamlFragmentManager.PROPERTY_NEW_PLAN_FRAGMENT_PATH, "plans/plan1.yml");
+ fragmentManager.setProperties(properties);
+ planCollection.save(plan);
+
+ assertFilesEqual(expectedFilesPath.resolve("plan1AfterModifyAndAdd.yml"), destinationDirectory.toPath().resolve("plans").resolve("plan1.yml"));
+ }
+
+ @Test
+ public void testPlanModifyAndAddAndRemove() throws IOException {
+ Optional optionalPlan = planCollection.find(Filters.equals("attributes.name", "Test Plan"), null, null, null, 100).findFirst();
+
+ assertTrue(optionalPlan.isPresent());
+
+ Plan plan = optionalPlan.get();
+
+ Echo firstEcho = (Echo) plan.getRoot().getChildren().get(0);
+ DynamicValue text = firstEcho.getText();
+ text.setDynamic(true);
+ text.setExpression("new Date().toString();");
+
+ planCollection.save(plan);
+
+ Sequence sequence = new Sequence();
+ Echo echo = new Echo();
+ echo.setText(new DynamicValue<>("Hello World"));
+ sequence.addChild(echo);
+
+ plan = new Plan(sequence);
+ Map attributes = new HashMap<>();
+ attributes.put("name", "New Name");
+ plan.setAttributes(attributes);
+
+ Properties properties = new Properties();
+ properties.setProperty(AutomationPackageYamlFragmentManager.PROPERTY_NEW_PLAN_FRAGMENT_PATH, "plans/plan1.yml");
+ fragmentManager.setProperties(properties);
+ planCollection.save(plan);
+
+ assertFilesEqual(expectedFilesPath.resolve("plan1AfterModifyAndAdd.yml"), destinationDirectory.toPath().resolve("plans").resolve("plan1.yml"));
+
+ planCollection.remove(Filters.equals("attributes.name", "New Name"));
+
+
+ assertFilesEqual(expectedFilesPath.resolve("plan1AfterModification.yml"), destinationDirectory.toPath().resolve("plans").resolve("plan1.yml"));
+ }
+
+ @Test
+ public void testAddPlanToDescriptorWithPresentButEmptyPlanArray() throws IOException {
+
+ Sequence sequence = new Sequence();
+ Echo echo = new Echo();
+ echo.setText(new DynamicValue<>("Hello World"));
+ sequence.addChild(echo);
+
+ Plan plan = new Plan(sequence);
+ Map attributes = new HashMap<>();
+ attributes.put("name", "New Name");
+ plan.setAttributes(attributes);
+
+ planCollection.save(plan);
+
+ assertFilesEqual(expectedFilesPath.resolve("descriptorAfterAdd.yml"), destinationDirectory.toPath().resolve("automation-package.yml"));
+ }
+
+
+ @Test
+ public void testAddPlanToNewFragment() throws IOException {
+
+ Sequence sequence = new Sequence();
+ Echo echo = new Echo();
+ echo.setText(new DynamicValue<>("Hello World"));
+ sequence.addChild(echo);
+
+ Plan plan = new Plan(sequence);
+ Map attributes = new HashMap<>();
+ attributes.put("name", "Hello World Plan");
+ plan.setAttributes(attributes);
+
+
+ Properties properties = new Properties();
+ properties.setProperty(AutomationPackageYamlFragmentManager.PROPERTY_NEW_PLAN_FRAGMENT_PATH, "plans/%name%.yml");
+ fragmentManager.setProperties(properties);
+ planCollection.save(plan);
+
+ assertFilesEqual(expectedFilesPath.resolve("Hello_World_Plan.yml"), destinationDirectory.toPath().resolve("plans").resolve("Hello_World_Plan.yml"));
+
+ }
+
+ private void assertFilesEqual(Path expected, Path actual) throws IOException {
+ String expectedLines = Files.readString(expected);
+ String actualLines = Files.readString(actual);
+
+ assertEquals(expectedLines, actualLines);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/.apignore b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/.apignore
new file mode 100644
index 0000000000..319325c32d
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/.apignore
@@ -0,0 +1,2 @@
+/ignored
+/ignoredFile.yml
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/automation-package.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/automation-package.yml
new file mode 100644
index 0000000000..c80fc11302
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/automation-package.yml
@@ -0,0 +1,22 @@
+schemaVersion: 1.0.0
+name: "My package"
+alertingRules:
+ - name: "Rule1"
+ description: "My test alerting rule"
+ eventClass: ExecutionEndedEvent
+ conditions:
+ - BindingCondition:
+ description: "condition 1"
+ bindingKey: "myKey"
+ negate: false
+ predicate:
+ BindingValueEqualsPredicate:
+ value: "myValue"
+plans: []
+fragments:
+ - "keywords.yml"
+ - "plans/*.yml"
+ - "schedules.yml"
+ - "parameters.yml"
+ - "parameters2.yml"
+ - "unknown.yml"
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/Hello_World_Plan.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/Hello_World_Plan.yml
new file mode 100644
index 0000000000..877fe4ae44
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/Hello_World_Plan.yml
@@ -0,0 +1,11 @@
+---
+plans:
+- version: "1.2.0"
+ name: "Hello World Plan"
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
+ agents: null
+ categories: null
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/descriptorAfterAdd.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/descriptorAfterAdd.yml
new file mode 100644
index 0000000000..ac6f041ffe
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/descriptorAfterAdd.yml
@@ -0,0 +1,31 @@
+schemaVersion: 1.0.0
+name: "My package"
+alertingRules:
+ - name: "Rule1"
+ description: "My test alerting rule"
+ eventClass: ExecutionEndedEvent
+ conditions:
+ - BindingCondition:
+ description: "condition 1"
+ bindingKey: "myKey"
+ negate: false
+ predicate:
+ BindingValueEqualsPredicate:
+ value: "myValue"
+plans:
+- version: "1.2.0"
+ name: "New Name"
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
+ agents: null
+ categories: null
+fragments:
+ - "keywords.yml"
+ - "plans/*.yml"
+ - "schedules.yml"
+ - "parameters.yml"
+ - "parameters2.yml"
+ - "unknown.yml"
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterAdd.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterAdd.yml
new file mode 100644
index 0000000000..2b847c9cdb
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterAdd.yml
@@ -0,0 +1,35 @@
+---
+fragments: []
+keywords: []
+plans:
+- name: "Test Plan"
+ root:
+ testCase:
+ children:
+ - echo:
+ text: "Just echo"
+ - echo:
+ text:
+ expression: "mySimpleKey"
+ - callKeyword:
+ nodeName: "CallMyKeyword2"
+ inputs:
+ - myInput: "myValue"
+ keyword: "MyKeyword2"
+ categories:
+ - "Yaml Plan"
+- version: "1.2.0"
+ name: "New Name"
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
+ agents: null
+ categories: null
+plansPlainText:
+- name: "Plain text plan"
+ rootType: "Sequence"
+ categories:
+ - "PlainTextPlan"
+ file: "plans/plan2.plan"
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterModification.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterModification.yml
new file mode 100644
index 0000000000..642a8f1072
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterModification.yml
@@ -0,0 +1,30 @@
+---
+fragments: []
+keywords: []
+plans:
+- version: "1.2.0"
+ name: "Test Plan"
+ root:
+ testCase:
+ nodeName: "Test Plan"
+ children:
+ - echo:
+ text:
+ expression: "new Date().toString();"
+ - echo:
+ text:
+ expression: "mySimpleKey"
+ - callKeyword:
+ nodeName: "CallMyKeyword2"
+ inputs:
+ - myInput: "myValue"
+ keyword: "MyKeyword2"
+ agents: null
+ categories:
+ - "Yaml Plan"
+plansPlainText:
+- name: "Plain text plan"
+ rootType: "Sequence"
+ categories:
+ - "PlainTextPlan"
+ file: "plans/plan2.plan"
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterModifyAndAdd.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterModifyAndAdd.yml
new file mode 100644
index 0000000000..7c8f7d27ea
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterModifyAndAdd.yml
@@ -0,0 +1,39 @@
+---
+fragments: []
+keywords: []
+plans:
+- version: "1.2.0"
+ name: "Test Plan"
+ root:
+ testCase:
+ nodeName: "Test Plan"
+ children:
+ - echo:
+ text:
+ expression: "new Date().toString();"
+ - echo:
+ text:
+ expression: "mySimpleKey"
+ - callKeyword:
+ nodeName: "CallMyKeyword2"
+ inputs:
+ - myInput: "myValue"
+ keyword: "MyKeyword2"
+ agents: null
+ categories:
+ - "Yaml Plan"
+- version: "1.2.0"
+ name: "New Name"
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
+ agents: null
+ categories: null
+plansPlainText:
+- name: "Plain text plan"
+ rootType: "Sequence"
+ categories:
+ - "PlainTextPlan"
+ file: "plans/plan2.plan"
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRemove.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRemove.yml
new file mode 100644
index 0000000000..f01060a03f
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRemove.yml
@@ -0,0 +1,9 @@
+---
+fragments: []
+keywords: []
+plansPlainText:
+- name: "Plain text plan"
+ rootType: "Sequence"
+ categories:
+ - "PlainTextPlan"
+ file: "plans/plan2.plan"
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRename.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRename.yml
new file mode 100644
index 0000000000..8cd5bc5d57
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRename.yml
@@ -0,0 +1,29 @@
+---
+fragments: []
+keywords: []
+plans:
+- version: "1.2.0"
+ name: "New Plan Name"
+ root:
+ testCase:
+ nodeName: "Test Plan"
+ children:
+ - echo:
+ text: "Just echo"
+ - echo:
+ text:
+ expression: "mySimpleKey"
+ - callKeyword:
+ nodeName: "CallMyKeyword2"
+ inputs:
+ - myInput: "myValue"
+ keyword: "MyKeyword2"
+ agents: null
+ categories:
+ - "Yaml Plan"
+plansPlainText:
+- name: "Plain text plan"
+ rootType: "Sequence"
+ categories:
+ - "PlainTextPlan"
+ file: "plans/plan2.plan"
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/ignoredFile.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/ignoredFile.yml
new file mode 100644
index 0000000000..06f858207b
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/ignoredFile.yml
@@ -0,0 +1 @@
+#I should be ignored
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/keywords.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/keywords.yml
new file mode 100644
index 0000000000..5d33a14488
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/keywords.yml
@@ -0,0 +1,30 @@
+keywords:
+ - JMeter:
+ name: "JMeter keyword from automation package"
+ description: "JMeter keyword 1"
+ executeLocally: false
+ useCustomTemplate: true
+ callTimeout: 1000
+ jmeterTestplan: "jmeterProject1/jmeterProject1.xml"
+ - Composite:
+ name: "Composite keyword from AP"
+ plan:
+ root:
+ testCase:
+ children:
+ - echo:
+ text: "Just echo"
+ - return:
+ output:
+ - output1: "value"
+ - output2:
+ expression: "'some thing dynamic'"
+ - GeneralScript:
+ name: "GeneralScript keyword from AP"
+ scriptLanguage: javascript
+ scriptFile: "jsProject/jsSample.js"
+ librariesFile: "lib/fakeLib.jar"
+ - Node:
+ name: "NodeAutomation"
+ jsfile: "nodeProject/nodeSample.ts"
+
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters.yml
new file mode 100644
index 0000000000..587e90b308
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters.yml
@@ -0,0 +1,14 @@
+parameters:
+ - key: myKey
+ value: myValue
+ description: some description
+ activationScript: abc
+ priority: 10
+ protectedValue: true
+ scope: APPLICATION
+ scopeEntity: entity
+ - key: mySimpleKey
+ value: mySimpleValue
+ - key: myDynamicParam
+ value:
+ expression: "mySimpleKey"
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters2.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters2.yml
new file mode 100644
index 0000000000..8754e85903
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters2.yml
@@ -0,0 +1,9 @@
+parameters:
+ - key: myKey2
+ value: myValue2
+ description: some description 2
+ activationScript: abc
+ priority: 10
+ protectedValue: true
+ scope: APPLICATION
+ scopeEntity: entity
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plan.plan b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plan.plan
new file mode 100644
index 0000000000..66b7ca6d84
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plan.plan
@@ -0,0 +1,3 @@
+Sequence
+Echo "Testing annotated plan"
+End
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan1.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan1.yml
new file mode 100644
index 0000000000..26e6c014f5
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan1.yml
@@ -0,0 +1,26 @@
+---
+fragments: []
+keywords: []
+plans:
+- name: "Test Plan"
+ root:
+ testCase:
+ children:
+ - echo:
+ text: "Just echo"
+ - echo:
+ text:
+ expression: "mySimpleKey"
+ - callKeyword:
+ nodeName: "CallMyKeyword2"
+ inputs:
+ - myInput: "myValue"
+ keyword: "MyKeyword2"
+ categories:
+ - "Yaml Plan"
+plansPlainText:
+- name: "Plain text plan"
+ rootType: "Sequence"
+ categories:
+ - "PlainTextPlan"
+ file: "plans/plan2.plan"
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.plan b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.plan
new file mode 100644
index 0000000000..66b7ca6d84
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.plan
@@ -0,0 +1,3 @@
+Sequence
+Echo "Testing annotated plan"
+End
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.yml
new file mode 100644
index 0000000000..2a576d02da
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.yml
@@ -0,0 +1,17 @@
+plans:
+ - name: Test Plan with Composite
+ categories:
+ - Yaml Plan
+ - Composite
+ root:
+ testCase:
+ children:
+ - echo:
+ text: "Calling composite"
+ - callKeyword:
+ keyword: "Composite keyword from AP"
+ children:
+ - check:
+ expression: "output.output1.equals('value')"
+ - check:
+ expression: "output.output2.equals('some thing dynamic')"
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/firstPlainText.plan b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/firstPlainText.plan
new file mode 100644
index 0000000000..e9dd736886
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/firstPlainText.plan
@@ -0,0 +1,3 @@
+Sequence
+Echo "First plain text plan"
+End
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/secondPlainText.plan b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/secondPlainText.plan
new file mode 100644
index 0000000000..00c3bacb0d
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/secondPlainText.plan
@@ -0,0 +1,3 @@
+Sequence
+Echo "Second plain text plan"
+End
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/schedules.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/schedules.yml
new file mode 100644
index 0000000000..6d95ed7161
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/schedules.yml
@@ -0,0 +1,7 @@
+schedules:
+ - name: "firstSchedule"
+ cron: "0 15 10 ? * *"
+ cronExclusions:
+ - "0 0 9 25 * ?"
+ - "0 0 9 20 * ?"
+ planName: "Test Plan"
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/unknown.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/unknown.yml
new file mode 100644
index 0000000000..d378e6078d
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/unknown.yml
@@ -0,0 +1,3 @@
+unknown:
+ - someFieldA: valueA
+ someFieldB: valueB
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/AutomationPackageReader.java b/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/AutomationPackageReader.java
index c0a1ad4b0b..72b07722f9 100644
--- a/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/AutomationPackageReader.java
+++ b/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/AutomationPackageReader.java
@@ -24,6 +24,7 @@
import step.automation.packages.model.ScriptAutomationPackageKeyword;
import step.automation.packages.yaml.AutomationPackageDescriptorReader;
import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
import step.automation.packages.yaml.model.AutomationPackageDescriptorYaml;
import step.automation.packages.yaml.model.AutomationPackageFragmentYaml;
import step.core.plans.Plan;
@@ -122,7 +123,8 @@ protected AutomationPackageContent buildAutomationPackage(AutomationPackageDescr
// apply imported fragments recursively
if (descriptor != null) {
- fillAutomationPackageWithImportedFragments(res, descriptor, archive);
+ Map fragmentMap = new HashMap<>();
+ fillAutomationPackageWithImportedFragments(res, descriptor, archive, fragmentMap);
}
return res;
}
@@ -173,7 +175,22 @@ protected AutomationPackageContent newContentInstance() {
abstract protected void fillAutomationPackageWithAnnotatedKeywordsAndPlans(T archive, AutomationPackageContent res) throws AutomationPackageReadingException;
- public void fillAutomationPackageWithImportedFragments(AutomationPackageContent targetPackage, AutomationPackageFragmentYaml fragment, T archive) throws AutomationPackageReadingException {
+ public AutomationPackageYamlFragmentManager provideAutomationPackageYamlFragmentManager(T archive) throws AutomationPackageReadingException {
+ AutomationPackageDescriptorReader reader = getOrCreateDescriptorReader();
+ URL descriptorURL = archive.getDescriptorYamlUrl();
+ try (InputStream inputStream = descriptorURL.openStream()){
+ AutomationPackageDescriptorYaml descriptor = reader.readAutomationPackageDescriptor(inputStream, archive.getOriginalFileName());
+ descriptor.setFragmentUrl(descriptorURL);
+ AutomationPackageContent res = newContentInstance();
+ Map fragmentMap = new HashMap<>();
+ fillAutomationPackageWithImportedFragments(res, descriptor, archive, fragmentMap);
+ return new AutomationPackageYamlFragmentManager(descriptor, fragmentMap, getOrCreateDescriptorReader());
+ } catch (IOException e) {
+ throw new AutomationPackageReadingException("Failed to read automation package for editing", e);
+ }
+ }
+
+ private void fillAutomationPackageWithImportedFragments(AutomationPackageContent targetPackage, AutomationPackageFragmentYaml fragment, T archive, Map fragmentYamlMap) throws AutomationPackageReadingException {
fillContentSections(targetPackage, fragment, archive);
if (!fragment.getFragments().isEmpty()) {
@@ -182,7 +199,9 @@ public void fillAutomationPackageWithImportedFragments(AutomationPackageContent
for (URL resource : resources) {
try (InputStream fragmentYamlStream = resource.openStream()) {
fragment = getOrCreateDescriptorReader().readAutomationPackageFragment(fragmentYamlStream, importedFragmentReference, archive.getAutomationPackageName());
- fillAutomationPackageWithImportedFragments(targetPackage, fragment, archive);
+ fragmentYamlMap.put(resource.toString(), fragment);
+ fragment.setFragmentUrl(resource);
+ fillAutomationPackageWithImportedFragments(targetPackage, fragment, archive, fragmentYamlMap);
} catch (IOException e) {
throw new AutomationPackageReadingException("Unable to read fragment in automation package: " + importedFragmentReference, e);
}
diff --git a/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java b/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java
index f8898bd7b8..1c8068e575 100644
--- a/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java
+++ b/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java
@@ -4,11 +4,11 @@
import org.apache.commons.lang3.StringUtils;
import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
import step.automation.packages.model.ScriptAutomationPackageKeyword;
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
import step.core.accessors.AbstractOrganizableObject;
import step.core.dynamicbeans.DynamicValue;
import step.core.plans.Plan;
import step.core.scanner.AnnotationScanner;
-import step.engine.plugins.LocalFunctionPlugin;
import step.functions.Function;
import step.functions.manager.FunctionManagerImpl;
import step.handlers.javahandler.Keyword;
@@ -22,7 +22,10 @@
import step.plugins.functions.types.CompositeFunctionUtils;
import step.plugins.java.GeneralScriptFunction;
-import java.io.*;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
@@ -241,7 +244,7 @@ private static boolean isCompositeFunction(Keyword annotation) {
* @param automationPackage the JAR file to be read
* @param apVersion the automation package version
* @param keywordLib the package library file
- * @return the automation package content raed from the provided files
+ * @return the automation package content read from the provided files
* @throws AutomationPackageReadingException in case of error
*/
public AutomationPackageContent readAutomationPackageFromJarFile(File automationPackage, String apVersion, File keywordLib) throws AutomationPackageReadingException {
@@ -251,4 +254,17 @@ public AutomationPackageContent readAutomationPackageFromJarFile(File automation
throw new AutomationPackageReadingException("IO Exception", e);
}
}
+
+ /** Convenient method for test
+ * @param automationPackage the JAR file to be read
+ * @return the automation package content read from the provided files for editing
+ * @throws AutomationPackageReadingException in case of error
+ */
+ public AutomationPackageYamlFragmentManager provideAutomationPackageYamlFragmentManager(File automationPackage) throws AutomationPackageReadingException {
+ try (JavaAutomationPackageArchive automationPackageArchive = new JavaAutomationPackageArchive(automationPackage, null, null)) {
+ return provideAutomationPackageYamlFragmentManager(automationPackageArchive);
+ } catch (IOException e) {
+ throw new AutomationPackageReadingException("IO Exception", e);
+ }
+ }
}
diff --git a/step-automation-packages/step-automation-packages-schema/src/test/java/step/automation/packages/yaml/schema/YamlAutomationPackageSchemaGeneratorTest.java b/step-automation-packages/step-automation-packages-schema/src/test/java/step/automation/packages/yaml/schema/YamlAutomationPackageSchemaGeneratorTest.java
index 795f4766a8..b4b540d83d 100644
--- a/step-automation-packages/step-automation-packages-schema/src/test/java/step/automation/packages/yaml/schema/YamlAutomationPackageSchemaGeneratorTest.java
+++ b/step-automation-packages/step-automation-packages-schema/src/test/java/step/automation/packages/yaml/schema/YamlAutomationPackageSchemaGeneratorTest.java
@@ -35,6 +35,6 @@ public void generateJsonSchema() throws IOException, JsonSchemaPreparationExcept
String errorMessage = "Published schema doesn't match to the actual one. To fix the test you need to publish " +
"the generated schema printed above and actualize the published schema in current test";
- Assert.assertEquals(errorMessage, publishedSchema, currentSchema);
+ Assert.assertEquals(errorMessage, publishedSchema.toPrettyString(), currentSchema.toPrettyString());
}
}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageConcurrentEditException.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageConcurrentEditException.java
new file mode 100644
index 0000000000..5f9feb513e
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageConcurrentEditException.java
@@ -0,0 +1,7 @@
+package step.automation.packages.yaml;
+
+public class AutomationPackageConcurrentEditException extends AutomationPackageUpdateException {
+ public AutomationPackageConcurrentEditException(String s) {
+ super(s);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageDescriptorReader.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageDescriptorReader.java
index 5cd4764660..6c84eb6c7b 100644
--- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageDescriptorReader.java
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageDescriptorReader.java
@@ -18,8 +18,11 @@
******************************************************************************/
package step.automation.packages.yaml;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
+import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import org.apache.commons.lang3.StringUtils;
@@ -29,13 +32,15 @@
import step.artefacts.handlers.JsonSchemaValidator;
import step.automation.packages.AutomationPackageReadingException;
import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
-import step.automation.packages.deserialization.AutomationPackageSerializationRegistryAware;
+import step.automation.packages.yaml.deserialization.PatchableYamlListDeserializer;
+import step.automation.packages.yaml.deserialization.PatchableYamlModelDeserializer;
+import step.automation.packages.yaml.deserialization.PatchingParserDelegate;
import step.automation.packages.yaml.model.AutomationPackageDescriptorYaml;
import step.automation.packages.yaml.model.AutomationPackageDescriptorYamlImpl;
import step.automation.packages.yaml.model.AutomationPackageFragmentYaml;
import step.automation.packages.yaml.model.AutomationPackageFragmentYamlImpl;
import step.core.accessors.DefaultJacksonMapperProvider;
-import step.core.yaml.deserializers.StepYamlDeserializersScanner;
+import step.core.yaml.PatchableYamlModel;
import step.plans.parser.yaml.YamlPlanReader;
import step.plans.parser.yaml.model.YamlPlanVersions;
import step.plans.parser.yaml.schema.YamlPlanValidationException;
@@ -105,8 +110,10 @@ protected T readAutomationPackageYamlF
}
}
- T res = yamlObjectMapper.reader().withAttribute("version", version).readValue(yamlDescriptorString, targetClass);
-
+ PatchingParserDelegate parser = new PatchingParserDelegate(yamlObjectMapper.createParser(yamlDescriptorString));
+ yamlObjectMapper.setNodeFactory(new LocatedYamlObjectFactory(parser));
+ T res = yamlObjectMapper.reader().withAttribute("version", version).readValue(parser, targetClass);
+ res.setCurrentYaml(yamlDescriptorString);
logAfterRead(packageName, res);
return res;
} catch (IOException | YamlPlanValidationException e) {
@@ -143,32 +150,54 @@ protected String readJsonSchema(String jsonSchemaPath) {
}
}
- protected ObjectMapper createYamlObjectMapper() {
+ public ObjectMapper createYamlObjectMapper() {
YAMLFactory yamlFactory = new YAMLFactory();
// Disable native type id to enable conversion to generic Documents
yamlFactory.disable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID);
ObjectMapper yamlMapper = DefaultJacksonMapperProvider.getObjectMapper(yamlFactory);
-
+ yamlMapper.setInjectableValues(new InjectableValues.Std()
+ .addValue(ObjectMapper.class, yamlMapper)
+ .addValue(AutomationPackageSerializationRegistry.class, serializationRegistry)
+ );
// configure custom deserializers
- SimpleModule module = new SimpleModule();
-
- // register deserializers to read yaml plans
- planReader.registerAllSerializersAndDeserializers(module, yamlMapper, true);
+ SimpleModule module = new SimpleModule() {
+ @Override
+ public void setupModule(SetupContext context) {
+ super.setupModule(context);
+
+ context.addBeanDeserializerModifier(new BeanDeserializerModifier() {
+ @Override
+ public JsonDeserializer> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer> deserializer) {
+ if (PatchableYamlModel.class.isAssignableFrom(beanDesc.getBeanClass())
+ && !beanDesc.getBeanClass().equals(PatchableYamlModel.class)) {
+ return new PatchableYamlModelDeserializer<>(deserializer);
+ }
+ return super.modifyDeserializer(config, beanDesc, deserializer);
+ }
- // add annotated jackson deserializers
- StepYamlDeserializersScanner.addAllDeserializerAddonsToModule(module, yamlMapper, List.of(stepYamlDeserializer -> {
- if (stepYamlDeserializer instanceof AutomationPackageSerializationRegistryAware) {
- ((AutomationPackageSerializationRegistryAware) stepYamlDeserializer).setSerializationRegistry(serializationRegistry);
+ @Override
+ public JsonDeserializer> modifyCollectionDeserializer(DeserializationConfig config, CollectionType type, BeanDescription beanDesc, JsonDeserializer> deserializer) {
+ if (deserializer instanceof CollectionDeserializer) {
+ return new PatchableYamlListDeserializer((CollectionDeserializer) deserializer);
+ }
+ return deserializer;
+ }
+ });
}
- }));
-
+ };
+ // register deserializers to read yaml plans
+ planReader.registerAllSerializersAndDeserializers(module, yamlMapper, true);
yamlMapper.registerModule(module);
return yamlMapper;
}
+ public ObjectMapper getYamlObjectMapper() {
+ return yamlObjectMapper;
+ }
+
public YamlPlanReader getPlanReader() {
return this.planReader;
}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageUpdateException.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageUpdateException.java
new file mode 100644
index 0000000000..9024882c50
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageUpdateException.java
@@ -0,0 +1,11 @@
+package step.automation.packages.yaml;
+
+public class AutomationPackageUpdateException extends RuntimeException {
+ public AutomationPackageUpdateException(String s, Exception e) {
+ super(s, e);
+ }
+
+ public AutomationPackageUpdateException(String s) {
+ super(s);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageWriteToDiskException.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageWriteToDiskException.java
new file mode 100644
index 0000000000..5ebf9deee5
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageWriteToDiskException.java
@@ -0,0 +1,7 @@
+package step.automation.packages.yaml;
+
+public class AutomationPackageWriteToDiskException extends AutomationPackageUpdateException {
+ public AutomationPackageWriteToDiskException(String s, Exception e) {
+ super(s, e);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageYamlFragmentManager.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageYamlFragmentManager.java
new file mode 100644
index 0000000000..15c0fe9c49
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageYamlFragmentManager.java
@@ -0,0 +1,256 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.automation.packages.yaml;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.io.FileUtils;
+import step.automation.packages.yaml.deserialization.PatchingParserDelegate;
+import step.automation.packages.yaml.model.AutomationPackageDescriptorYaml;
+import step.automation.packages.yaml.model.AutomationPackageFragmentYaml;
+import step.automation.packages.yaml.model.AutomationPackageFragmentYamlImpl;
+import step.core.accessors.AbstractOrganizableObject;
+import step.core.plans.Plan;
+import step.core.yaml.PatchableYamlList;
+import step.core.yaml.PatchableYamlModel;
+import step.plans.parser.yaml.YamlPlan;
+
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+public class AutomationPackageYamlFragmentManager {
+
+
+ public static final String PROPERTY_NEW_PLAN_FRAGMENT_PATH = "newFragmentPaths.plans";
+ private final AutomationPackageDescriptorReader descriptorReader;
+
+ private final Map planToYamlPlan = new ConcurrentHashMap<>();
+ private final Map planToYamlFragment = new ConcurrentHashMap<>();
+ private final Map pathToYamlFragment;
+ private Properties properties = new Properties();
+ private final AutomationPackageFragmentYaml descriptorYaml;
+
+ public AutomationPackageYamlFragmentManager(AutomationPackageDescriptorYaml descriptorYaml, Map fragmentMap, AutomationPackageDescriptorReader descriptorReader) {
+
+ this.descriptorReader = descriptorReader;
+ this.descriptorYaml = descriptorYaml;
+
+ pathToYamlFragment = fragmentMap;
+
+ initializeMaps(descriptorYaml);
+
+ pathToYamlFragment.values().forEach(this::initializeMaps);
+ }
+
+ public void setProperties(Properties properties) {
+ this.properties = properties;
+ }
+
+ public void initializeMaps(AutomationPackageFragmentYaml fragment) {
+ pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment);
+ for (YamlPlan p: fragment.getPlans()) {
+ Plan plan = descriptorReader.getPlanReader().yamlPlanToPlan(p);
+ planToYamlPlan.put(plan, p);
+ planToYamlFragment.put(plan, fragment);
+ }
+ }
+
+ public Iterable getPlans() {
+ return planToYamlPlan.keySet();
+ }
+
+ public Plan savePlan(Plan p) {
+ YamlPlan newYamlPlan = descriptorReader.getPlanReader().planToYamlPlan(p);
+
+ AutomationPackageFragmentYaml fragment = planToYamlFragment.get(p);
+ if (fragment == null) {
+ fragment = fragmentForNewPlan(p);
+ planToYamlFragment.put(p, fragment);
+ pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment);
+ addFragmentEntity(fragment, YamlPlan.PLANS_ENTITY_NAME, fragment.getPlans(), newYamlPlan);
+ } else {
+ YamlPlan yamlPlan = planToYamlPlan.get(p);
+ modifyFragmentEntity(fragment, fragment.getPlans(), yamlPlan, newYamlPlan);
+ }
+ planToYamlPlan.put(p, newYamlPlan);
+
+ return p;
+ }
+
+ private void addFragmentEntity(AutomationPackageFragmentYaml fragment, String collectionName, PatchableYamlList entityList, T newEntity) {
+ if (!entityList.isEmpty()) {
+ T lastEntity = entityList.get(entityList.size()-1);
+ entityList.add(newEntity);
+
+ writeFragmentToDisk(fragment, yaml -> {
+
+ String listItemIndent = yaml.substring(lastEntity.getStartFieldOffset(), lastEntity.getStartOffset());
+ String indent = " ".repeat(lastEntity.getIndent());
+ String entityYaml = entityStringWithIndent(indent, newEntity);
+
+ return yaml.substring(0, lastEntity.getEndOffset())
+ + listItemIndent + entityYaml + yaml.substring(lastEntity.getEndOffset());
+ });
+
+ } else {
+ entityList.add(newEntity);
+ writeFragmentToDisk(fragment, yaml -> {
+
+ String listYaml = collectionName + ":\n"
+ + entityStringWithIndent("", entityList);
+
+ if (yaml == null) {
+ return "---\n" + listYaml;
+ } else {
+ return yaml.substring(0, entityList.getStartFieldOffset())
+ + listYaml + yaml.substring(entityList.getEndOffset());
+ }
+
+ });
+ }
+ }
+
+ private void modifyFragmentEntity(AutomationPackageFragmentYaml fragment, PatchableYamlList entityList, T oldEntity, T newEntity) {
+ entityList.replaceAll(plan -> plan == oldEntity ? newEntity : plan);
+ writeFragmentToDisk(fragment, yaml -> {
+ int indent = oldEntity.getIndent();
+ String indentString = " ".repeat(indent);
+ String newArtefactString = entityStringWithIndent(indentString, newEntity);
+ return yaml.substring(0, oldEntity.getStartOffset())
+ + newArtefactString + yaml.substring(oldEntity.getEndOffset());
+
+ });
+ }
+
+ private String entityStringWithIndent(String indentString, Object entity) {
+ try {
+ return descriptorReader
+ .getYamlObjectMapper()
+ .writeValueAsString(entity)
+ .replaceAll("---\n", "")
+ .trim()
+ .replaceAll("\n", "\n" + indentString);
+ } catch (JsonProcessingException e) {
+ throw new AutomationPackageUpdateException("Error Serializing new object", e);
+ }
+ }
+
+ private void removeFragmentEntity(AutomationPackageFragmentYaml fragment, PatchableYamlList entityList, T entity) {
+ entityList.remove(entity);
+ writeFragmentToDisk(fragment, yaml -> {
+ int s = entityList.isEmpty() ? entityList.getStartFieldOffset() : entity.getStartFieldOffset();
+ return yaml.substring(0, s) + yaml.substring(entity.getEndOffset());
+ });
+ }
+
+ private AutomationPackageFragmentYaml fragmentForNewPlan(Plan p) {
+
+ String planFragmentPath = properties.getProperty(PROPERTY_NEW_PLAN_FRAGMENT_PATH, descriptorYaml.getFragmentUrl().getPath());
+ planFragmentPath = planFragmentPath.replaceAll("%name%", sanitizeFilename(p.getAttribute(AbstractOrganizableObject.NAME)));
+
+ Path path = new File(planFragmentPath).toPath();
+ if (!path.isAbsolute()) {
+ Path apRoot = Path.of(descriptorYaml.getFragmentUrl().getPath())
+ .getParent();
+ path = apRoot.resolve(path);
+ }
+
+ try {
+ URL url = path.toUri().toURL();
+
+
+ if (pathToYamlFragment.containsKey(url.toString())) return pathToYamlFragment.get(url.toString());
+ AutomationPackageFragmentYaml fragment = new AutomationPackageFragmentYamlImpl();
+ fragment.setFragmentUrl(url);
+ return fragment;
+ } catch (MalformedURLException e) {
+ throw new AutomationPackageUpdateException(MessageFormat.format("Error creating path for new fragment: {0}", path), e);
+ }
+
+ }
+
+ public String sanitizeFilename(String inputName) {
+ return inputName.replaceAll("[^a-zA-Z0-9-_.]", "_");
+ }
+
+ public void removePlan(Plan p) {
+ AutomationPackageFragmentYaml fragment = planToYamlFragment.get(p);
+ YamlPlan yamlPlan = planToYamlPlan.get(p);
+
+ fragment.getPlans().remove(yamlPlan);
+
+ planToYamlPlan.remove(p);
+ planToYamlFragment.remove(p);
+
+ removeFragmentEntity(fragment, fragment.getPlans(), yamlPlan);
+ }
+
+ private void updateFragmentObjectOffsets(AutomationPackageFragmentYaml fragment) {
+ try {
+ ObjectMapper mapper = descriptorReader.getYamlObjectMapper();
+ JsonParser parser = new PatchingParserDelegate(mapper.createParser(fragment.getCurrentYaml()));
+
+ AutomationPackageFragmentYaml newFragment = descriptorReader.getYamlObjectMapper().readValue(parser, fragment.getClass());
+
+ updateFragmentObjectOffsets(newFragment.getPlans(), fragment.getPlans());
+ } catch (IOException e) {
+ throw new AutomationPackageUpdateException("Error re-writing automation package fragment {0}", e);
+ }
+ }
+
+ private void updateFragmentObjectOffsets(PatchableYamlList newOffsetEntities, PatchableYamlList entities) {
+ Iterator newIt = newOffsetEntities.iterator();
+ Iterator it = entities.iterator();
+
+ while (newIt.hasNext() && it.hasNext()) {
+ it.next().setPatchingBounds(newIt.next());
+ }
+
+ if (newIt.hasNext() || it.hasNext()) {
+ throw new AutomationPackageUpdateException("Error with updating fragment object offsets. Inconsistent collection size.");
+ }
+
+ entities.setPatchingBounds(newOffsetEntities);
+ }
+
+ private synchronized void writeFragmentToDisk(AutomationPackageFragmentYaml fragment, Function yamlModifier) {
+ try {
+ File file = new File(fragment.getFragmentUrl().toURI());
+ if (file.exists()) {
+ if (!fragment.getCurrentYaml().equals(FileUtils.readFileToString(file, StandardCharsets.UTF_8))) {
+ throw new AutomationPackageConcurrentEditException(MessageFormat.format("Automation package fragment {0} was edited outside the editor.", fragment.getFragmentUrl()));
+ }
+ }
+ fragment.setCurrentYaml(yamlModifier.apply(fragment.getCurrentYaml()));
+ updateFragmentObjectOffsets(fragment);
+ FileUtils.writeStringToFile(file, fragment.getCurrentYaml(), StandardCharsets.UTF_8);
+ } catch (IOException | URISyntaxException e) {
+ throw new AutomationPackageWriteToDiskException(MessageFormat.format("Error when writing automation package fragment {0} back to disk.", fragment.getFragmentUrl()), e);
+ }
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/LocatedJsonNode.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/LocatedJsonNode.java
new file mode 100644
index 0000000000..78b600cc6c
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/LocatedJsonNode.java
@@ -0,0 +1,43 @@
+package step.automation.packages.yaml;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.ObjectCodec;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TreeTraversingParser;
+
+import javax.json.Json;
+
+public class LocatedJsonNode extends ObjectNode {
+
+ private JsonLocation startLocation;
+ private JsonLocation endLocation;
+
+ public LocatedJsonNode(JsonNodeFactory nodeFactory) {
+ super(nodeFactory);
+ }
+
+ @Override
+ public JsonParser traverse(ObjectCodec codec) {
+ // wrap the default TreeTraversingParser with your own delegate
+ return new LocationAwareTreeTraversingParser(super.traverse(codec), this);
+ }
+
+ @Override
+ public JsonParser traverse() {
+ return new LocationAwareTreeTraversingParser(super.traverse(), this);
+ }
+
+ public JsonLocation getStartLocation() {
+ return startLocation;
+ }
+
+ public void setEndLocation(JsonLocation endLocation) {
+ this.endLocation = endLocation;
+ }
+
+ public void setStartLocation(JsonLocation startLocation) {
+ this.startLocation = startLocation;
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/LocatedYamlObjectFactory.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/LocatedYamlObjectFactory.java
new file mode 100644
index 0000000000..5dd759fc2d
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/LocatedYamlObjectFactory.java
@@ -0,0 +1,19 @@
+package step.automation.packages.yaml;
+
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import step.automation.packages.yaml.deserialization.PatchingParserDelegate;
+
+public class LocatedYamlObjectFactory extends JsonNodeFactory {
+ private final PatchingParserDelegate parser;
+
+ public LocatedYamlObjectFactory(PatchingParserDelegate parser) {
+ this.parser = parser;
+
+ }
+
+ @Override
+ public ObjectNode objectNode() {
+ return parser.setCurrentObjectNode(new LocatedJsonNode(this));
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/LocationAwareTreeTraversingParser.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/LocationAwareTreeTraversingParser.java
new file mode 100644
index 0000000000..f2c0648325
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/LocationAwareTreeTraversingParser.java
@@ -0,0 +1,22 @@
+package step.automation.packages.yaml;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import step.automation.packages.yaml.deserialization.PatchingParserDelegate;
+
+
+public class LocationAwareTreeTraversingParser extends PatchingParserDelegate {
+
+ private final LocatedJsonNode sourceNode;
+
+ public LocationAwareTreeTraversingParser(JsonParser delegate,
+ LocatedJsonNode sourceNode) {
+ super(delegate);
+ this.sourceNode = sourceNode;
+ }
+
+ @Override
+ public JsonLocation currentLocation() {
+ return sourceNode.getStartLocation();
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/PatchableYamlListDeserializer.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/PatchableYamlListDeserializer.java
new file mode 100644
index 0000000000..790ce21f2e
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/PatchableYamlListDeserializer.java
@@ -0,0 +1,49 @@
+package step.automation.packages.yaml.deserialization;
+
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.deser.NullValueProvider;
+import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer;
+import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
+import step.core.yaml.PatchableYamlList;
+
+import java.io.IOException;
+import java.util.Collection;
+
+public class PatchableYamlListDeserializer extends CollectionDeserializer {
+
+ public PatchableYamlListDeserializer(CollectionDeserializer delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public Collection deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ if (p instanceof PatchingParserDelegate) {
+ PatchingParserDelegate patchingParser = (PatchingParserDelegate) p;
+ JsonLocation startFieldLocation = patchingParser.getDistinctLocationBeforeToken(JsonToken.FIELD_NAME);
+ JsonLocation startLocation = patchingParser.getDistinctLocationBeforeToken(JsonToken.START_ARRAY);
+ Collection entity = super.deserialize(p, ctxt);
+ PatchableYamlList patchableYamlList = new PatchableYamlList<>(entity);
+ patchableYamlList.setPatchingBounds(startLocation, (int) startFieldLocation.getCharOffset(), patchingParser.currentLocation());
+ return patchableYamlList;
+ }
+ return super.deserialize(p, ctxt);
+ }
+
+
+ @Override
+ protected CollectionDeserializer withResolved(
+ JsonDeserializer> keyDeser,
+ JsonDeserializer> valueDeser,
+ TypeDeserializer valueTypeDeser,
+ NullValueProvider nuller,
+ Boolean unwrapSingle) {
+ CollectionDeserializer resolved = super.withResolved(keyDeser, valueDeser, valueTypeDeser, nuller, unwrapSingle);
+ // Return your custom instance instead of the default one
+ return new PatchableYamlListDeserializer(resolved);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/PatchableYamlModelDeserializer.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/PatchableYamlModelDeserializer.java
new file mode 100644
index 0000000000..615816b748
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/PatchableYamlModelDeserializer.java
@@ -0,0 +1,56 @@
+package step.automation.packages.yaml.deserialization;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
+import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
+import step.core.yaml.PatchableYamlModel;
+
+import java.io.IOException;
+
+public class PatchableYamlModelDeserializer extends JsonDeserializer implements ContextualDeserializer {
+
+ private final JsonDeserializer delegate;
+
+ public PatchableYamlModelDeserializer(JsonDeserializer> delegate) {
+ this.delegate = (JsonDeserializer) delegate;
+ }
+
+ @Override
+ public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ if (p instanceof PatchingParserDelegate) {
+ PatchingParserDelegate patchingParser = (PatchingParserDelegate) p;
+ int startListItemOffset;
+ if (p.getLastClearedToken() == JsonToken.END_OBJECT) {
+ startListItemOffset = (int) patchingParser.getDistinctLocationBeforeToken(JsonToken.END_OBJECT).getCharOffset();
+ } else {
+ startListItemOffset = (int) patchingParser.getDistinctLocationBeforeToken(JsonToken.START_ARRAY).getCharOffset() + 1;
+ }
+ JsonLocation startItem = patchingParser.currentLocation();
+ T entity = delegate.deserialize(p, ctxt);
+ entity.setPatchingBounds(startItem, startListItemOffset, patchingParser.getLastDistinctLocation());
+
+ return entity;
+ }
+ return delegate.deserialize(p, ctxt);
+ }
+
+ @Override
+ public JsonDeserializer> createContextual(DeserializationContext ctxt,
+ BeanProperty property) throws JsonMappingException {
+ JsonDeserializer> contextual = delegate;
+ if (contextual instanceof ContextualDeserializer) {
+ // make sure to propagate createContextual to the delegate
+ contextual = ((ContextualDeserializer) contextual).createContextual(ctxt, property);
+ }
+ if (contextual instanceof ResolvableDeserializer) {
+ ((ResolvableDeserializer) contextual).resolve(ctxt);
+ }
+ return new PatchableYamlModelDeserializer<>(contextual);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/PatchingParserDelegate.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/PatchingParserDelegate.java
new file mode 100644
index 0000000000..3b6f1951c8
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/PatchingParserDelegate.java
@@ -0,0 +1,58 @@
+package step.automation.packages.yaml.deserialization;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.util.JsonParserDelegate;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import step.automation.packages.yaml.LocatedJsonNode;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+
+public class PatchingParserDelegate extends JsonParserDelegate {
+
+ private final Map distinctLocationBeforeToken = new HashMap<>();
+
+ private JsonLocation lastDistinctLocation;
+
+ private final Deque nodeStack = new ArrayDeque<>();
+
+ public PatchingParserDelegate(JsonParser d) {
+ super(d);
+ }
+
+ @Override
+ public JsonToken nextToken() throws IOException {
+ JsonLocation preLocation = currentLocation();
+ JsonToken token = super.nextToken();
+ if (!preLocation.equals(currentLocation())) {
+ lastDistinctLocation = preLocation;
+ }
+ distinctLocationBeforeToken.put(token, lastDistinctLocation);
+
+ if (token == JsonToken.END_OBJECT && !nodeStack.isEmpty()) {
+ LocatedJsonNode currentNode = nodeStack.pop();
+ currentNode.setEndLocation(currentLocation());
+ }
+
+ return token;
+ }
+
+ protected JsonLocation getDistinctLocationBeforeToken(JsonToken token) {
+ return distinctLocationBeforeToken.get(token);
+ }
+
+ protected JsonLocation getLastDistinctLocation() {
+ return lastDistinctLocation;
+ }
+
+ public ObjectNode setCurrentObjectNode(LocatedJsonNode jsonNode) {
+ jsonNode.setStartLocation(currentLocation());
+ nodeStack.add(jsonNode);
+ return jsonNode;
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/YamlAutomationPackageDescriptorDeserializer.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/YamlAutomationPackageDescriptorDeserializer.java
deleted file mode 100644
index e81e60510d..0000000000
--- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/YamlAutomationPackageDescriptorDeserializer.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020, exense GmbH
- *
- * This file is part of STEP
- *
- * STEP is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * STEP is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with STEP. If not, see .
- ******************************************************************************/
-package step.automation.packages.yaml.deserialization;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import step.automation.packages.yaml.model.AutomationPackageDescriptorYamlImpl;
-import step.core.yaml.deserializers.StepYamlDeserializerAddOn;
-
-@StepYamlDeserializerAddOn(targetClasses = {AutomationPackageDescriptorYamlImpl.class})
-public class YamlAutomationPackageDescriptorDeserializer extends YamlAutomationPackageFragmentDeserializer {
-
- public YamlAutomationPackageDescriptorDeserializer(ObjectMapper yamlObjectMapper) {
- super(yamlObjectMapper);
- }
-
- @Override
- protected Class> getObjectClass() {
- return AutomationPackageDescriptorYamlImpl.class;
- }
-
-}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/YamlAutomationPackageFragmentDeserializer.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/YamlAutomationPackageFragmentDeserializer.java
deleted file mode 100644
index 3e9441978d..0000000000
--- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/YamlAutomationPackageFragmentDeserializer.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020, exense GmbH
- *
- * This file is part of STEP
- *
- * STEP is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * STEP is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with STEP. If not, see .
- ******************************************************************************/
-package step.automation.packages.yaml.deserialization;
-
-import com.fasterxml.jackson.core.JacksonException;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.ObjectCodec;
-import com.fasterxml.jackson.databind.*;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
-import step.automation.packages.deserialization.AutomationPackageSerializationRegistryAware;
-import step.automation.packages.yaml.model.AbstractAutomationPackageFragmentYaml;
-import step.automation.packages.yaml.model.AutomationPackageDescriptorYamlImpl;
-import step.automation.packages.yaml.model.AutomationPackageFragmentYamlImpl;
-import step.core.yaml.deserializers.StepYamlDeserializer;
-import step.core.yaml.deserializers.StepYamlDeserializerAddOn;
-import step.core.yaml.SerializationUtils;
-
-import java.io.IOException;
-import java.util.*;
-
-@StepYamlDeserializerAddOn(targetClasses = {AutomationPackageFragmentYamlImpl.class})
-public class YamlAutomationPackageFragmentDeserializer extends StepYamlDeserializer
- implements AutomationPackageSerializationRegistryAware {
-
- protected AutomationPackageSerializationRegistry registry;
-
- public YamlAutomationPackageFragmentDeserializer(ObjectMapper yamlObjectMapper) {
- super(yamlObjectMapper);
- }
-
- @Override
- public AbstractAutomationPackageFragmentYaml deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
- JsonDeserializer defaultDeserializerForClass = getDefaultDeserializerForClass(p, ctxt, getObjectClass());
- ObjectCodec oc = p.getCodec();
- JsonNode node = oc.readTree(p);
-
- ObjectNode nonBasicFields = node.deepCopy();
- Class> clazz = getObjectClass();
- List basicFields = SerializationUtils.getJsonFieldNames(yamlObjectMapper, clazz);
- nonBasicFields.remove(basicFields);
-
- try (JsonParser treeParser = oc.treeAsTokens(node)) {
- ctxt.getConfig().initialize(treeParser);
-
- if (treeParser.getCurrentToken() == null) {
- treeParser.nextToken();
- }
- AbstractAutomationPackageFragmentYaml res = (AbstractAutomationPackageFragmentYaml) defaultDeserializerForClass.deserialize(treeParser, ctxt);
-
- if (registry != null) {
- Map> nonBasicFieldsMap = new HashMap<>();
- Iterator> fields = nonBasicFields.fields();
- while (fields.hasNext()) {
- Map.Entry next = fields.next();
- List list = new ArrayList<>();
- if (next.getValue() != null) {
- // acquire reader for the right type
- Class> targetClass = registry.resolveClassForYamlField(next.getKey());
- if (targetClass != null) {
- list = yamlObjectMapper.readerForListOf(targetClass).readValue(next.getValue());
- }
- }
- nonBasicFieldsMap.put(next.getKey(), list);
- }
- res.setAdditionalFields(nonBasicFieldsMap);
- }
- return res;
- }
-
- }
-
- protected Class> getObjectClass() {
- return AutomationPackageFragmentYamlImpl.class;
- }
-
- @Override
- public void setSerializationRegistry(AutomationPackageSerializationRegistry registry) {
- this.registry = registry;
- }
-}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AbstractAutomationPackageFragmentYaml.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AbstractAutomationPackageFragmentYaml.java
index 022ce163b5..97b06e71be 100644
--- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AbstractAutomationPackageFragmentYaml.java
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AbstractAutomationPackageFragmentYaml.java
@@ -18,25 +18,45 @@
******************************************************************************/
package step.automation.packages.yaml.model;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
import step.automation.packages.model.YamlAutomationPackageKeyword;
+import step.core.yaml.PatchableYamlList;
import step.plans.automation.YamlPlainTextPlan;
import step.plans.parser.yaml.YamlPlan;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.io.IOException;
+import java.net.URL;
+import java.util.*;
public abstract class AbstractAutomationPackageFragmentYaml implements AutomationPackageFragmentYaml {
+ private final ObjectMapper mapper;
+ private final AutomationPackageSerializationRegistry serializationRegistry;
private List fragments = new ArrayList<>();
private List keywords = new ArrayList<>();
- private List plans = new ArrayList<>();
+ private PatchableYamlList plans = new PatchableYamlList<>();
private List plansPlainText = new ArrayList<>();
+ private final Map> additionalFields = new HashMap<>();
+
+ @JsonCreator
+ public AbstractAutomationPackageFragmentYaml(@JacksonInject(useInput = OptBoolean.FALSE) ObjectMapper mapper, @JacksonInject(useInput = OptBoolean.FALSE) AutomationPackageSerializationRegistry serializationRegistry) {
+ this.mapper = mapper;
+ this.serializationRegistry = serializationRegistry;
+ }
+
+ public AbstractAutomationPackageFragmentYaml() {
+ this.mapper = null;
+ this.serializationRegistry = null;
+ }
+
+ @JsonIgnore
+ private URL url;
+
@JsonIgnore
- private Map> additionalFields;
+ private String currentYaml;
@Override
public List getKeywords() {
@@ -44,17 +64,17 @@ public List getKeywords() {
}
@JsonSetter(nulls = Nulls.AS_EMPTY)
- public void setKeywords(List keywords) {
+ public void setKeywords(PatchableYamlList keywords) {
this.keywords = keywords;
}
@Override
- public List getPlans() {
+ public PatchableYamlList getPlans() {
return plans;
}
@JsonSetter(nulls = Nulls.AS_EMPTY)
- public void setPlans(List plans) {
+ public void setPlans(PatchableYamlList plans) {
this.plans = plans;
}
@@ -68,13 +88,22 @@ public void setFragments(List fragments) {
this.fragments = fragments;
}
- @Override
+ @JsonAnyGetter
public Map> getAdditionalFields() {
return additionalFields;
}
- public void setAdditionalFields(Map> additionalFields) {
- this.additionalFields = additionalFields;
+ @JsonAnySetter
+ @Override
+ public void setAdditionalFields(String key, JsonNode node) throws IOException {
+ if (mapper == null || serializationRegistry == null) return;
+
+ // acquire reader for the right type
+ Class> targetClass = serializationRegistry.resolveClassForYamlField(key);
+ if (targetClass == null) return;
+
+ List> list = mapper.readerForListOf(targetClass).readValue(node);
+ additionalFields.put(key, list);
}
@Override
@@ -86,4 +115,24 @@ public List getPlansPlainText() {
public void setPlansPlainText(List plansPlainText) {
this.plansPlainText = plansPlainText;
}
+
+ @JsonIgnore
+ public void setFragmentUrl(URL url) {
+ this.url = url;
+ }
+
+ @JsonIgnore
+ public URL getFragmentUrl() {
+ return url;
+ }
+
+ @JsonIgnore
+ public void setCurrentYaml(String yaml) {
+ this.currentYaml = yaml;
+ }
+
+ @JsonIgnore
+ public String getCurrentYaml() {
+ return currentYaml;
+ }
}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageDescriptorYamlImpl.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageDescriptorYamlImpl.java
index 2dbaf31f1c..a39b1f51db 100644
--- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageDescriptorYamlImpl.java
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageDescriptorYamlImpl.java
@@ -18,6 +18,12 @@
******************************************************************************/
package step.automation.packages.yaml.model;
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.OptBoolean;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
+
import java.util.HashMap;
import java.util.Map;
@@ -29,6 +35,15 @@ public class AutomationPackageDescriptorYamlImpl extends AbstractAutomationPacka
private String name;
+ @JsonCreator
+ public AutomationPackageDescriptorYamlImpl(@JacksonInject(useInput = OptBoolean.FALSE) ObjectMapper mapper, @JacksonInject(useInput = OptBoolean.FALSE) AutomationPackageSerializationRegistry serializationRegistry) {
+ super(mapper, serializationRegistry);
+ }
+
+ public AutomationPackageDescriptorYamlImpl() {
+ super();
+ }
+
@Override
public String getName() {
return name;
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYaml.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYaml.java
index a141f5e090..f63c798441 100644
--- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYaml.java
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYaml.java
@@ -18,10 +18,16 @@
******************************************************************************/
package step.automation.packages.yaml.model;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.JsonNode;
import step.automation.packages.model.YamlAutomationPackageKeyword;
+import step.core.yaml.PatchableYamlList;
import step.plans.automation.YamlPlainTextPlan;
import step.plans.parser.yaml.YamlPlan;
+import javax.json.Json;
+import java.io.IOException;
+import java.net.URL;
import java.util.List;
import java.util.Map;
@@ -29,7 +35,7 @@ public interface AutomationPackageFragmentYaml {
List getKeywords();
- List getPlans();
+ PatchableYamlList getPlans();
List getPlansPlainText();
@@ -40,4 +46,14 @@ public interface AutomationPackageFragmentYaml {
default List getAdditionalField(String k) {
return (List) getAdditionalFields().get(k);
}
+
+ void setAdditionalFields(String key, JsonNode value) throws IOException;
+
+ URL getFragmentUrl();
+
+ void setFragmentUrl(URL url);
+
+ String getCurrentYaml();
+
+ void setCurrentYaml(String yaml);
}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYamlImpl.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYamlImpl.java
index f0fdcc15e0..3ec0ac228f 100644
--- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYamlImpl.java
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYamlImpl.java
@@ -18,6 +18,18 @@
******************************************************************************/
package step.automation.packages.yaml.model;
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.OptBoolean;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
+
public class AutomationPackageFragmentYamlImpl extends AbstractAutomationPackageFragmentYaml {
+ @JsonCreator
+ public AutomationPackageFragmentYamlImpl(@JacksonInject(useInput = OptBoolean.FALSE) ObjectMapper mapper, @JacksonInject(useInput = OptBoolean.FALSE) AutomationPackageSerializationRegistry serializationRegistry) {
+ super(mapper, serializationRegistry);
+ }
+
+ public AutomationPackageFragmentYamlImpl() { super();}
}
diff --git a/step-core-model/src/main/java/step/core/yaml/AbstractYamlModel.java b/step-core-model/src/main/java/step/core/yaml/AbstractYamlModel.java
index 30eb698863..1da8fa469a 100644
--- a/step-core-model/src/main/java/step/core/yaml/AbstractYamlModel.java
+++ b/step-core-model/src/main/java/step/core/yaml/AbstractYamlModel.java
@@ -19,6 +19,7 @@
package step.core.yaml;
import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.core.JsonLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import step.core.ReflectionUtils;
@@ -30,6 +31,7 @@
public class AbstractYamlModel {
private static final Logger log = LoggerFactory.getLogger(AbstractYamlModel.class);
+
protected void copyFieldsToObject(Object to, boolean ignoreNulls) {
List allFieldsYaml = getAutoCopyFields();
List allFieldsTo = ReflectionUtils.getAllFieldsInHierarchy(to.getClass(), null);
diff --git a/step-core-model/src/main/java/step/core/yaml/PatchableAbstractYamlModel.java b/step-core-model/src/main/java/step/core/yaml/PatchableAbstractYamlModel.java
new file mode 100644
index 0000000000..473ab1f3b6
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/PatchableAbstractYamlModel.java
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * Copyright (C) 2020, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.yaml;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.core.JsonLocation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import step.core.ReflectionUtils;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class PatchableAbstractYamlModel extends AbstractYamlModel implements PatchableYamlModel {
+
+ @JsonIgnore
+ private int startOffset = -1;
+
+ @JsonIgnore
+ private int startFieldOffset = -1;
+
+ @JsonIgnore
+ private int startColumn = -1;
+
+ @JsonIgnore
+ private int endOffset = -1;
+
+ @JsonIgnore
+ public void setPatchingBounds(JsonLocation startLocation, int startFieldLocation, JsonLocation endLocation) {
+ startFieldOffset = startFieldLocation;
+ startOffset = (int) startLocation.getCharOffset();
+ endOffset = (int) endLocation.getCharOffset();
+ startColumn = startLocation.getColumnNr() -1;
+ }
+
+ @JsonIgnore
+ public int getStartOffset(){
+ return startOffset;
+ }
+
+ @JsonIgnore
+ public int getIndent() {
+ return startColumn;
+ }
+
+ @JsonIgnore
+ public int getEndOffset() {
+ return endOffset;
+ }
+
+ @JsonIgnore
+ public void setPatchingBounds(PatchableYamlModel newBoundedArtefact) {
+ startFieldOffset = newBoundedArtefact.getStartFieldOffset();
+ startOffset = newBoundedArtefact.getStartOffset();
+ startColumn = newBoundedArtefact.getIndent();
+ endOffset = newBoundedArtefact.getEndOffset();
+ }
+
+ @Override
+ public int getStartFieldOffset() {
+ return startFieldOffset;
+ }
+
+}
diff --git a/step-core-model/src/main/java/step/core/yaml/PatchableYamlList.java b/step-core-model/src/main/java/step/core/yaml/PatchableYamlList.java
new file mode 100644
index 0000000000..99cbb3356b
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/PatchableYamlList.java
@@ -0,0 +1,83 @@
+/*******************************************************************************
+ * Copyright (C) 2020, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.yaml;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.core.JsonLocation;
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class PatchableYamlList extends ArrayList implements PatchableYamlModel {
+
+ @JsonIgnore
+ private int startOffset = -1;
+
+ @JsonIgnore
+ private int startColumn = -1;
+
+ @JsonIgnore
+ private int endOffset = -1;
+
+ @JsonIgnore
+ private int startFieldOffset;
+
+ public PatchableYamlList() {
+ super();
+ }
+
+ public PatchableYamlList(Collection delegate) {
+ super(delegate);
+ }
+
+ @JsonIgnore
+ public void setPatchingBounds(JsonLocation startLocation, int startFieldLocation, JsonLocation endLocation) {
+ startFieldOffset = startFieldLocation;
+ startOffset = (int) startLocation.getCharOffset();
+ endOffset = (int) endLocation.getCharOffset();
+ startColumn = startLocation.getColumnNr() -1;
+ }
+
+ @JsonIgnore
+ public int getStartOffset(){
+ return startOffset;
+ }
+
+ @JsonIgnore
+ public int getIndent() {
+ return startColumn;
+ }
+
+ @JsonIgnore
+ public int getEndOffset() {
+ return endOffset;
+ }
+
+ @JsonIgnore
+ public void setPatchingBounds(PatchableYamlModel newBoundedArtefact) {
+ startFieldOffset = newBoundedArtefact.getStartFieldOffset();
+ startOffset = newBoundedArtefact.getStartOffset();
+ startColumn = newBoundedArtefact.getIndent();
+ endOffset = newBoundedArtefact.getEndOffset();
+ }
+
+ @Override
+ public int getStartFieldOffset() {
+ return startFieldOffset;
+ }
+}
diff --git a/step-core-model/src/main/java/step/core/yaml/PatchableYamlModel.java b/step-core-model/src/main/java/step/core/yaml/PatchableYamlModel.java
new file mode 100644
index 0000000000..ee74a3e9cc
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/PatchableYamlModel.java
@@ -0,0 +1,18 @@
+package step.core.yaml;
+
+import com.fasterxml.jackson.core.JsonLocation;
+
+public interface PatchableYamlModel {
+
+ void setPatchingBounds(JsonLocation startLocation, int startFieldLocation, JsonLocation endLocation);
+
+ int getStartOffset();
+
+ int getIndent();
+
+ int getEndOffset();
+
+ void setPatchingBounds(PatchableYamlModel newBoundedArtefact);
+
+ int getStartFieldOffset();
+}
diff --git a/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java b/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java
index c21da6031a..07268c8bfc 100644
--- a/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java
+++ b/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java
@@ -61,6 +61,8 @@ public String getAutomationPackageName() {
abstract public boolean hasAutomationPackageDescriptor();
+ abstract public URL getDescriptorYamlUrl();
+
abstract public InputStream getDescriptorYaml();
abstract public InputStream getResourceAsStream(String resourcePath) throws IOException;
diff --git a/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java b/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java
index 41eb5f0091..10d97d6e2d 100644
--- a/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java
+++ b/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java
@@ -122,6 +122,17 @@ public boolean hasAutomationPackageDescriptor() {
return false;
}
+ @Override
+ public URL getDescriptorYamlUrl() {
+ for (String metadataFile : METADATA_FILES) {
+ URL yamlDescriptor = classLoaderForMainApFile.getResource(metadataFile);
+ if (yamlDescriptor != null) {
+ return yamlDescriptor;
+ }
+ }
+ return null;
+ }
+
@Override
public InputStream getDescriptorYaml() {
for (String metadataFile : METADATA_FILES) {
diff --git a/step-core/src/main/java/step/automation/packages/deserialization/AutomationPackageSerializationRegistryAware.java b/step-core/src/main/java/step/automation/packages/deserialization/AutomationPackageSerializationRegistryAware.java
deleted file mode 100644
index bdcbbe372d..0000000000
--- a/step-core/src/main/java/step/automation/packages/deserialization/AutomationPackageSerializationRegistryAware.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020, exense GmbH
- *
- * This file is part of STEP
- *
- * STEP is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * STEP is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with STEP. If not, see .
- ******************************************************************************/
-package step.automation.packages.deserialization;
-
-public interface AutomationPackageSerializationRegistryAware {
-
- void setSerializationRegistry(AutomationPackageSerializationRegistry registry);
-}
diff --git a/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializersScanner.java b/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializersScanner.java
index dd26a10a3a..8051aba5eb 100644
--- a/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializersScanner.java
+++ b/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializersScanner.java
@@ -36,7 +36,7 @@ public class StepYamlDeserializersScanner {
/**
* Scans and returns all {@link StepYamlDeserializer} classes annotated with {@link StepYamlDeserializerAddOn}
*/
- public static List> scanDeserializerAddons(ObjectMapper yamlObjectMapper, List>> configurators) {
+ public static List> scanDeserializerAddons(ObjectMapper yamlObjectMapper) {
List> result = new ArrayList<>();
List> annotatedClasses = new ArrayList<>(CachedAnnotationScanner.getClassesWithAnnotation(StepYamlDeserializerAddOn.LOCATION, StepYamlDeserializerAddOn.class, Thread.currentThread().getContextClassLoader()));
for (Class> annotatedClass : annotatedClasses) {
@@ -45,11 +45,6 @@ public static List> scanDeserializerAddons(ObjectMapper yaml
Arrays.stream(annotation.targetClasses()).forEach(aClass -> {
try {
StepYamlDeserializer newDeserializer = (StepYamlDeserializer) annotatedClass.getConstructor(ObjectMapper.class).newInstance(yamlObjectMapper);
- if (configurators != null) {
- for (Consumer> configurator : configurators) {
- configurator.accept(newDeserializer);
- }
- }
result.add(new DeserializerBind<>((Class) aClass, newDeserializer));
} catch (Exception e) {
throw new RuntimeException("Cannot prepare deserializer", e);
@@ -61,20 +56,9 @@ public static List> scanDeserializerAddons(ObjectMapper yaml
return result;
}
- /**
- * Scans and returns all {@link StepYamlDeserializer} classes annotated with {@link StepYamlDeserializerAddOn}
- */
- public static List> scanDeserializerAddons(ObjectMapper yamlObjectMapper) {
- return scanDeserializerAddons(yamlObjectMapper, null);
- }
-
public static SimpleModule addAllDeserializerAddonsToModule(SimpleModule module, ObjectMapper yamlObjectMapper) {
- return addAllDeserializerAddonsToModule(module, yamlObjectMapper, null);
- }
-
- public static SimpleModule addAllDeserializerAddonsToModule(SimpleModule module, ObjectMapper yamlObjectMapper, List>> configurators) {
SimpleModule res = module;
- for (StepYamlDeserializersScanner.DeserializerBind> deser : StepYamlDeserializersScanner.scanDeserializerAddons(yamlObjectMapper, configurators)) {
+ for (StepYamlDeserializersScanner.DeserializerBind> deser : StepYamlDeserializersScanner.scanDeserializerAddons(yamlObjectMapper)) {
res = module.addDeserializer((Class) deser.clazz, deser.deserializer);
}
return res;
diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/plans/parser/yaml/YamlPlan.java b/step-plans/step-plans-base-artefacts/src/main/java/step/plans/parser/yaml/YamlPlan.java
index 782c1ade15..1dfe98892f 100644
--- a/step-plans/step-plans-base-artefacts/src/main/java/step/plans/parser/yaml/YamlPlan.java
+++ b/step-plans/step-plans-base-artefacts/src/main/java/step/plans/parser/yaml/YamlPlan.java
@@ -18,6 +18,7 @@
******************************************************************************/
package step.plans.parser.yaml;
+import step.core.yaml.PatchableAbstractYamlModel;
import step.core.yaml.model.NamedYamlArtefact;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@@ -27,7 +28,7 @@
import java.util.List;
-public class YamlPlan {
+public class YamlPlan extends PatchableAbstractYamlModel {
public static final String PLANS_ENTITY_NAME = "plans";
@@ -81,7 +82,7 @@ public List getCategories() {
return categories;
}
- public void setCategories(List categories) {
- this.categories = categories;
- }
+ public void setCategories(List categories) {
+ this.categories = categories;
+ }
}
diff --git a/step-plans/step-plans-parser/src/main/java/step/plans/automation/YamlPlainTextPlan.java b/step-plans/step-plans-parser/src/main/java/step/plans/automation/YamlPlainTextPlan.java
index 2b7157c612..6dde45e740 100644
--- a/step-plans/step-plans-parser/src/main/java/step/plans/automation/YamlPlainTextPlan.java
+++ b/step-plans/step-plans-parser/src/main/java/step/plans/automation/YamlPlainTextPlan.java
@@ -18,11 +18,14 @@
******************************************************************************/
package step.plans.automation;
+import step.core.yaml.AbstractYamlModel;
+import step.core.yaml.PatchableAbstractYamlModel;
import step.plans.nl.RootArtefactType;
+import step.core.yaml.PatchableYamlModel;
import java.util.List;
-public class YamlPlainTextPlan {
+public class YamlPlainTextPlan extends PatchableAbstractYamlModel {
private String name;
diff --git a/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/YamlPlanReader.java b/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/YamlPlanReader.java
index 6886a23a58..c0629a811f 100644
--- a/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/YamlPlanReader.java
+++ b/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/YamlPlanReader.java
@@ -271,7 +271,7 @@ public Plan yamlPlanToPlan(YamlPlan yamlPlan) {
return plan;
}
- protected YamlPlan planToYamlPlan(Plan plan) {
+ public YamlPlan planToYamlPlan(Plan plan) {
YamlPlan yamlPlan = new YamlPlan();
yamlPlan.setName(plan.getAttribute(AbstractOrganizableObject.NAME));
yamlPlan.setVersion(currentVersion.toString());