diff --git a/.editorconfig b/.editorconfig index 9293c5b4f5..781452eac0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -36,7 +36,7 @@ indent_size = 2 [*.{yml,yaml}] indent_style = space indent_size = 2 - +ij_yaml_spaces_within_brackets = false ######################################## # Markdown diff --git a/pom.xml b/pom.xml index 25b6a3c5a2..e2f2af287b 100644 --- a/pom.xml +++ b/pom.xml @@ -72,24 +72,25 @@ - step-commons step-agent + step-ap-ide + step-automation-packages + step-cli + step-commons step-constants - step-core-model + step-controller + step-controller-plugins step-core - step-automation-packages + step-core-model step-functions step-functions-plugins - step-plans - step-controller - step-controller-plugins - step-maven-plugin - step-repositories step-ide - step-libs-maven-client step-json-schema - step-cli + step-libs-maven-client step-livereporting + step-maven-plugin + step-plans + step-repositories diff --git a/step-ap-ide/.gitignore b/step-ap-ide/.gitignore new file mode 100644 index 0000000000..69fc4a95eb --- /dev/null +++ b/step-ap-ide/.gitignore @@ -0,0 +1 @@ +filemanager/ diff --git a/step-ap-ide/README.md b/step-ap-ide/README.md new file mode 100644 index 0000000000..e01867da80 --- /dev/null +++ b/step-ap-ide/README.md @@ -0,0 +1,12 @@ +This is still very early Work in progress, so some of it is quick and dirty to at least get an initial prototype to work. + +Basically, it's a modified and stripped down version of the step controller, meant to be executed lika a local application. +Technically, it still consists of a backend and frontend. For development, they will be separated (as they are for the main Step product), +so you'll also need to launch both of them separately. + +To start the frontend, check out branch SED-4429-step-ap-ide of the step-frontend-workspace project, then run +npm run serve:os:cli:local . For a little more information, see the SED-4557 ticket. + +To start the backend and use the application, start the StepUp main class. This technically launches the actual controller, +and a client app that is basically a browser. Once started, you can also use a regular browser pointed to http://localhost:4201 . + diff --git a/step-ap-ide/TESTS b/step-ap-ide/TESTS new file mode 100644 index 0000000000..5473711052 --- /dev/null +++ b/step-ap-ide/TESTS @@ -0,0 +1,14 @@ +Plan: + +Mode (PER_OBJECT / FRAGMENT), foreach: +create new with different name (3x); foreach + rename to longer name + rename back to shorter name + delete + +rename to existing (ie conflicting) name + + +BUGS: +1. Adding Plan to fragment does not make the fragment visible in AP (i.e. does not add to fragments section) +2. diff --git a/step-ap-ide/pom.xml b/step-ap-ide/pom.xml new file mode 100644 index 0000000000..4e32a62cc3 --- /dev/null +++ b/step-ap-ide/pom.xml @@ -0,0 +1,130 @@ + + 4.0.0 + + step-ap-ide + ${project.groupId}:${project.artifactId} + jar + + + ch.exense.step + step + 0.0.0-SNAPSHOT + + + + 21.0.2 + + + + + ch.exense.step + step-framework-server + + + ch.exense.step + step-core + ${project.version} + + + ch.exense.step + step-controller-backend + ${project.version} + + + ch.exense.step + step-automation-packages-collections + ${project.version} + + + + org.openjfx + javafx-controls + ${javafx.version} + win + + + org.openjfx + javafx-web + ${javafx.version} + win + + + + org.openjfx + javafx-controls + ${javafx.version} + linux + + + org.openjfx + javafx-web + ${javafx.version} + linux + + + + org.openjfx + javafx-controls + ${javafx.version} + mac + + + org.openjfx + javafx-web + ${javafx.version} + mac + + + + org.openjfx + javafx-controls + ${javafx.version} + mac-aarch64 + + + org.openjfx + javafx-web + ${javafx.version} + mac-aarch64 + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.2 + + + package + + shade + + + false + + + + + step.ap_ide.StepUp + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + diff --git a/step-ap-ide/src/main/java/step/ap_ide/CurrentlyOpenedAutomationPackageCollectionFactory.java b/step-ap-ide/src/main/java/step/ap_ide/CurrentlyOpenedAutomationPackageCollectionFactory.java new file mode 100644 index 0000000000..c7cb2ee986 --- /dev/null +++ b/step-ap-ide/src/main/java/step/ap_ide/CurrentlyOpenedAutomationPackageCollectionFactory.java @@ -0,0 +1,63 @@ +package step.ap_ide; + +import step.core.collections.AutomationPackageCollectionFactory; +import step.core.collections.Collection; +import step.core.collections.CollectionFactory; +import step.core.collections.EntityVersion; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +public class CurrentlyOpenedAutomationPackageCollectionFactory implements CollectionFactory { + + private static CurrentlyOpenedAutomationPackageCollectionFactory INSTANCE; + + private AutomationPackageCollectionFactory currentAPFactory; + private final ConcurrentHashMap> collectionsByName = new ConcurrentHashMap<>(); + + public CurrentlyOpenedAutomationPackageCollectionFactory(Properties ignored) { + if (INSTANCE == null) { + INSTANCE = this; + } else { + throw new IllegalStateException("Only one instance is allowed"); + } + } + + public static CurrentlyOpenedAutomationPackageCollectionFactory getInstance() { + if (INSTANCE == null) { + throw new IllegalStateException("No instance created yet"); + } + return INSTANCE; + } + + public void setCurrentFactory(AutomationPackageCollectionFactory currentAPFactory) { + if (this.currentAPFactory != null) { + try { + this.currentAPFactory.close(); + } catch (IOException e) { + // TODO better logging + e.printStackTrace(); + } + } + this.currentAPFactory = currentAPFactory; + collectionsByName.values().forEach(collection -> collection.setFromCurrentFactory(currentAPFactory)); + } + + @SuppressWarnings("unchecked") + @Override + public Collection getCollection(String name, Class entityClass) { + return (Collection) collectionsByName.computeIfAbsent(name, n -> new DynamicallyDelegatingCollection(name, entityClass, currentAPFactory)); + } + + @SuppressWarnings("rawtypes") + @Override + public Collection getVersionedCollection(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() throws IOException { + + } +} diff --git a/step-ap-ide/src/main/java/step/ap_ide/DynamicallyDelegatingCollection.java b/step-ap-ide/src/main/java/step/ap_ide/DynamicallyDelegatingCollection.java new file mode 100644 index 0000000000..a56fb47e8c --- /dev/null +++ b/step-ap-ide/src/main/java/step/ap_ide/DynamicallyDelegatingCollection.java @@ -0,0 +1,140 @@ +package step.ap_ide; + +import step.core.collections.Collection; +import step.core.collections.CollectionFactory; +import step.core.collections.Filter; +import step.core.collections.IndexField; +import step.core.collections.Order; +import step.core.collections.SearchOrder; +import step.core.collections.inmemory.InMemoryCollection; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +public class DynamicallyDelegatingCollection implements Collection { + private final String name; + private final Class type; + private final Collection fallback = new InMemoryCollection<>(); + private final AtomicReference> currentCollection = new AtomicReference<>(null); + + public DynamicallyDelegatingCollection(String name, Class type, CollectionFactory currentFactory) { + this.name = name; + this.type = type; + setFromCurrentFactory(currentFactory); + } + + public void setFromCurrentFactory(CollectionFactory currentFactory) { + if (currentFactory != null) { + currentCollection.set(currentFactory.getCollection(name, type)); + } else { + currentCollection.set(null); + } + } + + private Collection current() { + return Optional.ofNullable(currentCollection.get()).orElse(fallback); + } + + @Override + public String getName() { + return name; + } + + @Override + public long count(Filter filter, Integer limit) { + return current().count(filter, limit); + } + + @Override + public long estimatedCount() { + return current().estimatedCount(); + } + + @Override + public Stream find(Filter filter, SearchOrder order, Integer skip, Integer limit, int maxTime) { + return current().find(filter, order, skip, limit, maxTime); + } + + @Override + public Stream findLazy(Filter filter, SearchOrder order, Integer skip, Integer limit, int maxTime) { + return current().findLazy(filter, order, skip, limit, maxTime); + } + + @Override + public Stream findReduced(Filter filter, SearchOrder order, Integer skip, Integer limit, int maxTime, List reduceFields) { + return current().findReduced(filter, order, skip, limit, maxTime, reduceFields); + } + + @Override + public List distinct(String columnName, Filter filter) { + return current().distinct(columnName, filter); + } + + @Override + public void remove(Filter filter) { + current().remove(filter); + } + + @Override + public T save(T entity) { + return current().save(entity); + } + + @Override + public void save(Iterable entities) { + current().save(entities); + } + + @Override + public void createOrUpdateIndex(String field) { + // no-op + } + + @Override + public void createOrUpdateIndex(IndexField indexField) { + // no-op + + } + + @Override + public void createOrUpdateIndex(String field, Order order) { + // no-op + + } + + @Override + public void createOrUpdateCompoundIndex(String... fields) { + // no-op + + } + + @Override + public void createOrUpdateCompoundIndex(LinkedHashSet linkedHashSet) { + // no-op + + } + + @Override + public void rename(String newName) { + // no-op + + } + + @Override + public void drop() { + throw new UnsupportedOperationException(); + } + + @Override + public Class getEntityClass() { + return type; + } + + @Override + public void dropIndex(String indexName) { + //no-op + } +} diff --git a/step-ap-ide/src/main/java/step/ap_ide/FXApp.java b/step-ap-ide/src/main/java/step/ap_ide/FXApp.java new file mode 100644 index 0000000000..3c37d5aaaf --- /dev/null +++ b/step-ap-ide/src/main/java/step/ap_ide/FXApp.java @@ -0,0 +1,129 @@ +package step.ap_ide; + +import javafx.application.Application; +import javafx.concurrent.Worker; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ToolBar; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import javafx.scene.web.WebView; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Files; + +public class FXApp extends Application { + + private static final Logger logger = LoggerFactory.getLogger(FXApp.class); + + private final WebView webView = new WebView(); + private final MenuBar menuBar = new MenuBar(); + private final ToolBar toolBar = new ToolBar(); + + private void initWebView() { + // Route JS alerts and errors to log + webView.getEngine().setOnAlert(event -> logger.warn("JS Alert: {}", event.getData())); + webView.getEngine().setOnError(event -> logger.error("JS Error: {}", event.getMessage())); + + webView.getEngine().getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> { + logger.debug("Webview load state: " + newState); + if (newState == Worker.State.FAILED) { + logger.error("Webview Network Error: " + webView.getEngine().getLoadWorker().getException()); + } + }); + + if (false) { + // Not sure what this was good for, but keeping it for now. + // 3. Inject JS error handler exactly when the Document object is created + webView.getEngine().documentProperty().addListener((obs, oldDoc, newDoc) -> { + if (newDoc != null) { + try { + webView.getEngine().executeScript("window.onerror = function(msg, url, line) { alert(msg + ' at line ' + line); };"); + } catch (Exception e) { + logger.error("Could not inject error handler: " + e.getMessage()); + } + } + }); + } + } + + private void initMenuBar() { + Menu fileMenu = new Menu("File"); + MenuItem exitItem = new MenuItem("Exit"); + exitItem.setOnAction(e -> System.exit(0)); + fileMenu.getItems().add(exitItem); + menuBar.getMenus().add(fileMenu); + } + + private void initToolBar() { + Button reloadButton = new Button("Reload"); + reloadButton.setOnAction(e -> webView.getEngine().reload()); + + Button dumpHtmlButton = new Button("Dump HTML"); + dumpHtmlButton.setOnAction(e -> { + try { + String html = (String) webView.getEngine().executeScript("document.documentElement.outerHTML"); + System.out.println("--- CURRENT HTML ---"); + System.out.println(html); + System.out.println("--------------------"); + } catch (Exception ex) { + System.err.println("Cannot read HTML yet: " + ex.getMessage()); + } + }); + + Button newEmptyApButton = new Button("New Empty AP"); + newEmptyApButton.setOnAction(e -> { + try { + File workDir = Files.createTempDirectory("automationPackageCollectionTest").toFile(); + System.err.println("CREATED AND NOW USING NEW WORKDIR: " + workDir.getAbsolutePath()); + StepUp.useAutomationPackageDirectory(workDir); + webView.getEngine().reload(); + } catch (Exception ex) { + logger.error("error", ex); + } + }); + + toolBar.getItems().addAll(reloadButton, dumpHtmlButton, newEmptyApButton); + } + + private Scene initScene() { + VBox topContainer = new VBox(menuBar, toolBar); + BorderPane root = new BorderPane(); + root.setTop(topContainer); + root.setCenter(webView); + + return new Scene(root, 1600, 1000); + } + + @Override + public void start(Stage stage) { + stage.setTitle("StepUp"); + stage.setOnCloseRequest(event -> { + logger.info("Window closed, terminating JVM..."); + System.exit(0); + }); + + initWebView(); + initMenuBar(); + initToolBar(); + + stage.setScene(initScene()); + stage.show(); + + webView.getEngine().load("http://127.0.0.1:4201"); + } + + public static void main(String[] args) { + // absolutely required when using dev frontend, otherwise + // FX webview will (wrongly) try to upgrade non-SSL HTTP requests to HTTP2, + // and Frontend will (wrongly) never answer these upgrade requests. State of IT in 2026. + System.setProperty("com.sun.webkit.useHTTP2Loader", "false"); + launch(args); + } +} diff --git a/step-ap-ide/src/main/java/step/ap_ide/StepUp.java b/step-ap-ide/src/main/java/step/ap_ide/StepUp.java new file mode 100644 index 0000000000..938961b7f2 --- /dev/null +++ b/step-ap-ide/src/main/java/step/ap_ide/StepUp.java @@ -0,0 +1,120 @@ +package step.ap_ide; + +import ch.exense.commons.app.Configuration; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import step.automation.packages.AutomationPackageHookRegistry; +import step.automation.packages.JavaAutomationPackageArchive; +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.core.collections.AutomationPackageCollectionFactory; +import step.framework.server.ControllerServer; +import step.parameter.Parameter; +import step.parameter.automation.AutomationPackageParametersRegistration; +import step.plans.parser.yaml.YamlPlan; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Objects; +import java.util.Properties; + +public class StepUp { + + private static final Logger logger = LoggerFactory.getLogger(StepUp.class); + + // These are meant for development to have something to play with without having to recreate everything from scratch + private static final String workDirName = "work"; + private static final String initialDirName = "src/main/resources/work-initial"; + + public static void main(String[] args) throws Exception { + Configuration configuration = new Configuration(); + // this uses the existing methods, we could also refactor the implementation to make it a little easier. + InputStream propsStream = StepUp.class.getClassLoader().getResourceAsStream("step.properties"); + configuration.getUnderlyingPropertyObject().load(propsStream); + new ControllerServer(configuration).start(); + initWorkdir(); + FXApp.main(args); // this will never return + } + + private static final JavaAutomationPackageReader READER; + + static { + AutomationPackageSerializationRegistry serializationRegistry = new AutomationPackageSerializationRegistry(); + AutomationPackageHookRegistry hookRegistry = new AutomationPackageHookRegistry(); + // required for reading parameters, apparently the manager can be null + AutomationPackageParametersRegistration.registerParametersHooks(hookRegistry, serializationRegistry, null); + READER = new JavaAutomationPackageReader(YamlAutomationPackageVersions.ACTUAL_JSON_SCHEMA_PATH, hookRegistry, serializationRegistry, new Configuration()); + } + + private static void initWorkdir() throws Exception { + File workDir = new File(workDirName); + File initialDir = new File(initialDirName); + if (!workDir.isDirectory() || (workDir.isDirectory() && Objects.requireNonNull(workDir.listFiles()).length == 0)) { + logger.info("Work directory is not present or empty, initializing from " + initialDirName); + if (!initialDir.isDirectory()) { + workDir = Files.createTempDirectory("step-ap-ide-").toFile(); + logger.warn("initialDir is not present, using temporary directory: {}", workDir.getAbsolutePath()); + } else { + FileUtils.copyDirectory(initialDir, workDir); + if (!workDir.isDirectory()) { + throw new RuntimeException("Something went wrong while initializing directory: " + workDir.getAbsolutePath()); + } + } + } else { + logger.info("Using existing work directory at: " + workDir.getAbsolutePath()); + logger.info("You may delete this directory to start over from scratch"); + } + useAutomationPackageDirectory(workDir); + } + + static void useAutomationPackageDirectory(File apDir) throws Exception { + verifyOrCreateMainAPFile(apDir); + var fragmentManager = StepUp.READER.getAutomationPackageYamlFragmentManager(apDir); + Properties properties = new Properties(); + + // variant 1: + // parameters all go into parameters.yml, plans go into separate files in plans/$PLAN_NAME.yml + // Only works if the target files/directories already exist, so disabled for now + if (1 == 0) { + properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, Parameter.ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name()); + properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, Parameter.ENTITY_NAME), "parameters.yml"); + properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, YamlPlan.PLANS_ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.PER_OBJECT.name()); + // keywords seem to use PER_OBJECT by default? + } + // variant 2: simple, everything goes into main descriptor + if (1 == 1) { + String mainFile = Paths.get(fragmentManager.descriptorYaml.getFragmentUrl().toURI()).toFile().getName(); + properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, Parameter.ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name()); + properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, Parameter.ENTITY_NAME), mainFile); + properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, YamlPlan.PLANS_ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name()); + properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, YamlPlan.PLANS_ENTITY_NAME), mainFile); + properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, "keywords"), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name()); + properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, "keywords"), mainFile); + } + fragmentManager.setProperties(properties); + var automationPackageCollectionFactory = new AutomationPackageCollectionFactory(new Properties(), fragmentManager); + + CurrentlyOpenedAutomationPackageCollectionFactory.getInstance().setCurrentFactory(automationPackageCollectionFactory); + } + + private static void verifyOrCreateMainAPFile(File apDir) throws Exception { + for (String fileName : JavaAutomationPackageArchive.METADATA_FILES) { + if (new File(apDir, fileName).isFile()) { + return; + } + } + File descriptor = new File(apDir, JavaAutomationPackageArchive.METADATA_FILES.getFirst()); + logger.info("Initializing AP directory with new descriptor: {}", descriptor.getAbsolutePath()); + PrintWriter pw = new PrintWriter(new FileOutputStream(descriptor)); + pw.println("schemaVersion: 1.0.0"); + pw.println("name: \"My package\""); // TODO: make this configurable somehow + pw.close(); + } +} diff --git a/step-ap-ide/src/main/resources/logback.xml b/step-ap-ide/src/main/resources/logback.xml new file mode 100644 index 0000000000..3b6d6bcaed --- /dev/null +++ b/step-ap-ide/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff --git a/step-ap-ide/src/main/resources/step.properties b/step-ap-ide/src/main/resources/step.properties new file mode 100644 index 0000000000..9681de1e36 --- /dev/null +++ b/step-ap-ide/src/main/resources/step.properties @@ -0,0 +1,69 @@ +controller.url=http://localhost:8080/ +port=8080 +#These cannot be disabled +plugins.StepControllerPlugin.enabled=true +plugins.MigrationExecutionPlugin.enabled=true +plugins.MigrationManagerPlugin.enabled=true +#These are required to get the non-disabled ones to work (in order of crashes i.e. dependencies) +plugins.VersionManagerPlugin.enabled=true +plugins.SchedulerPlugin.enabled=true +plugins.ControllerSettingPlugin.enabled=true +plugins.ObjectHookControllerPlugin.enabled=true +plugins.AutomationPackagePlugin.enabled=true +plugins.FunctionControllerPlugin.enabled=true +plugins.GridPlugin.enabled=true +plugins.AsyncTaskManagerPlugin.enabled=true +# The following are needed too as they cause runtime exceptions or other errors if absent +plugins.ScreenTemplatePlugin.enabled=true +plugins.PlanPlugin.enabled=true +plugins.TablePlugin.enabled=true +plugins.TableSettingsPlugin.enabled=true +# UI errors (only???) +plugins.BookmarkPlugin.enabled=true +plugins.ParameterManagerControllerPlugin.enabled=true +plugins.ExecutionPlugin.enabled=true +plugins.ExecutionTypeControllerPlugin.enabled=true +#Required functionality +plugins.CompositeFunctionTypeControllerPlugin.enabled=true +#Disable all other plugins for now +plugins.ArtifactRepositoryPlugin.enabled=false +plugins.AutomationPackageParameterPlugin.enabled=false +plugins.AutomationPackageRepositoriesPlugin.enabled=false +plugins.AutomationPackageSchedulerPlugin.enabled=false +plugins.BaseExecutionTypeControllerPlugin.enabled=false +plugins.DataPoolPlugin.enabled=false +plugins.EncryptionManagerDependencyPlugin.enabled=false +plugins.EntityLockingPlugin.enabled=false +plugins.ExportManagerPlugin.enabled=false +plugins.FunctionPackagePlugin.enabled=false +plugins.GeneralScriptFunctionControllerPlugin.enabled=false +plugins.InteractivePlugin.enabled=false +plugins.JMeterPlugin.enabled=false +plugins.LiveReportingControllerPlugin.enabled=false +plugins.MeasurementControllerPlugin.enabled=false +plugins.MigrationManagerTasksPlugin.enabled=false +plugins.MigrationTasksRegistrationPlugin.enabled=false +plugins.NodePlugin.enabled=false +plugins.QuotaManagerControllerPlugin.enabled=false +plugins.RawMeasurementsControllerPlugin.enabled=false +plugins.ReferenceFinderPlugin.enabled=false +plugins.RemoteCollectionPlugin.enabled=false +plugins.ResourceManagerControllerPlugin.enabled=false +plugins.ReportLayoutPlugin.enabled=false +plugins.ScriptEditorPlugin.enabled=false +plugins.StagingRepositoryPlugin.enabled=false +plugins.StreamingResourcesControllerPlugin.enabled=false +plugins.ThreadManagerControllerPlugin.enabled=false +plugins.TimeSeriesControllerPlugin.enabled=false +plugins.VersionProviderPlugin.enabled=false +plugins.ViewControllerPlugin.enabled=false +plugins.YamlEditorPlanPlugin.enabled=false +#datasource.db.type=step.core.collections.mongodb.MongoDBCollectionFactory +#datasource.db.collections=all +#datasource.db.properties.host=localhost +#datasource.db.properties.database=wtf +datasource.mem.type=step.core.collections.inmemory.InMemoryCollectionFactory +#datasource.mem.collections=controllerlogs +datasource.mem.collections=all +datasource.ap.type=step.ap_ide.CurrentlyOpenedAutomationPackageCollectionFactory +datasource.ap.collections=wtf,plans,functions,parameters diff --git a/step-ap-ide/src/main/resources/work-initial/.apignore b/step-ap-ide/src/main/resources/work-initial/.apignore new file mode 100644 index 0000000000..319325c32d --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/.apignore @@ -0,0 +1,2 @@ +/ignored +/ignoredFile.yml \ No newline at end of file diff --git a/step-ap-ide/src/main/resources/work-initial/automation-package.yml b/step-ap-ide/src/main/resources/work-initial/automation-package.yml new file mode 100644 index 0000000000..dc1f2ce474 --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/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: "parameterInMainDescriptor" + value: "value1" +fragments: + - "keywords.yml" + - "plans/*.yml" + - "schedules.yml" + - "parameters.yml" + - "parameters2.yml" + - "unknown.yml" diff --git a/step-ap-ide/src/main/resources/work-initial/ignoredFile.yml b/step-ap-ide/src/main/resources/work-initial/ignoredFile.yml new file mode 100644 index 0000000000..06f858207b --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/ignoredFile.yml @@ -0,0 +1 @@ +#I should be ignored \ No newline at end of file diff --git a/step-ap-ide/src/main/resources/work-initial/keywords.yml b/step-ap-ide/src/main/resources/work-initial/keywords.yml new file mode 100644 index 0000000000..5d33a14488 --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/keywords.yml @@ -0,0 +1,30 @@ +keywords: + - JMeter: + name: "JMeter keyword from automation package" + description: "JMeter keyword 1" + executeLocally: false + useCustomTemplate: true + callTimeout: 1000 + jmeterTestplan: "jmeterProject1/jmeterProject1.xml" + - Composite: + name: "Composite keyword from AP" + plan: + root: + testCase: + children: + - echo: + text: "Just echo" + - return: + output: + - output1: "value" + - output2: + expression: "'some thing dynamic'" + - GeneralScript: + name: "GeneralScript keyword from AP" + scriptLanguage: javascript + scriptFile: "jsProject/jsSample.js" + librariesFile: "lib/fakeLib.jar" + - Node: + name: "NodeAutomation" + jsfile: "nodeProject/nodeSample.ts" + \ No newline at end of file diff --git a/step-ap-ide/src/main/resources/work-initial/parameters.yml b/step-ap-ide/src/main/resources/work-initial/parameters.yml new file mode 100644 index 0000000000..1909ed829c --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/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-ap-ide/src/main/resources/work-initial/parameters2.yml b/step-ap-ide/src/main/resources/work-initial/parameters2.yml new file mode 100644 index 0000000000..8754e85903 --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/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-ap-ide/src/main/resources/work-initial/plan.plan b/step-ap-ide/src/main/resources/work-initial/plan.plan new file mode 100644 index 0000000000..66b7ca6d84 --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/plan.plan @@ -0,0 +1,3 @@ +Sequence +Echo "Testing annotated plan" +End \ No newline at end of file diff --git a/step-ap-ide/src/main/resources/work-initial/plans/plan1.yml b/step-ap-ide/src/main/resources/work-initial/plans/plan1.yml new file mode 100644 index 0000000000..26e6c014f5 --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/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-ap-ide/src/main/resources/work-initial/plans/plan2.plan b/step-ap-ide/src/main/resources/work-initial/plans/plan2.plan new file mode 100644 index 0000000000..66b7ca6d84 --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/plans/plan2.plan @@ -0,0 +1,3 @@ +Sequence +Echo "Testing annotated plan" +End \ No newline at end of file diff --git a/step-ap-ide/src/main/resources/work-initial/plans/plan2.yml b/step-ap-ide/src/main/resources/work-initial/plans/plan2.yml new file mode 100644 index 0000000000..2a576d02da --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/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-ap-ide/src/main/resources/work-initial/plansPlainText/firstPlainText.plan b/step-ap-ide/src/main/resources/work-initial/plansPlainText/firstPlainText.plan new file mode 100644 index 0000000000..e9dd736886 --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/plansPlainText/firstPlainText.plan @@ -0,0 +1,3 @@ +Sequence +Echo "First plain text plan" +End \ No newline at end of file diff --git a/step-ap-ide/src/main/resources/work-initial/plansPlainText/secondPlainText.plan b/step-ap-ide/src/main/resources/work-initial/plansPlainText/secondPlainText.plan new file mode 100644 index 0000000000..00c3bacb0d --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/plansPlainText/secondPlainText.plan @@ -0,0 +1,3 @@ +Sequence +Echo "Second plain text plan" +End \ No newline at end of file diff --git a/step-ap-ide/src/main/resources/work-initial/schedules.yml b/step-ap-ide/src/main/resources/work-initial/schedules.yml new file mode 100644 index 0000000000..6d95ed7161 --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/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-ap-ide/src/main/resources/work-initial/unknown.yml b/step-ap-ide/src/main/resources/work-initial/unknown.yml new file mode 100644 index 0000000000..d378e6078d --- /dev/null +++ b/step-ap-ide/src/main/resources/work-initial/unknown.yml @@ -0,0 +1,3 @@ +unknown: + - someFieldA: valueA + someFieldB: valueB \ No newline at end of file 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..b2066ba5f8 --- /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 + + + + 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..b5869177dd --- /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..813aad256b --- /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..ad7bfdfc2f --- /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..a68b9488a6 --- /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..d7d707f7eb --- /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..395e245be3 --- /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..475e4c8b80 --- /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..6dca1e6e92 --- /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..9a4a97b961 --- /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..d6ee24d376 --- /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..cbb10acc18 --- /dev/null +++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/expected/plan1AfterRemove.yml @@ -0,0 +1,10 @@ +--- +fragments: [] +keywords: [] +plans: [] +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..91fc8259f6 --- /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..3ed06774ee --- /dev/null +++ b/step-automation-packages/step-automation-packages-collections/src/test/resources/testdata/ap1/jmeterProject1/jmeterProject1.xml @@ -0,0 +1,4 @@ + + + + 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..4fead6c631 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()) { @@ -181,8 +205,10 @@ public void fillAutomationPackageWithImportedFragments(AutomationPackageContent List resources = archive.getResourcesByPattern(importedFragmentReference); for (URL resource : resources) { try (InputStream fragmentYamlStream = resource.openStream()) { - fragment = getOrCreateDescriptorReader().readAutomationPackageFragment(fragmentYamlStream, importedFragmentReference, archive.getAutomationPackageName()); - fillAutomationPackageWithImportedFragments(targetPackage, fragment, archive); + fragment = getOrCreateDescriptorReader().readAutomationPackageFragment(fragmentYamlStream, resource.toString(), archive.getAutomationPackageName()); + 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..4e139641cc 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,6 +18,7 @@ ******************************************************************************/ package step.automation.packages.yaml; +import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @@ -29,13 +30,14 @@ 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.PatchingContext; +import step.core.yaml.deserialization.PatchableYamlList; +import step.core.yaml.deserialization.PatchingParserDelegate; import step.plans.parser.yaml.YamlPlanReader; import step.plans.parser.yaml.model.YamlPlanVersions; import step.plans.parser.yaml.schema.YamlPlanValidationException; @@ -43,7 +45,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.List; +import java.util.HashMap; import java.util.Map; public class AutomationPackageDescriptorReader { @@ -72,7 +74,7 @@ public AutomationPackageDescriptorReader(String jsonSchemaPath, AutomationPackag public AutomationPackageDescriptorYaml readAutomationPackageDescriptor(InputStream yamlDescriptor, String packageName) throws AutomationPackageReadingException { log.info("Reading automation package descriptor..."); - return readAutomationPackageYamlFile(yamlDescriptor, getDescriptorClass(), packageName); + return readAutomationPackageYamlFile("automation-package.yml", yamlDescriptor, getDescriptorClass(), packageName); } protected Class getDescriptorClass() { @@ -81,14 +83,14 @@ protected Class getDescriptorClass() public AutomationPackageFragmentYaml readAutomationPackageFragment(InputStream yamlFragment, String fragmentName, String packageName) throws AutomationPackageReadingException { log.info("Reading automation package descriptor fragment ({})...", fragmentName); - return readAutomationPackageYamlFile(yamlFragment, getFragmentClass(), packageName); + return readAutomationPackageYamlFile(fragmentName, yamlFragment, getFragmentClass(), packageName); } protected Class getFragmentClass() { return AutomationPackageFragmentYamlImpl.class; } - protected T readAutomationPackageYamlFile(InputStream yaml, Class targetClass, String packageName) throws AutomationPackageReadingException { + protected T readAutomationPackageYamlFile(String location, InputStream yaml, Class targetClass, String packageName) throws AutomationPackageReadingException { try { String yamlDescriptorString = new String(yaml.readAllBytes(), StandardCharsets.UTF_8); String version = null; @@ -104,9 +106,25 @@ protected T readAutomationPackageYamlF throw new YamlPlanValidationException(message, ex); } } + PatchingContext context = new PatchingContext(location, 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 +142,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 +161,17 @@ 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); + yamlFactory.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR); 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..a6487f001b --- /dev/null +++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageYamlFragmentManager.java @@ -0,0 +1,344 @@ +/******************************************************************************* + * 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.PatchingContext; +import step.core.yaml.deserialization.AutomationPackagePerObjectSaveUnsupportedException; +import step.core.yaml.deserialization.AutomationPackageUpdateException; +import step.core.yaml.deserialization.PatchableYamlList; +import step.functions.Function; +import step.parameter.Parameter; +import step.parameter.automation.AutomationPackageParameter; +import step.plans.parser.yaml.YamlPlan; +import step.plugins.functions.types.CompositeFunction; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class AutomationPackageYamlFragmentManager { + + + protected final Path apRoot; + protected 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"; + protected final AutomationPackageDescriptorReader descriptorReader; + + protected final Map patchableMap = new ConcurrentHashMap<>(); + protected final Map fragmentMap = new ConcurrentHashMap<>(); + protected final Map pathToYamlFragment; + + protected Properties properties = new Properties(); + public 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 oldYamlPlan = (YamlPlan) patchableMap.get(plan); + modifyFragmentEntity(fragment, fragment.getPlans(), oldYamlPlan, 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, "keywords"); + fragmentMap.put(function, fragment); + pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment); + YamlAutomationPackageKeyword newKeyword = createNewYamlKeyword(function); + if (newKeyword != null) { + patchableMap.put(function, newKeyword); + addFragmentEntity(fragment, fragment.getKeywords(), newKeyword); + } else { + System.err.println("SAVING OF FUNCTION OF TYPE " + function.getClass().getName() + " IS NOT CURRENTLY SUPPORTED"); + } + } else { + YamlAutomationPackageKeyword yamlKeyword = (YamlAutomationPackageKeyword) patchableMap.get(function); + yamlKeyword.getYamlKeyword().updateFromFunction(function); + modifyFragmentEntity(fragment, fragment.getKeywords(), yamlKeyword, yamlKeyword); + } + return function; + } + + private YamlAutomationPackageKeyword createNewYamlKeyword(step.functions.Function function) { + // FIXME: I know, this is is a giant horrible stinking hack for now, there needs to be a better way. + if (function instanceof CompositeFunction compositeFunction) { + try { + // I don't know what the proper way is to serialize this, but we know that deserialization should work... + YamlPlan plan = descriptorReader.getPlanReader().planToYamlPlan(compositeFunction.getPlan()); + plan.setName(null); + // we only want to use the serialization functions here + PatchingContext patchingContext = new PatchingContext(descriptorReader.yamlObjectMapper); + StringBuilder yaml = new StringBuilder(""" + keywords: + - Composite: + plan: + """); + yaml.append(patchingContext.serialize(plan, " ".repeat(8))); + + // There are more attributes, this is just a PoC anyway + Optional.ofNullable(function.getAttribute("name")).ifPresent(value -> { + yaml.append(patchingContext.serialize(Map.of("name", value), " ".repeat(6))); + }); + Optional.ofNullable(function.getDescription()).ifPresent(value -> { + yaml.append(patchingContext.serialize(Map.of("description", value), " ".repeat(6))); + }); + + InputStream is = new ByteArrayInputStream(yaml.toString().getBytes()); + var fragment = descriptorReader.readAutomationPackageFragment(is, "horrible-hack", "horrible-hack"); + return fragment.getKeywords().stream().findFirst().orElse(null); + } catch (Exception e) { + // TODO: better error handling + e.printStackTrace(); + return null; + } + } else { + return null; + } + } + + 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(url.toString(), "---", descriptorYaml.getPatchingContext().getMapper()); + AutomationPackageFragmentYaml fragment = new AutomationPackageFragmentYamlImpl(context); + fragment.setFragmentUrl(url); + return fragment; + } catch (MalformedURLException e) { + throw new AutomationPackageUpdateException(MessageFormat.format("Error creating path for new fragment: {0}", path), e); + } + + } + + 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.getKeywords().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; + if (fragmentMap.get(object) == null) { + fragment = fragmentForNewObject(object, fieldName); + + YO newYamlObject = newYamlObjectCreator.apply(fragment.getPatchingContext()); + PatchableYamlList list = (PatchableYamlList) fragment.getAdditionalFields() + .computeIfAbsent(fieldName, k -> new PatchableYamlList(fragment.getPatchingContext(), fieldName)); + fragmentMap.put(object, fragment); + pathToYamlFragment.put(fragment.getFragmentUrl().toString(), fragment); + addFragmentEntity(fragment, list, newYamlObject); + patchableMap.put(object, newYamlObject); + } else { + fragment = fragmentMap.get(object); + YO newYamlObject = newYamlObjectCreator.apply(fragment.getPatchingContext()); + PatchableYamlList list = (PatchableYamlList) fragment.getAdditionalFields() + .computeIfAbsent(fieldName, k -> new PatchableYamlList(fragment.getPatchingContext(), fieldName)); + + YO oldYamlObject = (YO) patchableMap.get(object); + modifyFragmentEntity(fragment, list, oldYamlObject, newYamlObject); + patchableMap.put(object, newYamlObject); + } + 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..815cfaaa2f --- /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.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +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.PatchingContext; +import step.core.yaml.deserialization.PatchableYamlList; + +import java.io.IOException; + +public abstract class AbstractYamlAutomationPackageFragmentDeserializer extends BeanDeserializer implements ContextualDeserializer, ResolvableDeserializer { + + private final BeanDeserializer delegate; + + public AbstractYamlAutomationPackageFragmentDeserializer(BeanDeserializer deserializer) { + super(deserializer); + delegate = deserializer; + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return deserialize(p, ctxt, new AutomationPackageFragmentYamlImpl((PatchingContext) ctxt.getAttribute(PatchingContext.class))); + } + + @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..d009a93eb9 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,37 @@ ******************************************************************************/ 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.core.yaml.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..521d7a4137 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,37 @@ ******************************************************************************/ package step.automation.packages.yaml.deserialization; -import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.node.ObjectNode; -import step.automation.packages.deserialization.AutomationPackageSerializationRegistry; -import step.automation.packages.deserialization.AutomationPackageSerializationRegistryAware; -import step.automation.packages.yaml.model.AbstractAutomationPackageFragmentYaml; -import step.automation.packages.yaml.model.AutomationPackageDescriptorYamlImpl; +import 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.AutomationPackageFragmentYamlImpl; -import step.core.yaml.deserializers.StepYamlDeserializer; +import step.core.yaml.PatchingContext; import step.core.yaml.deserializers.StepYamlDeserializerAddOn; -import step.core.yaml.SerializationUtils; import java.io.IOException; -import java.util.*; @StepYamlDeserializerAddOn(targetClasses = {AutomationPackageFragmentYamlImpl.class}) -public class YamlAutomationPackageFragmentDeserializer extends StepYamlDeserializer - implements AutomationPackageSerializationRegistryAware { +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..9fd9baabbd 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 @@ -20,12 +20,14 @@ 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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; @@ -33,40 +35,34 @@ @StepYamlDeserializerAddOn(targetClasses = {YamlAutomationPackageKeyword.class}) public class YamlKeywordDeserializer extends StepYamlDeserializer { + private static final Logger logger = LoggerFactory.getLogger(YamlKeywordDeserializer.class); - 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; - } + String yamlName = jsonParser.nextFieldName(); + + try { + PatchingParserDelegate patchingParser = (PatchingParserDelegate) jsonParser; + Class clazz = Class.forName(keywordsLookuper.yamlKeywordClassToJava(yamlName)); + swallowToken(jsonParser); + logger.debug("{} About to deserialize YamlAutomationPackageKeyword (yamlName={} -> class={})", this, yamlName, clazz.getName()); + YamlAutomationPackageKeyword keyword = new YamlAutomationPackageKeyword((AbstractYamlFunction) deserializationContext.readValue(jsonParser, clazz), patchingParser.getPatchingContext()); + swallowToken(jsonParser); + return keyword; + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } - 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())); + private void swallowToken(JsonParser jsonParser) throws IOException { + var tk = jsonParser.nextToken(); + logger.debug("{} Swallowed token: {}", this, tk); } } 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..c4891c8311 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,69 @@ ******************************************************************************/ package step.automation.packages.yaml.model; +import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; +import org.apache.commons.io.FileUtils; import step.automation.packages.model.YamlAutomationPackageKeyword; +import step.automation.packages.yaml.AutomationPackageWriteToDiskException; +import step.core.yaml.PatchingContext; +import step.core.yaml.deserialization.AutomationPackageConcurrentEditException; +import step.core.yaml.deserialization.PatchableYamlList; import step.plans.automation.YamlPlainTextPlan; import step.plans.parser.yaml.YamlPlan; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +@JsonInclude(JsonInclude.Include.NON_EMPTY) public abstract class AbstractAutomationPackageFragmentYaml implements AutomationPackageFragmentYaml { private List fragments = new ArrayList<>(); - private List keywords = new ArrayList<>(); - private List plans = new ArrayList<>(); - private List plansPlainText = new ArrayList<>(); + private PatchableYamlList keywords; + private PatchableYamlList plans; + private PatchableYamlList plansPlainText; + + 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); + plansPlainText = new PatchableYamlList<>(patchingContext, "plansPlainText"); + } @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 +94,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 @@ -83,7 +111,49 @@ public List getPlansPlainText() { } @JsonSetter(nulls = Nulls.AS_EMPTY) - public void setPlansPlainText(List plansPlainText) { + public void setPlansPlainText(PatchableYamlList 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.getCurrentYaml(), StandardCharsets.UTF_8); + resetLastModified(); + } catch (IOException | URISyntaxException e) { + throw new AutomationPackageWriteToDiskException(MessageFormat.format("Error when writing automation package fragment {0} back to disk.", url), e); + } + } } 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..32fbed50a6 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,10 @@ ******************************************************************************/ package step.automation.packages.yaml.model; +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.OptBoolean; +import step.core.yaml.PatchingContext; + import java.util.HashMap; import java.util.Map; @@ -29,6 +33,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 +63,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..7cf2be19b3 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 @@ -19,25 +19,41 @@ package step.automation.packages.yaml.model; import step.automation.packages.model.YamlAutomationPackageKeyword; +import step.core.yaml.PatchingContext; +import step.core.yaml.deserialization.PatchableYamlList; 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..d1c87063fe 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,13 @@ ******************************************************************************/ package step.automation.packages.yaml.model; +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.OptBoolean; +import step.core.yaml.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-automation-packages/step-automation-packages-yaml/src/test/java/step/core/yaml/PatchingContextTest.java b/step-automation-packages/step-automation-packages-yaml/src/test/java/step/core/yaml/PatchingContextTest.java new file mode 100644 index 0000000000..d4279eddfb --- /dev/null +++ b/step-automation-packages/step-automation-packages-yaml/src/test/java/step/core/yaml/PatchingContextTest.java @@ -0,0 +1,234 @@ +package step.core.yaml; + +import org.junit.Assert; +import org.junit.Test; +import step.automation.packages.deserialization.AutomationPackageSerializationRegistry; +import step.automation.packages.yaml.AutomationPackageDescriptorReader; +import step.automation.packages.yaml.YamlAutomationPackageVersions; +import step.automation.packages.yaml.model.AutomationPackageDescriptorYaml; +import step.core.scheduler.automation.AutomationPackageSchedule; +import step.core.scheduler.automation.AutomationPackageScheduleRegistration; +import step.core.yaml.deserialization.PatchableYamlList; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class PatchingContextTest { + + private static final String EXPECTED_UNMODIFIED = """ + schemaVersion: 1.0.0 + name: "complete-package" + fragments: + - "importPlans.yml" + - "importKeywords.yml" + keywords: + - JMeter: + name: "JMeter keyword from automation package" + description: "JMeter keyword 1" + executeLocally: false + useCustomTemplate: true + callTimeout: 1000 + jmeterTestplan: "jmeterProject1/jmeterProject1.xml" + routing: + criteriaA: valueA + criteriaB: valueB + schema: + type: object + properties: + firstName: + type: string + lastName: + type: string + required: [ "firstName", "lastName" ] + - Composite: + name: "Composite1" + plan: + root: + testCase: + children: + - echo: + text: "Just echo" + - return: + output: + - output1: "value" + - output2: + expression: "'some thing dynamic'" + plans: + - name: First Plan + root: + sequence: + continueOnError: false + children: + - if: + nodeName: IfBlock + condition: + expression: "controllerSettings.getSettingByKey('housekeeping_enabled').getValue()=='true'" + description: "my description" + children: + - assert: + actual: + expression: "'status'" + operator: "EQUALS" + doNegate: false + expected: + expression: "'ok'" + customErrorMessage: "my custom error" + - name: Second Plan + root: + testCase: + children: + - echo: + text: "Just echo" + schedules: + - name: "My first task" + cron: "*/5 * * * *" + executionParameters: + environment: "TEST" + planName: "First Plan" + - name: "My second task" + cron: "0 * * * *" + executionParameters: + environment: "PROD" + planName: "Second Plan" + """; + + private static final String EXPECTED_REMOVED_1 = """ + schemaVersion: 1.0.0 + name: "complete-package" + fragments: + - "importPlans.yml" + - "importKeywords.yml" + keywords: + - Composite: + name: "Composite1" + plan: + root: + testCase: + children: + - echo: + text: "Just echo" + - return: + output: + - output1: "value" + - output2: + expression: "'some thing dynamic'" + plans: + - name: First Plan + root: + sequence: + continueOnError: false + children: + - if: + nodeName: IfBlock + condition: + expression: "controllerSettings.getSettingByKey('housekeeping_enabled').getValue()=='true'" + description: "my description" + children: + - assert: + actual: + expression: "'status'" + operator: "EQUALS" + doNegate: false + expected: + expression: "'ok'" + customErrorMessage: "my custom error" + schedules: + - name: "My first task" + cron: "*/5 * * * *" + executionParameters: + environment: "TEST" + planName: "First Plan" + """; + + private static final String EXPECTED_REMOVED_2_MODIFIED = """ + schemaVersion: 1.0.0 + name: "complete-package" + fragments: + - "importPlans.yml" + - "importKeywords.yml" + keywords: [] + plans: [] + schedules: + - name: "This is now the new schedule name which is rather long, really long in fact" + active: false + cron: "*/5 * * * *" + planName: "First Plan" + executionParameters: {} + """; + + + private final AutomationPackageDescriptorReader reader; + + public PatchingContextTest() { + AutomationPackageSerializationRegistry serializationRegistry = new AutomationPackageSerializationRegistry(); + AutomationPackageScheduleRegistration.registerSerialization(serializationRegistry); + reader = new AutomationPackageDescriptorReader(YamlAutomationPackageVersions.ACTUAL_JSON_SCHEMA_PATH, serializationRegistry); + } + + @Test + public void testLowLevelDeletionModification() throws Exception { + File file = new File("src/test/resources/step/automation/packages/yaml/descriptors/completeDescriptor2.yml"); + try (InputStream is = new FileInputStream(file)) { + AutomationPackageDescriptorYaml descriptor = reader.readAutomationPackageDescriptor(is, ""); + PatchingContext patchingContext = descriptor.getPatchingContext(); + String current = ensureValid(patchingContext.getCurrentYaml()); + Assert.assertEquals(EXPECTED_UNMODIFIED, current); + + // we know that the claimed chunks are in the order [keywords, plans, schedules] + List lists = patchingContext.chunks.values().stream() + .filter(m -> m instanceof PatchableYamlList) + .map(x -> (PatchableYamlList) x) + .toList(); + assertEquals(3, lists.size()); + // remove first keyword, second plan, second schedule + lists.get(0).remove(0); + lists.get(1).remove(1); + lists.get(2).remove(1); + + current = ensureValid(patchingContext.getCurrentYaml()); + Assert.assertEquals(EXPECTED_REMOVED_1, current); + + // empty first two lists, modify schedule a little + AutomationPackageSchedule schedule1 = (AutomationPackageSchedule) lists.get(2).getFirst(); + schedule1.setName("This is now the new schedule name which is rather long, really long in fact"); + schedule1.setActive(false); + schedule1.getExecutionParameters().clear(); + // we have to tell the entity explicitly that it was modified + schedule1.setModified(); + lists.subList(0, 2).forEach(ArrayList::removeFirst); + + current = ensureValid(patchingContext.getCurrentYaml()); + Assert.assertEquals(EXPECTED_REMOVED_2_MODIFIED, current); + + lists.get(2).removeFirst(); + current = ensureValid(patchingContext.getCurrentYaml()); + + Assert.assertEquals(""" + schemaVersion: 1.0.0 + name: "complete-package" + fragments: + - "importPlans.yml" + - "importKeywords.yml" + keywords: [] + plans: [] + schedules: [] + """, current); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String ensureValid(String yaml) throws Exception { + InputStream in = new ByteArrayInputStream(yaml.getBytes()); + // we're simply reading it and expecting it not to fail. + reader.readAutomationPackageDescriptor(in, ""); + return yaml; + } +} diff --git a/step-automation-packages/step-automation-packages-yaml/src/test/resources/step/automation/packages/yaml/descriptors/completeDescriptor2.yml b/step-automation-packages/step-automation-packages-yaml/src/test/resources/step/automation/packages/yaml/descriptors/completeDescriptor2.yml new file mode 100644 index 0000000000..df780f8314 --- /dev/null +++ b/step-automation-packages/step-automation-packages-yaml/src/test/resources/step/automation/packages/yaml/descriptors/completeDescriptor2.yml @@ -0,0 +1,74 @@ +schemaVersion: 1.0.0 +name: "complete-package" +fragments: + - "importPlans.yml" + - "importKeywords.yml" +keywords: + - JMeter: + name: "JMeter keyword from automation package" + description: "JMeter keyword 1" + executeLocally: false + useCustomTemplate: true + callTimeout: 1000 + jmeterTestplan: "jmeterProject1/jmeterProject1.xml" + routing: + criteriaA: valueA + criteriaB: valueB + schema: + type: object + properties: + firstName: + type: string + lastName: + type: string + required: [ "firstName", "lastName" ] + - Composite: + name: "Composite1" + plan: + root: + testCase: + children: + - echo: + text: "Just echo" + - return: + output: + - output1: "value" + - output2: + expression: "'some thing dynamic'" +plans: + - name: First Plan + root: + sequence: + continueOnError: false + children: + - if: + nodeName: IfBlock + condition: + expression: "controllerSettings.getSettingByKey('housekeeping_enabled').getValue()=='true'" + description: "my description" + children: + - assert: + actual: + expression: "'status'" + operator: "EQUALS" + doNegate: false + expected: + expression: "'ok'" + customErrorMessage: "my custom error" + - name: Second Plan + root: + testCase: + children: + - echo: + text: "Just echo" +schedules: + - name: "My first task" + cron: "*/5 * * * *" + executionParameters: + environment: "TEST" + planName: "First Plan" + - name: "My second task" + cron: "0 * * * *" + executionParameters: + environment: "PROD" + planName: "Second Plan" diff --git a/step-core-model/pom.xml b/step-core-model/pom.xml index 8cef784375..8d11505e7b 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/PatchableYamlModel.java b/step-core-model/src/main/java/step/core/yaml/PatchableYamlModel.java new file mode 100644 index 0000000000..0303ecbeef --- /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; + +public interface PatchableYamlModel { + + void setPatchingContext(PatchingContext context); + + PatchingContext getPatchingContext(); + + String getCurrentYaml(String contextIndent); + + void setModified(); + + enum StartingLineDeterminationStrategy { + SAME_LINE, + NEXT_CONTENT_LINE + } + + StartingLineDeterminationStrategy getStartingLineDeterminationStrategy(); + + void onParsed(JsonLocation startLocation, JsonLocation endLocation); +} 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..ce6f3bbd53 --- /dev/null +++ b/step-core-model/src/main/java/step/core/yaml/PatchableYamlModelBase.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (C) 2020, exense GmbH + * + * This file is part of STEP + * + * STEP is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * STEP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with STEP. If not, see . + ******************************************************************************/ +package step.core.yaml; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonLocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PatchableYamlModelBase extends AbstractYamlModel implements PatchableYamlModel { + private static final Logger logger = LoggerFactory.getLogger(PatchableYamlModelBase.class); + + @JsonIgnore + private PatchingContext context; + + public PatchableYamlModelBase(PatchingContext context) { + this.context = context; + } + + @Override + @JsonIgnore + public final PatchingContext getPatchingContext() { + return context; + } + + @JsonIgnore + private boolean modified = false; + + public void setModified() { + this.modified = true; + } + + @Override + @JsonIgnore + public final void setPatchingContext(PatchingContext context) { + this.context = context; + } + + @JsonIgnore + public String getCurrentYaml(String contextIndent) { + if (!modified) { + String chunk = context.getChunk(this); + if (chunk != null) { + // use the original chunk, but still re-indent appropriately if needed + return context.reindent(chunk, contextIndent); + } + // fallthrough in case of any problem? + throw new IllegalStateException(); + } + return context.serialize(this, contextIndent); + } + + @Override + @JsonIgnore + public StartingLineDeterminationStrategy getStartingLineDeterminationStrategy() { + // Let's hope that this really is true for all subclasses :-) + return StartingLineDeterminationStrategy.SAME_LINE; + } + + @Override + public void onParsed(JsonLocation startLocation, JsonLocation endLocation) { + context.claimChunk(startLocation, endLocation, this); + } + +} diff --git a/step-core-model/src/main/java/step/core/yaml/PatchingContext.java b/step-core-model/src/main/java/step/core/yaml/PatchingContext.java new file mode 100644 index 0000000000..b5df67456b --- /dev/null +++ b/step-core-model/src/main/java/step/core/yaml/PatchingContext.java @@ -0,0 +1,283 @@ +package step.core.yaml; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import step.core.yaml.deserialization.AutomationPackageUpdateException; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +public class PatchingContext { + private static final Logger logger = LoggerFactory.getLogger(PatchingContext.class); + private final String sourceLocation; + private final CopyOnWriteArrayList initialLines; + + private final ObjectMapper mapper; + + protected final ConcurrentNavigableMap chunks = new ConcurrentSkipListMap<>(); + + public PatchingContext() { + this(new ObjectMapper()); + } + + public PatchingContext(ObjectMapper mapper) { + this(null, "", mapper); + } + + public PatchingContext(String sourceLocation, String yaml, ObjectMapper mapper) { + this.sourceLocation = sourceLocation; + this.initialLines = new CopyOnWriteArrayList<>(yaml.lines().toList()); + this.mapper = mapper; + } + + public ObjectMapper getMapper() { + return mapper; + } + + public String getChunk(PatchableYamlModel entity) { + return getChunkBounds(entity).map(this::getChunk) + .orElse(null); + } + + private Optional getChunkBounds(PatchableYamlModel entity) { + // this is not terribly efficient as it will be O(n), but we're talking small amounts of data + return chunks.entrySet().stream() + .filter(entry -> entry.getValue().equals(entity)) + .findFirst() + .map(Map.Entry::getKey); + } + + private String getChunk(ChunkBounds bounds) { + return String.join("\n", getLines(bounds)) + "\n"; + } + + private List getLines(ChunkBounds bounds) { + return initialLines.subList(bounds.startLineNumber - 1, bounds.endLineNumber); + } + + public record ChunkBounds(int startLineNumber, int endLineNumber) implements Comparable + + { + private static final Comparator COMPARATOR = Comparator + .comparingInt(ChunkBounds::startLineNumber) // lower startLine first + .thenComparing(Comparator.comparingInt(ChunkBounds::endLineNumber).reversed()); // larger endLine (i.e. larger chunk) first + + @Override + public int compareTo (ChunkBounds that){ + return COMPARATOR.compare(this, that); + } + + public boolean encompasses (ChunkBounds inner){ + return inner.startLineNumber >= this.startLineNumber && inner.endLineNumber <= this.endLineNumber; + } + } + + public String getCurrentYaml() { + List allBounds = getAllOuterBounds(); + StringBuilder yaml = new StringBuilder(); + for (ChunkBounds bound : allBounds) { + PatchableYamlModel patchableYamlModel = chunks.get(bound); + if (patchableYamlModel == null) { + // unclaimed, return original lines + yaml.append(getChunk(bound)); + } else { + // claimed, return whatever that patchable currently thinks its content is, + // using its original indentation + String indent = detectIndent(initialLines.get(bound.startLineNumber - 1)); // we only need the first line + yaml.append(patchableYamlModel.getCurrentYaml(indent)); + } + } + return yaml.toString(); + } + + /** + * @return a sorted list of bounds, encompassing the entire original + * file. Note this only returns top-level bounds, not bounds that are + * encompassed by other bounds. For instance: returns list bounds, + * but not bounds of items inside those lists. This also returns + * bounds for lines that are NOT claimed by anything. + */ + private List getAllOuterBounds() { + List allBounds = getClaimedOuterBounds(); + List unclaimedBounds = new ArrayList<>(); + // claimedBounds is sorted, we need to fill the gaps + int startLineNumber = 1; + for (ChunkBounds bound : allBounds) { + if (bound.startLineNumber > startLineNumber) { + unclaimedBounds.add(new ChunkBounds(startLineNumber, bound.startLineNumber - 1)); + } + startLineNumber = bound.endLineNumber + 1; + } + allBounds.addAll(unclaimedBounds); + unclaimedBounds.clear(); // not needed anymore, might as well free it + allBounds.sort(ChunkBounds.COMPARATOR); + if (!allBounds.isEmpty() && allBounds.getLast().endLineNumber < initialLines.size()) { + allBounds.add(new ChunkBounds(allBounds.getLast().endLineNumber + 1, initialLines.size())); + } + return allBounds; + } + + /** + * + * @return a sorted list of (only) the outer bounds of claimed lines, i.e. the chunk bounds + * that are owned by some PatchableYamlModel. Inner claims (e.g. actual objects inside a list) are disregarded + */ + private List getClaimedOuterBounds() { + List allBounds = chunks.keySet().stream().toList(); + ChunkBounds lastBound = null; + List outerBounds = new ArrayList<>(); + for (ChunkBounds bound : allBounds) { + if (lastBound != null) { + if (lastBound.encompasses(bound)) { + continue; + } + } + lastBound = bound; + outerBounds.add(bound); + } + return outerBounds; + } + + private String serializeUnindented(Object entity) { + try { + return mapper.writeValueAsString(entity) + .replaceFirst("^---\\s*\\n", "") + .trim(); + } catch (JsonProcessingException e) { + throw new AutomationPackageUpdateException("Error Serializing YAML object", e); + } + } + + /* + Note for this and following methods: because of how YAML works, contextIndent could + be simply a string of spaces (" "), but it also could contain a dash if the item + is contained in a list (" - "). Only the first line needs the list marker, all others + need to be aligned and consist only of spaces. + */ + public String serialize(Object entity, String contextIndent) { + return indent(serializeUnindented(entity), contextIndent); + } + + String indent(String chunk, String contextIndent) { + if (chunk == null || chunk.isEmpty()) { + return chunk; + } + String onlyIndent = contextIndent.replace('-', ' '); + AtomicBoolean firstLine = new AtomicBoolean(true); + return chunk.lines() + .map(line -> firstLine.getAndSet(false) ? contextIndent + line : onlyIndent + line) + .collect(Collectors.joining("\n", "", "\n")); + } + + public String reindent(String chunk, String contextIndent) { + if (chunk == null || chunk.isEmpty()) { + return chunk; + } + String existingIndent = detectIndent(chunk); + if (existingIndent.equals(contextIndent)) { + return chunk; + } + String unindented = stripIndent(chunk, existingIndent.length()); + return indent(unindented, contextIndent); + } + + private String stripIndent(String chunk, int length) { + return chunk.lines() + .map(l -> { + // handle potential comments which may not be properly aligned. + if (l.trim().startsWith("#")) { + if (l.indexOf('#') < length) { + return l.trim(); + } + } + // short/empty lines + if (l.length() <= length) { + return ""; + } + // any other line - chop off indent + return l.substring(length); + }) + .collect(Collectors.joining("\n", "", "\n")); + } + + private String detectIndent(String chunk) { + // chunk is guaranteed to be non-empty; we don't care how many lines this has, we only need to look at the first one. + int pos = 0; + int len = chunk.length(); + + // Leading spaces + while (pos < len && chunk.charAt(pos) == ' ') { + pos++; + } + + // Optional list indicator (dash) and following spaces + if (pos < len && chunk.charAt(pos) == '-') { + pos++; + // 3. Consume spaces after the dash + while (pos < len && chunk.charAt(pos) == ' ') { + pos++; + } + } + return chunk.substring(0, pos); + } + + + public ChunkBounds claimChunk(JsonLocation startLocation, JsonLocation endLocation, PatchableYamlModel entity) { + int startLineNumber = startLocation.getLineNr(); + int endLineNumber = endLocation.getLineNr(); + String startLine = initialLines.get(startLineNumber - 1); + if (startLocation.getColumnNr() >= startLine.length()) { + // sometimes the start is at the correct line, sometimes it's at the end (in fact, AFTER the end) of some previous line + PatchableYamlModel.StartingLineDeterminationStrategy strategy = entity.getStartingLineDeterminationStrategy(); + logger.debug("{}: entity of {} seems to start at end of line {} (columnNr={}, lineLength={}), determining correct entity start line (strategy={})", sourceLocation, + entity.getClass(), startLocation.getLineNr(), startLocation.getColumnNr(), startLine.length(), strategy); + if (strategy == PatchableYamlModel.StartingLineDeterminationStrategy.NEXT_CONTENT_LINE) { + boolean found = false; + for (int l = startLineNumber; l < initialLines.size(); l++) { + String candidate = initialLines.get(l).trim(); + if (!candidate.isEmpty() && !candidate.startsWith("#")) { + startLineNumber = l + 1; + logger.debug("Using first non-empty, non-comment line (lineNumber={}, line content={})", startLineNumber, candidate); + found = true; + break; + } + } + if (!found) { + logger.debug("Unable to determine correct start line for entity, keeping original line number!"); + } + } + } + ChunkBounds bounds = new ChunkBounds(startLineNumber, endLineNumber); + chunks.put(bounds, entity); + return bounds; + } + + public ChunkBounds appendAndClaim(PatchableYamlModel entity, String chunk) { + List lines = chunk.lines().toList(); + // yes, initialLines won't be so "initial" anymore ;-) + initialLines.addAll(lines); + int endLine = initialLines.size(); + int startLine = endLine - lines.size() + 1; + ChunkBounds bounds = new ChunkBounds(startLine, endLine); + chunks.put(bounds, entity); + return bounds; + } + + + public void replaceEntity(PatchableYamlModel oldPatchable, PatchableYamlModel newPatchable) { + newPatchable.setPatchingContext(this); + newPatchable.setModified(); + getChunkBounds(oldPatchable).ifPresent(bounds -> chunks.put(bounds, newPatchable)); + } +} 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..99cd566165 --- /dev/null +++ b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlList.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * 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 step.core.yaml.PatchingContext; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import java.util.function.UnaryOperator; + +public class PatchableYamlList extends ArrayList implements PatchableYamlModel { + + + private final PatchingContext patchingContext; + private final String fieldName; + private volatile PatchingContext.ChunkBounds bounds; + + public PatchableYamlList(PatchingContext patchingContext, String fieldName) { + + this(new ArrayList<>(), patchingContext, fieldName); + } + + protected PatchableYamlList(Collection content, PatchingContext patchingContext, String fieldName) { + super(content); + this.patchingContext = Objects.requireNonNull(patchingContext); + this.fieldName = Objects.requireNonNull(fieldName); + } + + @Override + @JsonIgnore + public PatchingContext getPatchingContext() { + return patchingContext; + } + + @Override + @JsonIgnore + public String getCurrentYaml(String contextIndent) { + if (isEmpty()) { + return contextIndent + fieldName + ": []\n"; + } + String childIndent = " ".repeat(contextIndent.length()) + " - "; + // Simply return a concatenated list of the current items; they're responsible for their own serialization + // Note that in theory we could even try to preserve the original comments between items (if there are any), + // but this could become complicated if entries get deleted, so we omit it for now. + StringBuilder sb = new StringBuilder(); + sb.append(contextIndent).append(fieldName).append(":").append("\n"); + stream().map(item -> (PatchableYamlModel) item).forEach(item -> { + sb.append(item.getCurrentYaml(childIndent)); + }); + return sb.toString(); + + } + + @Override + public boolean add(T item) { + if (bounds == null) { + // This list has not been registered with the context yet, meaning it hasn't been parsed from a file, but manually added. + // We'll need to add it to the context as a new object. + synchronized (this) { + if (bounds == null) { + // FIXME: For now, this assumes all lists are top-level (see the hardcoded indent below) + bounds = patchingContext.appendAndClaim(this, getCurrentYaml("")); + } + } + } + + PatchableYamlModel patchableItem = (PatchableYamlModel) item; + patchableItem.setPatchingContext(this.getPatchingContext()); + patchableItem.setModified(); + super.add(item); + return true; + } + + @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); + patchingContext.replaceEntity(oldEntity, newEntity); + } + + @Override + @JsonIgnore + public void setPatchingContext(PatchingContext context) { + throw new UnsupportedOperationException(); + } + + @Override + public StartingLineDeterminationStrategy getStartingLineDeterminationStrategy() { + return StartingLineDeterminationStrategy.NEXT_CONTENT_LINE; + } + + @Override + public void setModified() { + throw new UnsupportedOperationException(); + } + + @Override + public void onParsed(JsonLocation startLocation, JsonLocation endLocation) { + bounds = patchingContext.claimChunk(startLocation, endLocation, this); + } +} 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..f5b81d0f2d --- /dev/null +++ b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlListDeserializer.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.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.onParsed(startLocation, patchingParser.getLastDistinctLocation()); + //patchableYamlList.getPatchingContext().claimChunk(, patchableYamlList); + 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..4a96e32eed --- /dev/null +++ b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchableYamlModelDeserializer.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * 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.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 patchingParser) { + JsonLocation startItem = patchingParser.currentLocation(); + T entity = delegate.deserialize(p, ctxt); + entity.onParsed(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/PatchingParserDelegate.java b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchingParserDelegate.java new file mode 100644 index 0000000000..eef97c33c9 --- /dev/null +++ b/step-core-model/src/main/java/step/core/yaml/deserialization/PatchingParserDelegate.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.core.util.JsonParserDelegate; +import step.core.yaml.PatchingContext; + +import java.io.IOException; +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 363ba35429..7e35fe27bc 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..aecc711f00 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,22 @@ ******************************************************************************/ package step.parameter.automation; +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 step.commons.activation.Expression; import step.core.dynamicbeans.DynamicValue; -import step.core.yaml.AbstractYamlModel; +import step.core.yaml.PatchableYamlModelBase; +import step.core.yaml.PatchingContext; import step.core.yaml.YamlFieldCustomCopy; import step.core.yaml.YamlModel; 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 +44,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 +102,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..bcc75be83e 100644 --- a/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java +++ b/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java @@ -61,7 +61,19 @@ 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; 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..e1e76a63c9 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,21 @@ package step.automation.packages.model; import step.automation.packages.StagingAutomationPackageContext; +import step.core.yaml.PatchableYamlModelBase; +import step.core.yaml.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..fa657dbb40 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.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..74ba1898bb 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.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; } 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..b58d97518d 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,21 +18,24 @@ ******************************************************************************/ 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; -import java.util.Map; -import java.util.Objects; - @YamlModel(name = "Composite") +@JsonInclude(JsonInclude.Include.NON_DEFAULT) public class YamlCompositeFunction extends AbstractYamlFunction { @YamlFieldCustomCopy @@ -62,7 +65,27 @@ 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 + // FIXME: discuss what exactly this is supposed to do and how it relates to the comment above :-) + if (this.plan.getName() != null && !this.plan.getName().isEmpty()) { + this.plan.setName(plan.getAttribute(AbstractOrganizableObject.NAME)); + } + // I have no idea why that mapper would be null sometimes: + // ObjectMapper mapper = this.plan.getRoot().getYamlArtefact().getYamlObjectMapper(); + // That one should work... + ObjectMapper mapper = this.getPlan().getPatchingContext().getMapper(); + this.plan.setRoot(new NamedYamlArtefact(AbstractYamlArtefact.toYamlArtefact(plan.getRoot(), mapper))); + } + } + + + private Plan yamlPlanToPlan(YamlPlan yamlPlan) { Plan plan = new Plan(yamlPlan.getRoot().getYamlArtefact().toArtefact()); // 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..32f85099f4 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,17 @@ ******************************************************************************/ package step.core.scheduler.automation; -import java.util.Map; +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 step.core.yaml.PatchableYamlModelBase; +import step.core.yaml.PatchingContext; + import java.util.List; +import java.util.Map; -public class AutomationPackageSchedule { +public class AutomationPackageSchedule extends PatchableYamlModelBase { public static final String SCHEDULE_DEF = "ScheduleDef"; public static final String FIELD_NAME_IN_AP = "schedules"; @@ -29,15 +36,20 @@ public class AutomationPackageSchedule { private Boolean active = true; private String cron; + @JsonInclude(JsonInclude.Include.NON_NULL) // otherwise fails schema check after re-reading serialized value private List cronExclusions; private String planName; + @JsonInclude(JsonInclude.Include.NON_NULL) 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..33607d5e5a 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.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..063df0d9ed 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,7 +42,15 @@ 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.PatchingContext; +import step.core.yaml.deserialization.PatchableYamlList; +import step.core.yaml.deserialization.PatchableYamlListDeserializer; +import step.core.yaml.deserialization.PatchableYamlModelDeserializer; +import step.core.yaml.deserializers.StepYamlDeserializer; import step.core.yaml.deserializers.StepYamlDeserializersScanner; +import step.core.yaml.model.AbstractYamlArtefact; +import step.core.yaml.model.NamedYamlArtefact; import step.core.yaml.serializers.StepYamlSerializersScanner; import step.migration.MigrationManager; import step.plans.nl.RootArtefactType; @@ -44,8 +58,6 @@ import step.plans.parser.yaml.deserializers.UpgradableYamlPlanDeserializer; import step.plans.parser.yaml.migrations.AbstractYamlPlanMigrationTask; import step.plans.parser.yaml.migrations.YamlPlanMigration; -import step.core.yaml.model.AbstractYamlArtefact; -import step.core.yaml.model.NamedYamlArtefact; import step.plans.parser.yaml.model.YamlPlanVersions; import step.plans.parser.yaml.schema.YamlPlanValidationException; import step.repositories.parser.StepsParser; @@ -53,8 +65,13 @@ 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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import static step.core.scanner.Classes.newInstanceAs; @@ -155,7 +172,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 +212,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; } @@ -203,26 +220,72 @@ public static ObjectMapper createDefaultYamlMapper() { YAMLFactory yamlFactory = new YAMLFactory(); // Disable native type id to enable conversion to generic Documents yamlFactory.disable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID); + yamlFactory.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR); 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 + + 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); + } - return registerBasicSerializersAndDeserializers(module, resultingMapper) - .addDeserializer(YamlPlan.class, new UpgradableYamlPlanDeserializer(upgradablePlan ? currentVersion : null, jsonSchema, migrationManager, nonUpgradableYamlMapper)); + @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 +334,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 +356,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