diff --git a/src/main/frontend/model/hops/createChildNode.ts b/src/main/frontend/model/hops/createChildNode.ts
index 12a0509..21476a1 100644
--- a/src/main/frontend/model/hops/createChildNode.ts
+++ b/src/main/frontend/model/hops/createChildNode.ts
@@ -4,6 +4,7 @@ export interface Type extends AnyHop {
type: 'createChildNode';
name: string;
primaryType?: string;
+ runOnExistingNode: boolean;
conflict: ConflictResolutionStrategy;
hops?: Hop[];
}
@@ -11,6 +12,7 @@ export interface Type extends AnyHop {
export const defaultConfig: Partial = {
conflict: 'ignore',
name: 'child-name',
+ runOnExistingNode: false,
};
export const title = 'Create Node';
diff --git a/src/main/frontend/sections/editor/types/CreateChildNodeStep.tsx b/src/main/frontend/sections/editor/types/CreateChildNodeStep.tsx
index e49c8f1..e3ba1b0 100644
--- a/src/main/frontend/sections/editor/types/CreateChildNodeStep.tsx
+++ b/src/main/frontend/sections/editor/types/CreateChildNodeStep.tsx
@@ -8,6 +8,7 @@ import { Help } from '../../../widgets/Help';
import { Input } from '../../../widgets/Input';
import { Pipeline } from '../Pipeline';
import { Conflict } from '../../../widgets/Conflict';
+import { Switch } from '../../../widgets/Switch';
export const CreateChildNodeStep = forwardRef(function CreateChildNodeStep(
{ parentHops, hop },
@@ -32,9 +33,17 @@ export const CreateChildNodeStep = forwardRef (hop.conflict = conflict)}
/>
+ {hop.conflict == 'ignore' ? (
+ (hop.runOnExistingNode = runOnExistingNode)}
+ />
+ ) : undefined}
Name of New Child Node
The name of the child node to be created.
@@ -52,10 +61,11 @@ export const CreateChildNodeStep = forwardRefnt:unstructured
If the target node exists
+ How to handle the case where the target node already exists.
+ Run on existing node
- How to handle the case where the target node already exists. Note that choosing “Ignore conflict” will use the
- existing node to run descendent pipeline steps on. To stop the descendent pipeline from running in this case,
- choose “Throw an exception” and place this step inside a “Catch Pipeline Errors” step.
+ Whether to run the descendant hops if the target node already existed (only applicable if “If the target node
+ exists” is set to “Ignore conflict”).
diff --git a/src/main/frontend/widgets/Conflict.tsx b/src/main/frontend/widgets/Conflict.tsx
index 9b15c12..9bc53fb 100644
--- a/src/main/frontend/widgets/Conflict.tsx
+++ b/src/main/frontend/widgets/Conflict.tsx
@@ -9,16 +9,23 @@ export type Options = [value: string, label: string, icon?: CoralIcon][];
export const Conflict: FC<{
label?: string;
forceLabel?: string;
+ ignoreLabel?: string;
value: ConflictResolutionStrategy;
onChange(newValue: ConflictResolutionStrategy): void;
-}> = ({ label = 'Conflict Resolution', forceLabel = 'Force the given action', value, onChange }) => {
+}> = ({
+ label = 'Conflict Resolution',
+ forceLabel = 'Force the given action',
+ ignoreLabel = 'Ignore conflict, abort the current action',
+ value,
+ onChange,
+}) => {
return (
onChange(v)}
list={[
- ['ignore', 'Ignore conflict, abort the current action'],
+ ['ignore', ignoreLabel],
['throw', 'Throw an exception, stop pipeline'],
['force', forceLabel],
]}
diff --git a/src/main/java/com/swisscom/aem/tools/impl/hops/CopyNode.java b/src/main/java/com/swisscom/aem/tools/impl/hops/CopyNode.java
index 4f56aae..f9fb8b2 100644
--- a/src/main/java/com/swisscom/aem/tools/impl/hops/CopyNode.java
+++ b/src/main/java/com/swisscom/aem/tools/impl/hops/CopyNode.java
@@ -37,8 +37,7 @@ public void run(Config config, Node node, HopContext context) throws RepositoryE
final String newName = context.evaluateTemplate(config.newName);
final MoveNode.NewNodeDescriptor descriptor = MoveNode.resolvePathToNewNode(parent, newName, config.conflict, context);
- if (descriptor.isNeedsReplacing()) {
- // Replacing not supported when copying
+ if (descriptor.isTargetExists()) {
return;
}
diff --git a/src/main/java/com/swisscom/aem/tools/impl/hops/CreateChildNode.java b/src/main/java/com/swisscom/aem/tools/impl/hops/CreateChildNode.java
index d140dda..378b1ba 100644
--- a/src/main/java/com/swisscom/aem/tools/impl/hops/CreateChildNode.java
+++ b/src/main/java/com/swisscom/aem/tools/impl/hops/CreateChildNode.java
@@ -29,8 +29,20 @@ public void run(Config config, Node node, HopContext context) throws RepositoryE
final MoveNode.NewNodeDescriptor descriptor = MoveNode.resolvePathToNewNode(node, name, config.conflict, context);
final Node childNode;
- if (descriptor.isNeedsReplacing()) {
+ if (descriptor.isTargetExists()) {
+ if (!config.runOnExistingNode) {
+ return;
+ }
childNode = descriptor.getParent().getNode(descriptor.getNewChildName());
+ context.trace("Running the createChild hops on the existing node {}", childNode.getPath());
+ if (!childNode.isNodeType(config.primaryType)) {
+ context.warn(
+ "Existing node {} has type {} but {} was requested",
+ childNode.getPath(),
+ childNode.getPrimaryNodeType().getName(),
+ config.primaryType
+ );
+ }
} else {
context.info(
"Creating new node {} (type {}) under {}",
@@ -72,6 +84,8 @@ public static final class Config implements HopConfig {
@Nonnull
private ConflictResolution conflict = ConflictResolution.IGNORE;
+ private boolean runOnExistingNode;
+
@Nonnull
private List hops = Collections.emptyList();
}
diff --git a/src/main/java/com/swisscom/aem/tools/impl/hops/MoveNode.java b/src/main/java/com/swisscom/aem/tools/impl/hops/MoveNode.java
index 014afb1..1755bc7 100644
--- a/src/main/java/com/swisscom/aem/tools/impl/hops/MoveNode.java
+++ b/src/main/java/com/swisscom/aem/tools/impl/hops/MoveNode.java
@@ -62,8 +62,9 @@ public static NewNodeDescriptor resolvePathToNewNode(
parent = getParentNode(parts, parent, session, target);
- final boolean needsReplacing = parent.hasNode(target);
- if (needsReplacing) {
+ // FIXME: What about repositories with support for same-name siblings?
+ boolean targetExists = parent.hasNode(target);
+ if (targetExists) {
final Node childNode = parent.getNode(target);
switch (conflict) {
case IGNORE:
@@ -72,6 +73,7 @@ public static NewNodeDescriptor resolvePathToNewNode(
case FORCE:
context.info("Replacing existing node {}", childNode.getPath());
childNode.remove();
+ targetExists = false;
break;
case THROW:
throw new HopperException(String.format("Node %s already exists", childNode.getPath()));
@@ -80,7 +82,7 @@ public static NewNodeDescriptor resolvePathToNewNode(
}
}
- return new NewNodeDescriptor(parent, target, needsReplacing);
+ return new NewNodeDescriptor(parent, target, targetExists);
}
private static Node getParentNode(List parts, Node startParent, Session session, String target)
@@ -120,7 +122,7 @@ public void run(Config config, Node node, HopContext context) throws RepositoryE
}
final NewNodeDescriptor descriptor = resolvePathToNewNode(parent, newName, config.conflict, context);
- if (descriptor.getParent().hasNode(descriptor.getNewChildName())) {
+ if (descriptor.targetExists) {
return;
}
@@ -148,7 +150,7 @@ public static class NewNodeDescriptor {
private final Node parent;
private final String newChildName;
- private final boolean needsReplacing;
+ private final boolean targetExists;
}
@AllArgsConstructor
diff --git a/src/test/java/com/swisscom/aem/tools/impl/hops/CreateChildNodeTest.java b/src/test/java/com/swisscom/aem/tools/impl/hops/CreateChildNodeTest.java
new file mode 100644
index 0000000..4a9f60b
--- /dev/null
+++ b/src/test/java/com/swisscom/aem/tools/impl/hops/CreateChildNodeTest.java
@@ -0,0 +1,117 @@
+package com.swisscom.aem.tools.impl.hops;
+
+import static com.swisscom.aem.tools.testsupport.AemUtil.childNames;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.swisscom.aem.tools.jcrhopper.HopperException;
+import com.swisscom.aem.tools.jcrhopper.Runner;
+import com.swisscom.aem.tools.jcrhopper.RunnerBuilder;
+import com.swisscom.aem.tools.jcrhopper.config.ConflictResolution;
+import com.swisscom.aem.tools.jcrhopper.config.Script;
+import io.wcm.testing.mock.aem.junit5.AemContext;
+import io.wcm.testing.mock.aem.junit5.AemContextExtension;
+import io.wcm.testing.mock.aem.junit5.JcrOakAemContext;
+import java.util.Arrays;
+import java.util.Collections;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(AemContextExtension.class)
+class CreateChildNodeTest {
+
+ public final AemContext context = new JcrOakAemContext();
+
+ private RunnerBuilder builder;
+ private Session session;
+
+ @BeforeEach
+ public void setUp() {
+ context.create().resource("/content");
+ context.create().resource("/content/child");
+ context.create().resource("/content/child/one");
+
+ builder = Runner.builder().addHop(new CreateChildNode());
+ session = context.resourceResolver().adaptTo(Session.class);
+ }
+
+ @Test
+ public void create_nonexisting() throws RepositoryException, HopperException {
+ builder.build(new Script(new CreateChildNode.Config().withName("child-two"))).run(session.getNode("/content"), true);
+
+ assertEquals(Arrays.asList("child", "child-two"), childNames(context.resourceResolver().getResource("/content")));
+ assertEquals("nt:unstructured", session.getNode("/content/child-two").getPrimaryNodeType().getName());
+ }
+
+ @Test
+ public void create_existing_abort() {
+ assertThrows(HopperException.class, () -> {
+ builder
+ .build(
+ new Script(
+ new CreateChildNode.Config().withName("child").withConflict(ConflictResolution.THROW),
+ new CreateChildNode.Config().withName("child2").withConflict(ConflictResolution.THROW)
+ )
+ )
+ .run(session.getNode("/content"), true);
+ });
+
+ assertEquals(Collections.singletonList("child"), childNames(context.resourceResolver().getResource("/content")));
+ }
+
+ @Test
+ public void create_existing_ignore_no_recurse() throws RepositoryException, HopperException {
+ builder
+ .build(
+ new Script(
+ new CreateChildNode.Config()
+ .withName("child")
+ .withConflict(ConflictResolution.IGNORE)
+ .withRunOnExistingNode(false)
+ .withHops(Collections.singletonList(new CreateChildNode.Config().withName("../child3"))),
+ new CreateChildNode.Config().withName("child2").withConflict(ConflictResolution.THROW)
+ )
+ )
+ .run(session.getNode("/content"), true);
+ assertEquals(Arrays.asList("child", "child2"), childNames(context.resourceResolver().getResource("/content")));
+ }
+
+ @Test
+ public void create_existing_ignore_recurse() throws RepositoryException, HopperException {
+ builder
+ .build(
+ new Script(
+ new CreateChildNode.Config()
+ .withName("child")
+ .withConflict(ConflictResolution.IGNORE)
+ .withRunOnExistingNode(true)
+ .withHops(Collections.singletonList(new CreateChildNode.Config().withName("../child3"))),
+ new CreateChildNode.Config().withName("child2").withConflict(ConflictResolution.THROW)
+ )
+ )
+ .run(session.getNode("/content"), true);
+ assertEquals(Arrays.asList("child", "child3", "child2"), childNames(context.resourceResolver().getResource("/content")));
+ }
+
+ @Test
+ public void create_existing_force() throws RepositoryException, HopperException {
+ assertEquals("nt:unstructured", session.getNode("/content/child").getPrimaryNodeType().getName());
+ builder
+ .build(
+ new Script(
+ new CreateChildNode.Config()
+ .withName("child")
+ .withPrimaryType("cq:Page")
+ .withConflict(ConflictResolution.FORCE)
+ .withHops(Collections.singletonList(new CreateChildNode.Config().withName("../child3"))),
+ new CreateChildNode.Config().withName("child2").withConflict(ConflictResolution.THROW)
+ )
+ )
+ .run(session.getNode("/content"), true);
+ assertEquals(Arrays.asList("child", "child3", "child2"), childNames(context.resourceResolver().getResource("/content")));
+ assertEquals("cq:Page", session.getNode("/content/child").getPrimaryNodeType().getName());
+ }
+}
diff --git a/src/test/java/com/swisscom/aem/tools/jcrhopper/ScriptTest.java b/src/test/java/com/swisscom/aem/tools/jcrhopper/ScriptTest.java
index e33a4e3..baf6c9c 100644
--- a/src/test/java/com/swisscom/aem/tools/jcrhopper/ScriptTest.java
+++ b/src/test/java/com/swisscom/aem/tools/jcrhopper/ScriptTest.java
@@ -41,8 +41,8 @@ public void fromJson() throws IOException {
assertEquals(
"Script(hops=[" +
"SetProperty.Config(propertyName=sling:resourceType, value='swisscom/sdx/components/containers/tabs', conflict=FORCE), " +
- "CreateChildNode.Config(name=contents, primaryType=nt:unstructured, conflict=IGNORE, hops=[" +
- "CreateChildNode.Config(name=shared, primaryType=nt:unstructured, conflict=IGNORE, hops=[" +
+ "CreateChildNode.Config(name=contents, primaryType=nt:unstructured, conflict=IGNORE, runOnExistingNode=false, hops=[" +
+ "CreateChildNode.Config(name=shared, primaryType=nt:unstructured, conflict=IGNORE, runOnExistingNode=false, hops=[" +
"SetProperty.Config(propertyName=sling:resourceType, value='swisscom/sdx/components/responsivegrid', conflict=FORCE)" +
"])" +
"]), " +
diff --git a/src/test/java/com/swisscom/aem/tools/testsupport/AemUtil.java b/src/test/java/com/swisscom/aem/tools/testsupport/AemUtil.java
new file mode 100644
index 0000000..27027ab
--- /dev/null
+++ b/src/test/java/com/swisscom/aem/tools/testsupport/AemUtil.java
@@ -0,0 +1,17 @@
+package com.swisscom.aem.tools.testsupport;
+
+import java.util.List;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+import org.apache.sling.api.resource.Resource;
+
+public final class AemUtil {
+
+ public static List childNames(Resource parent) {
+ return StreamSupport.stream(Spliterators.spliteratorUnknownSize(parent.listChildren(), Spliterator.ORDERED), false)
+ .map(Resource::getName)
+ .collect(Collectors.toList());
+ }
+}