package it.inaf.oats.vospace.persistence; 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.List; import java.util.Optional; 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.StructuredDataNode; import net.ivoa.xml.vospace.v2.View; 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; /** * * @author bertocco */ @Repository public class NodeDAO { @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 = getParentPath(path); String sql = "SELECT path, relative_path from " + "node n join node_vos_path p on n.node_id = p.node_id " + "where p.vos_path = ?"; List paths = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, parentPath); return ps; }, (row, index) -> { return getPathsFromResultSet(row); }); 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, owner_id, 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, getNodeName(myNode)); ps.setBoolean(++i, getIsBusy(myNode)); ps.setString(++i, getProperty(myNode, getPropertyURI("creator"))); ps.setString(++i, getProperty(myNode, getPropertyURI("creator"))); ps.setArray(++i, fromPropertyToArray(ps, getProperty(myNode, getPropertyURI("groupread")))); ps.setArray(++i, fromPropertyToArray(ps, getProperty(myNode, getPropertyURI("groupwrite")))); ps.setBoolean(++i, Boolean.valueOf(getProperty(myNode, getPropertyURI("publicread")))); ps.setObject(++i, paths.get(0).path, Types.OTHER); ps.setObject(++i, paths.get(0).relativePath, Types.OTHER); ps.setObject(++i, 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 os.vos_path, n.node_id, type, async_trans, 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" + "WHERE n.path ~ (" + getFirstLevelChildrenSelector(path) + ")::lquery\n" + "OR os.vos_path = ? ORDER BY vos_path"; List parentAndChildren = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, path); ps.setString(2, 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); } private String getFirstLevelChildrenSelector(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 = 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<>(); addProperty(getPropertyURI("length"), rs.getString("content_length"), properties); addProperty(getPropertyURI("btime"), rs.getString("created_on"), properties); addProperty(getPropertyURI("creator"), rs.getString("creator_id"), properties); addProperty(getPropertyURI("mtime"), rs.getString("last_modified"), properties); addProperty(getPropertyURI("groupread"), getGroupsString(rs, "group_read"), properties); addProperty(getPropertyURI("groupwrite"), getGroupsString(rs, "group_write"), properties); addProperty(getPropertyURI("publicread"), rs.getString("is_public"), properties); addProperty("urn:async_trans", rs.getString("async_trans"), properties); node.setProperties(properties); return node; } private String getPropertyURI(String propertyName) { return "ivo://ivoa.net/vospace/core#".concat(propertyName); } 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 Node getTypedNode(String type) { Node node; switch (type) { case "container": node = new ContainerNode(); break; case "data": node = new DataNode(); break; case "structured": node = new StructuredDataNode(); break; default: throw new UnsupportedOperationException("Node type " + type + " not supported yet"); } return node; } 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 String getNodeName(String path) { String[] parsedPath = path.split("/"); return parsedPath[parsedPath.length - 1]; } private String getNodeName(Node myNode) { String uri = myNode.getUri(); return getNodeName(uri); } private boolean getIsBusy(Node myNode) { if (myNode instanceof DataNode) { DataNode dataNode = (DataNode) myNode; return dataNode.isBusy(); } return false; } // Copied from CreateNodeController: to be moved in a common utility class private String getParentPath(String path) { String[] parsedPath = path.split("[/]+"); if (parsedPath.length < 2 || !parsedPath[0].isEmpty()) { throw new IllegalArgumentException(); } StringBuilder sb = new StringBuilder(); sb.append("/"); for (int i = 1; i < parsedPath.length - 1; i++) { sb.append(parsedPath[i]); if (i < parsedPath.length - 2) { sb.append("/"); } } return sb.toString(); } private String getProperty(Node node, String uri) { for (Property property : node.getProperties()) { if (uri.equals(property.getUri())) { return property.getValue(); } } return null; } 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) throws SQLException { if (array == null) { return null; } return Arrays.stream((String[]) array.getArray()) .map(uri -> { View view = new View(); view.setUri(uri); return view; }) .collect(Collectors.toList()); } private String getDbNodeType(Node node) { if (node instanceof ContainerNode) { return "container"; } else if (node instanceof DataNode) { return "data"; } throw new UnsupportedOperationException("Unable to retrieve database node type for class " + node.getClass().getCanonicalName()); } 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; } } }