Skip to content
Snippets Groups Projects
Commit 9ed68210 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Implemented basic node listing feature

parent 044b1b76
No related branches found
No related tags found
No related merge requests found
target/** target/**
nbactions.xml nbactions.xml
nb-configuration.xml
/target/
<?xml version="1.0" encoding="UTF-8"?>
<project-shared-configuration>
<!--
This file contains additional configuration written by modules in the NetBeans IDE.
The configuration is intended to be shared among all the users of project and
therefore it is assumed to be part of version control checkout.
Without this configuration present, some functionality in the IDE may be limited or fail altogether.
-->
<properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
<!--
Properties that influence various parts of the IDE, especially code formatting and the like.
You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up.
That way multiple projects can share the same settings (useful for formatting rules for example).
Any value defined here will override the pom.xml file value but is only applicable to the current project.
-->
<netbeans.hint.jdkPlatform>JDK_15</netbeans.hint.jdkPlatform>
</properties>
</project-shared-configuration>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
<artifactId>vospace-oats</artifactId> <artifactId>vospace-oats</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<name>vospace-oats</name> <name>vospace-oats</name>
<description>Demo project for Spring Boot</description> <description>VOSpace REST service</description>
<properties> <properties>
<java.version>11</java.version> <java.version>11</java.version>
...@@ -36,28 +36,12 @@ ...@@ -36,28 +36,12 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!-- JAXB dependency -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
<!-- Jackson-JAXB compatibility --> <!-- Jackson-JAXB compatibility -->
<dependency> <dependency>
<groupId>com.fasterxml.jackson.module</groupId> <groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jaxb-annotations</artifactId> <artifactId>jackson-module-jaxb-annotations</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId> <artifactId>spring-boot-devtools</artifactId>
...@@ -81,6 +65,14 @@ ...@@ -81,6 +65,14 @@
<groupId>it.oats.inaf</groupId> <groupId>it.oats.inaf</groupId>
<artifactId>vospace-datamodel</artifactId> <artifactId>vospace-datamodel</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<exclusions>
<!-- Transitive dependency excluded to avoid duplicated dependency issues.
We want to use always the version provided by Spring Boot -->
<exclusion>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jaxb-annotations</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
...@@ -89,16 +81,18 @@ ...@@ -89,16 +81,18 @@
<version>2.0.0-SNAPSHOT</version> <version>2.0.0-SNAPSHOT</version>
</dependency> </dependency>
<!-- Embedded PostgreSQL: -->
<dependency> <dependency>
<groupId>it.inaf.ia2</groupId> <groupId>com.opentable.components</groupId>
<artifactId>rap-client</artifactId> <artifactId>otj-pg-embedded</artifactId>
<version>1.0-SNAPSHOT</version> <version>0.13.3</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>it.inaf.ia2</groupId> <groupId>io.zonky.test.postgres</groupId>
<artifactId>gms-client</artifactId> <artifactId>embedded-postgres-binaries-linux-amd64</artifactId>
<version>1.0-SNAPSHOT</version> <version>12.5.0</version>
<scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package it.inaf.oats.vospace; package it.inaf.oats.vospace;
import java.util.List;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Node;
import it.inaf.oats.vospace.persistence.NodeDAO; import it.inaf.oats.vospace.persistence.NodeDAO;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
@RestController @RestController
public class ListNodeController { public class ListNodeController {
...@@ -25,14 +17,27 @@ public class ListNodeController { ...@@ -25,14 +17,27 @@ public class ListNodeController {
@Autowired @Autowired
private NodeDAO nodeDAO; private NodeDAO nodeDAO;
@GetMapping(value="/{nodeName}") @GetMapping(value = {"/nodes", "/nodes/**"},
public ResponseEntity<List<Node>>listNodes(@PathVariable("nodeName")String node_name) { produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_XML_VALUE})
public ResponseEntity<Node> listNode(HttpServletRequest request) {
// dal nome del nodo devo ricavarmi l'ivo_id String path = getPath(request);
String node_ivo_id = ""; return ResponseEntity.ok(nodeDAO.listNode(path));
return ResponseEntity.ok(nodeDAO.listNode(node_ivo_id));
} }
/**
* Slash is a special character in defining REST endpoints and trying to
* define a PathVariable containing slashes doesn't work, so the endpoint
* has been defined using "/nodes/**" instead of "/nodes/{path}" and the
* path is extracted manually parsing the request URL.
*/
private String getPath(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
String[] split = requestURL.split("/nodes/");
String path = "/";
if (split.length == 2) {
path += split[1];
}
return path;
}
} }
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package it.inaf.oats.vospace.persistence; package it.inaf.oats.vospace.persistence;
import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Node;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.sql.DataSource; import javax.sql.DataSource;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.DataNode;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.http.ResponseEntity;
/** /**
* *
...@@ -32,6 +20,8 @@ import org.springframework.http.ResponseEntity; ...@@ -32,6 +20,8 @@ import org.springframework.http.ResponseEntity;
@Repository @Repository
public class NodeDAO { public class NodeDAO {
@Value("${vospace-authority}")
private String authority;
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
...@@ -42,7 +32,6 @@ public class NodeDAO { ...@@ -42,7 +32,6 @@ public class NodeDAO {
public Node createNode(Node myNode) { public Node createNode(Node myNode) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("INSERT INTO "); sb.append("INSERT INTO ");
sb.append("NodeProperty"); sb.append("NodeProperty");
...@@ -54,22 +43,70 @@ public class NodeDAO { ...@@ -54,22 +43,70 @@ public class NodeDAO {
return myNode; return myNode;
} }
public Node listNode(String path) {
String sql = "SELECT os.os_path, n.node_id, type, async_trans, owner_id, group_read, group_write, is_public, content_length, created_on, last_modified from node n\n"
+ "JOIN node_os_path os ON n.node_id = os.node_id\n"
+ "WHERE n.path ~ (" + getFirstLevelChildrenSelector(path) + ")::lquery\n"
+ "OR os.os_path = ? ORDER BY os_path";
public List<Node> listNode(String nodeIvoId) { List<Node> parentAndChildren = jdbcTemplate.query(conn -> {
String sql = "SELECT * FROM Node WHERE ivo_id=?";
return jdbcTemplate.query(conn -> {
PreparedStatement ps = conn.prepareStatement(sql); PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, nodeIvoId); ps.setString(1, path);
ps.setString(2, path);
return ps; return ps;
}, (row, index) -> { }, (row, index) -> {
Node newNode = new Node(); return getNodeFromResultSet(row);
newNode.setUri(row.getString("ivo_id"));
return newNode;
}); });
// 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 node;
} }
private String getFirstLevelChildrenSelector(String path) {
String select = "(SELECT path FROM node WHERE node_id = (SELECT node_id FROM node_os_path WHERE os_path = ?))::varchar || '";
if (!"/".equals(path)) {
select += ".";
}
select += "*{1}'";
return select;
}
private Node getNodeFromResultSet(ResultSet rs) throws SQLException {
Node node = getTypedNode(rs.getString("type"));
node.setUri(getUri(rs.getString("os_path")));
return node;
}
private Node getTypedNode(String type) {
Node node;
switch (type) {
case "container":
node = new ContainerNode();
break;
case "data":
node = new DataNode();
break;
default:
throw new UnsupportedOperationException("Node type " + type + " not supported yet");
}
return node;
}
private String getUri(String path) {
return "vos://" + authority + path;
}
} }
...@@ -19,9 +19,9 @@ server.servlet.context-path=/vospace ...@@ -19,9 +19,9 @@ server.servlet.context-path=/vospace
# For development only: # For development only:
spring.profiles.active=dev spring.profiles.active=dev
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/vospacedb spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/vospace_testdb
spring.datasource.username=vospaceusr spring.datasource.username=postgres
spring.datasource.password=Peper0ne spring.datasource.password=postgres
# enable debug logging # enable debug logging
# this is equivalent to passing '--debug' as command line argument # this is equivalent to passing '--debug' as command line argument
...@@ -32,3 +32,5 @@ logging.level.org.springframework.web=TRACE ...@@ -32,3 +32,5 @@ logging.level.org.springframework.web=TRACE
#logging.level.org.springframework.boot=DEBUG #logging.level.org.springframework.boot=DEBUG
# log to file (absolute/relative path of log file) # log to file (absolute/relative path of log file)
#logging.file=path/to/log/file.log #logging.file=path/to/log/file.log
vospace-authority=example.com!vospace
package it.inaf.oats.vospace;
import it.inaf.oats.vospace.persistence.NodeDAO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.mockito.ArgumentMatchers.eq;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
@ExtendWith(MockitoExtension.class)
public class ListNodeControllerTest {
@Mock
private NodeDAO dao;
@InjectMocks
private ListNodeController controller;
private MockMvc mockMvc;
@BeforeEach
public void init() {
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
@Test
public void testRootXml() throws Exception {
mockMvc.perform(get("/nodes")
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().isOk());
verify(dao, times(1)).listNode(eq("/"));
}
@Test
public void testNodeXml() throws Exception {
mockMvc.perform(get("/nodes/mynode")
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().isOk());
verify(dao, times(1)).listNode(eq("/mynode"));
}
}
package it.inaf.oats.vospace.persistence;
import com.opentable.db.postgres.embedded.EmbeddedPostgres;
import com.opentable.db.postgres.embedded.PgBinaryResolver;
import com.opentable.db.postgres.embedded.UncompressBundleDirectoryResolver;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
/**
* Generates a DataSource that can be used for testing DAO classes. It loads an
* embedded Postgres database and fills it using the data from
* vospace-transfer-service repository (folder must exists; it location can be
* configured using the init_database_scripts_path in test.properties).
*/
@TestConfiguration
public class DataSourceConfig {
@Value("${init_database_scripts_path}")
private String scriptPath;
/**
* Using the prototype scope we are generating a different database in each
* test.
*/
@Bean
@Scope("prototype")
@Primary
public DataSource dataSource() throws Exception {
DataSource embeddedPostgresDS = EmbeddedPostgres.builder()
.setPgDirectoryResolver(new UncompressBundleDirectoryResolver(new CustomPostgresBinaryResolver()))
.start().getPostgresDatabase();
initDatabase(embeddedPostgresDS);
return embeddedPostgresDS;
}
private class CustomPostgresBinaryResolver implements PgBinaryResolver {
/**
* Loads specific embedded Postgres version.
*/
@Override
public InputStream getPgBinary(String system, String architecture) throws IOException {
ClassPathResource resource = new ClassPathResource(String.format("postgres-%s-%s.txz", system.toLowerCase(), architecture));
return resource.getInputStream();
}
}
/**
* Loads SQL scripts for database initialization from
* vospace-transfer-service repo directory.
*/
private void initDatabase(DataSource dataSource) throws Exception {
try ( Connection conn = dataSource.getConnection()) {
File currentDir = new File(DataSourceConfig.class.getClassLoader().getResource(".").getFile());
Path scriptDir = currentDir.toPath().resolve(scriptPath);
List<String> scripts = Arrays.asList("00-init.sql", "01-pgsql_path.sql", "03-indexes.sql", "05-data.sql", "06-os_path_view.sql");
for (String script : scripts) {
ByteArrayResource scriptResource = replaceDollarQuoting(scriptDir.resolve(script));
ScriptUtils.executeSqlScript(conn, scriptResource);
}
}
}
/**
* It seems that dollar quoting (used in UDF) is broken in JDBC. Replacing
* it with single quotes solves the problem. We replace the quoting here
* instead of inside the original files because dollar quoting provides a
* better visibility.
*/
private ByteArrayResource replaceDollarQuoting(Path sqlScriptPath) throws Exception {
String scriptContent = Files.readString(sqlScriptPath);
if (scriptContent.contains("$func$")) {
String func = extractFunctionDefinition(scriptContent);
String originalFunction = "$func$" + func + "$func$";
String newFunction = "'" + func.replaceAll("'", "''") + "'";
scriptContent = scriptContent.replace(originalFunction, newFunction);
}
return new ByteArrayResource(scriptContent.getBytes());
}
private String extractFunctionDefinition(String scriptContent) {
Pattern pattern = Pattern.compile("\\$func\\$(.*?)\\$func\\$", Pattern.DOTALL);
Matcher matcher = pattern.matcher(scriptContent);
if (matcher.find()) {
return matcher.group(1);
}
throw new IllegalArgumentException(scriptContent + " doesn't contain $func$");
}
}
package it.inaf.oats.vospace.persistence;
import javax.sql.DataSource;
import net.ivoa.xml.vospace.v2.ContainerNode;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {DataSourceConfig.class})
@TestPropertySource(locations = "classpath:test.properties")
public class NodeDAOTest {
@Autowired
private DataSource dataSource;
private NodeDAO dao;
@BeforeEach
public void init() {
dao = new NodeDAO(dataSource);
}
@Test
public void testListNode() {
ContainerNode root = (ContainerNode) dao.listNode("/");
assertEquals(1, root.getNodes().size());
}
}
init_database_scripts_path=../../../vospace-transfer-service/file_catalog
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment