Skip to content

Commit

Permalink
fix(runner): refactor copyNode hop
Browse files Browse the repository at this point in the history
Now uses serialization/deserialization for the copy
This gracefully handles all weird edge cases (like copying a node into itself and overwriting an existing node)
  • Loading branch information
sabberworm committed Nov 2, 2024
1 parent 2f72b27 commit 82624f5
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 48 deletions.
117 changes: 75 additions & 42 deletions src/main/java/com/swisscom/aem/tools/impl/hops/CopyNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,76 @@
import com.swisscom.aem.tools.jcrhopper.config.Hop;
import com.swisscom.aem.tools.jcrhopper.config.HopConfig;
import com.swisscom.aem.tools.jcrhopper.context.HopContext;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.Session;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.With;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.commons.xml.ToXmlContentHandler;
import org.osgi.service.component.annotations.Component;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

@Slf4j
@Component(service = Hop.class)
@SuppressFBWarnings
public class CopyNode implements Hop<CopyNode.Config> {

@Override
public void run(Config config, Node node, HopContext context) throws RepositoryException, HopperException {
final Node parent = node.getParent();
if (parent == null) {
context.error("Copying the root node isn’t allowed");
return;
if (StringUtils.equals(node.getPath(), "/")) {
throw new HopperException("Copying the root node isn’t allowed");
}

final String newName = context.evaluateTemplate(config.newName);
final MoveNode.NewNodeDescriptor descriptor = MoveNode.resolvePathToNewNode(parent, newName, config.conflict, context);
final MoveNode.NewNodeDescriptor descriptor = MoveNode.resolvePathToNewNode(node.getParent(), newName, config.conflict, context);
if (descriptor.isTargetExists()) {
return;
}

final String absolutePath = descriptor.getParent().getPath() + '/' + descriptor.getNewChildName();
context.info("Copying node from {} to {}", node.getPath(), absolutePath);
final Session session = node.getSession();

try {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
final ToXmlContentHandler serializingContentHandler = new ToXmlContentHandler(os);
session.exportSystemView(
node.getPath(),
new RootRenamingContentHandler(serializingContentHandler, node.getName(), descriptor.getNewChildName()),
false,
false
);
// Remove the replaced node only after the snapshot has been taken because it’s possible it was part of the copied tree
descriptor.removeReplacedNode();

context.info("Copying node from {} to {}", node.getPath(), descriptor.getAbsolutePath());

final Node copied = copyRecursive(node, descriptor.getParent(), descriptor.getNewChildName(), context);
context.runHops(copied, config.hops);
session.importXML(
descriptor.getParent().getPath(),
new ByteArrayInputStream(os.toByteArray()),
ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW
);
} catch (IOException | SAXException e) {
throw new HopperException("Importing the node at " + descriptor.getAbsolutePath() + " failed", e);
}

context.runHops(session.getNode(descriptor.getAbsolutePath()), config.hops);
}

@Nonnull
Expand All @@ -54,34 +83,6 @@ public Class<Config> getConfigType() {
return Config.class;
}

private Node copyRecursive(Node source, Node parent, String targetName, HopContext context) throws RepositoryException {
final Node target = parent.addNode(targetName, source.getPrimaryNodeType().getName());
for (NodeType mixin : source.getMixinNodeTypes()) {
target.addMixin(mixin.getName());
}
context.debug("Created {} node at {}", target.getPrimaryNodeType().getName(), target.getPath());
final PropertyIterator propIt = source.getProperties();
while (propIt.hasNext()) {
final Property prop = propIt.nextProperty();
try {
if (prop.isMultiple()) {
target.setProperty(prop.getName(), prop.getValues(), prop.getType());
} else {
target.setProperty(prop.getName(), prop.getValue(), prop.getType());
}
} catch (ConstraintViolationException ex) {
// Property is protected, assume it’s already covered implicitly by primary or mixin type
log.debug("Property {} is protected, skipping", prop.getName());
}
}
final NodeIterator nodeIt = source.getNodes();
while (nodeIt.hasNext()) {
final Node child = nodeIt.nextNode();
copyRecursive(child, target, child.getName(), context);
}
return target;
}

@Nonnull
@Override
public String getConfigTypeName() {
Expand All @@ -104,4 +105,36 @@ public static final class Config implements HopConfig {
@Nonnull
private List<HopConfig> hops = Collections.emptyList();
}

@RequiredArgsConstructor
private static final class RootRenamingContentHandler implements ContentHandler {

@Delegate(types = ContentHandler.class)
private final ContentHandler inner;

private final String oldName;
private final String newName;

private boolean isRootNode = true;

@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
Attributes usedAttributes = atts;
if (isRootNode) {
final AttributesImpl attributesImpl = new AttributesImpl(usedAttributes);
final int index = attributesImpl.getIndex("sv:name");
if (index == -1) {
log.warn("sv:name attribute not found on element {}", qName);
} else if (StringUtils.equals(attributesImpl.getValue(index), oldName)) {
attributesImpl.setValue(index, newName);
usedAttributes = attributesImpl;
} else {
log.warn("sv:name expected to be {} but is {}", oldName, attributesImpl.getValue(index));
}
}

isRootNode = false;
inner.startElement(uri, localName, qName, usedAttributes);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public void run(Config config, Node node, HopContext context) throws RepositoryE
final String primaryType = context.evaluateTemplate(config.primaryType);

final MoveNode.NewNodeDescriptor descriptor = MoveNode.resolvePathToNewNode(node, name, config.conflict, context);
descriptor.removeReplacedNode();

final Node childNode;
if (descriptor.isTargetExists()) {
Expand Down
28 changes: 22 additions & 6 deletions src/main/java/com/swisscom/aem/tools/impl/hops/MoveNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static NewNodeDescriptor resolvePathToNewNode(
}

@SuppressWarnings("PMD.LooseCoupling")
final LinkedList<String> parts = Arrays.stream(target.split("\\/"))
final LinkedList<String> parts = Arrays.stream(target.split("/"))
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toCollection(LinkedList::new));

Expand All @@ -65,6 +65,7 @@ public static NewNodeDescriptor resolvePathToNewNode(

// FIXME: What about repositories with support for same-name siblings?
boolean targetExists = parent.hasNode(target);
Node nodeToRemove = null;
if (targetExists) {
final Node childNode = parent.getNode(target);
switch (conflict) {
Expand All @@ -73,7 +74,7 @@ public static NewNodeDescriptor resolvePathToNewNode(
break;
case FORCE:
context.info("Replacing existing node {}", childNode.getPath());
childNode.remove();
nodeToRemove = childNode;
targetExists = false;
break;
case THROW:
Expand All @@ -83,7 +84,8 @@ public static NewNodeDescriptor resolvePathToNewNode(
}
}

return new NewNodeDescriptor(parent, target, targetExists);
final String absolutePath = StringUtils.stripEnd(parent.getPath(), "/") + '/' + target;
return new NewNodeDescriptor(parent, target, absolutePath, targetExists, nodeToRemove);
}

private static Node getParentNode(List<String> parts, Node startParent, Session session, String target)
Expand Down Expand Up @@ -128,11 +130,10 @@ public void run(Config config, Node node, HopContext context) throws RepositoryE
}

final Node effectiveParent = descriptor.getParent();
final String absolutePath = StringUtils.stripEnd(effectiveParent.getPath(), "/") + '/' + descriptor.getNewChildName();
context.info("Moving node from {} to {}", node.getPath(), absolutePath);
context.info("Moving node from {} to {}", node.getPath(), descriptor.absolutePath);

final String nextSiblingName = getNextSiblingName(node, parent, descriptor.parent);
node.getSession().move(node.getPath(), absolutePath);
node.getSession().move(node.getPath(), descriptor.absolutePath);

if (nextSiblingName != null) {
effectiveParent.orderBefore(descriptor.newChildName, nextSiblingName);
Expand Down Expand Up @@ -172,7 +173,22 @@ public static class NewNodeDescriptor {

private final Node parent;
private final String newChildName;
private final String absolutePath;
private final boolean targetExists;
private final Node nodeToRemove;

/**
* Removes the replaced node if conflict was set to FORCE.
* <p>
* Usually required to be called before the action is set to execute
*
* @throws RepositoryException if the removal fails
*/
public void removeReplacedNode() throws RepositoryException {
if (nodeToRemove != null) {
nodeToRemove.remove();
}
}
}

@AllArgsConstructor
Expand Down
Loading

0 comments on commit 82624f5

Please sign in to comment.