Commit d93836d2 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Implemented creation of links (single and by list of links upload)

parent 3aecd050
Loading
Loading
Loading
Loading
Loading
+102 −0
Original line number Original line Diff line number Diff line
/*
 * This file is part of vospace-ui
 * Copyright (C) 2021 Istituto Nazionale di Astrofisica
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package it.inaf.ia2.vospace.ui.controller;

import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.CreateLinkRequest;
import it.inaf.ia2.vospace.ui.exception.BadRequestException;
import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.LinkNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class CreateLinksController extends BaseController {

    private static final Logger LOG = LoggerFactory.getLogger(CreateLinksController.class);

    @Autowired
    private VOSpaceClient client;

    @PostMapping(value = "/createLink", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<?> createLink(@RequestBody CreateLinkRequest request) {

        ContainerNode parent = getFolder(request.getFolder());

        String uri = parent.getUri() + "/" + request.getNodeName();

        LinkNode link = new LinkNode();
        link.setUri(uri);
        link.setTarget(request.getUrl());

        client.createNode(link);

        return ResponseEntity.noContent().build();
    }

    @PostMapping(value = "/uploadLinks", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<?> uploadLinks(@RequestParam(value = "file", required = true) MultipartFile file,
            @RequestParam("folder") String folder) throws IOException {

        ContainerNode parent = getFolder(folder);

        String fileContent = new String(file.getBytes());

        // Execute HTTP calls for links creation in bunches of 20 calls performed in parallel
        List<List<CompletableFuture<?>>> httpCallsGroups = new ArrayList<>();
        List<CompletableFuture<?>> currentHttpCallsGroup = new ArrayList<>();
        httpCallsGroups.add(currentHttpCallsGroup);

        for (String url : fileContent.replaceAll("\\r\\n?", "\n").split("\n")) { // normalize newlines and split on them
            if (!url.isBlank()) {

                String fileName = url.substring(url.lastIndexOf("/") + 1);
                String uri = parent.getUri() + "/" + fileName;

                LinkNode link = new LinkNode();
                link.setUri(uri);
                link.setTarget(url);

                if (currentHttpCallsGroup.size() > 20) {
                    currentHttpCallsGroup = new ArrayList<>();
                    httpCallsGroups.add(currentHttpCallsGroup);
                }

                currentHttpCallsGroup.add(CompletableFuture.supplyAsync(() -> client.createNode(link), Runnable::run));
            }
        }

        for (List<CompletableFuture<?>> httpCallsGroup : httpCallsGroups) {
            CompletableFuture.allOf(httpCallsGroup.toArray(CompletableFuture[]::new)).join();
        }

        return ResponseEntity.noContent().build();
    }

    private ContainerNode getFolder(String folderPath) {
        try {
            return (ContainerNode) client.getNode("/" + folderPath);
        } catch (VOSpaceStatusException ex) {
            if (ex.getHttpStatus() == 404) {
                throw new BadRequestException("Folder parameter specified a non-existent folder: /" + folderPath);
            }
            throw ex;
        }
    }
}
+37 −0
Original line number Original line Diff line number Diff line
/*
 * This file is part of vospace-ui
 * Copyright (C) 2021 Istituto Nazionale di Astrofisica
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package it.inaf.ia2.vospace.ui.data;

public class CreateLinkRequest {

    private String url;
    private String folder;
    private String nodeName;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getFolder() {
        return folder;
    }

    public void setFolder(String folder) {
        this.folder = folder;
    }

    public String getNodeName() {
        return nodeName;
    }

    public void setNodeName(String nodeName) {
        this.nodeName = nodeName;
    }
}
+115 −0
Original line number Original line Diff line number Diff line
/*
 * This file is part of vospace-ui
 * Copyright (C) 2021 Istituto Nazionale di Astrofisica
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package it.inaf.ia2.vospace.ui.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.CreateLinkRequest;
import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException;
import net.ivoa.xml.vospace.v2.ContainerNode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import org.mockito.Mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {"vospace-authority=example.com!vospace"})
public class CreateLinksControllerTest {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @MockBean
    private VOSpaceClient client;

    @Autowired
    private MockMvc mockMvc;

    @Mock
    private User user;

    @BeforeEach
    public void setUp() {
        when(user.getName()).thenReturn("user_id");
    }

    @Test
    public void testCreateSingleLink() throws Exception {

        ContainerNode myFolder = new ContainerNode();
        myFolder.setUri("vos://ia2.inaf.it!vospace/path/to/myfolder");
        when(client.getNode("/path/to/myfolder")).thenReturn(myFolder);

        CreateLinkRequest request = new CreateLinkRequest();

        request.setFolder("path/to/myfolder");
        request.setUrl("http://archives.ia2.inaf.it/files/aao/SC182172.fits.gz");
        request.setNodeName("myLink");

        String requestPayload = MAPPER.writeValueAsString(request);

        mockMvc.perform(post("/createLink")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestPayload)
                .sessionAttr("user_data", user))
                .andDo(print())
                .andExpect(status().isNoContent());

        verify(client, times(1)).createNode(argThat(node -> {
            return node.getUri().equals("vos://ia2.inaf.it!vospace/path/to/myfolder/myLink");
        }));
    }

    @Test
    public void testUploadLinksNonExistentFolder() throws Exception {

        when(client.getNode("/path/to/non-existent")).thenThrow(new VOSpaceStatusException("Not found", 404));

        mockMvc.perform(multipart("/uploadLinks")
                .file(getListOfLinksMockMultipartFile())
                .param("folder", "path/to/non-existent")
                .sessionAttr("user_data", user))
                .andDo(print())
                .andExpect(status().isBadRequest());
    }

    @Test
    public void testUploadLinks() throws Exception {

        ContainerNode myFolder = new ContainerNode();
        when(client.getNode("/path/to/myfolder")).thenReturn(myFolder);

        mockMvc.perform(multipart("/uploadLinks")
                .file(getListOfLinksMockMultipartFile())
                .param("folder", "path/to/myfolder")
                .sessionAttr("user_data", user))
                .andDo(print())
                .andExpect(status().isNoContent());

        verify(client, times(75)).createNode(any());
    }

    private MockMultipartFile getListOfLinksMockMultipartFile() throws Exception {
        return new MockMultipartFile("file", UploadControllerTest.class.getClassLoader().getResourceAsStream("list-of-links.txt"));
    }
}
+75 −0
Original line number Original line Diff line number Diff line
http://archives.ia2.inaf.it/files/aao/SC182159.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182160.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182161.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182169.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182170.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182171.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182172.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182173.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182174.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182175.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182176.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182177.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182178.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182338.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182339.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182340.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182341.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182342.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182343.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182344.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182345.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182346.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182347.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182348.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182349.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182350.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182351.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182352.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182353.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182354.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182355.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182356.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182162.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182163.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182164.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182165.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182166.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182167.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182168.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182357.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182358.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182359.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182360.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182361.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182362.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182363.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182364.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182365.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182366.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182367.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182368.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182369.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182370.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182371.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182372.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182373.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182374.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182375.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182376.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182377.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182378.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182379.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182380.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182381.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182382.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182386.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182383.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182384.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182385.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182387.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182388.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182389.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182390.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182391.fits.gz
http://archives.ia2.inaf.it/files/aao/SC182392.fits.gz
+32 −0
Original line number Original line Diff line number Diff line
@@ -151,6 +151,38 @@ export default {
      source
      source
    };
    };
  },
  },
  createLink(nodeUrl, folder, nodeName) {
    let url = BASE_API_URL + 'createLink';
    return apiRequest({
      method: 'POST',
      url: url,
      withCredentials: true,
      headers: {
        'Cache-Control': 'no-cache'
      },
      data: {
        url: nodeUrl,
        folder,
        nodeName
      }
    });
  },
  uploadLinks(file, path) {
    let formData = new FormData();
    formData.append('file', file);

    let url = BASE_API_URL + 'uploadLinks?folder=' + escapePath(path);
    return apiRequest({
      method: 'POST',
      url: url,
      withCredentials: true,
      headers: {
        'Cache-Control': 'no-cache',
        'Content-Type': 'multipart/form-data'
      },
      data: formData
    });
  },
  deleteNodes(paths, calledFromUploadModal = false) {
  deleteNodes(paths, calledFromUploadModal = false) {
    let url = BASE_API_URL + 'delete';
    let url = BASE_API_URL + 'delete';
    return apiRequest({
    return apiRequest({
Loading