Skip to content
NodeDAO.java 30.8 KiB
Newer Older
Sonia Zorba's avatar
Sonia Zorba committed
/*
 * This file is part of vospace-rest
 * Copyright (C) 2021 Istituto Nazionale di Astrofisica
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
Sara Bertocco's avatar
Sara Bertocco committed
package it.inaf.oats.vospace.persistence;

Sara Bertocco's avatar
Sara Bertocco committed
import it.inaf.oats.vospace.DeleteNodeController;
import it.inaf.oats.vospace.URIUtils;
Sara Bertocco's avatar
Sara Bertocco committed
import it.inaf.oats.vospace.datamodel.NodeProperties;
import it.inaf.oats.vospace.datamodel.NodeUtils;
import it.inaf.oats.vospace.exception.InternalFaultException;
Sara Bertocco's avatar
Sara Bertocco committed
import net.ivoa.xml.vospace.v2.Node;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
Sara Bertocco's avatar
Sara Bertocco committed
import java.util.HashSet;
Sara Bertocco's avatar
Sara Bertocco committed
import java.util.List;
Sara Bertocco's avatar
Sara Bertocco committed
import java.util.Map;
import java.util.Optional;
Sara Bertocco's avatar
Sara Bertocco committed
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
Sara Bertocco's avatar
Sara Bertocco committed
import javax.sql.DataSource;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.DataNode;
import net.ivoa.xml.vospace.v2.LinkNode;
import net.ivoa.xml.vospace.v2.Property;
import net.ivoa.xml.vospace.v2.View;
Sara Bertocco's avatar
Sara Bertocco committed
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Sara Bertocco's avatar
Sara Bertocco committed
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
Sara Bertocco's avatar
Sara Bertocco committed
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class NodeDAO {

Sara Bertocco's avatar
Sara Bertocco committed
    private static final Logger LOG = LoggerFactory.getLogger(DeleteNodeController.class);
    @Value("${vospace-authority}")
    private String authority;
Sara Bertocco's avatar
Sara Bertocco committed

    private final JdbcTemplate jdbcTemplate;
Sara Bertocco's avatar
Sara Bertocco committed
    @Autowired
    public NodeDAO(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public void createNode(Node node) {
        createNode(node, null);
    }

    public void createNode(Node myNode, String jobId) {
Sonia Zorba's avatar
Sonia Zorba committed

        String nodeVosPath = URIUtils.returnVosPathFromNodeURI(myNode, authority);
Sonia Zorba's avatar
Sonia Zorba committed

        List<NodePaths> paths = getNodePathsFromDB(nodeVosPath);
        if (paths.isEmpty()) {
            throw new IllegalStateException("Unable to find parent node during node creation");
Sara Bertocco's avatar
Sara Bertocco committed
        StringBuilder sb = new StringBuilder();
        sb.append(" (name, job_id, creator_id, group_read, group_write,");
        sb.append(" is_public, parent_path, parent_relative_path, type, accept_views, provide_views, target)");
        sb.append(" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
Sonia Zorba's avatar
Sonia Zorba committed

        jdbcTemplate.update(conn -> {
            PreparedStatement ps = conn.prepareStatement(sb.toString());
            ps.setString(++i, NodeUtils.getNodeName(nodeVosPath));
            if (jobId == null) {
                ps.setNull(++i, Types.VARCHAR);
            } else {
                ps.setString(++i, jobId);
            }
Sonia Zorba's avatar
Sonia Zorba committed
            ps.setString(++i, NodeProperties.getNodePropertyByURI(myNode, NodeProperties.CREATOR_URI));
            ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getNodePropertyByURI(myNode, NodeProperties.GROUP_READ_URI)));
            ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getNodePropertyByURI(myNode, NodeProperties.GROUP_WRITE_URI)));
            ps.setBoolean(++i, Boolean.valueOf(NodeProperties.getNodePropertyByURI(myNode, NodeProperties.PUBLIC_READ_URI)));
Sonia Zorba's avatar
Sonia Zorba committed
            ps.setObject(++i, paths.get(0).getPath(), Types.OTHER);
            ps.setObject(++i, paths.get(0).getRelativePath(), Types.OTHER);
Sara Bertocco's avatar
Sara Bertocco committed
            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);
            if (myNode instanceof LinkNode) {
                ps.setString(++i, ((LinkNode) myNode).getTarget());
            } else {
                ps.setNull(++i, Types.VARCHAR);
            }
Sara Bertocco's avatar
Sara Bertocco committed
    }

    public Optional<Node> listNode(String path) {
Sara Bertocco's avatar
Sara Bertocco committed

        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.job_id IS NOT NULL AS 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, c.quota, c.content_md5, c.target\n"
                + "JOIN node c ON c.parent_path = n.path OR c.path = n.path\n"
                + "WHERE n.node_id = id_from_vos_path(?)\n"
                + "ORDER BY vos_path";
Sara Bertocco's avatar
Sara Bertocco committed

        List<Node> parentAndChildren = jdbcTemplate.query(conn -> {
Sara Bertocco's avatar
Sara Bertocco committed
            PreparedStatement ps = conn.prepareStatement(sql);
            int i = 0;
            ps.setString(++i, path);
            ps.setString(++i, path);
            ps.setString(++i, "/".equals(path) ? "" : "/");
            ps.setString(++i, path);
Sara Bertocco's avatar
Sara Bertocco committed
            return ps;
        }, (row, index) -> {
            return getNodeFromResultSet(row);
Sara Bertocco's avatar
Sara Bertocco committed
        });

        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 List<String> listNodeChildren(String path) {

        String sql = "SELECT n.name\n"
                + "FROM node n\n"
                + "WHERE n.path ~ ('*.' || id_from_vos_path(?) || '.*{1}')::lquery\n"
                + "ORDER BY n.path";

        List<String> childrenNames = jdbcTemplate.query(conn -> {
            PreparedStatement ps = conn.prepareStatement(sql);
            int i = 0;
            ps.setString(++i, path);
            return ps;
        }, (row, index) -> {
            return row.getString("name");
        });
    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 = URIUtils.returnVosPathFromNodeURI(newNode, authority);
        boolean isLinkNode = newNode instanceof LinkNode;

        // If is link ignore recursive: LinkNodes are supposed to be childless
        if (recursive && !isLinkNode) {
Sara Bertocco's avatar
Sara Bertocco committed
            updatePermissionsRecursively(newNode, vosPath);
Sonia Zorba's avatar
Sonia Zorba committed
        } else {
            jdbcTemplate.update(conn -> {
Sonia Zorba's avatar
Sonia Zorba committed

                String sql = "UPDATE node\n"
                        + "SET group_read = ?, group_write = ?, is_public = ?\n";

                if (isLinkNode) {
                    sql += ", target = ?\n";
                }
                sql += "WHERE node_id = id_from_vos_path(?)\n";
Sonia Zorba's avatar
Sonia Zorba committed

                PreparedStatement ps = conn.prepareStatement(sql);
Sonia Zorba's avatar
Sonia Zorba committed
                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)));
                if (isLinkNode) {
                    ps.setString(++i, ((LinkNode) newNode).getTarget() );
                }
Sonia Zorba's avatar
Sonia Zorba committed
                ps.setString(++i, vosPath);
                return ps;
            });
        }

Sara Bertocco's avatar
Sara Bertocco committed
    }

    private Node getNodeFromResultSet(ResultSet rs) throws SQLException {

Sara Bertocco's avatar
Sara Bertocco committed
        Node node = NodeUtils.getTypedNode(rs.getString("type"));

            DataNode dataNode = (DataNode) node;

            dataNode.setBusy(rs.getBoolean("busy_state"));
            dataNode.setAccepts(getViews(rs.getArray("accept_views")));
            dataNode.setProvides(getViews(rs.getArray("provide_views")));
        } else if (node instanceof LinkNode) {
            LinkNode linkNode = (LinkNode) node;
            linkNode.setTarget(rs.getString("target"));
        node.setUri(getUri(rs.getString("vos_path")));
        List<Property> 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
Sara Bertocco's avatar
Sara Bertocco committed
        addProperty(NodeProperties.CREATOR_URI, rs.getString("creator_id"),
Sonia Zorba's avatar
Sonia Zorba committed
                properties);

Sara Bertocco's avatar
Sara Bertocco committed
        addProperty(NodeProperties.MODIFICATION_TIME_URI, rs.getString("last_modified"),
Sara Bertocco's avatar
Sara Bertocco committed
        addProperty(NodeProperties.GROUP_READ_URI, getGroupsString(rs, "group_read"),
Sara Bertocco's avatar
Sara Bertocco committed
        addProperty(NodeProperties.GROUP_WRITE_URI, getGroupsString(rs, "group_write"),
Sara Bertocco's avatar
Sara Bertocco committed
        addProperty(NodeProperties.PUBLIC_READ_URI, String.valueOf(rs.getBoolean("is_public")),
Sonia Zorba's avatar
Sonia Zorba committed
        addProperty(NodeProperties.QUOTA_URI, rs.getString("quota"), properties);
Sonia Zorba's avatar
Sonia Zorba committed
        addProperty(NodeProperties.MD5_URI, rs.getString("content_md5"), properties);
Sonia Zorba's avatar
Sonia Zorba committed
        addProperty("urn:async_trans", String.valueOf(rs.getBoolean("async_trans")),
Sonia Zorba's avatar
Sonia Zorba committed
        addProperty("urn:sticky", String.valueOf(rs.getBoolean("sticky")), properties);

    public Optional<Long> getNodeId(String nodeVosPath) {
        String sql = "SELECT id_from_vos_path(?) AS node_id";
        Long nodeId = jdbcTemplate.queryForObject(sql, Long.class, nodeVosPath);
        return Optional.ofNullable(nodeId);
Sonia Zorba's avatar
Sonia Zorba committed
    }
    public Optional<ShortNodeDescriptor> getShortNodeDescriptor(String nodeVosPath,
            String userId, List<String> userGroups) {
        String sql = "SELECT path,\n"
                + "NOT (n.async_trans OR COALESCE(location_type = 'async', FALSE)) AS is_writable,\n"
                + "n.sticky AS is_sticky,\n"
                + "((SELECT COUNT(*) FROM (SELECT UNNEST(?) INTERSECT SELECT UNNEST(n.group_write)) AS allowed_groups ) = 0 AND\n"
                + "n.creator_id <> ?) AS is_permission_denied,\n"
                + "n.type = 'container' AS is_container,\n"
                + "n.job_id IS NOT NULL AS busy_state\n"
                + "FROM node n \n"
                + "LEFT JOIN location loc ON loc.location_id = n.location_id\n"
                + "WHERE n.node_id = id_from_vos_path(?)\n";

        Optional<ShortNodeDescriptor> sndOpt = jdbcTemplate.query(conn -> {
            PreparedStatement ps = conn.prepareStatement(sql);
            if (userGroups == null) {
                groups = new String[0];
            } else {
                groups = userGroups.toArray(String[]::new);
            }
            ps.setArray(1, ps.getConnection().createArrayOf("varchar", groups));
            ps.setString(2, userId);
            ps.setString(3, nodeVosPath);
            return ps;
        }, rs -> {
            if (!rs.next()) {
                return Optional.empty();
            }

            String nodePath = rs.getString("path");
            Boolean isContainer = rs.getBoolean(("is_container"));
            Boolean isWritable = rs.getBoolean("is_writable");
            Boolean isBusy = rs.getBoolean("busy_state");
            Boolean isPermissionDenied = rs.getBoolean("is_permission_denied");
            Boolean isSticky = rs.getBoolean("is_sticky");
            ShortNodeDescriptor result = new ShortNodeDescriptor(nodePath, isContainer, isWritable, isBusy, isPermissionDenied, isSticky);
    public void renameNode(Long nodeId, String name) {
        String sql = "UPDATE node SET name = ?, os_name = COALESCE(os_name, name) "
                + "WHERE node_id = ?";

        jdbcTemplate.update(conn -> {
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setString(1, name);
            ps.setLong(2, nodeId);
            return ps;
        });

    }
Sonia Zorba's avatar
Sonia Zorba committed
    public void moveNodeBranch(Long sourceRootId, String destParentLtreePath) {

        String sql = "UPDATE node c SET "
                + "parent_path = (? || SUBPATH(c.path, (SELECT nlevel(parent_path) FROM node WHERE node_id = ?), -1))::ltree, "
Sonia Zorba's avatar
Sonia Zorba committed
                + "parent_relative_path = COALESCE(c.parent_relative_path, c.parent_path) " // not sure about this
                + "FROM node n "
                + "WHERE n.path @> c.path AND n.node_id = ?";

        jdbcTemplate.update(conn -> {
            PreparedStatement ps = conn.prepareStatement(sql);
Sonia Zorba's avatar
Sonia Zorba committed
            ps.setString(1, destParentLtreePath);
            ps.setLong(2, sourceRootId);
Sonia Zorba's avatar
Sonia Zorba committed
            ps.setLong(3, sourceRootId);
Sonia Zorba's avatar
Sonia Zorba committed
    }
    public void copyBranch(String sourceVosPath, String destVosPath) {
Sonia Zorba's avatar
Sonia Zorba committed

        String destVosParentPath = NodeUtils.getParentPath(destVosPath);
        String destName = NodeUtils.getNodeName(destVosPath);

        String parentInsert = "INSERT INTO node (node_id, parent_path, parent_relative_path, name, type, location_id, creator_id, group_write, group_read, is_public,\n"
                + "job_id, tstamp_wrapper_dir, format, async_trans, sticky, accept_views, provide_views, protocols, target)\n";
        // If destination has path '' no prefix, else "destination_path."
        String ctePathPrefix = "SELECT CASE WHEN path::varchar = '' THEN '' ELSE (path::varchar || '.') END AS prefix\n"
                + "FROM node WHERE node_id = id_from_vos_path(?)";

        // Calculates also new path, even if it's usually generated by database functions
        String cteCopiedNodes = "SELECT nextval('node_node_id_seq') AS new_node_id,\n"
                + "((SELECT prefix FROM path_prefix) || currval('node_node_id_seq'))::ltree AS new_path,\n"
                + "path, relative_path, parent_path, parent_relative_path, ? AS name,\n"
                + "type, location_id, creator_id, group_write, group_read, is_public,\n"
                + "job_id, tstamp_wrapper_dir, format, async_trans, sticky, accept_views, provide_views, protocols, target\n"
                + "FROM node WHERE node_id = id_from_vos_path(?)\n"
                + "UNION ALL\n"
                + "SELECT nextval('node_node_id_seq') AS new_node_id,\n"
                + "(p.new_path::varchar || '.' || currval('node_node_id_seq'))::ltree,\n"
                + "n.path, n.relative_path, n.parent_path, n.parent_relative_path, n.name,\n"
                + "n.type, n.location_id, n.creator_id, n.group_write, n.group_read, n.is_public,\n"
                + "n.job_id, n.tstamp_wrapper_dir, n.format, n.async_trans, n.sticky, n.accept_views, n.provide_views, n.protocols, n.target\n"
                + "FROM node n\n"
                + "JOIN copied_nodes p ON p.path = n.parent_path";

        String cteCopiedNodesPaths = "SELECT subpath(new_path, 0, nlevel(new_path) - 1) AS new_parent_path,\n"
                + "nlevel(parent_path) - nlevel(parent_relative_path) AS rel_offset, * FROM copied_nodes";

        String parentSelect = "SELECT\n"
                + "new_node_id, new_parent_path,\n"
                + "CASE WHEN nlevel(new_parent_path) <= rel_offset THEN ''::ltree ELSE subpath(new_parent_path, rel_offset) END new_parent_relative_path,\n"
                + "name, type, location_id, creator_id, group_write, group_read, is_public,\n"
                + "job_id, tstamp_wrapper_dir, format, async_trans, sticky, accept_views, provide_views, protocols, target\n"

        String sql = parentInsert
                + "WITH RECURSIVE path_prefix AS ("
                + ctePathPrefix + "),\n"
                + "copied_nodes AS ("
                + cteCopiedNodes + "),\n"
                + "copied_nodes_paths AS ("
                + cteCopiedNodesPaths + ")\n"
        jdbcTemplate.update(conn -> {
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setString(1, destVosParentPath);
            ps.setString(2, destName);
            ps.setString(3, sourceVosPath);
Sonia Zorba's avatar
Sonia Zorba committed
    public boolean isBranchBusy(long parentNodeId) {

        String sql = "SELECT COUNT(c.node_id) > 0 "
                + "FROM node n "
                + "JOIN node c ON c.path <@ n.path "
                + "WHERE n.node_id = ? AND c.job_id IS NOT NULL";
Sonia Zorba's avatar
Sonia Zorba committed

        return jdbcTemplate.queryForObject(sql, new Object[]{parentNodeId}, new int[]{Types.BIGINT}, Boolean.class);
    public void setBranchJobId(Long rootNodeId, String jobId) {
        String sql = "UPDATE node c SET job_id = ? "
                + "FROM node r "
                + "WHERE r.node_id = ? "
                + "AND r.path @> c.path";

        jdbcTemplate.update(conn -> {
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setString(1, jobId);
            ps.setLong(2, rootNodeId);
            return ps;
        });
    }
Sonia Zorba's avatar
Sonia Zorba committed

    public void releaseBusyNodesByJobId(String jobId) {
        String sql = "UPDATE node SET job_id = NULL WHERE job_id = ?";
Sonia Zorba's avatar
Sonia Zorba committed

        jdbcTemplate.update(conn -> {
            PreparedStatement ps = conn.prepareStatement(sql);
Sonia Zorba's avatar
Sonia Zorba committed
            ps.setString(1, jobId);
Sonia Zorba's avatar
Sonia Zorba committed
        });
Sonia Zorba's avatar
Sonia Zorba committed
    public boolean isBranchWritable(long parentNodeId, String userId, List<String> userGroups) {

        String sql = "SELECT COUNT(c.node_id) = 0 "
                + "FROM node n "
                + "JOIN node c ON c.path <@ n.path "
                + "LEFT JOIN location loc ON c.location_id = loc.location_id "
                + "WHERE n.node_id = ? "
                + "AND (c.async_trans OR c.sticky OR location_type = 'async' OR "
                + "((SELECT COUNT(*) FROM (SELECT UNNEST(?) INTERSECT SELECT UNNEST(c.group_write)) AS allowed_groups) = 0 "
                + "AND c.creator_id <> ?))";

        return jdbcTemplate.query(sql, ps -> {
            ps.setLong(1, parentNodeId);
            if (userGroups == null) {
                groups = new String[0];
            } else {
                groups = userGroups.toArray(String[]::new);
            }
            ps.setArray(2, ps.getConnection().createArrayOf("varchar", groups));

            ps.setString(3, userId);
        }, row -> {
            if (!row.next()) {
                throw new IllegalStateException("Expected one result");
            }
            return row.getBoolean(1);
        });
    }

    public boolean isBranchReadable(long parentNodeId, String userId, List<String> userGroups) {

        String sql = "SELECT COUNT(c.node_id) = 0 "
                + "FROM node n "
                + "JOIN node c ON c.path <@ n.path "
Nicola Fulvio Calabria's avatar
Nicola Fulvio Calabria committed
                + "WHERE n.node_id = ? AND "
                + "NOT COALESCE(c.is_public, FALSE) "
                + "AND (SELECT COUNT(*) FROM (SELECT UNNEST(?) INTERSECT SELECT UNNEST(c.group_read)) AS allowed_groups) = 0 "
                + "AND c.creator_id <> ?";

        return jdbcTemplate.query(sql, ps -> {
            ps.setLong(1, parentNodeId);

            String[] groups;
            if (userGroups == null) {
                groups = new String[0];
            } else {
                groups = userGroups.toArray(String[]::new);
            ps.setArray(2, ps.getConnection().createArrayOf("varchar", groups));
Sonia Zorba's avatar
Sonia Zorba committed
            ps.setString(3, userId);
        }, row -> {
            if (!row.next()) {
                throw new IllegalStateException("Expected one result");
            }
            return row.getBoolean(1);
        });
    }
        int nodesWithPath = countNodesWithPath(path);
            throw new IllegalStateException("Node at path "
            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, job_id, creator_id, group_read, "
                + "group_write, is_public, quota, content_type, content_encoding, "
                + "content_length, content_md5, created_on, last_modified, "
                + "accept_views, provide_views, protocols, target, sticky)\n";
        String deleteSql = "DELETE \n"
                + "FROM node n\n"
Sonia Zorba's avatar
Sonia Zorba committed
                + "USING node p\n"
                + "WHERE n.path <@ p.path AND p.node_id = id_from_vos_path(?)\n"
                + "RETURNING\n"
                + "n.node_id, n.parent_path, n.parent_relative_path, "
                + "n.name, n.os_name, n.tstamp_wrapper_dir, n.type, n.location_id, n.format, "
                + "n.async_trans, n.job_id, n.creator_id, n.group_read, "
                + "n.group_write, n.is_public, n.quota, n.content_type, n.content_encoding, "
Sonia Zorba's avatar
Sonia Zorba committed
                + "n.content_length, n.content_md5, n.created_on, n.last_modified, "
                + "n.accept_views, n.provide_views, n.protocols, n.target, n.sticky\n";
Sonia Zorba's avatar
Sonia Zorba committed

        String withSql = "WITH del AS (" + deleteSql + ")";

        String sql = withSql + insertSql + "SELECT * FROM del\n";

Sonia Zorba's avatar
Sonia Zorba committed
        jdbcTemplate.update(sql, path);
    public int countNodesWithPath(String path) {
Sonia Zorba's avatar
Sonia Zorba committed
        String sql = "SELECT COUNT(*) FROM node WHERE node_id = id_from_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"
Sonia Zorba's avatar
Sonia Zorba committed
                + "COALESCE(os_name, name) AS os_name\n"
                + "FROM node n\n"
Sonia Zorba's avatar
Sonia Zorba committed
                + "WHERE node_id = id_from_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) {

Sonia Zorba's avatar
Sonia Zorba committed
        String sql = "UPDATE node SET location_id = ?, os_name = ? WHERE node_id = id_from_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<Property> list) {
        if (value != null) {
            Property prop = new Property();
            prop.setUri(uri);
            prop.setValue(value);
            list.add(prop);
        }
    }

    private String getUri(String path) {
        return URIUtils.returnURIFromVosPath(path, authority);

    private NodePaths getPathsFromResultSet(ResultSet rs) throws SQLException {
        NodePaths paths = new NodePaths(rs.getString("path"), rs.getString("relative_path"));
Sonia Zorba's avatar
Sonia Zorba committed
        return paths;
    private Array fromPropertyToArray(PreparedStatement ps, String myProperty) throws SQLException {
        if (myProperty == null || myProperty.isBlank()) {
        } else {
            return ps.getConnection().createArrayOf("varchar", myProperty.split(" "));
Sonia Zorba's avatar
Sonia Zorba committed
        }
    private Array fromViewsToArray(PreparedStatement ps, Node node, Function<DataNode, List<View>> viewsExtractor) throws SQLException {
        if (node instanceof DataNode) {
            DataNode dataNode = (DataNode) node;
            List<View> 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;
    }

Sara Bertocco's avatar
Sara Bertocco committed
    private List<View> getViews(Array array) {
        if (array == null) {
            return null;
        }
Sara Bertocco's avatar
Sara Bertocco committed
        try {
            return Arrays.stream((String[]) array.getArray())
Sonia Zorba's avatar
Sonia Zorba committed
                    .map(uri -> {
                        View view = new View();
                        view.setUri(uri);
                        return view;
                    })
                    .collect(Collectors.toList());
Sara Bertocco's avatar
Sara Bertocco committed
        } catch (SQLException ex) {
            throw new RuntimeException(ex);
        }
Sara Bertocco's avatar
Sara Bertocco committed
    }
Sara Bertocco's avatar
Sara Bertocco committed
    /*
    Map contains:
    Key             column
    column name     value
    column name     value
    and value is a String containing comma separated groups having permissions
Sonia Zorba's avatar
Sonia Zorba committed
     */
    private Map<String, List<String>> getPermissionsFromDB(String vosPath) {

Sara Bertocco's avatar
Sara Bertocco committed
        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 -> {
Sonia Zorba's avatar
Sonia Zorba committed
            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<String> getArrayValue(ResultSet rs, String key) throws SQLException {
        Array array = rs.getArray(key);
        if (array == null) {
            return new ArrayList<>();
        }
        return Arrays.asList((String[]) array.getArray());
Sara Bertocco's avatar
Sara Bertocco committed
    private void updatePermissionsRecursively(Node newNode, String vosPath) {
Sonia Zorba's avatar
Sonia Zorba committed

        Map<String, List<String>> permissions = getPermissionsFromDB(vosPath);

        List<String> existingGroupReadList = permissions.get("group_read");
        List<String> existingGroupWriteList = permissions.get("group_write");

Sara Bertocco's avatar
Sara Bertocco committed
        List<String> newGroupReadList = NodeProperties.getNodePropertyAsListByURI(newNode, NodeProperties.GROUP_READ_URI);
        List<String> newGroupWriteList = NodeProperties.getNodePropertyAsListByURI(newNode, NodeProperties.GROUP_WRITE_URI);
Sara Bertocco's avatar
Sara Bertocco committed
        Set<String> existingGroupRead = new HashSet<>(existingGroupReadList);
        Set<String> existingGroupWrite = new HashSet<>(existingGroupWriteList);
Sara Bertocco's avatar
Sara Bertocco committed
        Set<String> newGroupRead = new HashSet<>(newGroupReadList);
        Set<String> newGroupWrite = new HashSet<>(newGroupWriteList);
Sonia Zorba's avatar
Sonia Zorba committed

        Set<String> groupReadToAdd = differenceBetweenSets(newGroupRead, existingGroupRead);
        Set<String> groupReadToRemove = differenceBetweenSets(existingGroupRead, newGroupRead);

        Set<String> groupWriteToAdd = differenceBetweenSets(newGroupWrite, existingGroupWrite);
        Set<String> groupWriteToRemove = differenceBetweenSets(existingGroupWrite, newGroupWrite);

Sonia Zorba's avatar
Sonia Zorba committed
        String sql = "UPDATE node c SET "
                + "group_read = update_array(c.group_read, ?, ?), "
                + "group_write = update_array(c.group_write, ?, ?), "
Sonia Zorba's avatar
Sonia Zorba committed
                + "is_public = ? "
Sonia Zorba's avatar
Sonia Zorba committed
                + "FROM node n "
                + "WHERE n.node_id = id_from_vos_path(?) AND c.path <@ n.path";
Sara Bertocco's avatar
Sara Bertocco committed

        jdbcTemplate.update(conn -> {
            PreparedStatement ps = conn.prepareStatement(sql);
            int i = 0;
Sonia Zorba's avatar
Sonia Zorba committed
            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()));
Sara Bertocco's avatar
Sara Bertocco committed
            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<String> differenceBetweenSets(Set<String> a, Set<String> b) {
Sara Bertocco's avatar
Sara Bertocco committed
        Set<String> diff = new HashSet<>(a);
        diff.removeAll(b);
Sara Bertocco's avatar
Sara Bertocco committed
        return diff;
    }
    private List<NodePaths> getNodePathsFromDB(String path) {
Sara Bertocco's avatar
Sara Bertocco committed
        String parentPath = NodeUtils.getParentPath(path);

        String sql = "SELECT path, relative_path "
                + "FROM node n "
                + "WHERE node_id = id_from_vos_path(?)";
Sara Bertocco's avatar
Sara Bertocco committed

        List<NodePaths> paths = jdbcTemplate.query(conn -> {
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setString(1, parentPath);
            return ps;
        }, (row, index) -> {
            return getPathsFromResultSet(row);
        });
Sara Bertocco's avatar
Sara Bertocco committed
        return paths;
    public class ShortNodeDescriptor {

        private final String nodeLtreePath;

        private final boolean container;
        private final boolean writable;
        private final boolean busy;
        private final boolean permissionDenied;
        private final boolean sticky;
        public ShortNodeDescriptor(String nodeLtreePath, boolean container, boolean writable, boolean busy, boolean permissionDenied, boolean sticky) {
            this.nodeLtreePath = nodeLtreePath;
            this.container = container;
            this.writable = writable;
            this.busy = busy;
            this.permissionDenied = permissionDenied;
        }

        public String getDestinationNodeLtreePath() {
            return nodeLtreePath;
        }

        public boolean isContainer() {
            return container;
        }

        public boolean isWritable() {
            return writable;
        }

        public boolean isBusy() {
            return busy;
        }
        public boolean isPermissionDenied() {
            return permissionDenied;
        }
Sonia Zorba's avatar
Sonia Zorba committed

        public boolean isSticky() {
            return sticky;
        }
        private final String path;
        private final String relativePath;
        public NodePaths(String myPath, String myRelativePath) {
            this.relativePath = myRelativePath;
Sara Bertocco's avatar
Sara Bertocco committed
        public String getPath() {
            return this.path;
Sara Bertocco's avatar
Sara Bertocco committed
        public String getRelativePath() {
            return this.relativePath;
Sara Bertocco's avatar
Sara Bertocco committed
}