Loading src/main/java/it/inaf/oats/vospace/BaseNodeController.java +5 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,11 @@ public abstract class BaseNodeController { private HttpServletRequest servletRequest; protected String getPath() { // This is to allow calls from the code to CreateNodeController // since request url is not set if(servletRequest.getRequestURL() == null) return null; String requestURL = servletRequest.getRequestURL().toString(); try { return NodeUtils.getPathFromRequestURLString(requestURL); Loading src/main/java/it/inaf/oats/vospace/CreateNodeController.java +13 −5 Original line number Diff line number Diff line Loading @@ -37,15 +37,23 @@ public class CreateNodeController extends BaseNodeController { produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}) public Node createNode(@RequestBody Node node, User principal) { String path = getPath(); LOG.debug("createNode called for path {}", path); LOG.debug("createNode called for node with URI {}", node.getUri()); // Validate payload node URI if (!isValidURI(node.getUri())) { throw new InvalidURIException(node.getUri()); } String path; if(getPath() == null) { LOG.debug("createNode called internally with null path"); path = node.getUri().replaceAll("vos://[^/]+", ""); } else { path = getPath(); LOG.debug("createNode called for path {}", path); } // Check if payload URI is consistent with http request if (!isUrlConsistentWithPayloadURI(node.getUri(), path)) { throw new InvalidURIException(node.getUri(), path); Loading src/main/java/it/inaf/oats/vospace/JobService.java +4 −5 Original line number Diff line number Diff line Loading @@ -105,9 +105,8 @@ public class JobService { handleVoSpaceUrlsListResult(job, transfer); break; case moveNode: throw new UnsupportedOperationException("Not implemented yet"); // handleMoveNode(job, transfer); // break; handleMoveNode(transfer); break; default: throw new UnsupportedOperationException("Not implemented yet"); } Loading Loading @@ -147,9 +146,9 @@ public class JobService { uriService.setTransferJobResult(job, transfer); } private void handleMoveNode(JobSummary job, Transfer transfer) private void handleMoveNode(Transfer transfer) { moveService.processMoveJob(job, transfer); moveService.processMoveJob(transfer); } private JobDirection getJobDirection(Transfer transfer) { Loading src/main/java/it/inaf/oats/vospace/MoveService.java +124 −27 Original line number Diff line number Diff line Loading @@ -8,62 +8,121 @@ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; import it.inaf.oats.vospace.datamodel.NodeProperties; import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.exception.ContainerNotFoundException; import it.inaf.oats.vospace.exception.DuplicateNodeException; import it.inaf.oats.vospace.exception.InternalFaultException; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.exception.NodeNotFoundException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.persistence.NodeDAO; import java.util.List; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Transfer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; @Service @EnableTransactionManagement public class MoveService { @Autowired private NodeDAO nodeDao; @Value("${vospace-authority}") private String authority; @Autowired private CreateNodeController createNodeController; @Autowired private HttpServletRequest servletRequest; public void processMoveJob(JobSummary job, Transfer transfer) { @Transactional(rollbackFor = { Exception.class }) public void processMoveJob(Transfer transfer) { // Get Source Path String sourcePath = transfer.getTarget(); // Get Source Vos Path String sourcePath = transfer.getTarget().substring("vos://".length() + authority.length()); // Get Destination Path (it's in transfer direction) String destinationPath = transfer.getDirection(); // Get Destination Vos Path (it's in transfer direction) String destinationPath = transfer.getDirection().substring("vos://".length() + authority.length()); // Extract User permissions from servlet request User user = (User) servletRequest.getUserPrincipal(); Long sourceId = nodeDao.getNodeId(sourcePath); List<Node> branchList = nodeDao.listNodesInBranch(sourceId, true); // Generic common validation for move process job paths this.validatePath(sourcePath); this.validatePath(destinationPath); // Get source node (this locks it with SELECT ... FOR UPDATE) Optional<Long> sourceIdOpt = nodeDao.getNodeId(sourcePath); if (sourceIdOpt.isEmpty()) { throw new NodeNotFoundException(sourcePath); } Long sourceId = sourceIdOpt.get(); // Get node branch with root == source. All nodes are locked // with SELECT ... FOR UPDATE List<Node> sourceBranchNodeList = nodeDao.listNodesInBranch(sourceId, true); // Check feasibility of move on source branch if (!isWritePermissionsValid(branchList, user)) { // Check feasibility of move for source branch if (!isWritePermissionsValid(sourceBranchNodeList, user)) { throw new PermissionDeniedException(sourcePath); } if(sourcePath.equals(destinationPath)) if (sourcePath.equals(destinationPath)) { return; } if(!isMoveable(branchList)) { if (!isMoveable(sourceBranchNodeList)) { throw new NodeBusyException(sourcePath); } // Set branch at busy nodeDao.setBranchBusy(sourceId, true); // EDGE CASE: a node with the same destination path is created by another // process in the database between destination check and move. // This applies also to rename. // the move process would overwrite it or worse create two nodes with // different ids and same vos path // possible solution: check for busyness of parent node when creating // a new node? May it work and be compliant? // check if destination node exists before if (this.checkNodeExistence(destinationPath)) { throw new DuplicateNodeException(destinationPath); } // Compare source and destination paths parents and see if it's just a rename if (NodeUtils.getParentPath(sourcePath) .equals(NodeUtils.getParentPath(destinationPath))) { // Compare source and destination paths and see if it's just a rename if(NodeUtils.getParentPath(sourcePath).equals(NodeUtils.getParentPath(destinationPath))) { nodeDao.renameNode(sourceId, NodeUtils.getLastPathElement(destinationPath)); } else { Long destParentId; Optional<Long> optDest = nodeDao.getNodeId(NodeUtils.getParentPath(destinationPath)); if (optDest.isEmpty()) { // Try to create parent container(s) destParentId = this.createDestination(NodeUtils.getParentPath(destinationPath), user); } else { this.moveNode(sourceId, sourcePath, destinationPath, user); Node parentNode = nodeDao.getNodeById(optDest.get(), true) .orElseThrow(()-> new NodeNotFoundException(NodeUtils.getParentPath(destinationPath))); this.validateDestinationParentNode(parentNode, user); destParentId = optDest.get(); } this.moveNode(sourceId, destParentId, NodeUtils.getLastPathElement(destinationPath)); } nodeDao.setBranchBusy(sourceId, false); Loading @@ -81,7 +140,7 @@ public class MoveService { } // All nodes must comply to have a true // All nodes must comply to have a true output private boolean isMoveable(List<Node> list) { return list.stream().allMatch((n) -> { boolean busy = NodeUtils.getIsBusy(n); Loading @@ -94,10 +153,48 @@ public class MoveService { }); } private void moveNode(Long sourceId, Long destParentId, String newNodeName) { nodeDao.moveNodeBranch(sourceId, destParentId); nodeDao.renameNode(sourceId, newNodeName); } private void validatePath(String path) { if (path.equals("/")) { throw new IllegalArgumentException("Cannot move root node or to root node"); } } private void moveNode(Long sourceId, String sourcePath, String destPath, User user) { private boolean checkNodeExistence(String path) { Optional<Long> optNodeId = nodeDao.getNodeId(path); return optNodeId.isPresent(); } // Returns node id of created destination private Long createDestination(String path, User user) { List<String> components = NodeUtils.subPathComponents(path); for (int i = 0; i < components.size(); i++) { if (!this.checkNodeExistence(components.get(i))) { ContainerNode node = new ContainerNode(); node.setUri("vos://" + this.authority + components.get(i)); createNodeController.createNode(node, user); } } return nodeDao.getNodeId(path).orElseThrow(()-> new InternalFaultException("Unable to create destination at path: "+path)); } private void validateDestinationParentNode(Node node, User user) { if (!(node instanceof ContainerNode)) { throw new ContainerNotFoundException( NodeUtils.getVosPath(node)); } if (!NodeUtils.checkIfWritable(node, user.getName(), user.getGroups())) { throw new PermissionDeniedException(NodeUtils.getVosPath(node)); } } } src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java +113 −9 Original line number Diff line number Diff line Loading @@ -231,24 +231,61 @@ public class NodeDAO { return node; } public Long getNodeId(String nodePath) { public Optional<Long> getNodeId(String nodeVosPath) { String sql = "SELECT node_id FROM node_vos_path WHERE vos_path = ? FOR UPDATE"; List<Long> nodeIdList = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, nodePath); ps.setString(1, nodeVosPath); return ps; }, (row, index) -> { return row.getLong("node_id"); }); // Node id is if (nodeIdList.isEmpty()) { throw new NodeNotFoundException(nodePath); switch (nodeIdList.size()) { case 0: return Optional.empty(); case 1: return Optional.of(nodeIdList.get(0)); default: throw new InternalFaultException("More than 1 node id at path: " + nodeVosPath); } } // Node id is PRIMARY KEY: uniqueness is enforced at DB level return nodeIdList.get(0); public Optional<Node> getNodeById(Long nodeId, boolean enforceTapeStoredCheck) { String sql = "SELECT os.vos_path, loc.location_type, n.node_id, type, async_trans, sticky, busy_state, creator_id, group_read, group_write,\n" + "is_public, content_length, created_on, last_modified, accept_views, provide_views\n" + "FROM node n\n" + "JOIN node_vos_path os ON n.node_id = os.node_id\n" + "JOIN location loc ON n.location_id = loc.location_id\n" + "WHERE n.node_id = ?\n" + "FOR UPDATE"; List<Node> result = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setLong(1, nodeId); return ps; }, (row, index) -> { if (enforceTapeStoredCheck && row.getString("location_type").equals("async")) { throw new InternalFaultException( "Node id: " + nodeId + " has async location type. " + "Failure due to enforced check."); } return getNodeFromResultSet(row); }); switch (result.size()) { case 0: return Optional.empty(); case 1: return Optional.of(result.get(0)); default: throw new InternalFaultException("Multiple nodes with id: " + nodeId); } } // First node is the root node Loading @@ -268,7 +305,8 @@ public class NodeDAO { }, (row, index) -> { if (enforceTapeStoredCheck && row.getString("location_type").equals("async")) { throw new InternalFaultException( "At least one node in branch has async location type. " "At least one node in branch with root id: " + rootNodeId + " has async location type. " + "Failure due to enforced check."); } return getNodeFromResultSet(row); Loading Loading @@ -303,6 +341,72 @@ public class NodeDAO { } /* // unused? public Optional<String> getNodeLtreePathById(Long nodeId) { String sql = "SELECT path FROM node WHERE node_id = ? FOR UPDATE"; List<String> pathList = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setLong(1, nodeId); return ps; }, (row, index) -> { return row.getString("path"); }); switch (pathList.size()) { case 0: return Optional.empty(); case 1: return Optional.of(pathList.get(0)); default: throw new InternalFaultException("More than one id = " + nodeId); } } //remove public String getParentPath(Long id) { String sql = "SELECT parent_path FROM node WHERE node_id = ?"; List<String> nodeIdList = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setLong(1, id); return ps; }, (row, index) -> { return row.getString("parent_path"); }); if(nodeIdList.size() > 0) { return nodeIdList.get(0); } else { return null; } } */ public void moveNodeBranch(Long sourceRootId, Long destinationParentId) { String sql = "UPDATE node\n"+ "SET parent_path = ((SELECT path FROM node WHERE node_id = ?) ||\n"+ "(CASE WHEN node_id = ? THEN '' ELSE subpath(parent_path, index(parent_path,(?::varchar)::ltree)) END))\n" + "WHERE path ~ ('*.' || ? || '.*')::lquery"; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setLong(1, destinationParentId); ps.setLong(2, sourceRootId); ps.setLong(3, sourceRootId); ps.setLong(4, sourceRootId); return ps; }); } public void deleteNode(String path) { int nodesWithPath = countNodesWithPath(path); if (nodesWithPath == 0) { Loading Loading
src/main/java/it/inaf/oats/vospace/BaseNodeController.java +5 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,11 @@ public abstract class BaseNodeController { private HttpServletRequest servletRequest; protected String getPath() { // This is to allow calls from the code to CreateNodeController // since request url is not set if(servletRequest.getRequestURL() == null) return null; String requestURL = servletRequest.getRequestURL().toString(); try { return NodeUtils.getPathFromRequestURLString(requestURL); Loading
src/main/java/it/inaf/oats/vospace/CreateNodeController.java +13 −5 Original line number Diff line number Diff line Loading @@ -37,15 +37,23 @@ public class CreateNodeController extends BaseNodeController { produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}) public Node createNode(@RequestBody Node node, User principal) { String path = getPath(); LOG.debug("createNode called for path {}", path); LOG.debug("createNode called for node with URI {}", node.getUri()); // Validate payload node URI if (!isValidURI(node.getUri())) { throw new InvalidURIException(node.getUri()); } String path; if(getPath() == null) { LOG.debug("createNode called internally with null path"); path = node.getUri().replaceAll("vos://[^/]+", ""); } else { path = getPath(); LOG.debug("createNode called for path {}", path); } // Check if payload URI is consistent with http request if (!isUrlConsistentWithPayloadURI(node.getUri(), path)) { throw new InvalidURIException(node.getUri(), path); Loading
src/main/java/it/inaf/oats/vospace/JobService.java +4 −5 Original line number Diff line number Diff line Loading @@ -105,9 +105,8 @@ public class JobService { handleVoSpaceUrlsListResult(job, transfer); break; case moveNode: throw new UnsupportedOperationException("Not implemented yet"); // handleMoveNode(job, transfer); // break; handleMoveNode(transfer); break; default: throw new UnsupportedOperationException("Not implemented yet"); } Loading Loading @@ -147,9 +146,9 @@ public class JobService { uriService.setTransferJobResult(job, transfer); } private void handleMoveNode(JobSummary job, Transfer transfer) private void handleMoveNode(Transfer transfer) { moveService.processMoveJob(job, transfer); moveService.processMoveJob(transfer); } private JobDirection getJobDirection(Transfer transfer) { Loading
src/main/java/it/inaf/oats/vospace/MoveService.java +124 −27 Original line number Diff line number Diff line Loading @@ -8,62 +8,121 @@ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; import it.inaf.oats.vospace.datamodel.NodeProperties; import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.exception.ContainerNotFoundException; import it.inaf.oats.vospace.exception.DuplicateNodeException; import it.inaf.oats.vospace.exception.InternalFaultException; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.exception.NodeNotFoundException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.persistence.NodeDAO; import java.util.List; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Transfer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; @Service @EnableTransactionManagement public class MoveService { @Autowired private NodeDAO nodeDao; @Value("${vospace-authority}") private String authority; @Autowired private CreateNodeController createNodeController; @Autowired private HttpServletRequest servletRequest; public void processMoveJob(JobSummary job, Transfer transfer) { @Transactional(rollbackFor = { Exception.class }) public void processMoveJob(Transfer transfer) { // Get Source Path String sourcePath = transfer.getTarget(); // Get Source Vos Path String sourcePath = transfer.getTarget().substring("vos://".length() + authority.length()); // Get Destination Path (it's in transfer direction) String destinationPath = transfer.getDirection(); // Get Destination Vos Path (it's in transfer direction) String destinationPath = transfer.getDirection().substring("vos://".length() + authority.length()); // Extract User permissions from servlet request User user = (User) servletRequest.getUserPrincipal(); Long sourceId = nodeDao.getNodeId(sourcePath); List<Node> branchList = nodeDao.listNodesInBranch(sourceId, true); // Generic common validation for move process job paths this.validatePath(sourcePath); this.validatePath(destinationPath); // Get source node (this locks it with SELECT ... FOR UPDATE) Optional<Long> sourceIdOpt = nodeDao.getNodeId(sourcePath); if (sourceIdOpt.isEmpty()) { throw new NodeNotFoundException(sourcePath); } Long sourceId = sourceIdOpt.get(); // Get node branch with root == source. All nodes are locked // with SELECT ... FOR UPDATE List<Node> sourceBranchNodeList = nodeDao.listNodesInBranch(sourceId, true); // Check feasibility of move on source branch if (!isWritePermissionsValid(branchList, user)) { // Check feasibility of move for source branch if (!isWritePermissionsValid(sourceBranchNodeList, user)) { throw new PermissionDeniedException(sourcePath); } if(sourcePath.equals(destinationPath)) if (sourcePath.equals(destinationPath)) { return; } if(!isMoveable(branchList)) { if (!isMoveable(sourceBranchNodeList)) { throw new NodeBusyException(sourcePath); } // Set branch at busy nodeDao.setBranchBusy(sourceId, true); // EDGE CASE: a node with the same destination path is created by another // process in the database between destination check and move. // This applies also to rename. // the move process would overwrite it or worse create two nodes with // different ids and same vos path // possible solution: check for busyness of parent node when creating // a new node? May it work and be compliant? // check if destination node exists before if (this.checkNodeExistence(destinationPath)) { throw new DuplicateNodeException(destinationPath); } // Compare source and destination paths parents and see if it's just a rename if (NodeUtils.getParentPath(sourcePath) .equals(NodeUtils.getParentPath(destinationPath))) { // Compare source and destination paths and see if it's just a rename if(NodeUtils.getParentPath(sourcePath).equals(NodeUtils.getParentPath(destinationPath))) { nodeDao.renameNode(sourceId, NodeUtils.getLastPathElement(destinationPath)); } else { Long destParentId; Optional<Long> optDest = nodeDao.getNodeId(NodeUtils.getParentPath(destinationPath)); if (optDest.isEmpty()) { // Try to create parent container(s) destParentId = this.createDestination(NodeUtils.getParentPath(destinationPath), user); } else { this.moveNode(sourceId, sourcePath, destinationPath, user); Node parentNode = nodeDao.getNodeById(optDest.get(), true) .orElseThrow(()-> new NodeNotFoundException(NodeUtils.getParentPath(destinationPath))); this.validateDestinationParentNode(parentNode, user); destParentId = optDest.get(); } this.moveNode(sourceId, destParentId, NodeUtils.getLastPathElement(destinationPath)); } nodeDao.setBranchBusy(sourceId, false); Loading @@ -81,7 +140,7 @@ public class MoveService { } // All nodes must comply to have a true // All nodes must comply to have a true output private boolean isMoveable(List<Node> list) { return list.stream().allMatch((n) -> { boolean busy = NodeUtils.getIsBusy(n); Loading @@ -94,10 +153,48 @@ public class MoveService { }); } private void moveNode(Long sourceId, Long destParentId, String newNodeName) { nodeDao.moveNodeBranch(sourceId, destParentId); nodeDao.renameNode(sourceId, newNodeName); } private void validatePath(String path) { if (path.equals("/")) { throw new IllegalArgumentException("Cannot move root node or to root node"); } } private void moveNode(Long sourceId, String sourcePath, String destPath, User user) { private boolean checkNodeExistence(String path) { Optional<Long> optNodeId = nodeDao.getNodeId(path); return optNodeId.isPresent(); } // Returns node id of created destination private Long createDestination(String path, User user) { List<String> components = NodeUtils.subPathComponents(path); for (int i = 0; i < components.size(); i++) { if (!this.checkNodeExistence(components.get(i))) { ContainerNode node = new ContainerNode(); node.setUri("vos://" + this.authority + components.get(i)); createNodeController.createNode(node, user); } } return nodeDao.getNodeId(path).orElseThrow(()-> new InternalFaultException("Unable to create destination at path: "+path)); } private void validateDestinationParentNode(Node node, User user) { if (!(node instanceof ContainerNode)) { throw new ContainerNotFoundException( NodeUtils.getVosPath(node)); } if (!NodeUtils.checkIfWritable(node, user.getName(), user.getGroups())) { throw new PermissionDeniedException(NodeUtils.getVosPath(node)); } } }
src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java +113 −9 Original line number Diff line number Diff line Loading @@ -231,24 +231,61 @@ public class NodeDAO { return node; } public Long getNodeId(String nodePath) { public Optional<Long> getNodeId(String nodeVosPath) { String sql = "SELECT node_id FROM node_vos_path WHERE vos_path = ? FOR UPDATE"; List<Long> nodeIdList = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, nodePath); ps.setString(1, nodeVosPath); return ps; }, (row, index) -> { return row.getLong("node_id"); }); // Node id is if (nodeIdList.isEmpty()) { throw new NodeNotFoundException(nodePath); switch (nodeIdList.size()) { case 0: return Optional.empty(); case 1: return Optional.of(nodeIdList.get(0)); default: throw new InternalFaultException("More than 1 node id at path: " + nodeVosPath); } } // Node id is PRIMARY KEY: uniqueness is enforced at DB level return nodeIdList.get(0); public Optional<Node> getNodeById(Long nodeId, boolean enforceTapeStoredCheck) { String sql = "SELECT os.vos_path, loc.location_type, n.node_id, type, async_trans, sticky, busy_state, creator_id, group_read, group_write,\n" + "is_public, content_length, created_on, last_modified, accept_views, provide_views\n" + "FROM node n\n" + "JOIN node_vos_path os ON n.node_id = os.node_id\n" + "JOIN location loc ON n.location_id = loc.location_id\n" + "WHERE n.node_id = ?\n" + "FOR UPDATE"; List<Node> result = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setLong(1, nodeId); return ps; }, (row, index) -> { if (enforceTapeStoredCheck && row.getString("location_type").equals("async")) { throw new InternalFaultException( "Node id: " + nodeId + " has async location type. " + "Failure due to enforced check."); } return getNodeFromResultSet(row); }); switch (result.size()) { case 0: return Optional.empty(); case 1: return Optional.of(result.get(0)); default: throw new InternalFaultException("Multiple nodes with id: " + nodeId); } } // First node is the root node Loading @@ -268,7 +305,8 @@ public class NodeDAO { }, (row, index) -> { if (enforceTapeStoredCheck && row.getString("location_type").equals("async")) { throw new InternalFaultException( "At least one node in branch has async location type. " "At least one node in branch with root id: " + rootNodeId + " has async location type. " + "Failure due to enforced check."); } return getNodeFromResultSet(row); Loading Loading @@ -303,6 +341,72 @@ public class NodeDAO { } /* // unused? public Optional<String> getNodeLtreePathById(Long nodeId) { String sql = "SELECT path FROM node WHERE node_id = ? FOR UPDATE"; List<String> pathList = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setLong(1, nodeId); return ps; }, (row, index) -> { return row.getString("path"); }); switch (pathList.size()) { case 0: return Optional.empty(); case 1: return Optional.of(pathList.get(0)); default: throw new InternalFaultException("More than one id = " + nodeId); } } //remove public String getParentPath(Long id) { String sql = "SELECT parent_path FROM node WHERE node_id = ?"; List<String> nodeIdList = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setLong(1, id); return ps; }, (row, index) -> { return row.getString("parent_path"); }); if(nodeIdList.size() > 0) { return nodeIdList.get(0); } else { return null; } } */ public void moveNodeBranch(Long sourceRootId, Long destinationParentId) { String sql = "UPDATE node\n"+ "SET parent_path = ((SELECT path FROM node WHERE node_id = ?) ||\n"+ "(CASE WHEN node_id = ? THEN '' ELSE subpath(parent_path, index(parent_path,(?::varchar)::ltree)) END))\n" + "WHERE path ~ ('*.' || ? || '.*')::lquery"; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setLong(1, destinationParentId); ps.setLong(2, sourceRootId); ps.setLong(3, sourceRootId); ps.setLong(4, sourceRootId); return ps; }); } public void deleteNode(String path) { int nodesWithPath = countNodesWithPath(path); if (nodesWithPath == 0) { Loading