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 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