From a650ae0792fe6f1a5934f23693e76046cd73ff0d Mon Sep 17 00:00:00 2001 From: Martin Weise <martin.weise@tuwien.ac.at> Date: Tue, 28 Jun 2022 17:52:52 +0200 Subject: [PATCH] Deposit files works for invenio --- .invenio/.gitignore | 4 +- .invenio/api_authentication/configuration.py | 2 +- .invenio/api_container/configuration.py | 2 +- .invenio/api_database/configuration.py | 2 +- .invenio/api_document/configuration.py | 2 +- .invenio/api_identifier/configuration.py | 2 +- .../api_query/api/table_data_endpoint_api.py | 4 +- .invenio/api_query/configuration.py | 2 +- .invenio/api_table/configuration.py | 2 +- .invenio/{analyze.ipynb => deposit.ipynb} | 10 +- .invenio/dev.py | 89 ++--- .invenio/feature_extract.ipynb | 367 ++++++++++++++++++ .invenio/requirements.txt | 3 +- .../at/tuwien/endpoints/DocumentEndpoint.java | 28 +- .../at/tuwien/endpoints/FileEndpoint.java | 19 +- .../src/main/resources/application-docker.yml | 1 + .../src/main/resources/application.yml | 4 +- .../src/test/java/at/tuwien/BaseUnitTest.java | 47 ++- .../endpoint/DocumentEndpointUnitTest.java | 39 ++ .../DocumentServiceIntegrationTest.java | 8 +- .../service/FileServiceIntegrationTest.java | 40 +- .../src/test/resources/images/mock.png | Bin 0 -> 11666 bytes .../java/at/tuwien/auth/AuthTokenFilter.java | 2 +- .../java/at/tuwien/config/GatewayConfig.java | 12 +- .../at/tuwien/config/WebSecurityConfig.java | 1 - .../exception/CommitFileUploadException.java | 21 + .../tuwien/exception/FileUploadException.java | 21 + .../at/tuwien/gateway/DocumentGateway.java | 20 +- .../AuthenticationServiceGatewayImpl.java | 10 +- .../impl/InvenioDocumentGatewayImpl.java | 128 ++++-- .../java/at/tuwien/mapper/DocumentMapper.java | 6 + .../at/tuwien/service/DocumentService.java | 10 +- .../java/at/tuwien/service/FileService.java | 10 +- .../service/impl/InvenioDraftServiceImpl.java | 22 +- .../service/impl/InvenioFileServiceImpl.java | 15 +- .../gatewayservice/config/GatewayConfig.java | 5 + .../FdaIdentifierServiceApplication.java | 1 - .../src/main/resources/application.yml | 1 + .../api/document/file/FileAnnounceDto.java | 37 ++ .../at/tuwien/api/document/file/FileDto.java | 72 ++++ .../api/document/file/FileEntryDto.java | 43 ++ .../tuwien/api/document/file/FileKeyDto.java | 23 ++ .../record/{DraftDto.java => RecordDto.java} | 2 +- fda-ui/server-middleware/index.js | 8 +- 44 files changed, 975 insertions(+), 172 deletions(-) rename .invenio/{analyze.ipynb => deposit.ipynb} (82%) mode change 100644 => 100755 .invenio/dev.py create mode 100644 .invenio/feature_extract.ipynb create mode 100644 fda-document-service/rest-service/src/test/resources/images/mock.png create mode 100644 fda-document-service/services/src/main/java/at/tuwien/exception/CommitFileUploadException.java create mode 100644 fda-document-service/services/src/main/java/at/tuwien/exception/FileUploadException.java create mode 100644 fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileAnnounceDto.java create mode 100644 fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileDto.java create mode 100644 fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileEntryDto.java create mode 100644 fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileKeyDto.java rename fda-metadata-db/api/src/main/java/at/tuwien/api/document/record/{DraftDto.java => RecordDto.java} (98%) diff --git a/.invenio/.gitignore b/.invenio/.gitignore index ed8ebf583f..150c6f7559 100644 --- a/.invenio/.gitignore +++ b/.invenio/.gitignore @@ -1 +1,3 @@ -__pycache__ \ No newline at end of file +__pycache__ + +features.csv \ No newline at end of file diff --git a/.invenio/api_authentication/configuration.py b/.invenio/api_authentication/configuration.py index a35f4efa83..2638b5e36f 100644 --- a/.invenio/api_authentication/configuration.py +++ b/.invenio/api_authentication/configuration.py @@ -46,7 +46,7 @@ class Configuration(six.with_metaclass(TypeWithDefault, object)): def __init__(self): """Constructor""" # Default Base url - self.host = "http://localhost:9097" + self.host = "http://localhost:9095" # Temp file folder for downloading files self.temp_folder_path = None diff --git a/.invenio/api_container/configuration.py b/.invenio/api_container/configuration.py index b93a7ea5d3..23d21b8381 100644 --- a/.invenio/api_container/configuration.py +++ b/.invenio/api_container/configuration.py @@ -46,7 +46,7 @@ class Configuration(six.with_metaclass(TypeWithDefault, object)): def __init__(self): """Constructor""" # Default Base url - self.host = "http://localhost:9091" + self.host = "http://localhost:9095" # Temp file folder for downloading files self.temp_folder_path = None diff --git a/.invenio/api_database/configuration.py b/.invenio/api_database/configuration.py index 758efb3300..5ee4406b93 100644 --- a/.invenio/api_database/configuration.py +++ b/.invenio/api_database/configuration.py @@ -46,7 +46,7 @@ class Configuration(six.with_metaclass(TypeWithDefault, object)): def __init__(self): """Constructor""" # Default Base url - self.host = "http://localhost:9092" + self.host = "http://localhost:9095" # Temp file folder for downloading files self.temp_folder_path = None diff --git a/.invenio/api_document/configuration.py b/.invenio/api_document/configuration.py index bb7e50accf..3acecd4c60 100644 --- a/.invenio/api_document/configuration.py +++ b/.invenio/api_document/configuration.py @@ -46,7 +46,7 @@ class Configuration(six.with_metaclass(TypeWithDefault, object)): def __init__(self): """Constructor""" # Default Base url - self.host = "http://localhost:9099" + self.host = "http://localhost:9095" # Temp file folder for downloading files self.temp_folder_path = None diff --git a/.invenio/api_identifier/configuration.py b/.invenio/api_identifier/configuration.py index 7845c1cc68..28b5cf8755 100644 --- a/.invenio/api_identifier/configuration.py +++ b/.invenio/api_identifier/configuration.py @@ -46,7 +46,7 @@ class Configuration(six.with_metaclass(TypeWithDefault, object)): def __init__(self): """Constructor""" # Default Base url - self.host = "http://localhost:9096" + self.host = "http://localhost:9095" # Temp file folder for downloading files self.temp_folder_path = None diff --git a/.invenio/api_query/api/table_data_endpoint_api.py b/.invenio/api_query/api/table_data_endpoint_api.py index 12101dabc2..34b6434bf0 100644 --- a/.invenio/api_query/api/table_data_endpoint_api.py +++ b/.invenio/api_query/api/table_data_endpoint_api.py @@ -259,7 +259,7 @@ class TableDataEndpointApi(object): auth_settings = [] # noqa: E501 return self.api_client.call_api( - '/api/container/{id}/database/{databaseId}/table/{tableId}/data', 'HEAD', + '/api/container/{id}/database/{databaseId}/table/{tableId}/data', 'GET', path_params, query_params, header_params, @@ -380,7 +380,7 @@ class TableDataEndpointApi(object): auth_settings = [] # noqa: E501 return self.api_client.call_api( - '/api/container/{id}/database/{databaseId}/table/{tableId}/data', 'GET', + '/api/container/{id}/database/{databaseId}/table/{tableId}/data', 'HEAD', path_params, query_params, header_params, diff --git a/.invenio/api_query/configuration.py b/.invenio/api_query/configuration.py index 1b566374df..2991fff8bf 100644 --- a/.invenio/api_query/configuration.py +++ b/.invenio/api_query/configuration.py @@ -46,7 +46,7 @@ class Configuration(six.with_metaclass(TypeWithDefault, object)): def __init__(self): """Constructor""" # Default Base url - self.host = "http://localhost:9093" + self.host = "http://localhost:9095" # Temp file folder for downloading files self.temp_folder_path = None diff --git a/.invenio/api_table/configuration.py b/.invenio/api_table/configuration.py index f24b34599f..557adf4db5 100644 --- a/.invenio/api_table/configuration.py +++ b/.invenio/api_table/configuration.py @@ -46,7 +46,7 @@ class Configuration(six.with_metaclass(TypeWithDefault, object)): def __init__(self): """Constructor""" # Default Base url - self.host = "http://localhost:9094" + self.host = "http://localhost:9095" # Temp file folder for downloading files self.temp_folder_path = None diff --git a/.invenio/analyze.ipynb b/.invenio/deposit.ipynb similarity index 82% rename from .invenio/analyze.ipynb rename to .invenio/deposit.ipynb index 542a3613a6..6fe7d9c770 100644 --- a/.invenio/analyze.ipynb +++ b/.invenio/deposit.ipynb @@ -2,15 +2,11 @@ "cells": [ { "cell_type": "markdown", + "source": [], "metadata": { - "collapsed": true, - "pycharm": { - "name": "#%% md\n" - } + "collapsed": false }, - "source": [ - "# Test" - ] + "outputs": [] } ], "metadata": { diff --git a/.invenio/dev.py b/.invenio/dev.py old mode 100644 new mode 100755 index aa0cb1af32..81a94da41a --- a/.invenio/dev.py +++ b/.invenio/dev.py @@ -1,67 +1,34 @@ #!/usr/bin/env python3 -import json - +import re +import csv import requests -from api_authentication.api.authentication_endpoint_api import AuthenticationEndpointApi -from api_authentication.api.user_endpoint_api import UserEndpointApi -from api_container.api.container_endpoint_api import ContainerEndpointApi -from api_database.api.container_database_endpoint_api import ContainerDatabaseEndpointApi +doi = '10.5281/zenodo.5649276' +headers = { + 'Authorize': 'Bearer djCvqkoOW69keHajybZiwE8bBjyir2QSZOLKpAtc4S1Wp17KXgcHmMoWJwft' +} -authentication = AuthenticationEndpointApi() -user = UserEndpointApi() -container = ContainerEndpointApi() -database = ContainerDatabaseEndpointApi() +# Resolve DOI +response = requests.get('https://doi.org/' + doi) +id = re.findall('/([a-z0-9-]+)$', response.url)[0] +host = re.findall('^https?:\/\/([a-z0-9]+\.[a-z]+)', response.url)[0] +print("Resolved DOI to", host, "and record id", id) -# # Create account -# response = user.register({ -# 'username': 'mweise', -# 'password': 'fda', -# 'email': 'martin.weise@tuwien.ac.at' -# }) -# print('Created account with username %s' % response.username) -# -# # Create authentication -# response = authentication.authenticate_user1({ -# 'username': 'mweise', -# 'password': 'fda' -# }) -# container.api_client.default_headers = { -# 'Authorization': 'Bearer ' + response.token -# } -# database.api_client.default_headers = { -# 'Authorization': 'Bearer ' + response.token -# } -# -# # Create container -# response = container.create1({ -# 'name': 'MIR ' + str(uuid.uuid1()), -# 'repository': 'mariadb', -# 'tag': '10.5' -# }) -# cid = response.id -# print('Created container with id %d' % cid) -# -# # Start container -# response = container.modify({ -# 'action': 'START' -# }, cid) -# time.sleep(5) -# print('Started container with id %d' % cid) -# -# # Create database -# response = database.create({ -# 'name': 'MIR ' + str(uuid.uuid1()), -# 'description': 'Music Information Retrieval', -# 'is_public': True -# }, cid) -# dbid = response.id -# print('Created database with id %d' % dbid) +# Find files +url = 'https://' + host + '/api/records/' + id +response = requests.get(url, headers=headers) +record = response.json() -# Analyse Table -response = requests.post('http://localhost:5000/api/analyse/determinedt', json={ - 'filepath': '/tmp/test.csv', -}) -data = json.loads(response.content) -print('Determined data types') -print(response) +# Write some .csv +i = 0 +with open('./features.csv', 'w') as f: + writer = csv.writer(f) + writer.writerow(['key', 'size', 'link']) + for file in record['files']: + requests.get(file['links']['self']) + print("... feature extract from", file['links']['self']) + writer.writerow([file['key'], file['size'], file['links']['self']]) + i += 1 + if i > 10: + break +print("Generated a feature .csv") diff --git a/.invenio/feature_extract.ipynb b/.invenio/feature_extract.ipynb new file mode 100644 index 0000000000..9de40c112c --- /dev/null +++ b/.invenio/feature_extract.ipynb @@ -0,0 +1,367 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": true, + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "# Feature Extraction & Deposit\n", + "\n", + "In this notebook we define an example of creating a database from a .csv and perform feature extraction of audio files. The APIs are auto-generated from the Swagger Endpoint documentations using [`generate.sh`](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-docs/-/blob/master/swagger/generate.sh). Steps we perform:\n", + "\n", + " 1. Download a music file from a public repository\n", + " 2. Perform feature extraction\n", + " 3. Create an account at DBRepo\n", + " 4. Create an authentication token\n", + " 5. Create a mariadb container\n", + " 6. Start the mariadb container\n", + " 7. Create a database within the mariadb container\n", + " 8. Import the feature .csv (manually)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "outputs": [], + "source": [ + "import os.path\n", + "import uuid\n", + "import time\n", + "import re\n", + "import csv\n", + "import requests as rq\n", + "from api_authentication.api.authentication_endpoint_api import AuthenticationEndpointApi\n", + "from api_authentication.api.user_endpoint_api import UserEndpointApi\n", + "from api_container.api.container_endpoint_api import ContainerEndpointApi\n", + "from api_database.api.container_database_endpoint_api import ContainerDatabaseEndpointApi\n", + "from api_table.api.table_endpoint_api import TableEndpointApi\n", + "\n", + "authentication = AuthenticationEndpointApi()\n", + "user = UserEndpointApi()\n", + "container = ContainerEndpointApi()\n", + "database = ContainerDatabaseEndpointApi()\n", + "table = TableEndpointApi()\n", + "\n", + "doi = \"10.5281/zenodo.5649276\"\n", + "email = \"some@example.com\"" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## 1. Download wav\n", + "\n", + "Resolve the DOI to URI" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 21, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resolved DOI to zenodo.org and record id 5649276\n" + ] + } + ], + "source": [ + "response = rq.get(\"https://doi.org/\" + doi)\n", + "id = re.findall(\"/([a-z0-9-]+)$\", response.url)[0]\n", + "host = re.findall(\"^https?:\\/\\/([a-z0-9]+\\.[a-z]+)\", response.url)[0]\n", + "print(\"Resolved DOI to\", host, \"and record id\", id)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## 2. Perform feature extraction" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 22, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0044_20200518133554_1_m4a_1.wav\n", + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0044_20200518133554_2_m4a_1.wav\n", + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0066_20200611134530_1_m4a_0.wav\n", + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0066_20200611134530_2_m4a_0.wav\n", + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0066_20200612072315_1_m4a_0.wav\n", + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0066_20200612072315_2_m4a_0.wav\n", + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0066_20200613082517_1_m4a_0.wav\n", + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0066_20200613082517_2_m4a_0.wav\n", + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0066_20200614080017_1_m4a_0.wav\n", + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0066_20200614080017_2_m4a_0.wav\n", + "... feature extract from https://zenodo.org/api/files/22d69a63-2aff-47ae-b818-be78a23e9889/colive.0066_20200615070238_1_m4a_0.wav\n", + "Generated a feature .csv in your home directory\n" + ] + } + ], + "source": [ + "response = rq.get(\"https://\" + host + \"/api/records/\" + id)\n", + "record = response.json()\n", + "\n", + "i = 0\n", + "with open(os.path.expanduser(\"~/features.csv\"), \"w\") as f:\n", + " writer = csv.writer(f)\n", + " writer.writerow([\"key\", \"size\", \"link\"])\n", + " for file in record[\"files\"]:\n", + " rq.get(file[\"links\"][\"self\"])\n", + " print(\"... feature extract from\", file[\"links\"][\"self\"])\n", + " writer.writerow([file[\"key\"], file[\"size\"], file[\"links\"][\"self\"]])\n", + " i += 1\n", + " if i > 10:\n", + " break\n", + "print(\"Generated a feature .csv in your home directory\")" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## 3. Create an account at DBRepo" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 23, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'authorities': [{'authority': 'ROLE_RESEARCHER'}],\n", + " 'containers': None,\n", + " 'databases': None,\n", + " 'email': 'martinweiseat@gmail.com',\n", + " 'firstname': None,\n", + " 'id': 2,\n", + " 'identifiers': None,\n", + " 'lastname': None,\n", + " 'titles_after': None,\n", + " 'titles_before': None,\n", + " 'username': 'user'}\n" + ] + } + ], + "source": [ + "response = user.register({\n", + " \"username\": \"user\",\n", + " \"password\": \"user\",\n", + " \"email\": email\n", + "})\n", + "print(response)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## 4. Create an authentication token" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 24, + "outputs": [], + "source": [ + "response = authentication.authenticate_user1({\n", + " \"username\": \"user\",\n", + " \"password\": \"user\"\n", + "})\n", + "container.api_client.default_headers = {\"Authorization\": \"Bearer \" + response.token}\n", + "database.api_client.default_headers = {\"Authorization\": \"Bearer \" + response.token}\n", + "table.api_client.default_headers = {\"Authorization\": \"Bearer \" + response.token}" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## 5. Create a mariadb container" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 25, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'hash': 'f5a649a71aae3748e62228721c44627ffc866f665d677bb890c37b9111590ffa',\n", + " 'id': 2,\n", + " 'internal_name': 'fda-userdb-mir-1010b964-f6fa-11ec-9f77-64bc58900b78',\n", + " 'is_public': None,\n", + " 'name': 'MIR 1010b964-f6fa-11ec-9f77-64bc58900b78'}\n" + ] + } + ], + "source": [ + "response = container.create1({\n", + " \"name\": \"MIR \" + str(uuid.uuid1()),\n", + " \"repository\": \"mariadb\",\n", + " \"tag\": \"10.5\"\n", + "})\n", + "container_id = response.id\n", + "print(response)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## 6. Start the mariadb container" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 26, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'hash': 'f5a649a71aae3748e62228721c44627ffc866f665d677bb890c37b9111590ffa',\n", + " 'id': 2,\n", + " 'internal_name': 'fda-userdb-mir-1010b964-f6fa-11ec-9f77-64bc58900b78',\n", + " 'is_public': None,\n", + " 'name': 'MIR 1010b964-f6fa-11ec-9f77-64bc58900b78'}\n" + ] + } + ], + "source": [ + "response = container.modify({\n", + " \"action\": \"START\"\n", + "}, container_id)\n", + "time.sleep(5)\n", + "print(response)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## 7. Create a database within the mariadb container" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 27, + "outputs": [], + "source": [ + "response = database.create({\n", + " \"name\": \"MIR \" + str(uuid.uuid1()),\n", + " \"description\": \"Music Information Retrieval\",\n", + " \"is_public\": True\n", + "}, container_id)\n", + "database_id = response.id" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## 8. Import the feature .csv\n", + "\n", + "Now open [http://localhost:3000/](http://localhost:3000/) and import the .csv file by clicking the database. After successful creation of the table, come back here." + ], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/.invenio/requirements.txt b/.invenio/requirements.txt index 47f78a7d88..cead3141b6 100644 --- a/.invenio/requirements.txt +++ b/.invenio/requirements.txt @@ -1,2 +1 @@ -invenio-client==0.1.0 -six==1.16.0 \ No newline at end of file +requests==2.28.0 \ No newline at end of file diff --git a/fda-document-service/rest-service/src/main/java/at/tuwien/endpoints/DocumentEndpoint.java b/fda-document-service/rest-service/src/main/java/at/tuwien/endpoints/DocumentEndpoint.java index 2436b9748b..395e77b5a3 100644 --- a/fda-document-service/rest-service/src/main/java/at/tuwien/endpoints/DocumentEndpoint.java +++ b/fda-document-service/rest-service/src/main/java/at/tuwien/endpoints/DocumentEndpoint.java @@ -1,7 +1,7 @@ package at.tuwien.endpoints; import at.tuwien.api.document.record.CreateDraftDto; -import at.tuwien.api.document.record.DraftDto; +import at.tuwien.api.document.record.RecordDto; import at.tuwien.exception.DraftRecordCreateException; import at.tuwien.service.DocumentService; import io.swagger.v3.oas.annotations.Operation; @@ -20,9 +20,8 @@ import java.security.Principal; @Log4j2 -@RestController @CrossOrigin(origins = "*") -@ControllerAdvice +@RestController @RequestMapping("/api/document") public class DocumentEndpoint { @@ -37,9 +36,9 @@ public class DocumentEndpoint { @PreAuthorize("hasRole('ROLE_RESEARCHER')") @Transactional(readOnly = true) @Operation(summary = "Create a draft", security = @SecurityRequirement(name = "bearerAuth")) - public ResponseEntity<DraftDto> create(@NotNull @Valid @RequestBody CreateDraftDto data, + public ResponseEntity<RecordDto> create(@NotNull @Valid @RequestBody CreateDraftDto data, @NotNull Principal principal) throws DraftRecordCreateException { - final DraftDto document = documentService.create(data, principal); + final RecordDto document = documentService.create(data, principal); return ResponseEntity.status(HttpStatus.CREATED) .body(document); } @@ -48,22 +47,33 @@ public class DocumentEndpoint { @PreAuthorize("hasRole('ROLE_RESEARCHER')") @Transactional(readOnly = true) @Operation(summary = "Find a draft", security = @SecurityRequirement(name = "bearerAuth")) - public ResponseEntity<DraftDto> find(@NotNull @PathVariable("id") String documentId, + public ResponseEntity<RecordDto> find(@NotNull @PathVariable("id") String documentId, @NotNull Principal principal) throws DraftRecordCreateException { - final DraftDto document = documentService.findById(documentId, principal); + final RecordDto document = documentService.findById(documentId, principal); log.info("Found draft record with id {}", documentId); log.debug("found draft record {}", document); return ResponseEntity.status(HttpStatus.OK) .body(document); } + @PutMapping("/{id}/publish") + @PreAuthorize("hasRole('ROLE_RESEARCHER')") + @Transactional(readOnly = true) + @Operation(summary = "Publish a draft", security = @SecurityRequirement(name = "bearerAuth")) + public ResponseEntity<RecordDto> publish(@NotNull @PathVariable("id") String documentId, + @NotNull Principal principal) throws DraftRecordCreateException { + final RecordDto document = documentService.publish(documentId, principal); + return ResponseEntity.status(HttpStatus.ACCEPTED) + .body(document); + } + @PostMapping("/{id}") @PreAuthorize("hasRole('ROLE_RESEARCHER')") @Transactional(readOnly = true) @Operation(summary = "Reserve draft DOI", security = @SecurityRequirement(name = "bearerAuth")) - public ResponseEntity<DraftDto> reserve(@NotNull @PathVariable("id") String documentId, + public ResponseEntity<RecordDto> reserve(@NotNull @PathVariable("id") String documentId, @NotNull Principal principal) throws DraftRecordCreateException { - final DraftDto document = documentService.reserveDoi(documentId, principal); + final RecordDto document = documentService.reserveDoi(documentId, principal); return ResponseEntity.status(HttpStatus.CREATED) .body(document); } diff --git a/fda-document-service/rest-service/src/main/java/at/tuwien/endpoints/FileEndpoint.java b/fda-document-service/rest-service/src/main/java/at/tuwien/endpoints/FileEndpoint.java index 5457e3c67a..402d37ed3b 100644 --- a/fda-document-service/rest-service/src/main/java/at/tuwien/endpoints/FileEndpoint.java +++ b/fda-document-service/rest-service/src/main/java/at/tuwien/endpoints/FileEndpoint.java @@ -1,6 +1,8 @@ package at.tuwien.endpoints; -import at.tuwien.api.document.file.FileStartDto; +import at.tuwien.api.document.file.FileDto; +import at.tuwien.exception.FileUploadException; +import at.tuwien.exception.CommitFileUploadException; import at.tuwien.exception.DraftRecordCreateException; import at.tuwien.service.FileService; import io.swagger.v3.oas.annotations.Operation; @@ -12,6 +14,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import javax.validation.constraints.NotNull; import java.security.Principal; @@ -20,7 +23,6 @@ import java.security.Principal; @Log4j2 @RestController @CrossOrigin(origins = "*") -@ControllerAdvice @RequestMapping("/api/document/{id}/file") public class FileEndpoint { @@ -34,11 +36,14 @@ public class FileEndpoint { @PostMapping @PreAuthorize("hasRole('ROLE_RESEARCHER')") @Transactional(readOnly = true) - @Operation(summary = "Start draft files", security = @SecurityRequirement(name = "bearerAuth")) - public ResponseEntity<FileStartDto> start(@NotNull @PathVariable("id") String documentId, - @NotNull Principal principal) throws DraftRecordCreateException { - final FileStartDto document = fileService.start(documentId, principal); - return ResponseEntity.status(HttpStatus.CREATED) + @Operation(summary = "Upload file", security = @SecurityRequirement(name = "bearerAuth")) + public ResponseEntity<FileDto> uploadFile(@NotNull @PathVariable("id") String documentId, + @NotNull @RequestParam("file") MultipartFile file, + @NotNull Principal principal) + throws DraftRecordCreateException, CommitFileUploadException, FileUploadException, + org.apache.tomcat.util.http.fileupload.FileUploadException { + final FileDto document = fileService.uploadFile(documentId, file, principal); + return ResponseEntity.status(HttpStatus.ACCEPTED) .body(document); } diff --git a/fda-document-service/rest-service/src/main/resources/application-docker.yml b/fda-document-service/rest-service/src/main/resources/application-docker.yml index 46f56dc08b..6cc0944799 100644 --- a/fda-document-service/rest-service/src/main/resources/application-docker.yml +++ b/fda-document-service/rest-service/src/main/resources/application-docker.yml @@ -30,5 +30,6 @@ eureka: fda: mount.path: /tmp ready.path: /ready + gateway.endpoint: http://fda-gateway-service:9095 document.endpoint: https://test.researchdata.tuwien.ac.at dev.token: "${TOKEN}" \ No newline at end of file diff --git a/fda-document-service/rest-service/src/main/resources/application.yml b/fda-document-service/rest-service/src/main/resources/application.yml index 47db505983..bf035c9f11 100644 --- a/fda-document-service/rest-service/src/main/resources/application.yml +++ b/fda-document-service/rest-service/src/main/resources/application.yml @@ -7,7 +7,7 @@ spring: username: postgres password: postgres jpa: - show-sql: false + show-sql: true database-platform: org.hibernate.dialect.PostgreSQLDialect hibernate: ddl-auto: validate @@ -16,7 +16,6 @@ spring: name: fda-document-service cloud: loadbalancer.ribbon.enabled: false -springdoc.swagger-ui.enabled: true server.port: 9099 logging: pattern.console: "%d %highlight(%-5level) %msg%n" @@ -31,5 +30,6 @@ eureka: fda: mount.path: /tmp ready.path: ./ready + gateway.endpoint: http://localhost:9095 document.endpoint: https://test.researchdata.tuwien.ac.at dev.token: "${TOKEN}" \ No newline at end of file diff --git a/fda-document-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java b/fda-document-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java index 7f4f0cc7eb..c772a6be5d 100644 --- a/fda-document-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java +++ b/fda-document-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java @@ -2,6 +2,8 @@ package at.tuwien; import at.tuwien.api.document.metadata.*; import at.tuwien.api.document.record.*; +import at.tuwien.api.user.UserDetailsDto; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.test.context.TestPropertySource; import java.time.Instant; @@ -11,30 +13,58 @@ import java.util.List; @TestPropertySource(locations = "classpath:application.properties") public abstract class BaseUnitTest { + public final static Long USER_1_ID = 1L; public final static String USER_1_USERNAME = "junit"; + public final static UserDetails USER_1_DETAILS = UserDetailsDto.builder() + .id(USER_1_ID) + .username(USER_1_USERNAME) + .build(); + public final static AccessTypeDto DOCUMENT_1_RECORD_TYPE = AccessTypeDto.PUBLIC; public final static FileTypeDto DOCUMENT_1_FILE_TYPE = FileTypeDto.PUBLIC; + public final static AccessTypeDto DOCUMENT_2_RECORD_TYPE = AccessTypeDto.PUBLIC; + public final static FileTypeDto DOCUMENT_2_FILE_TYPE = FileTypeDto.RESTRICTED; + public final static AccessOptionsDto DOCUMENT_1_ACCESS_OPTIONS = AccessOptionsDto.builder() .record(DOCUMENT_1_RECORD_TYPE) .files(DOCUMENT_1_FILE_TYPE) .build(); - public final static Boolean DOCUMENT_1_FILES_ENABLED = true; + public final static AccessOptionsDto DOCUMENT_2_ACCESS_OPTIONS = AccessOptionsDto.builder() + .record(DOCUMENT_2_RECORD_TYPE) + .files(DOCUMENT_2_FILE_TYPE) + .build(); + + public final static Boolean DOCUMENT_1_FILES_ENABLED = false; + + public final static Boolean DOCUMENT_2_FILES_ENABLED = true; public final static FilesOptionsDto DOCUMENT_1_FILES_OPTIONS = FilesOptionsDto.builder() .enabled(DOCUMENT_1_FILES_ENABLED) .build(); - public final static String DOCUMENT_1_TITLE = "Test Draft"; + public final static FilesOptionsDto DOCUMENT_2_FILES_OPTIONS = FilesOptionsDto.builder() + .enabled(DOCUMENT_2_FILES_ENABLED) + .build(); + + public final static String DOCUMENT_1_TITLE = "Public Test-Record"; public final static String DOCUMENT_1_RESOURCE_TYPE_TYPE = "other"; public final static Date DOCUMENT_1_PUBLICATION_DATE = Date.from(Instant.now()); + public final static String DOCUMENT_2_TITLE = "Restricted Test-Record"; + public final static String DOCUMENT_2_RESOURCE_TYPE_TYPE = "other"; + public final static Date DOCUMENT_2_PUBLICATION_DATE = Date.from(Instant.now()); + public final static ResourceTypeDto DOCUMENT_1_RESOURCE_TYPE = ResourceTypeDto.builder() .id(DOCUMENT_1_RESOURCE_TYPE_TYPE) .build(); + public final static ResourceTypeDto DOCUMENT_2_RESOURCE_TYPE = ResourceTypeDto.builder() + .id(DOCUMENT_2_RESOURCE_TYPE_TYPE) + .build(); + public final static String IDENTIFIER_1_IDENTIFIER = "0000-0003-4216-302X"; public final static IdentifierTypeDto IDENTIFIER_1_TYPE = IdentifierTypeDto.ORCID; @@ -72,10 +102,23 @@ public abstract class BaseUnitTest { .creators(List.of(CREATOR_1)) .build(); + public final static MetadataDto DOCUMENT_2_METADATA = MetadataDto.builder() + .title(DOCUMENT_2_TITLE) + .resourceType(DOCUMENT_2_RESOURCE_TYPE) + .publicationDate(DOCUMENT_2_PUBLICATION_DATE) + .creators(List.of(CREATOR_1)) + .build(); + public final static CreateDraftDto DOCUMENT_1_CREATE_DRAFT = CreateDraftDto.builder() .access(DOCUMENT_1_ACCESS_OPTIONS) .files(DOCUMENT_1_FILES_OPTIONS) .metadata(DOCUMENT_1_METADATA) .build(); + public final static CreateDraftDto DOCUMENT_2_CREATE_DRAFT = CreateDraftDto.builder() + .access(DOCUMENT_2_ACCESS_OPTIONS) + .files(DOCUMENT_2_FILES_OPTIONS) + .metadata(DOCUMENT_2_METADATA) + .build(); + } diff --git a/fda-document-service/rest-service/src/test/java/at/tuwien/endpoint/DocumentEndpointUnitTest.java b/fda-document-service/rest-service/src/test/java/at/tuwien/endpoint/DocumentEndpointUnitTest.java index 0965a30b1e..534038dbad 100644 --- a/fda-document-service/rest-service/src/test/java/at/tuwien/endpoint/DocumentEndpointUnitTest.java +++ b/fda-document-service/rest-service/src/test/java/at/tuwien/endpoint/DocumentEndpointUnitTest.java @@ -1,13 +1,52 @@ package at.tuwien.endpoint; import at.tuwien.BaseUnitTest; +import at.tuwien.api.document.record.CreateDraftDto; +import at.tuwien.api.document.record.RecordDto; +import at.tuwien.endpoints.DocumentEndpoint; +import at.tuwien.exception.DraftRecordCreateException; +import at.tuwien.gateway.AuthenticationServiceGateway; +import org.apache.http.auth.BasicUserPrincipal; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.security.Principal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + @ExtendWith(SpringExtension.class) @SpringBootTest public class DocumentEndpointUnitTest extends BaseUnitTest { + @Autowired + private DocumentEndpoint documentEndpoint; + + @MockBean + private AuthenticationServiceGateway authenticationServiceGateway; + + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"ROLE_RESEARCHER"}) + public void create_succeed() throws DraftRecordCreateException { + final CreateDraftDto request = DOCUMENT_1_CREATE_DRAFT; + final Principal principal = new BasicUserPrincipal(USER_1_USERNAME); + + /* mock */ + when(authenticationServiceGateway.validate(anyString())) + .thenReturn(USER_1_DETAILS); + + /* test */ + final ResponseEntity<RecordDto> response = documentEndpoint.create(request, principal); + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + } } diff --git a/fda-document-service/rest-service/src/test/java/at/tuwien/service/DocumentServiceIntegrationTest.java b/fda-document-service/rest-service/src/test/java/at/tuwien/service/DocumentServiceIntegrationTest.java index 055fd45ca2..cdef81d0dc 100644 --- a/fda-document-service/rest-service/src/test/java/at/tuwien/service/DocumentServiceIntegrationTest.java +++ b/fda-document-service/rest-service/src/test/java/at/tuwien/service/DocumentServiceIntegrationTest.java @@ -2,7 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.api.document.record.CreateDraftDto; -import at.tuwien.api.document.record.DraftDto; +import at.tuwien.api.document.record.RecordDto; import at.tuwien.exception.DraftRecordCreateException; import lombok.extern.log4j.Log4j2; import org.apache.http.auth.BasicUserPrincipal; @@ -35,7 +35,7 @@ public class DocumentServiceIntegrationTest extends BaseUnitTest { /* mock */ /* test */ - final DraftDto response = documentService.create(request, principal); + final RecordDto response = documentService.create(request, principal); assertEquals(DOCUMENT_1_TITLE, response.getMetadata().getTitle()); } @@ -47,8 +47,8 @@ public class DocumentServiceIntegrationTest extends BaseUnitTest { /* mock */ /* test */ - final DraftDto document = documentService.create(request, principal); - final DraftDto response = documentService.reserveDoi(document.getId(), principal); + final RecordDto document = documentService.create(request, principal); + final RecordDto response = documentService.reserveDoi(document.getId(), principal); assertNotNull(response.getPids().getDoi()); } diff --git a/fda-document-service/rest-service/src/test/java/at/tuwien/service/FileServiceIntegrationTest.java b/fda-document-service/rest-service/src/test/java/at/tuwien/service/FileServiceIntegrationTest.java index 9172889617..b694b019aa 100644 --- a/fda-document-service/rest-service/src/test/java/at/tuwien/service/FileServiceIntegrationTest.java +++ b/fda-document-service/rest-service/src/test/java/at/tuwien/service/FileServiceIntegrationTest.java @@ -1,21 +1,31 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; -import at.tuwien.api.document.file.FileStartDto; +import at.tuwien.api.document.file.FileDto; import at.tuwien.api.document.record.CreateDraftDto; -import at.tuwien.api.document.record.DraftDto; +import at.tuwien.api.document.record.RecordDto; +import at.tuwien.exception.FileUploadException; +import at.tuwien.exception.CommitFileUploadException; import at.tuwien.exception.DraftRecordCreateException; import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.FileUtils; import org.apache.http.auth.BasicUserPrincipal; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.multipart.MultipartFile; +import java.io.File; +import java.io.IOException; import java.security.Principal; +import static org.junit.jupiter.api.Assertions.*; + + @Log4j2 @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @ExtendWith(SpringExtension.class) @@ -29,15 +39,35 @@ public class FileServiceIntegrationTest extends BaseUnitTest { private FileService fileService; @Test - public void start_succeeds() throws DraftRecordCreateException { + public void upload_succeeds() + throws DraftRecordCreateException, IOException, CommitFileUploadException, FileUploadException { + final CreateDraftDto request = DOCUMENT_2_CREATE_DRAFT; + final Principal principal = new BasicUserPrincipal(USER_1_USERNAME); + final File mockFile = new File("src/test/resources/images/mock.png"); + + /* mock */ + final MultipartFile file = new MockMultipartFile(mockFile.getName(), FileUtils.openInputStream(mockFile) + .readAllBytes()); + final RecordDto document = documentService.create(request, principal); + assertFalse(document.getIsPublished()); + + /* test */ + final FileDto response = fileService.uploadFile(document.getId(), file, principal); + assertEquals(file.getName(), response.getKey()); + } + @Test + public void publish_succeeds() + throws DraftRecordCreateException { final CreateDraftDto request = DOCUMENT_1_CREATE_DRAFT; final Principal principal = new BasicUserPrincipal(USER_1_USERNAME); /* mock */ - final DraftDto document = documentService.create(request, principal); + final RecordDto document = documentService.create(request, principal); /* test */ - final FileStartDto response = fileService.start(document.getId(), principal); + final RecordDto response = documentService.publish(document.getId(), principal); + assertTrue(response.getIsPublished()); + assertNotNull(response.getPids().getDoi()); } } diff --git a/fda-document-service/rest-service/src/test/resources/images/mock.png b/fda-document-service/rest-service/src/test/resources/images/mock.png new file mode 100644 index 0000000000000000000000000000000000000000..4094aa6f22d95544339040f01f4f3d19f587d1d3 GIT binary patch literal 11666 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 E0DWw>p8x;= literal 0 HcmV?d00001 diff --git a/fda-document-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-document-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java index e33f0dba4d..4d819a1786 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java +++ b/fda-document-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -55,4 +55,4 @@ public class AuthTokenFilter extends OncePerRequestFilter { } return null; } -} \ No newline at end of file +} diff --git a/fda-document-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/fda-document-service/services/src/main/java/at/tuwien/config/GatewayConfig.java index 006541ad8c..776e4aa5d8 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/config/GatewayConfig.java +++ b/fda-document-service/services/src/main/java/at/tuwien/config/GatewayConfig.java @@ -9,11 +9,21 @@ import org.springframework.web.util.DefaultUriBuilderFactory; @Configuration public class GatewayConfig { + @Value("${fda.gateway.endpoint}") + private String gatewayEndpoint; + @Value("${fda.document.endpoint}") private String documentEndpoint; @Bean - public RestTemplate restTemplate() { + public RestTemplate gatewayRestTemplate() { + final RestTemplate restTemplate = new RestTemplate(); + restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(gatewayEndpoint)); + return restTemplate; + } + + @Bean + public RestTemplate documentRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(documentEndpoint)); return restTemplate; diff --git a/fda-document-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-document-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 70dac68fcd..472089fc21 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/fda-document-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -65,7 +65,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { /* set permissions on endpoints */ http.authorizeRequests() /* our public endpoints */ - .antMatchers(HttpMethod.GET, "/api/document/**").permitAll() .antMatchers("/v3/api-docs.yaml", "/v3/api-docs/**", "/swagger-ui/**", diff --git a/fda-document-service/services/src/main/java/at/tuwien/exception/CommitFileUploadException.java b/fda-document-service/services/src/main/java/at/tuwien/exception/CommitFileUploadException.java new file mode 100644 index 0000000000..430f7bba54 --- /dev/null +++ b/fda-document-service/services/src/main/java/at/tuwien/exception/CommitFileUploadException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.CONFLICT) +public class CommitFileUploadException extends Exception { + + public CommitFileUploadException(String msg) { + super(msg); + } + + public CommitFileUploadException(String msg, Throwable thr) { + super(msg, thr); + } + + public CommitFileUploadException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-document-service/services/src/main/java/at/tuwien/exception/FileUploadException.java b/fda-document-service/services/src/main/java/at/tuwien/exception/FileUploadException.java new file mode 100644 index 0000000000..590060826f --- /dev/null +++ b/fda-document-service/services/src/main/java/at/tuwien/exception/FileUploadException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.FAILED_DEPENDENCY) +public class FileUploadException extends Exception { + + public FileUploadException(String msg) { + super(msg); + } + + public FileUploadException(String msg, Throwable thr) { + super(msg, thr); + } + + public FileUploadException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-document-service/services/src/main/java/at/tuwien/gateway/DocumentGateway.java b/fda-document-service/services/src/main/java/at/tuwien/gateway/DocumentGateway.java index fe6a27d6d5..36d86d6d02 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/gateway/DocumentGateway.java +++ b/fda-document-service/services/src/main/java/at/tuwien/gateway/DocumentGateway.java @@ -1,18 +1,26 @@ package at.tuwien.gateway; -import at.tuwien.api.document.file.FileStartDto; +import at.tuwien.api.document.file.FileDto; import at.tuwien.api.document.record.CreateDraftDto; -import at.tuwien.api.document.record.DraftDto; +import at.tuwien.api.document.record.RecordDto; +import at.tuwien.exception.FileUploadException; +import at.tuwien.exception.CommitFileUploadException; import at.tuwien.exception.DraftRecordCreateException; +import org.springframework.web.multipart.MultipartFile; public interface DocumentGateway { - DraftDto createDraft(CreateDraftDto data, String token) throws DraftRecordCreateException; + RecordDto createDraft(CreateDraftDto data, String token) throws DraftRecordCreateException; - DraftDto reserveDraftDoi(String id, String token) throws DraftRecordCreateException; + RecordDto reserveDraftDoi(String id, String token) throws DraftRecordCreateException; - DraftDto findDraft(String id, String token) throws DraftRecordCreateException; + RecordDto findDraft(String id, String token) throws DraftRecordCreateException; - FileStartDto startUpload(String id, String token) throws DraftRecordCreateException; + RecordDto publishDraft(String id, String token) throws DraftRecordCreateException; + + FileDto uploadFile(String id, MultipartFile file, String token) + throws DraftRecordCreateException, FileUploadException, + org.apache.tomcat.util.http.fileupload.FileUploadException, + CommitFileUploadException; void delete(String id, String token) throws DraftRecordCreateException; } diff --git a/fda-document-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java b/fda-document-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java index 56d691a6c8..5492233420 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java +++ b/fda-document-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java @@ -18,20 +18,20 @@ import org.springframework.web.client.RestTemplate; public class AuthenticationServiceGatewayImpl implements AuthenticationServiceGateway { private final UserMapper userMapper; - private final RestTemplate restTemplate; + private final RestTemplate gatewayRestTemplate; @Autowired - public AuthenticationServiceGatewayImpl(UserMapper userMapper, RestTemplate restTemplate) { + public AuthenticationServiceGatewayImpl(UserMapper userMapper, RestTemplate gatewayRestTemplate) { this.userMapper = userMapper; - this.restTemplate = restTemplate; + this.gatewayRestTemplate = gatewayRestTemplate; } @Override public UserDetails validate(String token) { final HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + token); - final ResponseEntity<UserDto> response = restTemplate.exchange("/api/auth", HttpMethod.PUT, - new HttpEntity<>("", headers), UserDto.class); + final ResponseEntity<UserDto> response = gatewayRestTemplate.exchange("/api/auth", HttpMethod.PUT, + new HttpEntity<>(null, headers), UserDto.class); return userMapper.userDtoToUserDetailsDto(response.getBody()); } diff --git a/fda-document-service/services/src/main/java/at/tuwien/gateway/impl/InvenioDocumentGatewayImpl.java b/fda-document-service/services/src/main/java/at/tuwien/gateway/impl/InvenioDocumentGatewayImpl.java index 318594ee12..bca9d5b863 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/gateway/impl/InvenioDocumentGatewayImpl.java +++ b/fda-document-service/services/src/main/java/at/tuwien/gateway/impl/InvenioDocumentGatewayImpl.java @@ -1,36 +1,49 @@ package at.tuwien.gateway.impl; -import at.tuwien.api.document.file.FileStartDto; +import at.tuwien.api.document.file.FileAnnounceDto; +import at.tuwien.api.document.file.FileDto; +import at.tuwien.api.document.file.FileKeyDto; import at.tuwien.api.document.record.CreateDraftDto; -import at.tuwien.api.document.record.DraftDto; +import at.tuwien.api.document.record.RecordDto; +import at.tuwien.exception.FileUploadException; +import at.tuwien.exception.CommitFileUploadException; import at.tuwien.exception.DraftRecordCreateException; import at.tuwien.gateway.DocumentGateway; +import at.tuwien.mapper.DocumentMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; @Slf4j @Component public class InvenioDocumentGatewayImpl implements DocumentGateway { - private RestTemplate restTemplate; + private final static String OCTET_STREAM = "application/octet-stream"; + + private final DocumentMapper documentMapper; + private final RestTemplate documentRestTemplate; @Autowired - public InvenioDocumentGatewayImpl(RestTemplate restTemplate) { - this.restTemplate = restTemplate; + public InvenioDocumentGatewayImpl(DocumentMapper documentMapper, RestTemplate documentRestTemplate) { + this.documentMapper = documentMapper; + this.documentRestTemplate = documentRestTemplate; } @Override - public DraftDto createDraft(CreateDraftDto data, String token) throws DraftRecordCreateException { + public RecordDto createDraft(CreateDraftDto data, String token) throws DraftRecordCreateException { log.trace("sending {}", data); final String url = "/api/records"; - final ResponseEntity<DraftDto> response; + final ResponseEntity<RecordDto> response; try { - response = restTemplate.exchange(url, HttpMethod.POST, - new HttpEntity<>(data, headers(token)), DraftDto.class); + response = documentRestTemplate.exchange(url, HttpMethod.POST, + new HttpEntity<>(data, headers(token)), RecordDto.class); } catch (HttpClientErrorException.BadRequest e) { log.error("Failed to create draft record"); throw new DraftRecordCreateException("Failed to create draft record", e); @@ -39,12 +52,12 @@ public class InvenioDocumentGatewayImpl implements DocumentGateway { } @Override - public DraftDto reserveDraftDoi(String id, String token) throws DraftRecordCreateException { + public RecordDto reserveDraftDoi(String id, String token) throws DraftRecordCreateException { final String url = "/api/records/" + id + "/draft/pids/doi"; - final ResponseEntity<DraftDto> response; + final ResponseEntity<RecordDto> response; try { - response = restTemplate.exchange(url, HttpMethod.POST, - new HttpEntity<>(null, headers(token)), DraftDto.class); + response = documentRestTemplate.exchange(url, HttpMethod.POST, + new HttpEntity<>(null, headers(token)), RecordDto.class); } catch (HttpClientErrorException.BadRequest e) { log.error("Failed to reserve draft doi"); throw new DraftRecordCreateException("Failed to reserve draft doi", e); @@ -53,12 +66,12 @@ public class InvenioDocumentGatewayImpl implements DocumentGateway { } @Override - public DraftDto findDraft(String id, String token) throws DraftRecordCreateException { + public RecordDto findDraft(String id, String token) throws DraftRecordCreateException { final String url = "/api/records/" + id + "/draft"; - final ResponseEntity<DraftDto> response; + final ResponseEntity<RecordDto> response; try { - response = restTemplate.exchange(url, HttpMethod.GET, - new HttpEntity<>(null, headers(token)), DraftDto.class); + response = documentRestTemplate.exchange(url, HttpMethod.GET, + new HttpEntity<>(null, headers(token)), RecordDto.class); } catch (HttpClientErrorException.BadRequest e) { log.error("Failed to find draft record"); throw new DraftRecordCreateException("Failed to create find record", e); @@ -67,24 +80,76 @@ public class InvenioDocumentGatewayImpl implements DocumentGateway { } @Override - public FileStartDto startUpload(String id, String token) throws DraftRecordCreateException { - final String url = "/api/records/" + id + "/draft/files"; - final ResponseEntity<FileStartDto> response; + public RecordDto publishDraft(String id, String token) throws DraftRecordCreateException { + final String url = "/api/records/" + id + "/draft/actions/publish"; + final ResponseEntity<RecordDto> response; try { - response = restTemplate.exchange(url, HttpMethod.POST, - new HttpEntity<>(null, headers(token)), FileStartDto.class); + response = documentRestTemplate.exchange(url, HttpMethod.POST, + new HttpEntity<>(null, headers(token)), RecordDto.class); } catch (HttpClientErrorException.BadRequest e) { - log.error("Failed to start draft files"); - throw new DraftRecordCreateException("Failed to start draft files", e); + log.error("Failed to publish draft record"); + throw new DraftRecordCreateException("Failed to publish find record", e); } return response.getBody(); } + @Override + public FileDto uploadFile(String id, MultipartFile file, String token) + throws FileUploadException { + /* announce */ + final String url1 = "/api/records/" + id + "/draft/files"; + final List<FileKeyDto> files = List.of(documentMapper.stringToFileKeyDto(file.getName())); + final ResponseEntity<FileAnnounceDto> response1; + try { + response1 = documentRestTemplate.exchange(url1, HttpMethod.POST, + new HttpEntity<>(files, headers(token)), FileAnnounceDto.class); + } catch (HttpClientErrorException.BadRequest e) { + log.error("Failed to announce draft file"); + throw new FileUploadException("Failed to announce draft file", e); + } + if (response1.getStatusCode() != HttpStatus.CREATED) { + log.error("Failed to announce file upload"); + throw new FileUploadException("Failed to announce file upload"); + } + /* upload */ + final String url2 = "/api/records/" + id + "/draft/files/" + file.getName() + "/content"; + final ResponseEntity<Void> response2; + try { + response2 = documentRestTemplate.exchange(url2, HttpMethod.PUT, + new HttpEntity<>(file.getBytes(), headers(token, OCTET_STREAM)), Void.class); + } catch (HttpClientErrorException.BadRequest e) { + log.error("Failed to upload draft file"); + throw new FileUploadException("Failed to upload draft file", e); + } catch (IOException e) { + log.error("Failed to get draft file bytes"); + throw new FileUploadException("Failed to get draft file bytes", e); + } + if (response2.getStatusCode() != HttpStatus.OK) { + log.error("Failed to upload file"); + throw new FileUploadException("Failed to upload file"); + } + /* commit */ + final String url3 = "/api/records/" + id + "/draft/files/" + file.getName() + "/commit"; + final ResponseEntity<FileDto> response3; + try { + response3 = documentRestTemplate.exchange(url3, HttpMethod.POST, + new HttpEntity<>(null, headers(token)), FileDto.class); + } catch (HttpClientErrorException.BadRequest e) { + log.error("Failed to commit draft file"); + throw new FileUploadException("Failed to commit draft file", e); + } + if (response3.getStatusCode() != HttpStatus.OK) { + log.error("Failed to commit file"); + throw new FileUploadException("Failed to commit file"); + } + return response3.getBody(); + } + @Override public void delete(String id, String token) throws DraftRecordCreateException { final String url = "/api/records" + id + "/draft"; try { - restTemplate.exchange(url, HttpMethod.DELETE, + documentRestTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(null, headers(token)), Void.class); } catch (HttpClientErrorException.BadRequest e) { log.error("Failed to delete draft record"); @@ -99,9 +164,20 @@ public class InvenioDocumentGatewayImpl implements DocumentGateway { * @return The headers. */ private HttpHeaders headers(String token) { + return headers(token, "application/json"); + } + + /** + * Prepares the headers for all requests to authorize with the bearer token and content type. + * + * @param token The token. + * @param contentType The content type. + * @return The headers. + */ + private HttpHeaders headers(String token, String contentType) { final HttpHeaders headers = new HttpHeaders(); headers.add("Authorization", "Bearer " + token); - headers.add("Content-Type", "application/json"); + headers.add("Content-Type", contentType); return headers; } } diff --git a/fda-document-service/services/src/main/java/at/tuwien/mapper/DocumentMapper.java b/fda-document-service/services/src/main/java/at/tuwien/mapper/DocumentMapper.java index 0132073b62..2457da7cec 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/mapper/DocumentMapper.java +++ b/fda-document-service/services/src/main/java/at/tuwien/mapper/DocumentMapper.java @@ -1,10 +1,16 @@ package at.tuwien.mapper; +import at.tuwien.api.document.file.FileKeyDto; import org.mapstruct.*; @Mapper(componentModel = "spring") public interface DocumentMapper { + default FileKeyDto stringToFileKeyDto(String data) { + return FileKeyDto.builder() + .key(data) + .build(); + } } diff --git a/fda-document-service/services/src/main/java/at/tuwien/service/DocumentService.java b/fda-document-service/services/src/main/java/at/tuwien/service/DocumentService.java index 5d102b335c..182feb2d5b 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/service/DocumentService.java +++ b/fda-document-service/services/src/main/java/at/tuwien/service/DocumentService.java @@ -1,18 +1,20 @@ package at.tuwien.service; import at.tuwien.api.document.record.CreateDraftDto; -import at.tuwien.api.document.record.DraftDto; +import at.tuwien.api.document.record.RecordDto; import at.tuwien.exception.DraftRecordCreateException; import java.security.Principal; public interface DocumentService { - DraftDto findById(String id, Principal principal) throws DraftRecordCreateException; + RecordDto findById(String id, Principal principal) throws DraftRecordCreateException; - DraftDto create(CreateDraftDto data, Principal principal) throws DraftRecordCreateException; + RecordDto create(CreateDraftDto data, Principal principal) throws DraftRecordCreateException; - DraftDto reserveDoi(String id, Principal principal) throws DraftRecordCreateException; + RecordDto publish(String id, Principal principal) throws DraftRecordCreateException; + + RecordDto reserveDoi(String id, Principal principal) throws DraftRecordCreateException; void delete(String id, Principal principal) throws DraftRecordCreateException; } diff --git a/fda-document-service/services/src/main/java/at/tuwien/service/FileService.java b/fda-document-service/services/src/main/java/at/tuwien/service/FileService.java index 5113be20f9..67fb8932b5 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/service/FileService.java +++ b/fda-document-service/services/src/main/java/at/tuwien/service/FileService.java @@ -1,11 +1,17 @@ package at.tuwien.service; -import at.tuwien.api.document.file.FileStartDto; +import at.tuwien.api.document.file.FileDto; +import at.tuwien.exception.FileUploadException; +import at.tuwien.exception.CommitFileUploadException; import at.tuwien.exception.DraftRecordCreateException; +import org.springframework.web.multipart.MultipartFile; import java.security.Principal; public interface FileService { - FileStartDto start(String id, Principal principal) throws DraftRecordCreateException; + + FileDto uploadFile(String id, MultipartFile file, Principal principal) + throws DraftRecordCreateException, CommitFileUploadException, FileUploadException, + org.apache.tomcat.util.http.fileupload.FileUploadException; } diff --git a/fda-document-service/services/src/main/java/at/tuwien/service/impl/InvenioDraftServiceImpl.java b/fda-document-service/services/src/main/java/at/tuwien/service/impl/InvenioDraftServiceImpl.java index 456fd6b540..06c038521a 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/service/impl/InvenioDraftServiceImpl.java +++ b/fda-document-service/services/src/main/java/at/tuwien/service/impl/InvenioDraftServiceImpl.java @@ -1,7 +1,7 @@ package at.tuwien.service.impl; import at.tuwien.api.document.record.CreateDraftDto; -import at.tuwien.api.document.record.DraftDto; +import at.tuwien.api.document.record.RecordDto; import at.tuwien.config.InvenioConfig; import at.tuwien.exception.DraftRecordCreateException; import at.tuwien.gateway.DocumentGateway; @@ -26,27 +26,37 @@ public class InvenioDraftServiceImpl implements DocumentService { } @Override - public DraftDto findById(String id, Principal principal) throws DraftRecordCreateException { + public RecordDto findById(String id, Principal principal) throws DraftRecordCreateException { /* get token */ /* remote */ return documentGateway.findDraft(id, invenioConfig.getDebugToken()); } @Override - public DraftDto create(CreateDraftDto data, Principal principal) throws DraftRecordCreateException { + public RecordDto create(CreateDraftDto data, Principal principal) throws DraftRecordCreateException { /* get token */ /* remote */ - final DraftDto document = documentGateway.createDraft(data, invenioConfig.getDebugToken()); + final RecordDto document = documentGateway.createDraft(data, invenioConfig.getDebugToken()); log.info("Created draft record with id {}", document.getId()); log.debug("created draft record {}", document); return document; } @Override - public DraftDto reserveDoi(String id, Principal principal) throws DraftRecordCreateException { + public RecordDto publish(String id, Principal principal) throws DraftRecordCreateException { /* get token */ /* remote */ - final DraftDto document = documentGateway.reserveDraftDoi(id, invenioConfig.getDebugToken()); + final RecordDto document = documentGateway.publishDraft(id, invenioConfig.getDebugToken()); + log.info("Published draft record with id {}", document.getId()); + log.debug("published draft record {}", document); + return document; + } + + @Override + public RecordDto reserveDoi(String id, Principal principal) throws DraftRecordCreateException { + /* get token */ + /* remote */ + final RecordDto document = documentGateway.reserveDraftDoi(id, invenioConfig.getDebugToken()); log.info("Reserved DOI {} for draft record with id {}", document.getPids().getDoi(), document.getId()); log.debug("reserved PID {} for draft record with id {}", document.getPids(), document); return document; diff --git a/fda-document-service/services/src/main/java/at/tuwien/service/impl/InvenioFileServiceImpl.java b/fda-document-service/services/src/main/java/at/tuwien/service/impl/InvenioFileServiceImpl.java index 459454f346..4110d07846 100644 --- a/fda-document-service/services/src/main/java/at/tuwien/service/impl/InvenioFileServiceImpl.java +++ b/fda-document-service/services/src/main/java/at/tuwien/service/impl/InvenioFileServiceImpl.java @@ -1,13 +1,16 @@ package at.tuwien.service.impl; -import at.tuwien.api.document.file.FileStartDto; +import at.tuwien.api.document.file.FileDto; import at.tuwien.config.InvenioConfig; +import at.tuwien.exception.FileUploadException; +import at.tuwien.exception.CommitFileUploadException; import at.tuwien.exception.DraftRecordCreateException; import at.tuwien.gateway.DocumentGateway; import at.tuwien.service.FileService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; import java.security.Principal; @@ -25,12 +28,14 @@ public class InvenioFileServiceImpl implements FileService { } @Override - public FileStartDto start(String id, Principal principal) throws DraftRecordCreateException { + public FileDto uploadFile(String id, MultipartFile file, Principal principal) + throws DraftRecordCreateException, CommitFileUploadException, FileUploadException, + org.apache.tomcat.util.http.fileupload.FileUploadException { /* get token */ /* remote */ - final FileStartDto document = documentGateway.startUpload(id, invenioConfig.getDebugToken()); - log.info("Started draft files with id {}", id); - log.debug("started draft files {}", document); + final FileDto document = documentGateway.uploadFile(id, file, invenioConfig.getDebugToken()); + log.info("Deposited draft file content for record with id {}", id); + log.debug("Deposited draft file content for record {}", document); return document; } diff --git a/fda-gateway-service/gateway/src/main/java/at/tuwien/gatewayservice/config/GatewayConfig.java b/fda-gateway-service/gateway/src/main/java/at/tuwien/gatewayservice/config/GatewayConfig.java index 040bbb0e96..27a3863e80 100644 --- a/fda-gateway-service/gateway/src/main/java/at/tuwien/gatewayservice/config/GatewayConfig.java +++ b/fda-gateway-service/gateway/src/main/java/at/tuwien/gatewayservice/config/GatewayConfig.java @@ -56,6 +56,11 @@ public class GatewayConfig { .method("POST", "GET", "PUT", "DELETE") .and() .uri("lb://fda-units-service")) + .route("fda-document-service", r -> r.path("/api/document/**") + .and() + .method("POST", "GET", "PUT", "DELETE") + .and() + .uri("lb://fda-document-service")) .build(); } diff --git a/fda-identifier-service/rest-service/src/main/java/at/tuwien/FdaIdentifierServiceApplication.java b/fda-identifier-service/rest-service/src/main/java/at/tuwien/FdaIdentifierServiceApplication.java index 185a41efc0..56712b0f03 100644 --- a/fda-identifier-service/rest-service/src/main/java/at/tuwien/FdaIdentifierServiceApplication.java +++ b/fda-identifier-service/rest-service/src/main/java/at/tuwien/FdaIdentifierServiceApplication.java @@ -8,7 +8,6 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.transaction.annotation.EnableTransactionManagement; - @SpringBootApplication @EnableJpaAuditing @EnableTransactionManagement diff --git a/fda-identifier-service/rest-service/src/main/resources/application.yml b/fda-identifier-service/rest-service/src/main/resources/application.yml index 11335fa111..e71ce69b3f 100644 --- a/fda-identifier-service/rest-service/src/main/resources/application.yml +++ b/fda-identifier-service/rest-service/src/main/resources/application.yml @@ -22,6 +22,7 @@ logging: level: root: warn at.tuwien.: debug + at.tuwien.gateway.: trace org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: debug eureka: instance.hostname: fda-identifier-service diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileAnnounceDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileAnnounceDto.java new file mode 100644 index 0000000000..c2941ccdec --- /dev/null +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileAnnounceDto.java @@ -0,0 +1,37 @@ + +package at.tuwien.api.document.file; + +import at.tuwien.api.document.links.LinksDto; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Getter +@Setter +@ToString +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FileAnnounceDto { + + @JsonProperty("default_preview") + @Parameter(name = "file name") + private String defaultPreview; + + @NotNull + @Parameter(name = "file enabled") + private Boolean enabled; + + @NotNull + @Parameter(name = "file entries") + private List<FileEntryDto> entries; + + @Parameter(name = "file links") + private LinksDto links; + +} diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileDto.java new file mode 100644 index 0000000000..63f820e372 --- /dev/null +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileDto.java @@ -0,0 +1,72 @@ +package at.tuwien.api.document.file; + +import at.tuwien.api.document.links.LinksDto; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.*; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.Instant; + + +@Getter +@Setter +@ToString +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FileDto { + + @NotBlank + @JsonProperty("bucket_id") + @Parameter(name = "bucket id", description = "Bucket id.") + private String bucketId; + + @NotBlank + @Parameter(name = "file checksum", description = "File checksum.", example = "md5:ef8fcf1f046bb24f1db1f1a376ddbfbb") + private String checksum; + + @NotNull + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX", timezone = "UTC+2") + @Parameter(name = "file creation timestamp") + private Instant created; + + @NotNull + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX", timezone = "UTC+2") + @Parameter(name = "file updated timestamp") + private Instant updated; + + @NotBlank + @JsonProperty("file_id") + @Parameter(name = "file id") + private String fileId; + + @NotBlank + @Parameter(name = "file key", example = "mock.png") + private String key; + + @NotNull + @Parameter(name = "file links") + private LinksDto links; + + @Parameter(name = "file mimetype") + private String mimetype; + + @Parameter(name = "file size") + private Long size; + + @Parameter(name = "file status") + private String status; + + @JsonProperty("storage_class") + @Parameter(name = "file storage class", example = "S") + private String storageClass; + + @JsonProperty("version_id") + @Parameter(name = "file version id") + private String versionId; +} diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileEntryDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileEntryDto.java new file mode 100644 index 0000000000..d4c7909d5a --- /dev/null +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileEntryDto.java @@ -0,0 +1,43 @@ + +package at.tuwien.api.document.file; + +import at.tuwien.api.document.links.LinksDto; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.*; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.Instant; + +@Getter +@Setter +@ToString +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FileEntryDto { + + @NotBlank + @Parameter(name = "file name", description = "Name of the file.") + private String key; + + @NotNull + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX", timezone = "UTC+2") + @Parameter(name = "file updated") + private Instant updated; + + @NotNull + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX", timezone = "UTC+2") + @Parameter(name = "file created") + private Instant created; + + @Parameter(name = "file status") + private String status; + + @Parameter(name = "file links") + private LinksDto links; + +} diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileKeyDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileKeyDto.java new file mode 100644 index 0000000000..79c7f3d7b7 --- /dev/null +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/document/file/FileKeyDto.java @@ -0,0 +1,23 @@ + +package at.tuwien.api.document.file; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.*; + +import javax.validation.constraints.NotBlank; + +@Getter +@Setter +@ToString +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FileKeyDto { + + @NotBlank + @Parameter(name = "file name", description = "Name of the file.", example = "mock.png") + private String key; + +} diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/document/record/DraftDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/document/record/RecordDto.java similarity index 98% rename from fda-metadata-db/api/src/main/java/at/tuwien/api/document/record/DraftDto.java rename to fda-metadata-db/api/src/main/java/at/tuwien/api/document/record/RecordDto.java index d754f22fce..0f41c523bb 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/document/record/DraftDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/document/record/RecordDto.java @@ -18,7 +18,7 @@ import java.time.Instant; @AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) -public class DraftDto { +public class RecordDto { @NotNull(message = "access is required") @Parameter(name = "access") diff --git a/fda-ui/server-middleware/index.js b/fda-ui/server-middleware/index.js index b74dab0892..819f649f24 100644 --- a/fda-ui/server-middleware/index.js +++ b/fda-ui/server-middleware/index.js @@ -25,6 +25,7 @@ app.post('/table_from_csv', upload.single('file'), async (req, res) => { // send path to analyse service let analysis + let json try { const analyseUrl = `${process.env.API}/api/analyse/determinedt` analysis = await fetch(analyseUrl, { @@ -35,9 +36,8 @@ app.post('/table_from_csv', upload.single('file'), async (req, res) => { console.error('data type determination failed', error) throw error }) - const json = await analysis.json() - analysis = JSON.parse(json) - if (!analysis.columns) { + json = await analysis.json() + if (!json.columns) { return res.json({ success: false, message: 'Columns array missing' }) } } catch (error) { @@ -47,7 +47,7 @@ app.post('/table_from_csv', upload.single('file'), async (req, res) => { // map messytables / CoMi's `determine_dt` column types to ours // e.g. "Integer" -> "NUMBER" - let entries = Object.entries(analysis.columns) + let entries = Object.entries(json.columns) entries = entries.map(([k, v]) => { if (colTypeMap[v]) { v = colTypeMap[v] -- GitLab