/* * This file is part of vospace-rest * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.oats.vospace.persistence; import it.inaf.oats.vospace.DeleteNodeController; import it.inaf.oats.vospace.datamodel.NodeProperties; import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.exception.InternalFaultException; import java.sql.Array; import net.ivoa.xml.vospace.v2.Node; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import javax.sql.DataSource; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.Property; import net.ivoa.xml.vospace.v2.View; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository public class NodeDAO { private static final Logger LOG = LoggerFactory.getLogger(DeleteNodeController.class); @Value("${vospace-authority}") private String authority; private final JdbcTemplate jdbcTemplate; @Autowired public NodeDAO(DataSource dataSource) { jdbcTemplate = new JdbcTemplate(dataSource); } public void createNode(Node myNode) { String nodeURI = myNode.getUri(); String path = nodeURI.replaceAll("vos://[^/]+", ""); String parentPath = NodeUtils.getParentPath(path); List paths = getNodePathsFromDB(nodeURI); if (paths.isEmpty()) { throw new IllegalStateException("Unable to find parent node during node creation"); } if (paths.size() > 1) { throw new IllegalStateException("Multiple ltree parent paths found for " + parentPath); } StringBuilder sb = new StringBuilder(); sb.append("INSERT INTO node"); sb.append(" (name, busy_state, creator_id, group_read, group_write,"); sb.append(" is_public, parent_path, parent_relative_path, type, accept_views, provide_views)"); sb.append(" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sb.toString()); int i = 0; ps.setString(++i, NodeUtils.getNodeName(myNode)); ps.setBoolean(++i, NodeUtils.getIsBusy(myNode)); ps.setString(++i, NodeProperties.getStandardNodePropertyByName(myNode, "creator")); ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getStandardNodePropertyByName(myNode, "groupread"))); ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getStandardNodePropertyByName(myNode, "groupwrite"))); ps.setBoolean(++i, Boolean.valueOf(NodeProperties.getStandardNodePropertyByName(myNode, "publicread"))); ps.setObject(++i, paths.get(0).path, Types.OTHER); ps.setObject(++i, paths.get(0).relativePath, Types.OTHER); ps.setObject(++i, NodeUtils.getDbNodeType(myNode), Types.OTHER); ps.setObject(++i, fromViewsToArray(ps, myNode, d -> d.getAccepts()), Types.OTHER); ps.setObject(++i, fromViewsToArray(ps, myNode, d -> d.getProvides()), Types.OTHER); return ps; }); } public Optional listNode(String path) { String sql = "SELECT (CASE WHEN c.path = n.path THEN ? ELSE (? || ? || c.name) END) AS vos_path, c.node_id, c.name,\n" + "c.type, c.async_trans, c.sticky, c.busy_state, c.creator_id, c.group_read, c.group_write,\n" + "c.is_public, c.content_length, c.created_on, c.last_modified, c.accept_views, c.provide_views\n" + "FROM node n\n" + "JOIN node c ON c.path ~ (n.path::varchar || ? || '*{1}')::lquery OR c.path = n.path\n" + "WHERE n.node_id = id_from_vos_path(?)\n" + "ORDER BY vos_path"; List parentAndChildren = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); int i = 0; ps.setString(++i, path); ps.setString(++i, path); ps.setString(++i, "/".equals(path) ? "" : "/"); ps.setString(++i, "/".equals(path) ? "" : "."); ps.setString(++i, path); return ps; }, (row, index) -> { return getNodeFromResultSet(row); }); if (parentAndChildren.isEmpty()) { return Optional.empty(); } // Query returns parent as first node Node node = parentAndChildren.get(0); // Fill children if (node instanceof ContainerNode && parentAndChildren.size() > 1) { ContainerNode parent = (ContainerNode) node; for (int i = 1; i < parentAndChildren.size(); i++) { parent.getNodes().add(parentAndChildren.get(i)); } } return Optional.of(node); } public Node setNode(Node newNode) { return setNode(newNode, false); } /** * If recursive flag is true the update is applied to children too. */ public Node setNode(Node newNode, boolean recursive) { String vosPath = NodeUtils.getVosPath(newNode); StringBuilder sb = new StringBuilder(); sb.append("UPDATE node"); sb.append(" SET group_read = ?, group_write = ?, is_public = ? "); sb.append(" FROM node_vos_path p WHERE p.vos_path = ? AND p.node_id = node.node_id "); if (recursive) { updatePermissionsRecursively(newNode, vosPath); } else { jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sb.toString()); int i = 0; ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getNodePropertyByURI(newNode, NodeProperties.GROUP_READ_URI))); ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getNodePropertyByURI(newNode, NodeProperties.GROUP_WRITE_URI))); ps.setBoolean(++i, Boolean.valueOf(NodeProperties.getNodePropertyByURI(newNode, NodeProperties.PUBLIC_READ_URI))); ps.setString(++i, vosPath); return ps; }); } return newNode; } private String getAllLevelsChildrenSelector(String path) { String select = "(SELECT path FROM node WHERE node_id = (SELECT node_id FROM node_vos_path WHERE vos_path = ?))::varchar || '"; if (!"/".equals(path)) { select += "."; } select += "*{1,}'"; return select; } private Node getNodeFromResultSet(ResultSet rs) throws SQLException { Node node = NodeUtils.getTypedNode(rs.getString("type")); if (node instanceof DataNode) { DataNode dataNode = (DataNode) node; dataNode.setBusy(rs.getBoolean("busy_state")); dataNode.setAccepts(getViews(rs.getArray("accept_views"))); dataNode.setProvides(getViews(rs.getArray("provide_views"))); } node.setUri(getUri(rs.getString("vos_path"))); List properties = new ArrayList<>(); // Content length is required for CADC client compatibility String contentLength = rs.getString("content_length"); addProperty(NodeProperties.LENGTH_URI, contentLength == null ? "0" : contentLength, properties); String creationTime = rs.getString("created_on").replace(" ", "T"); addProperty(NodeProperties.INITIAL_CREATION_TIME_URI, creationTime, properties); addProperty(NodeProperties.DATE_URI, creationTime, properties); // required by CADC addProperty(NodeProperties.CREATOR_URI, rs.getString("creator_id"), properties); addProperty(NodeProperties.MODIFICATION_TIME_URI, rs.getString("last_modified"), properties); addProperty(NodeProperties.GROUP_READ_URI, getGroupsString(rs, "group_read"), properties); addProperty(NodeProperties.GROUP_WRITE_URI, getGroupsString(rs, "group_write"), properties); addProperty(NodeProperties.PUBLIC_READ_URI, String.valueOf(rs.getBoolean("is_public")), properties); addProperty("urn:async_trans", String.valueOf(rs.getBoolean("async_trans")), properties); addProperty("urn:sticky", String.valueOf(rs.getBoolean("sticky")), properties); node.setProperties(properties); return node; } public Optional getNodeId(String nodeVosPath) { String sql = "SELECT node_id FROM node_vos_path WHERE vos_path = ?"; List nodeIdList = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, nodeVosPath); return ps; }, (row, index) -> { return row.getLong("node_id"); }); 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); } } public Optional 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 = ?"; List 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 public List listNodesInBranch(Long rootNodeId, 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.path ~ ('*.' || ? || '.*')::lquery\n" + "ORDER BY n.path"; List result = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, Long.toString(rootNodeId)); return ps; }, (row, index) -> { if (enforceTapeStoredCheck && row.getString("location_type").equals("async")) { throw new InternalFaultException( "At least one node in branch with root id: " + rootNodeId + " has async location type. " + "Failure due to enforced check."); } return getNodeFromResultSet(row); }); return result; } public void setBranchBusy(Long rootNodeId, boolean busyState) { String sql = "UPDATE node SET busy_state = ?\n" + "WHERE path ~ ('*.' || ? || '.*')::lquery"; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setBoolean(1, busyState); ps.setLong(2, rootNodeId); return ps; }); } public void renameNode(Long nodeId, String name) { String sql = "UPDATE node SET name = ?\n" + "WHERE path ~ ('*.' || ?)::lquery"; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, name); ps.setLong(2, nodeId); return ps; }); } /* // unused? public Optional getNodeLtreePathById(Long nodeId) { String sql = "SELECT path FROM node WHERE node_id = ? FOR UPDATE"; List 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 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) { throw new IllegalStateException("Node at path " + path + " not found"); } if (nodesWithPath > 1) { throw new IllegalStateException("Multiple nodes at path " + path); } String insertSql = "INSERT INTO deleted_node " + "(node_id, parent_path, parent_relative_path, " + "name, os_name, tstamp_wrapper_dir, type, location_id, format, " + "async_trans, busy_state, creator_id, group_read, " + "group_write, is_public, delta, content_type, content_encoding, " + "content_length, content_md5, created_on, last_modified, " + "accept_views, provide_views, protocols, sticky)\n"; String deleteSql = "DELETE \n" + "FROM node n\n" + "USING node_vos_path os\n" + "WHERE n.node_id = os.node_id AND\n" + "(n.path ~ (" + getAllLevelsChildrenSelector(path) + ")::lquery\n" + "OR os.vos_path = ?) RETURNING\n" + "n.node_id, parent_path, parent_relative_path, " + "name, os_name, tstamp_wrapper_dir, type, location_id, format, " + "async_trans, busy_state, creator_id, group_read, " + "group_write, is_public, delta, content_type, content_encoding, " + "content_length, content_md5, created_on, last_modified, " + "accept_views, provide_views, protocols, sticky\n"; String withSql = "WITH del AS (" + deleteSql + ")"; String sql = withSql + insertSql + "SELECT * FROM del\n"; jdbcTemplate.update(sql, path, path); } // utility method for deleteNode public int countNodesWithPath(String path) { String sql = "SELECT COUNT(*) from " + "node_vos_path p " + "where p.vos_path = ?"; Object[] args = {path}; int[] types = {Types.VARCHAR}; return jdbcTemplate.queryForObject(sql, args, types, Integer.class); } public String getNodeOsName(String vosPath) { String sql = "SELECT \n" + "(CASE WHEN os_name IS NOT NULL THEN os_name ELSE name END) AS os_name\n" + "FROM node n\n" + "JOIN node_vos_path p ON n.node_id = p.node_id\n" + "WHERE p.vos_path = ?"; Object[] args = {vosPath}; int[] types = {Types.VARCHAR}; return jdbcTemplate.queryForObject(sql, args, types, String.class); } public void setNodeLocation(String vosPath, int locationId, String nodeOsName) { String sql = "UPDATE node SET location_id = ?, os_name = ? WHERE node_id = " + "(SELECT node_id FROM node_vos_path WHERE vos_path = ?)"; int updated = jdbcTemplate.update(sql, ps -> { ps.setInt(1, locationId); ps.setString(2, nodeOsName); ps.setString(3, vosPath); }); if (updated != 1) { throw new InternalFaultException("Unable to set node location for path " + vosPath); } } private String getGroupsString(ResultSet rs, String column) throws SQLException { Array array = rs.getArray(column); if (array == null) { return null; } return String.join(" ", (String[]) array.getArray()); } // If value is null does nothing private void addProperty(String uri, String value, List list) { if (value != null) { Property prop = new Property(); prop.setUri(uri); prop.setValue(value); list.add(prop); } } private String getUri(String path) { return "vos://" + authority + path; } private NodePaths getPathsFromResultSet(ResultSet rs) throws SQLException { NodePaths paths = new NodePaths(rs.getString("path"), rs.getString("relative_path")); return paths; } private Array fromPropertyToArray(PreparedStatement ps, String myProperty) throws SQLException { if (myProperty == null || myProperty.isBlank()) { return null; } else { return ps.getConnection().createArrayOf("varchar", myProperty.split(" ")); } } private Array fromViewsToArray(PreparedStatement ps, Node node, Function> viewsExtractor) throws SQLException { if (node instanceof DataNode) { DataNode dataNode = (DataNode) node; List views = viewsExtractor.apply(dataNode); if (views != null && !views.isEmpty()) { Object[] array = views.stream().map(v -> v.getUri()).toArray(); return ps.getConnection().createArrayOf("varchar", array); } } return null; } private List getViews(Array array) { if (array == null) { return null; } try { return Arrays.stream((String[]) array.getArray()) .map(uri -> { View view = new View(); view.setUri(uri); return view; }) .collect(Collectors.toList()); } catch (SQLException ex) { throw new RuntimeException(ex); } } /* Map contains: Key column column name value column name value and value is a String containing comma separated groups having permissions */ private Map> getPermissionsFromDB(String vosPath) { String sql = "SELECT group_read, group_write " + "FROM node n WHERE node_id = id_from_vos_path(?)"; return jdbcTemplate.query(sql, new Object[]{vosPath}, new int[]{Types.VARCHAR}, rs -> { if (!rs.next()) { throw new InternalFaultException("No records found for " + vosPath); } return Map.of( "group_read", getArrayValue(rs, "group_read"), "group_write", getArrayValue(rs, "group_write") ); }); } private List getArrayValue(ResultSet rs, String key) throws SQLException { Array array = rs.getArray(key); if (array == null) { return new ArrayList<>(); } return Arrays.asList((String[]) array.getArray()); } private void updatePermissionsRecursively(Node newNode, String vosPath) { Map> permissions = getPermissionsFromDB(vosPath); List existingGroupReadList = permissions.get("group_read"); List existingGroupWriteList = permissions.get("group_write"); List newGroupReadList = NodeProperties.getNodePropertyAsListByURI(newNode, NodeProperties.GROUP_READ_URI); List newGroupWriteList = NodeProperties.getNodePropertyAsListByURI(newNode, NodeProperties.GROUP_WRITE_URI); Set existingGroupRead = new HashSet<>(existingGroupReadList); Set existingGroupWrite = new HashSet<>(existingGroupWriteList); Set newGroupRead = new HashSet<>(newGroupReadList); Set newGroupWrite = new HashSet<>(newGroupWriteList); Set groupReadToAdd = differenceBetweenSets(newGroupRead, existingGroupRead); Set groupReadToRemove = differenceBetweenSets(existingGroupRead, newGroupRead); Set groupWriteToAdd = differenceBetweenSets(newGroupWrite, existingGroupWrite); Set groupWriteToRemove = differenceBetweenSets(existingGroupWrite, newGroupWrite); String sql = "UPDATE node SET " + "group_read = update_array(group_read, ?, ?), " + "group_write = update_array(group_write, ?, ?), " + "is_public = ? " + "WHERE path <@ (SELECT path FROM node n " + "JOIN node_vos_path p ON n.node_id = p.node_id " + "AND p.vos_path = ?)"; jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sql); int i = 0; ps.setArray(++i, ps.getConnection().createArrayOf("varchar", groupReadToAdd.toArray())); ps.setArray(++i, ps.getConnection().createArrayOf("varchar", groupReadToRemove.toArray())); ps.setArray(++i, ps.getConnection().createArrayOf("varchar", groupWriteToAdd.toArray())); ps.setArray(++i, ps.getConnection().createArrayOf("varchar", groupWriteToRemove.toArray())); ps.setBoolean(++i, Boolean.valueOf(NodeProperties.getNodePropertyByURI(newNode, NodeProperties.PUBLIC_READ_URI))); ps.setString(++i, vosPath); return ps; }); } // Returns the difference a minus b private Set differenceBetweenSets(Set a, Set b) { Set diff = new HashSet<>(a); diff.removeAll(b); return diff; } private List getNodePathsFromDB(String nodeURI) { String path = nodeURI.replaceAll("vos://[^/]+", ""); String parentPath = NodeUtils.getParentPath(path); String sql = "SELECT path, relative_path " + "FROM node n " + "WHERE node_id = id_from_vos_path(?)"; List paths = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, parentPath); return ps; }, (row, index) -> { return getPathsFromResultSet(row); }); return paths; } private class NodePaths { private final String path; private final String relativePath; public NodePaths(String myPath, String myRelativePath) { this.path = myPath; this.relativePath = myRelativePath; } @Override public String toString() { return relativePath + " " + path; } public String getPath() { return this.path; } public String getRelativePath() { return this.relativePath; } } }