From caed62f6d75cfe8d27a74869bfbfaf5bb6a0b3c1 Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Wed, 11 Dec 2019 10:18:13 +0100 Subject: [PATCH] Afs virtual script (#973) * Add include scripts in AbstractScript nodes Signed-off-by: Paul Bui-Quang * Add generic script type Signed-off-by: Paul Bui-Quang --- .../powsybl/action/dsl/afs/ActionScript.java | 6 +- .../java/com/powsybl/afs/DependencyCache.java | 2 +- .../powsybl/afs/OrderedDependencyManager.java | 108 +++++++++++++++ .../java/com/powsybl/afs/ProjectFile.java | 2 +- .../ext/base/AbstractModificationScript.java | 84 ++---------- .../powsybl/afs/ext/base/AbstractScript.java | 127 ++++++++++++++++++ .../powsybl/afs/ext/base/GenericScript.java | 29 ++++ .../ext/base/LocalNetworkCacheService.java | 2 +- .../afs/ext/base/ModificationScript.java | 2 +- .../powsybl/afs/ext/base/StorableScript.java | 2 + .../afs/ext/base/ModificationScriptTest.java | 52 ++++++- 11 files changed, 329 insertions(+), 87 deletions(-) create mode 100644 afs/afs-core/src/main/java/com/powsybl/afs/OrderedDependencyManager.java create mode 100644 afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/AbstractScript.java create mode 100644 afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/GenericScript.java diff --git a/action/action-dsl-afs/src/main/java/com/powsybl/action/dsl/afs/ActionScript.java b/action/action-dsl-afs/src/main/java/com/powsybl/action/dsl/afs/ActionScript.java index dfa94090b22..ad4f58b72da 100644 --- a/action/action-dsl-afs/src/main/java/com/powsybl/action/dsl/afs/ActionScript.java +++ b/action/action-dsl-afs/src/main/java/com/powsybl/action/dsl/afs/ActionScript.java @@ -9,7 +9,7 @@ import com.powsybl.action.dsl.ActionDb; import com.powsybl.action.dsl.ActionDslLoader; import com.powsybl.afs.ProjectFileCreationContext; -import com.powsybl.afs.ext.base.AbstractModificationScript; +import com.powsybl.afs.ext.base.AbstractScript; import com.powsybl.afs.ext.base.ScriptType; import com.powsybl.contingency.ContingenciesProvider; import com.powsybl.contingency.Contingency; @@ -23,7 +23,7 @@ /** * @author Geoffroy Jamgotchian */ -public class ActionScript extends AbstractModificationScript implements ContingenciesProvider { +public class ActionScript extends AbstractScript implements ContingenciesProvider { public static final String PSEUDO_CLASS = "actionScript"; public static final int VERSION = 0; @@ -48,7 +48,7 @@ public ScriptType getScriptType() { public ActionDb load(Network network) { Objects.requireNonNull(network); - return new ActionDslLoader(readScript()).load(network); + return new ActionDslLoader(readScript(true)).load(network); } @Override diff --git a/afs/afs-core/src/main/java/com/powsybl/afs/DependencyCache.java b/afs/afs-core/src/main/java/com/powsybl/afs/DependencyCache.java index 6a0e0989f2a..4d9f6466c1c 100644 --- a/afs/afs-core/src/main/java/com/powsybl/afs/DependencyCache.java +++ b/afs/afs-core/src/main/java/com/powsybl/afs/DependencyCache.java @@ -20,7 +20,7 @@ * * @author Geoffroy Jamgotchian */ -public class DependencyCache { +public class DependencyCache { private final ProjectFile projectFile; diff --git a/afs/afs-core/src/main/java/com/powsybl/afs/OrderedDependencyManager.java b/afs/afs-core/src/main/java/com/powsybl/afs/OrderedDependencyManager.java new file mode 100644 index 00000000000..cad9879a5a1 --- /dev/null +++ b/afs/afs-core/src/main/java/com/powsybl/afs/OrderedDependencyManager.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2019, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package com.powsybl.afs; + +import org.apache.commons.lang3.tuple.ImmutablePair; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Paul Bui-Quang + */ +public class OrderedDependencyManager { + + private final ProjectFile projectFile; + + private List> dependencyCache = null; + + public OrderedDependencyManager(ProjectFile projectFile) { + this.projectFile = Objects.requireNonNull(projectFile); + projectFile.addListener(new DefaultProjectFileListener() { + @Override + public void dependencyChanged(String name) { + dependencyCache = null; + } + }); + } + + public void appendDependencies(String name, List projectNodes) { + List nodes = getDependencies(name); + nodes.addAll(projectNodes); + setDependencies(name, nodes); + } + + public void insertDependencies(String name, int index, List projectNodes) { + List nodes = getDependencies(name); + nodes.addAll(index, projectNodes); + setDependencies(name, nodes); + } + + public void removeDependency(String name, int index) { + List nodes = getDependencies(name); + nodes.remove(index); + setDependencies(name, nodes); + } + + public void removeDependencies(String name, List nodeIds) { + List nodes = getDependencies(name) + .stream() + .filter(dep -> !nodeIds.contains(dep.getId())) + .collect(Collectors.toList()); + setDependencies(name, nodes); + } + + public void setDependencies(String name, List projectNodes) { + getDependenciesFor(name).map(el -> el.getRight().getName()).forEach(projectFile::removeDependencies); + for (int i = 0; i < projectNodes.size(); i++) { + projectFile.setDependencies(name + "_" + i, Collections.singletonList(projectNodes.get(i))); + } + } + + public List getDependencies(String name) { + return getDependenciesFor(name) + .sorted(Comparator.comparing(ImmutablePair::getLeft)) + .map(dep -> dep.getRight().getProjectNode()) + .collect(Collectors.toList()); + } + + public List getDependencies(String name, Class nodeClass) { + return getDependenciesFor(name) + .filter(dep -> nodeClass.isAssignableFrom(dep.getRight().getProjectNode().getClass())) + .sorted(Comparator.comparing(ImmutablePair::getLeft)) + .map(dep -> nodeClass.cast(dep.getRight().getProjectNode())) + .collect(Collectors.toList()); + } + + private List> getDependencyCache() { + if (dependencyCache == null) { + dependencyCache = projectFile.getDependencies(); + } + return dependencyCache; + } + + private Stream>> getDependenciesFor(String name) { + Pattern pattern = Pattern.compile(name + "_(\\d+)"); + return getDependencyCache() + .stream() + .map(dep -> { + Matcher depMatch = pattern.matcher(dep.getName()); + if (depMatch.matches()) { + return ImmutablePair.of(depMatch.group(1), dep); + } + return null; + }) + .filter(Objects::nonNull); + } + +} diff --git a/afs/afs-core/src/main/java/com/powsybl/afs/ProjectFile.java b/afs/afs-core/src/main/java/com/powsybl/afs/ProjectFile.java index b3c3e5f2592..8c84f5f44da 100644 --- a/afs/afs-core/src/main/java/com/powsybl/afs/ProjectFile.java +++ b/afs/afs-core/src/main/java/com/powsybl/afs/ProjectFile.java @@ -93,7 +93,7 @@ public void replaceDependency(String oldDependencyId, ProjectNode replacementNod }).collect(Collectors.toList()))); } - public List getDependencies(String name, Class nodeClass) { + public List getDependencies(String name, Class nodeClass) { Objects.requireNonNull(name); Objects.requireNonNull(nodeClass); return storage.getDependencies(info.getId(), name).stream() diff --git a/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/AbstractModificationScript.java b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/AbstractModificationScript.java index 921d1cfd1c6..cc70d8bdacf 100644 --- a/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/AbstractModificationScript.java +++ b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/AbstractModificationScript.java @@ -1,89 +1,21 @@ -/** - * Copyright (c) 2018, RTE (http://www.rte-france.com) +/* + * Copyright (c) 2019, RTE (http://www.rte-france.com) * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * */ + package com.powsybl.afs.ext.base; -import com.google.common.io.CharStreams; -import com.powsybl.afs.ProjectFile; import com.powsybl.afs.ProjectFileCreationContext; -import com.powsybl.afs.storage.events.AppStorageListener; -import com.powsybl.afs.storage.events.NodeDataUpdated; -import com.powsybl.afs.storage.events.NodeEvent; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; /** - * @author Geoffroy Jamgotchian + * @author Paul Bui-Quang */ -public abstract class AbstractModificationScript extends ProjectFile implements StorableScript { - - private static final String NODE_DATA_UPDATED = "NODE_DATA_UPDATED"; - - private final String scriptContentName; - - private final List listeners = new ArrayList<>(); - - private final AppStorageListener l = eventList -> processEvents(eventList.getEvents(), info.getId(), listeners); - +@Deprecated +public abstract class AbstractModificationScript extends AbstractScript { public AbstractModificationScript(ProjectFileCreationContext context, int codeVersion, String scriptContentName) { - super(context, codeVersion); - this.scriptContentName = Objects.requireNonNull(scriptContentName); - storage.getEventsBus().addListener(l); - } - - private void processEvents(List events, String nodeId, List listeners) { - for (NodeEvent event : events) { - if (event.getType().equals(NODE_DATA_UPDATED)) { - NodeDataUpdated dataUpdated = (NodeDataUpdated) event; - if (dataUpdated.getId().equals(nodeId) && scriptContentName.equals(dataUpdated.getDataName())) { - for (ScriptListener listener : listeners) { - listener.scriptUpdated(); - } - } - } - } - } - - @Override - public String readScript() { - try { - return CharStreams.toString(new InputStreamReader(storage.readBinaryData(info.getId(), scriptContentName).orElseThrow(AssertionError::new), StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - @Override - public void writeScript(String content) { - try (Reader reader = new StringReader(content); - Writer writer = new OutputStreamWriter(storage.writeBinaryData(info.getId(), scriptContentName), StandardCharsets.UTF_8)) { - CharStreams.copy(reader, writer); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - storage.updateModificationTime(info.getId()); - storage.flush(); - - // invalidate backward dependencies - invalidate(); - } - - @Override - public void addListener(ScriptListener listener) { - Objects.requireNonNull(listener); - listeners.add(listener); - } - - @Override - public void removeListener(ScriptListener listener) { - Objects.requireNonNull(listener); - listeners.remove(listener); + super(context, codeVersion, scriptContentName); } } diff --git a/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/AbstractScript.java b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/AbstractScript.java new file mode 100644 index 00000000000..8c49c485154 --- /dev/null +++ b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/AbstractScript.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2018, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package com.powsybl.afs.ext.base; + +import com.google.common.io.CharStreams; +import com.powsybl.afs.OrderedDependencyManager; +import com.powsybl.afs.ProjectFile; +import com.powsybl.afs.ProjectFileCreationContext; +import com.powsybl.afs.storage.events.AppStorageListener; +import com.powsybl.afs.storage.events.NodeDataUpdated; +import com.powsybl.afs.storage.events.NodeEvent; +import org.apache.commons.lang3.StringUtils; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @author Geoffroy Jamgotchian + */ +public abstract class AbstractScript extends ProjectFile implements StorableScript { + + private static final String NODE_DATA_UPDATED = "NODE_DATA_UPDATED"; + + private static final String INCLUDED_SCRIPTS_DEPENDENCY_NAME = "scriptIncludes"; + private static final String DEFAULT_SCRIPTS_DELIMITER = "\n\n"; + private final String scriptContentName; + private final List listeners = new ArrayList<>(); + private final AppStorageListener l = eventList -> processEvents(eventList.getEvents(), info.getId(), listeners); + protected final OrderedDependencyManager orderedDependencyManager = new OrderedDependencyManager(this); + + public AbstractScript(ProjectFileCreationContext context, int codeVersion, String scriptContentName) { + super(context, codeVersion); + this.scriptContentName = Objects.requireNonNull(scriptContentName); + storage.getEventsBus().addListener(l); + } + + private void processEvents(List events, String nodeId, List listeners) { + for (NodeEvent event : events) { + if (NODE_DATA_UPDATED.equals(event.getType())) { + NodeDataUpdated dataUpdated = (NodeDataUpdated) event; + if (dataUpdated.getId().equals(nodeId) && scriptContentName.equals(dataUpdated.getDataName())) { + for (ScriptListener listener : listeners) { + listener.scriptUpdated(); + } + } + } + } + } + + public List getIncludedScripts() { + return orderedDependencyManager.getDependencies(INCLUDED_SCRIPTS_DEPENDENCY_NAME, AbstractScript.class); + } + + public void addGenericScript(GenericScript genericScript) { + orderedDependencyManager.appendDependencies(INCLUDED_SCRIPTS_DEPENDENCY_NAME, Collections.singletonList(genericScript)); + } + + public void addScript(T includeScript) { + orderedDependencyManager.appendDependencies(INCLUDED_SCRIPTS_DEPENDENCY_NAME, Collections.singletonList(includeScript)); + } + + public void removeScript(String scriptNodeId) { + orderedDependencyManager.removeDependencies(INCLUDED_SCRIPTS_DEPENDENCY_NAME, Collections.singletonList(scriptNodeId)); + } + + @Override + public String readScript(boolean withIncludes) { + String ownContent = readScript(); + if (withIncludes) { + String includesScript = orderedDependencyManager + .getDependencies(INCLUDED_SCRIPTS_DEPENDENCY_NAME, AbstractScript.class) + .stream() + .map(script -> script.readScript(true)) + .collect(Collectors.joining(DEFAULT_SCRIPTS_DELIMITER)); + if (StringUtils.isNotBlank(includesScript)) { + includesScript += DEFAULT_SCRIPTS_DELIMITER; + } + return includesScript + ownContent; + } + return ownContent; + } + + @Override + public String readScript() { + try { + return CharStreams.toString(new InputStreamReader(storage.readBinaryData(info.getId(), scriptContentName).orElseThrow(AssertionError::new), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void writeScript(String content) { + try (Reader reader = new StringReader(content); + Writer writer = new OutputStreamWriter(storage.writeBinaryData(info.getId(), scriptContentName), StandardCharsets.UTF_8)) { + CharStreams.copy(reader, writer); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + storage.updateModificationTime(info.getId()); + storage.flush(); + + // invalidate backward dependencies + invalidate(); + } + + @Override + public void addListener(ScriptListener listener) { + Objects.requireNonNull(listener); + listeners.add(listener); + } + + @Override + public void removeListener(ScriptListener listener) { + Objects.requireNonNull(listener); + listeners.remove(listener); + } +} diff --git a/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/GenericScript.java b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/GenericScript.java new file mode 100644 index 00000000000..9cdfee53d34 --- /dev/null +++ b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/GenericScript.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2019, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package com.powsybl.afs.ext.base; + +import com.powsybl.afs.ProjectFileCreationContext; + +/** + * Default general purpose Groovy script + * + * @author Paul Bui-Quang + */ +public class GenericScript extends AbstractScript { + public static final String PSEUDO_CLASS = "genericScript"; + public static final int VERSION = 0; + private static final String SCRIPT_CONTENT = "scriptContent"; + + public GenericScript(ProjectFileCreationContext context) { + super(context, VERSION, SCRIPT_CONTENT); + } + + @Override + public ScriptType getScriptType() { + return ScriptType.GROOVY; + } +} diff --git a/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/LocalNetworkCacheService.java b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/LocalNetworkCacheService.java index 0a06f355612..3e712252159 100644 --- a/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/LocalNetworkCacheService.java +++ b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/LocalNetworkCacheService.java @@ -55,7 +55,7 @@ private static ScriptResult loadNetworkFromImportedCase(ImportedCase im } private static ScriptResult applyScript(Network network, String previousScriptOutput, ModificationScript script) { - ScriptResult result = ScriptUtils.runScript(network, script.getScriptType(), script.readScript()); + ScriptResult result = ScriptUtils.runScript(network, script.getScriptType(), script.readScript(true)); if (result.getError() == null) { return new ScriptResult<>(network, previousScriptOutput + result.getOutput(), null); } else { diff --git a/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/ModificationScript.java b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/ModificationScript.java index 10465d5c029..561e2a4742f 100644 --- a/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/ModificationScript.java +++ b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/ModificationScript.java @@ -11,7 +11,7 @@ /** * @author Geoffroy Jamgotchian */ -public class ModificationScript extends AbstractModificationScript { +public class ModificationScript extends AbstractScript { public static final String PSEUDO_CLASS = "modificationScript"; public static final int VERSION = 0; diff --git a/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/StorableScript.java b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/StorableScript.java index 8a936a95107..e03b25698d9 100644 --- a/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/StorableScript.java +++ b/afs/afs-ext-base/src/main/java/com/powsybl/afs/ext/base/StorableScript.java @@ -20,6 +20,8 @@ default String getScriptLabel() { String readScript(); + String readScript(boolean withInclude); + void writeScript(String content); void addListener(ScriptListener listener); diff --git a/afs/afs-ext-base/src/test/java/com/powsybl/afs/ext/base/ModificationScriptTest.java b/afs/afs-ext-base/src/test/java/com/powsybl/afs/ext/base/ModificationScriptTest.java index b8e75a6a45e..f56035e4911 100644 --- a/afs/afs-ext-base/src/test/java/com/powsybl/afs/ext/base/ModificationScriptTest.java +++ b/afs/afs-ext-base/src/test/java/com/powsybl/afs/ext/base/ModificationScriptTest.java @@ -14,6 +14,7 @@ import org.junit.Test; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.*; @@ -77,13 +78,12 @@ public void test() { assertFalse(script.isFolder()); assertTrue(script.getDependencies().isEmpty()); assertEquals("println 'hello'", script.readScript()); - boolean[] scriptUpdated = new boolean[1]; - scriptUpdated[0] = false; - ScriptListener listener = () -> scriptUpdated[0] = true; + AtomicBoolean scriptUpdated = new AtomicBoolean(false); + ScriptListener listener = () -> scriptUpdated.set(true); script.addListener(listener); script.writeScript("println 'bye'"); assertEquals("println 'bye'", script.readScript()); - assertTrue(scriptUpdated[0]); + assertTrue(scriptUpdated.get()); script.removeListener(listener); // check script file is correctly scanned @@ -91,5 +91,49 @@ public void test() { ProjectNode firstNode = rootFolder.getChildren().get(0); assertTrue(firstNode instanceof ModificationScript); assertEquals("script", firstNode.getName()); + + ModificationScript include1 = rootFolder.fileBuilder(ModificationScriptBuilder.class) + .withName("include_script1") + .withType(ScriptType.GROOVY) + .withContent("var foo=\"bar\"") + .build(); + assertNotNull(include1); + script.addScript(include1); + String contentWithInclude = script.readScript(true); + assertEquals(contentWithInclude, "var foo=\"bar\"\n\nprintln 'bye'"); + + script.addScript(include1); + contentWithInclude = script.readScript(true); + assertEquals(contentWithInclude, "var foo=\"bar\"\n\nvar foo=\"bar\"\n\nprintln 'bye'"); + + ModificationScript include2 = rootFolder.fileBuilder(ModificationScriptBuilder.class) + .withName("include_script2") + .withType(ScriptType.GROOVY) + .withContent("var p0=1") + .build(); + script.removeScript(include1.getId()); + script.addScript(include1); + script.addScript(include2); + contentWithInclude = script.readScript(true); + assertEquals(contentWithInclude, "var foo=\"bar\"\n\nvar p0=1\n\nprintln 'bye'"); + + ModificationScript include3 = rootFolder.fileBuilder(ModificationScriptBuilder.class) + .withName("include_script3") + .withType(ScriptType.GROOVY) + .withContent("var pmax=2") + .build(); + script.addScript(include3); + script.removeScript(include2.getId()); + contentWithInclude = script.readScript(true); + assertEquals(contentWithInclude, "var foo=\"bar\"\n\nvar pmax=2\n\nprintln 'bye'"); + + include3.addScript(include2); + contentWithInclude = script.readScript(true); + assertEquals(contentWithInclude, "var foo=\"bar\"\n\nvar p0=1\n\nvar pmax=2\n\nprintln 'bye'"); + + List includes = script.getIncludedScripts(); + assertEquals(includes.size(), 2); + assertEquals(includes.get(0).getId(), include1.getId()); + assertEquals(includes.get(1).getId(), include3.getId()); } }