From 763e78e34d2d3ff16033a8ec9f5cc387adec3668 Mon Sep 17 00:00:00 2001 From: Martin Weise <martin.weise@tuwien.ac.at> Date: Thu, 30 Mar 2023 20:08:42 +0200 Subject: [PATCH] Updated env vars and added user service --- docker-compose.yml | 20 ++ .../java/at/tuwien/config/GatewayConfig.java | 5 + .../java/at/tuwien/entities/user/User.java | 3 + fda-ui/.env.example | 3 +- fda-ui/components/dialogs/Persist.vue | 9 +- fda-ui/config.js | 1 + fda-ui/nuxt.config.js | 5 +- .../database/_database_id/info.vue | 101 +++--- .../database/_database_id/settings.vue | 61 +++- fda-user-service/.gitignore | 44 +++ .../.mvn/wrapper/MavenWrapperDownloader.java | 118 +++++++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + fda-user-service/Dockerfile | 42 +++ fda-user-service/mvnw | 310 ++++++++++++++++++ fda-user-service/mvnw.cmd | 182 ++++++++++ fda-user-service/pom.xml | 288 ++++++++++++++++ fda-user-service/report/pom.xml | 56 ++++ fda-user-service/rest-service/pom.xml | 46 +++ .../at/tuwien/FdaUserServiceApplication.java | 25 ++ .../java/at/tuwien/config/SwaggerConfig.java | 46 +++ .../java/at/tuwien/endpoint/UserEndpoint.java | 58 ++++ .../tuwien/handlers/ApiExceptionHandler.java | 249 ++++++++++++++ .../src/main/resources/application-docker.yml | 40 +++ .../src/main/resources/application-local.yml | 40 +++ .../src/main/resources/application.yml | 40 +++ .../src/main/resources/config.properties | 0 .../main/resources/mariadb_hibernate.cfg.xml | 20 ++ .../src/test/resources/application.properties | 32 ++ .../src/test/resources/schema.sql | 25 ++ .../test/resources/sensor/1_querystore.sql | 75 +++++ .../src/test/resources/sensor/2_traffic.sql | 8 + .../test/resources/traffic/1_querystore.sql | 75 +++++ .../src/test/resources/traffic/2_traffic.sql | 43 +++ .../test/resources/weather/1_querystore.sql | 75 +++++ .../src/test/resources/weather/2_weather.sql | 55 ++++ .../src/test/resources/weather/location.csv | 2 + .../src/test/resources/zoo/1_querystore.sql | 75 +++++ .../src/test/resources/zoo/2_zoo.sql | 191 +++++++++++ fda-user-service/service_ready | 6 + fda-user-service/services/pom.xml | 16 + .../java/at/tuwien/auth/AuthTokenFilter.java | 100 ++++++ .../java/at/tuwien/config/JacksonConfig.java | 31 ++ .../java/at/tuwien/config/ReadyConfig.java | 25 ++ .../at/tuwien/config/WebSecurityConfig.java | 87 +++++ .../at/tuwien/exception/AmqpException.java | 21 ++ .../exception/ColumnParseException.java | 21 ++ .../exception/ContainerNotFoundException.java | 21 ++ .../DatabaseConnectionException.java | 23 ++ .../exception/DatabaseNotFoundException.java | 23 ++ .../exception/FileStorageException.java | 20 ++ .../IdentifierNotFoundException.java | 21 ++ .../exception/ImageNotSupportedException.java | 23 ++ .../tuwien/exception/NotAllowedException.java | 21 ++ .../tuwien/exception/PaginationException.java | 21 ++ .../QueryAlreadyPersistedException.java | 19 ++ .../exception/QueryMalformedException.java | 23 ++ .../exception/QueryNotFoundException.java | 21 ++ .../tuwien/exception/QueryStoreException.java | 19 ++ .../at/tuwien/exception/SortException.java | 21 ++ .../exception/TableMalformedException.java | 21 ++ .../exception/TableNotFoundException.java | 21 ++ .../exception/TupleDeleteException.java | 19 ++ .../exception/UserNotFoundException.java | 21 ++ .../exception/ViewMalformedException.java | 21 ++ .../exception/ViewNotFoundException.java | 21 ++ .../java/at/tuwien/mapper/UserMapper.java | 31 ++ .../tuwien/repository/jpa/UserRepository.java | 14 + .../java/at/tuwien/service/UserService.java | 39 +++ .../tuwien/service/impl/UserServiceImpl.java | 66 ++++ .../src/test/resources/application.properties | 14 + 71 files changed, 3276 insertions(+), 65 deletions(-) create mode 100644 fda-user-service/.gitignore create mode 100644 fda-user-service/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 fda-user-service/.mvn/wrapper/maven-wrapper.jar create mode 100644 fda-user-service/.mvn/wrapper/maven-wrapper.properties create mode 100644 fda-user-service/Dockerfile create mode 100755 fda-user-service/mvnw create mode 100644 fda-user-service/mvnw.cmd create mode 100644 fda-user-service/pom.xml create mode 100644 fda-user-service/report/pom.xml create mode 100644 fda-user-service/rest-service/pom.xml create mode 100644 fda-user-service/rest-service/src/main/java/at/tuwien/FdaUserServiceApplication.java create mode 100644 fda-user-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java create mode 100644 fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java create mode 100644 fda-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java create mode 100644 fda-user-service/rest-service/src/main/resources/application-docker.yml create mode 100644 fda-user-service/rest-service/src/main/resources/application-local.yml create mode 100644 fda-user-service/rest-service/src/main/resources/application.yml create mode 100644 fda-user-service/rest-service/src/main/resources/config.properties create mode 100644 fda-user-service/rest-service/src/main/resources/mariadb_hibernate.cfg.xml create mode 100644 fda-user-service/rest-service/src/test/resources/application.properties create mode 100644 fda-user-service/rest-service/src/test/resources/schema.sql create mode 100644 fda-user-service/rest-service/src/test/resources/sensor/1_querystore.sql create mode 100644 fda-user-service/rest-service/src/test/resources/sensor/2_traffic.sql create mode 100644 fda-user-service/rest-service/src/test/resources/traffic/1_querystore.sql create mode 100644 fda-user-service/rest-service/src/test/resources/traffic/2_traffic.sql create mode 100644 fda-user-service/rest-service/src/test/resources/weather/1_querystore.sql create mode 100644 fda-user-service/rest-service/src/test/resources/weather/2_weather.sql create mode 100644 fda-user-service/rest-service/src/test/resources/weather/location.csv create mode 100644 fda-user-service/rest-service/src/test/resources/zoo/1_querystore.sql create mode 100644 fda-user-service/rest-service/src/test/resources/zoo/2_zoo.sql create mode 100644 fda-user-service/service_ready create mode 100644 fda-user-service/services/pom.xml create mode 100644 fda-user-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/config/JacksonConfig.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/config/ReadyConfig.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/AmqpException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/ColumnParseException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseConnectionException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/FileStorageException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/IdentifierNotFoundException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/ImageNotSupportedException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/PaginationException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/QueryAlreadyPersistedException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/SortException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/TupleDeleteException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserRepository.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/service/UserService.java create mode 100644 fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java create mode 100644 fda-user-service/services/src/test/resources/application.properties diff --git a/docker-compose.yml b/docker-compose.yml index 2ec72e2df9..07f8df8fa9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -262,6 +262,26 @@ services: logging: driver: json-file + fda-user-service: + restart: on-failure + container_name: dbrepo-user-service + hostname: user-service + build: ./fda-user-service + image: fda-user-service + networks: + core: + ports: + - "9098:9098" + env_file: + - .env + depends_on: + fda-metadata-db: + condition: service_healthy + fda-authentication-service: + condition: service_healthy + logging: + driver: json-file + fda-semantics-service: restart: on-failure container_name: dbrepo-semantics-service diff --git a/fda-gateway-service/rest-service/src/main/java/at/tuwien/config/GatewayConfig.java b/fda-gateway-service/rest-service/src/main/java/at/tuwien/config/GatewayConfig.java index 937479e041..bb1450fde7 100644 --- a/fda-gateway-service/rest-service/src/main/java/at/tuwien/config/GatewayConfig.java +++ b/fda-gateway-service/rest-service/src/main/java/at/tuwien/config/GatewayConfig.java @@ -16,6 +16,11 @@ public class GatewayConfig { .method("POST", "GET", "PUT", "DELETE") .filters(f -> f.rewritePath("/api/auth/(?<segment>.*)", "/${segment}")) .uri("lb://authentication-service")) + .route("user-service", r -> r.path("/api/user/**") + .and() + .method("POST", "GET", "PUT", "DELETE") + .and() + .uri("lb://user-service")) .route("broker-service", r -> r.path("/api/broker/**") .and() .method("POST", "GET", "PUT", "DELETE") diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java index f394a01803..abd946bdb9 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java @@ -36,6 +36,9 @@ public class User { @Column(name = "last_name") private String lastname; + @Column(name = "realm id") + private String realmId; + @Column(unique = true, nullable = false) private String email; diff --git a/fda-ui/.env.example b/fda-ui/.env.example index 5a83ca9863..366635afbd 100644 --- a/fda-ui/.env.example +++ b/fda-ui/.env.example @@ -7,4 +7,5 @@ BROKER_USERNAME=fda BROKER_PASSWORD=fda SANDBOX=false SHARED_FILESYSTEM=/tmp -DBREPO_CLIENT_SECRET= +DBREPO_CLIENT_SECRET=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG +DEFAULT_PID_PUBLISHER= diff --git a/fda-ui/components/dialogs/Persist.vue b/fda-ui/components/dialogs/Persist.vue index a8595e18fd..7ad914ef20 100644 --- a/fda-ui/components/dialogs/Persist.vue +++ b/fda-ui/components/dialogs/Persist.vue @@ -77,7 +77,7 @@ required /> </v-col> </v-row> - <v-row v-for="(creator,i) in identifier.creators" :key="`c-${i}`" dense> + <v-row v-for="(creator, i) in identifier.creators" :key="`c-${i}`" dense> <v-col cols="3"> <v-text-field v-model="creator.firstname" @@ -108,7 +108,7 @@ name="orcid" label="ORCID" /> </v-col> - <v-col cols="1" class="mt-5"> + <v-col v-if="i > 0" cols="1" class="mt-5"> <v-btn icon x-small @click="deleteCreator(i)"> <v-icon>mdi-close</v-icon> </v-btn> @@ -334,6 +334,8 @@ export default { }, mounted () { this.loadLicenses() + this.addCreator() + this.identifier.publisher = this.$config.defaultPublisher }, methods: { cancel () { @@ -356,6 +358,9 @@ export default { }) }, deleteCreator (index) { + if (index === 0) { + return + } this.identifier.creators.splice(index, 1) }, deleteRelatedIdentifier (index) { diff --git a/fda-ui/config.js b/fda-ui/config.js index a11e09b5c4..b2ea4ef41e 100644 --- a/fda-ui/config.js +++ b/fda-ui/config.js @@ -15,5 +15,6 @@ config.tokenMax = process.env.TOKEN_MAX || 5 config.elasticPassword = process.env.ELASTIC_PASSWORD || 'elastic' config.elasticPassword = process.env.ELASTIC_PASSWORD || 'elastic' config.clientSecret = process.env.DBREPO_CLIENT_SECRET +config.defaultPublisher = process.env.DEFAULT_PID_PUBLISHER module.exports = config diff --git a/fda-ui/nuxt.config.js b/fda-ui/nuxt.config.js index 840e02341c..9530a9de94 100644 --- a/fda-ui/nuxt.config.js +++ b/fda-ui/nuxt.config.js @@ -1,6 +1,6 @@ import path from 'path' import colors from 'vuetify/es5/util/colors' -import { sandbox, title, icon, brokerUsername, brokerPassword, sharedFilesystem, version, logo, mailVerify, tokenMax, elasticPassword, clientSecret, api, search } from './config' +import { sandbox, title, icon, brokerUsername, brokerPassword, sharedFilesystem, version, logo, mailVerify, tokenMax, elasticPassword, clientSecret, api, search, defaultPublisher } from './config' if (sandbox) { console.info('[FDA] Running in sandbox environment') @@ -77,7 +77,8 @@ export default { mailVerify, tokenMax, elasticPassword, - clientSecret + clientSecret, + defaultPublisher }, proxy: { diff --git a/fda-ui/pages/container/_container_id/database/_database_id/info.vue b/fda-ui/pages/container/_container_id/database/_database_id/info.vue index b9224362e6..0eebace712 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/info.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/info.vue @@ -4,24 +4,38 @@ <v-progress-linear v-if="loading" /> <v-tabs-items v-model="tab"> <v-tab-item> - <v-card v-if="isDataSteward || hasIdentifier || (!hasIdentifier && isCreator && isResearcher)" flat tile> + <v-card v-if="showIdentifierCard" flat tile> <v-card-title>Identifier</v-card-title> <v-card-text v-if="hasIdentifier"> <v-list dense> <v-list-item> <v-list-item-content> - <v-list-item-title v-if="publisher" class="mt-2"> + <v-list-item-title class="mt-2"> Persistent Identifier </v-list-item-title> - <v-list-item-content v-if="publisher"> + <v-list-item-content> <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" /> <a v-if="!loading" :href="pid">{{ pid }}</a> </v-list-item-content> - <v-list-item-title v-if="publisher" class="mt-2"> + <v-list-item-title class="mt-2"> + Database Title + </v-list-item-title> + <v-list-item-content> + <v-skeleton-loader v-if="loading" type="paragraph" width="50%" /> + <span v-if="!loading">{{ identifier.title }}</span> + </v-list-item-content> + <v-list-item-title class="mt-2"> + Database Description + </v-list-item-title> + <v-list-item-content> + <v-skeleton-loader v-if="loading" type="paragraph" width="50%" /> + <span v-if="!loading">{{ identifier.description }}</span> + </v-list-item-content> + <v-list-item-title class="mt-2"> Database Publisher </v-list-item-title> - <v-list-item-content v-if="publisher"> - {{ publisher }} + <v-list-item-content> + {{ database.identifier.publisher }} </v-list-item-content> <v-list-item-title v-if="identifier.creators.length > 0" class="mt-2"> Creators @@ -93,18 +107,17 @@ </v-list-item> </v-list> </v-card-text> - <v-card-text> + <v-card-text v-if="canCreateIdentifier || canDeleteIdentifier"> <v-card-actions> <v-btn - v-if="!hasIdentifier && (isDataSteward || (!hasIdentifier && isCreator && isResearcher))" + v-if="canCreateIdentifier" small color="primary" @click="editDbDialog = true"> Get Database PID </v-btn> - <!-- v-if="isDataSteward && hasIdentifier"--> <v-btn - v-if="false" + v-if="canDeleteIdentifier" small :loading="loadingDelete" color="error" @@ -114,7 +127,7 @@ </v-card-actions> </v-card-text> </v-card> - <v-divider v-if="isDataSteward || hasIdentifier || (!hasIdentifier && isCreator && isResearcher)" /> + <v-divider v-if="showIdentifierCard" /> <v-card flat tile> <v-card-title>Database</v-card-title> <v-card-text> @@ -135,13 +148,6 @@ <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" /> <span v-if="!loading">{{ internal_name }}</span> </v-list-item-content> - <v-list-item-title v-if="description" class="mt-2"> - Database Description - </v-list-item-title> - <v-list-item-content v-if="description"> - <v-skeleton-loader v-if="loading" type="paragraph" width="50%" /> - <span v-if="!loading">{{ description }}</span> - </v-list-item-content> <v-list-item-title class="mt-2"> Database Creator </v-list-item-title> @@ -222,7 +228,7 @@ import DBToolbar from '@/components/DBToolbar' import Persist from '@/components/dialogs/Persist' import OrcidIcon from '@/components/icons/OrcidIcon' import Citation from '@/components/identifier/Citation' -import { formatTimestampUTCLabel, formatUser, isDataSteward, isResearcher } from '@/utils' +import { formatTimestampUTCLabel, formatUser } from '@/utils' export default { components: { @@ -254,18 +260,6 @@ export default { baseUrl () { return location.protocol + '//' + location.host }, - description () { - if (!this.hasIdentifier) { - return '' - } - return this.database.identifier.description - }, - publisher () { - if (!this.hasIdentifier) { - return '' - } - return this.database.identifier.publisher - }, token () { return this.$store.state.token }, @@ -273,10 +267,10 @@ export default { return this.$store.state.user }, identifier () { - if (this.database) { - return this.$store.state.database.identifier + if (!this.database) { + return null } - return null + return this.$store.state?.database.identifier }, access () { return this.$store.state.access @@ -294,27 +288,12 @@ export default { headers: { Authorization: `Bearer ${this.token}`, Accept: 'application/json' } } }, - isResearcher () { - return isResearcher(this.user) - }, - isDataSteward () { - return isDataSteward(this.user) - }, pid () { return `${this.baseUrl}/pid/${this.database.identifier.id}` }, createdUTC () { return formatTimestampUTCLabel(this.database.created) }, - isCreator () { - if (!this.database) { - return false - } - if (!this.database.creator.username || !this.user || !this.user.username) { - return false - } - return this.database.creator.username === this.user.username - }, language () { return this.database.identifier.language }, @@ -327,6 +306,30 @@ export default { container_internal_name () { return this.database.container.internal_name }, + showIdentifierCard () { + if (this.hasIdentifier) { + return true + } + if (!this.user) { + return false + } + return this.canCreateIdentifier || this.canDeleteIdentifier || this.user.roles.includes('modify-identifier-metadata') + }, + canCreateIdentifier () { + if (!this.user) { + return false + } + if (this.hasIdentifier) { + return false + } + return this.user.roles.includes('create-identifier') + }, + canDeleteIdentifier () { + if (!this.user) { + return false + } + return this.user.roles.includes('delete-identifier') + }, contact () { if (this.database.contact === null || this.database.contact === undefined) { return null diff --git a/fda-ui/pages/container/_container_id/database/_database_id/settings.vue b/fda-ui/pages/container/_container_id/database/_database_id/settings.vue index 85631642ea..888ba4f681 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/settings.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/settings.vue @@ -5,8 +5,7 @@ <v-tabs-items v-model="tab"> <v-tab-item> <v-card v-if="isOwner" flat tile> - <v-card-title>Modify database access</v-card-title> - <v-card-subtitle>This is a dangerous operation</v-card-subtitle> + <v-card-title>Access</v-card-title> <v-data-table :headers="headers" :items="database.accesses" @@ -34,15 +33,8 @@ </v-card> <v-divider /> <v-card v-if="canModifyVisibility" flat tile> - <v-card-title>Modify database visibility</v-card-title> - <v-card-subtitle>This is a dangerous operation</v-card-subtitle> + <v-card-title>Visibility</v-card-title> <v-card-text> - <v-alert - v-if="database.is_public !== modifyVisibility.is_public" - border="left" - color="warning"> - <strong>Dangerous operation:</strong> you are about to change the visibility of the database. This affects all (sensitive) data held in the database. - </v-alert> <v-row dense> <v-col sm="6"> <v-select @@ -55,7 +47,29 @@ </v-row> <v-btn small - :disabled="database.is_public === modifyVisibility.is_public" + color="warning" + class="black--text" + @click="updateDatabaseVisibility"> + Modify Visibility + </v-btn> + </v-card-text> + </v-card> + <v-divider /> + <v-card v-if="canModifyOwnership" flat tile> + <v-card-title>Ownership</v-card-title> + <v-card-text> + <v-row dense> + <v-col sm="6"> + <v-select + id="owner" + v-model="modifyOwner.username" + :items="users" + label="Owner" + name="owner" /> + </v-col> + </v-row> + <v-btn + small color="warning" class="black--text" @click="updateDatabaseVisibility"> @@ -89,12 +103,17 @@ export default { dialogDelete: false, confirm: null, username: null, + users: [], loading: false, + loadingUsers: false, editAccessDialog: false, editVisibilityDialog: false, modifyVisibility: { is_public: null }, + modifyOwner: { + username: null + }, visibility: [ { text: 'Public', value: true }, { text: 'Private', value: false } @@ -153,6 +172,12 @@ export default { return false } return this.user.roles.includes('modify-database-visibility') + }, + canModifyOwnership () { + if (!this.isOwner) { + return false + } + return this.user.roles.includes('modify-database-owner') } }, watch: { @@ -164,6 +189,7 @@ export default { } }, mounted () { + this.loadUsers() if (!this.database) { return } @@ -197,6 +223,19 @@ export default { this.username = item.user.username this.editAccessDialog = true }, + async loadUsers () { + this.loadingUsers = true + try { + const res = await this.$axios.get('/api/user', this.config) + this.users = res.data + console.debug('users', this.users) + } catch (error) { + console.error('Failed to load users', error) + const { message } = error.response.data + this.$toast.error(`Failed to load users: ${message}`) + } + this.loadingUsers = false + }, async loadDatabase () { if (!this.$route.params.container_id || !this.$route.params.database_id) { return diff --git a/fda-user-service/.gitignore b/fda-user-service/.gitignore new file mode 100644 index 0000000000..d68acdb421 --- /dev/null +++ b/fda-user-service/.gitignore @@ -0,0 +1,44 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +# Documentation +docs/*.html +docs/css/ +docs/images/ + +# Docker +ready + +## JUnit +.attach_pid* + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/fda-user-service/.mvn/wrapper/MavenWrapperDownloader.java b/fda-user-service/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000000..a45eb6ba26 --- /dev/null +++ b/fda-user-service/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/fda-user-service/.mvn/wrapper/maven-wrapper.jar b/fda-user-service/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 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 literal 0 HcmV?d00001 diff --git a/fda-user-service/.mvn/wrapper/maven-wrapper.properties b/fda-user-service/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..642d572ce9 --- /dev/null +++ b/fda-user-service/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/fda-user-service/Dockerfile b/fda-user-service/Dockerfile new file mode 100644 index 0000000000..dd9fe0250e --- /dev/null +++ b/fda-user-service/Dockerfile @@ -0,0 +1,42 @@ +###### FIRST STAGE ###### +FROM fda-metadata-db:latest as dependency +MAINTAINER Martin Weise <martin.weise@tuwien.ac.at> + +###### SECOND STAGE ###### +FROM maven:slim as build + +COPY ./pom.xml ./ + +RUN mvn -fn -B dependency:go-offline > /dev/null + +COPY --from=dependency /root/.m2/repository/at/tuwien /root/.m2/repository/at/tuwien + +COPY ./rest-service ./rest-service +COPY ./services ./services +COPY ./report ./report + +# Make sure it compiles +RUN mvn -q clean package -DskipTests > /dev/null + +###### THIRD STAGE ###### +FROM openjdk:11-jre-slim as runtime + +ENV METADATA_DB=fda +ENV METADATA_USERNAME=root +ENV METADATA_PASSWORD=dbrepo +ENV LOG_LEVEL=debug +ENV DBREPO_CLIENT_SECRET=client-secret +ENV CLIENT_ID=dbrepo-client +ENV JWT_ISSUER=http://localhost:8080/realms/dbrepo +ENV JWT_PUBKEY=public-key + +COPY ./service_ready /usr/bin +RUN chmod +x /usr/bin/service_ready + +HEALTHCHECK --interval=10s --timeout=5s --retries=12 CMD service_ready + +COPY --from=build ./rest-service/target/rest-service-*.jar ./user-service.jar + +EXPOSE 9093 + +ENTRYPOINT ["java", "-Dlog4j2.formatMsgNoLookups=true", "-jar", "./user-service.jar"] diff --git a/fda-user-service/mvnw b/fda-user-service/mvnw new file mode 100755 index 0000000000..a16b5431b4 --- /dev/null +++ b/fda-user-service/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/fda-user-service/mvnw.cmd b/fda-user-service/mvnw.cmd new file mode 100644 index 0000000000..c8d43372c9 --- /dev/null +++ b/fda-user-service/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/fda-user-service/pom.xml b/fda-user-service/pom.xml new file mode 100644 index 0000000000..f06580d159 --- /dev/null +++ b/fda-user-service/pom.xml @@ -0,0 +1,288 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>2.3.10.RELEASE</version> + </parent> + + <groupId>at.tuwien</groupId> + <artifactId>fda-user-service</artifactId> + <version>1.1.0-alpha</version> + <name>fda-user-service</name> + <description> + The query service provides an interface to insert data into the tables created by the table service. It + also allows for view-only (possibly paginated and versioned) query execution to the raw data and consumes + messages in the message queue from the Broker Service. + </description> + <url>https://dbrepo-docs.ossdip.at</url> + <developers> + <developer> + <name>Martin Weise</name> + <email>martin.weise@tuwien.ac.at</email> + <organization>TU Wien</organization> + </developer> + <developer> + <name>Moritz Staudinger</name> + <email>moritz.staudinger@tuwien.ac.at</email> + <organization>TU Wien</organization> + </developer> + </developers> + + <packaging>pom</packaging> + <modules> + <module>rest-service</module> + <module>services</module> + <module>report</module> + </modules> + + <properties> + <java.version>11</java.version> + <spring-cloud.version>3.0.1</spring-cloud.version> + <mapstruct.version>1.4.2.Final</mapstruct.version> + <docker.version>3.2.7</docker.version> + <testcontainers.version>1.15.2</testcontainers.version> + <swagger.version>2.1.7</swagger.version> + <springfox.version>3.0.0</springfox.version> + <jacoco.version>0.8.7</jacoco.version> + <opencsv.version>5.4</opencsv.version> + <hibernate-c3po.version>5.6.3.Final</hibernate-c3po.version> + <maven-report.version>3.0.0</maven-report.version> + <jwt.version>4.3.0</jwt.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-amqp</artifactId> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-test</artifactId> + </dependency> + <dependency><!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.3-Release-Notes#validation-starter-no-longer-included-in-web-starters --> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-bootstrap</artifactId> + <version>${spring-cloud.version}</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + <!-- Authentication --> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>${jwt.version}</version> + </dependency> + <!-- Monitoring --> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + <scope>runtime</scope> + </dependency> + <!-- Data Source --> + <dependency> + <groupId>org.mariadb.jdbc</groupId> + <artifactId>mariadb-java-client</artifactId> + <version>${mariadb.version}</version> + </dependency> + <dependency> + <groupId>org.springframework.data</groupId> + <artifactId>spring-data-elasticsearch</artifactId> + </dependency> + <!-- AMPQ --> + <dependency> + <groupId>com.rabbitmq</groupId> + <artifactId>amqp-client</artifactId> + <version>${rabbit-amqp-client.version}</version> + </dependency> + <!-- Docker --> + <dependency> + <groupId>com.github.docker-java</groupId> + <artifactId>docker-java</artifactId> + <version>${docker.version}</version> + <exclusions> + <exclusion> + <groupId>javax.ws.rs</groupId> + <artifactId>jsr311-api</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>com.github.docker-java</groupId> + <artifactId>docker-java-transport-httpclient5</artifactId> + <version>${docker.version}</version> + </dependency> + <!-- IDE --> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + </dependency> + <!-- Entity, API, QueryStore --> + <dependency> + <groupId>at.tuwien</groupId> + <artifactId>fda-metadata-db-api</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>at.tuwien</groupId> + <artifactId>fda-metadata-db-entites</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + </dependency> + <!-- Testing --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-vintage-engine</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.h2database</groupId> + <artifactId>h2</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>${jacoco.version}</version> + </dependency> + <!-- Mapping --> + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct-processor</artifactId> + <version>${mapstruct.version}</version> + <optional>true</optional> + <!-- IntelliJ --> + </dependency> + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct</artifactId> + <version>${mapstruct.version}</version> + </dependency> + <!-- Gateway --> + <dependency> + <groupId>org.projectreactor</groupId> + <artifactId>reactor-spring</artifactId> + <version>1.0.1.RELEASE</version> + </dependency> + <dependency> + <groupId>javax.ws.rs</groupId> + <artifactId>javax.ws.rs-api</artifactId> + <version>2.1.1</version> + </dependency> + </dependencies> + + <build> + <resources> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + <includes> + <include>**/application*.yml</include> + <include>**/templates/*.xml</include> + </includes> + </resource> + </resources> + <plugins> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>${jacoco.version}</version> + <configuration> + <excludes> + <exclude>at/tuwien/utils/**/*</exclude> + <exclude>at/tuwien/mapper/**/*</exclude> + <exclude>at/tuwien/entities/**/*</exclude> + <exclude>at/tuwien/seeder/**/*</exclude> + <exclude>at/tuwien/handlers/**/*</exclude> + <exclude>at/tuwien/exception/**/*</exclude> + <exclude>at/tuwien/config/**/*</exclude> + <exclude>**/RabbitMqServiceImpl.class</exclude> + <exclude>**/ServiceSeeder.class</exclude> + <exclude>**/FdaQueryServiceApplication.class</exclude> + </excludes> + </configuration> + <executions> + <execution> + <id>default-prepare-agent</id> + <goals> + <goal>prepare-agent</goal> + </goals> + </execution> + <execution> + <id>report</id> + <phase>verify</phase> + <goals> + <goal>report</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-site-plugin</artifactId> + <version>3.7.1</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-project-info-reports-plugin</artifactId> + <version>3.0.0</version> +<!-- <configuration>--> +<!-- <outputDirectory>docs</outputDirectory>--> +<!-- </configuration>--> + </plugin> + <plugin> + <groupId>com.soebes.maven.plugins</groupId> + <artifactId>doxygen-maven-plugin</artifactId> + <version>1.1.0</version> + <configuration> + <haveDot>false</haveDot> + <quiet>false</quiet> + <projectName>This is a Test Project (basicReportTest)</projectName> + <outputDirectory>docs</outputDirectory> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/fda-user-service/report/pom.xml b/fda-user-service/report/pom.xml new file mode 100644 index 0000000000..b93720ffeb --- /dev/null +++ b/fda-user-service/report/pom.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <artifactId>fda-user-service</artifactId> + <groupId>at.tuwien</groupId> + <version>1.1.0-alpha</version> + </parent> + + <artifactId>report</artifactId> + <version>1.1.0-alpha</version> + <name>fda-user-service-report</name> + <description> + This module is only intended for the pipeline coverage report. See the detailed report in the + respective modules + </description> + + <dependencies> + <dependency> + <groupId>at.tuwien</groupId> + <artifactId>rest-service</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>at.tuwien</groupId> + <artifactId>services</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + + <properties> + <jacoco.version>0.8.7</jacoco.version> + </properties> + + <build> + <plugins> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>${jacoco.version}</version> + <executions> + <execution> + <id>report-aggregate</id> + <phase>verify</phase> + <goals> + <goal>report-aggregate</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> \ No newline at end of file diff --git a/fda-user-service/rest-service/pom.xml b/fda-user-service/rest-service/pom.xml new file mode 100644 index 0000000000..6a53b116b3 --- /dev/null +++ b/fda-user-service/rest-service/pom.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <artifactId>fda-user-service</artifactId> + <groupId>at.tuwien</groupId> + <version>1.1.0-alpha</version> + </parent> + + <artifactId>rest-service</artifactId> + <version>1.1.0-alpha</version> + <name>fda-user-service-rest</name> + + <dependencies> + <dependency> + <groupId>at.tuwien</groupId> + <artifactId>services</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.jetbrains</groupId> + <artifactId>annotations</artifactId> + <version>RELEASE</version> + <scope>compile</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>repackage</goal><!-- to make it exuteable with $ java -jar ./app.jar --> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> \ No newline at end of file diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/FdaUserServiceApplication.java b/fda-user-service/rest-service/src/main/java/at/tuwien/FdaUserServiceApplication.java new file mode 100644 index 0000000000..4a6c3dff08 --- /dev/null +++ b/fda-user-service/rest-service/src/main/java/at/tuwien/FdaUserServiceApplication.java @@ -0,0 +1,25 @@ +package at.tuwien; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@EnableJpaAuditing +@SpringBootApplication +@EnableTransactionManagement +@EnableScheduling +@EntityScan(basePackages = "at.tuwien.entities") +@EnableElasticsearchRepositories(basePackages = {"at.tuwien.repository.elastic"}) +@EnableJpaRepositories(basePackages = {"at.tuwien.repository.jpa"}) +public class FdaUserServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(FdaUserServiceApplication.class, args); + } + +} \ No newline at end of file diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java b/fda-user-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java new file mode 100644 index 0000000000..eb08f946aa --- /dev/null +++ b/fda-user-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java @@ -0,0 +1,46 @@ +package at.tuwien.config; + +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Value("${app.version:unknown}") + private String version; + + @Bean + public OpenAPI springShopOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("Database Repository User Service API") + .contact(new Contact() + .name("Prof. Andreas Rauber") + .email("andreas.rauber@tuwien.ac.at")) + .description("Service that manages the users") + .version(version) + .license(new License() + .name("Apache 2.0") + .url("https://www.apache.org/licenses/LICENSE-2.0"))) + .externalDocs(new ExternalDocumentation() + .description("Wiki Documentation") + .url("https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/wikis")); + } + + @Bean + public GroupedOpenApi publicApi() { + return GroupedOpenApi.builder() + .group("user-service") + .pathsToMatch("/api/**") + .build(); + } + +} + diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java b/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java new file mode 100644 index 0000000000..7bb8dc3149 --- /dev/null +++ b/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java @@ -0,0 +1,58 @@ +package at.tuwien.endpoint; + +import at.tuwien.api.auth.SignupRequestDto; +import at.tuwien.api.user.UserBriefDto; +import at.tuwien.mapper.UserMapper; +import at.tuwien.service.UserService; +import io.micrometer.core.annotation.Timed; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +@Log4j2 +@CrossOrigin(origins = "*") +@RestController +@RequestMapping("/api/user") +public class UserEndpoint { + + private final UserMapper userMapper; + private final UserService userService; + + @Autowired + public UserEndpoint(UserMapper userMapper, UserService userService) { + this.userMapper = userMapper; + this.userService = userService; + } + + @GetMapping + @Transactional(readOnly = true) + @Timed(value = "user.list", description = "Time needed to list all users in the metadata database") + @Operation(summary = "Find all users") + public ResponseEntity<List<UserBriefDto>> findAll() { + log.debug("endpoint find all users"); + final List<UserBriefDto> users = userService.findAll() + .stream() + .map(userMapper::userToUserBriefDto) + .collect(Collectors.toList()); + log.trace("find all users resulted in users {}", users); + return ResponseEntity.ok(users); + } + + @PostMapping + @Transactional + @Timed(value = "user.create", description = "Time needed to create a user in the metadata database") + @Operation(summary = "Create a user") + public ResponseEntity<UserBriefDto> create(SignupRequestDto data) { + log.debug("endpoint create a user, data={}", data); + final UserBriefDto dto = userMapper.userToUserBriefDto(userService.create(data)); + log.trace("create user resulted in dto {}", dto); + return ResponseEntity.ok(dto); + } + +} diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java b/fda-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java new file mode 100644 index 0000000000..b5d0c1658b --- /dev/null +++ b/fda-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java @@ -0,0 +1,249 @@ +package at.tuwien.handlers; + +import at.tuwien.api.error.ApiErrorDto; +import at.tuwien.exception.*; +import net.sf.jsqlparser.JSQLParserException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class ApiExceptionHandler extends ResponseEntityExceptionHandler { + + @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) + @ExceptionHandler(AmqpException.class) + public ResponseEntity<ApiErrorDto> handle(AmqpException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.NOT_ACCEPTABLE) + .message(e.getLocalizedMessage()) + .code("error.query.amqp") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.EXPECTATION_FAILED) + @ExceptionHandler(ColumnParseException.class) + public ResponseEntity<ApiErrorDto> handle(ColumnParseException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.EXPECTATION_FAILED) + .message(e.getLocalizedMessage()) + .code("error.query.columnparse") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(ContainerNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(ContainerNotFoundException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.NOT_FOUND) + .message(e.getLocalizedMessage()) + .code("error.query.containernotfound") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + @ExceptionHandler(DatabaseConnectionException.class) + public ResponseEntity<ApiErrorDto> handle(DatabaseConnectionException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.METHOD_NOT_ALLOWED) + .message(e.getLocalizedMessage()) + .code("error.query.databaseconnection") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(DatabaseNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(DatabaseNotFoundException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.NOT_FOUND) + .message(e.getLocalizedMessage()) + .code("error.query.databasenotfound") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(FileStorageException.class) + public ResponseEntity<ApiErrorDto> handle(FileStorageException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.BAD_REQUEST) + .message(e.getLocalizedMessage()) + .code("error.query.filestore") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(IdentifierNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(IdentifierNotFoundException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.NOT_FOUND) + .message(e.getLocalizedMessage()) + .code("error.query.identifiernotfound") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.CONFLICT) + @ExceptionHandler(ImageNotSupportedException.class) + public ResponseEntity<ApiErrorDto> handle(ImageNotSupportedException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.CONFLICT) + .message(e.getLocalizedMessage()) + .code("error.query.imagenotsupported") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + @ExceptionHandler(NotAllowedException.class) + public ResponseEntity<ApiErrorDto> handle(NotAllowedException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.METHOD_NOT_ALLOWED) + .message(e.getLocalizedMessage()) + .code("error.query.permission") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(PaginationException.class) + public ResponseEntity<ApiErrorDto> handle(PaginationException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.BAD_REQUEST) + .message(e.getLocalizedMessage()) + .code("error.query.pagination") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.CONFLICT) + @ExceptionHandler(QueryAlreadyPersistedException.class) + public ResponseEntity<ApiErrorDto> handle(QueryAlreadyPersistedException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.CONFLICT) + .message(e.getLocalizedMessage()) + .code("error.query.alreadypersisted") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(QueryMalformedException.class) + public ResponseEntity<ApiErrorDto> handle(QueryMalformedException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.BAD_REQUEST) + .message(e.getLocalizedMessage()) + .code("error.query.malformed") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(QueryNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(QueryNotFoundException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.NOT_FOUND) + .message(e.getLocalizedMessage()) + .code("error.query.notfound") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT) + @ExceptionHandler(QueryStoreException.class) + public ResponseEntity<ApiErrorDto> handle(QueryStoreException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.GATEWAY_TIMEOUT) + .message(e.getLocalizedMessage()) + .code("error.query.store") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(SortException.class) + public ResponseEntity<ApiErrorDto> handle(SortException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.BAD_REQUEST) + .message(e.getLocalizedMessage()) + .code("error.query.sort") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.LOCKED) + @ExceptionHandler(TableMalformedException.class) + public ResponseEntity<ApiErrorDto> handle(TableMalformedException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.LOCKED) + .message(e.getLocalizedMessage()) + .code("error.query.tablemalformed") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(TableNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(TableNotFoundException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.NOT_FOUND) + .message(e.getLocalizedMessage()) + .code("error.query.tablenotfound") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.CONFLICT) + @ExceptionHandler(TupleDeleteException.class) + public ResponseEntity<ApiErrorDto> handle(TupleDeleteException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.CONFLICT) + .message(e.getLocalizedMessage()) + .code("error.query.tupledelete") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(UserNotFoundException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.NOT_FOUND) + .message(e.getLocalizedMessage()) + .code("error.query.usernotfound") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.LOCKED) + @ExceptionHandler(ViewMalformedException.class) + public ResponseEntity<ApiErrorDto> handle(ViewMalformedException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.LOCKED) + .message(e.getLocalizedMessage()) + .code("error.query.viewmalformed") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(ViewNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(ViewNotFoundException e, WebRequest request) { + final ApiErrorDto response = ApiErrorDto.builder() + .status(HttpStatus.NOT_FOUND) + .message(e.getLocalizedMessage()) + .code("error.query.viewnotfound") + .build(); + return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); + } + +} diff --git a/fda-user-service/rest-service/src/main/resources/application-docker.yml b/fda-user-service/rest-service/src/main/resources/application-docker.yml new file mode 100644 index 0000000000..084cc82986 --- /dev/null +++ b/fda-user-service/rest-service/src/main/resources/application-docker.yml @@ -0,0 +1,40 @@ +app.version: '@project.version@' +spring: + main.banner-mode: off + datasource: + url: jdbc:mariadb://metadata-db:3306/fda + driver-class-name: org.mariadb.jdbc.Driver + username: "${METADATA_USERNAME}" + password: "${METADATA_PASSWORD}" + jpa: + show-sql: false + database-platform: org.hibernate.dialect.MariaDBDialect + hibernate: + ddl-auto: validate + use-new-id-generator-mappings: false + open-in-view: false + properties: + hibernate: + default_schema: fda + jdbc: + time_zone: UTC + application: + name: user-service + cloud: + loadbalancer.ribbon.enabled: false +management.endpoints.web.exposure.include: health,info,prometheus +server.port: 9098 +logging: + pattern.console: "%d %highlight(%-5level) %msg%n" + level: + root: warn + at.tuwien.: "${LOG_LEVEL}" + org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: debug +eureka: + instance.hostname: user-service + client.serviceUrl.defaultZone: http://discovery-service:9090/eureka/ +fda: + ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" \ No newline at end of file diff --git a/fda-user-service/rest-service/src/main/resources/application-local.yml b/fda-user-service/rest-service/src/main/resources/application-local.yml new file mode 100644 index 0000000000..4b86aa03a8 --- /dev/null +++ b/fda-user-service/rest-service/src/main/resources/application-local.yml @@ -0,0 +1,40 @@ +app.version: '@project.version@' +spring: + main.banner-mode: off + datasource: + url: jdbc:mariadb://localhost:3306/fda + driver-class-name: org.mariadb.jdbc.Driver + username: root + password: dbrepo + jpa: + show-sql: false + database-platform: org.hibernate.dialect.MariaDBDialect + hibernate: + ddl-auto: validate + use-new-id-generator-mappings: false + open-in-view: false + properties: + hibernate: + default_schema: fda + jdbc: + time_zone: UTC + application: + name: user-service + cloud: + loadbalancer.ribbon.enabled: false +management.endpoints.web.exposure.include: health,info,prometheus +server.port: 9098 +logging: + pattern.console: "%d %highlight(%-5level) %msg%n" + level: + root: warn + at.tuwien.: trace + org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: debug +eureka: + instance.hostname: user-service + client.serviceUrl.defaultZone: http://discovery-service:9090/eureka/ +fda: + ready.path: /ready + jwt: + issuer: http://localhost:8080/realms/dbrepo + public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB \ No newline at end of file diff --git a/fda-user-service/rest-service/src/main/resources/application.yml b/fda-user-service/rest-service/src/main/resources/application.yml new file mode 100644 index 0000000000..9ba4e1547a --- /dev/null +++ b/fda-user-service/rest-service/src/main/resources/application.yml @@ -0,0 +1,40 @@ +app.version: '@project.version@' +spring: + main.banner-mode: off + datasource: + url: "jdbc:mariadb://metadata-db:3306/${METADATA_DB}" + driver-class-name: org.mariadb.jdbc.Driver + username: "${METADATA_USERNAME}" + password: "${METADATA_PASSWORD}" + jpa: + show-sql: false + database-platform: org.hibernate.dialect.MariaDBDialect + hibernate: + ddl-auto: validate + use-new-id-generator-mappings: false + open-in-view: false + properties: + hibernate: + default_schema: "${METADATA_DB}" + jdbc: + time_zone: UTC + application: + name: user-service + cloud: + loadbalancer.ribbon.enabled: false +management.endpoints.web.exposure.include: health,info,prometheus +server.port: 9098 +logging: + pattern.console: "%d %highlight(%-5level) %msg%n" + level: + root: warn + at.tuwien.: "${LOG_LEVEL}" + org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: debug +eureka: + instance.hostname: user-service + client.serviceUrl.defaultZone: http://discovery-service:9090/eureka/ +fda: + ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" diff --git a/fda-user-service/rest-service/src/main/resources/config.properties b/fda-user-service/rest-service/src/main/resources/config.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fda-user-service/rest-service/src/main/resources/mariadb_hibernate.cfg.xml b/fda-user-service/rest-service/src/main/resources/mariadb_hibernate.cfg.xml new file mode 100644 index 0000000000..01f90448ca --- /dev/null +++ b/fda-user-service/rest-service/src/main/resources/mariadb_hibernate.cfg.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE hibernate-configuration PUBLIC + "-//Hibernate/Hibernate Configuration DTD 3.0//EN" + "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> +<hibernate-configuration> + <session-factory> + <property name="current_session_context_class">thread</property> + <property name="transaction.coordinator_class">jdbc</property> + <property name="c3p0.min_size">1</property> + <property name="c3p0.max_size">30</property> + <property name="c3p0.acquire_increment">1</property> + <property name="c3p0.timeout">1800</property> + <property name="show_sql">true</property> + <property name="format_sql">true</property> + <property name="hbm2ddl.auto">update</property> + <mapping class="at.tuwien.querystore.Column" /> + <mapping class="at.tuwien.querystore.Query" /> + <mapping class="at.tuwien.querystore.Table" /> + </session-factory> +</hibernate-configuration> diff --git a/fda-user-service/rest-service/src/test/resources/application.properties b/fda-user-service/rest-service/src/test/resources/application.properties new file mode 100644 index 0000000000..4eea490a91 --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/application.properties @@ -0,0 +1,32 @@ +# enable local spring profile +spring.profiles.active=local + +# disable discovery +spring.cloud.discovery.enabled=false + +# disable cloud config and config discovery +spring.cloud.config.discovery.enabled=false +spring.cloud.config.enabled=false + +# internal datasource +spring.datasource.url=jdbc:h2:mem:testdb;DATABASE_TO_UPPER=false;DB_CLOSE_ON_EXIT=FALSE;INIT=RUNSCRIPT FROM './src/test/resources/schema.sql' +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=false + +# additional logging +logging.level.root=error +logging.level.at.tuwien.=trace + +# broker service +spring.rabbitmq.host=dbrepo-broker-service +spring.rabbitmq.username=guest +spring.rabbitmq.password=guest + +# search service +fda.consumers=2 +fda.gateway.endpoint: http://localhost:15672 +fda.elastic.endpoint=dbrepo-search-service:9200 \ No newline at end of file diff --git a/fda-user-service/rest-service/src/test/resources/schema.sql b/fda-user-service/rest-service/src/test/resources/schema.sql new file mode 100644 index 0000000000..906d8df808 --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/schema.sql @@ -0,0 +1,25 @@ +CREATE SCHEMA IF NOT EXISTS `fda`; +SET SCHEMA `fda`; +DROP TABLE IF EXISTS fda.mdb_concepts; +CREATE TABLE IF NOT EXISTS fda.mdb_concepts +( + uri VARCHAR(500) not null, + name VARCHAR(255), + created timestamp NOT NULL DEFAULT NOW(), + created_by bigint, + PRIMARY KEY (uri) +); +DROP TABLE IF EXISTS fda.mdb_units; +CREATE TABLE IF NOT EXISTS fda.mdb_units +( + uri VARCHAR(500) not null, + name VARCHAR(255), + created timestamp NOT NULL DEFAULT NOW(), + created_by bigint, + PRIMARY KEY (uri) +); +-- Modified for H2 +-- Assume id=1 is invalid +-- Assume id=2 is still valid token +-- CREATE VIEW IF NOT EXISTS fda.mdb_invalid_tokens AS +-- (SELECT `id`, `token_hash`, `creator`, `created`, `expires`, `last_used` FROM fda.`mdb_tokens` WHERE `id` = 1); \ No newline at end of file diff --git a/fda-user-service/rest-service/src/test/resources/sensor/1_querystore.sql b/fda-user-service/rest-service/src/test/resources/sensor/1_querystore.sql new file mode 100644 index 0000000000..2762d130a0 --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/sensor/1_querystore.sql @@ -0,0 +1,75 @@ +CREATE SEQUENCE `qs_queries_seq`; +CREATE TABLE `qs_queries` +( + `id` bigint not null primary key default nextval(`qs_queries_seq`), + `created` datetime not null default now(), + `executed` datetime not null default now(), + `created_by` varchar(255) not null, + `query` text not null, + `query_normalized` text not null, + `is_persisted` boolean not null, + `query_hash` varchar(255) not null, + `result_hash` varchar(255), + `result_number` bigint +); +DELIMITER $$ +CREATE PROCEDURE hash_table(IN name VARCHAR(255), OUT hash VARCHAR(255)) +BEGIN + DECLARE _sql TEXT; + SELECT CONCAT('SELECT SHA2(GROUP_CONCAT(CONCAT_WS(\'\',', + GROUP_CONCAT(CONCAT('`', column_name, '`') ORDER BY column_name), + ') SEPARATOR \',\'), 256) AS hash FROM `', name, '` INTO @hash;') + FROM `information_schema`.`columns` + WHERE `table_schema` = DATABASE() + AND `table_name` = name + INTO _sql; + PREPARE stmt FROM _sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET hash = @hash; +END $$ +DELIMITER $$ +CREATE PROCEDURE store_query(IN query TEXT, IN executed DATETIME, OUT queryId BIGINT) +BEGIN + DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256); + DECLARE _username varchar(255) DEFAULT REGEXP_REPLACE(current_user(), '@.*', ''); + DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')'); + PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash); + SELECT COUNT(*) FROM _tmp INTO @count; + IF @hash IS NULL THEN + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + ELSE + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + END IF; +END $$ +DELIMITER $$ +CREATE + DEFINER = 'root' PROCEDURE _store_query(IN _username VARCHAR(255), IN query TEXT, IN executed DATETIME, OUT queryId BIGINT) +BEGIN + DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256); + DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')'); + PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash); + SELECT COUNT(*) FROM _tmp INTO @count; + IF @hash IS NULL THEN + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + ELSE + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + END IF; +END $$ +DELIMITER ; \ No newline at end of file diff --git a/fda-user-service/rest-service/src/test/resources/sensor/2_traffic.sql b/fda-user-service/rest-service/src/test/resources/sensor/2_traffic.sql new file mode 100644 index 0000000000..93d293a6fe --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/sensor/2_traffic.sql @@ -0,0 +1,8 @@ +CREATE SEQUENCE seq_sensor + START 1; + +CREATE TABLE sensor +( + `timestamp` TIMESTAMP NULL, + primary key (`timestamp`) +) with system versioning; \ No newline at end of file diff --git a/fda-user-service/rest-service/src/test/resources/traffic/1_querystore.sql b/fda-user-service/rest-service/src/test/resources/traffic/1_querystore.sql new file mode 100644 index 0000000000..2762d130a0 --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/traffic/1_querystore.sql @@ -0,0 +1,75 @@ +CREATE SEQUENCE `qs_queries_seq`; +CREATE TABLE `qs_queries` +( + `id` bigint not null primary key default nextval(`qs_queries_seq`), + `created` datetime not null default now(), + `executed` datetime not null default now(), + `created_by` varchar(255) not null, + `query` text not null, + `query_normalized` text not null, + `is_persisted` boolean not null, + `query_hash` varchar(255) not null, + `result_hash` varchar(255), + `result_number` bigint +); +DELIMITER $$ +CREATE PROCEDURE hash_table(IN name VARCHAR(255), OUT hash VARCHAR(255)) +BEGIN + DECLARE _sql TEXT; + SELECT CONCAT('SELECT SHA2(GROUP_CONCAT(CONCAT_WS(\'\',', + GROUP_CONCAT(CONCAT('`', column_name, '`') ORDER BY column_name), + ') SEPARATOR \',\'), 256) AS hash FROM `', name, '` INTO @hash;') + FROM `information_schema`.`columns` + WHERE `table_schema` = DATABASE() + AND `table_name` = name + INTO _sql; + PREPARE stmt FROM _sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET hash = @hash; +END $$ +DELIMITER $$ +CREATE PROCEDURE store_query(IN query TEXT, IN executed DATETIME, OUT queryId BIGINT) +BEGIN + DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256); + DECLARE _username varchar(255) DEFAULT REGEXP_REPLACE(current_user(), '@.*', ''); + DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')'); + PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash); + SELECT COUNT(*) FROM _tmp INTO @count; + IF @hash IS NULL THEN + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + ELSE + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + END IF; +END $$ +DELIMITER $$ +CREATE + DEFINER = 'root' PROCEDURE _store_query(IN _username VARCHAR(255), IN query TEXT, IN executed DATETIME, OUT queryId BIGINT) +BEGIN + DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256); + DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')'); + PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash); + SELECT COUNT(*) FROM _tmp INTO @count; + IF @hash IS NULL THEN + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + ELSE + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + END IF; +END $$ +DELIMITER ; \ No newline at end of file diff --git a/fda-user-service/rest-service/src/test/resources/traffic/2_traffic.sql b/fda-user-service/rest-service/src/test/resources/traffic/2_traffic.sql new file mode 100644 index 0000000000..3038fe5a7a --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/traffic/2_traffic.sql @@ -0,0 +1,43 @@ +/* https://www.kaggle.com/laa283/zurich-public-transport/version/2 */ +CREATE SEQUENCE seq_traffic + START 1; + +CREATE TABLE traffic_zurich +( + linie bigint null, + richtung bigint null, + betriebsdatum date null, + fahrzeug bigint null, + kurs bigint null, + seq_von bigint null, + halt_diva_von bigint null, + halt_punkt_diva_von bigint null, + halt_kurz_von1 varchar(255) null, + datum_von date null, + soll_an_von bigint null, + ist_an_von bigint null, + soll_ab_von bigint null, + ist_ab_von bigint null, + seq_nach bigint null, + halt_diva_nach bigint null, + halt_punkt_diva_nach bigint null, + halt_kurz_nach1 varchar(255) null, + datum_nach DATE null, + soll_an_nach bigint null, + ist_an_nach1 bigint null, + soll_ab_nach bigint null, + ist_ab_nach bigint null, + fahrt_id bigint null, + fahrweg_id bigint null, + fw_no bigint null, + fw_typ bigint null, + fw_kurz bigint null, + fw_lang varchar(255) null, + umlauf_von varchar(255) null, + halt_id_von bigint null, + halt_id_nach bigint null, + halt_punkt_id_von bigint null, + halt_punkt_id_nach bigint null, + id bigint default nextval(`seq_traffic`) not null, + primary key (id) +) with system versioning; \ No newline at end of file diff --git a/fda-user-service/rest-service/src/test/resources/weather/1_querystore.sql b/fda-user-service/rest-service/src/test/resources/weather/1_querystore.sql new file mode 100644 index 0000000000..2762d130a0 --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/weather/1_querystore.sql @@ -0,0 +1,75 @@ +CREATE SEQUENCE `qs_queries_seq`; +CREATE TABLE `qs_queries` +( + `id` bigint not null primary key default nextval(`qs_queries_seq`), + `created` datetime not null default now(), + `executed` datetime not null default now(), + `created_by` varchar(255) not null, + `query` text not null, + `query_normalized` text not null, + `is_persisted` boolean not null, + `query_hash` varchar(255) not null, + `result_hash` varchar(255), + `result_number` bigint +); +DELIMITER $$ +CREATE PROCEDURE hash_table(IN name VARCHAR(255), OUT hash VARCHAR(255)) +BEGIN + DECLARE _sql TEXT; + SELECT CONCAT('SELECT SHA2(GROUP_CONCAT(CONCAT_WS(\'\',', + GROUP_CONCAT(CONCAT('`', column_name, '`') ORDER BY column_name), + ') SEPARATOR \',\'), 256) AS hash FROM `', name, '` INTO @hash;') + FROM `information_schema`.`columns` + WHERE `table_schema` = DATABASE() + AND `table_name` = name + INTO _sql; + PREPARE stmt FROM _sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET hash = @hash; +END $$ +DELIMITER $$ +CREATE PROCEDURE store_query(IN query TEXT, IN executed DATETIME, OUT queryId BIGINT) +BEGIN + DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256); + DECLARE _username varchar(255) DEFAULT REGEXP_REPLACE(current_user(), '@.*', ''); + DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')'); + PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash); + SELECT COUNT(*) FROM _tmp INTO @count; + IF @hash IS NULL THEN + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + ELSE + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + END IF; +END $$ +DELIMITER $$ +CREATE + DEFINER = 'root' PROCEDURE _store_query(IN _username VARCHAR(255), IN query TEXT, IN executed DATETIME, OUT queryId BIGINT) +BEGIN + DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256); + DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')'); + PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash); + SELECT COUNT(*) FROM _tmp INTO @count; + IF @hash IS NULL THEN + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + ELSE + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + END IF; +END $$ +DELIMITER ; \ No newline at end of file diff --git a/fda-user-service/rest-service/src/test/resources/weather/2_weather.sql b/fda-user-service/rest-service/src/test/resources/weather/2_weather.sql new file mode 100644 index 0000000000..29967f2977 --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/weather/2_weather.sql @@ -0,0 +1,55 @@ +/* https://www.kaggle.com/jsphyg/weather-dataset-rattle-package */ +CREATE TABLE weather_location +( + location VARCHAR(255) PRIMARY KEY, + lat DOUBLE PRECISION NULL, + lng DOUBLE PRECISION NULL +) WITH SYSTEM VERSIONING; + +CREATE TABLE weather_aus +( + id BIGINT NOT NULL PRIMARY KEY, + `date` DATE NOT NULL, + location VARCHAR(255) NULL, + mintemp DOUBLE PRECISION NULL, + rainfall DOUBLE PRECISION NULL, + FOREIGN KEY (location) REFERENCES weather_location (location), + UNIQUE (`date`), + CHECK (`mintemp` > 0) +) WITH SYSTEM VERSIONING; + +CREATE TABLE sensor +( + `timestamp` TIMESTAMP NOT NULL, + PRIMARY KEY (`timestamp`), + UNIQUE (`timestamp`) +) WITH SYSTEM VERSIONING; + +INSERT INTO weather_location (location, lat, lng) +VALUES ('Albury', -36.0653583, 146.9112214), + ('Melbourne', null, null), + ('Sydney', -33.847927, 150.6517942); + +INSERT INTO weather_aus (id, `date`, location, mintemp, rainfall) +VALUES (1, '2008-12-01', 'Albury', 13.4, 0.6), + (2, '2008-12-02', 'Albury', 7.4, 0), + (3, '2008-12-03', 'Albury', 12.9, 0); + +######################################################################################################################## +## TEST CASE PRE-REQUISITE ## +######################################################################################################################## + +CREATE VIEW junit2 AS +( +SELECT `location`, `lat`, `lng` +FROM `weather_location` +WHERE `location` = 'Albury'); + +CREATE VIEW `hs_weather_aus` AS +SELECT * +FROM (SELECT `id`, ROW_START AS inserted_at, IF(ROW_END > NOW(), NULL, ROW_END) AS deleted_at, COUNT(*) as total + FROM `weather_aus` FOR SYSTEM_TIME ALL + GROUP BY inserted_at, deleted_at + ORDER BY deleted_at DESC + LIMIT 50) AS v +ORDER BY v.inserted_at, v.deleted_at ASC; diff --git a/fda-user-service/rest-service/src/test/resources/weather/location.csv b/fda-user-service/rest-service/src/test/resources/weather/location.csv new file mode 100644 index 0000000000..b9410c65c9 --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/weather/location.csv @@ -0,0 +1,2 @@ +Albury,-36.0653583,146.9112214 +Sydney,-33.847927,150.6517942 \ No newline at end of file diff --git a/fda-user-service/rest-service/src/test/resources/zoo/1_querystore.sql b/fda-user-service/rest-service/src/test/resources/zoo/1_querystore.sql new file mode 100644 index 0000000000..2762d130a0 --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/zoo/1_querystore.sql @@ -0,0 +1,75 @@ +CREATE SEQUENCE `qs_queries_seq`; +CREATE TABLE `qs_queries` +( + `id` bigint not null primary key default nextval(`qs_queries_seq`), + `created` datetime not null default now(), + `executed` datetime not null default now(), + `created_by` varchar(255) not null, + `query` text not null, + `query_normalized` text not null, + `is_persisted` boolean not null, + `query_hash` varchar(255) not null, + `result_hash` varchar(255), + `result_number` bigint +); +DELIMITER $$ +CREATE PROCEDURE hash_table(IN name VARCHAR(255), OUT hash VARCHAR(255)) +BEGIN + DECLARE _sql TEXT; + SELECT CONCAT('SELECT SHA2(GROUP_CONCAT(CONCAT_WS(\'\',', + GROUP_CONCAT(CONCAT('`', column_name, '`') ORDER BY column_name), + ') SEPARATOR \',\'), 256) AS hash FROM `', name, '` INTO @hash;') + FROM `information_schema`.`columns` + WHERE `table_schema` = DATABASE() + AND `table_name` = name + INTO _sql; + PREPARE stmt FROM _sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET hash = @hash; +END $$ +DELIMITER $$ +CREATE PROCEDURE store_query(IN query TEXT, IN executed DATETIME, OUT queryId BIGINT) +BEGIN + DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256); + DECLARE _username varchar(255) DEFAULT REGEXP_REPLACE(current_user(), '@.*', ''); + DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')'); + PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash); + SELECT COUNT(*) FROM _tmp INTO @count; + IF @hash IS NULL THEN + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + ELSE + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + END IF; +END $$ +DELIMITER $$ +CREATE + DEFINER = 'root' PROCEDURE _store_query(IN _username VARCHAR(255), IN query TEXT, IN executed DATETIME, OUT queryId BIGINT) +BEGIN + DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256); + DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')'); + PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash); + SELECT COUNT(*) FROM _tmp INTO @count; + IF @hash IS NULL THEN + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL); + ELSE + INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`, + `result_number`, `executed`) + SELECT _username, query, query, false, _queryhash, @hash, @count, executed + WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash); + END IF; +END $$ +DELIMITER ; \ No newline at end of file diff --git a/fda-user-service/rest-service/src/test/resources/zoo/2_zoo.sql b/fda-user-service/rest-service/src/test/resources/zoo/2_zoo.sql new file mode 100644 index 0000000000..50f02ee2c5 --- /dev/null +++ b/fda-user-service/rest-service/src/test/resources/zoo/2_zoo.sql @@ -0,0 +1,191 @@ +create sequence seq_zoo_id; +create sequence seq_names_id; +create table zoo +( + id bigint not null default nextval(`seq_zoo_id`), + animal_name varchar(255) null, + hair tinyint(1) null, + feathers tinyint(1) null, + eggs tinyint(1) null, + milk tinyint(1) null, + airborne tinyint(1) null, + aquatic tinyint(1) null, + predator tinyint(1) null, + toothed tinyint(1) null, + backbone tinyint(1) null, + breathes tinyint(1) null, + venomous tinyint(1) null, + fins tinyint(1) null, + legs bigint null, + tail tinyint(1) null, + domestic tinyint(1) null, + catsize tinyint(1) null, + class_type bigint null, + primary key (id) +) with system versioning; + +create table names +( + id bigint not null default nextval(`seq_names_id`), + firstname varchar(255), + lastname varchar(255), + primary key (id), + unique key (firstname, lastname) +) with system versioning; + +create table likes +( + name_id bigint not null, + zoo_id bigint not null, + primary key (name_id, zoo_id), + foreign key (name_id) references names (id), + foreign key (zoo_id) references zoo (id) +) with system versioning; + +INSERT INTO zoo (id, animal_name, hair, feathers, eggs, milk, airborne, aquatic, predator, toothed, backbone, breathes, + venomous, fins, legs, tail, domestic, catsize, class_type) +VALUES (1, 'aardvark', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 0, 0, 1, 1), + (2, 'antelope', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (3, 'bass', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4), + (4, 'bear', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 0, 0, 1, 1), + (5, 'boar', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (6, 'buffalo', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (7, 'calf', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 1, 1, 1), + (8, 'carp', 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 4), + (9, 'catfish', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4), + (10, 'cavy', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 0, 1, 0, 1), + (11, 'cheetah', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (12, 'chicken', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 1, 0, 2), + (13, 'chub', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4), + (14, 'clam', 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7), + (15, 'crab', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 7), + (16, 'crayfish', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 6, 0, 0, 0, 7), + (17, 'crow', 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2), + (18, 'deer', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (19, 'dogfish', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 4), + (20, 'dolphin', 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1), + (21, 'dove', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 1, 0, 2), + (22, 'duck', 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2), + (23, 'elephant', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (24, 'flamingo', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2), + (25, 'flea', 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6), + (26, 'frog', 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 4, 0, 0, 0, 5), + (27, 'frog', 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 4, 0, 0, 0, 5), + (28, 'fruitbat', 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 2, 1, 0, 0, 1), + (29, 'giraffe', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (30, 'girl', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 2, 0, 1, 1, 1), + (31, 'gnat', 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6), + (32, 'goat', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 1, 1, 1), + (33, 'gorilla', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 2, 0, 0, 1, 1), + (34, 'gull', 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2), + (35, 'haddock', 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4), + (36, 'hamster', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 1, 0, 1), + (37, 'hare', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 0, 1), + (38, 'hawk', 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2), + (39, 'herring', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4), + (40, 'honeybee', 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 6, 0, 1, 0, 6), + (41, 'housefly', 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6), + (42, 'kiwi', 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2), + (43, 'ladybird', 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6), + (44, 'lark', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2), + (45, 'leopard', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (46, 'lion', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (47, 'lobster', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 6, 0, 0, 0, 7), + (48, 'lynx', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (49, 'mink', 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (50, 'mole', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 0, 1), + (51, 'mongoose', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (52, 'moth', 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6), + (53, 'newt', 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 4, 1, 0, 0, 5), + (54, 'octopus', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 8, 0, 0, 1, 7), + (55, 'opossum', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 0, 1), + (56, 'oryx', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (57, 'ostrich', 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2), + (58, 'parakeet', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 1, 0, 2), + (59, 'penguin', 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2), + (60, 'pheasant', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2), + (61, 'pike', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 4), + (62, 'piranha', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4), + (63, 'pitviper', 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 3), + (64, 'platypus', 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (65, 'polecat', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (66, 'pony', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 1, 1, 1), + (67, 'porpoise', 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1), + (68, 'puma', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (69, 'pussycat', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 1, 1, 1), + (70, 'raccoon', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (71, 'reindeer', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 1, 1, 1), + (72, 'rhea', 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2), + (73, 'scorpion', 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 8, 1, 0, 0, 7), + (74, 'seahorse', 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4), + (75, 'seal', 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1), + (76, 'sealion', 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 2, 1, 0, 1, 1), + (77, 'seasnake', 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 3), + (78, 'seawasp', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 7), + (79, 'skimmer', 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2), + (80, 'skua', 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2), + (81, 'slowworm', 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 3), + (82, 'slug', 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 7), + (83, 'sole', 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4), + (84, 'sparrow', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2), + (85, 'squirrel', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 2, 1, 0, 0, 1), + (86, 'starfish', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 5, 0, 0, 0, 7), + (87, 'stingray', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 4), + (88, 'swan', 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2), + (89, 'termite', 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6), + (90, 'toad', 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 4, 0, 0, 0, 5), + (91, 'tortoise', 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 4, 1, 0, 1, 3), + (92, 'tuatara', 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 0, 3), + (93, 'tuna', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 4), + (94, 'vampire', 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 2, 1, 0, 0, 1), + (95, 'vole', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 0, 1), + (96, 'vulture', 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2), + (97, 'wallaby', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 2, 1, 0, 1, 1), + (98, 'wasp', 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 6, 0, 0, 0, 6), + (99, 'wolf', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1), + (100, 'worm', 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 7), + (101, 'wren', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2); + +INSERT INTO names (firstname, lastname) +VALUES ('Moritz', 'Staudinger'), + ('Martin', 'Weise'), + ('Eva', 'Gergely'), + ('Cornelia', 'Michlits'), + ('Kirill', 'Stytsenko'); + +INSERT INTO likes (name_id, zoo_id) +VALUES (1, 5), + (1, 10), + (2, 3), + (2, 80), + (3, 4), + (4, 4), + (5, 100); + +######################################################################################################################## +## TEST CASE PRE-REQUISITE ## +######################################################################################################################## + +CREATE VIEW mock_view AS +( +SELECT `id`, + `animal_name`, + `hair`, + `feathers`, + `eggs`, + `milk`, + `airborne`, + `aquatic`, + `predator`, + `toothed`, + `backbone`, + `breathes`, + `venomous`, + `fins`, + `legs`, + `tail`, + `domestic`, + `catsize`, + `class_type` +FROM `zoo` +WHERE `class_type` = 1); diff --git a/fda-user-service/service_ready b/fda-user-service/service_ready new file mode 100644 index 0000000000..b2e4f9df68 --- /dev/null +++ b/fda-user-service/service_ready @@ -0,0 +1,6 @@ +#!/bin/bash +if [ -f /ready ]; then + echo "service is ready and accepting connections" + exit 0 +fi +exit 1 \ No newline at end of file diff --git a/fda-user-service/services/pom.xml b/fda-user-service/services/pom.xml new file mode 100644 index 0000000000..a42439c741 --- /dev/null +++ b/fda-user-service/services/pom.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <artifactId>fda-user-service</artifactId> + <groupId>at.tuwien</groupId> + <version>1.1.0-alpha</version> + </parent> + + <artifactId>services</artifactId> + <version>1.1.0-alpha</version> + <name>fda-user-service-services</name> + +</project> diff --git a/fda-user-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-user-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java new file mode 100644 index 0000000000..b2b01c42ee --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -0,0 +1,100 @@ +package at.tuwien.auth; + +import at.tuwien.api.auth.RealmAccessDto; +import at.tuwien.api.user.UserDetailsDto; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.stream.Collectors; + +@Slf4j +public class AuthTokenFilter extends OncePerRequestFilter { + + @Value("${fda.jwt.issuer}") + private String issuer; + + @Value("${fda.jwt.public_key}") + private String publicKey; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + final String jwt = parseJwt(request); + if (jwt != null) { + final UserDetails userDetails = verifyJwt(jwt); + log.debug("authenticated user {}", userDetails); + final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + public UserDetails verifyJwt(String token) throws ServletException { + final KeyFactory kf; + try { + kf = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + log.error("Failed to find RSA algorithm"); + throw new ServletException("Failed to find RSA algorithm", e); + } + final X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)); + final RSAPublicKey pubKey; + try { + pubKey = (RSAPublicKey) kf.generatePublic(keySpecX509); + } catch (InvalidKeySpecException e) { + log.error("Provided public key is invalid"); + throw new ServletException("Provided public key is invalid", e); + } + final Algorithm algorithm = Algorithm.RSA256(pubKey, null); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(issuer) + .withAudience("spring") + .build(); + final DecodedJWT jwt = verifier.verify(token); + final RealmAccessDto realmAccess = jwt.getClaim("realm_access").as(RealmAccessDto.class); + return UserDetailsDto.builder() + .username(jwt.getClaim("client_id").asString()) + .authorities(Arrays.stream(realmAccess.getRoles()).map(SimpleGrantedAuthority::new).collect(Collectors.toList())) + .build(); + } + + /** + * Parses the token from the HTTP header of the request + * + * @param request The request. + * @return The token. + */ + public String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7, headerAuth.length()); + } + return null; + } +} \ No newline at end of file diff --git a/fda-user-service/services/src/main/java/at/tuwien/config/JacksonConfig.java b/fda-user-service/services/src/main/java/at/tuwien/config/JacksonConfig.java new file mode 100644 index 0000000000..fba7f99cf2 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/config/JacksonConfig.java @@ -0,0 +1,31 @@ +package at.tuwien.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Date; +import java.util.TimeZone; + +@Slf4j +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() throws JsonProcessingException { + final ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.findAndRegisterModules(); + objectMapper.registerModule(new Jdk8Module()); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + objectMapper.setTimeZone(TimeZone.getTimeZone("UTC")); + log.debug("current time is {}", objectMapper.writeValueAsString(new Date())); + return objectMapper; + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/config/ReadyConfig.java b/fda-user-service/services/src/main/java/at/tuwien/config/ReadyConfig.java new file mode 100644 index 0000000000..2250fa5088 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/config/ReadyConfig.java @@ -0,0 +1,25 @@ +package at.tuwien.config; + +import com.google.common.io.Files; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; + +import java.io.File; +import java.io.IOException; + +@Log4j2 +@Configuration +public class ReadyConfig { + + @Value("${fda.ready.path}") + private String readyPath; + + @EventListener(ApplicationReadyEvent.class) + public void init() throws IOException { + Files.touch(new File(readyPath)); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java new file mode 100644 index 0000000000..0a2af8f294 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -0,0 +1,87 @@ +package at.tuwien.config; + +import at.tuwien.auth.AuthTokenFilter; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import javax.servlet.http.HttpServletResponse; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@SecurityScheme( + name = "bearerAuth", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + scheme = "bearer" +) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Bean + public AuthTokenFilter authTokenFilter() { + return new AuthTokenFilter(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + /* enable CORS and disable CSRF */ + http = http.cors().and().csrf().disable(); + /* set session management to stateless */ + http = http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and(); + /* set unauthorized requests exception handler */ + http = http + .exceptionHandling() + .authenticationEntryPoint( + (request, response, ex) -> { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, + ex.getMessage() + ); + } + ).and(); + /* set permissions on endpoints */ + http.authorizeRequests() + /* our internal endpoints */ + .antMatchers(HttpMethod.GET, "/actuator/prometheus/**").permitAll() + /* our public endpoints */ + .antMatchers(HttpMethod.GET, "/api/user/**").permitAll() + .antMatchers(HttpMethod.POST, "/api/user/**").permitAll() + .antMatchers("/v3/api-docs.yaml", + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html").permitAll() + /* our private endpoints */ + .anyRequest().authenticated(); + /* add JWT token filter */ + http.addFilterBefore(authTokenFilter(), + UsernamePasswordAuthenticationFilter.class + ); + } + + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/AmqpException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/AmqpException.java new file mode 100644 index 0000000000..6af0750d6f --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/AmqpException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_ACCEPTABLE) +public class AmqpException extends Exception { + + public AmqpException(String msg) { + super(msg); + } + + public AmqpException(String msg, Throwable thr) { + super(msg, thr); + } + + public AmqpException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/ColumnParseException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/ColumnParseException.java new file mode 100644 index 0000000000..c0c1e109de --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/ColumnParseException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) +public class ColumnParseException extends Exception { + + public ColumnParseException(String msg) { + super(msg); + } + + public ColumnParseException(String msg, Throwable thr) { + super(msg, thr); + } + + public ColumnParseException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java new file mode 100644 index 0000000000..85d49d4cb3 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND) +public class ContainerNotFoundException extends Exception { + + public ContainerNotFoundException(String msg) { + super(msg); + } + + public ContainerNotFoundException(String msg, Throwable thr) { + super(msg, thr); + } + + public ContainerNotFoundException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseConnectionException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseConnectionException.java new file mode 100644 index 0000000000..3c1f797647 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseConnectionException.java @@ -0,0 +1,23 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.io.IOException; + +@ResponseStatus(code = HttpStatus.METHOD_NOT_ALLOWED) +public class DatabaseConnectionException extends Exception { + + public DatabaseConnectionException(String msg) { + super(msg); + } + + public DatabaseConnectionException(String msg, Throwable thr) { + super(msg, thr); + } + + public DatabaseConnectionException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java new file mode 100644 index 0000000000..b9ca79c783 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java @@ -0,0 +1,23 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.io.IOException; + +@ResponseStatus(code = HttpStatus.NOT_FOUND) +public class DatabaseNotFoundException extends Exception { + + public DatabaseNotFoundException(String msg) { + super(msg); + } + + public DatabaseNotFoundException(String msg, Throwable thr) { + super(msg, thr); + } + + public DatabaseNotFoundException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/FileStorageException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/FileStorageException.java new file mode 100644 index 0000000000..ab068e4245 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/FileStorageException.java @@ -0,0 +1,20 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.BAD_REQUEST) +public class FileStorageException extends Exception { + + public FileStorageException(String msg) { + super(msg); + } + + public FileStorageException(String msg, Throwable thr) { + super(msg, thr); + } + + public FileStorageException(Throwable thr) { + super(thr); + } +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/IdentifierNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/IdentifierNotFoundException.java new file mode 100644 index 0000000000..f0bb71f364 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/IdentifierNotFoundException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND) +public class IdentifierNotFoundException extends Exception { + + public IdentifierNotFoundException(String msg) { + super(msg); + } + + public IdentifierNotFoundException(String msg, Throwable thr) { + super(msg, thr); + } + + public IdentifierNotFoundException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/ImageNotSupportedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/ImageNotSupportedException.java new file mode 100644 index 0000000000..70963128f8 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/ImageNotSupportedException.java @@ -0,0 +1,23 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.io.IOException; + +@ResponseStatus(code = HttpStatus.CONFLICT) +public class ImageNotSupportedException extends Exception { + + public ImageNotSupportedException(String msg) { + super(msg); + } + + public ImageNotSupportedException(String msg, Throwable thr) { + super(msg, thr); + } + + public ImageNotSupportedException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java new file mode 100644 index 0000000000..44c3d430f9 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.METHOD_NOT_ALLOWED) +public class NotAllowedException extends Exception { + + public NotAllowedException(String msg) { + super(msg); + } + + public NotAllowedException(String msg, Throwable thr) { + super(msg, thr); + } + + public NotAllowedException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/PaginationException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/PaginationException.java new file mode 100644 index 0000000000..9d56aec9c2 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/PaginationException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.BAD_REQUEST) +public class PaginationException extends Exception { + + public PaginationException(String msg) { + super(msg); + } + + public PaginationException(String msg, Throwable thr) { + super(msg, thr); + } + + public PaginationException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/QueryAlreadyPersistedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryAlreadyPersistedException.java new file mode 100644 index 0000000000..48d3bb0ad9 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryAlreadyPersistedException.java @@ -0,0 +1,19 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.CONFLICT) +public class QueryAlreadyPersistedException extends Exception { + + public QueryAlreadyPersistedException(String msg) { + super(msg); + } + + public QueryAlreadyPersistedException(String msg, Throwable thr) { + super(msg, thr); + } + + public QueryAlreadyPersistedException(Throwable thr) { super(thr); + } +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java new file mode 100644 index 0000000000..3d81b6ba4e --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java @@ -0,0 +1,23 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.io.IOException; + +@ResponseStatus(code = HttpStatus.BAD_REQUEST) +public class QueryMalformedException extends Exception { + + public QueryMalformedException(String msg) { + super(msg); + } + + public QueryMalformedException(String msg, Throwable thr) { + super(msg, thr); + } + + public QueryMalformedException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java new file mode 100644 index 0000000000..a5e9075489 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND) +public class QueryNotFoundException extends Exception { + + public QueryNotFoundException(String msg) { + super(msg); + } + + public QueryNotFoundException(String msg, Throwable thr) { + super(msg, thr); + } + + public QueryNotFoundException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java new file mode 100644 index 0000000000..b1f472f2a1 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java @@ -0,0 +1,19 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.GATEWAY_TIMEOUT) +public class QueryStoreException extends Exception { + + public QueryStoreException(String msg) { + super(msg); + } + + public QueryStoreException(String msg, Throwable thr) { + super(msg, thr); + } + + public QueryStoreException(Throwable thr) { super(thr); + } +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/SortException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/SortException.java new file mode 100644 index 0000000000..7415590ad6 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/SortException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.BAD_REQUEST) +public class SortException extends Exception { + + public SortException(String msg) { + super(msg); + } + + public SortException(String msg, Throwable thr) { + super(msg, thr); + } + + public SortException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java new file mode 100644 index 0000000000..542c789ad5 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.LOCKED) +public class TableMalformedException extends Exception { + + public TableMalformedException(String msg) { + super(msg); + } + + public TableMalformedException(String msg, Throwable thr) { + super(msg, thr); + } + + public TableMalformedException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java new file mode 100644 index 0000000000..89fa3ed467 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND) +public class TableNotFoundException extends Exception { + + public TableNotFoundException(String msg) { + super(msg); + } + + public TableNotFoundException(String msg, Throwable thr) { + super(msg, thr); + } + + public TableNotFoundException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/TupleDeleteException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/TupleDeleteException.java new file mode 100644 index 0000000000..87e5e4a483 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/TupleDeleteException.java @@ -0,0 +1,19 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.CONFLICT) +public class TupleDeleteException extends Exception { + + public TupleDeleteException(String msg) { + super(msg); + } + + public TupleDeleteException(String msg, Throwable thr) { + super(msg, thr); + } + + public TupleDeleteException(Throwable thr) { super(thr); + } +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java new file mode 100644 index 0000000000..0abb87f609 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "User not found") +public class UserNotFoundException extends Exception { + + public UserNotFoundException(String message) { + super(message); + } + + public UserNotFoundException(String message, Throwable thr) { + super(message, thr); + } + + public UserNotFoundException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java new file mode 100644 index 0000000000..5dfdaf170e --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.LOCKED) +public class ViewMalformedException extends Exception { + + public ViewMalformedException(String msg) { + super(msg); + } + + public ViewMalformedException(String msg, Throwable thr) { + super(msg, thr); + } + + public ViewMalformedException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java new file mode 100644 index 0000000000..2f260975ff --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "View not found") +public class ViewNotFoundException extends Exception { + + public ViewNotFoundException(String message) { + super(message); + } + + public ViewNotFoundException(String message, Throwable thr) { + super(message, thr); + } + + public ViewNotFoundException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java new file mode 100644 index 0000000000..cba134ae89 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java @@ -0,0 +1,31 @@ +package at.tuwien.mapper; + +import at.tuwien.api.auth.SignupRequestDto; +import at.tuwien.api.user.GrantedAuthorityDto; +import at.tuwien.api.user.UserBriefDto; +import at.tuwien.api.user.UserDetailsDto; +import at.tuwien.api.user.UserDto; +import at.tuwien.entities.user.User; +import org.mapstruct.Mapper; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +@Mapper(componentModel = "spring") +public interface UserMapper { + + org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserMapper.class); + + UserDetailsDto userDtoToUserDetailsDto(UserDto data); + + UserDto userToUserDto(User data); + + UserBriefDto userToUserBriefDto(User data); + + User signupRequestDtoToUser(SignupRequestDto data); + + default GrantedAuthority grantedAuthorityDtoToGrantedAuthority(GrantedAuthorityDto data) { + final GrantedAuthority authority = new SimpleGrantedAuthority(data.getAuthority()); + log.trace("mapped granted authority {} to granted authority {}", data, authority); + return authority; + } +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserRepository.java b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserRepository.java new file mode 100644 index 0000000000..e77de8f6a8 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserRepository.java @@ -0,0 +1,14 @@ +package at.tuwien.repository.jpa; + +import at.tuwien.entities.user.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository<User, String> { + + Optional<User> findByUsername(String username); + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java b/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java new file mode 100644 index 0000000000..d9371a4695 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java @@ -0,0 +1,39 @@ +package at.tuwien.service; + +import at.tuwien.api.auth.SignupRequestDto; +import at.tuwien.entities.container.Container; +import at.tuwien.entities.user.User; +import at.tuwien.exception.UserNotFoundException; + +import java.security.Principal; +import java.util.List; + +public interface UserService { + + /** + * Finds all users + * + * @return The list of users. + */ + List<User> findAll(); + + /** + * Finds a user by username. + * + * @param username The username. + * @return The user. + * @throws UserNotFoundException The user was not found in the metadata database. + */ + User findByUsername(String username) throws UserNotFoundException; + + User create(SignupRequestDto data); + + /** + * Finds a user by id. + * + * @param id The id. + * @return The user. + * @throws UserNotFoundException The user was not found in the metadata database. + */ + User find(String id) throws UserNotFoundException; +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java new file mode 100644 index 0000000000..5578d9186b --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java @@ -0,0 +1,66 @@ +package at.tuwien.service.impl; + +import at.tuwien.api.auth.SignupRequestDto; +import at.tuwien.entities.user.User; +import at.tuwien.exception.UserNotFoundException; +import at.tuwien.mapper.UserMapper; +import at.tuwien.repository.jpa.UserRepository; +import at.tuwien.service.UserService; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Log4j2 +@Service +public class UserServiceImpl implements UserService { + + private final UserMapper userMapper; + private final UserRepository userRepository; + + @Autowired + public UserServiceImpl(UserMapper userMapper, UserRepository userRepository) { + this.userMapper = userMapper; + this.userRepository = userRepository; + } + + @Override + public List<User> findAll() { + return userRepository.findAll(); + } + + @Override + public User findByUsername(String username) throws UserNotFoundException { + final Optional<User> optional = userRepository.findByUsername(username); + if (optional.isEmpty()) { + log.error("Failed to retrieve user with username {}", username); + throw new UserNotFoundException("Failed to retrieve user"); + } + return optional.get(); + } + + @Override + public User create(SignupRequestDto data) { + final User user = userMapper.signupRequestDtoToUser(data); + user.setRealmId("82c39861-d877-4667-a0f3-4daa2ee230e0"); + user.setEmailVerified(false); + user.setId(UUID.randomUUID().toString()); + final User entity = userRepository.save(user); + log.info("Created user with id {}", entity.getId()); + return entity; + } + + @Override + public User find(String id) throws UserNotFoundException { + final Optional<User> optional = userRepository.findById(id); + if (optional.isEmpty()) { + log.error("Failed to retrieve user with id {}", id); + throw new UserNotFoundException("Failed to retrieve user"); + } + return optional.get(); + } + +} diff --git a/fda-user-service/services/src/test/resources/application.properties b/fda-user-service/services/src/test/resources/application.properties new file mode 100644 index 0000000000..69df4a3123 --- /dev/null +++ b/fda-user-service/services/src/test/resources/application.properties @@ -0,0 +1,14 @@ +# disable discovery +spring.cloud.discovery.enabled = false + +# disable cloud config and config discovery +spring.cloud.config.discovery.enabled = false +spring.cloud.config.enabled = false + +# disable datasource +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=update \ No newline at end of file -- GitLab