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

Implemented folder creation

parent 79b85ff5
No related branches found
No related tags found
No related merge requests found
Pipeline #879 failed
Showing
with 173 additions and 6 deletions
......@@ -4,4 +4,4 @@
**/dist/*
.env.local
nbactions.xml
.env.development.local
......@@ -2,8 +2,8 @@ package it.inaf.ia2.vospace.ui.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.vospace.ui.VOSpaceException;
import it.inaf.ia2.vospace.ui.VOSpaceUiApplication;
import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
......@@ -40,6 +40,9 @@ public class VOSpaceClient {
@Value("${use-json}")
private boolean useJson;
@Value("${vospace-authority}")
private String authority;
private static final ObjectMapper MAPPER = new ObjectMapper();
private final HttpClient httpClient;
......@@ -95,6 +98,19 @@ public class VOSpaceClient {
return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Transfer.class)).getProtocols();
}
public Node createNode(Node node) {
String path = node.getUri().substring(("vos://" + authority).length());
HttpRequest request = getRequest("/nodes" + path)
.header("Accept", useJson ? "application/json" : "text/xml")
.header("Content-Type", useJson ? "application/json" : "text/xml")
.PUT(HttpRequest.BodyPublishers.ofString(marshal(node)))
.build();
return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class));
}
private <T, U> U call(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, int expectedStatusCode, Function<T, U> responseHandler) {
try {
return httpClient.sendAsync(request, responseBodyHandler)
......
package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.exception.BadRequestException;
import it.inaf.ia2.vospace.ui.service.NodesService;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Protocol;
import net.ivoa.xml.vospace.v2.Transfer;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -12,6 +15,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
......@@ -64,6 +69,28 @@ public class NodesController {
return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
}
@PostMapping(value = "/folder")
public void newFolder(@RequestBody Map<String, String> params) {
String parentPath = getRequiredParam(params, "parentPath");
if (!parentPath.startsWith("/")) {
parentPath = "/" + parentPath;
}
String name = getRequiredParam(params, "name");
ContainerNode node = new ContainerNode();
node.setUri("vos://" + authority + parentPath + "/" + name);
client.createNode(node);
}
private String getRequiredParam(Map<String, String> params, String key) {
if (!params.containsKey(key)) {
throw new BadRequestException("Missing mandatory parameter " + key);
}
return params.get(key);
}
/**
* Slash is a special character in defining REST endpoints and trying to
* define a PathVariable containing slashes doesn't work, so the endpoint
......
package it.inaf.ia2.vospace.ui.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public class BadRequestException extends VOSpaceException {
public BadRequestException(String message) {
super(message);
}
}
package it.inaf.ia2.vospace.ui;
package it.inaf.ia2.vospace.ui.exception;
public class VOSpaceException extends RuntimeException {
......
package it.inaf.ia2.vospace.ui.service;
import it.inaf.ia2.vospace.ui.VOSpaceException;
import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
import java.util.Optional;
import net.ivoa.xml.vospace.v2.Node;
import net.ivoa.xml.vospace.v2.Property;
......
......@@ -15,6 +15,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import static org.mockito.Mockito.mock;
......@@ -40,6 +41,7 @@ public class VOSpaceClientTest {
try ( MockedStatic<HttpClient> staticMock = Mockito.mockStatic(HttpClient.class)) {
staticMock.when(HttpClient::newBuilder).thenReturn(builder);
voSpaceClient = new VOSpaceClient("http://localhost/vospace");
ReflectionTestUtils.setField(voSpaceClient, "authority", "ia2.inaf.it!vospace");
}
voSpaceClient.servletRequest = mock(HttpServletRequest.class);
......@@ -56,6 +58,24 @@ public class VOSpaceClientTest {
assertEquals("vos://ia2.inaf.it!vospace/node1", node.getUri());
}
@Test
public void testCreateNode() {
ReflectionTestUtils.setField(voSpaceClient, "useJson", false);
CompletableFuture response = getMockedStreamResponseFuture(200, getResourceFileContent("node-response.xml"));
when(mockedHttpClient.sendAsync(argThat(request -> {
assertEquals("/vospace/nodes/mynode/newnode", request.uri().getPath());
return true;
}), any())).thenReturn(response);
ContainerNode newNode = new ContainerNode();
newNode.setUri("vos://ia2.inaf.it!vospace/mynode/newnode");
ContainerNode responseNode = (ContainerNode) voSpaceClient.createNode(newNode);
assertEquals(newNode.getUri(), responseNode.getUri());
}
protected static String getResourceFileContent(String fileName) {
try ( InputStream in = VOSpaceClientTest.class.getClassLoader().getResourceAsStream(fileName)) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
......
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<vos:node xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
busy="false" xsi:type="vos:ContainerNode" uri="vos://ia2.inaf.it!vospace/mynode/newnode">
<vos:properties>
<vos:property uri="ivo://ivoa.net/vospace/core#btime">2021-01-15 18:24:03.749352</vos:property>
<vos:property uri="ivo://ivoa.net/vospace/core#groupwrite">group1</vos:property>
</vos:properties>
</vos:node>
\ No newline at end of file
......@@ -45,5 +45,8 @@ export default {
},
getUserInfo() {
return fetch(user, false);
},
createFolder() {
return fetch({});
}
}
......@@ -84,5 +84,20 @@ export default {
},
data: paths
});
},
createFolder(path, newFolderName) {
let url = BASE_API_URL + 'folder';
return apiRequest({
method: 'POST',
url: url,
withCredentials: true,
headers: {
'Cache-Control': 'no-cache'
},
data: {
parentPath: path,
name: newFolderName
}
});
}
}
......@@ -2,7 +2,7 @@
<div class="container">
<b-breadcrumb :items="breadcrumbs"></b-breadcrumb>
<div class="mb-3">
<b-button variant="success" class="mr-2" :disabled="true">New folder</b-button>
<b-button variant="success" class="mr-2" :disabled="false" v-b-modal.create-folder-modal>New folder</b-button>
<b-button variant="success" class="mr-2" :disabled="true">Upload files</b-button>
<b-button variant="primary" class="mr-2" v-if="tapeButtonEnabled" @click="startRecallFromTapeJob">Recall from tape</b-button>
</div>
......@@ -28,16 +28,19 @@
<tbody id="nodes"></tbody>
</table>
</b-card>
<CreateFolderModal />
</div>
</template>
<script>
import { BIconCheckSquare, BIconSquare } from 'bootstrap-vue'
import CreateFolderModal from './modal/CreateFolderModal.vue'
export default {
components: {
BIconCheckSquare,
BIconSquare
BIconSquare,
CreateFolderModal
},
computed: {
breadcrumbs() {
......
<template>
<b-modal id="create-folder-modal" title="Create folder" okTitle="Create" @show="reset" @ok="createFolder">
<b-form inline>
<label class="w-25" for="new-folder-name-input">Folder name</label>
<b-form-input v-model.trim="newFolderName" id="new-folder-name-input" ref="newFolderNameInput" class="w-75" aria-describedby="new-folder-name-input-feedback" :state="newFolderNameState" v-on:input="resetError"
@keydown.native.enter="createFolder">
</b-form-input>
<b-form-invalid-feedback id="new-folder-name-input-feedback" class="text-right">{{newFolderNameError}}</b-form-invalid-feedback>
</b-form>
</b-modal>
</template>
<script>
export default {
data() {
return {
newFolderName: null,
newFolderNameError: null
}
},
computed: {
newFolderNameState() {
if (this.newFolderNameError) {
return false;
}
return null;
}
},
methods: {
reset() {
this.newFolderName = null;
this.resetError();
},
resetError() {
this.newFolderNameError = null;
},
createFolder(event) {
// Prevent modal from closing
event.preventDefault();
if (!this.newFolderName) {
this.newFolderNameError = "Folder name is required";
} else {
this.$store.dispatch('createFolder', this.newFolderName)
.then(() => { //res
//this.$store.commit('updateGroupsPanel', res);
this.$bvModal.hide('create-folder-modal');
})
.catch(res => {
this.newFolderNameError = res.message;
});
}
}
}
}
</script>
......@@ -80,6 +80,13 @@ export default new Vuex.Store({
loadUserInfo({ commit }) {
client.getUserInfo()
.then(res => commit('setUsername', res.username));
},
createFolder({ state, dispatch }, newFolderName) {
client.createFolder(state.path, newFolderName)
.then(() => {
// Reload current node
dispatch('setPath', state.path);
});
}
}
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment