diff --git a/step-automation-packages/pom.xml b/step-automation-packages/pom.xml
index a5210c3099..3b9e7fbb26 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-collections
+
diff --git a/step-automation-packages/step-automation-packages-collections/pom.xml b/step-automation-packages/step-automation-packages-collections/pom.xml
new file mode 100644
index 0000000000..14f7950774
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+
+ ch.exense.step
+ step-automation-packages
+ 0.0.0-SNAPSHOT
+
+
+ step-automation-packages-collections
+
+
+
+ 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-collections/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java
new file mode 100644
index 0000000000..ddffbe846f
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java
@@ -0,0 +1,70 @@
+/*******************************************************************************
+ * 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.InMemoryCollectionFactory;
+import step.core.plans.Plan;
+import step.functions.Function;
+import step.parameter.Parameter;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class AutomationPackageCollectionFactory implements CollectionFactory {
+
+ private final InMemoryCollectionFactory baseFactory;
+ private final AutomationPackageYamlFragmentManager fragmentManager;
+ private final Map> collectionsByName = new ConcurrentHashMap<>();
+
+ public AutomationPackageCollectionFactory(Properties properties, AutomationPackageYamlFragmentManager fragmentManager) {
+ this.fragmentManager = fragmentManager;
+ this.baseFactory = new InMemoryCollectionFactory(properties);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Collection getCollection(String name, Class entityClass) {
+ return (Collection) collectionsByName.computeIfAbsent(name, (_name) -> {
+ if (Plan.class.isAssignableFrom(entityClass)) {
+ return new AutomationPackagePlanCollection(fragmentManager);
+ } else if (Parameter.class.isAssignableFrom(entityClass)) {
+ return new AutomationPackageParameterCollection(fragmentManager);
+ } else if (Function.class.isAssignableFrom(entityClass)) {
+ return new AutomationPackageFunctionCollection(fragmentManager);
+ }
+ return baseFactory.getCollection(name, entityClass);
+ });
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Collection getVersionedCollection(String name) {
+ // TODO: I'm pretty sure the previous implementation was incorrect.
+ // Fix this once we need it and know what the correct implementation is.
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close() throws IOException {
+ baseFactory.close();
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageFunctionCollection.java b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageFunctionCollection.java
new file mode 100644
index 0000000000..7b9119596a
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageFunctionCollection.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * 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.model.YamlAutomationPackageKeyword;
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.core.collections.inmemory.InMemoryCollection;
+import step.functions.Function;
+
+public class AutomationPackageFunctionCollection extends InMemoryCollection implements Collection {
+
+
+ private final AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackageFunctionCollection(AutomationPackageYamlFragmentManager fragmentManager) {
+ super(true, YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME);
+ this.fragmentManager = fragmentManager;
+ initialzeRecordsFromFragments(fragmentManager);
+ }
+
+ private void initialzeRecordsFromFragments(AutomationPackageYamlFragmentManager fragmentManager) {
+ // initialization into the collection memory. Calls super save to avoid calling fragmentManager.savePlan
+ fragmentManager.getBusinessObjects(Function.class).forEach(super::save);
+ }
+
+ @Override
+ public Function save(Function p){
+ return super.save(fragmentManager.saveFunction(p));
+ }
+
+ @Override
+ public void save(Iterable iterable) {
+ for (Function p : iterable) {
+ save(p);
+ }
+ }
+
+ @Override
+ public void remove(Filter filter) {
+ find(filter, null, null, null, 0).forEach(fragmentManager::removeFunction);
+ super.remove(filter);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageParameterCollection.java b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageParameterCollection.java
new file mode 100644
index 0000000000..a7780a210e
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageParameterCollection.java
@@ -0,0 +1,61 @@
+/*******************************************************************************
+ * 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.parameter.Parameter;
+import step.parameter.automation.AutomationPackageParameter;
+
+public class AutomationPackageParameterCollection extends InMemoryCollection implements Collection {
+
+
+ private final AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackageParameterCollection(AutomationPackageYamlFragmentManager fragmentManager) {
+ super(true, Parameter.ENTITY_NAME);
+ this.fragmentManager = fragmentManager;
+ initialzeRecordsFromFragments(fragmentManager);
+ }
+
+ private void initialzeRecordsFromFragments(AutomationPackageYamlFragmentManager fragmentManager) {
+ // initialization into the collection memory. Calls super save to avoid calling fragmentManager.savePlan
+ fragmentManager.getBusinessObjects(Parameter.class).forEach(super::save);
+ }
+
+ @Override
+ public Parameter save(Parameter parameter){
+ return super.save(fragmentManager.saveAdditionalFieldObject(parameter, context -> AutomationPackageParameter.forContext(context, parameter), Parameter.ENTITY_NAME));
+ }
+
+ @Override
+ public void save(Iterable iterable) {
+ for (Parameter p : iterable) {
+ save(p);
+ }
+ }
+
+ @Override
+ public void remove(Filter filter) {
+ find(filter, null, null, null, 0).forEach(parameter ->
+ fragmentManager.removeAdditionalFieldObject(parameter, Parameter.ENTITY_NAME)
+ );
+ super.remove(filter);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackagePlanCollection.java b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackagePlanCollection.java
new file mode 100644
index 0000000000..0ba94ff0eb
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackagePlanCollection.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * 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;
+import step.plans.parser.yaml.YamlPlan;
+
+public class AutomationPackagePlanCollection extends InMemoryCollection implements Collection {
+
+
+ private final AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackagePlanCollection(AutomationPackageYamlFragmentManager fragmentManager) {
+ super(true, YamlPlan.PLANS_ENTITY_NAME);
+ this.fragmentManager = fragmentManager;
+ initialzeRecordsFromFragments(fragmentManager);
+ }
+
+ private void initialzeRecordsFromFragments(AutomationPackageYamlFragmentManager fragmentManager) {
+ // initialization into the collection memory. Calls super save to avoid calling fragmentManager.savePlan
+ fragmentManager.getBusinessObjects(Plan.class).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, 0).forEach(fragmentManager::removePlan);
+ super.remove(filter);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionFactoryTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionFactoryTest.java
new file mode 100644
index 0000000000..e9fbe009c5
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionFactoryTest.java
@@ -0,0 +1,20 @@
+package step.core.collections;
+
+import org.junit.Assert;
+import org.junit.Test;
+import step.core.plans.Plan;
+import step.plans.parser.yaml.YamlPlan;
+
+import java.util.Properties;
+
+public class AutomationPackageCollectionFactoryTest extends AutomationPackageCollectionTestBase {
+
+ @Test
+ public void testCollectionIdempotence() {
+ AutomationPackageCollectionFactory cf = new AutomationPackageCollectionFactory(new Properties(), fragmentManager);
+ Collection c1 = cf.getCollection(YamlPlan.PLANS_ENTITY_NAME, Plan.class);
+ Assert.assertTrue(c1.estimatedCount() > 0); // just for good measure
+ Collection c2 = cf.getCollection(YamlPlan.PLANS_ENTITY_NAME, Plan.class);
+ Assert.assertSame(c1, c2);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionTestBase.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionTestBase.java
new file mode 100644
index 0000000000..3ed25186ed
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionTestBase.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * 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.mockito.Mockito;
+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.AutomationPackageYamlFragmentManager;
+import step.automation.packages.yaml.YamlAutomationPackageVersions;
+import step.parameter.ParameterManager;
+import step.parameter.automation.AutomationPackageParametersRegistration;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+
+public class AutomationPackageCollectionTestBase {
+
+ private final JavaAutomationPackageReader reader;
+
+ // To use a different source directory, override in subclass constructor
+ protected File sourceDirectory = new File("src/test/resources/testdata/ap1");
+ protected File destinationDirectory;
+ protected Path expectedFilesPath = new File("src/test/resources/expected").toPath();
+ protected AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackageCollectionTestBase() {
+ 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 {
+ destinationDirectory = Files.createTempDirectory("automationPackageCollectionTest").toFile();
+ FileUtils.copyDirectory(sourceDirectory, destinationDirectory);
+
+ fragmentManager = reader.getAutomationPackageYamlFragmentManager(destinationDirectory);
+ }
+
+ @After
+ public void tearDown() throws IOException, AutomationPackageReadingException {
+ // Attempt to re-read the just written Automation package from scratch
+ reader.getAutomationPackageYamlFragmentManager(destinationDirectory);
+ FileUtils.deleteDirectory(destinationDirectory);
+ }
+
+
+ protected void assertFilesEqual(Path expected, Path actual) throws IOException {
+ String expectedLines = Files.readString(expected);
+ String actualLines = Files.readString(actual);
+
+ assertEquals(expectedLines, actualLines);
+ }
+
+ protected void setPropertiesWriteToFragment(String entityName, String fragment) {
+ Properties properties = new Properties();
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, entityName), fragment);
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, entityName), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name());
+
+ fragmentManager.setProperties(properties);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageKeywordCollectionTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageKeywordCollectionTest.java
new file mode 100644
index 0000000000..5a52ed56b5
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageKeywordCollectionTest.java
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * 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 org.junit.Before;
+import org.junit.Test;
+import step.artefacts.Echo;
+import step.automation.packages.AutomationPackageReadingException;
+import step.automation.packages.model.YamlAutomationPackageKeyword;
+import step.core.accessors.AbstractOrganizableObject;
+import step.core.dynamicbeans.DynamicValue;
+import step.functions.Function;
+import step.plugins.functions.types.CompositeFunction;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class AutomationPackageKeywordCollectionTest extends AutomationPackageCollectionTestBase {
+
+ private Collection functionCollection;
+
+ public AutomationPackageKeywordCollectionTest() {
+ super();
+ }
+
+ @Before
+ public void setUp() throws IOException, AutomationPackageReadingException {
+ super.setUp();
+ AutomationPackageCollectionFactory collectionFactory = new AutomationPackageCollectionFactory(new Properties(), fragmentManager);
+ functionCollection = collectionFactory.getCollection(YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME, Function.class);
+ }
+
+ @Test
+ public void testLoadAllKeywords() throws IOException {
+ List functions = functionCollection.find(Filters.empty(), null, null, null, 100).collect(Collectors.toList());
+
+ assertEquals(4, functions.size());
+ Set functionNames = functions.stream().map(f -> f.getAttribute(AbstractOrganizableObject.NAME)).collect(Collectors.toSet());
+
+ assertTrue(functionNames.contains("NodeAutomation"));
+ assertTrue(functionNames.contains("JMeter keyword from automation package"));
+ assertTrue(functionNames.contains("Composite keyword from AP"));
+ assertTrue(functionNames.contains("GeneralScript keyword from AP"));
+ }
+
+ @Test
+ public void testModifyCompositeKeyword() throws IOException {
+ Optional optionalFunction = functionCollection.find(Filters.equals("attributes.name", "Composite keyword from AP"), null, null, null, 100).findFirst();
+ assertTrue(optionalFunction.isPresent());
+
+ CompositeFunction compositeFunction = (CompositeFunction) optionalFunction.get();
+
+ Echo echo = (Echo) compositeFunction.getPlan().getRoot().getChildren().getFirst();
+ echo.setText(new DynamicValue<>("Modified Echo"));
+
+ setPropertiesWriteToFragment(YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME, "keywords.yml");
+ functionCollection.save(compositeFunction);
+
+ assertFilesEqual(expectedFilesPath.resolve("keywordsAfterCompositeModification.yml"), destinationDirectory.toPath().resolve("keywords.yml"));
+ }
+
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageParameterCollectionTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageParameterCollectionTest.java
new file mode 100644
index 0000000000..8c33637c55
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageParameterCollectionTest.java
@@ -0,0 +1,94 @@
+/*******************************************************************************
+ * 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 org.junit.Before;
+import org.junit.Test;
+import step.automation.packages.AutomationPackageReadingException;
+import step.core.dynamicbeans.DynamicValue;
+import step.core.yaml.deserialization.AutomationPackagePerObjectSaveUnsupportedException;
+import step.parameter.Parameter;
+
+import java.io.IOException;
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class AutomationPackageParameterCollectionTest extends AutomationPackageCollectionTestBase {
+
+ private Collection parameterCollection;
+
+ public AutomationPackageParameterCollectionTest() {
+ super();
+ }
+
+ @Before
+ public void setUp() throws IOException, AutomationPackageReadingException {
+ super.setUp();
+ AutomationPackageCollectionFactory collectionFactory = new AutomationPackageCollectionFactory(new Properties(), fragmentManager);
+ parameterCollection = collectionFactory.getCollection(Parameter.ENTITY_NAME, Parameter.class);
+ }
+
+ @Test
+ public void testParameterModify() throws IOException {
+ Optional optionalParameter = parameterCollection.find(Filters.equals("key", "mySimpleKey"), null, null, null, 100).findFirst();
+
+ assertTrue(optionalParameter.isPresent());
+
+ Parameter parameter = optionalParameter.get();
+
+ parameter.getValue().setValue("myModifiedValue");
+ parameterCollection.save(parameter);
+
+ assertFilesEqual(expectedFilesPath.resolve("parametersAfterModification.yml"), destinationDirectory.toPath().resolve("parameters.yml"));
+ }
+
+
+ @Test
+ public void testParameterAddAndModify() throws IOException {
+
+
+ Parameter parameter = new Parameter(null, "addedParameter", "test", "This is an added Parameter before modification");
+ assertThrows(AutomationPackagePerObjectSaveUnsupportedException.class, () -> parameterCollection.save(parameter));
+
+
+ setPropertiesWriteToFragment(Parameter.ENTITY_NAME, "parameters.yml");
+ parameterCollection.save(parameter);
+
+ assertFilesEqual(expectedFilesPath.resolve("parametersAfterAdd.yml"), destinationDirectory.toPath().resolve("parameters.yml"));
+
+ parameter.setDescription("This is an added Parameter with a new description");
+ parameterCollection.save(parameter);
+
+ parameter.setValue(new DynamicValue<>("foo"));
+ parameterCollection.save(parameter);
+
+ assertFilesEqual(expectedFilesPath.resolve("parametersAfterAddAndModification.yml"), destinationDirectory.toPath().resolve("parameters.yml"));
+ }
+
+ @Test
+ public void testParametersInRootApAreAddedOnlyOnce() throws Exception {
+ // SED-4681
+ assertEquals(1, parameterCollection.find(Filters.equals("key", "paramInMainAP"), null, null, null, 0).count());
+ }
+
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackagePlanCollectionTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackagePlanCollectionTest.java
new file mode 100644
index 0000000000..0abe4b2e61
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackagePlanCollectionTest.java
@@ -0,0 +1,268 @@
+/*******************************************************************************
+ * 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 org.junit.Before;
+import org.junit.Test;
+import step.artefacts.Echo;
+import step.artefacts.Sequence;
+import step.automation.packages.AutomationPackageReadingException;
+import step.core.dynamicbeans.DynamicValue;
+import step.core.plans.Plan;
+import step.core.yaml.deserialization.AutomationPackageConcurrentEditException;
+import step.plans.parser.yaml.YamlPlan;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class AutomationPackagePlanCollectionTest extends AutomationPackageCollectionTestBase {
+
+ private Collection planCollection;
+
+ public AutomationPackagePlanCollectionTest() {
+ super();
+ }
+
+ @Before
+ public void setUp() throws IOException, AutomationPackageReadingException {
+ super.setUp();
+ AutomationPackageCollectionFactory collectionFactory = new AutomationPackageCollectionFactory(new Properties(), fragmentManager);
+ planCollection = collectionFactory.getCollection(YamlPlan.PLANS_ENTITY_NAME, Plan.class);
+ }
+
+ @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.copy(sourceDirectory.toPath().resolve("plans").resolve("plan1.yml"),
+ destinationDirectory.toPath().resolve("plans").resolve("plan1.yml"),
+ StandardCopyOption.REPLACE_EXISTING);
+
+ 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);
+
+
+ setPropertiesWriteToFragment(YamlPlan.PLANS_ENTITY_NAME, "plans/plan1.yml");
+
+ 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);
+
+ setPropertiesWriteToFragment(YamlPlan.PLANS_ENTITY_NAME, "plans/plan1.yml");
+
+ 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);
+
+
+ setPropertiesWriteToFragment(YamlPlan.PLANS_ENTITY_NAME, "plans/plan1.yml");
+
+ 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);
+
+ setPropertiesWriteToFragment(YamlPlan.PLANS_ENTITY_NAME, "automation-package.yml");
+
+ 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);
+
+ planCollection.save(plan);
+
+ assertFilesEqual(expectedFilesPath.resolve("Hello World Plan.yml"), destinationDirectory.toPath().resolve("plans").resolve("Hello World Plan.yml"));
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageWithEmptyWildcardTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageWithEmptyWildcardTest.java
new file mode 100644
index 0000000000..3b87a0fab9
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageWithEmptyWildcardTest.java
@@ -0,0 +1,17 @@
+package step.core.collections;
+
+import org.junit.Test;
+
+import java.io.File;
+
+public class AutomationPackageWithEmptyWildcardTest extends AutomationPackageCollectionTestBase {
+
+ public AutomationPackageWithEmptyWildcardTest() {
+ super.sourceDirectory = new File("src/test/resources/testdata/ap-with-empty-wildcard");
+ }
+
+ @Test
+ public void testLoading() {
+ // we're happy if this package managed to load without throwing an exception
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageWithNonexistentFragmentTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageWithNonexistentFragmentTest.java
new file mode 100644
index 0000000000..c9680d979e
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageWithNonexistentFragmentTest.java
@@ -0,0 +1,31 @@
+package step.core.collections;
+
+import ch.exense.commons.app.Configuration;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+import step.automation.packages.AutomationPackageHookRegistry;
+import step.automation.packages.JavaAutomationPackageReader;
+import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
+import step.automation.packages.yaml.YamlAutomationPackageVersions;
+import step.parameter.ParameterManager;
+import step.parameter.automation.AutomationPackageParametersRegistration;
+
+import java.io.File;
+
+public class AutomationPackageWithNonexistentFragmentTest {
+
+ @Test
+ public void testLoading() throws Exception {
+ AutomationPackageSerializationRegistry serializationRegistry = new AutomationPackageSerializationRegistry();
+ AutomationPackageHookRegistry hookRegistry = new AutomationPackageHookRegistry();
+ AutomationPackageParametersRegistration.registerParametersHooks(hookRegistry, serializationRegistry, Mockito.mock(ParameterManager.class));
+ var reader = new JavaAutomationPackageReader(YamlAutomationPackageVersions.ACTUAL_JSON_SCHEMA_PATH, hookRegistry, serializationRegistry, new Configuration());
+ try {
+ reader.getAutomationPackageYamlFragmentManager(new File("src/test/resources/testdata/ap-with-nonexisting-fragment"));
+ Assert.fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ Assert.assertEquals("Illegal resource definition, resource cannot be found: nonexisting.yml", e.getMessage());
+ }
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageWithNonexistentWildcardTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageWithNonexistentWildcardTest.java
new file mode 100644
index 0000000000..756878ef30
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageWithNonexistentWildcardTest.java
@@ -0,0 +1,33 @@
+package step.core.collections;
+
+import ch.exense.commons.app.Configuration;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+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.YamlAutomationPackageVersions;
+import step.parameter.ParameterManager;
+import step.parameter.automation.AutomationPackageParametersRegistration;
+
+import java.io.File;
+import java.io.IOException;
+
+public class AutomationPackageWithNonexistentWildcardTest {
+
+ @Test
+ public void testLoading() throws Exception {
+ AutomationPackageSerializationRegistry serializationRegistry = new AutomationPackageSerializationRegistry();
+ AutomationPackageHookRegistry hookRegistry = new AutomationPackageHookRegistry();
+ AutomationPackageParametersRegistration.registerParametersHooks(hookRegistry, serializationRegistry, Mockito.mock(ParameterManager.class));
+ var reader = new JavaAutomationPackageReader(YamlAutomationPackageVersions.ACTUAL_JSON_SCHEMA_PATH, hookRegistry, serializationRegistry, new Configuration());
+ try {
+ reader.getAutomationPackageYamlFragmentManager(new File("src/test/resources/testdata/ap-with-nonexisting-wildcard"));
+ Assert.fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ Assert.assertEquals("Illegal resource definition, resource cannot be found: nonexisting/*.yml", e.getMessage());
+ }
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/Hello World Plan.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/Hello World Plan.yml
new file mode 100644
index 0000000000..c1c6fe467e
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/Hello World Plan.yml
@@ -0,0 +1,8 @@
+---
+plans:
+- name: "Hello World Plan"
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/descriptorAfterAdd.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/descriptorAfterAdd.yml
new file mode 100644
index 0000000000..9db0b00a0e
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/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:
+- name: "New Name"
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
+parameters:
+ - key: "paramInMainAP"
+ value: "once"
+fragments:
+ - "keywords.yml"
+ - "plans/*.yml"
+ - "schedules.yml"
+ - "parameters.yml"
+ - "parameters2.yml"
+ - "unknown.yml"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/keywordsAfterCompositeModification.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/keywordsAfterCompositeModification.yml
new file mode 100644
index 0000000000..4fadfecdad
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/keywordsAfterCompositeModification.yml
@@ -0,0 +1,29 @@
+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: "Modified 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"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/parametersAfterAdd.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/parametersAfterAdd.yml
new file mode 100644
index 0000000000..0ab61a81ad
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/parametersAfterAdd.yml
@@ -0,0 +1,17 @@
+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"
+ - key: "addedParameter"
+ value: "test"
+ description: "This is an added Parameter before modification"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/parametersAfterAddAndModification.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/parametersAfterAddAndModification.yml
new file mode 100644
index 0000000000..5d87e49243
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/parametersAfterAddAndModification.yml
@@ -0,0 +1,17 @@
+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"
+ - key: "addedParameter"
+ value: "foo"
+ description: "This is an added Parameter with a new description"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/parametersAfterModification.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/parametersAfterModification.yml
new file mode 100644
index 0000000000..6bb1b72540
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/parametersAfterModification.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: "myModifiedValue"
+ - key: myDynamicParam
+ value:
+ expression: "mySimpleKey"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterAdd.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterAdd.yml
new file mode 100644
index 0000000000..c6cb8b9ebc
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterAdd.yml
@@ -0,0 +1,32 @@
+---
+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"
+- name: "New Name"
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
+plansPlainText:
+- name: "Plain text plan"
+ rootType: "Sequence"
+ categories:
+ - "PlainTextPlan"
+ file: "plans/plan2.plan"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterModification.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterModification.yml
new file mode 100644
index 0000000000..81cc315f24
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterModification.yml
@@ -0,0 +1,28 @@
+---
+fragments: []
+keywords: []
+plans:
+- 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"
+ 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-collections/src/test/resources/expected/plan1AfterModifyAndAdd.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterModifyAndAdd.yml
new file mode 100644
index 0000000000..237084b8b7
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterModifyAndAdd.yml
@@ -0,0 +1,34 @@
+---
+fragments: []
+keywords: []
+plans:
+- 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"
+ categories:
+ - "Yaml Plan"
+- name: "New Name"
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
+plansPlainText:
+- name: "Plain text plan"
+ rootType: "Sequence"
+ categories:
+ - "PlainTextPlan"
+ file: "plans/plan2.plan"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterRemove.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterRemove.yml
new file mode 100644
index 0000000000..f01060a03f
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/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-collections/src/test/resources/expected/plan1AfterRename.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterRename.yml
new file mode 100644
index 0000000000..bc4856bdc6
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterRename.yml
@@ -0,0 +1,27 @@
+---
+fragments: []
+keywords: []
+plans:
+- 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"
+ 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-collections/src/test/resources/testdata/ap-with-empty-wildcard/automation-package.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-empty-wildcard/automation-package.yml
new file mode 100644
index 0000000000..e02870463b
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-empty-wildcard/automation-package.yml
@@ -0,0 +1,5 @@
+schemaVersion: 1.0.0
+name: "My package"
+plans: [ ]
+fragments:
+ - "existingbutempty/*.yml"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-empty-wildcard/existingbutempty/README.txt b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-empty-wildcard/existingbutempty/README.txt
new file mode 100644
index 0000000000..5f778139c2
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-empty-wildcard/existingbutempty/README.txt
@@ -0,0 +1 @@
+This directory exists, but no files matching existingbutempty/*.yml (referenced in the AP) exist.
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-nonexisting-fragment/automation-package.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-nonexisting-fragment/automation-package.yml
new file mode 100644
index 0000000000..c25032a65e
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-nonexisting-fragment/automation-package.yml
@@ -0,0 +1,5 @@
+schemaVersion: 1.0.0
+name: "My package"
+plans: [ ]
+fragments:
+ - "nonexisting.yml"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-nonexisting-wildcard/automation-package.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-nonexisting-wildcard/automation-package.yml
new file mode 100644
index 0000000000..b0cd686950
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap-with-nonexisting-wildcard/automation-package.yml
@@ -0,0 +1,5 @@
+schemaVersion: 1.0.0
+name: "My package"
+plans: [ ]
+fragments:
+ - "nonexisting/*.yml"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/.apignore b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/.apignore
new file mode 100644
index 0000000000..319325c32d
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/.apignore
@@ -0,0 +1,2 @@
+/ignored
+/ignoredFile.yml
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/automation-package.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/automation-package.yml
new file mode 100644
index 0000000000..3238de01e0
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/automation-package.yml
@@ -0,0 +1,25 @@
+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: [ ]
+parameters:
+ - key: "paramInMainAP"
+ value: "once"
+fragments:
+ - "keywords.yml"
+ - "plans/*.yml"
+ - "schedules.yml"
+ - "parameters.yml"
+ - "parameters2.yml"
+ - "unknown.yml"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/ignoredFile.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/ignoredFile.yml
new file mode 100644
index 0000000000..06f858207b
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/ignoredFile.yml
@@ -0,0 +1 @@
+#I should be ignored
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/jmeterProject1/jmeterProject1.xml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/jmeterProject1/jmeterProject1.xml
new file mode 100644
index 0000000000..2d1d641e24
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/jmeterProject1/jmeterProject1.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/jsProject/jsSample.js b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/jsProject/jsSample.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/keywords.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/keywords.yml
new file mode 100644
index 0000000000..d6606acdb4
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/keywords.yml
@@ -0,0 +1,29 @@
+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"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/lib/fakeLib.jar b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/lib/fakeLib.jar
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/nodeProject/nodeSample.ts b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/nodeProject/nodeSample.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/parameters.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/parameters.yml
new file mode 100644
index 0000000000..1909ed829c
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/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"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/parameters2.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/parameters2.yml
new file mode 100644
index 0000000000..8754e85903
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/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-collections/src/test/resources/testdata/ap1/plan.plan b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/plan.plan
new file mode 100644
index 0000000000..66b7ca6d84
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/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-collections/src/test/resources/testdata/ap1/plans/plan1.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/plans/plan1.yml
new file mode 100644
index 0000000000..26e6c014f5
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/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-collections/src/test/resources/testdata/ap1/plans/plan2.plan b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/plans/plan2.plan
new file mode 100644
index 0000000000..66b7ca6d84
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/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-collections/src/test/resources/testdata/ap1/plans/plan2.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/plans/plan2.yml
new file mode 100644
index 0000000000..2a576d02da
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/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-collections/src/test/resources/testdata/ap1/plansPlainText/firstPlainText.plan b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/plansPlainText/firstPlainText.plan
new file mode 100644
index 0000000000..e9dd736886
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/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-collections/src/test/resources/testdata/ap1/plansPlainText/secondPlainText.plan b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/plansPlainText/secondPlainText.plan
new file mode 100644
index 0000000000..00c3bacb0d
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/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-collections/src/test/resources/testdata/ap1/schedules.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/schedules.yml
new file mode 100644
index 0000000000..6d95ed7161
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/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-collections/src/test/resources/testdata/ap1/unknown.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/unknown.yml
new file mode 100644
index 0000000000..d378e6078d
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/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-controller/src/test/java/step/automation/packages/AutomationPackageReaderTest.java b/step-automation-packages/step-automation-packages-controller/src/test/java/step/automation/packages/AutomationPackageReaderTest.java
index e368b45c67..ae9d4acd8b 100644
--- a/step-automation-packages/step-automation-packages-controller/src/test/java/step/automation/packages/AutomationPackageReaderTest.java
+++ b/step-automation-packages/step-automation-packages-controller/src/test/java/step/automation/packages/AutomationPackageReaderTest.java
@@ -71,7 +71,7 @@ public AutomationPackageReaderTest() {
public void testReadFromPackage() throws AutomationPackageReadingException {
File automationPackageJar = new File("src/test/resources/samples/step-automation-packages-sample1.jar");
- AutomationPackageContent automationPackageContent = reader.readAutomationPackageFromJarFile(automationPackageJar, null, null);
+ AutomationPackageContent automationPackageContent = readAutomationPackageFromJarFile(automationPackageJar, null, null);
assertNotNull(automationPackageContent);
// 6 keywords: 4 from descriptor and two from java class with @Keyword annotation
@@ -208,7 +208,7 @@ public void testReadFromPackage() throws AutomationPackageReadingException {
public void testFragmentsWithPackageAP() throws AutomationPackageReadingException {
File automationPackage = FileHelper.getClassLoaderResourceAsFile(this.getClass().getClassLoader(), "step/automation/packages/step-automation-packages.zip");
- AutomationPackageContent automationPackageContent = reader.readAutomationPackageFromJarFile(automationPackage, null, null);
+ AutomationPackageContent automationPackageContent = readAutomationPackageFromJarFile(automationPackage, null, null);
assertNotNull(automationPackageContent);
List plans = automationPackageContent.getPlans();
@@ -225,7 +225,7 @@ public void testFragmentsWithExplodedAP() throws AutomationPackageReadingExcepti
File tempFolder = FileHelper.createTempFolder();
FileHelper.unzip(this.getClass().getClassLoader().getResourceAsStream("step/automation/packages/step-automation-packages.zip"), tempFolder);
- AutomationPackageContent automationPackageContent = reader.readAutomationPackageFromJarFile(tempFolder, null, null);
+ AutomationPackageContent automationPackageContent = readAutomationPackageFromJarFile(tempFolder, null, null);
assertNotNull(automationPackageContent);
List plans = automationPackageContent.getPlans();
@@ -241,14 +241,14 @@ public void testFragmentsWithExplodedAP() throws AutomationPackageReadingExcepti
public void testInvalidAPNames() {
File automationPackage = FileHelper.getClassLoaderResourceAsFile(this.getClass().getClassLoader(), "step/automation/packages/step-automation-packages-invalidNameBackSlash.zip");
try {
- reader.readAutomationPackageFromJarFile(automationPackage, null, null);
+ readAutomationPackageFromJarFile(automationPackage, null, null);
fail();
} catch (AutomationPackageReadingException e) {
assertEquals("Package name contains unsafe characters: My package\\. Simple quote and backslash characters are not allowed.", e.getMessage());
}
automationPackage = FileHelper.getClassLoaderResourceAsFile(this.getClass().getClassLoader(), "step/automation/packages/step-automation-packages-invalidNameSimpleQuote.zip");
try {
- reader.readAutomationPackageFromJarFile(automationPackage, null, null);
+ readAutomationPackageFromJarFile(automationPackage, null, null);
fail();
} catch (AutomationPackageReadingException e) {
assertEquals("Package name contains unsafe characters: My package';. Simple quote and backslash characters are not allowed.", e.getMessage());
@@ -265,7 +265,7 @@ public void testMissingDescriptor() throws IOException, AutomationPackageReading
boolean deleteOk = descriptor.delete();
Assert.assertTrue(deleteOk);
- AutomationPackageContent automationPackageContent = reader.readAutomationPackageFromJarFile(tempFolder, null, null);
+ AutomationPackageContent automationPackageContent = readAutomationPackageFromJarFile(tempFolder, null, null);
assertNotNull(automationPackageContent);
assertEquals(tempFolder.getName(), automationPackageContent.getName());
@@ -276,4 +276,22 @@ public void testMissingDescriptor() throws IOException, AutomationPackageReading
Assert.assertTrue("Temp folder cannot be removed", FileHelper.deleteFolder(tempFolder));
}
+
+ /**
+ * Convenient method for test
+ *
+ * @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 read from the provided files
+ * @throws AutomationPackageReadingException in case of error
+ */
+ private AutomationPackageContent readAutomationPackageFromJarFile(File automationPackage, String apVersion, File keywordLib) throws AutomationPackageReadingException {
+ try (JavaAutomationPackageArchive automationPackageArchive = new JavaAutomationPackageArchive(automationPackage, keywordLib, null)) {
+ return reader.readAutomationPackage(automationPackageArchive, apVersion);
+ } catch (IOException e) {
+ throw new AutomationPackageReadingException("IO Exception", e);
+ }
+ }
+
}
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..73171c5cd5 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
@@ -21,25 +21,34 @@
import ch.exense.commons.app.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
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;
+import step.core.yaml.deserialization.PatchableYamlList;
import step.functions.Function;
+import step.plans.automation.YamlPlainTextPlan;
import step.plans.nl.RootArtefactType;
import step.plans.nl.parser.PlanParser;
-import step.plans.automation.YamlPlainTextPlan;
import step.plans.parser.yaml.YamlPlanReader;
-import step.plugins.java.GeneralScriptFunction;
import step.repositories.parser.StepsParser;
+import step.resources.LocalResourceManagerImpl;
-import java.io.*;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
-import java.util.*;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
@@ -50,9 +59,8 @@
* these resources are not stored yet).
*/
public abstract class AutomationPackageReader {
-
+ private static final Logger logger = LoggerFactory.getLogger(AutomationPackageReader.class);
public static final String AP_VERSION_SEPARATOR = ".";
- protected static final Logger log = LoggerFactory.getLogger(AutomationPackageReader.class);
private final PlanParser planTextPlanParser;
protected String jsonSchemaPath;
protected final AutomationPackageHookRegistry hookRegistry;
@@ -122,7 +130,7 @@ protected AutomationPackageContent buildAutomationPackage(AutomationPackageDescr
// apply imported fragments recursively
if (descriptor != null) {
- fillAutomationPackageWithImportedFragments(res, descriptor, archive);
+ fillAutomationPackageWithImportedFragments(res, descriptor, archive, new HashMap<>());
}
return res;
}
@@ -173,7 +181,23 @@ 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 getAutomationPackageYamlFragmentManager(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 content = newContentInstance();
+ Map fragmentMap = new ConcurrentHashMap<>();
+ fillAutomationPackageWithImportedFragments(content, descriptor, archive, fragmentMap);
+ StagingAutomationPackageContext stagingContext = new StagingAutomationPackageContext(null, AutomationPackageOperationMode.LOCAL, new LocalResourceManagerImpl(Path.of(descriptorURL.getPath()).getParent().toFile()), archive, content, null, null, new HashMap<>());
+ return new AutomationPackageYamlFragmentManager(descriptor, fragmentMap, getOrCreateDescriptorReader(), stagingContext);
+ } 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 +206,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);
}
@@ -197,10 +223,10 @@ protected void fillContentSections(AutomationPackageContent targetPackage, Autom
readPlainTextPlans(targetPackage, fragment, archive);
- for (Map.Entry> additionalField : fragment.getAdditionalFields().entrySet()) {
+ for (Map.Entry> additionalField : fragment.getAdditionalFields().entrySet()) {
boolean hooked = hookRegistry.onAdditionalDataRead(additionalField.getKey(), additionalField.getValue(), targetPackage);
if (!hooked) {
- log.warn("Hook not found for additional field " + additionalField.getKey() + ". The additional field has been skipped");
+ logger.warn("Hook not found for additional field " + additionalField.getKey() + ". The additional field has been skipped");
}
}
}
@@ -269,7 +295,7 @@ protected synchronized AutomationPackageDescriptorReader getOrCreateDescriptorRe
}
public synchronized void updateJsonSchema(String jsonSchemaPath) {
- log.info("Change json schema for automation package to {}", jsonSchemaPath);
+ logger.info("Change json schema for automation package to {}", jsonSchemaPath);
this.jsonSchemaPath = jsonSchemaPath;
this.descriptorReader = null;
}
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..f1fafc7a22 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
@@ -2,13 +2,15 @@
import ch.exense.commons.app.Configuration;
import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
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 +24,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;
@@ -32,6 +37,7 @@
import java.util.Set;
public class JavaAutomationPackageReader extends AutomationPackageReader {
+ private static final Logger logger = LoggerFactory.getLogger(JavaAutomationPackageReader.class);
protected final StepClassParser stepClassParser;
@@ -62,13 +68,13 @@ protected void fillAutomationPackageWithAnnotatedKeywordsAndPlans(JavaAutomation
// instead of this we keep the scriptFile blank and fill it further in AutomationPackageKeywordsAttributesApplier (after we upload the jar file as resource)
List scannedKeywords = extractAnnotatedKeywords(annotationScanner, null, null);
if (!scannedKeywords.isEmpty()) {
- log.info("{} annotated keywords found in automation package {}", scannedKeywords.size(), StringUtils.defaultString(archive.getAutomationPackageName()));
+ logger.info("{} annotated keywords found in automation package {}", scannedKeywords.size(), StringUtils.defaultString(archive.getAutomationPackageName()));
}
res.getKeywords().addAll(scannedKeywords);
List annotatedPlans = extractAnnotatedPlans(archive, annotationScanner, stepClassParser);
if (!annotatedPlans.isEmpty()) {
- log.info("{} annotated plans found in automation package {}", annotatedPlans.size(), StringUtils.defaultString(archive.getAutomationPackageName()));
+ logger.info("{} annotated plans found in automation package {}", annotatedPlans.size(), StringUtils.defaultString(archive.getAutomationPackageName()));
}
res.getPlans().addAll(annotatedPlans);
} catch (JsonSchemaPreparationException e) {
@@ -154,7 +160,7 @@ private static List getPlanFromPlansAnnotation(Annotation
try {
((AutoCloseable) classLoader).close();
} catch (Exception e) {
- log.error("Unable to close the classloader created from provided package file '{}' after reading its content.", archive.getOriginalFile().getName());
+ logger.error("Unable to close the classloader created from provided package file '{}' after reading its content.", archive.getOriginalFile().getName());
}
}
}
@@ -173,7 +179,7 @@ private static List extractAnnotatedKeywords(Ann
for (Method m : methods) {
Keyword annotation = m.getAnnotation(Keyword.class);
if (annotation == null) {
- log.warn("Keyword annotation is not found for method " + m.getName());
+ logger.warn("Keyword annotation is not found for method " + m.getName());
continue;
}
@@ -236,17 +242,15 @@ private static boolean isCompositeFunction(Keyword annotation) {
}
/**
- * Convenient method for test
+ * Reads automation package into a yaml fragment manager
*
* @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 fragment manager read from the provided files for editing
* @throws AutomationPackageReadingException in case of error
*/
- public AutomationPackageContent readAutomationPackageFromJarFile(File automationPackage, String apVersion, File keywordLib) throws AutomationPackageReadingException {
- try (JavaAutomationPackageArchive automationPackageArchive = new JavaAutomationPackageArchive(automationPackage, keywordLib, null)) {
- return readAutomationPackage(automationPackageArchive, apVersion);
+ public AutomationPackageYamlFragmentManager getAutomationPackageYamlFragmentManager(File automationPackage) throws AutomationPackageReadingException {
+ try (JavaAutomationPackageArchive automationPackageArchive = new JavaAutomationPackageArchive(automationPackage, null, null)) {
+ return getAutomationPackageYamlFragmentManager(automationPackageArchive);
} catch (IOException e) {
throw new AutomationPackageReadingException("IO Exception", e);
}
diff --git a/step-automation-packages/step-automation-packages-schema/src/main/java/step/automation/packages/yaml/schema/YamlKeywordSchemaGenerator.java b/step-automation-packages/step-automation-packages-schema/src/main/java/step/automation/packages/yaml/schema/YamlKeywordSchemaGenerator.java
index 37e234c68a..cb234eb386 100644
--- a/step-automation-packages/step-automation-packages-schema/src/main/java/step/automation/packages/yaml/schema/YamlKeywordSchemaGenerator.java
+++ b/step-automation-packages/step-automation-packages-schema/src/main/java/step/automation/packages/yaml/schema/YamlKeywordSchemaGenerator.java
@@ -23,7 +23,7 @@
import jakarta.json.spi.JsonProvider;
import step.core.yaml.YamlModelUtils;
import step.automation.packages.model.AbstractYamlFunction;
-import step.automation.packages.yaml.AutomationPackageKeywordsLookuper;
+import step.core.yaml.AutomationPackageKeywordsLookuper;
import step.core.scanner.CachedAnnotationScanner;
import step.core.yaml.schema.AggregatedJsonSchemaFieldProcessor;
import step.core.yaml.schema.JsonSchemaDefinitionAddOn;
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/AutomationPackageDescriptorReader.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageDescriptorReader.java
index 5cd4764660..dac6eabcab 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,20 +32,23 @@
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.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.core.yaml.deserialization.*;
+import step.plans.parser.yaml.YamlPlan;
import step.plans.parser.yaml.YamlPlanReader;
+import step.plans.parser.yaml.deserializers.UpgradableYamlPlanDeserializer;
import step.plans.parser.yaml.model.YamlPlanVersions;
import step.plans.parser.yaml.schema.YamlPlanValidationException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -104,9 +110,25 @@ protected T readAutomationPackageYamlF
throw new YamlPlanValidationException(message, ex);
}
}
+ PatchingContext context = new PatchingContext(yamlDescriptorString, yamlObjectMapper);
+ PatchingParserDelegate parser = new PatchingParserDelegate(yamlObjectMapper.createParser(yamlDescriptorString), context);
- T res = yamlObjectMapper.reader().withAttribute("version", version).readValue(yamlDescriptorString, targetClass);
+ Map, Object> injections = new HashMap<>();
+ injections.put(AutomationPackageSerializationRegistry.class, serializationRegistry);
+ injections.put(PatchingContext.class, context);
+ injections.put(ObjectMapper.class, yamlObjectMapper);
+ InjectableValues.Std injectableValues = new InjectableValues.Std();
+ injections.forEach(injectableValues::addValue);
+
+ yamlObjectMapper.setInjectableValues(injectableValues);
+
+ T res = yamlObjectMapper.reader()
+ .withAttributes(injections)
+ .withAttribute("version", version)
+ .readValue(parser, targetClass);
+
+ res.setPatchingContext(context);
logAfterRead(packageName, res);
return res;
} catch (IOException | YamlPlanValidationException e) {
@@ -124,7 +146,7 @@ protected void logAfterRead(String pac
if (!res.getPlansPlainText().isEmpty()) {
log.info("{} plain text plan(s) found in automation package {}", res.getPlans().size(), StringUtils.defaultString(packageName));
}
- for (Map.Entry> additionalEntry : res.getAdditionalFields().entrySet()) {
+ for (Map.Entry> additionalEntry : res.getAdditionalFields().entrySet()) {
log.info("{} {} found in automation package {}", additionalEntry.getValue().size(), additionalEntry.getKey(), StringUtils.defaultString(packageName));
}
if (!res.getFragments().isEmpty()) {
@@ -143,27 +165,16 @@ protected String readJsonSchema(String jsonSchemaPath) {
}
}
- protected ObjectMapper createYamlObjectMapper() {
+ private 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);
- // configure custom deserializers
- SimpleModule module = new SimpleModule();
// register deserializers to read yaml plans
- planReader.registerAllSerializersAndDeserializers(module, yamlMapper, true);
-
- // add annotated jackson deserializers
- StepYamlDeserializersScanner.addAllDeserializerAddonsToModule(module, yamlMapper, List.of(stepYamlDeserializer -> {
- if (stepYamlDeserializer instanceof AutomationPackageSerializationRegistryAware) {
- ((AutomationPackageSerializationRegistryAware) stepYamlDeserializer).setSerializationRegistry(serializationRegistry);
- }
- }));
-
-
+ SimpleModule module = planReader.registerAllSerializersAndDeserializers(yamlMapper, true);
yamlMapper.registerModule(module);
return yamlMapper;
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..6a3c277482
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageWriteToDiskException.java
@@ -0,0 +1,9 @@
+package step.automation.packages.yaml;
+
+import step.core.yaml.deserialization.AutomationPackageUpdateException;
+
+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..5f05a50670
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageYamlFragmentManager.java
@@ -0,0 +1,297 @@
+/*******************************************************************************
+ * 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 step.automation.packages.StagingAutomationPackageContext;
+import step.automation.packages.model.YamlAutomationPackageKeyword;
+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.PatchableYamlModel;
+import step.core.yaml.PatchableYamlModelBase;
+import step.core.yaml.deserialization.AutomationPackagePerObjectSaveUnsupportedException;
+import step.core.yaml.deserialization.AutomationPackageUpdateException;
+import step.core.yaml.deserialization.PatchableYamlList;
+import step.core.yaml.deserialization.PatchingContext;
+import step.functions.Function;
+import step.parameter.Parameter;
+import step.parameter.automation.AutomationPackageParameter;
+import step.plans.parser.yaml.YamlPlan;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.text.MessageFormat;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+public class AutomationPackageYamlFragmentManager {
+
+
+ private final Path apRoot;
+ private final StagingAutomationPackageContext stagingContext;
+
+ public enum NewObjectFragmentMode {
+ /**
+ * Write new objects into fragment with fixed path. PATH indicates fragment yaml. Default: default is [ap field name].yml
+ */
+ FRAGMENT,
+ /**
+ * Write new objects into new fragment, fragment name is given by object name. PATH indicates subfolder of fragment, default is [ap field name].
+ */
+ PER_OBJECT,
+ }
+
+ public static final String PROPERTY_NEW_OBJECT_FRAGMENT_MODE = "newFragmentPaths.%s.mode";
+ public static final String PROPERTY_NEW_OBJECT_FRAGMENT_PATH = "newFragmentPaths.%s.path";
+ private final AutomationPackageDescriptorReader descriptorReader;
+
+ private final Map patchableMap = new ConcurrentHashMap<>();
+ private final Map fragmentMap = new ConcurrentHashMap<>();
+ private final Map pathToYamlFragment;
+
+ private Properties properties = new Properties();
+ private final AutomationPackageFragmentYaml descriptorYaml;
+
+ public AutomationPackageYamlFragmentManager(AutomationPackageDescriptorYaml descriptorYaml, Map fragmentMap, AutomationPackageDescriptorReader descriptorReader, StagingAutomationPackageContext stagingContext) {
+
+ this.descriptorReader = descriptorReader;
+ this.descriptorYaml = descriptorYaml;
+
+ pathToYamlFragment = fragmentMap;
+ apRoot = Path.of(descriptorYaml.getFragmentUrl().getPath())
+ .getParent();
+
+ this.stagingContext = stagingContext;
+ initializeMaps(descriptorYaml);
+
+ pathToYamlFragment.values().stream()
+ .filter(f -> f != descriptorYaml)
+ .forEach(this::initializeMaps);
+ }
+
+ public void setProperties(Properties properties) {
+ this.properties = properties;
+ }
+
+ public void initializeMaps(AutomationPackageFragmentYaml fragment) {
+ pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment);
+ for (YamlPlan yamlPlan : fragment.getPlans()) {
+ Plan plan = descriptorReader.getPlanReader().yamlPlanToPlan(yamlPlan);
+ patchableMap.put(plan, yamlPlan);
+ fragmentMap.put(plan, fragment);
+ }
+
+ for (YamlAutomationPackageKeyword keyword : fragment.getKeywords()) {
+ try {
+ Function function = keyword.prepareKeyword(stagingContext);
+ patchableMap.put(function, keyword);
+ fragmentMap.put(function, fragment);
+ } catch (Exception e) {
+ /* TODO: requires proper handling of keywords
+ which map to resources or require StagingAutomationPackageContext in another way.
+ */
+ System.out.println(e);
+ }
+ }
+
+ PatchableYamlList parameters = fragment.getAdditionalField(Parameter.ENTITY_NAME);
+ if (parameters != null) {
+ for (Object object : parameters) {
+ AutomationPackageParameter yamlParameter = (AutomationPackageParameter) object;
+ Parameter parameter = yamlParameter.toParameter();
+ patchableMap.put(parameter, yamlParameter);
+ fragmentMap.put(parameter, fragment);
+ }
+ }
+ }
+
+ public Iterable getBusinessObjects(Class boClass) {
+ return patchableMap.keySet().stream()
+ .filter(businessObject -> boClass.isAssignableFrom(businessObject.getClass()))
+ .map(businessObject -> (BO) businessObject).collect(Collectors.toList());
+ }
+
+
+ public synchronized Plan savePlan(Plan plan) {
+ YamlPlan newYamlPlan = descriptorReader.getPlanReader().planToYamlPlan(plan);
+
+ AutomationPackageFragmentYaml fragment = fragmentMap.get(plan);
+ if (fragment == null) {
+ fragment = fragmentForNewObject(plan, YamlPlan.PLANS_ENTITY_NAME);
+ fragmentMap.put(plan, fragment);
+ pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment);
+ addFragmentEntity(fragment, fragment.getPlans(), newYamlPlan);
+ } else {
+ YamlPlan yamlPlan = (YamlPlan) patchableMap.get(plan);
+ modifyFragmentEntity(fragment, fragment.getPlans(), yamlPlan, newYamlPlan);
+ }
+ patchableMap.put(plan, newYamlPlan);
+
+ return plan;
+ }
+
+
+ public synchronized step.functions.Function saveFunction(step.functions.Function function) {
+ AutomationPackageFragmentYaml fragment = fragmentMap.get(function);
+ if (fragment == null) {
+ fragment = fragmentForNewObject(function, YamlPlan.PLANS_ENTITY_NAME);
+ fragmentMap.put(function, fragment);
+ pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment);
+ //addFragmentEntity(fragment, fragment.getKeywords(), newYamlKeyword);
+ } else {
+ YamlAutomationPackageKeyword yamlKeyword = (YamlAutomationPackageKeyword) patchableMap.get(function);
+ yamlKeyword.getYamlKeyword().updateFromFunction(function);
+ modifyFragmentEntity(fragment, fragment.getKeywords(), yamlKeyword, yamlKeyword);
+ }
+ //patchableMap.put(function, y);
+
+ return function;
+ }
+
+ private void addFragmentEntity(AutomationPackageFragmentYaml fragment, PatchableYamlList entityList, T newEntity) {
+ entityList.add(newEntity);
+ fragment.writeToDisk();
+ }
+
+ private void modifyFragmentEntity(AutomationPackageFragmentYaml fragment, PatchableYamlList entityList, T oldEntity, T newEntity) {
+ entityList.replaceItem(oldEntity, newEntity);
+ fragment.writeToDisk();
+ }
+
+ private AutomationPackageFragmentYaml fragmentForNewObject(AbstractOrganizableObject p, String fieldName) {
+
+ NewObjectFragmentMode mode = NewObjectFragmentMode.valueOf(properties.getProperty(String.format(PROPERTY_NEW_OBJECT_FRAGMENT_MODE, fieldName), NewObjectFragmentMode.PER_OBJECT.name()));
+ String defaultRelativeFragmentPath = fieldName;
+ if (mode == NewObjectFragmentMode.FRAGMENT) {
+ defaultRelativeFragmentPath = defaultRelativeFragmentPath + ".yml";
+ }
+
+
+ if (mode == NewObjectFragmentMode.PER_OBJECT && !p.hasAttribute(AbstractOrganizableObject.NAME)) {
+ throw new AutomationPackagePerObjectSaveUnsupportedException(String.format("""
+ Saving by object name is unsupported for %1$s, please configure the entity to be stored in a specified single fragment, i.e.
+
+ %2$s = %1$s.yml
+ %3$s = %4$s
+ """, fieldName, String.format(PROPERTY_NEW_OBJECT_FRAGMENT_PATH, fieldName), String.format(PROPERTY_NEW_OBJECT_FRAGMENT_MODE, fieldName), NewObjectFragmentMode.FRAGMENT.name()));
+ }
+
+ String relativeFragmentPath = properties.getProperty(String.format(PROPERTY_NEW_OBJECT_FRAGMENT_PATH, fieldName), defaultRelativeFragmentPath);
+ Path path = new File(relativeFragmentPath).toPath();
+ if (!path.isAbsolute()) {
+ path = apRoot.resolve(path);
+ }
+
+ if (mode == NewObjectFragmentMode.PER_OBJECT) {
+ path = path.resolve(sanitizeFilename(p.getAttribute(AbstractOrganizableObject.NAME)) + ".yml");
+ }
+
+ try {
+ URL url = path.toUri().toURL();
+
+
+ if (pathToYamlFragment.containsKey(url.toString())) {
+ return pathToYamlFragment.get(url.toString());
+ }
+ PatchingContext context = new PatchingContext("---", descriptorYaml.getPatchingContext().getMapper());
+ AutomationPackageFragmentYaml fragment = new AutomationPackageFragmentYamlImpl(context);
+ 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 URLEncoder.encode(inputName, Charset.defaultCharset()).replace("+", " ");
+ }
+
+ public void removePlan(Plan plan) {
+ AutomationPackageFragmentYaml fragment = fragmentMap.get(plan);
+ YamlPlan yamlPlan = (YamlPlan) patchableMap.get(plan);
+
+ fragment.getPlans().remove(yamlPlan);
+
+ patchableMap.remove(plan);
+ fragmentMap.remove(plan);
+
+ fragment.writeToDisk();
+ }
+
+ public void removeFunction(step.functions.Function function) {
+ AutomationPackageFragmentYaml fragment = fragmentMap.get(function);
+ YamlAutomationPackageKeyword yamlKeyword = (YamlAutomationPackageKeyword) patchableMap.get(function);
+
+ fragment.getPlans().remove(yamlKeyword);
+
+ patchableMap.remove(function);
+ fragmentMap.remove(function);
+
+ fragment.writeToDisk();
+ }
+
+
+ public void removeAdditionalFieldObject(BO object, String fieldName) {
+
+ AutomationPackageFragmentYaml fragment = fragmentMap.get(object);
+ PatchableYamlModel yamlObject = patchableMap.get(object);
+
+ fragment.getAdditionalField(fieldName)
+ .remove(yamlObject);
+
+ patchableMap.remove(object);
+ fragmentMap.remove(object);
+
+ fragment.writeToDisk();
+ }
+
+ public synchronized BO saveAdditionalFieldObject(BO object, java.util.function.Function newYamlObjectCreator, String fieldName) {
+ AutomationPackageFragmentYaml fragment = fragmentMap.get(object);
+ if (fragment == null) {
+ fragment = fragmentForNewObject(object, fieldName);
+
+ YO newYamlObject = newYamlObjectCreator.apply(fragment.getPatchingContext());
+ PatchableYamlList list = (PatchableYamlList) fragment.getAdditionalFields().getOrDefault(fieldName, new PatchableYamlList(fragment.getPatchingContext(), fieldName));
+
+ fragmentMap.put(object, fragment);
+ pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment);
+ addFragmentEntity(fragment, list, newYamlObject);
+ patchableMap.put(object, newYamlObject);
+ } else {
+ YO newYamlObject = newYamlObjectCreator.apply(fragment.getPatchingContext());
+ PatchableYamlList list = (PatchableYamlList) fragment.getAdditionalFields().getOrDefault(fieldName, new PatchableYamlList(fragment.getPatchingContext(), fieldName));
+
+ YO oldYamlObject = (YO) patchableMap.get(object);
+ modifyFragmentEntity(fragment, list, oldYamlObject, newYamlObject);
+ patchableMap.put(object, newYamlObject);
+ }
+ return object;
+ }
+
+
+}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/AbstractYamlAutomationPackageFragmentDeserializer.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/AbstractYamlAutomationPackageFragmentDeserializer.java
new file mode 100644
index 0000000000..d22526a3f1
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/AbstractYamlAutomationPackageFragmentDeserializer.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * 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.JsonParser;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.BeanDeserializer;
+import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
+import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
+import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
+import step.automation.packages.yaml.model.AbstractAutomationPackageFragmentYaml;
+import step.automation.packages.yaml.model.AutomationPackageFragmentYamlImpl;
+import step.core.yaml.deserialization.PatchableYamlList;
+import step.core.yaml.deserialization.PatchingContext;
+import step.core.yaml.deserializers.StepYamlDeserializerAddOn;
+
+import java.io.IOException;
+import java.util.List;
+
+public abstract class AbstractYamlAutomationPackageFragmentDeserializer extends BeanDeserializer implements ContextualDeserializer, ResolvableDeserializer {
+
+ private final BeanDeserializer delegate;
+
+ public AbstractYamlAutomationPackageFragmentDeserializer(BeanDeserializer deserializer) {
+ super(deserializer);
+ delegate = deserializer;
+ }
+
+ @Override
+ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ return deserialize(p, ctxt, new AutomationPackageFragmentYamlImpl((PatchingContext) ctxt.getAttribute(PatchingContext.class)));
+ }
+
+ @Override
+ protected void handleUnknownVanilla(JsonParser p, DeserializationContext ctxt, Object intoValue, String propName) throws IOException {
+ try {
+ AutomationPackageSerializationRegistry registry = (AutomationPackageSerializationRegistry) ctxt.getAttribute(AutomationPackageSerializationRegistry.class);
+ Class> targetClass = registry.resolveClassForYamlField(propName);
+ JavaType listType = ctxt.getTypeFactory()
+ .constructCollectionType(PatchableYamlList.class, targetClass);
+ PatchableYamlList> list = ctxt.readValue(p, listType);
+
+ AbstractAutomationPackageFragmentYaml fragment = (AbstractAutomationPackageFragmentYaml) intoValue;
+ fragment.setAdditionalFields(propName, list);
+ } catch (ClassCastException | NullPointerException e) {
+ super.handleUnknownVanilla(p, ctxt, intoValue, propName);
+ }
+ }
+
+ @Override
+ public void resolve(DeserializationContext ctxt) throws JsonMappingException {
+ if (_delegateDeserializer instanceof ResolvableDeserializer) {
+ ((ResolvableDeserializer) _delegateDeserializer).resolve(ctxt);
+ }
+ }
+
+}
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
index e81e60510d..c010ebdd43 100644
--- 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
@@ -18,20 +18,38 @@
******************************************************************************/
package step.automation.packages.yaml.deserialization;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.core.JsonParser;
+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.BeanDeserializer;
import step.automation.packages.yaml.model.AutomationPackageDescriptorYamlImpl;
+import step.automation.packages.yaml.model.AutomationPackageFragmentYamlImpl;
+import step.core.yaml.deserialization.PatchingContext;
import step.core.yaml.deserializers.StepYamlDeserializerAddOn;
+import java.io.IOException;
+
@StepYamlDeserializerAddOn(targetClasses = {AutomationPackageDescriptorYamlImpl.class})
-public class YamlAutomationPackageDescriptorDeserializer extends YamlAutomationPackageFragmentDeserializer {
+public class YamlAutomationPackageDescriptorDeserializer extends AbstractYamlAutomationPackageFragmentDeserializer {
+
+ private final BeanDeserializer delegate;
- public YamlAutomationPackageDescriptorDeserializer(ObjectMapper yamlObjectMapper) {
- super(yamlObjectMapper);
+ public YamlAutomationPackageDescriptorDeserializer(BeanDeserializer deserializer) {
+ super(deserializer);
+ delegate = deserializer;
}
@Override
- protected Class> getObjectClass() {
- return AutomationPackageDescriptorYamlImpl.class;
+ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ return deserialize(p, ctxt, new AutomationPackageDescriptorYamlImpl((PatchingContext) ctxt.getAttribute(PatchingContext.class)));
}
+ @Override
+ public JsonDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
+ BeanDeserializer resolved = (BeanDeserializer) delegate.createContextual(ctxt, property);
+ resolved.resolve(ctxt);
+ return new YamlAutomationPackageDescriptorDeserializer(resolved);
+ }
}
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
index 3e9441978d..857dc8644c 100644
--- 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
@@ -18,80 +18,39 @@
******************************************************************************/
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 com.fasterxml.jackson.databind.deser.BeanDeserializer;
+import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
+import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
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.deserialization.PatchingContext;
import step.core.yaml.deserializers.StepYamlDeserializerAddOn;
-import step.core.yaml.SerializationUtils;
import java.io.IOException;
-import java.util.*;
+import java.util.List;
@StepYamlDeserializerAddOn(targetClasses = {AutomationPackageFragmentYamlImpl.class})
-public class YamlAutomationPackageFragmentDeserializer extends StepYamlDeserializer
- implements AutomationPackageSerializationRegistryAware {
+public class YamlAutomationPackageFragmentDeserializer extends AbstractYamlAutomationPackageFragmentDeserializer {
- protected AutomationPackageSerializationRegistry registry;
+ private final BeanDeserializer delegate;
- public YamlAutomationPackageFragmentDeserializer(ObjectMapper yamlObjectMapper) {
- super(yamlObjectMapper);
+ public YamlAutomationPackageFragmentDeserializer(BeanDeserializer deserializer) {
+ super(deserializer);
+ delegate = deserializer;
}
@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;
+ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ return deserialize(p, ctxt, new AutomationPackageFragmentYamlImpl((PatchingContext) ctxt.getAttribute(PatchingContext.class)));
}
@Override
- public void setSerializationRegistry(AutomationPackageSerializationRegistry registry) {
- this.registry = registry;
+ public JsonDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
+ BeanDeserializer resolved = (BeanDeserializer) delegate.createContextual(ctxt, property);
+ resolved.resolve(ctxt);
+ return new YamlAutomationPackageFragmentDeserializer(resolved);
}
}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/YamlKeywordDeserializer.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/YamlKeywordDeserializer.java
index 540ade334f..686d00d367 100644
--- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/YamlKeywordDeserializer.java
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/deserialization/YamlKeywordDeserializer.java
@@ -18,14 +18,15 @@
******************************************************************************/
package step.automation.packages.yaml.deserialization;
+import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import step.automation.packages.model.AbstractYamlFunction;
import step.automation.packages.model.YamlAutomationPackageKeyword;
-import step.automation.packages.yaml.AutomationPackageKeywordsLookuper;
-import step.core.yaml.deserializers.NamedEntityYamlDeserializer;
+import step.core.yaml.AutomationPackageKeywordsLookuper;
+import step.core.yaml.deserialization.PatchingParserDelegate;
import step.core.yaml.deserializers.StepYamlDeserializer;
import step.core.yaml.deserializers.StepYamlDeserializerAddOn;
@@ -34,39 +35,28 @@
@StepYamlDeserializerAddOn(targetClasses = {YamlAutomationPackageKeyword.class})
public class YamlKeywordDeserializer extends StepYamlDeserializer {
- private final AutomationPackageKeywordsLookuper keywordsLookuper;
+ private final AutomationPackageKeywordsLookuper keywordsLookuper = new AutomationPackageKeywordsLookuper();
- public YamlKeywordDeserializer() {
- this(null);
- }
-
- public YamlKeywordDeserializer(ObjectMapper yamlObjectMapper) {
- super(yamlObjectMapper);
- this.keywordsLookuper = new AutomationPackageKeywordsLookuper();
+ public YamlKeywordDeserializer(JsonDeserializer> deserializer, ObjectMapper yamlObjectMapper) {
+ super(deserializer, yamlObjectMapper);
}
@Override
public YamlAutomationPackageKeyword deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
- JsonNode node = jsonParser.getCodec().readTree(jsonParser);
- NamedEntityYamlDeserializer> nameEntityDeserializer = new NamedEntityYamlDeserializer<>() {
- @Override
- protected String resolveTargetClassNameByYamlName(String yamlName) {
- return null;
- }
-
- protected Class> resolveTargetClassByYamlName(String yamlName) {
- try {
- String className = keywordsLookuper.yamlKeywordClassToJava(yamlName);
- if (className == null) {
- throw new RuntimeException("Unable to resolve keyword class for '" + yamlName + "'");
- }
- return Class.forName(className);
- } catch (ClassNotFoundException e) {
- throw new RuntimeException("Unable to resolve keyword class for '" + yamlName + "'");
- }
- }
- };
- return new YamlAutomationPackageKeyword(nameEntityDeserializer.deserialize(node, jsonParser.getCodec()));
+ String yamlName = jsonParser.nextFieldName();
+
+ try {
+ PatchingParserDelegate patchingParser = (PatchingParserDelegate) jsonParser;
+ Class> clazz = Class.forName(keywordsLookuper.yamlKeywordClassToJava(yamlName));
+ JsonLocation startItem = patchingParser.currentLocation();
+ jsonParser.nextToken();
+ YamlAutomationPackageKeyword keyword = new YamlAutomationPackageKeyword((AbstractYamlFunction>) deserializationContext.readValue(jsonParser, clazz), patchingParser.getPatchingContext());
+ keyword.setPatchingBounds(startItem, patchingParser.getLastDistinctLocation());
+ jsonParser.nextToken();
+ return keyword;
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
}
}
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..284bf5155b 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,43 +18,64 @@
******************************************************************************/
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 org.apache.commons.io.FileUtils;
+import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
import step.automation.packages.model.YamlAutomationPackageKeyword;
+import step.automation.packages.yaml.AutomationPackageWriteToDiskException;
+import step.core.yaml.deserialization.AutomationPackageConcurrentEditException;
+import step.core.yaml.deserialization.PatchableYamlList;
+import step.core.yaml.deserialization.PatchingContext;
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.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.*;
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
public abstract class AbstractAutomationPackageFragmentYaml implements AutomationPackageFragmentYaml {
private List fragments = new ArrayList<>();
- private List keywords = new ArrayList<>();
- private List plans = new ArrayList<>();
+ private PatchableYamlList keywords;
+ private PatchableYamlList plans;
private List plansPlainText = new ArrayList<>();
+ private final Map> additionalFields = new HashMap<>();
+ private PatchingContext context;
+ private long fileLastModified = 0;
+
+ public AbstractAutomationPackageFragmentYaml(PatchingContext patchingContext) {
+ context = patchingContext;
+ plans = new PatchableYamlList<>(patchingContext, YamlPlan.PLANS_ENTITY_NAME);
+ keywords = new PatchableYamlList<>(patchingContext, YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME);
+ }
+
@JsonIgnore
- private Map> additionalFields;
+ private URL url;
@Override
- public List getKeywords() {
+ public PatchableYamlList getKeywords() {
return keywords;
}
@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 +89,15 @@ public void setFragments(List fragments) {
this.fragments = fragments;
}
+ @JsonAnyGetter
@Override
- public Map> getAdditionalFields() {
+ public Map> getAdditionalFields() {
return additionalFields;
}
- public void setAdditionalFields(Map> additionalFields) {
- this.additionalFields = additionalFields;
+ @Override
+ public void setAdditionalFields(String key, PatchableYamlList> list) {
+ additionalFields.put(key, list);
}
@Override
@@ -86,4 +109,46 @@ public List getPlansPlainText() {
public void setPlansPlainText(List plansPlainText) {
this.plansPlainText = plansPlainText;
}
+
+ @JsonIgnore
+ public void setFragmentUrl(URL url) {
+ resetLastModified();
+ this.url = url;
+ }
+
+ private void resetLastModified() {
+ fileLastModified = System.currentTimeMillis();
+ }
+
+ @JsonIgnore
+ public URL getFragmentUrl() {
+ return url;
+ }
+
+ @JsonIgnore
+ @Override
+ public void setPatchingContext(PatchingContext context) {
+ this.context = context;
+ }
+
+ @JsonIgnore
+ @Override
+ public PatchingContext getPatchingContext() {
+ return context;
+ }
+
+
+ @Override
+ public void writeToDisk() {
+ try {
+ File file = new File(url.toURI());
+ if (file.exists() && file.lastModified() > fileLastModified) {
+ throw new AutomationPackageConcurrentEditException(MessageFormat.format("Automation package fragment {0} was edited outside the editor.", url));
+ }
+ FileUtils.writeStringToFile(file, context.getYaml(), StandardCharsets.UTF_8);
+ resetLastModified();
+ } catch (IOException | URISyntaxException e) {
+ throw new AutomationPackageWriteToDiskException(MessageFormat.format("Error when writing automation package fragment {0} back to disk.", url), e);
+ }
+ }
}
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..e7be231568 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,13 @@
******************************************************************************/
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 step.core.yaml.deserialization.PatchingContext;
+
import java.util.HashMap;
import java.util.Map;
@@ -29,6 +36,10 @@ public class AutomationPackageDescriptorYamlImpl extends AbstractAutomationPacka
private String name;
+ public AutomationPackageDescriptorYamlImpl(@JacksonInject(useInput = OptBoolean.FALSE) PatchingContext patchingContext) {
+ super(patchingContext);
+ }
+
@Override
public String getName() {
return name;
@@ -55,4 +66,5 @@ public Map getAttributes() {
public void setAttributes(Map attributes) {
this.attributes = attributes;
}
+
}
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..69f09fe8ee 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,26 +18,43 @@
******************************************************************************/
package step.automation.packages.yaml.model;
+import com.fasterxml.jackson.databind.JsonNode;
import step.automation.packages.model.YamlAutomationPackageKeyword;
+import step.core.yaml.deserialization.PatchableYamlList;
+import step.core.yaml.deserialization.PatchingContext;
import step.plans.automation.YamlPlainTextPlan;
import step.plans.parser.yaml.YamlPlan;
+import java.io.IOException;
+import java.net.URL;
import java.util.List;
import java.util.Map;
public interface AutomationPackageFragmentYaml {
- List getKeywords();
+ PatchableYamlList getKeywords();
- List getPlans();
+ PatchableYamlList getPlans();
List getPlansPlainText();
List getFragments();
- Map> getAdditionalFields();
+ Map> getAdditionalFields();
- default List getAdditionalField(String k) {
- return (List) getAdditionalFields().get(k);
+ default PatchableYamlList getAdditionalField(String k) {
+ return (PatchableYamlList) getAdditionalFields().get(k);
}
+
+ void setAdditionalFields(String key, PatchableYamlList> value) throws IOException;
+
+ URL getFragmentUrl();
+
+ void setFragmentUrl(URL url);
+
+ PatchingContext getPatchingContext();
+
+ void setPatchingContext(PatchingContext context);
+
+ void writeToDisk();
}
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..6d4aecc70a 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,16 @@
******************************************************************************/
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 step.core.yaml.deserialization.PatchingContext;
+
public class AutomationPackageFragmentYamlImpl extends AbstractAutomationPackageFragmentYaml {
+ public AutomationPackageFragmentYamlImpl(@JacksonInject(useInput = OptBoolean.FALSE) PatchingContext patchingContext) {
+ super(patchingContext);
+ }
}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/serialization/YamlKeywordSerializer.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/serialization/YamlKeywordSerializer.java
new file mode 100644
index 0000000000..053955a991
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/serialization/YamlKeywordSerializer.java
@@ -0,0 +1,47 @@
+package step.automation.packages.yaml.serialization;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import step.automation.packages.model.YamlAutomationPackageKeyword;
+import step.core.yaml.YamlModelUtils;
+import step.core.yaml.deserializers.StepYamlDeserializerAddOn;
+import step.core.yaml.serializers.StepYamlSerializer;
+import step.core.yaml.serializers.StepYamlSerializerAddOn;
+
+import java.io.IOException;
+
+/*******************************************************************************
+ * 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 .
+ ******************************************************************************/
+@StepYamlSerializerAddOn(targetClasses = {YamlAutomationPackageKeyword.class})
+public class YamlKeywordSerializer extends StepYamlSerializer {
+
+
+ public YamlKeywordSerializer(ObjectMapper yamlObjectMapper) {
+ super(yamlObjectMapper);
+ }
+
+ @Override
+ public void serialize(YamlAutomationPackageKeyword yamlKeyword, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
+ gen.writeStartObject();
+ gen.writeFieldName(YamlModelUtils.getEntityNameByClass(yamlKeyword.getYamlKeyword().getClass()));
+ gen.writeObject(yamlKeyword.getYamlKeyword());
+ gen.writeEndObject();
+ }
+}
diff --git a/step-core-model/pom.xml b/step-core-model/pom.xml
index ddef4e34fe..b2b6646b2d 100644
--- a/step-core-model/pom.xml
+++ b/step-core-model/pom.xml
@@ -43,6 +43,11 @@
ch.exense.step
step-framework-model
+
+ ch.exense.step
+ step-functions-plugins-java-keyword-handler
+ 0.0.0-SNAPSHOT
+
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..029ed3e0bb 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
@@ -29,7 +29,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/PatchableYamlModel.java b/step-core-model/src/main/java/step/core/yaml/PatchableYamlModel.java
new file mode 100644
index 0000000000..2dcfad3a80
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/PatchableYamlModel.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * 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.yaml;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import step.core.yaml.deserialization.PatchingContext;
+
+public interface PatchableYamlModel {
+
+ void setPatchingBounds(JsonLocation startLocation, JsonLocation endLocation);
+
+ int getStartOffset();
+
+ int getIndent();
+
+ int getEndOffset();
+
+ void setStartOffset(int startOffset);
+
+ void setEndOffset(int endOffset);
+
+ void setIndent(int indent);
+
+ void setContext(PatchingContext context);
+}
diff --git a/step-core-model/src/main/java/step/core/yaml/PatchableYamlModelBase.java b/step-core-model/src/main/java/step/core/yaml/PatchableYamlModelBase.java
new file mode 100644
index 0000000000..4146a38d8b
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/PatchableYamlModelBase.java
@@ -0,0 +1,87 @@
+/*******************************************************************************
+ * 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 step.core.yaml.deserialization.PatchingContext;
+
+public class PatchableYamlModelBase extends AbstractYamlModel implements PatchableYamlModel {
+
+ @JsonIgnore
+ private PatchingContext context;
+
+ @JsonIgnore
+ private int startOffset = -1;
+
+ @JsonIgnore
+ private int indent = -1;
+
+ @JsonIgnore
+ private int endOffset = -1;
+
+ public PatchableYamlModelBase(PatchingContext context) {
+ this.context = context;
+ }
+
+ @JsonIgnore
+ public void setPatchingBounds(JsonLocation startLocation, JsonLocation endLocation) {
+ startOffset = (int) startLocation.getCharOffset();
+ endOffset = context.ensureNextEndOfLineOffset((int) endLocation.getCharOffset());
+ indent = startLocation.getColumnNr() -1;
+ context.getPatchables().add(this);
+ }
+
+ @JsonIgnore
+ public int getStartOffset(){
+ return startOffset;
+ }
+
+ @JsonIgnore
+ public int getIndent() {
+ return indent;
+ }
+
+ @JsonIgnore
+ public int getEndOffset() {
+ return endOffset;
+ }
+
+ @Override
+ public void setStartOffset(int startOffset) {
+ this.startOffset = startOffset;
+ }
+
+ @Override
+ public void setEndOffset(int endOffset) {
+ this.endOffset = endOffset;
+ }
+
+
+ @JsonIgnore
+ @Override
+ public void setIndent(int indent) {
+ this.indent = indent;
+ }
+
+ @Override
+ public void setContext(PatchingContext context) {
+ this.context = context;
+ }
+}
diff --git a/step-core/src/main/java/step/automation/packages/deserialization/AutomationPackageSerializationRegistryAware.java b/step-core-model/src/main/java/step/core/yaml/deserialization/AutomationPackageConcurrentEditException.java
similarity index 76%
rename from step-core/src/main/java/step/automation/packages/deserialization/AutomationPackageSerializationRegistryAware.java
rename to step-core-model/src/main/java/step/core/yaml/deserialization/AutomationPackageConcurrentEditException.java
index bdcbbe372d..8c9412a879 100644
--- a/step-core/src/main/java/step/automation/packages/deserialization/AutomationPackageSerializationRegistryAware.java
+++ b/step-core-model/src/main/java/step/core/yaml/deserialization/AutomationPackageConcurrentEditException.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (C) 2020, exense GmbH
+ * Copyright (C) 2026, exense GmbH
*
* This file is part of STEP
*
@@ -16,9 +16,10 @@
* You should have received a copy of the GNU Affero General Public License
* along with STEP. If not, see .
******************************************************************************/
-package step.automation.packages.deserialization;
+package step.core.yaml.deserialization;
-public interface AutomationPackageSerializationRegistryAware {
-
- void setSerializationRegistry(AutomationPackageSerializationRegistry registry);
+public class AutomationPackageConcurrentEditException extends AutomationPackageUpdateException {
+ public AutomationPackageConcurrentEditException(String s) {
+ super(s);
+ }
}
diff --git a/step-core-model/src/main/java/step/core/yaml/deserialization/AutomationPackagePerObjectSaveUnsupportedException.java b/step-core-model/src/main/java/step/core/yaml/deserialization/AutomationPackagePerObjectSaveUnsupportedException.java
new file mode 100644
index 0000000000..20749728f5
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/deserialization/AutomationPackagePerObjectSaveUnsupportedException.java
@@ -0,0 +1,26 @@
+package step.core.yaml.deserialization;
+
+/*******************************************************************************
+ * 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 .
+ ******************************************************************************/
+public class AutomationPackagePerObjectSaveUnsupportedException extends AutomationPackageUpdateException {
+
+ public AutomationPackagePerObjectSaveUnsupportedException(String message) {
+ super(message);
+ }
+}
diff --git a/step-core-model/src/main/java/step/core/yaml/deserialization/AutomationPackageUpdateException.java b/step-core-model/src/main/java/step/core/yaml/deserialization/AutomationPackageUpdateException.java
new file mode 100644
index 0000000000..f0cf471b21
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/deserialization/AutomationPackageUpdateException.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * 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.yaml.deserialization;
+
+public class AutomationPackageUpdateException extends RuntimeException {
+ public AutomationPackageUpdateException(String message, Exception e) {
+ super(message, e);
+ }
+
+ public AutomationPackageUpdateException(String message) {
+ super(message);
+ }
+}
diff --git a/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlList.java b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlList.java
new file mode 100644
index 0000000000..7ba84bb908
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlList.java
@@ -0,0 +1,176 @@
+/*******************************************************************************
+ * 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.yaml.deserialization;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.core.JsonLocation;
+import step.core.yaml.PatchableYamlModel;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.UnaryOperator;
+
+public class PatchableYamlList extends ArrayList implements PatchableYamlModel{
+
+
+ private PatchingContext context;
+ private final String fieldName;
+
+ @JsonIgnore
+ private int startOffset = -1;
+
+ @JsonIgnore
+ private int indent = -1;
+
+ @JsonIgnore
+ private int endOffset = -1;
+
+ public PatchableYamlList(PatchingContext context, String fieldName) {
+
+ this(new ArrayList<>(), context, fieldName);
+ }
+
+ protected PatchableYamlList(Collection delegate, PatchingContext context, String fieldName) {
+ super(delegate);
+ this.context = context;
+ this.fieldName = fieldName;
+ }
+
+ @Override
+ public boolean remove(Object item) {
+ if (super.remove(item)) {
+ PatchableYamlModel patchableItem = (PatchableYamlModel) item;
+ context.removePatchable(patchableItem);
+
+ if (super.isEmpty()) {
+ context.removePatchable(this);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean add(T item) {
+ if (!context.contains(this)) {
+ context.appendEmptyPatchable(this);
+ }
+ PatchableYamlModel patchableItem = (PatchableYamlModel) item;
+
+ if (super.isEmpty()) {
+ patchableItem.setIndent(getListItemMarker().length());
+ context.replaceContainerPatchable(this, patchableItem, fieldName + ":\n" + getListItemMarker());
+ } else {
+ PatchableYamlModel last = (PatchableYamlModel) get(size()-1);
+
+ context.addPatchableAfter(last, patchableItem, getListItemMarker(last));
+ }
+ super.add(item);
+ return true;
+ }
+
+ private String getListItemMarker(PatchableYamlModel last) {
+ String yaml = context.getYaml();
+ int listItemMarkerStartOffset = yaml.lastIndexOf("\n", last.getStartOffset());
+ return yaml.substring( listItemMarkerStartOffset, last.getStartOffset());
+ }
+
+ private String getListItemMarker() {
+ return " ".repeat(indent) + "- ";
+ }
+
+ @Override
+ public void replaceAll(UnaryOperator operator) {
+ super.replaceAll(item -> {
+ T newItem = operator.apply(item);
+ return newItem;
+ });
+ }
+
+ @Override
+ public boolean addAll(Collection extends T> c) {
+ return c.stream().anyMatch(this::add);
+ }
+
+ @Override
+ public boolean removeAll(Collection> c) {
+ return c.stream().anyMatch(this::remove);
+ }
+
+ @Override
+ public boolean retainAll(Collection> c) {
+ return removeIf(o -> !c.contains(o));
+ }
+
+ @Override
+ public void clear() {
+ super.forEach(this::remove);
+ }
+
+ public void replaceItem(PatchableYamlModel oldEntity, PatchableYamlModel newEntity) {
+ replaceAll(item -> item == oldEntity ? (T) newEntity : item);
+ context.replacePatchable(oldEntity, newEntity);
+ }
+
+ @JsonIgnore
+ public void setPatchingBounds(JsonLocation startLocation, JsonLocation endLocation) {
+ startOffset = (int) startLocation.getCharOffset();
+ endOffset = context.ensureNextEndOfLineOffset((int) endLocation.getCharOffset());
+ indent = startLocation.getColumnNr() -1;
+ context.getPatchables().add(this);
+ }
+
+ @Override
+ public int getStartOffset(){
+ return startOffset;
+ }
+
+ @Override
+ public int getIndent() {
+ return indent;
+ }
+
+ @Override
+ public int getEndOffset() {
+ return endOffset;
+ }
+
+
+ @Override
+ public void setStartOffset(int startOffset) {
+ this.startOffset = startOffset;
+ }
+
+ @Override
+ public void setEndOffset(int endOffset) {
+ this.endOffset = endOffset;
+ }
+
+ @JsonIgnore
+ @Override
+ public void setIndent(int indent) {
+ this.indent = indent;
+ }
+
+ @Override
+ @JsonIgnore
+ public void setContext(PatchingContext context) {
+ this.context = context;
+ }
+}
diff --git a/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlListDeserializer.java b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlListDeserializer.java
new file mode 100644
index 0000000000..192ec9d849
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlListDeserializer.java
@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * 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.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.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 java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class PatchableYamlListDeserializer extends CollectionDeserializer {
+
+
+ private final CollectionDeserializer delegate;
+
+ public PatchableYamlListDeserializer(CollectionDeserializer delegate) {
+ super(delegate);
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Collection deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ if (p instanceof PatchingParserDelegate) {
+ PatchingParserDelegate patchingParser = (PatchingParserDelegate) p;
+ JsonLocation startLocation = patchingParser.getLastLocationForToken(JsonToken.FIELD_NAME);
+ Collection entity = delegate.deserialize(p, ctxt, new ArrayList<>());
+ PatchableYamlList patchableYamlList = new PatchableYamlList<>(entity, patchingParser.getPatchingContext(), patchingParser.currentName());
+ patchableYamlList.setPatchingBounds(startLocation, patchingParser.getLastDistinctLocation());
+ 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 new PatchableYamlListDeserializer(resolved);
+ }
+}
diff --git a/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlModelDeserializer.java b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlModelDeserializer.java
new file mode 100644
index 0000000000..af2993db76
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlModelDeserializer.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * 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.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;
+ JsonLocation startItem = patchingParser.currentLocation();
+ T entity = delegate.deserialize(p, ctxt);
+ entity.setPatchingBounds(startItem, 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-core-model/src/main/java/step/core/yaml/deserialization/PatchingContext.java b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchingContext.java
new file mode 100644
index 0000000000..8c759474e7
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchingContext.java
@@ -0,0 +1,166 @@
+/*******************************************************************************
+ * 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.yaml.deserialization;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import step.core.yaml.PatchableYamlModel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PatchingContext {
+ private String yaml;
+
+ private final List patchables = new ArrayList<>();
+ private final ObjectMapper mapper;
+
+ public PatchingContext() {
+ yaml = "";
+ mapper = new ObjectMapper();
+ }
+
+ public PatchingContext(String yaml, ObjectMapper mapper) {
+ this.yaml = yaml;
+ this.mapper = mapper;
+ }
+
+ public String getYaml() {
+ return yaml;
+ }
+
+ public ObjectMapper getMapper() {
+ return mapper;
+ }
+
+ public List getPatchables() {
+ return patchables;
+ }
+
+ public void setYaml(String yaml) {
+ this.yaml = yaml;
+ }
+
+ private String entityStringWithIndent(PatchableYamlModel entity, int indent) {
+ try {
+ String indentString = " ".repeat(indent);
+ return mapper
+ .writeValueAsString(entity)
+ .replaceAll("---\n", "")
+ .trim()
+ .replaceAll("\n", "\n" + indentString);
+ } catch (JsonProcessingException e) {
+ throw new AutomationPackageUpdateException("Error Serializing new object", e);
+ }
+ }
+
+ public void replacePatchable(PatchableYamlModel oldPatchable, PatchableYamlModel newPatchable) {
+ String entityString = entityStringWithIndent(newPatchable, oldPatchable.getIndent());
+
+ int endOffset = oldPatchable.getEndOffset();
+ int delta = entityString.length() - (endOffset - oldPatchable.getStartOffset());
+
+
+ yaml = yaml.substring(0, oldPatchable.getStartOffset())
+ + entityString
+ + yaml.substring(oldPatchable.getEndOffset());
+
+ newPatchable.setStartOffset(oldPatchable.getStartOffset());
+ newPatchable.setIndent(oldPatchable.getIndent());
+ newPatchable.setEndOffset(oldPatchable.getEndOffset() + delta);
+
+ patchables.replaceAll(p -> p == oldPatchable ? newPatchable : p);
+
+ updatePatchableOffsetsAfter(newPatchable, endOffset, delta);
+ }
+
+ public void removePatchable(PatchableYamlModel patchable) {
+ if (!patchables.contains(patchable)) return;
+
+ int previousLineEnd = ensurePreviousEndOfLineOffset(patchable.getStartOffset());
+ int delta = previousLineEnd - patchable.getEndOffset();
+ yaml = yaml.substring(0, previousLineEnd) + yaml.substring(patchable.getEndOffset());
+ updatePatchableOffsetsAfter(patchable, patchable.getEndOffset(), delta);
+ patchables.remove(patchable);
+ }
+
+
+
+ public void addPatchableAfter(PatchableYamlModel last, PatchableYamlModel patchableItem, String entityPrefix) {
+
+ String entityString = entityStringWithIndent(patchableItem, last.getIndent());
+
+ yaml = yaml.substring(0, last.getEndOffset()) + entityPrefix + entityString + yaml.substring(last.getEndOffset());
+
+ patchableItem.setStartOffset(last.getEndOffset() + entityPrefix.length());
+ patchableItem.setEndOffset(last.getEndOffset() + entityPrefix.length() + entityString.length());
+ patchableItem.setIndent(last.getIndent());
+ patchableItem.setContext(this);
+
+ patchables.add(patchables.indexOf(last)+1, patchableItem);
+
+ updatePatchableOffsetsAfter(patchableItem, patchableItem.getEndOffset(), entityString.length() + entityPrefix.length());
+ }
+
+ private void updatePatchableOffsetsAfter(PatchableYamlModel patchable, int endOffset, int delta) {
+ for(int i = patchables.indexOf(patchable) + 1; i < patchables.size(); i++) {
+ PatchableYamlModel successor = patchables.get(i);
+ if (successor.getStartOffset() >= endOffset) {
+ successor.setStartOffset(successor.getStartOffset()+delta);
+ }
+ successor.setEndOffset(successor.getEndOffset()+delta);
+ }
+ }
+
+ public boolean contains(PatchableYamlModel patchable) {
+ return patchables.contains(patchable);
+ }
+
+ public int ensureNextEndOfLineOffset(int offset) {
+ return Math.max(yaml.indexOf("\n", offset), offset);
+ }
+
+ public int ensurePreviousEndOfLineOffset(int offset) {
+ return Math.max(yaml.lastIndexOf("\n", offset), 0);
+ }
+
+ public void replaceContainerPatchable(PatchableYamlModel containerPatchable, PatchableYamlModel child, String containerPrefix) {
+ String childString = entityStringWithIndent(child, child.getIndent());
+ yaml = yaml.substring(0, containerPatchable.getStartOffset()) + containerPrefix + childString + yaml.substring(containerPatchable.getEndOffset());
+
+ containerPatchable.setEndOffset(containerPatchable.getStartOffset() + containerPrefix.length() + childString.length());
+ child.setStartOffset(containerPatchable.getStartOffset() + containerPrefix.length());
+ child.setEndOffset(containerPatchable.getEndOffset());
+
+ patchables.add(patchables.indexOf(containerPatchable), child);
+
+ int delta = containerPrefix.length() + childString.length() - (containerPatchable.getEndOffset() - containerPatchable.getStartOffset());
+ updatePatchableOffsetsAfter(containerPatchable, containerPatchable.getEndOffset(), delta);
+ }
+
+ public void appendEmptyPatchable(PatchableYamlModel patchable) {
+ patchable.setIndent(0);
+ yaml += "\n";
+ patchable.setStartOffset(yaml.length());
+ patchable.setEndOffset(yaml.length());
+ patchable.setIndent(0);
+ patchables.add(patchable);
+ yaml += "\n";
+ }
+}
diff --git a/step-core-model/src/main/java/step/core/yaml/deserialization/PatchingParserDelegate.java b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchingParserDelegate.java
new file mode 100644
index 0000000000..dedf45b116
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchingParserDelegate.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * 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.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 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 locationForToken = new HashMap<>();
+
+ private JsonLocation lastDistinctLocation;
+
+ protected final PatchingContext patchingContext;
+
+ public PatchingParserDelegate(JsonParser d, PatchingContext context) {
+
+ super(d);
+ patchingContext = context;
+ }
+
+ @Override
+ public JsonToken nextToken() throws IOException {
+ JsonLocation preLocation = currentLocation();
+ JsonToken token = super.nextToken();
+ if (!preLocation.equals(currentLocation())) {
+ lastDistinctLocation = preLocation;
+ }
+ locationForToken.put(token, preLocation);
+
+ return token;
+ }
+
+ protected JsonLocation getLastLocationForToken(JsonToken token) {
+ return locationForToken.get(token);
+ }
+
+ public JsonLocation getLastDistinctLocation() {
+ return lastDistinctLocation;
+ }
+
+ public PatchingContext getPatchingContext() {
+ return this.patchingContext;
+ }
+}
diff --git a/step-core-model/src/main/java/step/functions/Function.java b/step-core-model/src/main/java/step/functions/Function.java
index 0fe4ce5504..08981ec26d 100644
--- a/step-core-model/src/main/java/step/functions/Function.java
+++ b/step-core-model/src/main/java/step/functions/Function.java
@@ -43,10 +43,13 @@
@JsonTypeInfo(use = Id.CLASS, property = JSON_CLASS_FIELD)
public class Function extends AbstractOrganizableObject implements EnricheableObject, EvaluationExpression {
+
public final static String JSON_CLASS_FIELD = "type";
- protected DynamicValue callTimeout = new DynamicValue<>(180000);
- protected JsonObject schema = JsonProviderCache.createObjectBuilder().build();
+ public final static DynamicValue DEFAULT_CALL_TIMEOUT = new DynamicValue<>(180000);
+ protected DynamicValue callTimeout = DEFAULT_CALL_TIMEOUT;
+ public final static JsonObject DEFAULT_SCHEMA = JsonProviderCache.createObjectBuilder().build();
+ protected JsonObject schema = DEFAULT_SCHEMA;
protected boolean executeLocally;
protected Map tokenSelectionCriteria;
diff --git a/step-core-model/src/main/java/step/parameter/automation/AutomationPackageParameter.java b/step-core-model/src/main/java/step/parameter/automation/AutomationPackageParameter.java
index 50562e93a4..a07a308c81 100644
--- a/step-core-model/src/main/java/step/parameter/automation/AutomationPackageParameter.java
+++ b/step-core-model/src/main/java/step/parameter/automation/AutomationPackageParameter.java
@@ -18,16 +18,19 @@
******************************************************************************/
package step.parameter.automation;
+import com.fasterxml.jackson.annotation.*;
import step.commons.activation.Expression;
import step.core.dynamicbeans.DynamicValue;
-import step.core.yaml.AbstractYamlModel;
+import step.core.yaml.PatchableYamlModelBase;
import step.core.yaml.YamlFieldCustomCopy;
import step.core.yaml.YamlModel;
+import step.core.yaml.deserialization.PatchingContext;
import step.parameter.Parameter;
import step.parameter.ParameterScope;
@YamlModel(named = false)
-public class AutomationPackageParameter extends AbstractYamlModel {
+@JsonInclude(JsonInclude.Include.NON_DEFAULT)
+public class AutomationPackageParameter extends PatchableYamlModelBase {
protected String key;
protected DynamicValue value;
@@ -38,9 +41,24 @@ public class AutomationPackageParameter extends AbstractYamlModel {
protected Integer priority;
protected Boolean protectedValue = false;
+
+ @JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = ScopeFilter.class)
protected ParameterScope scope = ParameterScope.GLOBAL;
+
+ public static class ScopeFilter {
+ @Override
+ public boolean equals(Object obj) {
+ return obj == ParameterScope.GLOBAL;
+ }
+ }
+
protected String scopeEntity;
+ @JsonCreator
+ public AutomationPackageParameter(@JacksonInject(useInput = OptBoolean.FALSE) PatchingContext context) {
+ super(context);
+ }
+
public Parameter toParameter() {
Parameter res = new Parameter();
copyFieldsToObject(res, true);
@@ -81,4 +99,16 @@ public ParameterScope getScope() {
public String getScopeEntity() {
return scopeEntity;
}
+
+ public static AutomationPackageParameter forContext(PatchingContext context, Parameter parameter) {
+ AutomationPackageParameter yamlParameter = new AutomationPackageParameter(context);
+ yamlParameter.copyFieldsFromObject(parameter, true);
+ Expression expression = parameter.getActivationExpression();
+ if (expression == null) {
+ yamlParameter.activationScript = null;
+ } else {
+ yamlParameter.activationScript = expression.getScript();
+ }
+ return yamlParameter;
+ }
}
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..915b671095 100644
--- a/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java
+++ b/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java
@@ -61,8 +61,20 @@ public String getAutomationPackageName() {
abstract public boolean hasAutomationPackageDescriptor();
- abstract public InputStream getDescriptorYaml();
+ abstract public URL getDescriptorYamlUrl();
+ public InputStream getDescriptorYaml() {
+ URL url = getDescriptorYamlUrl();
+ if (url == null) {
+ return null;
+ }
+ try {
+ return url.openStream();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
abstract public InputStream getResourceAsStream(String resourcePath) throws IOException;
abstract public URL getResource(String resourcePath);
diff --git a/step-core/src/main/java/step/automation/packages/AutomationPackageHookRegistry.java b/step-core/src/main/java/step/automation/packages/AutomationPackageHookRegistry.java
index 7034fcbaee..32a6b29623 100644
--- a/step-core/src/main/java/step/automation/packages/AutomationPackageHookRegistry.java
+++ b/step-core/src/main/java/step/automation/packages/AutomationPackageHookRegistry.java
@@ -3,6 +3,7 @@
import step.core.AbstractStepContext;
import step.core.objectenricher.ObjectPredicate;
import step.core.repositories.ImportResult;
+import step.core.yaml.deserialization.PatchableYamlList;
import java.util.*;
@@ -25,7 +26,7 @@ public List getOrderedHookFieldNames() {
/**
* On reading the additional fields in yaml representation (additional data should be stored in AutomationPackageContent)
*/
- public boolean onAdditionalDataRead(String fieldName, List> yamlData, AutomationPackageContent targetContent) {
+ public boolean onAdditionalDataRead(String fieldName, PatchableYamlList> yamlData, AutomationPackageContent targetContent) {
AutomationPackageHook> hook = getHook(fieldName);
if (hook != null) {
hook.onAdditionalDataRead(fieldName, yamlData, targetContent);
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..651273088f 100644
--- a/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java
+++ b/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java
@@ -123,9 +123,9 @@ public boolean hasAutomationPackageDescriptor() {
}
@Override
- public InputStream getDescriptorYaml() {
+ public URL getDescriptorYamlUrl() {
for (String metadataFile : METADATA_FILES) {
- InputStream yamlDescriptor = classLoaderForMainApFile.getResourceAsStream(metadataFile);
+ URL yamlDescriptor = classLoaderForMainApFile.getResource(metadataFile);
if (yamlDescriptor != null) {
return yamlDescriptor;
}
diff --git a/step-core/src/main/java/step/automation/packages/ResourcePathMatchingResolver.java b/step-core/src/main/java/step/automation/packages/ResourcePathMatchingResolver.java
index 75765b5b0b..19553ff896 100644
--- a/step-core/src/main/java/step/automation/packages/ResourcePathMatchingResolver.java
+++ b/step-core/src/main/java/step/automation/packages/ResourcePathMatchingResolver.java
@@ -21,8 +21,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.net.*;
-import java.util.*;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
import java.util.regex.Pattern;
public class ResourcePathMatchingResolver {
@@ -37,11 +38,16 @@ public ResourcePathMatchingResolver(ClassLoader classLoader) {
public List getResourcesByPattern(String resourcePathPattern) {
List res = new ArrayList<>();
if (!containsWildcard(resourcePathPattern)) {
- res.add(classLoader.getResource(resourcePathPattern));
+ URL url = classLoader.getResource(resourcePathPattern);
+ if (url != null) {
+ res.add(url);
+ } else {
+ throw new IllegalArgumentException("Illegal resource definition, resource cannot be found: " + resourcePathPattern);
+ }
} else {
for (URL resource : findPathMatchingResources(resourcePathPattern)) {
if (logger.isDebugEnabled()) {
- logger.debug("Obtain resource from automation package: {}", resource);
+ logger.debug("Obtained resource from automation package: {}", resource);
}
res.add(resource);
}
@@ -62,7 +68,11 @@ protected List findPathMatchingResources(String locationPattern) {
throw new RuntimeException("Wildcards are currently not supported for the root element of the path: " + rootPath + ". You should put all the fragments into a folder and reference them as follow: myFolder/*");
} else {
URL resource = classLoader.getResource(rootPath);
- findPathMatchingResourcesRecursive(pathArray, 0, resource, result);
+ if (resource != null) {
+ findPathMatchingResourcesRecursive(pathArray, 0, resource, result);
+ } else {
+ throw new IllegalArgumentException("Illegal resource definition, resource cannot be found: " + locationPattern);
+ }
}
return result;
}
diff --git a/step-core/src/main/java/step/automation/packages/model/AbstractYamlFunction.java b/step-core/src/main/java/step/automation/packages/model/AbstractYamlFunction.java
index 3111a0880b..451275e59e 100644
--- a/step-core/src/main/java/step/automation/packages/model/AbstractYamlFunction.java
+++ b/step-core/src/main/java/step/automation/packages/model/AbstractYamlFunction.java
@@ -38,8 +38,8 @@ public abstract class AbstractYamlFunction extends AbstractY
@JsonSchema(defaultProvider = DefaultYamlFunctionNameProvider.class)
private String name;
- private DynamicValue callTimeout;
- private JsonObject schema;
+ private DynamicValue callTimeout = Function.DEFAULT_CALL_TIMEOUT;
+ private JsonObject schema = Function.DEFAULT_SCHEMA;
private boolean executeLocally;
@@ -120,6 +120,11 @@ public T applyAutomationPackageContext(StagingAutomationPackageContext context)
return res;
}
+ public void updateFromFunction(Function function) {
+ copyFieldsFromObject(function, false);
+ }
+
+
public static class DefaultYamlFunctionNameProvider implements JsonSchemaDefaultValueProvider {
public DefaultYamlFunctionNameProvider() {
diff --git a/step-core/src/main/java/step/automation/packages/model/YamlAutomationPackageKeyword.java b/step-core/src/main/java/step/automation/packages/model/YamlAutomationPackageKeyword.java
index 7cdbad74a4..ed114c6cb9 100644
--- a/step-core/src/main/java/step/automation/packages/model/YamlAutomationPackageKeyword.java
+++ b/step-core/src/main/java/step/automation/packages/model/YamlAutomationPackageKeyword.java
@@ -19,16 +19,23 @@
package step.automation.packages.model;
import step.automation.packages.StagingAutomationPackageContext;
+import step.core.yaml.PatchableYamlModelBase;
+import step.core.yaml.YamlModelUtils;
+import step.core.yaml.deserialization.PatchingContext;
import step.functions.Function;
-public class YamlAutomationPackageKeyword implements AutomationPackageKeyword {
+public class YamlAutomationPackageKeyword extends PatchableYamlModelBase implements AutomationPackageKeyword {
private AbstractYamlFunction> yamlKeyword;
- public YamlAutomationPackageKeyword(AbstractYamlFunction> yamlKeyword) {
+
+
+ public YamlAutomationPackageKeyword(AbstractYamlFunction> yamlKeyword, PatchingContext context) {
+ super(context);
this.yamlKeyword = yamlKeyword;
}
+
public AbstractYamlFunction> getYamlKeyword() {
return yamlKeyword;
}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageKeywordsLookuper.java b/step-core/src/main/java/step/core/yaml/AutomationPackageKeywordsLookuper.java
similarity index 96%
rename from step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageKeywordsLookuper.java
rename to step-core/src/main/java/step/core/yaml/AutomationPackageKeywordsLookuper.java
index 44a0ebdc46..65cea05448 100644
--- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageKeywordsLookuper.java
+++ b/step-core/src/main/java/step/core/yaml/AutomationPackageKeywordsLookuper.java
@@ -16,9 +16,8 @@
* You should have received a copy of the GNU Affero General Public License
* along with STEP. If not, see .
******************************************************************************/
-package step.automation.packages.yaml;
+package step.core.yaml;
-import step.core.yaml.YamlModelUtils;
import step.automation.packages.model.AbstractYamlFunction;
import java.util.List;
diff --git a/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializer.java b/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializer.java
index 5b1cd211ef..ef701ab617 100644
--- a/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializer.java
+++ b/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializer.java
@@ -18,35 +18,26 @@
******************************************************************************/
package step.core.yaml.deserializers;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.*;
-import com.fasterxml.jackson.databind.deser.BeanDeserializerFactory;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
-import com.fasterxml.jackson.databind.type.TypeFactory;
-import java.io.IOException;
+public abstract class StepYamlDeserializer extends JsonDeserializer implements ResolvableDeserializer {
-public abstract class StepYamlDeserializer extends JsonDeserializer {
+ protected final ObjectMapper yamlObjectMapper;
+ protected final JsonDeserializer> baseDeserializer;
- protected ObjectMapper yamlObjectMapper;
-
- public StepYamlDeserializer() {
- }
-
- public StepYamlDeserializer(ObjectMapper yamlObjectMapper) {
+ public StepYamlDeserializer(JsonDeserializer> deserializer, ObjectMapper yamlObjectMapper) {
+ this.baseDeserializer = deserializer;
this.yamlObjectMapper = yamlObjectMapper;
}
- protected JsonDeserializer getDefaultDeserializerForClass(JsonParser p, DeserializationContext ctxt, Class> clazz) throws IOException {
-
- DeserializationConfig config = ctxt.getConfig();
- JavaType type = TypeFactory.defaultInstance().constructType(clazz);
- JsonDeserializer defaultDeserializer = BeanDeserializerFactory.instance.buildBeanDeserializer(ctxt, type, config.introspect(type));
-
- if (defaultDeserializer instanceof ResolvableDeserializer) {
- ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
+ @Override
+ public void resolve(DeserializationContext ctxt) throws JsonMappingException {
+ if (baseDeserializer instanceof ResolvableDeserializer) {
+ ((ResolvableDeserializer) baseDeserializer).resolve(ctxt);
}
-
- return defaultDeserializer;
}
}
diff --git a/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializerAddOn.java b/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializerAddOn.java
index f13670a507..4bc3836f42 100644
--- a/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializerAddOn.java
+++ b/step-core/src/main/java/step/core/yaml/deserializers/StepYamlDeserializerAddOn.java
@@ -18,6 +18,9 @@
******************************************************************************/
package step.core.yaml.deserializers;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@@ -35,4 +38,5 @@
String LOCATION = "step";
Class>[] targetClasses();
+
}
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..0e71bfc417 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
@@ -18,16 +18,11 @@
******************************************************************************/
package step.core.yaml.deserializers;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import step.core.scanner.CachedAnnotationScanner;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.function.Consumer;
+import java.util.*;
public class StepYamlDeserializersScanner {
@@ -36,57 +31,20 @@ public class StepYamlDeserializersScanner {
/**
* Scans and returns all {@link StepYamlDeserializer} classes annotated with {@link StepYamlDeserializerAddOn}
*/
- public static List> scanDeserializerAddons(ObjectMapper yamlObjectMapper, List>> configurators) {
- List> result = new ArrayList<>();
+ public static Map, Class>> scanDeserializerAddons() {
+ Map, Class>> result = new HashMap<>();
List> annotatedClasses = new ArrayList<>(CachedAnnotationScanner.getClassesWithAnnotation(StepYamlDeserializerAddOn.LOCATION, StepYamlDeserializerAddOn.class, Thread.currentThread().getContextClassLoader()));
for (Class> annotatedClass : annotatedClasses) {
- if (StepYamlDeserializer.class.isAssignableFrom(annotatedClass)) {
- StepYamlDeserializerAddOn annotation = annotatedClass.getAnnotation(StepYamlDeserializerAddOn.class);
- 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);
- }
- });
- }
+ StepYamlDeserializerAddOn annotation = annotatedClass.getAnnotation(StepYamlDeserializerAddOn.class);
+ Arrays.stream(annotation.targetClasses()).forEach(aClass -> {
+ try {
+ result.put(aClass, annotatedClass);
+ } catch (Exception e) {
+ throw new RuntimeException("Cannot prepare deserializer", e);
+ }
+ });
}
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)) {
- res = module.addDeserializer((Class) deser.clazz, deser.deserializer);
- }
- return res;
- }
-
- public static class DeserializerBind {
- public Class clazz;
- public StepYamlDeserializer deserializer;
-
- public DeserializerBind(Class clazz, StepYamlDeserializer deserializer) {
- this.clazz = clazz;
- this.deserializer = deserializer;
- }
- }
}
diff --git a/step-core/src/main/java/step/core/yaml/deserializers/YamlDynamicValueDeserializer.java b/step-core/src/main/java/step/core/yaml/deserializers/YamlDynamicValueDeserializer.java
index c28309fd7d..4becc05c2c 100644
--- a/step-core/src/main/java/step/core/yaml/deserializers/YamlDynamicValueDeserializer.java
+++ b/step-core/src/main/java/step/core/yaml/deserializers/YamlDynamicValueDeserializer.java
@@ -33,17 +33,14 @@ public class YamlDynamicValueDeserializer extends StepYamlDeserializer deserializer, ObjectMapper yamlObjectMapper) {
+ super(deserializer, yamlObjectMapper);
}
@Override
public JsonDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
this.type = property.getType().containedType(0);
- YamlDynamicValueDeserializer deserializer = new YamlDynamicValueDeserializer();
+ YamlDynamicValueDeserializer deserializer = new YamlDynamicValueDeserializer(baseDeserializer, yamlObjectMapper);
deserializer.type = type;
return deserializer;
}
diff --git a/step-core/src/main/java/step/core/yaml/deserializers/YamlProtectedDynamicValueDeserializer.java b/step-core/src/main/java/step/core/yaml/deserializers/YamlProtectedDynamicValueDeserializer.java
index 7aa9134ad1..d7e06ea128 100644
--- a/step-core/src/main/java/step/core/yaml/deserializers/YamlProtectedDynamicValueDeserializer.java
+++ b/step-core/src/main/java/step/core/yaml/deserializers/YamlProtectedDynamicValueDeserializer.java
@@ -25,17 +25,14 @@
@StepYamlDeserializerAddOn(targetClasses = {ProtectedDynamicValue.class})
public class YamlProtectedDynamicValueDeserializer extends YamlDynamicValueDeserializer {
- public YamlProtectedDynamicValueDeserializer() {
- }
-
- public YamlProtectedDynamicValueDeserializer(ObjectMapper yamlObjectMapper) {
- super(yamlObjectMapper);
+ public YamlProtectedDynamicValueDeserializer(JsonDeserializer> deserializer, ObjectMapper yamlObjectMapper) {
+ super(deserializer, yamlObjectMapper);
}
@Override
public JsonDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
this.type = property.getType().containedType(0);
- YamlProtectedDynamicValueDeserializer deserializer = new YamlProtectedDynamicValueDeserializer();
+ YamlProtectedDynamicValueDeserializer deserializer = new YamlProtectedDynamicValueDeserializer(baseDeserializer, yamlObjectMapper);
deserializer.type = type;
return deserializer;
}
diff --git a/step-core/src/main/java/step/core/yaml/serializers/StepYamlSerializer.java b/step-core/src/main/java/step/core/yaml/serializers/StepYamlSerializer.java
index e96ea52f4d..7d2520d975 100644
--- a/step-core/src/main/java/step/core/yaml/serializers/StepYamlSerializer.java
+++ b/step-core/src/main/java/step/core/yaml/serializers/StepYamlSerializer.java
@@ -33,9 +33,6 @@ public abstract class StepYamlSerializer extends JsonSerializer {
protected ObjectMapper yamlObjectMapper;
- public StepYamlSerializer() {
- }
-
public StepYamlSerializer(ObjectMapper yamlObjectMapper) {
this.yamlObjectMapper = yamlObjectMapper;
}
diff --git a/step-core/src/main/java/step/core/yaml/serializers/YamlDynamicValueSerializer.java b/step-core/src/main/java/step/core/yaml/serializers/YamlDynamicValueSerializer.java
index be8cf54a01..7725f13cd1 100644
--- a/step-core/src/main/java/step/core/yaml/serializers/YamlDynamicValueSerializer.java
+++ b/step-core/src/main/java/step/core/yaml/serializers/YamlDynamicValueSerializer.java
@@ -29,9 +29,6 @@
@StepYamlSerializerAddOn(targetClasses = {DynamicValue.class})
public class YamlDynamicValueSerializer extends StepYamlSerializer> {
- public YamlDynamicValueSerializer() {
- }
-
public YamlDynamicValueSerializer(ObjectMapper yamlObjectMapper) {
super(yamlObjectMapper);
}
diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/YamlDynamicInputDeserializer.java b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/YamlDynamicInputDeserializer.java
index 6feeaec254..244617c31f 100644
--- a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/YamlDynamicInputDeserializer.java
+++ b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/YamlDynamicInputDeserializer.java
@@ -22,6 +22,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -42,8 +43,8 @@ public class YamlDynamicInputDeserializer extends StepYamlDeserializer deserializer, ObjectMapper yamlObjectMapper) {
+ super(deserializer, yamlObjectMapper);
}
/**
diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/YamlKeywordDefinitionDeserializer.java b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/YamlKeywordDefinitionDeserializer.java
index 57987fb49b..25ed34140f 100644
--- a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/YamlKeywordDefinitionDeserializer.java
+++ b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/YamlKeywordDefinitionDeserializer.java
@@ -20,6 +20,7 @@
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import step.core.accessors.AbstractOrganizableObject;
@@ -39,8 +40,8 @@ public class YamlKeywordDefinitionDeserializer extends StepYamlDeserializer deserializer, ObjectMapper yamlObjectMapper) {
+ super(deserializer, yamlObjectMapper);
}
@Override
diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/datasource/NamedYamlDataSourceDeserializer.java b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/datasource/NamedYamlDataSourceDeserializer.java
index 3b375e5065..2d022bd452 100644
--- a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/datasource/NamedYamlDataSourceDeserializer.java
+++ b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/datasource/NamedYamlDataSourceDeserializer.java
@@ -20,6 +20,7 @@
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import step.core.yaml.deserializers.NamedEntityYamlDeserializer;
@@ -31,8 +32,8 @@
@StepYamlDeserializerAddOn(targetClasses = {NamedYamlDataSource.class})
public class NamedYamlDataSourceDeserializer extends StepYamlDeserializer {
- public NamedYamlDataSourceDeserializer(ObjectMapper yamlObjectMapper) {
- super(yamlObjectMapper);
+ public NamedYamlDataSourceDeserializer(JsonDeserializer> deserializer, ObjectMapper yamlObjectMapper) {
+ super(deserializer, yamlObjectMapper);
}
@Override
diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/datasource/NamedYamlDataSourceSerializer.java b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/datasource/NamedYamlDataSourceSerializer.java
index 461cc2bea5..b235e2dffb 100644
--- a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/datasource/NamedYamlDataSourceSerializer.java
+++ b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/automation/datasource/NamedYamlDataSourceSerializer.java
@@ -37,9 +37,6 @@ public class NamedYamlDataSourceSerializer extends StepYamlSerializer .
+ ******************************************************************************/
+package step.plans.parser.yaml;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import step.core.yaml.deserialization.PatchingContext;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder(VersionedYamlPlan.VERSION_FIELD_NAME)
+public class VersionedYamlPlan extends YamlPlan {
+
+ // this name should be kept untouched to support the migrations for old versions
+ public static final String VERSION_FIELD_NAME = "version";
+
+ private String version;
+
+ public VersionedYamlPlan(PatchingContext context, String version) {
+
+ super(context);
+ this.version = version;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+}
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..25f0b1d250 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,23 +18,26 @@
******************************************************************************/
package step.plans.parser.yaml;
-import step.core.yaml.model.NamedYamlArtefact;
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.OptBoolean;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import step.core.plans.agents.configuration.AgentProvisioningConfiguration;
import step.core.plans.agents.configuration.AgentProvisioningConfigurationDeserializer;
import step.core.plans.agents.configuration.AgentProvisioningConfigurationSerializer;
-import step.core.plans.agents.configuration.AgentProvisioningConfiguration;
+import step.core.yaml.PatchableYamlModelBase;
+import step.core.yaml.deserialization.PatchingContext;
+import step.core.yaml.model.NamedYamlArtefact;
import java.util.List;
-public class YamlPlan {
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class YamlPlan extends PatchableYamlModelBase {
public static final String PLANS_ENTITY_NAME = "plans";
- // this name should be kept untouched to support the migrations for old versions
- public static final String VERSION_FIELD_NAME = "version";
-
- private String version;
private String name;
private NamedYamlArtefact root;
@@ -45,6 +48,11 @@ public class YamlPlan {
private List categories;
+ @JsonCreator
+ public YamlPlan(@JacksonInject(useInput = OptBoolean.FALSE, optional = OptBoolean.TRUE) PatchingContext context) {
+ super(context);
+ }
+
public String getName() {
return name;
}
@@ -61,14 +69,6 @@ public void setRoot(NamedYamlArtefact root) {
this.root = root;
}
- public String getVersion() {
- return version;
- }
-
- public void setVersion(String version) {
- this.version = version;
- }
-
public AgentProvisioningConfiguration getAgents() {
return agents;
}
@@ -81,7 +81,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-base-artefacts/src/main/java/step/plugins/functions/types/automation/YamlCompositeFunction.java b/step-plans/step-plans-base-artefacts/src/main/java/step/plugins/functions/types/automation/YamlCompositeFunction.java
index 28beecf7da..de1a5f281d 100644
--- a/step-plans/step-plans-base-artefacts/src/main/java/step/plugins/functions/types/automation/YamlCompositeFunction.java
+++ b/step-plans/step-plans-base-artefacts/src/main/java/step/plugins/functions/types/automation/YamlCompositeFunction.java
@@ -18,13 +18,18 @@
******************************************************************************/
package step.plugins.functions.types.automation;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.ObjectMapper;
import step.automation.packages.StagingAutomationPackageContext;
import step.automation.packages.model.AbstractYamlFunction;
import step.core.accessors.AbstractOrganizableObject;
import step.core.plans.Plan;
import step.core.yaml.YamlFieldCustomCopy;
import step.core.yaml.YamlModel;
+import step.core.yaml.model.AbstractYamlArtefact;
+import step.core.yaml.model.NamedYamlArtefact;
import step.core.yaml.schema.YamlJsonSchemaHelper;
+import step.functions.Function;
import step.jsonschema.JsonSchema;
import step.plans.parser.yaml.YamlPlan;
import step.plugins.functions.types.CompositeFunction;
@@ -33,6 +38,7 @@
import java.util.Objects;
@YamlModel(name = "Composite")
+@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class YamlCompositeFunction extends AbstractYamlFunction {
@YamlFieldCustomCopy
@@ -62,7 +68,23 @@ protected void fillDeclaredFields(CompositeFunction res, StagingAutomationPackag
}
}
- public Plan yamlPlanToPlan(YamlPlan yamlPlan) {
+ @Override
+ public void updateFromFunction(Function function) {
+ copyFieldsFromObject(function, false);
+
+ if (function instanceof CompositeFunction) {
+ Plan plan = ((CompositeFunction) function).getPlan();
+ // plan name is optional, the composite function name is used by default
+ if (this.plan.getName() != null && !this.plan.getName().isEmpty()) {
+ this.plan.setName(plan.getAttribute(AbstractOrganizableObject.NAME));;
+ }
+ ObjectMapper mapper = this.plan.getRoot().getYamlArtefact().getYamlObjectMapper();
+ this.plan.setRoot(new NamedYamlArtefact(AbstractYamlArtefact.toYamlArtefact(plan.getRoot(), mapper)));
+ }
+ }
+
+
+ private Plan yamlPlanToPlan(YamlPlan yamlPlan) {
Plan plan = new Plan(yamlPlan.getRoot().getYamlArtefact().toArtefact());
// plan name is optional, the composite function name is used by default
diff --git a/step-plans/step-plans-core/src/main/java/step/core/scheduler/automation/AutomationPackageSchedule.java b/step-plans/step-plans-core/src/main/java/step/core/scheduler/automation/AutomationPackageSchedule.java
index 050c380832..0a3b4fc65f 100644
--- a/step-plans/step-plans-core/src/main/java/step/core/scheduler/automation/AutomationPackageSchedule.java
+++ b/step-plans/step-plans-core/src/main/java/step/core/scheduler/automation/AutomationPackageSchedule.java
@@ -18,10 +18,16 @@
******************************************************************************/
package step.core.scheduler.automation;
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.OptBoolean;
+import step.core.yaml.PatchableYamlModelBase;
+import step.core.yaml.deserialization.PatchingContext;
+
import java.util.Map;
import java.util.List;
-public class AutomationPackageSchedule {
+public class AutomationPackageSchedule extends PatchableYamlModelBase {
public static final String SCHEDULE_DEF = "ScheduleDef";
public static final String FIELD_NAME_IN_AP = "schedules";
@@ -34,10 +40,13 @@ public class AutomationPackageSchedule {
private String assertionPlanName;
private Map executionParameters;
- public AutomationPackageSchedule() {
+ @JsonCreator
+ public AutomationPackageSchedule(@JacksonInject(useInput = OptBoolean.FALSE) PatchingContext patchingContext) {
+ super(patchingContext);
}
public AutomationPackageSchedule(String name, String cron, String planName, Map executionParameters) {
+ super(new PatchingContext());
this.name = name;
this.cron = cron;
this.planName = planName;
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..f9bfe54608 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,16 @@
******************************************************************************/
package step.plans.automation;
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.OptBoolean;
+import step.core.yaml.PatchableYamlModelBase;
+import step.core.yaml.deserialization.PatchingContext;
import step.plans.nl.RootArtefactType;
import java.util.List;
-public class YamlPlainTextPlan {
+public class YamlPlainTextPlan extends PatchableYamlModelBase {
private String name;
@@ -32,6 +37,11 @@ public class YamlPlainTextPlan {
private String file;
+ @JsonCreator
+ public YamlPlainTextPlan(@JacksonInject(useInput = OptBoolean.FALSE, optional = OptBoolean.TRUE) PatchingContext context) {
+ super(context);
+ }
+
public String getName() {
return 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..d88f06da38 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
@@ -20,11 +20,17 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.DeserializationConfig;
+import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.deser.BeanDeserializer;
+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.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import step.core.Version;
@@ -36,6 +42,12 @@
import step.core.plans.agents.configuration.AutomaticAgentProvisioningConfiguration;
import step.core.scanner.AnnotationScanner;
import step.core.scanner.CachedAnnotationScanner;
+import step.core.yaml.PatchableYamlModel;
+import step.core.yaml.deserialization.PatchableYamlList;
+import step.core.yaml.deserialization.PatchableYamlListDeserializer;
+import step.core.yaml.deserialization.PatchableYamlModelDeserializer;
+import step.core.yaml.deserialization.PatchingContext;
+import step.core.yaml.deserializers.StepYamlDeserializer;
import step.core.yaml.deserializers.StepYamlDeserializersScanner;
import step.core.yaml.serializers.StepYamlSerializersScanner;
import step.migration.MigrationManager;
@@ -53,6 +65,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
@@ -155,7 +168,7 @@ public Plan readYamlPlan(InputStream yamlPlanStream) throws IOException, YamlPla
* Writes the plan as YAML
*/
public void writeYamlPlan(OutputStream os, Plan plan) throws IOException {
- yamlMapper.writeValue(os, planToYamlPlan(plan));
+ yamlMapper.writeValue(os, planToVersionedYamlPlan(plan));
}
public void convertFromPlainTextToYaml(String planName, InputStream planTextInputStream, OutputStream yamlOutputStream) throws IOException, StepsParser.ParsingException {
@@ -195,7 +208,7 @@ protected ObjectMapper createYamlPlanObjectMapper() {
yamlMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
// configure custom deserializers
- yamlMapper.registerModule(registerAllSerializersAndDeserializers(new SimpleModule(), yamlMapper, true));
+ yamlMapper.registerModule(registerAllSerializersAndDeserializers(yamlMapper, true));
return yamlMapper;
}
@@ -206,23 +219,68 @@ public static ObjectMapper createDefaultYamlMapper() {
return DefaultJacksonMapperProvider.getObjectMapper(yamlFactory);
}
- private SimpleModule registerBasicSerializersAndDeserializers(SimpleModule module, ObjectMapper resultingMapper) {
- SimpleModule res = StepYamlDeserializersScanner.addAllDeserializerAddonsToModule(module, resultingMapper);
- res = StepYamlSerializersScanner.addAllSerializerAddonsToModule(res, resultingMapper);
- return res;
- }
-
- public SimpleModule registerAllSerializersAndDeserializers(SimpleModule module, ObjectMapper resultingMapper, boolean upgradablePlan) {
+ public SimpleModule registerAllSerializersAndDeserializers(ObjectMapper resultingMapper, boolean upgradablePlan) {
ObjectMapper nonUpgradableYamlMapper = createDefaultYamlMapper().registerModule(createModuleForNonUpgradablePlans(resultingMapper));
+ // configure custom deserializers
- return registerBasicSerializersAndDeserializers(module, resultingMapper)
- .addDeserializer(YamlPlan.class, new UpgradableYamlPlanDeserializer(upgradablePlan ? currentVersion : null, jsonSchema, migrationManager, nonUpgradableYamlMapper));
+ Map, Class>> deserializers = StepYamlDeserializersScanner.scanDeserializerAddons();
+
+
+ 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 (YamlPlan.class == beanDesc.getBeanClass()) {
+ deserializer = new UpgradableYamlPlanDeserializer(upgradablePlan ? currentVersion : null, jsonSchema, migrationManager, nonUpgradableYamlMapper, deserializer);
+ }
+
+ if (deserializers.containsKey(beanDesc.getBeanClass())) {
+ try {
+ Class> deserializerClass = deserializers.get(beanDesc.getBeanClass());
+ if (StepYamlDeserializer.class.isAssignableFrom(deserializerClass)) {
+ deserializer = (JsonDeserializer>) deserializerClass.getConstructor(JsonDeserializer.class, ObjectMapper.class).newInstance(deserializer, resultingMapper);
+ } else if (BeanDeserializer.class.isAssignableFrom(deserializerClass) && BeanDeserializer.class.isAssignableFrom(deserializer.getClass())) {
+ deserializer = (JsonDeserializer>) deserializerClass.getConstructor(BeanDeserializer.class).newInstance((BeanDeserializer) deserializer);
+ }
+ } catch (InstantiationException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ if (PatchableYamlModel.class.isAssignableFrom(beanDesc.getBeanClass())
+ && !beanDesc.getBeanClass().equals(PatchableYamlModel.class)) {
+ return new PatchableYamlModelDeserializer<>(deserializer);
+ }
+ return super.modifyDeserializer(config, beanDesc, deserializer);
+ }
+
+ @Override
+ public JsonDeserializer> modifyCollectionDeserializer(DeserializationConfig config, CollectionType type, BeanDescription beanDesc, JsonDeserializer> deserializer) {
+ if (deserializer instanceof CollectionDeserializer && beanDesc.getBeanClass().equals(PatchableYamlList.class)) {
+ return new PatchableYamlListDeserializer((CollectionDeserializer) deserializer);
+ }
+ return deserializer;
+ }
+ });
+ }
+ };
+ return StepYamlSerializersScanner.addAllSerializerAddonsToModule(module, resultingMapper);
}
private SimpleModule createModuleForNonUpgradablePlans(ObjectMapper resultingMapper) {
SimpleModule module = new SimpleModule();
- registerBasicSerializersAndDeserializers(module, resultingMapper);
- return module;
+ return StepYamlSerializersScanner.addAllSerializerAddonsToModule(module, resultingMapper);
}
protected ObjectMapper getYamlMapper() {
@@ -271,10 +329,20 @@ public Plan yamlPlanToPlan(YamlPlan yamlPlan) {
return plan;
}
- protected YamlPlan planToYamlPlan(Plan plan) {
- YamlPlan yamlPlan = new YamlPlan();
+ public VersionedYamlPlan planToVersionedYamlPlan(Plan plan) {
+ VersionedYamlPlan yamlPlan = new VersionedYamlPlan(new PatchingContext("", yamlMapper), currentVersion.toString());
+ setYamlPlanFieldsFromPlan(yamlPlan, plan);
+ return yamlPlan;
+ }
+
+ public YamlPlan planToYamlPlan(Plan plan) {
+ YamlPlan yamlPlan = new YamlPlan(new PatchingContext("", yamlMapper));
+ setYamlPlanFieldsFromPlan(yamlPlan, plan);
+ return yamlPlan;
+ }
+
+ private void setYamlPlanFieldsFromPlan(YamlPlan yamlPlan, Plan plan) {
yamlPlan.setName(plan.getAttribute(AbstractOrganizableObject.NAME));
- yamlPlan.setVersion(currentVersion.toString());
yamlPlan.setCategories(plan.getCategories());
yamlPlan.setRoot(new NamedYamlArtefact(AbstractYamlArtefact.toYamlArtefact(plan.getRoot(), yamlMapper)));
AgentProvisioningConfiguration agents = plan.getAgents();
@@ -283,7 +351,6 @@ protected YamlPlan planToYamlPlan(Plan plan) {
!((AutomaticAgentProvisioningConfiguration) agents).mode.equals(AutomaticAgentProvisioningConfiguration.PlanAgentsPoolAutoMode.auto_detect)) {
yamlPlan.setAgents(plan.getAgents());
}
- return yamlPlan;
}
}
diff --git a/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/deserializers/NamedYamlArtefactDeserializer.java b/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/deserializers/NamedYamlArtefactDeserializer.java
index e81194cc42..e696f9275c 100644
--- a/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/deserializers/NamedYamlArtefactDeserializer.java
+++ b/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/deserializers/NamedYamlArtefactDeserializer.java
@@ -19,9 +19,7 @@
package step.plans.parser.yaml.deserializers;
import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.ObjectNode;
import step.core.artefacts.AbstractArtefact;
import step.core.yaml.deserializers.NamedEntityYamlDeserializer;
@@ -39,12 +37,9 @@
@StepYamlDeserializerAddOn(targetClasses = {NamedYamlArtefact.class})
public class NamedYamlArtefactDeserializer extends StepYamlDeserializer {
- public NamedYamlArtefactDeserializer() {
- this(null);
- }
- public NamedYamlArtefactDeserializer(ObjectMapper stepYamlObjectMapper) {
- super(stepYamlObjectMapper);
+ public NamedYamlArtefactDeserializer(JsonDeserializer> deserializer, ObjectMapper yamlObjectMapper) {
+ super(deserializer, yamlObjectMapper);
}
@Override
diff --git a/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/deserializers/UpgradableYamlPlanDeserializer.java b/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/deserializers/UpgradableYamlPlanDeserializer.java
index 06d9735d80..1f2bcaa6d4 100644
--- a/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/deserializers/UpgradableYamlPlanDeserializer.java
+++ b/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/deserializers/UpgradableYamlPlanDeserializer.java
@@ -19,10 +19,8 @@
package step.plans.parser.yaml.deserializers;
import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
import org.everit.json.schema.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,6 +33,7 @@
import step.core.collections.Filters;
import step.core.collections.inmemory.InMemoryCollectionFactory;
import step.migration.MigrationManager;
+import step.plans.parser.yaml.VersionedYamlPlan;
import step.plans.parser.yaml.YamlPlan;
import step.plans.parser.yaml.schema.YamlPlanValidationException;
@@ -43,7 +42,7 @@
import static step.plans.parser.yaml.migrations.AbstractYamlPlanMigrationTask.YAML_PLANS_COLLECTION_NAME;
-public class UpgradableYamlPlanDeserializer extends JsonDeserializer {
+public class UpgradableYamlPlanDeserializer extends JsonDeserializer implements ResolvableDeserializer {
private static final Logger log = LoggerFactory.getLogger(UpgradableYamlPlanDeserializer.class);
private final Version currentVersion;
@@ -51,11 +50,14 @@ public class UpgradableYamlPlanDeserializer extends JsonDeserializer {
private final ObjectMapper yamlMapper;
private final String jsonSchema;
- public UpgradableYamlPlanDeserializer(Version currentVersion, String jsonSchema, MigrationManager migrationManager, ObjectMapper nonUpgradableYamlMapper) {
+ private JsonDeserializer> delegate;
+
+ public UpgradableYamlPlanDeserializer(Version currentVersion, String jsonSchema, MigrationManager migrationManager, ObjectMapper nonUpgradableYamlMapper, JsonDeserializer> delegate) {
this.currentVersion = currentVersion;
this.jsonSchema = jsonSchema;
this.migrationManager = migrationManager;
this.yamlMapper = nonUpgradableYamlMapper;
+ this.delegate = delegate;
}
@Override
@@ -64,7 +66,7 @@ public YamlPlan deserialize(JsonParser p, DeserializationContext ctxt) throws IO
if (currentVersion != null) {
Document yamlPlanDocument = p.getCodec().treeToValue(planJsonNode, Document.class);
- String planVersionString = yamlPlanDocument.getString(YamlPlan.VERSION_FIELD_NAME);
+ String planVersionString = yamlPlanDocument.getString(VersionedYamlPlan.VERSION_FIELD_NAME);
if (planVersionString == null) {
planVersionString = (String) ctxt.getAttribute("version");
@@ -88,7 +90,7 @@ public YamlPlan deserialize(JsonParser p, DeserializationContext ctxt) throws IO
Document migratedDocument = tempCollection.find(Filters.id(planDocument.getId()), null, null, null, 0).findFirst().orElseThrow();
// set actual version
- migratedDocument.replace(YamlPlan.VERSION_FIELD_NAME, currentVersion.toString());
+ migratedDocument.replace(VersionedYamlPlan.VERSION_FIELD_NAME, currentVersion.toString());
// remove automatically generated document id
migratedDocument.remove(AbstractIdentifiableObject.ID);
@@ -99,8 +101,7 @@ public YamlPlan deserialize(JsonParser p, DeserializationContext ctxt) throws IO
if (log.isDebugEnabled()) {
log.debug("Yaml plan after migrations: {}", bufferedYamlPlan);
}
-
- planJsonNode = yamlMapper.readTree(bufferedYamlPlan);
+ planJsonNode = yamlMapper.valueToTree(migratedDocument);
}
}
}
@@ -115,7 +116,16 @@ public YamlPlan deserialize(JsonParser p, DeserializationContext ctxt) throws IO
}
}
- return yamlMapper.treeToValue(planJsonNode, YamlPlan.class);
+ p = planJsonNode.traverse(p.getCodec());
+ p.nextToken();
+
+ return (YamlPlan) delegate.deserialize(p, ctxt);
}
+ @Override
+ public void resolve(DeserializationContext ctxt) throws JsonMappingException {
+ if (delegate instanceof ResolvableDeserializer) {
+ ((ResolvableDeserializer) delegate).resolve(ctxt);
+ }
+ }
}
diff --git a/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/serializers/NamedYamlArtefactSerializer.java b/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/serializers/NamedYamlArtefactSerializer.java
index 7384b23d5f..3927c0c824 100644
--- a/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/serializers/NamedYamlArtefactSerializer.java
+++ b/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/serializers/NamedYamlArtefactSerializer.java
@@ -47,10 +47,6 @@ public class NamedYamlArtefactSerializer extends StepYamlSerializer {
- public YamlResourceReferenceDeserializer(ObjectMapper yamlObjectMapper) {
- super(yamlObjectMapper);
+ public YamlResourceReferenceDeserializer(JsonDeserializer> deserializer, ObjectMapper yamlObjectMapper) {
+ super(deserializer, yamlObjectMapper);
}
@Override