diff --git a/step-ap-ide/src/main/java/step/ap_ide/StepUp.java b/step-ap-ide/src/main/java/step/ap_ide/StepUp.java
index 938961b7f2..709d01912d 100644
--- a/step-ap-ide/src/main/java/step/ap_ide/StepUp.java
+++ b/step-ap-ide/src/main/java/step/ap_ide/StepUp.java
@@ -21,7 +21,6 @@
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
-import java.nio.file.Paths;
import java.util.Objects;
import java.util.Properties;
@@ -40,7 +39,7 @@ public static void main(String[] args) throws Exception {
configuration.getUnderlyingPropertyObject().load(propsStream);
new ControllerServer(configuration).start();
initWorkdir();
- FXApp.main(args); // this will never return
+ //FXApp.main(args); // this will never return*/
}
private static final JavaAutomationPackageReader READER;
@@ -79,18 +78,18 @@ static void useAutomationPackageDirectory(File apDir) throws Exception {
var fragmentManager = StepUp.READER.getAutomationPackageYamlFragmentManager(apDir);
Properties properties = new Properties();
+ int variant = 1;
// variant 1:
// parameters all go into parameters.yml, plans go into separate files in plans/$PLAN_NAME.yml
- // Only works if the target files/directories already exist, so disabled for now
- if (1 == 0) {
+ // this is because parameters do not have unique names by design.
+ if (variant == 1) {
properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, Parameter.ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name());
properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, Parameter.ENTITY_NAME), "parameters.yml");
- properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, YamlPlan.PLANS_ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.PER_OBJECT.name());
- // keywords seem to use PER_OBJECT by default?
+ // per default, PER_OBJECT is used on all objects?
}
// variant 2: simple, everything goes into main descriptor
- if (1 == 1) {
- String mainFile = Paths.get(fragmentManager.descriptorYaml.getFragmentUrl().toURI()).toFile().getName();
+ if (variant == 2) {
+ String mainFile = fragmentManager.descriptorYaml.getFragmentPath().toFile().getName();
properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, Parameter.ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name());
properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, Parameter.ENTITY_NAME), mainFile);
properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, YamlPlan.PLANS_ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name());
diff --git a/step-ap-ide/src/main/resources/work-initial/jmeterProject1/jmeterProject1.xml b/step-ap-ide/src/main/resources/work-initial/jmeterProject1/jmeterProject1.xml
new file mode 100644
index 0000000000..3ed06774ee
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/jmeterProject1/jmeterProject1.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/step-ap-ide/src/main/resources/work-initial/jsProject/jsSample.js b/step-ap-ide/src/main/resources/work-initial/jsProject/jsSample.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/step-ap-ide/src/main/resources/work-initial/lib/fakeLib.jar b/step-ap-ide/src/main/resources/work-initial/lib/fakeLib.jar
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/step-ap-ide/src/main/resources/work-initial/nodeProject/nodeSample.ts b/step-ap-ide/src/main/resources/work-initial/nodeProject/nodeSample.ts
new file mode 100644
index 0000000000..e69de29bb2
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
index 813aad256b..aab8c3d53d 100644
--- 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
@@ -29,7 +29,7 @@ public class AutomationPackageFunctionCollection extends InMemoryCollection AutomationPackageParameter.forContext(context, parameter), Parameter.ENTITY_NAME));
+ return fragmentManager.saveAdditionalFieldObject(super.save(parameter), AutomationPackageParameter.fromParameter(parameter), Parameter.ENTITY_NAME);
}
@Override
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
index a68b9488a6..610e88cf8a 100644
--- 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
@@ -29,7 +29,7 @@ public class AutomationPackagePlanCollection extends InMemoryCollection im
private final AutomationPackageYamlFragmentManager fragmentManager;
public AutomationPackagePlanCollection(AutomationPackageYamlFragmentManager fragmentManager) {
- super(true, YamlPlan.PLANS_ENTITY_NAME);
+ super(false, YamlPlan.PLANS_ENTITY_NAME);
this.fragmentManager = fragmentManager;
initialzeRecordsFromFragments(fragmentManager);
}
@@ -41,7 +41,7 @@ private void initialzeRecordsFromFragments(AutomationPackageYamlFragmentManager
@Override
public Plan save(Plan p) {
- return super.save(fragmentManager.savePlan(p));
+ return fragmentManager.savePlan(super.save(p));
}
@Override
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
index 3ed25186ed..9d3a1560e6 100644
--- 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
@@ -84,9 +84,13 @@ protected void assertFilesEqual(Path expected, Path actual) throws IOException {
}
protected void setPropertiesWriteToFragment(String entityName, String fragment) {
+ setPropertiesWriteMode(entityName, fragment, AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT);
+ }
+
+ protected void setPropertiesWriteMode(String entityName, String path, AutomationPackageYamlFragmentManager.NewObjectFragmentMode mode) {
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());
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, entityName), path);
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, entityName), mode.name());
fragmentManager.setProperties(properties);
}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageFragmentReferenceTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageFragmentReferenceTest.java
new file mode 100644
index 0000000000..e70d95fa6a
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageFragmentReferenceTest.java
@@ -0,0 +1,259 @@
+/*******************************************************************************
+ * 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.automation.packages.model.YamlAutomationPackageKeyword;
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.core.artefacts.AbstractArtefact;
+import step.core.dynamicbeans.DynamicValue;
+import step.core.plans.Plan;
+import step.functions.Function;
+import step.plans.parser.yaml.YamlPlan;
+import step.plugins.functions.types.CompositeFunction;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.junit.Assert.assertFalse;
+
+public class AutomationPackageFragmentReferenceTest extends AutomationPackageCollectionTestBase {
+
+ private Collection functionCollection;
+ private Collection planCollection;
+
+ public AutomationPackageFragmentReferenceTest() {
+ 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);
+ functionCollection = collectionFactory.getCollection(YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME, Function.class);
+ }
+
+
+ @Test
+ public void testAddCompositeKeywordToNewFragmentAndRenameAndRemove() throws IOException {
+
+ Sequence sequence = new Sequence();
+ Echo echo = new Echo();
+ echo.setText(new DynamicValue<>("Hello World"));
+ sequence.addChild(echo);
+
+ Plan plan = new Plan(sequence);
+
+ CompositeFunction function = new CompositeFunction();
+ function.setPlan(plan);
+
+ Map attributes = new HashMap<>();
+ attributes.put(AbstractArtefact.NAME, "Hello World Composite Function");
+ function.setAttributes(attributes);
+
+ setPropertiesWriteMode(YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME, "newKeywordsPath", AutomationPackageYamlFragmentManager.NewObjectFragmentMode.PER_OBJECT);
+
+ functionCollection.save(function);
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("Hello World Composite Function.yml"),
+ destinationDirectory.toPath()
+ .resolve("newKeywordsPath")
+ .resolve("Hello World Composite Function.yml")
+ );
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("descriptorAfterNewKeywordsFragmentReference.yml"),
+ destinationDirectory.toPath()
+ .resolve("automation-package.yml")
+ );
+
+ attributes.put(AbstractArtefact.NAME, "This Keyword was renamed");
+
+ functionCollection.save(function);
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("This Keyword was renamed.yml"),
+ destinationDirectory.toPath()
+ .resolve("newKeywordsPath")
+ .resolve("This Keyword was renamed.yml")
+ );
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("descriptorAfterNewKeywordsFragmentReference.yml"),
+ destinationDirectory.toPath()
+ .resolve("automation-package.yml"));
+
+ functionCollection.remove(Filters.equals("attributes.name", "This Keyword was renamed"));
+
+ assertFalse(Files.exists(
+ destinationDirectory.toPath()
+ .resolve("newKeywordsPath")
+ .resolve("This Keyword was renamed.yml")
+ ));
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("descriptorAfterNewKeywordsFragmentReference.yml"),
+ destinationDirectory.toPath()
+ .resolve("automation-package.yml"));
+ }
+
+ @Test
+ public void testAddPlanToNewFragmentAndRenameAndRemove() 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(AbstractArtefact.NAME, "Hello World Plan");
+ plan.setAttributes(attributes);
+
+ setPropertiesWriteMode(YamlPlan.PLANS_ENTITY_NAME, "newPlansPath", AutomationPackageYamlFragmentManager.NewObjectFragmentMode.PER_OBJECT);
+
+ planCollection.save(plan);
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("Hello World Plan.yml"),
+ destinationDirectory.toPath()
+ .resolve("newPlansPath")
+ .resolve("Hello World Plan.yml")
+ );
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("descriptorAfterNewPlansFragmentReference.yml"),
+ destinationDirectory.toPath()
+ .resolve("automation-package.yml")
+ );
+
+ attributes.put(AbstractArtefact.NAME, "This Plan was renamed");
+
+ planCollection.save(plan);
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("This Plan was renamed.yml"),
+ destinationDirectory.toPath()
+ .resolve("newPlansPath")
+ .resolve("This Plan was renamed.yml")
+ );
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("descriptorAfterNewPlansFragmentReference.yml"),
+ destinationDirectory.toPath()
+ .resolve("automation-package.yml"));
+
+ planCollection.remove(Filters.equals("attributes.name", "This Plan was renamed"));
+
+ assertFalse(Files.exists(
+ destinationDirectory.toPath()
+ .resolve("newPlansPath")
+ .resolve("This Plan was renamed.yml")
+ ));
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("descriptorAfterNewPlansFragmentReference.yml"),
+ destinationDirectory.toPath()
+ .resolve("automation-package.yml"));
+ }
+
+ @Test
+ public void testAddTwoPlansToNewFragmentAndRemoveOne() 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(AbstractArtefact.NAME, "Hello World Plan");
+ plan.setAttributes(attributes);
+
+ setPropertiesWriteMode(YamlPlan.PLANS_ENTITY_NAME, "newPlansPath", AutomationPackageYamlFragmentManager.NewObjectFragmentMode.PER_OBJECT);
+
+ planCollection.save(plan);
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("Hello World Plan.yml"),
+ destinationDirectory.toPath()
+ .resolve("newPlansPath")
+ .resolve("Hello World Plan.yml")
+ );
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("descriptorAfterNewPlansFragmentReference.yml"),
+ destinationDirectory.toPath()
+ .resolve("automation-package.yml")
+ );
+
+ Plan plan2 = new Plan(sequence);
+ Map attributes2 = new HashMap<>();
+ attributes2.put(AbstractArtefact.NAME, "This Plan was renamed");
+ plan2.setAttributes(attributes2);
+
+ planCollection.save(plan2);
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("This Plan was renamed.yml"),
+ destinationDirectory.toPath()
+ .resolve("newPlansPath")
+ .resolve("This Plan was renamed.yml")
+ );
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("descriptorAfterNewPlansFragmentReference.yml"),
+ destinationDirectory.toPath()
+ .resolve("automation-package.yml"));
+
+ planCollection.remove(Filters.equals("attributes.name", "Hello World Plan"));
+
+ assertFalse(Files.exists(
+ destinationDirectory.toPath()
+ .resolve("newPlansPath")
+ .resolve("Hello World Plan.yml")
+ ));
+
+ assertFilesEqual(
+ expectedFilesPath
+ .resolve("descriptorAfterNewPlansFragmentReference.yml"),
+ destinationDirectory.toPath()
+ .resolve("automation-package.yml"));
+ }
+}
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
index 5a52ed56b5..e331f0e15a 100644
--- 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
@@ -82,4 +82,18 @@ public void testModifyCompositeKeyword() throws IOException {
assertFilesEqual(expectedFilesPath.resolve("keywordsAfterCompositeModification.yml"), destinationDirectory.toPath().resolve("keywords.yml"));
}
+ @Test
+ public void testRenameCompositeKeyword() 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();
+
+ compositeFunction.getAttributes().put(AbstractOrganizableObject.NAME, "Renamed Composite Keyword");
+
+ functionCollection.save(compositeFunction);
+
+ assertFilesEqual(expectedFilesPath.resolve("keywordsAfterCompositeRenamed.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
index 8c33637c55..c08d588e7b 100644
--- 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
@@ -57,6 +57,9 @@ public void testParameterModify() throws IOException {
Parameter parameter = optionalParameter.get();
parameter.getValue().setValue("myModifiedValue");
+
+ setPropertiesWriteToFragment(Parameter.ENTITY_NAME, "parameters.yml");
+
parameterCollection.save(parameter);
assertFilesEqual(expectedFilesPath.resolve("parametersAfterModification.yml"), destinationDirectory.toPath().resolve("parameters.yml"));
@@ -72,6 +75,7 @@ public void testParameterAddAndModify() throws IOException {
setPropertiesWriteToFragment(Parameter.ENTITY_NAME, "parameters.yml");
+ parameter.setPriority(1);
parameterCollection.save(parameter);
assertFilesEqual(expectedFilesPath.resolve("parametersAfterAdd.yml"), destinationDirectory.toPath().resolve("parameters.yml"));
@@ -80,6 +84,7 @@ public void testParameterAddAndModify() throws IOException {
parameterCollection.save(parameter);
parameter.setValue(new DynamicValue<>("foo"));
+ parameter.setPriority(null);
parameterCollection.save(parameter);
assertFilesEqual(expectedFilesPath.resolve("parametersAfterAddAndModification.yml"), destinationDirectory.toPath().resolve("parameters.yml"));
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/Hello World Composite Function.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/Hello World Composite Function.yml
new file mode 100644
index 0000000000..10a286e1a9
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/Hello World Composite Function.yml
@@ -0,0 +1,10 @@
+---
+keywords:
+ - Composite:
+ name: "Hello World Composite Function"
+ plan:
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/This Keyword was renamed.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/This Keyword was renamed.yml
new file mode 100644
index 0000000000..6216e9e111
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/This Keyword was renamed.yml
@@ -0,0 +1,10 @@
+---
+keywords:
+ - Composite:
+ name: "This Keyword was renamed"
+ plan:
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/This Plan was renamed.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/This Plan was renamed.yml
new file mode 100644
index 0000000000..55ef8445fc
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/This Plan was renamed.yml
@@ -0,0 +1,8 @@
+---
+plans:
+ - name: "This Plan was renamed"
+ root:
+ sequence:
+ children:
+ - echo:
+ text: "Hello World"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/descriptorAfterNewKeywordsFragmentReference.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/descriptorAfterNewKeywordsFragmentReference.yml
new file mode 100644
index 0000000000..b213e5a266
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/descriptorAfterNewKeywordsFragmentReference.yml
@@ -0,0 +1,26 @@
+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"
+ - "newKeywordsPath/*.yml"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/descriptorAfterNewPlansFragmentReference.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/descriptorAfterNewPlansFragmentReference.yml
new file mode 100644
index 0000000000..7724332157
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/descriptorAfterNewPlansFragmentReference.yml
@@ -0,0 +1,26 @@
+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"
+ - "newPlansPath/*.yml"
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/keywordsAfterCompositeRenamed.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/keywordsAfterCompositeRenamed.yml
new file mode 100644
index 0000000000..e4ad5103a8
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/keywordsAfterCompositeRenamed.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: "Renamed Composite Keyword"
+ 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/expected/parametersAfterAdd.yml b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/parametersAfterAdd.yml
index 0ab61a81ad..3997b99ca0 100644
--- 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
@@ -15,3 +15,4 @@ parameters:
- key: "addedParameter"
value: "test"
description: "This is an added Parameter before modification"
+ priority: 1
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 4fead6c631..531b779cac 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
@@ -29,6 +29,7 @@
import step.automation.packages.yaml.model.AutomationPackageFragmentYaml;
import step.core.plans.Plan;
import step.core.yaml.deserialization.PatchableYamlList;
+import step.core.yaml.deserialization.PatchableYamlPrimitive;
import step.functions.Function;
import step.plans.automation.YamlPlainTextPlan;
import step.plans.nl.RootArtefactType;
@@ -42,13 +43,11 @@
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
+import java.net.URISyntaxException;
import java.net.URL;
+import java.nio.file.FileSystemNotFoundException;
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.*;
import java.util.stream.Collectors;
/**
@@ -130,7 +129,7 @@ protected AutomationPackageContent buildAutomationPackage(AutomationPackageDescr
// apply imported fragments recursively
if (descriptor != null) {
- fillAutomationPackageWithImportedFragments(res, descriptor, archive, new HashMap<>());
+ fillAutomationPackageWithImportedFragments(res, descriptor, archive, new HashSet<>());
}
return res;
}
@@ -183,33 +182,54 @@ protected AutomationPackageContent newContentInstance() {
public AutomationPackageYamlFragmentManager getAutomationPackageYamlFragmentManager(T archive) throws AutomationPackageReadingException {
AutomationPackageDescriptorReader reader = getOrCreateDescriptorReader();
- URL descriptorURL = archive.getDescriptorYamlUrl();
- try (InputStream inputStream = descriptorURL.openStream()) {
+ 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);
+
+ try {
+ Path descriptorPath = Path.of(descriptorUrl.toURI());
+ descriptor.setFragmentPath(descriptorPath);
+ AutomationPackageContent content = newContentInstance();
+ Set fragments = new HashSet<>();
+ fillAutomationPackageWithImportedFragments(content, descriptor, archive, fragments);
+ StagingAutomationPackageContext stagingContext = new StagingAutomationPackageContext(null, AutomationPackageOperationMode.LOCAL, new LocalResourceManagerImpl(descriptorPath.getParent().toFile()), archive, content, null, null, new HashMap<>());
+ return new AutomationPackageYamlFragmentManager(archive.getResourcePathMatchingResolver(), descriptor, fragments, getOrCreateDescriptorReader(), stagingContext);
+ } catch (FileSystemNotFoundException | URISyntaxException e) {
+ throw new AutomationPackageReadingException("Failed to read automation package for editing. The most likely cause is that you were trying to load " +
+ "an automation package as a packaged jar. This is not supported and expected behaviour", e);
+ }
} 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 {
+ /**
+ *
+ * @param targetPackage Target Automation package content to be filled by fragment read entities
+ * @param fragment Fragment to read
+ * @param archive Automation package archive
+ * @param fragments Set of all automation package fragments collected during recursive reading of fragments.
+ * @throws AutomationPackageReadingException Thrown upon errors when reading the fragment
+ */
+ private void fillAutomationPackageWithImportedFragments(AutomationPackageContent targetPackage, AutomationPackageFragmentYaml fragment, T archive, Set fragments) throws AutomationPackageReadingException {
fillContentSections(targetPackage, fragment, archive);
if (!fragment.getFragments().isEmpty()) {
- for (String importedFragmentReference : fragment.getFragments()) {
- List resources = archive.getResourcesByPattern(importedFragmentReference);
+ for (PatchableYamlPrimitive importedFragmentReference : fragment.getFragments()) {
+ List resources = archive.getResourcesByPattern(importedFragmentReference.toString());
for (URL resource : resources) {
try (InputStream fragmentYamlStream = resource.openStream()) {
- fragment = getOrCreateDescriptorReader().readAutomationPackageFragment(fragmentYamlStream, resource.toString(), archive.getAutomationPackageName());
- fragmentYamlMap.put(resource.toString(), fragment);
- fragment.setFragmentUrl(resource);
- fillAutomationPackageWithImportedFragments(targetPackage, fragment, archive, fragmentYamlMap);
- } catch (IOException e) {
+ AutomationPackageFragmentYaml referencedFragment = getOrCreateDescriptorReader().readAutomationPackageFragment(fragmentYamlStream, resource.toString(), archive.getAutomationPackageName());
+ fragments.add(referencedFragment);
+ try {
+ referencedFragment.setFragmentPath(Path.of(resource.toURI()));
+ } catch (FileSystemNotFoundException e) {
+ logger.warn("Could not set Fragment path for fragment editing while loading fragment. " +
+ "This is likely due to loading the automation package as a jar and not as a file system folder. This is expected behaviour");
+ }
+
+ fillAutomationPackageWithImportedFragments(targetPackage, referencedFragment, archive, fragments);
+ } catch (IOException | URISyntaxException e) {
throw new AutomationPackageReadingException("Unable to read fragment in automation package: " + importedFragmentReference, e);
}
}
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 4e139641cc..3a9aa56898 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
@@ -167,6 +167,7 @@ private ObjectMapper createYamlObjectMapper() {
// Disable native type id to enable conversion to generic Documents
yamlFactory.disable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID);
yamlFactory.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR);
+ yamlFactory.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
ObjectMapper yamlMapper = DefaultJacksonMapperProvider.getObjectMapper(yamlFactory);
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
index a6487f001b..eeea9e7f57 100644
--- 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
@@ -18,6 +18,8 @@
******************************************************************************/
package step.automation.packages.yaml;
+import org.apache.commons.io.FileUtils;
+import step.automation.packages.ResourcePathMatchingResolver;
import step.automation.packages.StagingAutomationPackageContext;
import step.automation.packages.model.YamlAutomationPackageKeyword;
import step.automation.packages.yaml.model.AutomationPackageDescriptorYaml;
@@ -25,12 +27,14 @@
import step.automation.packages.yaml.model.AutomationPackageFragmentYamlImpl;
import step.core.accessors.AbstractOrganizableObject;
import step.core.plans.Plan;
+import step.core.yaml.NamedObjectPatchableYamlModel;
import step.core.yaml.PatchableYamlModel;
import step.core.yaml.PatchableYamlModelBase;
import step.core.yaml.PatchingContext;
+import step.core.yaml.deserialization.AutomationPackageConcurrentEditException;
import step.core.yaml.deserialization.AutomationPackagePerObjectSaveUnsupportedException;
-import step.core.yaml.deserialization.AutomationPackageUpdateException;
import step.core.yaml.deserialization.PatchableYamlList;
+import step.core.yaml.deserialization.PatchableYamlPrimitive;
import step.functions.Function;
import step.parameter.Parameter;
import step.parameter.automation.AutomationPackageParameter;
@@ -39,24 +43,25 @@
import java.io.ByteArrayInputStream;
import java.io.File;
+import java.io.IOException;
import java.io.InputStream;
-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.Optional;
import java.util.Properties;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
public class AutomationPackageYamlFragmentManager {
protected final Path apRoot;
protected final StagingAutomationPackageContext stagingContext;
+ private final ResourcePathMatchingResolver resourcePatchMatchingResolver;
public enum NewObjectFragmentMode {
/**
@@ -75,24 +80,24 @@ public enum NewObjectFragmentMode {
protected final Map patchableMap = new ConcurrentHashMap<>();
protected final Map fragmentMap = new ConcurrentHashMap<>();
- protected final Map pathToYamlFragment;
+ protected final Set fragments;
protected Properties properties = new Properties();
public final AutomationPackageFragmentYaml descriptorYaml;
- public AutomationPackageYamlFragmentManager(AutomationPackageDescriptorYaml descriptorYaml, Map fragmentMap, AutomationPackageDescriptorReader descriptorReader, StagingAutomationPackageContext stagingContext) {
-
+ public AutomationPackageYamlFragmentManager(ResourcePathMatchingResolver resourcePathMatchingResolver, AutomationPackageDescriptorYaml descriptorYaml, Set fragments, AutomationPackageDescriptorReader descriptorReader, StagingAutomationPackageContext stagingContext) {
+ this.resourcePatchMatchingResolver = resourcePathMatchingResolver;
this.descriptorReader = descriptorReader;
this.descriptorYaml = descriptorYaml;
- pathToYamlFragment = fragmentMap;
- apRoot = Path.of(descriptorYaml.getFragmentUrl().getPath())
- .getParent();
+ apRoot = descriptorYaml.getFragmentPath().getParent();
this.stagingContext = stagingContext;
initializeMaps(descriptorYaml);
- pathToYamlFragment.values().stream()
+ this.fragments = fragments;
+
+ fragments.stream()
.filter(f -> f != descriptorYaml)
.forEach(this::initializeMaps);
}
@@ -102,7 +107,6 @@ public void setProperties(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);
@@ -145,13 +149,12 @@ public synchronized Plan savePlan(Plan plan) {
AutomationPackageFragmentYaml fragment = fragmentMap.get(plan);
if (fragment == null) {
- fragment = fragmentForNewObject(plan, YamlPlan.PLANS_ENTITY_NAME);
+ fragment = fragmentForNewObject(newYamlPlan, YamlPlan.PLANS_ENTITY_NAME);
fragmentMap.put(plan, fragment);
- pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment);
addFragmentEntity(fragment, fragment.getPlans(), newYamlPlan);
} else {
YamlPlan oldYamlPlan = (YamlPlan) patchableMap.get(plan);
- modifyFragmentEntity(fragment, fragment.getPlans(), oldYamlPlan, newYamlPlan);
+ modifyFragmentEntity(fragment, fragment.getPlans(), oldYamlPlan, newYamlPlan, YamlPlan.PLANS_ENTITY_NAME);
}
patchableMap.put(plan, newYamlPlan);
@@ -161,11 +164,10 @@ public synchronized Plan savePlan(Plan plan) {
public synchronized step.functions.Function saveFunction(step.functions.Function function) {
AutomationPackageFragmentYaml fragment = fragmentMap.get(function);
+ YamlAutomationPackageKeyword newKeyword = createNewYamlKeyword(function);
if (fragment == null) {
- fragment = fragmentForNewObject(function, "keywords");
+ fragment = fragmentForNewObject(newKeyword, YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME);
fragmentMap.put(function, fragment);
- pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment);
- YamlAutomationPackageKeyword newKeyword = createNewYamlKeyword(function);
if (newKeyword != null) {
patchableMap.put(function, newKeyword);
addFragmentEntity(fragment, fragment.getKeywords(), newKeyword);
@@ -173,9 +175,9 @@ public synchronized step.functions.Function saveFunction(step.functions.Function
System.err.println("SAVING OF FUNCTION OF TYPE " + function.getClass().getName() + " IS NOT CURRENTLY SUPPORTED");
}
} else {
- YamlAutomationPackageKeyword yamlKeyword = (YamlAutomationPackageKeyword) patchableMap.get(function);
- yamlKeyword.getYamlKeyword().updateFromFunction(function);
- modifyFragmentEntity(fragment, fragment.getKeywords(), yamlKeyword, yamlKeyword);
+ YamlAutomationPackageKeyword oldKeyword = (YamlAutomationPackageKeyword) patchableMap.get(function);
+ modifyFragmentEntity(fragment, fragment.getKeywords(), oldKeyword, newKeyword, YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME);
+ patchableMap.put(function, newKeyword);
}
return function;
}
@@ -222,54 +224,136 @@ private void addFragmentEntity(AutomationPackageF
fragment.writeToDisk();
}
- private void modifyFragmentEntity(AutomationPackageFragmentYaml fragment, PatchableYamlList entityList, T oldEntity, T newEntity) {
+ private void modifyFragmentEntity(AutomationPackageFragmentYaml fragment, PatchableYamlList entityList, T oldEntity, T newEntity, String fieldName) {
entityList.replaceItem(oldEntity, newEntity);
+ Path oldRelativePath = determineObjectRelativePath(oldEntity, fieldName, false);
+ Path newRelativePath = determineObjectRelativePath(newEntity, fieldName, false);
+
+ // Path did not change - skip entire move logic
+ if (!oldRelativePath.equals(newRelativePath)) {
+ Path oldAbsolutePath = apRoot.resolve(oldRelativePath);
+
+ /* oldRelativePath is the path which would have been given to old version of the entity
+ by the fragment manager. If it matches the fragment path, this means that
+ the fragment path was intended to follow the naming convention based on the configuration
+ (i.e. PER_OBJECT naming)
+
+ if the paths don't match, then simply skip the renaming. This silently allows for:
+ - legacy fragments which don't follow the naming convention
+ - FRAGMENT type naming (fixed fragment for object types such as Parameters)
+ */
+ if (oldAbsolutePath.equals(fragment.getFragmentPath())) {
+ Path newAbsolutePath = apRoot.resolve(newRelativePath);
+
+ try {
+ FileUtils.moveFile(oldAbsolutePath.toFile(), newAbsolutePath.toFile());
+ fragment.setFragmentPath(newAbsolutePath);
+ } catch (IOException e) {
+ throw new AutomationPackageConcurrentEditException(
+ String.format("Unable to rename file %s to file %s. Was the file renamed or deleted outside the editor?", oldAbsolutePath, newAbsolutePath));
+ }
+
+ AutomationPackageFragmentYaml referencingFragment = determineReferencingFragment(oldRelativePath)
+ .orElse(descriptorYaml);
+
+ String newReference = resourcePatchMatchingResolver.getFragmentReferenceString(determineObjectRelativePath(newEntity, fieldName, true));
+ String oldReference = resourcePatchMatchingResolver.getFragmentReferenceString(oldRelativePath);
+
+ if (referencingFragment.getFragments().removeIf(f -> f.getValue().equals(oldReference))) {
+ referencingFragment.getFragments().add(new PatchableYamlPrimitive<>(referencingFragment.getPatchingContext(), newReference));
+ referencingFragment.writeToDisk();
+ };
+ }
+ }
fragment.writeToDisk();
}
- private AutomationPackageFragmentYaml fragmentForNewObject(AbstractOrganizableObject p, String fieldName) {
+ private void removeFragmentEntity(AutomationPackageFragmentYaml fragment, PatchableYamlList entityList, BO object) {
- 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";
+ PatchableYamlModel yamlObject = patchableMap.get(object);
+ entityList.remove(yamlObject);
+
+ patchableMap.remove(object);
+ fragmentMap.remove(object);
+
+ if (fragment.isEmpty()) {
+ try {
+ FileUtils.delete(fragment.getFragmentPath().toFile());
+
+ determineReferencingFragment(fragment.getFragmentPath()).ifPresent(referencingFragment -> {
+ String relativeFragmentReference = resourcePatchMatchingResolver
+ .getFragmentReferenceString(apRoot.relativize(fragment.getFragmentPath()));
+ if (referencingFragment.getFragments().removeIf(f -> f.getValue().equals(relativeFragmentReference))) {
+ referencingFragment.writeToDisk();
+ };
+ fragments.remove(fragment);
+ });
+ } catch (IOException e) {
+ throw new AutomationPackageConcurrentEditException(String.format("%s was removed outside the editor", fragment.getFragmentPath()));
+ }
+ } else {
+ fragment.writeToDisk();
}
+ }
+ private AutomationPackageFragmentYaml fragmentForNewObject(PatchableYamlModel p, String fieldName) {
- 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.
+ Path path = determineObjectRelativePath(p, fieldName, false);
+ Path absolutePath = apRoot.resolve(path);
- %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()));
+ Optional optionalExistingFragment = fragmentMap
+ .values().stream().filter(f -> f.getFragmentPath().equals(absolutePath)).findAny();
+ if (optionalExistingFragment.isPresent()) {
+ return optionalExistingFragment.get();
}
- 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);
- }
+ PatchingContext context = new PatchingContext(absolutePath.toString(), "---", descriptorYaml.getPatchingContext().getMapper());
+ AutomationPackageFragmentYaml fragment = new AutomationPackageFragmentYamlImpl(context);
+ fragments.add(fragment);
+ fragment.setFragmentPath(absolutePath);
- if (mode == NewObjectFragmentMode.PER_OBJECT) {
- path = path.resolve(sanitizeFilename(p.getAttribute(AbstractOrganizableObject.NAME)) + ".yml");
+ if (determineReferencingFragment(path).isEmpty()) {
+ String referencingPath = resourcePatchMatchingResolver.getFragmentReferenceString(determineObjectRelativePath(p, fieldName, true));
+ descriptorYaml.getFragments().add(new PatchableYamlPrimitive<>(descriptorYaml.getPatchingContext(), referencingPath));
+ descriptorYaml.writeToDisk();
}
+ return fragment;
+ }
- try {
- URL url = path.toUri().toURL();
+ private Optional determineReferencingFragment(Path path) {
+ return Stream.concat(Stream.of(descriptorYaml), fragments.stream())
+ .filter(fragment -> fragment.getFragments().stream()
+ .anyMatch(pattern -> resourcePatchMatchingResolver.isMatchingPath(pattern.getValue(), path)))
+ .findFirst();
+ }
+ public Path determineObjectRelativePath(PatchableYamlModel p, String fieldName, boolean globPattern) {
- if (pathToYamlFragment.containsKey(url.toString())) {
- return pathToYamlFragment.get(url.toString());
- }
- PatchingContext context = new PatchingContext(url.toString(), "---", 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);
+ 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";
}
+ String relativeFragmentPath = properties.getProperty(String.format(PROPERTY_NEW_OBJECT_FRAGMENT_PATH, fieldName), defaultRelativeFragmentPath);
+
+ return switch (mode) {
+ case NewObjectFragmentMode.FRAGMENT -> new File(relativeFragmentPath).toPath();
+ case NewObjectFragmentMode.PER_OBJECT -> {
+ if (p instanceof NamedObjectPatchableYamlModel namedObjectPatchableYamlModel) {
+ String name = globPattern ? "*" : namedObjectPatchableYamlModel.getName();
+ yield new File(relativeFragmentPath).toPath().resolve(sanitizeFilename(name + ".yml"));
+ }
+
+ 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()));
+ }
+ };
}
public String sanitizeFilename(String inputName) {
@@ -278,64 +362,35 @@ public String sanitizeFilename(String inputName) {
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();
+ removeFragmentEntity(fragment, fragment.getPlans(), plan);
}
public void removeFunction(step.functions.Function function) {
AutomationPackageFragmentYaml fragment = fragmentMap.get(function);
- YamlAutomationPackageKeyword yamlKeyword = (YamlAutomationPackageKeyword) patchableMap.get(function);
-
- fragment.getKeywords().remove(yamlKeyword);
-
- patchableMap.remove(function);
- fragmentMap.remove(function);
-
- fragment.writeToDisk();
+ removeFragmentEntity(fragment, fragment.getKeywords(), function);
}
public void removeAdditionalFieldObject(BO object, String fieldName) {
-
AutomationPackageFragmentYaml fragment = fragmentMap.get(object);
- PatchableYamlModel yamlObject = patchableMap.get(object);
+ removeFragmentEntity(fragment, fragment.getAdditionalField(fieldName), object);
+ }
- fragment.getAdditionalField(fieldName)
- .remove(yamlObject);
+ public synchronized BO saveAdditionalFieldObject(BO object, YO yamlObject, String fieldName) {
+ final AutomationPackageFragmentYaml fragment = fragmentMap.computeIfAbsent(object, o -> fragmentForNewObject(yamlObject, fieldName));
- patchableMap.remove(object);
- fragmentMap.remove(object);
+ yamlObject.setPatchingContext(fragment.getPatchingContext());
+ PatchableYamlList list = (PatchableYamlList) fragment.getAdditionalFields()
+ .computeIfAbsent(fieldName, f -> new PatchableYamlList(fragment.getPatchingContext(), fieldName));
- fragment.writeToDisk();
- }
- public synchronized BO saveAdditionalFieldObject(BO object, java.util.function.Function newYamlObjectCreator, String fieldName) {
- AutomationPackageFragmentYaml fragment;
- if (fragmentMap.get(object) == null) {
- fragment = fragmentForNewObject(object, fieldName);
-
- YO newYamlObject = newYamlObjectCreator.apply(fragment.getPatchingContext());
- PatchableYamlList list = (PatchableYamlList) fragment.getAdditionalFields()
- .computeIfAbsent(fieldName, k -> new PatchableYamlList(fragment.getPatchingContext(), fieldName));
- fragmentMap.put(object, fragment);
- pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment);
- addFragmentEntity(fragment, list, newYamlObject);
- patchableMap.put(object, newYamlObject);
+ if (patchableMap.get(object) == null) {
+ addFragmentEntity(fragment, list, yamlObject);
+ patchableMap.put(object, yamlObject);
} else {
- fragment = fragmentMap.get(object);
- YO newYamlObject = newYamlObjectCreator.apply(fragment.getPatchingContext());
- PatchableYamlList list = (PatchableYamlList) fragment.getAdditionalFields()
- .computeIfAbsent(fieldName, k -> new PatchableYamlList(fragment.getPatchingContext(), fieldName));
-
YO oldYamlObject = (YO) patchableMap.get(object);
- modifyFragmentEntity(fragment, list, oldYamlObject, newYamlObject);
- patchableMap.put(object, newYamlObject);
+ modifyFragmentEntity(fragment, list, oldYamlObject, yamlObject, fieldName);
+ patchableMap.put(object, yamlObject);
}
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
index 815cfaaa2f..4a92941594 100644
--- 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
@@ -27,25 +27,19 @@
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.PatchingContext;
import step.core.yaml.deserialization.PatchableYamlList;
import java.io.IOException;
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)));
- }
+ abstract public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException;
@Override
protected void handleUnknownVanilla(JsonParser p, DeserializationContext ctxt, Object intoValue, String propName) throws IOException {
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 c4891c8311..f715264730 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,34 +18,29 @@
******************************************************************************/
package step.automation.packages.yaml.model;
-import com.fasterxml.jackson.annotation.JsonAnyGetter;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
+import com.fasterxml.jackson.annotation.*;
import org.apache.commons.io.FileUtils;
import step.automation.packages.model.YamlAutomationPackageKeyword;
import step.automation.packages.yaml.AutomationPackageWriteToDiskException;
import step.core.yaml.PatchingContext;
import step.core.yaml.deserialization.AutomationPackageConcurrentEditException;
import step.core.yaml.deserialization.PatchableYamlList;
+import step.core.yaml.deserialization.PatchableYamlPrimitive;
import step.plans.automation.YamlPlainTextPlan;
import step.plans.parser.yaml.YamlPlan;
import java.io.File;
import java.io.IOException;
-import java.net.URISyntaxException;
-import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
import java.text.MessageFormat;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public abstract class AbstractAutomationPackageFragmentYaml implements AutomationPackageFragmentYaml {
- private List fragments = new ArrayList<>();
+ private PatchableYamlList> fragments;
private PatchableYamlList keywords;
private PatchableYamlList plans;
private PatchableYamlList plansPlainText;
@@ -59,10 +54,11 @@ public AbstractAutomationPackageFragmentYaml(PatchingContext patchingContext) {
plans = new PatchableYamlList<>(patchingContext, YamlPlan.PLANS_ENTITY_NAME);
keywords = new PatchableYamlList<>(patchingContext, YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME);
plansPlainText = new PatchableYamlList<>(patchingContext, "plansPlainText");
+ fragments = new PatchableYamlList<>(patchingContext, "fragments");
}
@JsonIgnore
- private URL url;
+ private Path path;
@Override
public PatchableYamlList getKeywords() {
@@ -85,12 +81,12 @@ public void setPlans(PatchableYamlList plans) {
}
@Override
- public List getFragments() {
+ public PatchableYamlList> getFragments() {
return fragments;
}
@JsonSetter(nulls = Nulls.AS_EMPTY)
- public void setFragments(List fragments) {
+ public void setFragments(PatchableYamlList> fragments) {
this.fragments = fragments;
}
@@ -116,9 +112,9 @@ public void setPlansPlainText(PatchableYamlList plansPlainTex
}
@JsonIgnore
- public void setFragmentUrl(URL url) {
+ public void setFragmentPath(Path path) {
resetLastModified();
- this.url = url;
+ this.path = path;
}
private void resetLastModified() {
@@ -126,8 +122,8 @@ private void resetLastModified() {
}
@JsonIgnore
- public URL getFragmentUrl() {
- return url;
+ public Path getFragmentPath() {
+ return path;
}
@JsonIgnore
@@ -146,14 +142,23 @@ public PatchingContext getPatchingContext() {
@Override
public void writeToDisk() {
try {
- File file = new File(url.toURI());
+ File file = path.toFile();
if (file.exists() && file.lastModified() > fileLastModified) {
- throw new AutomationPackageConcurrentEditException(MessageFormat.format("Automation package fragment {0} was edited outside the editor.", url));
+ throw new AutomationPackageConcurrentEditException(MessageFormat.format("Automation package fragment {0} was edited outside the editor.", path));
}
FileUtils.writeStringToFile(file, context.getCurrentYaml(), 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);
+ } catch (IOException e) {
+ throw new AutomationPackageWriteToDiskException(MessageFormat.format("Error when writing automation package fragment {0} back to disk.", path), e);
}
}
+
+ @Override
+ public boolean isEmpty() {
+ return getFragments().isEmpty() &&
+ getPlans().isEmpty() &&
+ getPlansPlainText().isEmpty() &&
+ getKeywords().isEmpty() &&
+ getAdditionalFields().isEmpty();
+ }
}
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 7cf2be19b3..8721c6963f 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
@@ -21,11 +21,12 @@
import step.automation.packages.model.YamlAutomationPackageKeyword;
import step.core.yaml.PatchingContext;
import step.core.yaml.deserialization.PatchableYamlList;
+import step.core.yaml.deserialization.PatchableYamlPrimitive;
import step.plans.automation.YamlPlainTextPlan;
import step.plans.parser.yaml.YamlPlan;
import java.io.IOException;
-import java.net.URL;
+import java.nio.file.Path;
import java.util.List;
import java.util.Map;
@@ -37,7 +38,7 @@ public interface AutomationPackageFragmentYaml {
List getPlansPlainText();
- List getFragments();
+ PatchableYamlList> getFragments();
Map> getAdditionalFields();
@@ -47,13 +48,15 @@ default PatchableYamlList getAdditionalField(String k) {
void setAdditionalFields(String key, PatchableYamlList> value) throws IOException;
- URL getFragmentUrl();
+ Path getFragmentPath();
- void setFragmentUrl(URL url);
+ void setFragmentPath(Path url);
PatchingContext getPatchingContext();
void setPatchingContext(PatchingContext context);
void writeToDisk();
+
+ boolean isEmpty();
}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/test/java/step/automation/packages/yaml/AutomationPackageDescriptorReaderTest.java b/step-automation-packages/step-automation-packages-yaml/src/test/java/step/automation/packages/yaml/AutomationPackageDescriptorReaderTest.java
index a751c0a7a5..c03d302bd2 100644
--- a/step-automation-packages/step-automation-packages-yaml/src/test/java/step/automation/packages/yaml/AutomationPackageDescriptorReaderTest.java
+++ b/step-automation-packages/step-automation-packages-yaml/src/test/java/step/automation/packages/yaml/AutomationPackageDescriptorReaderTest.java
@@ -34,6 +34,7 @@
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
+import java.util.stream.Collectors;
import static org.junit.Assert.*;
@@ -100,7 +101,7 @@ public void completeDescriptorReadTest() throws AutomationPackageReadingExceptio
assertEquals("*/5 * * * *", firstTask.getCron());
assertEquals("TEST", firstTask.getExecutionParameters().get("environment"));
- assertEquals(Arrays.asList("importPlans.yml", "importKeywords.yml"), descriptor.getFragments());
+ assertEquals(Arrays.asList("importPlans.yml", "importKeywords.yml"), descriptor.getFragments().stream().map(f -> f.getValue()).collect(Collectors.toUnmodifiableList()));
} catch (IOException e) {
throw new RuntimeException(e);
}
diff --git a/step-automation-packages/step-automation-packages-yaml/src/test/java/step/core/yaml/PatchingContextTest.java b/step-automation-packages/step-automation-packages-yaml/src/test/java/step/core/yaml/PatchingContextTest.java
index d4279eddfb..60987b1ba4 100644
--- a/step-automation-packages/step-automation-packages-yaml/src/test/java/step/core/yaml/PatchingContextTest.java
+++ b/step-automation-packages/step-automation-packages-yaml/src/test/java/step/core/yaml/PatchingContextTest.java
@@ -103,7 +103,6 @@ public class PatchingContextTest {
schemaVersion: 1.0.0
name: "complete-package"
fragments:
- - "importPlans.yml"
- "importKeywords.yml"
keywords:
- Composite:
@@ -151,7 +150,6 @@ public class PatchingContextTest {
schemaVersion: 1.0.0
name: "complete-package"
fragments:
- - "importPlans.yml"
- "importKeywords.yml"
keywords: []
plans: []
@@ -186,35 +184,35 @@ public void testLowLevelDeletionModification() throws Exception {
.filter(m -> m instanceof PatchableYamlList>)
.map(x -> (PatchableYamlList) x)
.toList();
- assertEquals(3, lists.size());
+ assertEquals(4, lists.size());
// remove first keyword, second plan, second schedule
lists.get(0).remove(0);
- lists.get(1).remove(1);
+ lists.get(1).remove(0);
lists.get(2).remove(1);
+ lists.get(3).remove(1);
current = ensureValid(patchingContext.getCurrentYaml());
Assert.assertEquals(EXPECTED_REMOVED_1, current);
// empty first two lists, modify schedule a little
- AutomationPackageSchedule schedule1 = (AutomationPackageSchedule) lists.get(2).getFirst();
+ AutomationPackageSchedule schedule1 = (AutomationPackageSchedule) lists.get(3).getFirst();
schedule1.setName("This is now the new schedule name which is rather long, really long in fact");
schedule1.setActive(false);
schedule1.getExecutionParameters().clear();
// we have to tell the entity explicitly that it was modified
schedule1.setModified();
- lists.subList(0, 2).forEach(ArrayList::removeFirst);
+ lists.subList(1, 3).forEach(ArrayList::removeFirst);
current = ensureValid(patchingContext.getCurrentYaml());
Assert.assertEquals(EXPECTED_REMOVED_2_MODIFIED, current);
- lists.get(2).removeFirst();
+ lists.get(3).removeFirst();
current = ensureValid(patchingContext.getCurrentYaml());
Assert.assertEquals("""
schemaVersion: 1.0.0
name: "complete-package"
fragments:
- - "importPlans.yml"
- "importKeywords.yml"
keywords: []
plans: []
diff --git a/step-core-model/src/main/java/step/core/yaml/NamedObjectPatchableYamlModel.java b/step-core-model/src/main/java/step/core/yaml/NamedObjectPatchableYamlModel.java
new file mode 100644
index 0000000000..3c2e741e38
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/NamedObjectPatchableYamlModel.java
@@ -0,0 +1,24 @@
+/*******************************************************************************
+ * 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;
+
+public interface NamedObjectPatchableYamlModel {
+
+ String getName();
+}
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
index ce6f3bbd53..d11289b8ec 100644
--- a/step-core-model/src/main/java/step/core/yaml/PatchableYamlModelBase.java
+++ b/step-core-model/src/main/java/step/core/yaml/PatchableYamlModelBase.java
@@ -27,7 +27,7 @@ public class PatchableYamlModelBase extends AbstractYamlModel implements Patchab
private static final Logger logger = LoggerFactory.getLogger(PatchableYamlModelBase.class);
@JsonIgnore
- private PatchingContext context;
+ protected PatchingContext context;
public PatchableYamlModelBase(PatchingContext context) {
this.context = context;
diff --git a/step-core-model/src/main/java/step/core/yaml/PatchingContext.java b/step-core-model/src/main/java/step/core/yaml/PatchingContext.java
index b5df67456b..02c8f5af3f 100644
--- a/step-core-model/src/main/java/step/core/yaml/PatchingContext.java
+++ b/step-core-model/src/main/java/step/core/yaml/PatchingContext.java
@@ -7,11 +7,8 @@
import org.slf4j.LoggerFactory;
import step.core.yaml.deserialization.AutomationPackageUpdateException;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.nio.file.Path;
+import java.util.*;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -32,13 +29,13 @@ public PatchingContext() {
}
public PatchingContext(ObjectMapper mapper) {
- this(null, "", mapper);
+ this("", "", mapper);
}
public PatchingContext(String sourceLocation, String yaml, ObjectMapper mapper) {
- this.sourceLocation = sourceLocation;
- this.initialLines = new CopyOnWriteArrayList<>(yaml.lines().toList());
- this.mapper = mapper;
+ this.sourceLocation = Objects.requireNonNull(sourceLocation);
+ this.initialLines = new CopyOnWriteArrayList<>(Objects.requireNonNull(yaml).lines().toList());
+ this.mapper = Objects.requireNonNull(mapper);
}
public ObjectMapper getMapper() {
@@ -66,21 +63,19 @@ private List getLines(ChunkBounds bounds) {
return initialLines.subList(bounds.startLineNumber - 1, bounds.endLineNumber);
}
- public record ChunkBounds(int startLineNumber, int endLineNumber) implements Comparable
-
- {
+ public record ChunkBounds(int startLineNumber, int endLineNumber) implements Comparable {
private static final Comparator COMPARATOR = Comparator
.comparingInt(ChunkBounds::startLineNumber) // lower startLine first
.thenComparing(Comparator.comparingInt(ChunkBounds::endLineNumber).reversed()); // larger endLine (i.e. larger chunk) first
@Override
- public int compareTo (ChunkBounds that){
- return COMPARATOR.compare(this, that);
- }
+ public int compareTo(ChunkBounds that) {
+ return COMPARATOR.compare(this, that);
+ }
- public boolean encompasses (ChunkBounds inner){
- return inner.startLineNumber >= this.startLineNumber && inner.endLineNumber <= this.endLineNumber;
- }
+ public boolean encompasses(ChunkBounds inner) {
+ return inner.startLineNumber >= this.startLineNumber && inner.endLineNumber <= this.endLineNumber;
+ }
}
public String getCurrentYaml() {
@@ -151,9 +146,7 @@ private List getClaimedOuterBounds() {
private String serializeUnindented(Object entity) {
try {
- return mapper.writeValueAsString(entity)
- .replaceFirst("^---\\s*\\n", "")
- .trim();
+ return mapper.writeValueAsString(entity);
} catch (JsonProcessingException e) {
throw new AutomationPackageUpdateException("Error Serializing YAML object", e);
}
diff --git a/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlPrimitive.java b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlPrimitive.java
new file mode 100644
index 0000000000..8b2c60719c
--- /dev/null
+++ b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlPrimitive.java
@@ -0,0 +1,57 @@
+package step.core.yaml.deserialization;
+
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.core.JsonLocation;
+import step.core.yaml.PatchableYamlModelBase;
+import step.core.yaml.PatchingContext;
+
+import java.util.Objects;
+
+/*******************************************************************************
+ * 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 PatchableYamlPrimitive extends PatchableYamlModelBase {
+ @JsonIgnore
+ private T value;
+
+ @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
+ public PatchableYamlPrimitive(@JacksonInject(useInput = OptBoolean.FALSE) PatchingContext context, T value) {
+ super(context);
+ this.value = value;
+ Objects.requireNonNull(value);
+ }
+
+ @Override
+ public String toString() {
+ return value.toString();
+ }
+
+ @JsonValue
+ public T getValue() {
+ return value;
+ }
+
+ public void setValue(T value) {
+ this.value = value;
+ }
+
+ @Override
+ public void onParsed(JsonLocation startLocation, JsonLocation endLocation) {
+ context.claimChunk(startLocation, startLocation, this);
+ }
+}
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 aecc711f00..40616b4dca 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
@@ -103,8 +103,8 @@ public String getScopeEntity() {
return scopeEntity;
}
- public static AutomationPackageParameter forContext(PatchingContext context, Parameter parameter) {
- AutomationPackageParameter yamlParameter = new AutomationPackageParameter(context);
+ public static AutomationPackageParameter fromParameter(Parameter parameter) {
+ AutomationPackageParameter yamlParameter = new AutomationPackageParameter(null);
yamlParameter.copyFieldsFromObject(parameter, true);
Expression expression = parameter.getActivationExpression();
if (expression == null) {
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 19553ff896..79bc36a272 100644
--- a/step-core/src/main/java/step/automation/packages/ResourcePathMatchingResolver.java
+++ b/step-core/src/main/java/step/automation/packages/ResourcePathMatchingResolver.java
@@ -18,10 +18,15 @@
******************************************************************************/
package step.automation.packages;
+import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
import java.net.URL;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
@@ -88,7 +93,7 @@ private void findPathMatchingResourcesRecursive(String[] pathArray, int currentL
if (nextLevel < pathArray.length) {
String nextPath = pathArray[nextLevel];
List urls = ClassLoaderResourceFilesystem.listDirectory(currentPath);
- Pattern pattern = Pattern.compile(nextPath.replaceAll("\\*", ".*"));
+ Pattern pattern = prepareMatchPattern(nextPath);
for (URL url : urls) {
String file = url.getFile();
if (file.endsWith(getPathSeparator())) {
@@ -109,4 +114,15 @@ private void findPathMatchingResourcesRecursive(String[] pathArray, int currentL
}
}
+ private Pattern prepareMatchPattern(String referencingString) {
+ return Pattern.compile(referencingString.replace("*", ".*"));
+ }
+
+ public boolean isMatchingPath(String referenceString, Path path) {
+ return prepareMatchPattern(referenceString).matcher(getFragmentReferenceString(path)).matches();
+ }
+
+ public String getFragmentReferenceString(Path path) {
+ return path.toString().replace(File.pathSeparator, getPathSeparator());
+ }
}
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 451275e59e..97462ba1a6 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
@@ -20,11 +20,11 @@
import jakarta.json.JsonObject;
import step.automation.packages.StagingAutomationPackageContext;
-import step.core.yaml.YamlModelUtils;
import step.core.accessors.AbstractOrganizableObject;
import step.core.dynamicbeans.DynamicValue;
import step.core.yaml.AbstractYamlModel;
import step.core.yaml.YamlFieldCustomCopy;
+import step.core.yaml.YamlModelUtils;
import step.functions.Function;
import step.jsonschema.JsonSchema;
import step.jsonschema.JsonSchemaDefaultValueProvider;
@@ -120,11 +120,6 @@ 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 e1e76a63c9..0c1af166d0 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,11 +19,13 @@
package step.automation.packages.model;
import step.automation.packages.StagingAutomationPackageContext;
+import step.core.yaml.NamedObjectPatchableYamlModel;
import step.core.yaml.PatchableYamlModelBase;
import step.core.yaml.PatchingContext;
import step.functions.Function;
-public class YamlAutomationPackageKeyword extends PatchableYamlModelBase implements AutomationPackageKeyword {
+public class YamlAutomationPackageKeyword extends PatchableYamlModelBase
+ implements AutomationPackageKeyword, NamedObjectPatchableYamlModel {
private AbstractYamlFunction> yamlKeyword;
@@ -34,6 +36,7 @@ public YamlAutomationPackageKeyword(AbstractYamlFunction> yamlKeyword, Patchin
}
+
public AbstractYamlFunction> getYamlKeyword() {
return yamlKeyword;
}
@@ -46,4 +49,9 @@ public void setYamlKeyword(AbstractYamlFunction> yamlKeyword) {
public Function prepareKeyword(StagingAutomationPackageContext context) {
return yamlKeyword.applyAutomationPackageContext(context);
}
+
+ @Override
+ public String getName() {
+ return yamlKeyword.getName();
+ }
}
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 74ba1898bb..28429275ea 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
@@ -27,6 +27,7 @@
import step.core.plans.agents.configuration.AgentProvisioningConfiguration;
import step.core.plans.agents.configuration.AgentProvisioningConfigurationDeserializer;
import step.core.plans.agents.configuration.AgentProvisioningConfigurationSerializer;
+import step.core.yaml.NamedObjectPatchableYamlModel;
import step.core.yaml.PatchableYamlModelBase;
import step.core.yaml.PatchingContext;
import step.core.yaml.model.NamedYamlArtefact;
@@ -34,7 +35,7 @@
import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL)
-public class YamlPlan extends PatchableYamlModelBase {
+public class YamlPlan extends PatchableYamlModelBase implements NamedObjectPatchableYamlModel {
public static final String PLANS_ENTITY_NAME = "plans";
@@ -53,6 +54,7 @@ public YamlPlan(@JacksonInject(useInput = OptBoolean.FALSE, optional = OptBoolea
super(context);
}
+ @Override
public String getName() {
return name;
}
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 b58d97518d..f4ef80c2cc 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
@@ -19,17 +19,13 @@
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;
@@ -65,26 +61,6 @@ protected void fillDeclaredFields(CompositeFunction res, StagingAutomationPackag
}
}
- @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
- // FIXME: discuss what exactly this is supposed to do and how it relates to the comment above :-)
- if (this.plan.getName() != null && !this.plan.getName().isEmpty()) {
- this.plan.setName(plan.getAttribute(AbstractOrganizableObject.NAME));
- }
- // I have no idea why that mapper would be null sometimes:
- // ObjectMapper mapper = this.plan.getRoot().getYamlArtefact().getYamlObjectMapper();
- // That one should work...
- ObjectMapper mapper = this.getPlan().getPatchingContext().getMapper();
- this.plan.setRoot(new NamedYamlArtefact(AbstractYamlArtefact.toYamlArtefact(plan.getRoot(), mapper)));
- }
- }
-
-
private Plan yamlPlanToPlan(YamlPlan yamlPlan) {
Plan plan = new Plan(yamlPlan.getRoot().getYamlArtefact().toArtefact());