Skip to content

Commit

Permalink
feat(runner): add option to control running descendant steps on exist…
Browse files Browse the repository at this point in the history
…ing nodes
  • Loading branch information
sabberworm committed Oct 31, 2024
1 parent 3293921 commit 8bb8aa8
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 15 deletions.
2 changes: 2 additions & 0 deletions src/main/frontend/model/hops/createChildNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ export interface Type extends AnyHop {
type: 'createChildNode';
name: string;
primaryType?: string;
runOnExistingNode: boolean;
conflict: ConflictResolutionStrategy;
hops?: Hop[];
}

export const defaultConfig: Partial<Type> = {
conflict: 'ignore',
name: 'child-name',
runOnExistingNode: false,
};

export const title = 'Create Node';
Expand Down
16 changes: 13 additions & 3 deletions src/main/frontend/sections/editor/types/CreateChildNodeStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement, { parentHops: Hop[]; hop: Type }>(function CreateChildNodeStep(
{ parentHops, hop },
Expand All @@ -32,9 +33,17 @@ export const CreateChildNodeStep = forwardRef<HTMLDivElement, { parentHops: Hop[
<Conflict
label="If the target node exists"
forceLabel="Replace the target node"
ignoreLabel="Ignore conflict"
value={hop.conflict ?? 'ignore'}
onChange={conflict => (hop.conflict = conflict)}
/>
{hop.conflict == 'ignore' ? (
<Switch
label="Run on existing node"
value={hop.runOnExistingNode}
onChange={runOnExistingNode => (hop.runOnExistingNode = runOnExistingNode)}
/>
) : undefined}
<Help title={title}>
<h5>Name of New Child Node</h5>
<p>The name of the child node to be created.</p>
Expand All @@ -52,10 +61,11 @@ export const CreateChildNodeStep = forwardRef<HTMLDivElement, { parentHops: Hop[
The primary type to set on the new node. If left empty, defaults to <code>nt:unstructured</code>
</p>
<h5>If the target node exists</h5>
<p>How to handle the case where the target node already exists.</p>
<h5>Run on existing node</h5>
<p>
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”).
</p>
</Help>
</StepEditor>
Expand Down
11 changes: 9 additions & 2 deletions src/main/frontend/widgets/Conflict.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Select
value={value}
label={label}
onChange={v => onChange(v)}
list={[
['ignore', 'Ignore conflict, abort the current action'],
['ignore', ignoreLabel],
['throw', 'Throw an exception, stop pipeline'],
['force', forceLabel],
]}
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/com/swisscom/aem/tools/impl/hops/CopyNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}",
Expand Down Expand Up @@ -72,6 +84,8 @@ public static final class Config implements HopConfig {
@Nonnull
private ConflictResolution conflict = ConflictResolution.IGNORE;

private boolean runOnExistingNode;

@Nonnull
private List<HopConfig> hops = Collections.emptyList();
}
Expand Down
12 changes: 7 additions & 5 deletions src/main/java/com/swisscom/aem/tools/impl/hops/MoveNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()));
Expand All @@ -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<String> parts, Node startParent, Session session, String target)
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)" +
"])" +
"]), " +
Expand Down
17 changes: 17 additions & 0 deletions src/test/java/com/swisscom/aem/tools/testsupport/AemUtil.java
Original file line number Diff line number Diff line change
@@ -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<String> childNames(Resource parent) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(parent.listChildren(), Spliterator.ORDERED), false)
.map(Resource::getName)
.collect(Collectors.toList());
}
}

0 comments on commit 8bb8aa8

Please sign in to comment.