Compare commits
7 Commits
master
...
spike/auto
Author | SHA1 | Date | |
---|---|---|---|
|
1af213a95a | ||
|
0675918362 | ||
|
e8c4946111 | ||
|
086fb11436 | ||
|
e7558cdbe8 | ||
|
c2ea66a87f | ||
|
4eceb41ebc |
73
.aliases
73
.aliases
@ -8,20 +8,12 @@ gradleWrapper () {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v unbuffer >/dev/null 2>&1; then
|
||||
# if `unbuffer` is available in PATH, use it to print report file-URIs at the end
|
||||
TEMPFILE=$(mktemp /tmp/gw.XXXXXX)
|
||||
unbuffer ./gradlew "$@" | tee $TEMPFILE
|
||||
echo
|
||||
grep --color=never "Report:" $TEMPFILE
|
||||
rm $TEMPFILE
|
||||
else
|
||||
# if `unbuffer` is not in PATH, simply run gradle
|
||||
./gradlew "$@"
|
||||
echo "HINT: it's suggested to install 'unbuffer' to print report URIs at the end of a gradle run"
|
||||
fi
|
||||
|
||||
TEMPFILE=$(mktemp /tmp/gw.XXXXXX)
|
||||
unbuffer ./gradlew "$@" | tee $TEMPFILE
|
||||
|
||||
echo
|
||||
grep --color=never "Report:" $TEMPFILE
|
||||
rm $TEMPFILE
|
||||
}
|
||||
|
||||
postgresAutodoc () {
|
||||
@ -38,13 +30,13 @@ postgresAutodoc () {
|
||||
fi
|
||||
postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \
|
||||
-m '(rbacobject|hs).*' \
|
||||
-l /usr/share/postgresql-autodoc -t neato &&
|
||||
-l /usr/share/postgresql-autodoc -t neato &&
|
||||
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-hs.svg && \
|
||||
echo "generated: $PWD/build/postgres-autodoc-hs.svg"
|
||||
|
||||
postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \
|
||||
-m '(global|rbac).*' \
|
||||
-l /usr/share/postgresql-autodoc -t neato &&
|
||||
-l /usr/share/postgresql-autodoc -t neato &&
|
||||
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-rbac.svg && \
|
||||
echo "generated $PWD/build/postgres-autodoc-rbac.svg"
|
||||
}
|
||||
@ -90,51 +82,8 @@ alias pg-sql-restore='gunzip --stdout | docker exec -i hsadmin-ng-postgres psql
|
||||
alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l'
|
||||
|
||||
alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources'
|
||||
alias gw-check='. .aliases; . .tc-environment; gw test check -x pitest'
|
||||
|
||||
# HOWTO: run all 'normal' tests (no scenario+import-tests): `gw-test`
|
||||
# You can also mention specific targets: `gw-test importOfficeData`.
|
||||
# This will always use the environment from `.tc-environment`.
|
||||
#
|
||||
# HOWTO: re-run tests even if no changed can be detected: `gw-test --rerun`
|
||||
# You can also mention specific targets: `gw-test scenarioTest --rerun`.
|
||||
# This will always use the environment from `.tc-environment`.
|
||||
#
|
||||
# HOWTO: run all tests (unit, integration+acceptance, import and scenario): `gw-test --all`
|
||||
# You can also re-run all these tests, which will take ~20min: `gw-test --all --rerun`
|
||||
# This will always use the environment from `.tc-environment`.
|
||||
#
|
||||
function _gwTest1() {
|
||||
echo
|
||||
printf -- '=%0.s' {1..80}; echo
|
||||
echo "RUNNING gw $@"
|
||||
printf -- '-%0.s' {1..80}; echo
|
||||
./gradlew "$@"
|
||||
printf -- '-%0.s' {1..80}; echo
|
||||
echo "DONE gw $@"
|
||||
}
|
||||
function _gwTest() {
|
||||
. .aliases;
|
||||
. .tc-environment;
|
||||
rm /tmp/gwTest.tmp
|
||||
if [ "$1" == "--all" ]; then
|
||||
shift # to remove the --all from $@
|
||||
# delierately in separate gradlew-calls to avoid Testcontains-PostgreSQL problem spillover
|
||||
time (_gwTest1 unitTest "$@" &&
|
||||
_gwTest1 officeIntegrationTest bookingIntegrationTest hostingIntegrationTest "$@" &&
|
||||
_gwTest1 scenarioTest "$@" &&
|
||||
_gwTest1 importOfficeData importHostingAssets "$@");
|
||||
elif [ $# -eq 0 ] || [[ $1 == -* ]]; then
|
||||
time _gwTest1 test "$@";
|
||||
else
|
||||
time _gwTest1 "$@";
|
||||
fi
|
||||
printf -- '=%0.s' {1..80}; echo
|
||||
}
|
||||
alias gw-test=_gwTest
|
||||
|
||||
alias howto=bin/howto
|
||||
alias cas-curl=bin/cas-curl
|
||||
alias gw-test='. .aliases; ./gradlew test'
|
||||
alias gw-check='. .aliases; gw test importOfficeData check -x pitest -x :dependencyCheckAnalyze'
|
||||
|
||||
# etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries
|
||||
alias gw-importOfficeData-in-docker-compose='
|
||||
@ -146,7 +95,3 @@ if [ ! -f .environment ]; then
|
||||
cp .tc-environment .environment
|
||||
fi
|
||||
source .environment
|
||||
|
||||
alias scenario-reports-upload='./gradlew scenarioTest convertMarkdownToHtml && ssh hsh03-hsngdev@h50.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@h50.hostsharing.net:doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office'
|
||||
alias scenario-reports-open='open https://hsngdev.hs-example.de/scenarios/office'
|
||||
|
||||
|
@ -4,4 +4,5 @@ export HSADMINNG_POSTGRES_ADMIN_PASSWORD=
|
||||
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
|
||||
export HSADMINNG_SUPERUSER=superuser-alex@hostsharing.net
|
||||
export HSADMINNG_MIGRATION_DATA_PATH=migration
|
||||
export LIQUIBASE_CONTEXT=
|
||||
export LANG=en_US.UTF-8
|
||||
|
@ -4,4 +4,5 @@ unset HSADMINNG_POSTGRES_ADMIN_PASSWORD
|
||||
unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME
|
||||
unset HSADMINNG_SUPERUSER
|
||||
unset HSADMINNG_MIGRATION_DATA_PATH
|
||||
unset LIQUIBASE_CONTEXT
|
||||
|
||||
|
103
Jenkinsfile
vendored
103
Jenkinsfile
vendored
@ -1,103 +0,0 @@
|
||||
pipeline {
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'etc/jenkinsAgent.Dockerfile'
|
||||
// additionalBuildArgs ...
|
||||
args '--network=bridge --user root -v $PWD:$PWD \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock --group-add 984 \
|
||||
--memory=6g --cpus=3'
|
||||
}
|
||||
}
|
||||
|
||||
environment {
|
||||
DOCKER_HOST = 'unix:///var/run/docker.sock'
|
||||
HSADMINNG_POSTGRES_ADMIN_USERNAME = 'admin'
|
||||
HSADMINNG_POSTGRES_RESTRICTED_USERNAME = 'restricted'
|
||||
HSADMINNG_MIGRATION_DATA_PATH = 'migration'
|
||||
}
|
||||
|
||||
triggers {
|
||||
pollSCM('H/1 * * * *')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage ('Compile') {
|
||||
steps {
|
||||
sh './gradlew clean processSpring compileJava compileTestJava --no-daemon'
|
||||
}
|
||||
}
|
||||
|
||||
stage ('Tests') {
|
||||
parallel {
|
||||
stage('Unit-Tests') {
|
||||
steps {
|
||||
sh './gradlew unitTest --no-daemon'
|
||||
}
|
||||
}
|
||||
stage('General-Tests') {
|
||||
steps {
|
||||
sh './gradlew generalTest --no-daemon'
|
||||
}
|
||||
}
|
||||
stage('Office-Tests') {
|
||||
steps {
|
||||
sh './gradlew officeIntegrationTest --no-daemon'
|
||||
}
|
||||
}
|
||||
stage('Booking+Hosting-Tests') {
|
||||
steps {
|
||||
sh './gradlew bookingIntegrationTest hostingIntegrationTest --no-daemon'
|
||||
}
|
||||
}
|
||||
stage('Import-Tests') {
|
||||
steps {
|
||||
sh './gradlew importOfficeData importHostingAssets --no-daemon'
|
||||
}
|
||||
}
|
||||
stage ('Scenario-Tests') {
|
||||
steps {
|
||||
sh './gradlew scenarioTest --no-daemon'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage ('Check') {
|
||||
steps {
|
||||
sh './gradlew check -x pitest -x dependencyCheckAnalyze --no-daemon'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
// archive test results
|
||||
junit 'build/test-results/test/*.xml'
|
||||
|
||||
// archive the JaCoCo coverage report in XML and HTML format
|
||||
jacoco(
|
||||
execPattern: 'build/jacoco/*.exec',
|
||||
classPattern: 'build/classes/java/main',
|
||||
sourcePattern: 'src/main/java'
|
||||
)
|
||||
|
||||
// archive scenario-test reports in HTML format
|
||||
sh '''
|
||||
./gradlew convertMarkdownToHtml
|
||||
'''
|
||||
archiveArtifacts artifacts:
|
||||
'build/doc/scenarios/*.html, ' +
|
||||
'build/reports/dependency-license/dependencies-without-allowed-license.json',
|
||||
allowEmptyArchive: true
|
||||
|
||||
// cleanup workspace
|
||||
cleanWs()
|
||||
}
|
||||
}
|
||||
}
|
256
README.md
256
README.md
@ -5,48 +5,41 @@ For architecture consider the files in the `doc` and `adr` folder.
|
||||
|
||||
<!-- generated TOC begin: -->
|
||||
- [Setting up the Development Environment](#setting-up-the-development-environment)
|
||||
- [PostgreSQL Server](#postgresql-server)
|
||||
- [Markdown](#markdown)
|
||||
- [Render Markdown embedded PlantUML](#render-markdown-embedded-plantuml)
|
||||
- [Render Markdown Embedded Mermaid Diagrams](#render-markdown-embedded-mermaid-diagrams)
|
||||
- [IDE Specific Settings](#ide-specific-settings)
|
||||
- [IntelliJ IDEA](#intellij-idea)
|
||||
- [Other Tools](#other-tools)
|
||||
- [PostgreSQL Server](#postgresql-server)
|
||||
- [Markdown](#markdown)
|
||||
- [Render Markdown embedded PlantUML](#render-markdown-embedded-plantuml)
|
||||
- [Render Markdown Embedded Mermaid Diagrams](#render-markdown-embedded-mermaid-diagrams)
|
||||
- [IDE Specific Settings](#ide-specific-settings)
|
||||
- [IntelliJ IDEA](#intellij-idea)
|
||||
- [Other Tools](#other-tools)
|
||||
- [Running the SQL files](#running-the-sql-files)
|
||||
- [For RBAC](#for-rbac)
|
||||
- [For Historization](#for-historization)
|
||||
- [For RBAC](#for-rbac)
|
||||
- [For Historization](#for-historization)
|
||||
- [Coding Guidelines](#coding-guidelines)
|
||||
- [Directory and Package Structure](#directory-and-package-structure)
|
||||
- [General Directory Structure](#general-directory-structure)
|
||||
- [Source Code Package Structure](#source-code-package-structure)
|
||||
- [Run Tests from Command Line](#run-tests-from-command-line)
|
||||
- [Spotless Code Formatting](#spotless-code-formatting)
|
||||
- [JaCoCo Test Code Coverage Check](#jacoco-test-code-coverage-check)
|
||||
- [PiTest Mutation Testing](#pitest-mutation-testing)
|
||||
- [Remark](#remark)
|
||||
- [OWASP Security Vulnerability Check](#owasp-security-vulnerability-check)
|
||||
- [Dependency-License-Compatibility](#dependency-license-compatibility)
|
||||
- [Dependency Version Upgrade](#dependency-version-upgrade)
|
||||
- [Biggest Flaws in our Architecture](#biggest-flaws-in-our-architecture)
|
||||
- [The RBAC System is too Complicated](#the-rbac-system-is-too-complicated)
|
||||
- [The Mapper is Error-Prone](#the-mapper-is-error-prone)
|
||||
- [Too Many Business-Rules Implemented in Controllers](#too-many-business-rules-implemented-in-controllers)
|
||||
- [Directory and Package Structure](#directory-and-package-structure)
|
||||
- [General Directory Structure](#general-directory-structure)
|
||||
- [Source Code Package Structure](#source-code-package-structure)
|
||||
- [Run Tests from Command Line](#run-tests-from-command-line)
|
||||
- [Spotless Code Formatting](#spotless-code-formatting)
|
||||
- [JaCoCo Test Code Coverage Check](#jacoco-test-code-coverage-check)
|
||||
- [PiTest Mutation Testing](#pitest-mutation-testing)
|
||||
- [Remark](#remark)
|
||||
- [OWASP Security Vulnerability Check](#owasp-security-vulnerability-check)
|
||||
- [Dependency-License-Compatibility](#dependency-license-compatibility)
|
||||
- [Dependency Version Upgrade](#dependency-version-upgrade)
|
||||
- [How To ...](#how-to-...)
|
||||
- [How to Run the Application With Other Profiles, e.g. production](#)
|
||||
- [How to Do a Clean Run of the Application](#how-to-do-a-clean-run-of-the-application)
|
||||
- [How to Configure .pgpass for the Default PostgreSQL Database?](#how-to-configure-.pgpass-for-the-default-postgresql-database?)
|
||||
- [How to Run the Tests Against a Local User-Space Podman Daemon?](#how-to-run-the-tests-against-a-local-user-space-podman-daemon?)
|
||||
- [Install and Run Podman](#install-and-run-podman)
|
||||
- [Use the Command Line to Run the Tests Against the Podman Daemon ](#use-the-command-line-to-run-the-tests-against-the-podman-daemon-)
|
||||
- [Use IntelliJ IDEA Run the Tests Against the Podman Daemon](#use-intellij-idea-run-the-tests-against-the-podman-daemon)
|
||||
- [~/.testcontainers.properties](#~/.testcontainers.properties)
|
||||
- [How to Run the Tests Against a Remote Podman or Docker Daemon?](#how-to-run-the-tests-against-a-remote-podman-or-docker-daemon?)
|
||||
- [How to Run the Application on a Different Port?](#how-to-run-the-application-on-a-different-port?)
|
||||
- [How to Use a Persistent Database for Integration Tests?](#how-to-use-a-persistent-database-for-integration-tests?)
|
||||
- [How to Amend Liquibase SQL Changesets?](#how-to-amend-liquibase-sql-changesets?)
|
||||
- [How to Re-Generate Spring-Controller-Interfaces from OpenAPI specs?](#how-to-re-generate-spring-controller-interfaces-from-openapi-specs?)
|
||||
- [How to Generate Database Table Diagrams?](#how-to-generate-database-table-diagrams?)
|
||||
- [How to Add (Real) Admin Users](#how-to-add-(real)-admin-users)
|
||||
- [How to Configure .pgpass for the Default PostgreSQL Database?](#how-to-configure-.pgpass-for-the-default-postgresql-database?)
|
||||
- [How to Run the Tests Against a Local User-Space Podman Daemon?](#how-to-run-the-tests-against-a-local-user-space-podman-daemon?)
|
||||
- [Install and Run Podman](#install-and-run-podman)
|
||||
- [Use the Command Line to Run the Tests Against the Podman Daemon ](#use-the-command-line-to-run-the-tests-against-the-podman-daemon-)
|
||||
- [Use IntelliJ IDEA Run the Tests Against the Podman Daemon](#use-intellij-idea-run-the-tests-against-the-podman-daemon)
|
||||
- [~/.testcontainers.properties](#~/.testcontainers.properties)
|
||||
- [How to Run the Tests Against a Remote Podman or Docker Daemon?](#how-to-run-the-tests-against-a-remote-podman-or-docker-daemon?)
|
||||
- [How to Run the Application on a Different Port?](#how-to-run-the-application-on-a-different-port?)
|
||||
- [How to Use a Persistent Database for Integration Tests?](#how-to-use-a-persistent-database-for-integration-tests?)
|
||||
- [How to Amend Liquibase SQL Changesets?](#how-to-amend-liquibase-sql-changesets?)
|
||||
- [How to Re-Generate Spring-Controller-Interfaces from OpenAPI specs?](#how-to-re-generate-spring-controller-interfaces-from-openapi-specs?)
|
||||
- [How to Generate Database Table Diagrams?](#how-to-generate-database-table-diagrams?)
|
||||
- [Further Documentation](#further-documentation)
|
||||
<!-- generated TOC end. -->
|
||||
|
||||
@ -58,73 +51,45 @@ Everything is tested on _Ubuntu Linux 22.04_ and _MacOS Monterey (12.4)_.
|
||||
To be able to build and run the Java Spring Boot application, you need the following tools:
|
||||
|
||||
- Docker 20.x (on MacOS you also need *Docker Desktop* or similar) or Podman
|
||||
- optionally: PostgreSQL Server 15.5-bookworm, if you want to use the database directly, not just via Docker
|
||||
- optionally: PostgreSQL Server 15.5-bookworm
|
||||
(see instructions below to install and run in Docker)
|
||||
- The matching Java JDK at will be automatically installed by Gradle toolchain support to `~/.gradle/jdks/`.
|
||||
- You also might need an IDE (e.g. *IntelliJ IDEA* or *Eclipse* or *VS Code* with *[STS](https://spring.io/tools)* and a GUI Frontend for *PostgreSQL* like *Postbird*.
|
||||
- Python 3 is expected in /usr/bin/python3 if you want to run the `howto` tool (see `bin/howto`)
|
||||
|
||||
If you have at least Docker and the Java JDK installed in appropriate versions and in your `PATH`, then you can start like this:
|
||||
|
||||
cd your-hsadmin-ng-directory
|
||||
|
||||
source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew'
|
||||
gw # initially downloads the configured Gradle version into the project
|
||||
source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew'
|
||||
gw # initially downloads the configured Gradle version into the project
|
||||
|
||||
gw test # compiles and runs unit- and integration-tests - takes >10min even on a fast machine
|
||||
# `gw test` does NOT run import- and scenario-tests.
|
||||
# Use `gw-test` instead to make sure .tc-environment is sourced.
|
||||
gw scenarioTest # compiles and scenario-tests - takes ~1min on a decent machine
|
||||
# Use `gw-test scenarioTest` instead to make sure .tc-environment is sourced.
|
||||
|
||||
howto test # shows more test information about how to run tests
|
||||
gw test # compiles and runs unit- and integration-tests
|
||||
|
||||
# if the container has not been built yet, run this:
|
||||
pg-sql-run # downloads + runs PostgreSQL in a Docker container on localhost:5432
|
||||
|
||||
# if the container has been built already and you want to keep the data, run this:
|
||||
pg-sql-run # downloads + runs PostgreSQL in a Docker container on localhost:5432
|
||||
# if the container has been built already, run this:
|
||||
pg-sql-start
|
||||
|
||||
Next, compile and run the application on `localhost:8080` and the management server on `localhost:8081`:
|
||||
|
||||
# this disables CAS-authentication, for using the REST-API with CAS-authentication, see `bin/cas-curl`.
|
||||
export HSADMINNG_CAS_SERVER=
|
||||
|
||||
# this runs the application with test-data and all modules:
|
||||
gw bootRun --args='--spring.profiles.active=dev,complete,test-data'
|
||||
|
||||
The meaning of these profiles is:
|
||||
|
||||
- **dev**: the PostgreSQL users are created via Liquibase
|
||||
- **complete**: all modules are started
|
||||
- **test-data**: some test data inserted
|
||||
|
||||
Running just `gw bootRun` would just run the *office* module, not insert any test-data and
|
||||
require the PostgreSQL users created in the database (see env-vars in `.aliases`).
|
||||
|
||||
Now we can access the REST API, e.g. using curl:
|
||||
gw bootRun # compiles and runs the application on localhost:8080
|
||||
|
||||
# the following command should reply with "pong":
|
||||
curl -f -s http://localhost:8080/api/ping
|
||||
curl http://localhost:8080/api/ping
|
||||
|
||||
# the following command should return a JSON array with just all customers:
|
||||
curl -f -s\
|
||||
curl \
|
||||
-H 'current-subject: superuser-alex@hostsharing.net' \
|
||||
http://localhost:8080/api/test/customers \
|
||||
| jq # just if `jq` is installed, to prettyprint the output
|
||||
http://localhost:8080/api/test/customers
|
||||
|
||||
# the following command should return a JSON array with just all packages visible for the admin of the customer yyy:
|
||||
curl -f -s\
|
||||
curl \
|
||||
-H 'current-subject: superuser-alex@hostsharing.net' -H 'assumed-roles: rbactest.customer#yyy:ADMIN' \
|
||||
http://localhost:8080/api/test/packages \
|
||||
| jq
|
||||
http://localhost:8080/api/test/packages
|
||||
|
||||
# add a new customer
|
||||
curl -f -s\
|
||||
curl \
|
||||
-H 'current-subject: superuser-alex@hostsharing.net' -H "Content-Type: application/json" \
|
||||
-d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \
|
||||
-X POST http://localhost:8080/api/test/customers \
|
||||
| jq
|
||||
-X POST http://localhost:8080/api/test/customers
|
||||
|
||||
If you wonder who 'superuser-alex@hostsharing.net' and 'superuser-fran@hostsharing.net' are and where the data comes from:
|
||||
Mike and Sven are just example global admin accounts as part of the example data which is automatically inserted in Testcontainers and Development environments.
|
||||
@ -132,7 +97,7 @@ Also try for example 'admin@xxx.example.com' or 'unknown@example.org'.
|
||||
|
||||
If you want a formatted JSON output, you can pipe the result to `jq` or similar.
|
||||
|
||||
And to see the full, currently implemented, API, open http://localhost:8081/actuator/swagger-ui/index.html (uses management-port and thus bypasses authentication).
|
||||
And to see the full, currently implemented, API, open http://localhost:8080/swagger-ui/index.html.
|
||||
|
||||
If you still need to install some of these tools, find some hints in the next chapters.
|
||||
|
||||
@ -212,7 +177,7 @@ To generate the TOC (Table of Contents), a little bash script from a
|
||||
Given this is in PATH as `md-toc`, use:
|
||||
|
||||
```shell
|
||||
md-toc <README.md 2 4 | cut -c5-
|
||||
md-toc <README.md 2 4 | cut -c5-'
|
||||
```
|
||||
|
||||
To render the Markdown files, especially to watch embedded PlantUML diagrams, you can use one of the following methods:
|
||||
@ -450,42 +415,36 @@ Some of these rules are checked with *ArchUnit* unit tests.
|
||||
|
||||
### Run Tests from Command Line
|
||||
|
||||
Run all unit-, integration- and acceptance-tests which have not yet been passed with the current source code:
|
||||
Run all tests which have not yet been passed with the current source code:
|
||||
|
||||
```shell
|
||||
gw test # uses the current environment, especially HSADMINNG_POSTGRES_JDBC_URL
|
||||
```
|
||||
|
||||
If the referenced database is not empty, the tests might fail.
|
||||
|
||||
To explicitly use the Testcontainers-environment, run:
|
||||
|
||||
```shell
|
||||
gw-test # uses the environment from .tc-environment
|
||||
gw test
|
||||
```
|
||||
|
||||
Force running all tests:
|
||||
|
||||
```shell
|
||||
gw-test --rerun
|
||||
gw cleanTest test
|
||||
```
|
||||
|
||||
To find more options about running tests, try `howto test`.
|
||||
|
||||
|
||||
### Spotless Code Formatting
|
||||
|
||||
Code formatting for Java is checked via *spotless*.
|
||||
The formatting style can be checked with this command:
|
||||
|
||||
```shell
|
||||
gw spotlessCheck
|
||||
```
|
||||
|
||||
This task is also included in `gw build` and `gw check`.
|
||||
|
||||
To apply formatting rules, use:
|
||||
|
||||
```shell
|
||||
gw-spotless
|
||||
gw spotlessApply
|
||||
```
|
||||
|
||||
The gradle task spotlessCheck is also included in `gw build` and `gw check`,
|
||||
thus if the formatting is not compliant to the rules, the build is going to fail.
|
||||
|
||||
|
||||
### JaCoCo Test Code Coverage Check
|
||||
|
||||
This project uses the JaCoCo test code coverage report with limit checks.
|
||||
@ -523,12 +482,13 @@ Classes to be scanned, tests to be executed and thresholds are configured in [bu
|
||||
A report is generated under [build/reports/pitest/index.html](./build/reports/pitest/index.html).
|
||||
A link to the report is also printed after the `pitest` run.
|
||||
|
||||
<!-- TODO.test: This task is also executed as part of `gw check`. -->
|
||||
This task is also executed as part of `gw check`.
|
||||
|
||||
#### Remark
|
||||
|
||||
In this project, there is a large amount of code is in *plsql*, especially for RBAC.
|
||||
*Java* ist mostly used for mapping and validating REST calls to database queries.
|
||||
In this project, there is little business logic in *Java* code;
|
||||
most business code is in *plsql*
|
||||
and *Java* ist mostly used for mapping REST calls to database queries.
|
||||
This mapping ist mostly done through *Spring* annotations and other implicit code.
|
||||
|
||||
Therefore, there are only few unit tests and thus mutation testing has limited value.
|
||||
@ -537,19 +497,9 @@ We'll see if this changes when the project progresses and more validations are a
|
||||
|
||||
### OWASP Security Vulnerability Check
|
||||
|
||||
An OWASP security vulnerability is configured, but you need an API key.
|
||||
Fetch it from https://nvd.nist.gov/developers/request-an-api-key.
|
||||
|
||||
Then add it to your `~/.gradle/gradle.properties` file:
|
||||
|
||||
```
|
||||
OWASP_API_KEY=........-....-....-....-............
|
||||
```
|
||||
|
||||
Now you can run the dependency vulnerability check:
|
||||
An OWASP security vulnerability is configured and can be utilized by running:
|
||||
|
||||
```shell
|
||||
gw dependencyCheckUpdate
|
||||
gw dependencyCheckAnalyze
|
||||
```
|
||||
|
||||
@ -562,7 +512,7 @@ In case of suppression, a note must be added to explain why it does not apply to
|
||||
|
||||
See also: https://jeremylong.github.io/DependencyCheck/dependency-check-gradle/index.html.
|
||||
|
||||
### How to Check Dependency-License-Compatibility
|
||||
### Dependency-License-Compatibility
|
||||
|
||||
The `gw check` phase depends on a dependency-license-compatibility check.
|
||||
If any dependency violates the configured [list of allowed licenses](etc/allowed-licenses.json), the build will fail.
|
||||
@ -592,7 +542,7 @@ The generated license can be found here: [index.html](build/reports/dependency-l
|
||||
|
||||
More information can be found on the [project's website](https://github.com/jk1/Gradle-License-Report).
|
||||
|
||||
### How to Upgrade Versions of Dependencies
|
||||
### Dependency Version Upgrade
|
||||
|
||||
Dependency versions can be automatically upgraded to the latest available version:
|
||||
|
||||
@ -615,14 +565,13 @@ that and creates too many (grant- and role-) rows and too even tables which coul
|
||||
|
||||
The basic idea is always to always have a fixed set of ordered role-types which apply for all DB-tables under RBAC,
|
||||
e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER.
|
||||
Grants between these for the same DB-row would be implicit by order comparison.
|
||||
Grants between these for the same DB-row would be implicit by order comparision.
|
||||
This way we would get rid of all explicit grants within the same DB-row
|
||||
and would not need the `rbac.role` table anymore.
|
||||
We would also reduce the depth of the expensive recursive CTE-query.
|
||||
|
||||
This has to be explored further. For now, we just keep it in mind and avoid roles+grants
|
||||
which would not fit into a simplified system with a fixed role-type-system.
|
||||
|
||||
This has to be explored further.
|
||||
For now, we just keep it in mind and
|
||||
|
||||
### The Mapper is Error-Prone
|
||||
|
||||
@ -632,50 +581,8 @@ E.g. the uuid of the target main object is often taken from an uuid of a sub-sub
|
||||
(For now, use `StrictMapper` to avoid this, for the case it happens.)
|
||||
|
||||
|
||||
### Too Many Business-Rules Implemented in Controllers
|
||||
|
||||
Some REST-Controllers implement too much code for business-roles.
|
||||
This should be extracted to services.
|
||||
|
||||
|
||||
## How To ...
|
||||
|
||||
Besides the following *How Tos* you can also find several *How Tos* in the source code:
|
||||
|
||||
```sh
|
||||
grep -r HOWTO src
|
||||
```
|
||||
|
||||
also try this (assumed you've sourced .aliases):
|
||||
```sh
|
||||
howto
|
||||
```
|
||||
|
||||
### How to Run the Application With Other Profiles, e.g. production:
|
||||
|
||||
Add `--args='--spring.profiles.active=...` with the wanted profile selector:
|
||||
|
||||
```sh
|
||||
gw bootRun --args='--spring.profiles.active=external-db,only -office,without-test-data'
|
||||
```
|
||||
|
||||
These profiles mean:
|
||||
|
||||
- **external-db**: an external PostgreSQL database is used with the PostgreSQL users already created as specified in the environment
|
||||
- **only-office**: only the Office module is started, but neither the Booking nor the Hosting modules
|
||||
- **without-test-data**: no test-data is inserted
|
||||
|
||||
|
||||
### How to Do a Clean Run of the Application
|
||||
|
||||
If you frequently need to run with a fresh database and a clean build, you can use this:
|
||||
|
||||
```sh
|
||||
export HSADMINNG_CAS_SERVER=
|
||||
gw clean && pg-sql-reset && sleep 5 && gw bootRun' 2>&1 | tee log
|
||||
```
|
||||
|
||||
|
||||
### How to Configure .pgpass for the Default PostgreSQL Database?
|
||||
|
||||
To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory:
|
||||
@ -874,29 +781,6 @@ postgres-autodoc
|
||||
The output will list the generated files.
|
||||
|
||||
|
||||
### How to Add (Real) Admin Users
|
||||
|
||||
```sql
|
||||
DO $$
|
||||
DECLARE
|
||||
-- replace with your admin account names
|
||||
admin_users TEXT[] := ARRAY['admin-1', 'admin-2', 'admin-3'];
|
||||
admin TEXT;
|
||||
BEGIN
|
||||
-- run as superuser
|
||||
call base.defineContext('adding real admin users', null, null, null);
|
||||
|
||||
-- for all new admin accounts
|
||||
FOREACH admin IN ARRAY admin_users LOOP
|
||||
call rbac.grantRoleToSubjectUnchecked(
|
||||
rbac.findRoleId(rbac.global_ADMIN()), -- granted by role
|
||||
rbac.findRoleId(rbac.global_ADMIN()), -- role to grant
|
||||
rbac.create_subject(admin)); -- creates the new admin account
|
||||
END LOOP;
|
||||
END $$;
|
||||
```
|
||||
|
||||
|
||||
## Further Documentation
|
||||
|
||||
- the `doc` directory contains architecture concepts and a glossary
|
||||
|
249
bin/cas-curl
249
bin/cas-curl
@ -1,249 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$2" == "--show-password" ]; then
|
||||
HSADMINNG_CAS_SHOW_PASSWORD=yes
|
||||
shift
|
||||
else
|
||||
HSADMINNG_CAS_SHOW_PASSWORD=
|
||||
fi
|
||||
|
||||
if [ "$1" == "--trace" ]; then
|
||||
function trace() {
|
||||
echo "$*" >&2
|
||||
}
|
||||
function doCurl() {
|
||||
set -x
|
||||
if [ -z "$HSADMINNG_CAS_ASSUME" ]; then
|
||||
curl --fail-with-body \
|
||||
--header "Authorization: $HSADMINNG_CAS_TICKET" \
|
||||
"$@"
|
||||
else
|
||||
curl --fail-with-body \
|
||||
--header "Authorization: $HSADMINNG_CAS_TICKET" \
|
||||
--header "assumed-roles: $HSADMINNG_CAS_ASSUME" \
|
||||
"$@"
|
||||
fi
|
||||
set +x
|
||||
}
|
||||
shift
|
||||
else
|
||||
function trace() {
|
||||
: # noop
|
||||
}
|
||||
function doCurl() {
|
||||
curl --fail-with-body --header "Authorization: $HSADMINNG_CAS_TICKET" "$@"
|
||||
}
|
||||
fi
|
||||
|
||||
export HSADMINNG_CAS_ASSUME_HEADER
|
||||
if [ -f ~/.cas-curl-assume ]; then
|
||||
HSADMINNG_CAS_ASSUME="$(cat ~/.cas-curl-assume)"
|
||||
else
|
||||
HSADMINNG_CAS_ASSUME=
|
||||
fi
|
||||
|
||||
if [ -z "$HSADMINNG_CAS_LOGIN" ] || [ -z "$HSADMINNG_CAS_VALIDATE" ] || \
|
||||
[ -z "$HSADMINNG_CAS_SERVICE_ID" ]; then
|
||||
cat >&2 <<EOF
|
||||
ERROR: environment incomplete
|
||||
|
||||
please set the following environment variables:
|
||||
export HSADMINNG_CAS_LOGIN=https://login.hostsharing.net/cas/v1/tickets
|
||||
export HSADMINNG_CAS_VALIDATE=https://login.hostsharing.net/cas/proxyValidate
|
||||
export HSADMINNG_CAS_USERNAME=<<optionally, your username, or leave empty after '='>>
|
||||
export HSADMINNG_CAS_PASSWORD=<<optionally, your password, or leave empty after '='>>
|
||||
export HSADMINNG_CAS_SERVICE_ID=https://hsadminng.hostsharing.net:443/
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function casCurlDocumentation() {
|
||||
cat <<EOF
|
||||
curl-wrapper utilizing CAS-authentication for hsadmin-ng
|
||||
usage: $0 [--trace] [--show-password] <<command>> [parameters]
|
||||
|
||||
commands:
|
||||
EOF
|
||||
# filters out help texts (containing double-# and following lines with leading single-#) from the commands itself
|
||||
# (the '' makes sure that this line is not found, just the lines with actual help texts)
|
||||
sed -n '/#''#/ {x; p; x; s/#''#//; p; :a; n; /^[[:space:]]*#/!b; s/^[[:space:]]*#//; p; ba}' <$0
|
||||
}
|
||||
|
||||
function casLogin() {
|
||||
# ticket granting ticket exists and not expired?
|
||||
if find ~/.cas-login-tgt -type f -size +0c -mmin -60 2>/dev/null | grep -q .; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -z "$HSADMINNG_CAS_USERNAME" ]; then
|
||||
read -e -p "Username: " HSADMINNG_CAS_USERNAME
|
||||
fi
|
||||
|
||||
if [ -z "$HSADMINNG_CAS_PASSWORD" ]; then
|
||||
read -s -e -p "Password: " HSADMINNG_CAS_PASSWORD
|
||||
fi
|
||||
|
||||
if [ "$HSADMINNG_CAS_SHOW_PASSWORD" == "--show-password" ]; then
|
||||
HSADMINNG_CAS_PASSWORD_DISPLAY=$HSADMINNG_CAS_PASSWORD
|
||||
else
|
||||
HSADMINNG_CAS_PASSWORD_DISPLAY="<<password hidden - use --show-password to show>>"
|
||||
fi
|
||||
|
||||
# Do NOT use doCurl here! We do neither want to print the password nor pass a CAS service ticket.
|
||||
trace "+ curl --fail-with-body -s -i -X POST \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
-d \"username=$HSADMINNG_CAS_USERNAME&password=$HSADMINNG_CAS_PASSWORD_DISPLAY\" \
|
||||
$HSADMINNG_CAS_LOGIN -o ~/.cas-login-tgt.response -D -"
|
||||
HSADMINNG_CAS_TGT=`curl --fail-with-body -s -i -X POST \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
-d "username=$HSADMINNG_CAS_USERNAME&password=$HSADMINNG_CAS_PASSWORD" \
|
||||
$HSADMINNG_CAS_LOGIN -o ~/.cas-login-tgt.response -D - \
|
||||
| grep -i "^Location: " | sed -e 's/^Location: //' -e 's/\\r//'`
|
||||
if [ -z "$HSADMINNG_CAS_TGT" ]; then
|
||||
echo "ERROR: could not get ticket granting ticket" >&2
|
||||
cat ~/.cas-login-tgt.response >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$HSADMINNG_CAS_TGT" >~/.cas-login-tgt
|
||||
trace "$HSADMINNG_CAS_TGT"
|
||||
}
|
||||
|
||||
function casLogout() {
|
||||
rm -f ~/.cas-login-tgt
|
||||
}
|
||||
|
||||
function casTicket() {
|
||||
HSADMINNG_CAS_TGT=$(<~/.cas-login-tgt)
|
||||
if [[ -z "$HSADMINNG_CAS_TGT" ]]; then
|
||||
echo "ERROR: cannot get CAS ticket granting ticket for $HSADMINNG_CAS_USERNAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
trace "CAS-TGT: $HSADMINNG_CAS_TGT"
|
||||
|
||||
trace "fetching CAS service ticket"
|
||||
trace "curl -s -d \"service=$HSADMINNG_CAS_SERVICE_ID\" $HSADMINNG_CAS_TGT"
|
||||
HSADMINNG_CAS_TICKET=$(curl -s -d "service=$HSADMINNG_CAS_SERVICE_ID" $HSADMINNG_CAS_TGT)
|
||||
if [[ -z "$HSADMINNG_CAS_TICKET" ]]; then
|
||||
echo "ERROR: cannot get CAS service ticket" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo $HSADMINNG_CAS_TICKET
|
||||
}
|
||||
|
||||
function casValidate() {
|
||||
HSADMINNG_CAS_TICKET=`casTicket`
|
||||
|
||||
trace "validating CAS-TICKET: $HSADMINNG_CAS_TICKET"
|
||||
# Do NOT use doCurl here! We do not pass a CAS service ticket.
|
||||
trace curl -i -s $HSADMINNG_CAS_VALIDATE?ticket=${HSADMINNG_CAS_TICKET}\&service=${HSADMINNG_CAS_SERVICE_ID}
|
||||
HSADMINNG_CAS_USER=`curl -i -s $HSADMINNG_CAS_VALIDATE?ticket=${HSADMINNG_CAS_TICKET}\&service=${HSADMINNG_CAS_SERVICE_ID} | grep -oPm1 "(?<=<cas:user>)[^<]+"`
|
||||
if [ -z "$HSADMINNG_CAS_USER" ]; then
|
||||
echo "validation failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "CAS-User: $HSADMINNG_CAS_USER"
|
||||
}
|
||||
|
||||
case "${1,,}" in
|
||||
|
||||
# -- generic commands --------------------------------------------------------------------------
|
||||
|
||||
""|"-h"|"--help"|"help") ## prints documentation about commands and options
|
||||
casCurlDocumentation
|
||||
exit
|
||||
;;
|
||||
|
||||
"env") ## prints all related HSADMINNG_CAS_... environment variables; use '--show-password' to show the password as well
|
||||
# example: cas-curl env --show-password
|
||||
echo "HSADMINNG_CAS_LOGIN: $HSADMINNG_CAS_LOGIN"
|
||||
echo "HSADMINNG_CAS_VALIDATE: $HSADMINNG_CAS_VALIDATE"
|
||||
echo "HSADMINNG_CAS_USERNAME: $HSADMINNG_CAS_USERNAME"
|
||||
if [ "$2" == "--show-password" ]; then
|
||||
echo "HSADMINNG_CAS_PASSWORD: $HSADMINNG_CAS_PASSWORD"
|
||||
elif [ -z "$HSADMINNG_CAS_PASSWORD" ]; then
|
||||
echo "HSADMINNG_CAS_PASSWORD: <<not given>>"
|
||||
else
|
||||
echo "HSADMINNG_CAS_PASSWORD: <<given, but hidden - add --show-password to show>>"
|
||||
fi
|
||||
echo "HSADMINNG_CAS_SERVICE_ID: $HSADMINNG_CAS_SERVICE_ID"
|
||||
;;
|
||||
|
||||
# --- authentication-related commands ------------------------------------------------------------
|
||||
|
||||
"login") ## reads username+password and fetches ticket granting ticket (bypasses HSADMINNG_CAS_USERNAME+HSADMINNG_CAS_PASSWORD)
|
||||
# example: cas-curl login
|
||||
casLogout
|
||||
export HSADMINNG_CAS_USERNAME=
|
||||
export HSADMINNG_CAS_PASSWORD=
|
||||
casLogin
|
||||
;;
|
||||
"assume") ## assumes the given comma-separated roles
|
||||
# example using object-id-name: cas-curl assume 'hs_office.relation#ExampleMandant-with-PARTNER-ExamplePartner:AGENT'
|
||||
# example using object-uuid: cas-curl assume 'hs_office.relation#1d3bc468-c5c8-11ef-9d0d-4751ecfda2b7:AGENT'
|
||||
shift
|
||||
if [ -z "$1" ]; then
|
||||
echo "ERROR: requires comma-separated list of roles to assume" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$1" >~/.cas-curl-assume
|
||||
;;
|
||||
"unassume") ## do not assume any particular role anymore, use the plain user as RBAC subject
|
||||
rm ~/.cas-curl-assume
|
||||
;;
|
||||
"validate") ## validates current ticket granting ticket and prints currently logged in user
|
||||
casValidate
|
||||
;;
|
||||
"logout") ## logout, deletes ticket granting ticket
|
||||
casLogout
|
||||
;;
|
||||
|
||||
# --- HTTP-commands ----------------------------------------------------------------------
|
||||
|
||||
"get") ## HTTP GET, add URL as parameter
|
||||
# example: cas-curl GET http://localhost:8080/api/hs/office/partners/P-10003 | jq
|
||||
# hint: '| jq' is just for human-readable formatted JSON output
|
||||
shift
|
||||
casLogin
|
||||
HSADMINNG_CAS_TICKET=`casTicket`
|
||||
doCurl "$*"
|
||||
;;
|
||||
"post") ## HTTP POST, add curl options to specify the request body and the URL as last parameter
|
||||
# example: cas-curl POST \
|
||||
# -d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \
|
||||
# http://localhost:8080/api/test/customers | jq
|
||||
# hint: '| jq' is just for human-readable formatted JSON output
|
||||
shift
|
||||
casLogin
|
||||
HSADMINNG_CAS_TICKET=`casTicket`
|
||||
doCurl --header "Content-Type: application/json" -X POST "$@"
|
||||
;;
|
||||
"patch") ## HTTP PATCH, add curl options to specify the request body and the URL as last parameterparameter
|
||||
# example: cas-curl PATCH \
|
||||
# -d '{ "reference":80002 }' \
|
||||
# http://localhost:8080/api/test/customers/ae90ac2a-4728-4ca9-802e-a0d0108b2324 | jq
|
||||
# hint: '| jq' is just for human-readable formatted JSON output
|
||||
shift
|
||||
casLogin
|
||||
HSADMINNG_CAS_TICKET=`casTicket`
|
||||
doCurl --header "Content-Type: application/json" -X POST "$*"
|
||||
;;
|
||||
"delete") ## HTTP DELETE, add curl options to specify the request body and the URL as last parameter
|
||||
# example: cas-curl DELETE http://localhost:8080/api/hs/office/persons/ae90ac2a-4728-4ca9-802e-a0d0108b2324
|
||||
shift
|
||||
casLogin
|
||||
HSADMINNG_CAS_TICKET=`casTicket`
|
||||
curl -X POST "$@"
|
||||
;;
|
||||
*)
|
||||
cat >&2 <<EOF
|
||||
unknown command: '$1'
|
||||
valid commands: help, login, logout, validate, get, post, patch, delete
|
||||
EOF
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
|
@ -4,7 +4,8 @@
|
||||
. .aliases
|
||||
|
||||
while true; do
|
||||
git fetch origin >/dev/null
|
||||
echo "Checking for new commits on any branch ..."
|
||||
git fetch origin
|
||||
branch_with_new_commits=`git fetch origin >/dev/null; git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads | grep '\[behind' | cut -d' ' -f1 | head -n1`
|
||||
|
||||
if [ -n "$branch_with_new_commits" ]; then
|
||||
@ -19,11 +20,11 @@ while true; do
|
||||
fi
|
||||
|
||||
echo "building ..."
|
||||
./gradlew gw clean test check -x pitest
|
||||
./gradlew test
|
||||
fi
|
||||
|
||||
# wait 10s with a little animation
|
||||
echo -e -n "\r\033[K waiting for changes (/) ..."
|
||||
echo -e -n " waiting for changes (/) ..."
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K waiting for changes (-) ..."
|
||||
sleep 2
|
||||
|
86
bin/howto
86
bin/howto
@ -1,86 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
import os
|
||||
import sys
|
||||
from urllib.parse import urljoin, quote
|
||||
|
||||
def path_to_file_uri(path):
|
||||
"""
|
||||
Converts a file path to a file URI.
|
||||
Handles absolute and relative paths.
|
||||
"""
|
||||
abs_path = os.path.abspath(path)
|
||||
return urljoin("file://", quote(abs_path))
|
||||
|
||||
def is_binary_file(filepath, chunk_size=1024):
|
||||
"""
|
||||
Prüft, ob eine Datei binär ist, indem sie den Inhalt der Datei auf nicht-druckbare Zeichen untersucht.
|
||||
"""
|
||||
try:
|
||||
with open(filepath, "rb") as file:
|
||||
chunk = file.read(chunk_size)
|
||||
if b"\0" in chunk: # Nullbyte ist ein typisches Zeichen für Binärdateien
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Prüfen, ob Datei binär ist: {filepath}: {e}")
|
||||
return True
|
||||
|
||||
def search_keywords_in_files(keywords):
|
||||
if not keywords:
|
||||
print("Bitte geben Sie mindestens ein Stichwort an.")
|
||||
sys.exit(1)
|
||||
|
||||
# Allowed comment symbols
|
||||
comment_symbols = {"//", "#", ";"}
|
||||
|
||||
for root, dirs, files in os.walk("."):
|
||||
# Ausschließen bestimmter Verzeichnisse
|
||||
dirs[:] = [d for d in dirs if d not in {".git", "build"}]
|
||||
|
||||
for file in files:
|
||||
filepath = os.path.join(root, file)
|
||||
|
||||
# Überspringen von Binärdateien
|
||||
if is_binary_file(filepath):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
for line_number, line in enumerate(lines, start=1):
|
||||
stripped_line = line.lstrip() # Entfernt führende Leerzeichen
|
||||
for symbol in comment_symbols:
|
||||
if stripped_line.startswith(symbol):
|
||||
# Entfernt das Kommentarzeichen und nachfolgende Leerzeichen
|
||||
howtoMatch = stripped_line[len(symbol):].lstrip()
|
||||
if howtoMatch.startswith(("HOWTO ", "HOWTO: ", "How to ")):
|
||||
if all(keyword in howtoMatch.lower() for keyword in keywords):
|
||||
|
||||
# Titelzeile ohne Kommentarzeichen
|
||||
print(howtoMatch.rstrip())
|
||||
|
||||
# Ausgabe nachfolgender Zeilen mit dem gleichen Kommentar-Präfix
|
||||
for subsequent_line in lines[line_number:]:
|
||||
subsequent_line = subsequent_line.lstrip()
|
||||
if subsequent_line.startswith(symbol):
|
||||
# Entfernt Kommentarzeichen aus Folgezeilen
|
||||
print("\t" + subsequent_line[len(symbol):].strip())
|
||||
else:
|
||||
break
|
||||
|
||||
# Link mit Zeilennummer
|
||||
print(f"--> {path_to_file_uri(filepath)}:{line_number}")
|
||||
|
||||
# Abstand zwischen Matches
|
||||
print()
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Lesen der Datei {filepath}: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Verwendung: bin/howto <Stichwort1> <Stichwort2> ...")
|
||||
sys.exit(1)
|
||||
|
||||
search_keywords_in_files([arg.lower() for arg in sys.argv[1:]])
|
221
build.gradle
221
build.gradle
@ -1,20 +1,17 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.4.1'
|
||||
id 'io.spring.dependency-management' version '1.1.7' // manages implicit dependencies
|
||||
id 'io.openapiprocessor.openapi-processor' version '2023.2' // generates Controller-interface and resources from API-spec
|
||||
id 'com.github.jk1.dependency-license-report' version '2.9' // checks dependency-license compatibility
|
||||
id "org.owasp.dependencycheck" version "12.0.1" // checks dependencies for known vulnerabilities
|
||||
id "com.diffplug.spotless" version "7.0.2" // formats + checks formatting for source-code
|
||||
id 'jacoco' // determines code-coverage of tests
|
||||
id 'info.solidsoft.pitest' version '1.15.0' // performs mutation testing
|
||||
id 'se.patrikerdes.use-latest-versions' version '0.2.18' // updates module and plugin versions
|
||||
id 'com.github.ben-manes.versions' version '0.52.0' // determines which dependencies have updates
|
||||
id 'org.springframework.boot' version '3.2.4'
|
||||
id 'io.spring.dependency-management' version '1.1.4'
|
||||
id 'io.openapiprocessor.openapi-processor' version '2023.2'
|
||||
id 'com.github.jk1.dependency-license-report' version '2.6'
|
||||
id "org.owasp.dependencycheck" version "9.0.10"
|
||||
id "com.diffplug.spotless" version "6.25.0"
|
||||
id 'jacoco'
|
||||
id 'info.solidsoft.pitest' version '1.15.0'
|
||||
id 'se.patrikerdes.use-latest-versions' version '0.2.18'
|
||||
id 'com.github.ben-manes.versions' version '0.51.0'
|
||||
}
|
||||
|
||||
// HOWTO: find out which dependency versions are managed by Spring Boot:
|
||||
// https://docs.spring.io/spring-boot/appendix/dependency-versions/coordinates.html
|
||||
|
||||
group = 'net.hostsharing'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
|
||||
@ -23,9 +20,6 @@ wrapper {
|
||||
gradleVersion = '8.5'
|
||||
}
|
||||
|
||||
// TODO.impl: self-attaching is deprecated, see:
|
||||
// https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
@ -64,27 +58,24 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0'
|
||||
implementation 'org.springdoc:springdoc-openapi:2.8.3'
|
||||
implementation 'org.postgresql:postgresql'
|
||||
implementation 'org.liquibase:liquibase-core'
|
||||
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.1'
|
||||
implementation 'org.springdoc:springdoc-openapi:2.4.0'
|
||||
implementation 'org.postgresql:postgresql:42.7.3'
|
||||
implementation 'org.liquibase:liquibase-core:4.27.0'
|
||||
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.3'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
|
||||
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
|
||||
implementation 'org.apache.commons:commons-text:1.13.0'
|
||||
implementation 'net.java.dev.jna:jna:5.16.0'
|
||||
implementation 'org.modelmapper:modelmapper:3.2.2'
|
||||
implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3'
|
||||
implementation 'org.reflections:reflections:0.10.2'
|
||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
||||
implementation 'net.java.dev.jna:jna:5.8.0'
|
||||
implementation 'org.modelmapper:modelmapper:3.2.0'
|
||||
implementation 'org.iban4j:iban4j:3.2.7-RELEASE'
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'
|
||||
implementation 'org.reflections:reflections:0.9.12'
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
testCompileOnly 'org.projectlombok:lombok'
|
||||
|
||||
// TODO.impl: version conflict with SpringDoc, check later and re-enable if fixed
|
||||
// developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
testAnnotationProcessor 'org.projectlombok:lombok'
|
||||
@ -94,12 +85,11 @@ dependencies {
|
||||
testImplementation 'org.testcontainers:junit-jupiter'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testImplementation 'org.testcontainers:postgresql'
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.2.1'
|
||||
testImplementation 'io.rest-assured:spring-mock-mvc'
|
||||
testImplementation 'org.hamcrest:hamcrest-core'
|
||||
testImplementation 'org.hamcrest:hamcrest-core:2.2'
|
||||
testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api'
|
||||
testImplementation 'org.wiremock:wiremock-standalone:3.10.0'
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
@ -127,8 +117,8 @@ openapiProcessor {
|
||||
springRoot {
|
||||
processorName 'spring'
|
||||
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
|
||||
apiPath "$projectDir/src/main/resources/api-definition/api-definition.yaml"
|
||||
mapping "$projectDir/src/main/resources/api-definition/api-mappings.yaml"
|
||||
apiPath "$projectDir/src/main/resources/api-definition.yaml"
|
||||
mapping "$projectDir/src/main/resources/api-mappings.yaml"
|
||||
targetDir "$buildDir/generated/sources/openapi-javax"
|
||||
showWarnings true
|
||||
openApiNullable true
|
||||
@ -180,9 +170,7 @@ openapiProcessor {
|
||||
}
|
||||
}
|
||||
sourceSets.main.java.srcDir 'build/generated/sources/openapi'
|
||||
|
||||
abstract class ProcessSpring extends DefaultTask {}
|
||||
|
||||
tasks.register('processSpring', ProcessSpring)
|
||||
['processSpringRoot',
|
||||
'processSpringRbac',
|
||||
@ -213,7 +201,7 @@ openApiGenerate.dependsOn processSpring
|
||||
spotless {
|
||||
java {
|
||||
removeUnusedImports()
|
||||
leadingTabsToSpaces(4)
|
||||
indentWithSpaces(4)
|
||||
endWithNewline()
|
||||
toggleOffOn()
|
||||
|
||||
@ -227,7 +215,7 @@ project.tasks.check.dependsOn(spotlessCheck)
|
||||
// HACK: no idea why spotless uses the output of these tasks, but we get warnings without those
|
||||
project.tasks.spotlessJava.dependsOn(
|
||||
tasks.generateLicenseReport,
|
||||
// tasks.pitest, TODO.test: PiTest currently does not work, needs to be fixed
|
||||
tasks.pitest,
|
||||
tasks.jacocoTestReport,
|
||||
tasks.processResources,
|
||||
tasks.processTestResources)
|
||||
@ -256,21 +244,19 @@ licenseReport {
|
||||
}
|
||||
project.tasks.check.dependsOn(checkLicense)
|
||||
|
||||
// HOWTO: run all tests except import- and scenario-tests: gw test
|
||||
// JaCoCo Test Code Coverage
|
||||
jacoco {
|
||||
toolVersion = "0.8.10"
|
||||
}
|
||||
test {
|
||||
finalizedBy jacocoTestReport // generate report after tests
|
||||
excludes = [
|
||||
'net.hostsharing.hsadminng.**.generated.**',
|
||||
]
|
||||
useJUnitPlatform {
|
||||
excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest'
|
||||
excludeTags 'import'
|
||||
}
|
||||
}
|
||||
|
||||
// JaCoCo Test Code Coverage for unit-tests
|
||||
jacoco {
|
||||
toolVersion = "0.8.10"
|
||||
}
|
||||
jacocoTestReport {
|
||||
dependsOn test
|
||||
afterEvaluate {
|
||||
@ -290,7 +276,7 @@ jacocoTestCoverageVerification {
|
||||
violationRules {
|
||||
rule {
|
||||
limit {
|
||||
minimum = 0.80 // TODO.test: improve instruction coverage
|
||||
minimum = 0.92
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,20 +288,15 @@ jacocoTestCoverageVerification {
|
||||
element = 'CLASS'
|
||||
excludes = [
|
||||
'net.hostsharing.hsadminng.**.generated.**',
|
||||
'net.hostsharing.hsadminng.rbac.test.dom.TestDomainEntity',
|
||||
'net.hostsharing.hsadminng.HsadminNgApplication',
|
||||
'net.hostsharing.hsadminng.ping.PingController',
|
||||
'net.hostsharing.hsadminng.rbac.generator.*',
|
||||
'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService',
|
||||
'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService.Node',
|
||||
'net.hostsharing.hsadminng.**.*Repository',
|
||||
'net.hostsharing.hsadminng.mapper.Mapper'
|
||||
]
|
||||
|
||||
limit {
|
||||
counter = 'LINE'
|
||||
value = 'COVEREDRATIO'
|
||||
minimum = 0.75 // TODO.test: improve line coverage
|
||||
minimum = 0.98
|
||||
}
|
||||
}
|
||||
rule {
|
||||
@ -329,73 +310,12 @@ jacocoTestCoverageVerification {
|
||||
limit {
|
||||
counter = 'BRANCH'
|
||||
value = 'COVEREDRATIO'
|
||||
minimum = 0.00 // TODO.test: improve branch coverage
|
||||
minimum = 1.00
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HOWTO: run all unit-tests which don't need a database: gw-test unitTest
|
||||
tasks.register('unitTest', Test) {
|
||||
useJUnitPlatform {
|
||||
excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest', 'generalIntegrationTest',
|
||||
'officeIntegrationTest', 'bookingIntegrationTest', 'hostingIntegrationTest'
|
||||
}
|
||||
|
||||
group 'verification'
|
||||
description 'runs all unit-tests which do not need a database'
|
||||
|
||||
mustRunAfter spotlessJava
|
||||
}
|
||||
|
||||
// HOWTO: run all integration tests which are not specific to a module, like base, rbac, config etc.
|
||||
tasks.register('generalIntegrationTest', Test) {
|
||||
useJUnitPlatform {
|
||||
includeTags 'generalIntegrationTest'
|
||||
}
|
||||
|
||||
group 'verification'
|
||||
description 'runs integration tests which are not specific to a module, like base, rbac, config etc.'
|
||||
|
||||
mustRunAfter spotlessJava
|
||||
}
|
||||
|
||||
// HOWTO: run all integration tests of the office module: gw-test officeIntegrationTest
|
||||
tasks.register('officeIntegrationTest', Test) {
|
||||
useJUnitPlatform {
|
||||
includeTags 'officeIntegrationTest'
|
||||
}
|
||||
|
||||
group 'verification'
|
||||
description 'runs integration tests of the office module'
|
||||
|
||||
mustRunAfter spotlessJava
|
||||
}
|
||||
|
||||
// HOWTO: run all integration tests of the booking module: gw-test bookingIntegrationTest
|
||||
tasks.register('bookingIntegrationTest', Test) {
|
||||
useJUnitPlatform {
|
||||
includeTags 'bookingIntegrationTest'
|
||||
}
|
||||
|
||||
group 'verification'
|
||||
description 'runs integration tests of the booking module'
|
||||
|
||||
mustRunAfter spotlessJava
|
||||
}
|
||||
|
||||
// HOWTO: run all integration tests of the hosting module: gw-test hostingIntegrationTest
|
||||
tasks.register('hostingIntegrationTest', Test) {
|
||||
useJUnitPlatform {
|
||||
includeTags 'hostingIntegrationTest'
|
||||
}
|
||||
|
||||
group 'verification'
|
||||
description 'runs integration tests of the hosting module'
|
||||
|
||||
mustRunAfter spotlessJava
|
||||
}
|
||||
|
||||
tasks.register('importOfficeData', Test) {
|
||||
useJUnitPlatform {
|
||||
includeTags 'importOfficeData'
|
||||
@ -418,30 +338,19 @@ tasks.register('importHostingAssets', Test) {
|
||||
mustRunAfter spotlessJava
|
||||
}
|
||||
|
||||
tasks.register('scenarioTest', Test) {
|
||||
useJUnitPlatform {
|
||||
includeTags 'scenarioTest'
|
||||
}
|
||||
|
||||
group 'verification'
|
||||
description 'run the import jobs as tests'
|
||||
|
||||
mustRunAfter spotlessJava
|
||||
}
|
||||
|
||||
// pitest mutation testing
|
||||
pitest {
|
||||
targetClasses = ['net.hostsharing.hsadminng.**']
|
||||
excludedClasses = [
|
||||
'net.hostsharing.hsadminng.config.**',
|
||||
// 'net.hostsharing.hsadminng.**.*Controller',
|
||||
'net.hostsharing.hsadminng.**.*Controller',
|
||||
'net.hostsharing.hsadminng.**.generated.**'
|
||||
]
|
||||
|
||||
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
|
||||
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*', '**ImportOfficeData', '**ImportHostingAssets']
|
||||
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*']
|
||||
|
||||
pitestVersion = '1.17.0'
|
||||
pitestVersion = '1.15.3'
|
||||
junit5PluginVersion = '1.1.0'
|
||||
|
||||
threads = 4
|
||||
@ -454,7 +363,7 @@ pitest {
|
||||
outputFormats = ['XML', 'HTML']
|
||||
timestampedReports = false
|
||||
}
|
||||
// project.tasks.check.dependsOn(project.tasks.pitest) TODO.test: PiTest currently does not work, needs to be fixed
|
||||
project.tasks.check.dependsOn(project.tasks.pitest)
|
||||
project.tasks.pitest.doFirst { // Why not doLast? See README.md!
|
||||
println "PiTest Mutation Report: file:///${project.rootDir}/build/reports/pitest/index.html"
|
||||
}
|
||||
@ -476,51 +385,3 @@ tasks.named("dependencyUpdates").configure {
|
||||
isNonStable(it.candidate.version)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Generate HTML from Markdown scenario-test-reports using Pandoc:
|
||||
tasks.register('convertMarkdownToHtml') {
|
||||
description = 'Generates HTML from Markdown scenario-test-reports using Pandoc.'
|
||||
group = 'Conversion'
|
||||
|
||||
// Define the template file and input directory
|
||||
def templateFile = file('doc/scenarios/.template.html')
|
||||
|
||||
// Task configuration and execution
|
||||
doFirst {
|
||||
// Check if pandoc is installed
|
||||
try {
|
||||
exec {
|
||||
commandLine 'pandoc', '--version'
|
||||
}
|
||||
} catch (Exception) {
|
||||
throw new GradleException("Pandoc is not installed or not found in the system path.")
|
||||
}
|
||||
|
||||
// Check if the template file exists
|
||||
if (!templateFile.exists()) {
|
||||
throw new GradleException("Template file 'doc/scenarios/.template.html' not found.")
|
||||
}
|
||||
}
|
||||
|
||||
doLast {
|
||||
// Gather all Markdown files in the current directory
|
||||
fileTree(dir: '.', include: 'build/doc/scenarios/*.md').each { file ->
|
||||
// Corrected way to create the output file path
|
||||
def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html'))
|
||||
|
||||
// Execute pandoc for each markdown file
|
||||
exec {
|
||||
commandLine 'pandoc', file.absolutePath, '--template', templateFile.absolutePath, '-o', outputFile.absolutePath
|
||||
}
|
||||
|
||||
println "Converted ${file.name} to ${outputFile.name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
convertMarkdownToHtml.dependsOn scenarioTest
|
||||
|
||||
// shortcut for compiling all files
|
||||
tasks.register('compile') {
|
||||
dependsOn 'compileJava', 'compileTestJava'
|
||||
}
|
||||
|
@ -1,119 +0,0 @@
|
||||
# Handling Automatic Creation of Hosting Assets for New Booking Items
|
||||
|
||||
**Status:**
|
||||
- [x] proposed by (Michael Hönnig)
|
||||
- [ ] accepted by (Participants)
|
||||
- [ ] rejected by (Participants)
|
||||
- [ ] superseded by (superseding ADR)
|
||||
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
When a customer creates a new booking item (e.g., `MANAGED_WEBSPACE`), the system must automatically create the related hosting asset.
|
||||
This process can sometimes fail or require additional data from the user, e.g. installing a DNS verification key, or a hostmaster, e.g. the target server to use.
|
||||
|
||||
The challenge is how to handle this automatic creation process while dealing with missing data, asynchronicity and failures while ensuring system consistency and proper user notification.
|
||||
|
||||
|
||||
### Technical Background
|
||||
|
||||
The creation of hosting assets can occur synchronously (in simple cases) or asynchronously (when additional steps like manual verification are needed).
|
||||
For example, a `DOMAIN_SETUP` hosting asset may require DNS verification from the user, and until this is provided, the related domain cannot be fully set up.
|
||||
|
||||
Additionally, not all data needed for creating the hosting asset is stored in the booking item.
|
||||
It's part of the HTTP request and later stored in the hosting asset, but we also need to store it before the hosting asset can be created asynchronously.
|
||||
|
||||
Current system behavior involves returning HTTP 201 upon booking item creation, but the automatic hosting asset creation might fail due to missing information.
|
||||
The system needs to manage the creation process in a way that ensures valid hosting assets are created and informs the user of any actions required while still returning a 201 HTTP code, not an error code.
|
||||
|
||||
|
||||
## Considered Options
|
||||
|
||||
For storing the data needed for the hosting-asset creation:
|
||||
|
||||
* STORAGE-1: Store temporary asset data in the `BookingItemEntity`, e.g. a JSON column.
|
||||
And delete the value of that column, once the hosting assets got successfully created.
|
||||
* STORAGE-2: Create hosting assets immediately, even if invalid, but mark them as "inactive" until completed and fully validated.
|
||||
* STORAGE-3: Store the asset data in a kind of event- or job-queue, which get deleted once the hosting-asset got successfully created.
|
||||
|
||||
For the user-notification status:
|
||||
|
||||
* STATUS-1: Introduce a status field in the booking-items.
|
||||
* STATUS-2: Store the status in the event-/job-queue entries.
|
||||
|
||||
### STORAGE-1: Temporary Data Storage in `BookingItemEntity`
|
||||
|
||||
Store asset-related data (e.g., domain name) in a temporary column or JSON field in the `BookingItemEntity` until the hosting assets are successfully created.
|
||||
Once assets are created, the temporary data is deleted to avoid inconsistencies.
|
||||
|
||||
#### Advantages
|
||||
- Easy to implement.
|
||||
|
||||
#### Disadvantages
|
||||
- Needs either a separate map of properties in the booking-item.
|
||||
- Or, if stored as a JSON field in the booking-item-resources, these are misused.
|
||||
- Requires additional cleanup logic to remove stale data.
|
||||
|
||||
### STORAGE-2: Inactive Hosting Assets Until Validation
|
||||
|
||||
Create the hosting assets immediately upon booking item creation but mark them as "inactive" until all required information (e.g., verification code) is provided and validation is complete.
|
||||
|
||||
#### Advantages
|
||||
- Avoids temporary external data storage for the hosting-assets.
|
||||
|
||||
#### Disadvantages
|
||||
- Validation becomes more complex as some properties need to be validated, others not.
|
||||
And some properties even need special treatment for new entities, which then becomes vague.
|
||||
- Inactive assets have to be filtered from operational assets.
|
||||
- Potential risk of incomplete or inconsistent assets being created, which may require correction.
|
||||
- Difficult to write tests for all possible combinations of validations.
|
||||
|
||||
### STORAGE-3: Event-Based Approach
|
||||
|
||||
The hosting asset data required for creation us passed to the API and stored in a `BookingItemCreatedEvent`.
|
||||
If hosting asset creation cannot happen synchronously, the event is stored and processed asynchronously in batches, retrying failed asset creation as needed.
|
||||
|
||||
#### Advantages
|
||||
- Clean-data-structure (separation of concerns).
|
||||
- Clear separation between booking item creation and hosting asset creation.
|
||||
- Only valid assets in the database.
|
||||
- Can handle complex asynchronous processes (like waiting for external verification) in a clean and structured manner.
|
||||
- Easier to manage retries and failures in asset creation without complicating the booking item structure.
|
||||
|
||||
#### Disadvantages
|
||||
- At the Spring controller level, the whole JSON is already converted into Java objects,
|
||||
but for storing the asset data in the even, we need JSON again.
|
||||
This could is not just a performance-overhead but could also lead to inconsistencies.
|
||||
|
||||
### STATUS-1: Store hosting-asset-creation-status in the `BookingItemEntity`
|
||||
|
||||
A status field would be added to booking-items to track the creation state of related hosting assets.
|
||||
The users could check their booking-items for the status of the hosting-asset creation, error messages and further instructions.
|
||||
|
||||
#### Advantages
|
||||
- Easy to implement.
|
||||
|
||||
#### Disadvantages
|
||||
- Adds a field to the booking-item which is makes no sense anymore once the related hosting asset is created.
|
||||
|
||||
|
||||
### Status-2: Store hosting-asset-creation-status in the `BookingItemCreateEvent`
|
||||
|
||||
A status field would be added to the booking-item-created event and get updated with the latest messages any time we try to create the hosting-asset.
|
||||
|
||||
#### Advantages
|
||||
- Clean-data-structure (separation of concerns)
|
||||
|
||||
#### Disadvantages
|
||||
- Accessing the status requires querying the event queue.
|
||||
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
**Chosen Option: STORAGE-3 with STATUS-2 (Event-Based Approach with `BookingItemCreatedEvent`)**
|
||||
|
||||
The event-based approach was selected as the best solution for handling automatic hosting asset creation. This option provides a clear separation between booking item creation and hosting asset creation, ensuring that no invalid or incomplete assets are created. The asynchronous nature of the event system allows for retries and external validation steps (such as user-entered verification codes) without disrupting the overall flow.
|
||||
|
||||
By using `BookingItemCreatedEvent` to store the hosting-asset data and the status,
|
||||
we don't need to misuse other data structures for temporary data
|
||||
and therefore hava a clean separation of concerns.
|
@ -1,124 +0,0 @@
|
||||
### hsadminNg fachliches Glossar
|
||||
|
||||
<!--
|
||||
Currently, this business glossary is only available in German because in many cases,
|
||||
the German terms are important for comprehensibility for those using this software.
|
||||
-->
|
||||
|
||||
Dieses ist eine Sammlung von Fachbegriffen, die in diesem Projekt benutzt werden.
|
||||
Ebenfalls aufgenommen sind technische Begriffe, die für Benutzer für das Verständnis der Schnittstellen nötig sind.
|
||||
|
||||
Falls etwas fehlt, bitte Bescheid geben.
|
||||
|
||||
|
||||
#### Partner
|
||||
|
||||
In diesem System ist ein _Partner_ grundsätzlich jeglicher Geschäftspartner der _Hostsharing eG_.
|
||||
Dies können grundsätzlich Kunden, siehe [Debitor](#Debitor), wie Lieferanten sein.
|
||||
Derzeit sind aber nur Debitoren implementiert.
|
||||
|
||||
Des Weiteren gibt es für jeden _Partner_ eine fünfstellige Partnernummer mit dem Prefix 'P-' (z.B. `P-123454`)
|
||||
sowie Zusatzinformationen (z.B. Registergerichtnummer oder Geburtsdatum), die zur genauen Identifikation benötigt werden.
|
||||
|
||||
Für einen _Partner_ kann es gleichzeitig mehrere [Debitoren](#Debitor)
|
||||
und zeitlich nacheinander mehrere [Mitgliedschaften](#Mitgliedschaft) geben.
|
||||
|
||||
Partner sind grundsätzlich als ist [Relation](#Relation) der Vertragsperson mit der Person _Hostsharing eG_ implementiert.
|
||||
|
||||
|
||||
### Debitor
|
||||
|
||||
Ein `Debitor` ist quasi ein Rechnungsempfänger für einen [Partner](#Partner).
|
||||
|
||||
Für einen _Partner_ kann es gleichzeitig mehrere [Debitoren](#Debitor) geben,
|
||||
z.B. für spezielle Projekte des Kunden oder verbundene Organisationen.
|
||||
|
||||
Des Weiteren gibt es für jeden _Partner_ eine fünfstellige Partnernummer mit dem Prefix 'P-' (z.B. `P-123454`)
|
||||
sowie Zusatzinformationen (z.B. Registergerichtsnummer oder Geburtsdatum), die zur genauen Identifikation benötigt werden.
|
||||
|
||||
Debitoren sind grundsätzlich als ist [Relation](#Relation) der Vertragsperson mit der Person des Vertragspartners implementiert.
|
||||
|
||||
|
||||
#### Relation
|
||||
|
||||
Eine _Relation_ ist eine typisierte und mit Kontaktdaten versehene Beziehung einer (_Holder_)-Person zu einer _Anchor_-Person.
|
||||
|
||||
Eine Relation ist eine Art Geschäftsrolle, wir haben hier aber keinen Begriff mit 'Rolle' verwendet,
|
||||
weil 'Role' (engl.) zu leicht mit der [RBAC-Rolle](#RBAC-Role) verwechselt werden könnte.
|
||||
|
||||
Die _Relation_ ist auch ein technisches Konzept und gehört nicht zur Domänensprache.
|
||||
Dieses Konzept ist jedoch für das Verständnis der ([API](#API)) notwendig.
|
||||
|
||||
|
||||
#### Ex-Partner
|
||||
|
||||
Ex-Partner bilden [Personen](#Person) ab, die vormals [Partner](#Partner) waren.
|
||||
Diese bleiben dadurch informationshalber im System verfügbar.
|
||||
|
||||
Implementiert ist der _Ex-Partner_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des Ex-Partner (_Holder_) zum neuen Partner (_Anchor_) dargestellt.
|
||||
Dieses kann zu einer Kettenbildung führen.
|
||||
|
||||
|
||||
#### Representative-Contact (ehemals _contractual_)
|
||||
|
||||
Ein _Representative_ ist eine natürliche Person, die für eine nicht-natürliche Person vertretungsberechtigt ist.
|
||||
|
||||
Implementiert ist der _Representative_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des Repräsentanten (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
|
||||
|
||||
|
||||
### VIP-Contact
|
||||
|
||||
Ein _VIP-Contact_ ist eine natürliche Person, die für einen Geschäftspartner eine wichtige Funktion übernimmt,
|
||||
nicht aber deren offizieller Repräsentant ist.
|
||||
|
||||
Implementiert ist der _VIP-Contact_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des VIP-Contact (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
|
||||
|
||||
|
||||
### Operations-Contact
|
||||
|
||||
Ein _Operations-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner technischer Ansprechpartner ist.
|
||||
|
||||
Ein Seiteneffekt ist, dass diese Person im Ticketsystem Znuny direkt dem Geschäftspartner zugeordnet werden kann.
|
||||
|
||||
Im Legacy System waren das die Kontakte mit der Rolle `operation` und `silent`.
|
||||
|
||||
Implementiert ist der _Operations-Contact_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des _Operations-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
|
||||
|
||||
|
||||
### OperationsAlert-Contact
|
||||
|
||||
Ein _OperationsAlert-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner bei technischen Probleme kontaktiert werden soll.
|
||||
|
||||
Im Legacy System waren das die Kontakte mit der Rolle `operation`.
|
||||
|
||||
Implementiert ist der _OperationsAlert-Contact_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des _OperationsAlert-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
|
||||
|
||||
|
||||
### Subscriber-Contact
|
||||
|
||||
Ein _Subscriber-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner eine bestimmte Mailingliste abonniert.
|
||||
|
||||
Implementiert ist der _Subscriber-Contact_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des _Subscriber-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
|
||||
Zusätzlich wird diese Relation mit dem Kurznamen der abonnierten Mailingliste markiert.
|
||||
|
||||
|
||||
#### Anchor / Relation-Anchor
|
||||
|
||||
siehe [Relation](#Relation)
|
||||
|
||||
|
||||
#### Holder / Relation-Holder
|
||||
|
||||
siehe [Relation](#Relation)
|
||||
|
||||
|
||||
#### API
|
||||
|
||||
Und API (Application-Programming-Interface) verstehen wir eine über HTTPS angesprochene programmatisch bedienbare Schnittstell
|
||||
zur Funktionalität des hsAdmin-NG-Systems.
|
@ -50,7 +50,6 @@ classDiagram
|
||||
UNKNOWN: nur für Import
|
||||
NATURAL_PERSON: natürliche Person
|
||||
LEGAL_PERSON: z.B. GmbH, e.K., eG, e.V.
|
||||
ORGANIZATIONAL_UNIT: z.B. "Admin-Team", "Buchhaltung"
|
||||
INCORORATED_FIRM: z.B. OHG, Partnerschaftsgesellschaft
|
||||
UNINCORPORATED_FIRM: z.B. GbR, ARGE, Erbengemeinschaft
|
||||
PUBLIC_INSTITUTION: KdöR, AöR [ohne Registergericht/Registernummer]
|
||||
@ -65,7 +64,7 @@ classDiagram
|
||||
}
|
||||
|
||||
class partner-MeierGmbH {
|
||||
+Numeric partnerNumber: P-12345
|
||||
+Numeric partnerNumber: 12345
|
||||
+Relation partnerRel
|
||||
}
|
||||
partner-MeierGmbH *-- rel-MeierGmbH
|
||||
|
@ -1,124 +0,0 @@
|
||||
<!doctype html>
|
||||
<html $if(lang)$ lang="$lang$" $endif$>
|
||||
<head>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js"></script>
|
||||
<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||
|
||||
<!-- <link rel="stylesheet" type="text/css" href="template.css" /> -->
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/template.css" />
|
||||
|
||||
<link href="https://vjs.zencdn.net/5.4.4/video-js.css" rel="stylesheet" />
|
||||
|
||||
<script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
|
||||
<!-- <script type='text/javascript' src='menu/js/jquery.cookie.js'></script> -->
|
||||
<!-- <script type='text/javascript' src='menu/js/jquery.hoverIntent.minified.js'></script> -->
|
||||
<!-- <script type='text/javascript' src='menu/js/jquery.dcjqaccordion.2.7.min.js'></script> -->
|
||||
|
||||
<!-- <link href="menu/css/skins/blue.css" rel="stylesheet" type="text/css" /> -->
|
||||
<!-- <link href="menu/css/skins/graphite.css" rel="stylesheet" type="text/css" /> -->
|
||||
<!-- <link href="menu/css/skins/grey.css" rel="stylesheet" type="text/css" /> -->
|
||||
|
||||
<!-- <script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> -->
|
||||
|
||||
|
||||
<!-- <script src="script.js"></script> -->
|
||||
|
||||
<!-- <script src="jquery.sticky-kit.js "></script> -->
|
||||
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.cookie.js'></script>
|
||||
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.hoverIntent.minified.js'></script>
|
||||
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.dcjqaccordion.2.7.min.js'></script>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/blue.css" rel="stylesheet" type="text/css" />
|
||||
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/graphite.css" rel="stylesheet" type="text/css" />
|
||||
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/grey.css" rel="stylesheet" type="text/css" />
|
||||
<link href="https://cdn.jsdelivr.net/gh/ryangrose/easy-pandoc-templates@948e28e5/css/elegant_bootstrap.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/script.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/jquery.sticky-kit.js"></script>
|
||||
<meta name="generator" content="pandoc" />
|
||||
$for(author-meta)$
|
||||
<meta name="author" content="$author-meta$" />
|
||||
$endfor$
|
||||
$if(date-meta)$
|
||||
<meta name="date" content="$date-meta$" />
|
||||
$endif$
|
||||
<title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
|
||||
<style type="text/css">code{white-space: pre;}</style>
|
||||
$if(quotes)$
|
||||
<style type="text/css">q { quotes: "“" "”" "‘" "’"; }</style>
|
||||
$endif$
|
||||
$if(highlighting-css)$
|
||||
<style type="text/css">
|
||||
$highlighting-css$
|
||||
</style>
|
||||
$endif$
|
||||
$for(css)$
|
||||
<link rel="stylesheet" href="$css$" $if(html5)$$else$type="text/css" $endif$/>
|
||||
$endfor$
|
||||
$if(math)$
|
||||
$math$
|
||||
$endif$
|
||||
$for(header-includes)$
|
||||
$header-includes$
|
||||
$endfor$
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
$if(title)$
|
||||
<div class="navbar navbar-static-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<span class="doc-title">$title$</span>
|
||||
<ul class="nav pull-right doc-info">
|
||||
$for(author)$
|
||||
<li><p class="navbar-text">$author$</p></li>
|
||||
$endfor$
|
||||
$if(date)$
|
||||
<li><p class="navbar-text">$date$</p></li>
|
||||
$endif$
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
$endif$
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
$if(toc)$
|
||||
<div id="$idprefix$TOC" class="span3">
|
||||
<div class="well toc">
|
||||
|
||||
$toc$
|
||||
|
||||
</div>
|
||||
</div>
|
||||
$endif$
|
||||
<div class="span$if(toc)$9$else$12$endif$">
|
||||
|
||||
$if(abstract)$
|
||||
<H1>$abstract-title$</H1>
|
||||
$abstract$
|
||||
$endif$
|
||||
|
||||
$for(include-before)$
|
||||
$include-before$
|
||||
$endfor$
|
||||
$body$
|
||||
$for(include-after)$
|
||||
$include-after$
|
||||
$endfor$
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://vjs.zencdn.net/5.4.4/video.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1 +0,0 @@
|
||||
find the generated ScenarioReports in build/doc/scenarios
|
@ -90,20 +90,6 @@ Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-cove
|
||||
TODO.test: Complete the Acceptance-Tests test concept.
|
||||
|
||||
|
||||
#### Scenario-Tests
|
||||
|
||||
Our Scenario-tests are induced by business use-cases.
|
||||
They test from the REST API all the way down to the database.
|
||||
|
||||
Most scenario-tests are positive tests, they test if business scenarios do work.
|
||||
But few might be negative tests, which test if specific forbidden data gets rejected.
|
||||
|
||||
Our scenario tests also generate test-reports which contain the REST-API calls needed for each scenario.
|
||||
These reports can be used as examples for the API usage from a business perspective.
|
||||
|
||||
There is an extra document regarding scenario-test, see [Scenario-Tests README](../src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md).
|
||||
|
||||
|
||||
#### Performance-Tests
|
||||
|
||||
Performance-critical scenarios have to be identified and a special performance-test has to be implemented.
|
||||
|
@ -1,34 +1,16 @@
|
||||
{
|
||||
"allowedLicenses": [
|
||||
{ "moduleLicense": "Apache 2" },
|
||||
{ "moduleLicense": "Apache 2.0" },
|
||||
{ "moduleLicense": "Apache-2.0" },
|
||||
{ "moduleLicense": "Apache 2" },
|
||||
{ "moduleLicense": "Apache License 2.0" },
|
||||
{ "moduleLicense": "Apache License v2.0" },
|
||||
{ "moduleLicense": "Apache License Version 2.0" },
|
||||
{ "moduleLicense": "Apache License, Version 2.0" },
|
||||
{ "moduleLicense": "The Apache License, Version 2.0" },
|
||||
{ "moduleLicense": "The Apache Software License, Version 2.0" },
|
||||
|
||||
{
|
||||
"moduleLicense": null,
|
||||
"#moduleLicense": "Apache License 2.0, see https://github.com/springdoc/springdoc-openapi/blob/main/LICENSE",
|
||||
"moduleVersion": "2.4.0",
|
||||
"moduleName": "org.springdoc:springdoc-openapi"
|
||||
},
|
||||
{
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.0.0",
|
||||
"moduleName": "org.jspecify:jspecify"
|
||||
},
|
||||
|
||||
{ "moduleLicense": "BSD License" },
|
||||
{ "moduleLicense": "BSD-2-Clause" },
|
||||
{ "moduleLicense": "BSD-3-Clause" },
|
||||
{ "moduleLicense": "The BSD License" },
|
||||
|
||||
{ "moduleLicense": "The New BSD License" },
|
||||
|
||||
{ "moduleLicense": "CDDL 1.1" },
|
||||
{ "moduleLicense": "CDDL/GPLv2+CE" },
|
||||
{ "moduleLicense": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0" },
|
||||
@ -47,21 +29,11 @@
|
||||
{ "moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception" },
|
||||
{ "moduleLicense": "GPL2 w/ CPE" },
|
||||
|
||||
{ "moduleLicense": "LGPL, version 2.1"},
|
||||
{ "moduleLicense": "LGPL-2.1-or-later"},
|
||||
|
||||
{ "moduleLicense": "MIT License" },
|
||||
{ "moduleLicense": "MIT" },
|
||||
{ "moduleLicense": "The MIT License (MIT)" },
|
||||
{ "moduleLicense": "The MIT License" },
|
||||
|
||||
{ "moduleLicense": "WTFPL" },
|
||||
|
||||
{
|
||||
"moduleLicense": "Public Domain, per Creative Commons CC0",
|
||||
"moduleVersion": "2.0.3"
|
||||
}
|
||||
|
||||
|
||||
{ "moduleName": "org.springdoc:springdoc-openapi" }
|
||||
]
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
FROM eclipse-temurin:21-jdk
|
||||
RUN apt-get update && \
|
||||
apt-get install -y bind9-utils pandoc && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
@ -1,5 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
|
||||
<suppress>
|
||||
<notes><![CDATA[
|
||||
Cyclic references are not possible if file comes in JSON text format.
|
||||
]]></notes>
|
||||
<packageUrl regex="true">^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$</packageUrl>
|
||||
<cpe>cpe:/a:fasterxml:jackson-databind</cpe>
|
||||
</suppress>
|
||||
<suppress>
|
||||
<notes><![CDATA[
|
||||
Internal tooling, not exposed to the Internet.
|
||||
@ -7,14 +14,4 @@
|
||||
<packageUrl regex="true">^pkg:maven/org\.pitest/pitest\-command\-line@.*$</packageUrl>
|
||||
<cpe>cpe:/a:line:line</cpe>
|
||||
</suppress>
|
||||
<suppress>
|
||||
<notes><![CDATA[
|
||||
file name: logback-core-1.5.12.jar
|
||||
A successful attack requires the user to have write access to a configuration file or environment vars.
|
||||
]]></notes>
|
||||
<packageUrl regex="true">^pkg:maven/ch\.qos\.logback/logback-core@.*$</packageUrl>
|
||||
<cpe>cpe:/a:qos:logback</cpe>
|
||||
<cve>CVE-2024-12798</cve>
|
||||
</suppress>
|
||||
|
||||
</suppressions>
|
||||
|
@ -1,105 +0,0 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.actuate.endpoint.SanitizableData;
|
||||
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
// HOWTO: exclude sensitive values, like passwords and other secrets, from being show by actuator endpoints:
|
||||
// either use: add your custom keys to management.endpoint.additionalKeysToSanitize,
|
||||
// or, if you need more heuristics, amend this code down here.
|
||||
@Component
|
||||
public class ActuatorSanitizer implements SanitizingFunction {
|
||||
|
||||
private static final String[] REGEX_PARTS = {"*", "$", "^", "+"};
|
||||
|
||||
private static final Set<String> DEFAULT_KEYS_TO_SANITIZE = Set.of(
|
||||
"password", "secret", "token", ".*credentials.*", "vcap_services", "^vcap\\.services.*$", "sun.java.command", "^spring[._]application[._]json$"
|
||||
);
|
||||
|
||||
private static final Set<String> URI_USERINFO_KEYS = Set.of(
|
||||
"uri", "uris", "url", "urls", "address", "addresses"
|
||||
);
|
||||
|
||||
private static final Pattern URI_USERINFO_PATTERN = Pattern.compile("^\\[?[A-Za-z][A-Za-z0-9\\+\\.\\-]+://.+:(.*)@.+$");
|
||||
|
||||
private final List<Pattern> keysToSanitize = new ArrayList<>();
|
||||
|
||||
public ActuatorSanitizer(@Value("${management.endpoint.additionalKeysToSanitize:}") final List<String> additionalKeysToSanitize) {
|
||||
addKeysToSanitize(DEFAULT_KEYS_TO_SANITIZE);
|
||||
addKeysToSanitize(URI_USERINFO_KEYS);
|
||||
addKeysToSanitize(additionalKeysToSanitize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SanitizableData apply(final SanitizableData data) {
|
||||
if (data.getValue() == null) {
|
||||
return data;
|
||||
}
|
||||
|
||||
for (final Pattern pattern : keysToSanitize) {
|
||||
if (pattern.matcher(data.getKey()).matches()) {
|
||||
if (keyIsUriWithUserInfo(pattern)) {
|
||||
return data.withValue(sanitizeUris(data.getValue().toString()));
|
||||
}
|
||||
|
||||
return data.withValue(SanitizableData.SANITIZED_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private void addKeysToSanitize(final Collection<String> keysToSanitize) {
|
||||
for (final String key : keysToSanitize) {
|
||||
this.keysToSanitize.add(getPattern(key));
|
||||
}
|
||||
}
|
||||
|
||||
private Pattern getPattern(final String value) {
|
||||
if (isRegex(value)) {
|
||||
return Pattern.compile(value, Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
return Pattern.compile(".*" + value + "$", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
private boolean isRegex(final String value) {
|
||||
for (final String part : REGEX_PARTS) {
|
||||
if (value.contains(part)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean keyIsUriWithUserInfo(final Pattern pattern) {
|
||||
for (String uriKey : URI_USERINFO_KEYS) {
|
||||
if (pattern.matcher(uriKey).matches()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Object sanitizeUris(final String value) {
|
||||
return Arrays.stream(value.split(",")).map(this::sanitizeUri).collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
private String sanitizeUri(final String value) {
|
||||
final var matcher = URI_USERINFO_PATTERN.matcher(value);
|
||||
final var password = matcher.matches() ? matcher.group(1) : null;
|
||||
if (password != null) {
|
||||
return StringUtils.replace(value, ":" + password + "@", ":" + SanitizableData.SANITIZED_VALUE + "@");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import java.util.*;
|
||||
|
||||
public class AuthenticatedHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final Map<String, String> customHeaders = new HashMap<>();
|
||||
|
||||
public AuthenticatedHttpServletRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
public void addHeader(final String name, final String value) {
|
||||
customHeaders.put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(final String name) {
|
||||
// Check custom headers first
|
||||
final var customHeaderValue = customHeaders.get(name);
|
||||
if (customHeaderValue != null) {
|
||||
return customHeaderValue;
|
||||
}
|
||||
// Fall back to the original headers
|
||||
return super.getHeader(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getHeaderNames() {
|
||||
// Combine original headers and custom headers
|
||||
final var headerNames = new HashSet<>(customHeaders.keySet());
|
||||
final var originalHeaderNames = super.getHeaderNames();
|
||||
while (originalHeaderNames.hasMoreElements()) {
|
||||
headerNames.add(originalHeaderNames.nextElement());
|
||||
}
|
||||
return Collections.enumeration(headerNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getHeaders(final String name) {
|
||||
// Combine original headers and custom header
|
||||
final var values = new HashSet<String>();
|
||||
if (customHeaders.containsKey(name)) {
|
||||
values.add(customHeaders.get(name));
|
||||
}
|
||||
final var originalValues = super.getHeaders(name);
|
||||
while (originalValues.hasMoreElements()) {
|
||||
values.add(originalValues.nextElement());
|
||||
}
|
||||
return Collections.enumeration(values);
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AuthenticationFilter implements Filter {
|
||||
|
||||
@Autowired
|
||||
private Authenticator authenticator;
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) {
|
||||
final var httpRequest = (HttpServletRequest) request;
|
||||
final var httpResponse = (HttpServletResponse) response;
|
||||
|
||||
try {
|
||||
final var currentSubject = authenticator.authenticate(httpRequest);
|
||||
|
||||
final var authenticatedRequest = new AuthenticatedHttpServletRequestWrapper(httpRequest);
|
||||
authenticatedRequest.addHeader("current-subject", currentSubject);
|
||||
|
||||
chain.doFilter(authenticatedRequest, response);
|
||||
} catch (final BadCredentialsException exc) {
|
||||
// TODO.impl: should not be necessary if ResponseStatusException worked
|
||||
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public interface Authenticator {
|
||||
|
||||
String authenticate(final HttpServletRequest httpRequest);
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
|
||||
public class CasAuthenticator implements Authenticator {
|
||||
|
||||
@Value("${hsadminng.cas.server}")
|
||||
private String casServerUrl;
|
||||
|
||||
@Value("${hsadminng.cas.service}")
|
||||
private String serviceUrl;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@SneakyThrows
|
||||
@Timed("app.cas.authenticate")
|
||||
public String authenticate(final HttpServletRequest httpRequest) {
|
||||
final var userName = StringUtils.isBlank(casServerUrl)
|
||||
? bypassCurrentSubject(httpRequest)
|
||||
: casValidation(httpRequest);
|
||||
final var authentication = new UsernamePasswordAuthenticationToken(userName, null, null);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return authentication.getName();
|
||||
}
|
||||
|
||||
private static String bypassCurrentSubject(final HttpServletRequest httpRequest) {
|
||||
final var userName = httpRequest.getHeader("current-subject");
|
||||
System.err.println("CasAuthenticator.bypassCurrentSubject: " + userName);
|
||||
return userName;
|
||||
}
|
||||
|
||||
private String casValidation(final HttpServletRequest httpRequest)
|
||||
throws SAXException, IOException, ParserConfigurationException {
|
||||
|
||||
final var ticket = httpRequest.getHeader("Authorization");
|
||||
final var url = casServerUrl + "/p3/serviceValidate" +
|
||||
"?service=" + serviceUrl +
|
||||
"&ticket=" + ticket;
|
||||
|
||||
System.err.println("CasAuthenticator.casValidation using URL: " + url);
|
||||
|
||||
final var response = restTemplate.getForObject(url, String.class);
|
||||
|
||||
final var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(new java.io.ByteArrayInputStream(response.getBytes()));
|
||||
if (doc.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) {
|
||||
// TODO.impl: for unknown reasons, this results in a 403 FORBIDDEN
|
||||
// throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "CAS service ticket could not be validated");
|
||||
System.err.println("CAS service ticket could not be validated");
|
||||
System.err.println("CAS-validation-URL: " + url);
|
||||
System.err.println(response);
|
||||
throw new BadCredentialsException("CAS service ticket could not be validated");
|
||||
}
|
||||
final var userName = doc.getElementsByTagName("cas:user").item(0).getTextContent();
|
||||
System.err.println("CAS-user: " + userName);
|
||||
return userName;
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Endpoint(id="metric-links")
|
||||
// BLOG: implement a custom Spring Actuator endpoint to view _clickable_ Spring Actuator (Micrometer) Metrics endpoints
|
||||
// HOWTO: implement a custom Spring Actuator endpoint
|
||||
public class CustomActuatorEndpoint {
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@ReadOperation
|
||||
public String getMetricsLinks() {
|
||||
final String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();
|
||||
final var metricsEndpoint = baseUrl + "/actuator/metrics";
|
||||
|
||||
final var response = restTemplate.getForObject(metricsEndpoint, ActuatorMetricsEndpointResource.class);
|
||||
|
||||
if (response == null || response.getNames() == null) {
|
||||
throw new IllegalStateException("no metrics available");
|
||||
}
|
||||
return generateJsonLinksToMetricEndpoints(response, metricsEndpoint);
|
||||
}
|
||||
|
||||
private static String generateJsonLinksToMetricEndpoints(final ActuatorMetricsEndpointResource response, final String metricsEndpoint) {
|
||||
final var links = response.getNames().stream()
|
||||
.map(name -> "\"" + name + "\": \"" + metricsEndpoint + "/" + name + "\"")
|
||||
.toList();
|
||||
return "{\n" + String.join(",\n", links) + "\n}";
|
||||
}
|
||||
|
||||
@Getter
|
||||
private static class ActuatorMetricsEndpointResource {
|
||||
private List<String> names;
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.openapitools.jackson.nullable.JsonNullableModule;
|
||||
@ -11,24 +9,15 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
|
||||
|
||||
@Configuration
|
||||
public class JsonObjectMapperConfiguration {
|
||||
|
||||
public static ObjectMapper build() {
|
||||
return new JsonObjectMapperConfiguration().customObjectMapper().build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
||||
// HOWTO: add JSON converters and specify other JSON mapping configurations
|
||||
return new Jackson2ObjectMapperBuilder()
|
||||
.modules(new JsonNullableModule(), new JavaTimeModule())
|
||||
.featuresToEnable(
|
||||
JsonParser.Feature.ALLOW_COMMENTS,
|
||||
DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
|
||||
)
|
||||
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS)
|
||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
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.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig {
|
||||
|
||||
@Bean
|
||||
@Profile("!test")
|
||||
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/api/**").permitAll() // TODO.impl: implement authentication
|
||||
.requestMatchers("/swagger-ui/**").permitAll()
|
||||
.requestMatchers("/v3/api-docs/**").permitAll()
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile("!test")
|
||||
public Authenticator casServiceTicketValidator() {
|
||||
return new CasAuthenticator();
|
||||
}
|
||||
|
||||
}
|
@ -58,7 +58,7 @@ public class Context {
|
||||
cast(:currentTask as varchar(127)),
|
||||
cast(:currentRequest as text),
|
||||
cast(:currentSubject as varchar(63)),
|
||||
cast(:assumedRoles as text));
|
||||
cast(:assumedRoles as varchar(1023)));
|
||||
""");
|
||||
query.setParameter("currentTask", shortenToMaxLength(currentTask, 127));
|
||||
query.setParameter("currentRequest", currentRequest);
|
||||
|
@ -46,7 +46,6 @@ public class CustomErrorResponse {
|
||||
this.path = path;
|
||||
this.statusCode = status.value();
|
||||
this.statusPhrase = status.getReasonPhrase();
|
||||
// HOWTO: debug serverside error response - set a breakpoint here
|
||||
this.message = message.startsWith("ERROR: [") ? message : "ERROR: [" + statusCode + "] " + message;
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,6 @@ public class RestResponseEntityExceptionHandler
|
||||
return errorResponse(request, HttpStatus.valueOf(statusCode.value()),
|
||||
Optional.ofNullable(response.getBody()).map(Object::toString).orElse(firstMessageLine(exc)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked,rawtypes")
|
||||
protected ResponseEntity handleHttpMessageNotReadable(
|
||||
@ -132,7 +131,7 @@ public class RestResponseEntityExceptionHandler
|
||||
final HttpStatusCode status,
|
||||
final WebRequest request) {
|
||||
final var errorList = exc
|
||||
.getParameterValidationResults()
|
||||
.getAllValidationResults()
|
||||
.stream()
|
||||
.map(ParameterValidationResult::getResolvableErrors)
|
||||
.flatMap(Collection::stream)
|
||||
|
@ -1,31 +0,0 @@
|
||||
package net.hostsharing.hsadminng.errors;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class Validate {
|
||||
|
||||
final String variableNames;
|
||||
|
||||
public static Validate validate(final String variableNames) {
|
||||
return new Validate(variableNames);
|
||||
}
|
||||
|
||||
public final void atMaxOne(final Object var1, final Object var2) {
|
||||
if (var1 != null && var2 != null) {
|
||||
throw new ValidationException(
|
||||
"At maximum one of (" + variableNames + ") must be non-null, " +
|
||||
"but are (" + var1 + ", " + var2 + ")");
|
||||
}
|
||||
}
|
||||
|
||||
public final void exactlyOne(final Object var1, final Object var2) {
|
||||
if ((var1 != null) == (var2 != null)) {
|
||||
throw new ValidationException(
|
||||
"Exactly one of (" + variableNames + ") must be non-null, " +
|
||||
"but are (" + var1 + ", " + var2 + ")");
|
||||
}
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ public final class HashGenerator {
|
||||
"abcdefghijklmnopqrstuvwxyz" +
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||
"0123456789/.";
|
||||
private static boolean couldBeHashEnabled; // TODO.legacy: remove after legacy data is migrated
|
||||
private static boolean couldBeHashEnabled; // TODO.impl: remove after legacy data is migrated
|
||||
|
||||
public enum Algorithm {
|
||||
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
|
||||
|
@ -5,9 +5,8 @@ import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
@ -15,7 +14,7 @@ import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
// a partial HsOfficeDebitorEntity to reduce the number of SQL queries to load the entity
|
||||
@Entity
|
||||
@ -25,7 +24,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayAs("BookingDebitor")
|
||||
public class HsBookingDebitorEntity implements Stringifyable, WithRoleId {
|
||||
public class HsBookingDebitorEntity implements Stringifyable {
|
||||
|
||||
public static final String DEBITOR_NUMBER_TAG = "D-";
|
||||
|
||||
|
@ -1,19 +1,14 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.debitor;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> {
|
||||
public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> {
|
||||
|
||||
@Timed("app.booking.debitor.repo.findByUuid")
|
||||
Optional<HsBookingDebitorEntity> findByUuid(UUID id);
|
||||
|
||||
@Timed("app.booking.debitor.repo.findByDebitorNumber")
|
||||
List<HsBookingDebitorEntity> findByDebitorNumber(int debitorNumber);
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Getter
|
||||
public class BookingItemCreatedAppEvent extends ApplicationEvent {
|
||||
|
||||
private BookingItemCreatedEventEntity entity;
|
||||
|
||||
public BookingItemCreatedAppEvent(
|
||||
@NotNull final Object source,
|
||||
@NotNull final HsBookingItemRealEntity newBookingItem,
|
||||
final String assetJson) {
|
||||
super(source);
|
||||
this.entity = new BookingItemCreatedEventEntity(newBookingItem, assetJson);
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MapsId;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_booking", name = "item_created_event")
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
public class BookingItemCreatedEventEntity implements BaseEntity {
|
||||
@Id
|
||||
@Column(name="bookingitemuuid")
|
||||
private UUID uuid;
|
||||
|
||||
@MapsId
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "bookingitemuuid", nullable = false)
|
||||
private HsBookingItemRealEntity bookingItem;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(name = "assetjson")
|
||||
private String assetJson;
|
||||
|
||||
@Setter
|
||||
@Column(name = "statusmessage")
|
||||
private String statusMessage;
|
||||
|
||||
public BookingItemCreatedEventEntity(
|
||||
@NotNull final HsBookingItemRealEntity newBookingItem,
|
||||
final String assetJson) {
|
||||
this.bookingItem = newBookingItem;
|
||||
this.assetJson = assetJson;
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> {
|
||||
|
||||
@Timed("app.booking.items.repo.save")
|
||||
BookingItemCreatedEventEntity save(HsBookingItemRealEntity current);
|
||||
|
||||
@Timed("app.booking.items.repo.findByBookingItem")
|
||||
BookingItemCreatedEventEntity findByBookingItem(HsBookingItemRealEntity newBookingItem);
|
||||
}
|
@ -14,9 +14,9 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
@ -45,7 +45,7 @@ import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
|
@ -1,8 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
|
||||
@ -15,8 +12,6 @@ import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@ -27,11 +22,9 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
||||
|
||||
@RestController
|
||||
@Profile("!only-office")
|
||||
public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
@Autowired
|
||||
@ -40,22 +33,15 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@Autowired
|
||||
private HsBookingItemRbacRepository bookingItemRepo;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper jsonMapper;
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper em;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.bookingItems.api.getListOfBookingItemsByProjectUuid")
|
||||
public ResponseEntity<List<HsBookingItemResource>> getListOfBookingItemsByProjectUuid(
|
||||
public ResponseEntity<List<HsBookingItemResource>> listBookingItemsByProjectUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID projectUuid) {
|
||||
@ -69,8 +55,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingItems.api.postNewBookingItem")
|
||||
public ResponseEntity<HsBookingItemResource> postNewBookingItem(
|
||||
public ResponseEntity<HsBookingItemResource> addBookingItem(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsBookingItemInsertResource body) {
|
||||
@ -78,8 +63,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
final var saveProcessor = new BookingItemEntitySaveProcessor(em, entityToSave);
|
||||
final var mapped = saveProcessor
|
||||
final var mapped = new BookingItemEntitySaveProcessor(em, entityToSave)
|
||||
.preprocessEntity()
|
||||
.validateEntity()
|
||||
.prepareForSave()
|
||||
@ -87,7 +71,6 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
.validateContext()
|
||||
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
|
||||
.revampProperties();
|
||||
publishSavedEvent(saveProcessor, body);
|
||||
|
||||
final var uri =
|
||||
MvcUriComponentsBuilder.fromController(getClass())
|
||||
@ -99,8 +82,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.bookingItems.api.getSingleBookingItemByUuid")
|
||||
public ResponseEntity<HsBookingItemResource> getSingleBookingItemByUuid(
|
||||
public ResponseEntity<HsBookingItemResource> getBookingItemByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID bookingItemUuid) {
|
||||
@ -117,7 +99,6 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingItems.api.deleteBookingIemByUuid")
|
||||
public ResponseEntity<Void> deleteBookingIemByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -132,7 +113,6 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingItems.api.patchBookingItem")
|
||||
public ResponseEntity<HsBookingItemResource> patchBookingItem(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -150,16 +130,6 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
private void publishSavedEvent(final BookingItemEntitySaveProcessor saveProcessor, final HsBookingItemInsertResource body) {
|
||||
try {
|
||||
final var bookingItemRealEntity = em.getReference(HsBookingItemRealEntity.class, saveProcessor.getEntity().getUuid());
|
||||
applicationEventPublisher.publishEvent(new BookingItemCreatedAppEvent(
|
||||
this, bookingItemRealEntity, jsonMapper.writeValueAsString(body.getHostingAsset())));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
final BiConsumer<HsBookingItem, HsBookingItemResource> ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setValidFrom(entity.getValidity().lower());
|
||||
if (entity.getValidity().hasUpperBound()) {
|
||||
@ -171,9 +141,6 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
final BiConsumer<HsBookingItemInsertResource, HsBookingItemRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
entity.setProject(em.find(HsBookingProjectRealEntity.class, resource.getProjectUuid()));
|
||||
ofNullable(resource.getParentItemUuid())
|
||||
.map(parentItemUuid -> em.find(HsBookingItemRealEntity.class, parentItemUuid))
|
||||
.ifPresent(entity::setParentItem);
|
||||
entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
|
||||
entity.putResources(KeyValueMap.from(resource.getResources()));
|
||||
};
|
||||
|
@ -5,8 +5,8 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
|
||||
import jakarta.persistence.AttributeOverride;
|
||||
import jakarta.persistence.AttributeOverrides;
|
||||
@ -15,20 +15,20 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.IOException;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NULLABLE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NULLABLE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_booking", name = "item_rv")
|
||||
@ -41,7 +41,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
})
|
||||
public class HsBookingItemRbacEntity extends HsBookingItem {
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("bookingItem", HsBookingItemRbacEntity.class)
|
||||
.withIdentityView(SQL.projection("caption"))
|
||||
.withRestrictedViewOrderBy(SQL.expression("validity"))
|
||||
|
@ -1,32 +1,23 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
public interface HsBookingItemRbacRepository extends HsBookingItemRepository<HsBookingItemRbacEntity>,
|
||||
Repository<HsBookingItemRbacEntity, UUID> {
|
||||
|
||||
@Timed("app.bookingItems.repo.findByUuid.rbac")
|
||||
Optional<HsBookingItemRbacEntity> findByUuid(final UUID bookingItemUuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.findByCaption.rbac")
|
||||
List<HsBookingItemRbacEntity> findByCaption(String bookingItemCaption);
|
||||
|
||||
@Timed("app.bookingItems.repo.findAllByProjectUuid.rbac")
|
||||
List<HsBookingItemRbacEntity> findAllByProjectUuid(final UUID projectItemUuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.save.rbac")
|
||||
HsBookingItemRbacEntity save(HsBookingItemRbacEntity current);
|
||||
|
||||
@Timed("app.bookingItems.repo.deleteByUuid.rbac")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.count.rbac")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,32 +1,23 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
public interface HsBookingItemRealRepository extends HsBookingItemRepository<HsBookingItemRealEntity>,
|
||||
Repository<HsBookingItemRealEntity, UUID> {
|
||||
|
||||
@Timed("app.bookingItems.repo.findByUuid.real")
|
||||
Optional<HsBookingItemRealEntity> findByUuid(final UUID bookingItemUuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.findByCaption.real")
|
||||
List<HsBookingItemRealEntity> findByCaption(String bookingItemCaption);
|
||||
|
||||
@Timed("app.bookingItems.repo.findAllByProjectUuid.real")
|
||||
List<HsBookingItemRealEntity> findAllByProjectUuid(final UUID projectItemUuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.save.real")
|
||||
HsBookingItemRealEntity save(HsBookingItemRealEntity current);
|
||||
|
||||
@Timed("app.bookingItems.repo.deleteByUuid.real")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.count.real")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,13 +1,9 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
public interface HsBookingItemRepository<E extends HsBookingItem> {
|
||||
|
||||
Optional<E> findByUuid(final UUID bookingItemUuid);
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||
@ -21,11 +20,7 @@ public class BookingItemEntitySaveProcessor {
|
||||
private final HsEntityValidator<HsBookingItem> validator;
|
||||
private String expectedStep = "preprocessEntity";
|
||||
private final EntityManager em;
|
||||
|
||||
@Getter
|
||||
private HsBookingItem entity;
|
||||
|
||||
@Getter
|
||||
private HsBookingItemResource resource;
|
||||
|
||||
public BookingItemEntitySaveProcessor(final EntityManager em, final HsBookingItem entity) {
|
||||
@ -48,7 +43,7 @@ public class BookingItemEntitySaveProcessor {
|
||||
return this;
|
||||
}
|
||||
|
||||
// TODO.legacy: remove once the migration of legacy data is done
|
||||
// TODO.impl: remove once the migration of legacy data is done
|
||||
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
||||
public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
||||
step("validateEntity", "prepareForSave");
|
||||
|
@ -13,21 +13,27 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRA
|
||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||
|
||||
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
||||
|
||||
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
|
||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
||||
public static final String TARGET_UNIX_USER_PROPERTY_NAME = "targetUnixUser";
|
||||
public static final String WEBSPACE_NAME_REGEX = "[a-z][a-z0-9]{2}[0-9]{2}";
|
||||
public static final String TARGET_UNIX_USER_NAME_REGEX = "^"+WEBSPACE_NAME_REGEX+"$|^"+WEBSPACE_NAME_REGEX+"-[a-z0-9\\._-]+$";
|
||||
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
|
||||
|
||||
HsDomainSetupBookingItemValidator() {
|
||||
super(
|
||||
// TODO.spec: feels wrong
|
||||
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
|
||||
.maxLength(253)
|
||||
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
|
||||
.notMatchesRegEx(REGISTRAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name")
|
||||
.required(),
|
||||
// TODO.legacy: remove the following property once we give up legacy compatibility
|
||||
stringProperty(TARGET_UNIX_USER_PROPERTY_NAME).writeOnce()
|
||||
.maxLength(253)
|
||||
.matchesRegEx(TARGET_UNIX_USER_NAME_REGEX).describedAs("is not a valid unix-user name")
|
||||
.writeOnce()
|
||||
.required(),
|
||||
stringProperty(VERIFICATION_CODE_PROPERTY_NAME)
|
||||
.minLength(12)
|
||||
.maxLength(64)
|
||||
@ -49,11 +55,6 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
||||
}
|
||||
|
||||
private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) {
|
||||
final var userDefinedVerificationCode = propertiesProvider.getDirectValue(VERIFICATION_CODE_PROPERTY_NAME, String.class);
|
||||
if (userDefinedVerificationCode != null) {
|
||||
return userDefinedVerificationCode;
|
||||
}
|
||||
|
||||
final var alphaNumeric = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
final var secureRandom = new SecureRandom();
|
||||
final var sb = new StringBuilder();
|
||||
|
@ -3,15 +3,31 @@ package net.hostsharing.hsadminng.hs.booking.project;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@ -50,4 +66,50 @@ public abstract class HsBookingProject implements Stringifyable, BaseEntity<HsBo
|
||||
return ofNullable(debitor).map(HsBookingDebitorEntity::toShortString).orElse("D-???????") +
|
||||
":" + caption;
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("project", HsBookingProjectRbacEntity.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.cleanIdentifier(bookingProject.caption) as idName
|
||||
FROM hs_booking.project bookingProject
|
||||
JOIN hs_office.debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(SQL.expression("caption"))
|
||||
.withUpdatableColumns("version", "caption")
|
||||
|
||||
.importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingDefaultCase(),
|
||||
dependsOnColumn("debitorUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
|
||||
.importEntityAlias("debitorRel", HsOfficeRelationRbacEntity.class, usingCase(DEBITOR),
|
||||
dependsOnColumn("debitorUuid"),
|
||||
fetchedBySql("""
|
||||
SELECT ${columns}
|
||||
FROM hs_office.relation debitorRel
|
||||
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
|
||||
WHERE debitor.uuid = ${REF}.debitorUuid
|
||||
"""),
|
||||
NOT_NULL)
|
||||
.toRole("debitorRel", ADMIN).grantPermission(INSERT)
|
||||
.toRole(GLOBAL, ADMIN).grantPermission(DELETE)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.incomingSuperRole("debitorRel", AGENT).unassumed();
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(AGENT)
|
||||
.createSubRole(TENANT, (with) -> {
|
||||
with.outgoingSubRole("debitorRel", TENANT);
|
||||
with.permission(SELECT);
|
||||
})
|
||||
|
||||
.limitDiagramTo("project", "debitorRel", "rbac.global");
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("6-hs-booking/620-booking-project/6203-hs-booking-project-rbac");
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,13 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.project;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjectsApi;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@ -21,14 +19,13 @@ import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
@RestController
|
||||
@Profile("!only-office")
|
||||
public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Autowired
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsBookingProjectRbacRepository bookingProjectRepo;
|
||||
@ -38,8 +35,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.bookingProjects.api.getListOfBookingProjectsByDebitorUuid")
|
||||
public ResponseEntity<List<HsBookingProjectResource>> getListOfBookingProjectsByDebitorUuid(
|
||||
public ResponseEntity<List<HsBookingProjectResource>> listBookingProjectsByDebitorUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID debitorUuid) {
|
||||
@ -53,8 +49,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingProjects.api.postNewBookingProject")
|
||||
public ResponseEntity<HsBookingProjectResource> postNewBookingProject(
|
||||
public ResponseEntity<HsBookingProjectResource> addBookingProject(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsBookingProjectInsertResource body) {
|
||||
@ -76,7 +71,6 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.bookingProjects.api.getBookingProjectByUuid")
|
||||
public ResponseEntity<HsBookingProjectResource> getBookingProjectByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -93,7 +87,6 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingProjects.api.deleteBookingIemByUuid")
|
||||
public ResponseEntity<Void> deleteBookingIemByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -108,7 +101,6 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingProjects.api.patchBookingProject")
|
||||
public ResponseEntity<HsBookingProjectResource> patchBookingProject(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
|
@ -6,30 +6,30 @@ import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.IOException;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_booking", name = "project_rv")
|
||||
@ -39,7 +39,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
@NoArgsConstructor
|
||||
public class HsBookingProjectRbacEntity extends HsBookingProject {
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("project", HsBookingProjectRbacEntity.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.cleanIdentifier(bookingProject.caption) as idName
|
||||
|
@ -1,32 +1,22 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.project;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
public interface HsBookingProjectRbacRepository extends HsBookingProjectRepository<HsBookingProjectRbacEntity>,
|
||||
Repository<HsBookingProjectRbacEntity, UUID> {
|
||||
|
||||
@Timed("app.bookingProjects.repo.findByUuid.rbac")
|
||||
Optional<HsBookingProjectRbacEntity> findByUuid(final UUID bookingProjectUuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.findByCaption.rbac")
|
||||
List<HsBookingProjectRbacEntity> findByCaption(final String projectCaption);
|
||||
|
||||
@Timed("app.bookingProjects.repo.findAllByDebitorUuid.rbac")
|
||||
List<HsBookingProjectRbacEntity> findAllByDebitorUuid(final UUID bookingProjectUuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.save.rbac")
|
||||
HsBookingProjectRbacEntity save(HsBookingProjectRbacEntity current);
|
||||
|
||||
@Timed("app.bookingProjects.repo.deleteByUuid.rbac")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.count.rbac")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,32 +1,22 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.project;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
public interface HsBookingProjectRealRepository extends HsBookingProjectRepository<HsBookingProjectRealEntity>,
|
||||
Repository<HsBookingProjectRealEntity, UUID> {
|
||||
|
||||
@Timed("app.bookingProjects.repo.findByUuid.real")
|
||||
Optional<HsBookingProjectRealEntity> findByUuid(final UUID bookingProjectUuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.findByCaption.real")
|
||||
List<HsBookingProjectRealEntity> findByCaption(final String projectCaption);
|
||||
|
||||
@Timed("app.bookingProjects.repo.findAllByDebitorUuid.real")
|
||||
List<HsBookingProjectRealEntity> findAllByDebitorUuid(final UUID bookingProjectUuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.save.real")
|
||||
HsBookingProjectRealEntity save(HsBookingProjectRealEntity current);
|
||||
|
||||
@Timed("app.bookingProjects.repo.deleteByUuid.real")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.count.real")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,30 +1,19 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.project;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
public interface HsBookingProjectRepository<E extends HsBookingProject> {
|
||||
|
||||
@Timed("app.booking.projects.repo.findByUuid")
|
||||
Optional<E> findByUuid(final UUID findByUuid);
|
||||
|
||||
@Timed("app.booking.projects.repo.findByCaption")
|
||||
Optional<E> findByUuid(final UUID bookingProjectUuid);
|
||||
List<E> findByCaption(final String projectCaption);
|
||||
|
||||
@Timed("app.booking.projects.repo.findAllByDebitorUuid")
|
||||
List<E> findAllByDebitorUuid(final UUID bookingProjectUuid);
|
||||
|
||||
@Timed("app.booking.projects.repo.save")
|
||||
E save(E current);
|
||||
|
||||
@Timed("app.booking.projects.repo.deleteByUuid")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.booking.projects.repo.count")
|
||||
long count();
|
||||
}
|
||||
|
@ -14,9 +14,9 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
@ -42,7 +42,7 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@ -89,9 +89,10 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
|
||||
@JoinColumn(name = "alarmcontactuuid")
|
||||
private HsOfficeContactRealEntity alarmContact;
|
||||
|
||||
@OneToMany(cascade = { CascadeType.PERSIST, CascadeType.REFRESH }, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@Builder.Default
|
||||
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid")
|
||||
private List<HsHostingAssetRealEntity> subHostingAssets;
|
||||
private List<HsHostingAssetRealEntity> subHostingAssets = new ArrayList<>();
|
||||
|
||||
@Column(name = "identifier")
|
||||
private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc
|
||||
@ -124,13 +125,6 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
|
||||
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config).assign(newConfig);
|
||||
}
|
||||
|
||||
public List<HsHostingAssetRealEntity> getSubHostingAssets() {
|
||||
if (subHostingAssets == null) {
|
||||
subHostingAssets = new ArrayList<>();
|
||||
}
|
||||
return subHostingAssets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PatchableMapWrapper<Object> directProps() {
|
||||
return getConfig();
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealRepository;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
|
||||
@ -12,10 +11,9 @@ import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAsse
|
||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
|
||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@ -28,7 +26,6 @@ import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
@RestController
|
||||
@Profile("!only-office")
|
||||
public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Autowired
|
||||
@ -38,7 +35,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsHostingAssetRbacRepository rbacAssetRepo;
|
||||
@ -51,8 +48,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.hosting.assets.api.getListOfHostingAssets")
|
||||
public ResponseEntity<List<HsHostingAssetResource>> getListOfHostingAssets(
|
||||
public ResponseEntity<List<HsHostingAssetResource>> listAssets(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID debitorUuid,
|
||||
@ -69,8 +65,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.hosting.assets.api.postNewHostingAsset")
|
||||
public ResponseEntity<HsHostingAssetResource> postNewHostingAsset(
|
||||
public ResponseEntity<HsHostingAssetResource> addAsset(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsHostingAssetInsertResource body) {
|
||||
@ -98,8 +93,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.hosting.assets.api.getSingleHostingAssetByUuid")
|
||||
public ResponseEntity<HsHostingAssetResource> getSingleHostingAssetByUuid(
|
||||
public ResponseEntity<HsHostingAssetResource> getAssetByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID assetUuid) {
|
||||
@ -115,8 +109,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.hosting.assets.api.deleteHostingAssetByUuid")
|
||||
public ResponseEntity<Void> deleteHostingAssetByUuid(
|
||||
public ResponseEntity<Void> deleteAssetUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID assetUuid) {
|
||||
@ -130,8 +123,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.hosting.assets.api.patchHostingAsset")
|
||||
public ResponseEntity<HsHostingAssetResource> patchHostingAsset(
|
||||
public ResponseEntity<HsHostingAssetResource> patchAsset(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID assetUuid,
|
||||
|
@ -1,10 +1,8 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
|
||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
|
||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@ -13,12 +11,10 @@ import java.util.Map;
|
||||
|
||||
|
||||
@RestController
|
||||
@Profile("!only-office")
|
||||
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
|
||||
|
||||
@Override
|
||||
@Timed("app.hosting.assets.api.getListOfHostingAssetTypes")
|
||||
public ResponseEntity<List<String>> getListOfHostingAssetTypes() {
|
||||
public ResponseEntity<List<String>> listAssetTypes() {
|
||||
final var resource = HostingAssetEntityValidatorRegistry.types().stream()
|
||||
.map(Enum::name)
|
||||
.toList();
|
||||
@ -26,8 +22,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Timed("app.hosting.assets.api.getListOfHostingAssetTypeProps")
|
||||
public ResponseEntity<List<Object>> getListOfHostingAssetTypeProps(
|
||||
public ResponseEntity<List<Object>> listAssetTypeProps(
|
||||
final HsHostingAssetTypeResource assetType) {
|
||||
|
||||
final Enum<HsHostingAssetType> type = HsHostingAssetType.of(assetType);
|
||||
|
@ -6,31 +6,31 @@ import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRbacEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.IOException;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.CaseDef.inCaseOf;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NULLABLE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.GUEST;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.REFERRER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.CaseDef.inCaseOf;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NULLABLE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.GUEST;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.REFERRER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_hosting", name = "asset_rv")
|
||||
@ -40,7 +40,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
@NoArgsConstructor
|
||||
public class HsHostingAssetRbacEntity extends HsHostingAsset {
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("asset", HsHostingAssetRbacEntity.class)
|
||||
.withIdentityView(SQL.projection("identifier"))
|
||||
.withRestrictedViewOrderBy(SQL.expression("identifier"))
|
||||
|
@ -1,7 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -9,13 +7,11 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
|
||||
public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<HsHostingAssetRbacEntity>, Repository<HsHostingAssetRbacEntity, UUID> {
|
||||
|
||||
@Timed("app.hostingAsset.repo.findByUuid.rbac")
|
||||
Optional<HsHostingAssetRbacEntity> findByUuid(final UUID serverUuid);
|
||||
|
||||
@Timed("app.hostingAsset.repo.findByIdentifier.rbac")
|
||||
List<HsHostingAssetRbacEntity> findByIdentifier(String assetIdentifier);
|
||||
|
||||
@Query(value = """
|
||||
@ -36,21 +32,16 @@ public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<H
|
||||
and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid)
|
||||
and (:type is null or :type=cast(ha.type as text))
|
||||
""", nativeQuery = true)
|
||||
@Timed("app.hostingAsset.repo.findAllByCriteriaImpl.rbac")
|
||||
// The JPQL query did not generate "left join" but just "join".
|
||||
// I also optimized the query by not using the _rv for hs_booking.item and hs_hosting.asset, only for hs_hosting.asset_rv.
|
||||
List<HsHostingAssetRbacEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
|
||||
|
||||
default List<HsHostingAssetRbacEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
|
||||
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
|
||||
}
|
||||
|
||||
@Timed("app.hostingAsset.repo.save.rbac")
|
||||
HsHostingAssetRbacEntity save(HsHostingAsset current);
|
||||
|
||||
@Timed("app.hostingAsset.repo.deleteByUuid.rbac")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.hostingAsset.repo.count.rbac")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,37 +1,18 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<HsHostingAssetRealEntity>, Repository<HsHostingAssetRealEntity, UUID> {
|
||||
|
||||
@Timed("app.hostingAsset.repo.findByUuid.real")
|
||||
Optional<HsHostingAssetRealEntity> findByUuid(final UUID serverUuid);
|
||||
|
||||
@Timed("app.hostingAsset.repo.findByIdentifier.real")
|
||||
List<HsHostingAssetRealEntity> findByIdentifier(String assetIdentifier);
|
||||
|
||||
default List<HsHostingAssetRealEntity> findByTypeAndIdentifier(@NotNull HsHostingAssetType type, @NotNull String identifier) {
|
||||
return findByTypeAndIdentifierImpl(type.name(), identifier);
|
||||
}
|
||||
|
||||
@Query("""
|
||||
select ha
|
||||
from HsHostingAssetRealEntity ha
|
||||
where cast(ha.type as String) = :type
|
||||
and ha.identifier = :identifier
|
||||
""")
|
||||
@Timed("app.hostingAsset.repo.findByTypeAndIdentifierImpl.real")
|
||||
List<HsHostingAssetRealEntity> findByTypeAndIdentifierImpl(@NotNull String type, @NotNull String identifier);
|
||||
|
||||
@Query(value = """
|
||||
select ha.uuid,
|
||||
ha.alarmcontactuuid,
|
||||
@ -52,19 +33,14 @@ public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<H
|
||||
""", nativeQuery = true)
|
||||
// The JPQL query did not generate "left join" but just "join".
|
||||
// I also optimized the query by not using the _rv for hs_booking.item and hs_hosting.asset, only for hs_hosting.asset_rv.
|
||||
@Timed("app.hostingAsset.repo.findAllByCriteriaImpl.real")
|
||||
List<HsHostingAssetRealEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
|
||||
|
||||
default List<HsHostingAssetRealEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
|
||||
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
|
||||
}
|
||||
|
||||
@Timed("app.hostingAsset.repo.save.real")
|
||||
HsHostingAssetRealEntity save(HsHostingAssetRealEntity current);
|
||||
|
||||
@Timed("app.hostingAsset.repo.deleteByUuid.real")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.hostingAsset.repo.count.real")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,34 +1,24 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Profile("!only-office")
|
||||
public interface HsHostingAssetRepository<E extends HsHostingAsset> {
|
||||
|
||||
@Timed("app.hosting.assets.repo.findByUuid")
|
||||
Optional<E> findByUuid(final UUID serverUuid);
|
||||
|
||||
@Timed("app.hosting.assets.repo.findByIdentifier")
|
||||
List<E> findByIdentifier(String assetIdentifier);
|
||||
|
||||
@Timed("app.hosting.assets.repo.findAllByCriteriaImpl")
|
||||
List<E> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
|
||||
|
||||
default List<E> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
|
||||
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
|
||||
}
|
||||
|
||||
@Timed("app.hosting.assets.repo.save")
|
||||
E save(HsHostingAsset current);
|
||||
|
||||
@Timed("app.hosting.assets.repo.deleteByUuid")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.hosting.assets.repo.count")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,159 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetSubInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.mapper.ToStringConverter;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.net.IDN;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP;
|
||||
|
||||
public class DomainSetupHostingAssetFactory extends HostingAssetFactory {
|
||||
|
||||
public DomainSetupHostingAssetFactory(
|
||||
final EntityManagerWrapper emw,
|
||||
final HsBookingItemRealEntity newBookingItemRealEntity,
|
||||
final HsHostingAssetAutoInsertResource asset,
|
||||
final StrictMapper StrictMapper) {
|
||||
super(emw, newBookingItemRealEntity, asset, StrictMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsHostingAsset create() {
|
||||
final var domainSetupAsset = createDomainSetupAsset(getDomainName());
|
||||
final var subHostingAssets = domainSetupAsset.getSubHostingAssets();
|
||||
|
||||
// TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups
|
||||
|
||||
final var domainHttpSetupAssetResource = findSubHostingAssetResource(HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP);
|
||||
final var assignedToUnixUserAssetEntity = domainHttpSetupAssetResource
|
||||
.map(HsHostingAssetSubInsertResource::getAssignedToAssetUuid)
|
||||
.map(uuid -> emw.find(HsHostingAssetRealEntity.class, uuid))
|
||||
.orElseThrow(() -> new ValidationException("DOMAIN_HTTP_SETUP subAsset with assignedToAssetUuid required in compatibility mode"));
|
||||
|
||||
subHostingAssets.add(
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_HTTP_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity)
|
||||
.identifier(getDomainName() + "|HTTP")
|
||||
.caption("HTTP-Setup für " + IDN.toUnicode(getDomainName())))
|
||||
);
|
||||
|
||||
// Do not add to subHostingAssets in compatibility mode, in this case, DNS setup works via file system.
|
||||
// The entity is created just for validation purposes.
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_DNS_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||
.identifier(getDomainName() + "|DNS")
|
||||
.caption("DNS-Setup für " + IDN.toUnicode(getDomainName())));
|
||||
|
||||
subHostingAssets.add(
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_MBOX_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||
.identifier(getDomainName() + "|MBOX")
|
||||
.caption("MBOX-Setup für " + IDN.toUnicode(getDomainName())))
|
||||
);
|
||||
|
||||
subHostingAssets.add(
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_SMTP_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||
.identifier(getDomainName() + "|SMTP")
|
||||
.caption("SMTP-Setup für " + IDN.toUnicode(getDomainName())))
|
||||
);
|
||||
|
||||
return domainSetupAsset;
|
||||
}
|
||||
|
||||
private HsHostingAssetRealEntity createDomainSetupAsset(final String domainName) {
|
||||
return HsHostingAssetRealEntity.builder()
|
||||
.bookingItem(fromBookingItem)
|
||||
.type(HsHostingAssetType.DOMAIN_SETUP)
|
||||
.identifier(domainName)
|
||||
.caption(asset.getCaption() != null ? asset.getCaption() : domainName)
|
||||
.alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid()))
|
||||
// the sub-hosting-assets get added later
|
||||
.build();
|
||||
}
|
||||
|
||||
private HsHostingAssetRealEntity createDomainSubSetupAssetEntity(
|
||||
final HsHostingAssetRealEntity domainSetupAsset,
|
||||
final HsHostingAssetType subAssetType,
|
||||
final Function<HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder<?, ?>, HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder<?, ?>> builderTransformer) {
|
||||
final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name());
|
||||
|
||||
final var subAssetResourceOptional = findSubHostingAssetResource(resourceType);
|
||||
|
||||
subAssetResourceOptional.ifPresentOrElse(
|
||||
this::verifyNotOverspecified,
|
||||
() -> { throw new ValidationException("sub-asset of type " + resourceType.name() + " required in legacy mode, but missing"); }
|
||||
);
|
||||
|
||||
return builderTransformer.apply(
|
||||
HsHostingAssetRealEntity.builder()
|
||||
.type(subAssetType)
|
||||
.parentAsset(domainSetupAsset))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Optional<HsHostingAssetSubInsertResource> findSubHostingAssetResource(final HsHostingAssetTypeResource resourceType) {
|
||||
return getSubHostingAssetResources().stream()
|
||||
.filter(ha -> ha.getType() == resourceType)
|
||||
.reduce(Reducer::toSingleElement);
|
||||
}
|
||||
|
||||
// TODO.legacy: while we need to stay compatible, only default values can be used, thus only the type can be specified
|
||||
private void verifyNotOverspecified(final HsHostingAssetSubInsertResource givenSubAssetResource) {
|
||||
final var convert = new ToStringConverter().ignoring("assignedToAssetUuid");
|
||||
final var expectedSubAssetResource = new HsHostingAssetSubInsertResource();
|
||||
expectedSubAssetResource.setType(givenSubAssetResource.getType());
|
||||
if ( !convert.from(givenSubAssetResource).equals(convert.from(expectedSubAssetResource)) ) {
|
||||
throw new ValidationException("sub asset " + givenSubAssetResource.getType() + " is over-specified, in compatibility mode, only default values allowed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getDomainName() {
|
||||
return asset.getIdentifier();
|
||||
}
|
||||
|
||||
private List<HsHostingAssetSubInsertResource> getSubHostingAssetResources() {
|
||||
return asset.getSubHostingAssets();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persist(final HsHostingAsset newHostingAsset) {
|
||||
super.persist(newHostingAsset);
|
||||
newHostingAsset.getSubHostingAssets().forEach(super::persist);
|
||||
}
|
||||
|
||||
private <T> T ref(final Class<T> entityClass, final UUID uuid) {
|
||||
return uuid != null ? emw.getReference(entityClass, uuid) : null;
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
|
||||
|
||||
@RequiredArgsConstructor
|
||||
abstract class HostingAssetFactory {
|
||||
|
||||
final EntityManagerWrapper emw;
|
||||
final HsBookingItemRealEntity fromBookingItem;
|
||||
final HsHostingAssetAutoInsertResource asset;
|
||||
final StrictMapper StrictMapper;
|
||||
|
||||
protected abstract HsHostingAsset create();
|
||||
|
||||
public String createAndPersist() {
|
||||
try {
|
||||
final HsHostingAsset newHostingAsset = create();
|
||||
persist(newHostingAsset);
|
||||
return null;
|
||||
} catch (final ValidationException exc) {
|
||||
return exc.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
protected void persist(final HsHostingAsset newHostingAsset) {
|
||||
new HostingAssetEntitySaveProcessor(emw, newHostingAsset)
|
||||
.preprocessEntity()
|
||||
.validateEntity()
|
||||
.prepareForSave()
|
||||
.save()
|
||||
.validateContext();
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.validation.ValidationException;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Profile("!only-office")
|
||||
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper emw;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper jsonMapper;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper StrictMapper;
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void onApplicationEvent(@NotNull BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
|
||||
if (containsAssetJson(bookingItemCreatedAppEvent)) {
|
||||
createRelatedHostingAsset(bookingItemCreatedAppEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsAssetJson(final BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
|
||||
return bookingItemCreatedAppEvent.getEntity().getAssetJson() != null;
|
||||
}
|
||||
|
||||
private void createRelatedHostingAsset(final BookingItemCreatedAppEvent event) throws JsonProcessingException {
|
||||
final var newBookingItemRealEntity = event.getEntity().getBookingItem();
|
||||
final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class);
|
||||
final var factory = switch (newBookingItemRealEntity.getType()) {
|
||||
case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER ->
|
||||
forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, StrictMapper);
|
||||
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, StrictMapper);
|
||||
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, StrictMapper);
|
||||
};
|
||||
if (factory != null) {
|
||||
final var statusMessage = factory.createAndPersist();
|
||||
// TODO.impl: once we implement retry, we need to amend this code (persist/merge/delete)
|
||||
if (statusMessage != null) {
|
||||
event.getEntity().setStatusMessage(statusMessage);
|
||||
emw.persist(event.getEntity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HostingAssetFactory forNowNoAutomaticHostingAssetCreationPossible(
|
||||
final EntityManagerWrapper emw,
|
||||
final HsBookingItemRealEntity fromBookingItem,
|
||||
final HsHostingAssetAutoInsertResource asset,
|
||||
final StrictMapper StrictMapper
|
||||
) {
|
||||
return new HostingAssetFactory(emw, fromBookingItem, asset, StrictMapper) {
|
||||
|
||||
@Override
|
||||
protected HsHostingAsset create() {
|
||||
// TODO.impl: we should validate the asset JSON, but some violations are un-avoidable at that stage
|
||||
throw new ValidationException("waiting for manual setup of hosting asset for booking item of type " + fromBookingItem.getType());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
|
||||
|
||||
public ManagedWebspaceHostingAssetFactory(
|
||||
final EntityManagerWrapper emw,
|
||||
final HsBookingItemRealEntity newBookingItemRealEntity,
|
||||
final HsHostingAssetAutoInsertResource asset,
|
||||
final StrictMapper StrictMapper) {
|
||||
super(emw, newBookingItemRealEntity, asset, StrictMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsHostingAsset create() {
|
||||
if (asset.getType() != HsHostingAssetTypeResource.MANAGED_WEBSPACE) {
|
||||
throw new ValidationException("requires MANAGED_WEBSPACE hosting asset, but got " +
|
||||
Optional.of(asset)
|
||||
.map(HsHostingAssetAutoInsertResource::getType)
|
||||
.map(Enum::name)
|
||||
.orElse(null));
|
||||
}
|
||||
final var managedWebspaceHostingAsset = StrictMapper.map(asset, HsHostingAssetRealEntity.class);
|
||||
managedWebspaceHostingAsset.setBookingItem(fromBookingItem);
|
||||
emw.createQuery(
|
||||
"SELECT asset FROM HsHostingAssetRealEntity asset WHERE asset.bookingItem.uuid=:bookingItemUuid",
|
||||
HsHostingAssetRealEntity.class)
|
||||
.setParameter("bookingItemUuid", fromBookingItem.getParentItem().getUuid())
|
||||
.getResultStream().findFirst()
|
||||
.ifPresent(managedWebspaceHostingAsset::setParentAsset);
|
||||
|
||||
return managedWebspaceHostingAsset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persist(final HsHostingAsset newManagedWebspaceHostingAsset) {
|
||||
super.persist(newManagedWebspaceHostingAsset);
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ public class HostingAssetEntitySaveProcessor {
|
||||
return this;
|
||||
}
|
||||
|
||||
// TODO.legacy: remove once the migration of legacy data is done
|
||||
// TODO.impl: remove once the migration of legacy data is done
|
||||
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
||||
public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
||||
step("validateEntity", "prepareForSave");
|
||||
|
@ -15,7 +15,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro
|
||||
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||
|
||||
// TODO.legacy: make package private once we've migrated the legacy data
|
||||
// TODO.impl: make package private once we've migrated the legacy data
|
||||
public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
|
||||
// according to RFC 1035 (section 5) and RFC 1034
|
||||
@ -33,7 +33,7 @@ public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityVal
|
||||
RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT;
|
||||
public static final String IDENTIFIER_SUFFIX = "|DNS";
|
||||
|
||||
private static List<String> zoneFileErrors = null; // TODO.legacy: remove once legacy data is migrated
|
||||
private static List<String> zoneFileErrors = null; // TODO.impl: remove once legacy data is migrated
|
||||
|
||||
HsDomainDnsSetupHostingAssetValidator() {
|
||||
super(
|
||||
|
@ -1,11 +1,10 @@
|
||||
package net.hostsharing.hsadminng.hs.office.bankaccount;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.iban4j.BicUtil;
|
||||
import org.iban4j.IbanUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -25,15 +24,14 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeBankAccountRepository bankAccountRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.bankAccounts.api.patchDebitor")
|
||||
public ResponseEntity<List<HsOfficeBankAccountResource>> getListOfBankAccounts(
|
||||
public ResponseEntity<List<HsOfficeBankAccountResource>> listBankAccounts(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final String holder) {
|
||||
@ -47,8 +45,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.bankAccounts.api.postNewBankAccount")
|
||||
public ResponseEntity<HsOfficeBankAccountResource> postNewBankAccount(
|
||||
public ResponseEntity<HsOfficeBankAccountResource> addBankAccount(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficeBankAccountInsertResource body) {
|
||||
@ -74,8 +71,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.bankAccounts.api.getSingleBankAccountByUuid")
|
||||
public ResponseEntity<HsOfficeBankAccountResource> getSingleBankAccountByUuid(
|
||||
public ResponseEntity<HsOfficeBankAccountResource> getBankAccountByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID bankAccountUuid) {
|
||||
@ -91,7 +87,6 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.bankAccounts.api.deleteBankAccountByUuid")
|
||||
public ResponseEntity<Void> deleteBankAccountByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
|
@ -3,20 +3,20 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "bankaccount_rv")
|
||||
@ -57,7 +57,7 @@ public class HsOfficeBankAccountEntity implements BaseEntity<HsOfficeBankAccount
|
||||
return holder;
|
||||
}
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class)
|
||||
.withIdentityView(SQL.projection("iban"))
|
||||
.withUpdatableColumns("holder", "iban", "bic")
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.bankaccount;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -10,31 +9,23 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeBankAccountRepository extends Repository<HsOfficeBankAccountEntity, UUID> {
|
||||
|
||||
@Timed("app.office.bankAccounts.repo.findByUuid")
|
||||
Optional<HsOfficeBankAccountEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
SELECT c FROM HsOfficeBankAccountEntity c
|
||||
WHERE lower(c.holder) like lower(concat(:holder, '%'))
|
||||
ORDER BY c.holder
|
||||
""")
|
||||
@Timed("app.office.bankAccounts.repo.findByOptionalHolderLikeImpl")
|
||||
""")
|
||||
List<HsOfficeBankAccountEntity> findByOptionalHolderLikeImpl(String holder);
|
||||
|
||||
default List<HsOfficeBankAccountEntity> findByOptionalHolderLike(String holder) {
|
||||
return findByOptionalHolderLikeImpl(holder == null ? "" : holder);
|
||||
}
|
||||
|
||||
|
||||
@Timed("app.office.bankAccounts.repo.findByIbanOrderByIbanAsc")
|
||||
List<HsOfficeBankAccountEntity> findByIbanOrderByIbanAsc(String iban);
|
||||
|
||||
@Timed("app.office.bankAccounts.repo.save")
|
||||
<S extends HsOfficeBankAccountEntity> S save(S entity);
|
||||
|
||||
@Timed("app.office.bankAccounts.repo.deleteByUuid")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.office.bankAccounts.repo.count")
|
||||
long count();
|
||||
}
|
||||
|
@ -11,10 +11,9 @@ import lombok.experimental.FieldNameConstants;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
@ -28,7 +27,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@ -38,7 +37,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@FieldNameConstants
|
||||
@DisplayAs("Contact")
|
||||
public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContact>, WithRoleId {
|
||||
public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContact> {
|
||||
|
||||
private static Stringify<HsOfficeContact> toString = stringify(HsOfficeContact.class, "contact")
|
||||
.withProp(Fields.caption, HsOfficeContact::getCaption)
|
||||
@ -55,14 +54,8 @@ public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContac
|
||||
@Column(name = "caption")
|
||||
private String caption;
|
||||
|
||||
@Builder.Default
|
||||
@Setter(AccessLevel.NONE)
|
||||
@Type(JsonType.class)
|
||||
@Column(name = "postaladdress")
|
||||
private Map<String, String> postalAddress = new HashMap<>();
|
||||
|
||||
@Transient
|
||||
private PatchableMapWrapper<String> postalAddressWrapper;
|
||||
private String postalAddress; // multiline free-format text
|
||||
|
||||
@Builder.Default
|
||||
@Setter(AccessLevel.NONE)
|
||||
@ -82,17 +75,6 @@ public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContac
|
||||
@Transient
|
||||
private PatchableMapWrapper<String> phoneNumbersWrapper;
|
||||
|
||||
public PatchableMapWrapper<String> getPostalAddress() {
|
||||
return PatchableMapWrapper.of(
|
||||
postalAddressWrapper,
|
||||
(newWrapper) -> {postalAddressWrapper = newWrapper;},
|
||||
postalAddress);
|
||||
}
|
||||
|
||||
public void putPostalAddress(Map<String, String> newPostalAddress) {
|
||||
getPostalAddress().assign(newPostalAddress);
|
||||
}
|
||||
|
||||
public PatchableMapWrapper<String> getEmailAddresses() {
|
||||
return PatchableMapWrapper.of(
|
||||
emailAddressesWrapper,
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.hostsharing.hsadminng.hs.office.contact;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
|
||||
@ -17,7 +16,6 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static net.hostsharing.hsadminng.errors.Validate.validate;
|
||||
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
|
||||
|
||||
@RestController
|
||||
@ -28,25 +26,20 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeContactRbacRepository contactRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.contacts.api.getListOfContacts")
|
||||
public ResponseEntity<List<HsOfficeContactResource>> getListOfContacts(
|
||||
public ResponseEntity<List<HsOfficeContactResource>> listContacts(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final String caption,
|
||||
final String emailAddress) {
|
||||
final String caption) {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
validate("caption, emailAddress").atMaxOne(caption, emailAddress);
|
||||
final var entities = emailAddress != null
|
||||
? contactRepo.findContactByEmailAddress(emailAddress)
|
||||
: contactRepo.findContactByOptionalCaptionLike(caption);
|
||||
final var entities = contactRepo.findContactByOptionalCaptionLike(caption);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsOfficeContactResource.class);
|
||||
return ResponseEntity.ok(resources);
|
||||
@ -54,8 +47,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.contacts.api.postNewContact")
|
||||
public ResponseEntity<HsOfficeContactResource> postNewContact(
|
||||
public ResponseEntity<HsOfficeContactResource> addContact(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficeContactInsertResource body) {
|
||||
@ -77,8 +69,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.contacts.api.getSingleContactByUuid")
|
||||
public ResponseEntity<HsOfficeContactResource> getSingleContactByUuid(
|
||||
public ResponseEntity<HsOfficeContactResource> getContactByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID contactUuid) {
|
||||
@ -94,7 +85,6 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.contacts.api.deleteContactByUuid")
|
||||
public ResponseEntity<Void> deleteContactByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -111,7 +101,6 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.contacts.api.patchContact")
|
||||
public ResponseEntity<HsOfficeContactResource> patchContact(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -131,7 +120,6 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
entity.putPostalAddress(from(resource.getPostalAddress()));
|
||||
entity.putEmailAddresses(from(resource.getEmailAddresses()));
|
||||
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
|
||||
};
|
||||
|
@ -18,8 +18,7 @@ class HsOfficeContactEntityPatcher implements EntityPatcher<HsOfficeContactPatch
|
||||
@Override
|
||||
public void apply(final HsOfficeContactPatchResource resource) {
|
||||
OptionalFromJson.of(resource.getCaption()).ifPresent(entity::setCaption);
|
||||
Optional.ofNullable(resource.getPostalAddress())
|
||||
.ifPresent(r -> entity.getPostalAddress().patch(KeyValueMap.from(resource.getPostalAddress())));
|
||||
OptionalFromJson.of(resource.getPostalAddress()).ifPresent(entity::setPostalAddress);
|
||||
Optional.ofNullable(resource.getEmailAddresses())
|
||||
.ifPresent(r -> entity.getEmailAddresses().patch(KeyValueMap.from(resource.getEmailAddresses())));
|
||||
Optional.ofNullable(resource.getPhoneNumbers())
|
||||
|
@ -3,17 +3,17 @@ package net.hostsharing.hsadminng.hs.office.contact;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "contact_rv")
|
||||
@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
@DisplayAs("RbacContact")
|
||||
public class HsOfficeContactRbacEntity extends HsOfficeContact {
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("contact", HsOfficeContactRbacEntity.class)
|
||||
.withIdentityView(SQL.projection("caption"))
|
||||
.withUpdatableColumns("caption", "postalAddress", "emailAddresses", "phoneNumbers")
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.contact;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -10,33 +9,18 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeContactRbacRepository extends Repository<HsOfficeContactRbacEntity, UUID> {
|
||||
|
||||
@Timed("app.office.contacts.repo.findByUuid.rbac")
|
||||
Optional<HsOfficeContactRbacEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
SELECT c FROM HsOfficeContactRbacEntity c
|
||||
WHERE :caption is null
|
||||
OR c.caption like concat(cast(:caption as text), '%')
|
||||
""")
|
||||
@Timed("app.office.contacts.repo.findContactByOptionalCaptionLike.rbac")
|
||||
""")
|
||||
List<HsOfficeContactRbacEntity> findContactByOptionalCaptionLike(String caption);
|
||||
|
||||
@Query(value = """
|
||||
select c.* from hs_office.contact_rv c
|
||||
where exists (
|
||||
SELECT 1 FROM jsonb_each_text(c.emailAddresses) AS kv(key, value)
|
||||
WHERE kv.value LIKE :emailAddressRegEx
|
||||
)
|
||||
""", nativeQuery = true)
|
||||
@Timed("app.office.contacts.repo.findContactByEmailAddress.rbac")
|
||||
List<HsOfficeContactRbacEntity> findContactByEmailAddress(final String emailAddressRegEx);
|
||||
|
||||
@Timed("app.office.contacts.repo.save.rbac")
|
||||
HsOfficeContactRbacEntity save(final HsOfficeContactRbacEntity entity);
|
||||
|
||||
@Timed("app.office.contacts.repo.deleteByUuid.rbac")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.office.contacts.repo.count.rbac")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.contact;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -10,23 +9,18 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeContactRealRepository extends Repository<HsOfficeContactRealEntity, UUID> {
|
||||
|
||||
@Timed("app.office.contacts.repo.findByUuid.real")
|
||||
Optional<HsOfficeContactRealEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
SELECT c FROM HsOfficeContactRealEntity c
|
||||
WHERE :caption is null
|
||||
OR c.caption like concat(cast(:caption as text), '%')
|
||||
""")
|
||||
@Timed("app.office.contacts.repo.findContactByOptionalCaptionLike.real")
|
||||
""")
|
||||
List<HsOfficeContactRealEntity> findContactByOptionalCaptionLike(String caption);
|
||||
|
||||
@Timed("app.office.contacts.repo.save.real")
|
||||
HsOfficeContactRealEntity save(final HsOfficeContactRealEntity entity);
|
||||
|
||||
@Timed("app.office.contacts.repo.deleteByUuid.real")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.office.contacts.repo.count.real")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,16 +1,10 @@
|
||||
package net.hostsharing.hsadminng.hs.office.coopassets;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
||||
@ -20,21 +14,13 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.REVERSAL;
|
||||
import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.TRANSFER;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.CLEARING;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DEPOSIT;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DISBURSAL;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.LOSS;
|
||||
import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*;
|
||||
|
||||
@RestController
|
||||
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
|
||||
@ -43,21 +29,14 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper emw;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeMembershipRepository membershipRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.coopAssets.api.getListOfCoopAssets")
|
||||
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets(
|
||||
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> listCoopAssets(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID membershipUuid,
|
||||
@ -70,17 +49,13 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
fromValueDate,
|
||||
toValueDate);
|
||||
|
||||
final var resources = mapper.mapList(
|
||||
entities,
|
||||
HsOfficeCoopAssetsTransactionResource.class,
|
||||
ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var resources = mapper.mapList(entities, HsOfficeCoopAssetsTransactionResource.class);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.coopAssets.api.postNewCoopAssetTransaction")
|
||||
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> postNewCoopAssetTransaction(
|
||||
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> addCoopAssetsTransaction(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
|
||||
@ -88,10 +63,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
context.define(currentSubject, assumedRoles);
|
||||
validate(requestBody);
|
||||
|
||||
final var entityToSave = mapper.map(
|
||||
requestBody,
|
||||
HsOfficeCoopAssetsTransactionEntity.class,
|
||||
RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
final var entityToSave = mapper.map(requestBody, HsOfficeCoopAssetsTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
final var saved = coopAssetsTransactionRepo.save(entityToSave);
|
||||
|
||||
final var uri =
|
||||
@ -99,15 +71,15 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
.path("/api/hs/office/coopassetstransactions/{id}")
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(saved, HsOfficeCoopAssetsTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsOfficeCoopAssetsTransactionResource.class);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.coopAssets.api.getSingleCoopAssetTransactionByUuid")
|
||||
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getSingleCoopAssetTransactionByUuid(
|
||||
final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) {
|
||||
|
||||
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getCoopAssetTransactionByUuid(
|
||||
final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
@ -115,11 +87,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
final var resource = mapper.map(
|
||||
result.get(),
|
||||
HsOfficeCoopAssetsTransactionResource.class,
|
||||
ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(resource);
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeCoopAssetsTransactionResource.class));
|
||||
|
||||
}
|
||||
|
||||
@ -134,7 +102,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
private static void validateDebitTransaction(
|
||||
final HsOfficeCoopAssetsTransactionInsertResource requestBody,
|
||||
final ArrayList<String> violations) {
|
||||
if (List.of(DEPOSIT, HsOfficeCoopAssetsTransactionTypeResource.ADOPTION).contains(requestBody.getTransactionType())
|
||||
if (List.of(DEPOSIT, ADOPTION).contains(requestBody.getTransactionType())
|
||||
&& requestBody.getAssetValue().signum() < 0) {
|
||||
violations.add("for %s, assetValue must be positive but is \"%.2f\"".formatted(
|
||||
requestBody.getTransactionType(), requestBody.getAssetValue()));
|
||||
@ -144,8 +112,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
private static void validateCreditTransaction(
|
||||
final HsOfficeCoopAssetsTransactionInsertResource requestBody,
|
||||
final ArrayList<String> violations) {
|
||||
if (List.of(DISBURSAL, HsOfficeCoopAssetsTransactionTypeResource.TRANSFER, CLEARING, LOSS)
|
||||
.contains(requestBody.getTransactionType())
|
||||
if (List.of(DISBURSAL, TRANSFER, CLEARING, LOSS).contains(requestBody.getTransactionType())
|
||||
&& requestBody.getAssetValue().signum() > 0) {
|
||||
violations.add("for %s, assetValue must be negative but is \"%.2f\"".formatted(
|
||||
requestBody.getTransactionType(), requestBody.getAssetValue()));
|
||||
@ -161,157 +128,10 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
}
|
||||
}
|
||||
|
||||
// TODO.refa: this logic needs to get extracted to a service
|
||||
final BiConsumer<HsOfficeCoopAssetsTransactionEntity, HsOfficeCoopAssetsTransactionResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setMembershipUuid(entity.getMembership().getUuid());
|
||||
resource.setMembershipMemberNumber(entity.getMembership().getTaggedMemberNumber());
|
||||
|
||||
withNonNull(
|
||||
resource.getReversalAssetTx(), reversalAssetTxResource -> {
|
||||
reversalAssetTxResource.setMembershipUuid(entity.getMembership().getUuid());
|
||||
reversalAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber());
|
||||
reversalAssetTxResource.setRevertedAssetTxUuid(entity.getUuid());
|
||||
withNonNull(
|
||||
entity.getAdoptionAssetTx(), adoptionAssetTx ->
|
||||
reversalAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid()));
|
||||
withNonNull(
|
||||
entity.getTransferAssetTx(), transferAssetTxResource ->
|
||||
reversalAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid()));
|
||||
});
|
||||
|
||||
withNonNull(
|
||||
resource.getRevertedAssetTx(), revertAssetTxResource -> {
|
||||
revertAssetTxResource.setMembershipUuid(entity.getMembership().getUuid());
|
||||
revertAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber());
|
||||
revertAssetTxResource.setReversalAssetTxUuid(entity.getUuid());
|
||||
withNonNull(
|
||||
entity.getRevertedAssetTx().getAdoptionAssetTx(), adoptionAssetTx ->
|
||||
revertAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid()));
|
||||
withNonNull(
|
||||
entity.getRevertedAssetTx().getTransferAssetTx(), transferAssetTxResource ->
|
||||
revertAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid()));
|
||||
});
|
||||
|
||||
withNonNull(
|
||||
resource.getAdoptionAssetTx(), adoptionAssetTxResource -> {
|
||||
adoptionAssetTxResource.setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid());
|
||||
adoptionAssetTxResource.setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber());
|
||||
adoptionAssetTxResource.setTransferAssetTxUuid(entity.getUuid());
|
||||
withNonNull(
|
||||
entity.getAdoptionAssetTx().getReversalAssetTx(), reversalAssetTx ->
|
||||
adoptionAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid()));
|
||||
});
|
||||
|
||||
withNonNull(
|
||||
resource.getTransferAssetTx(), transferAssetTxResource -> {
|
||||
resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid());
|
||||
resource.getTransferAssetTx()
|
||||
.setMembershipMemberNumber(entity.getTransferAssetTx().getTaggedMemberNumber());
|
||||
resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid());
|
||||
withNonNull(
|
||||
entity.getTransferAssetTx().getReversalAssetTx(), reversalAssetTx ->
|
||||
transferAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid()));
|
||||
});
|
||||
};
|
||||
|
||||
// TODO.refa: this logic needs to get extracted to a service
|
||||
final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
|
||||
if (resource.getMembershipUuid() != null) {
|
||||
final HsOfficeMembershipEntity membership = ofNullable(emw.find(
|
||||
HsOfficeMembershipEntity.class,
|
||||
resource.getMembershipUuid()))
|
||||
.orElseThrow(() -> new EntityNotFoundException("membership.uuid %s not found".formatted(
|
||||
resource.getMembershipUuid())));
|
||||
entity.setMembership(membership);
|
||||
}
|
||||
|
||||
if (entity.getTransactionType() == REVERSAL) {
|
||||
if (resource.getRevertedAssetTxUuid() == null) {
|
||||
throw new ValidationException("REVERSAL asset transaction requires revertedAssetTx.uuid");
|
||||
}
|
||||
final var revertedAssetTx = coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid())
|
||||
.orElseThrow(() -> new EntityNotFoundException("revertedAssetTx.uuid %s not found".formatted(
|
||||
resource.getRevertedAssetTxUuid())));
|
||||
revertedAssetTx.setReversalAssetTx(entity);
|
||||
entity.setRevertedAssetTx(revertedAssetTx);
|
||||
if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) {
|
||||
throw new ValidationException("given assetValue=" + resource.getAssetValue() +
|
||||
" but must be negative value from reverted asset tx: " + revertedAssetTx.getAssetValue());
|
||||
}
|
||||
|
||||
if (revertedAssetTx.getTransactionType() == TRANSFER) {
|
||||
final var adoptionAssetTx = revertedAssetTx.getAdoptionAssetTx();
|
||||
final var adoptionReversalAssetTx = HsOfficeCoopAssetsTransactionEntity.builder()
|
||||
.transactionType(REVERSAL)
|
||||
.membership(adoptionAssetTx.getMembership())
|
||||
.revertedAssetTx(adoptionAssetTx)
|
||||
.assetValue(adoptionAssetTx.getAssetValue().negate())
|
||||
.comment(resource.getComment())
|
||||
.reference(resource.getReference())
|
||||
.valueDate(resource.getValueDate())
|
||||
.build();
|
||||
adoptionAssetTx.setReversalAssetTx(adoptionReversalAssetTx);
|
||||
adoptionReversalAssetTx.setRevertedAssetTx(adoptionAssetTx);
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER) {
|
||||
final var adoptingMembership = determineAdoptingMembership(resource);
|
||||
if ( entity.getMembership() == adoptingMembership) {
|
||||
throw new ValidationException("transferring and adopting membership must be different, but both are " +
|
||||
adoptingMembership.getTaggedMemberNumber());
|
||||
}
|
||||
final var adoptingAssetTx = createAdoptingAssetTx(entity, adoptingMembership);
|
||||
entity.setAdoptionAssetTx(adoptingAssetTx);
|
||||
if ( resource.getReverseEntryUuid() != null ) {
|
||||
entity.setAdjustedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getReverseEntryUuid())
|
||||
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] reverseEntityUuid %s not found".formatted(resource.getReverseEntryUuid()))));
|
||||
}
|
||||
};
|
||||
|
||||
private HsOfficeMembershipEntity determineAdoptingMembership(final HsOfficeCoopAssetsTransactionInsertResource resource) {
|
||||
final var adoptingMembershipUuid = resource.getAdoptingMembershipUuid();
|
||||
final var adoptingMembershipMemberNumber = resource.getAdoptingMembershipMemberNumber();
|
||||
if (adoptingMembershipUuid != null && adoptingMembershipMemberNumber != null) {
|
||||
throw new ValidationException(
|
||||
// @formatter:off
|
||||
resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER
|
||||
? "either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both"
|
||||
: "adoptingMembership.uuid and adoptingMembership.memberNumber must not be given for transactionType="
|
||||
+ resource.getTransactionType());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
if (adoptingMembershipUuid != null) {
|
||||
final var adoptingMembership = membershipRepo.findByUuid(adoptingMembershipUuid);
|
||||
return adoptingMembership.orElseThrow(() ->
|
||||
new ValidationException(
|
||||
"adoptingMembership.uuid='" + adoptingMembershipUuid + "' not found or not accessible"));
|
||||
}
|
||||
|
||||
if (adoptingMembershipMemberNumber != null) {
|
||||
final var adoptingMemberNumber = Integer.valueOf(adoptingMembershipMemberNumber.substring("M-".length()));
|
||||
final var adoptingMembership = membershipRepo.findMembershipByMemberNumber(adoptingMemberNumber);
|
||||
return adoptingMembership.orElseThrow( () ->
|
||||
new ValidationException("adoptingMembership.memberNumber='" + adoptingMembershipMemberNumber
|
||||
+ "' not found or not accessible")
|
||||
);
|
||||
}
|
||||
|
||||
throw new ValidationException(
|
||||
"either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType="
|
||||
+ HsOfficeCoopAssetsTransactionTypeResource.TRANSFER);
|
||||
}
|
||||
|
||||
private HsOfficeCoopAssetsTransactionEntity createAdoptingAssetTx(
|
||||
final HsOfficeCoopAssetsTransactionEntity transferAssetTxEntity,
|
||||
final HsOfficeMembershipEntity adoptingMembership) {
|
||||
return HsOfficeCoopAssetsTransactionEntity.builder()
|
||||
.membership(adoptingMembership)
|
||||
.transactionType(HsOfficeCoopAssetsTransactionType.ADOPTION)
|
||||
.transferAssetTx(transferAssetTxEntity)
|
||||
.assetValue(transferAssetTxEntity.getAssetValue().negate())
|
||||
.comment(transferAssetTxEntity.getComment())
|
||||
.reference(transferAssetTxEntity.getReference())
|
||||
.valueDate(transferAssetTxEntity.getValueDate())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
|
||||
package net.hostsharing.hsadminng.hs.office.coopassets;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
@ -7,41 +8,30 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "coopassettx_rv")
|
||||
@ -60,14 +50,13 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getReference)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getComment)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getRevertedAssetTx)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getReversalAssetTx)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getAdoptionAssetTx)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getTransferAssetTx)
|
||||
.withProp(at -> ofNullable(at.getAdjustedAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
|
||||
.withProp(at -> ofNullable(at.getAdjustmentAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@GeneratedValue(generator = "UUID")
|
||||
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
|
||||
private UUID uuid;
|
||||
|
||||
@Version
|
||||
@ -88,7 +77,7 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
|
||||
* The signed value which directly affects the booking balance.
|
||||
*
|
||||
* <p>This means, that a DEPOSIT is always positive, a DISBURSAL is always negative,
|
||||
* but an REVERSAL can bei either positive or negative.
|
||||
* but an ADJUSTMENT can bei either positive or negative.
|
||||
* See {@link HsOfficeCoopAssetsTransactionType} for</p> more information.
|
||||
*/
|
||||
@Column(name = "assetvalue")
|
||||
@ -106,23 +95,15 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
|
||||
@Column(name = "comment")
|
||||
private String comment;
|
||||
|
||||
// Optionally, the UUID of the corresponding transaction for a reversal transaction.
|
||||
/**
|
||||
* Optionally, the UUID of the corresponding transaction for an adjustment transaction.
|
||||
*/
|
||||
@OneToOne
|
||||
@JoinColumn(name = "revertedassettxuuid")
|
||||
private HsOfficeCoopAssetsTransactionEntity revertedAssetTx;
|
||||
@JoinColumn(name = "adjustedassettxuuid")
|
||||
private HsOfficeCoopAssetsTransactionEntity adjustedAssetTx;
|
||||
|
||||
// and the other way around
|
||||
@OneToOne(mappedBy = "revertedAssetTx", cascade = CascadeType.PERSIST)
|
||||
private HsOfficeCoopAssetsTransactionEntity reversalAssetTx;
|
||||
|
||||
// Optionally, the UUID of the corresponding transaction for a transfer transaction.
|
||||
@OneToOne(cascade = CascadeType.PERSIST)
|
||||
@JoinColumn(name = "assetadoptiontxuuid")
|
||||
private HsOfficeCoopAssetsTransactionEntity adoptionAssetTx;
|
||||
|
||||
// and the other way around
|
||||
@OneToOne(mappedBy = "adoptionAssetTx", cascade = CascadeType.PERSIST)
|
||||
private HsOfficeCoopAssetsTransactionEntity transferAssetTx;
|
||||
@OneToOne(mappedBy = "adjustedAssetTx")
|
||||
private HsOfficeCoopAssetsTransactionEntity adjustmentAssetTx;
|
||||
|
||||
@Override
|
||||
public HsOfficeCoopAssetsTransactionEntity load() {
|
||||
@ -131,15 +112,15 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTaggedMemberNumber() {
|
||||
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-???????");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
}
|
||||
|
||||
public String getTaggedMemberNumber() {
|
||||
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-???????");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return "%s:%.3s:%+1.2f".formatted(
|
||||
@ -148,9 +129,9 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
|
||||
ofNullable(assetValue).orElse(BigDecimal.ZERO));
|
||||
}
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("coopAssetsTransaction", HsOfficeCoopAssetsTransactionEntity.class)
|
||||
.withIdentityView(SQL.projection("reference"))
|
||||
.withIdentityView(RbacView.SQL.projection("reference"))
|
||||
.withUpdatableColumns("comment")
|
||||
.importEntityAlias("membership", HsOfficeMembershipEntity.class, usingDefaultCase(),
|
||||
dependsOnColumn("membershipUuid"),
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.coopassets;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -11,7 +10,6 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeCoopAssetsTransactionRepository extends Repository<HsOfficeCoopAssetsTransactionEntity, UUID> {
|
||||
|
||||
@Timed("app.office.coopAssets.repo.findByUuid")
|
||||
Optional<HsOfficeCoopAssetsTransactionEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
@ -20,14 +18,11 @@ public interface HsOfficeCoopAssetsTransactionRepository extends Repository<HsOf
|
||||
AND ( CAST(:fromValueDate AS java.time.LocalDate) IS NULL OR (at.valueDate >= :fromValueDate))
|
||||
AND ( CAST(:toValueDate AS java.time.LocalDate)IS NULL OR (at.valueDate <= :toValueDate))
|
||||
ORDER BY at.membership.memberNumberSuffix, at.valueDate
|
||||
""")
|
||||
@Timed("app.office.coopAssets.repo.findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange")
|
||||
""")
|
||||
List<HsOfficeCoopAssetsTransactionEntity> findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange(
|
||||
UUID membershipUuid, LocalDate fromValueDate, LocalDate toValueDate);
|
||||
|
||||
@Timed("app.office.coopAssets.repo.save")
|
||||
HsOfficeCoopAssetsTransactionEntity save(final HsOfficeCoopAssetsTransactionEntity entity);
|
||||
|
||||
@Timed("app.office.coopAssets.repo.count")
|
||||
long count();
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ public enum HsOfficeCoopAssetsTransactionType {
|
||||
/**
|
||||
* correction of wrong bookings, value can be positive or negative
|
||||
*/
|
||||
REVERSAL,
|
||||
ADJUSTMENT,
|
||||
|
||||
/**
|
||||
* payment received from member after signing shares, value >0
|
||||
|
@ -1,13 +1,12 @@
|
||||
package net.hostsharing.hsadminng.hs.office.coopshares;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
||||
@ -24,7 +23,6 @@ import java.util.function.BiConsumer;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.CANCELLATION;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION;
|
||||
import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
|
||||
|
||||
@RestController
|
||||
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
|
||||
@ -33,18 +31,14 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeMembershipRepository membershipRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.coopShares.api.getListOfCoopShares")
|
||||
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> getListOfCoopShares(
|
||||
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> listCoopShares(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID membershipUuid,
|
||||
@ -57,17 +51,13 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
fromValueDate,
|
||||
toValueDate);
|
||||
|
||||
final var resources = mapper.mapList(
|
||||
entities,
|
||||
HsOfficeCoopSharesTransactionResource.class,
|
||||
ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var resources = mapper.mapList(entities, HsOfficeCoopSharesTransactionResource.class);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.coopShares.repo.postNewCoopSharesTransaction")
|
||||
public ResponseEntity<HsOfficeCoopSharesTransactionResource> postNewCoopSharesTransaction(
|
||||
public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficeCoopSharesTransactionInsertResource requestBody) {
|
||||
@ -75,10 +65,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
context.define(currentSubject, assumedRoles);
|
||||
validate(requestBody);
|
||||
|
||||
final var entityToSave = mapper.map(
|
||||
requestBody,
|
||||
HsOfficeCoopSharesTransactionEntity.class,
|
||||
RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
final var entityToSave = mapper.map(requestBody, HsOfficeCoopSharesTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
|
||||
final var saved = coopSharesTransactionRepo.save(entityToSave);
|
||||
|
||||
@ -87,26 +74,22 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
.path("/api/hs/office/coopsharestransactions/{id}")
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(saved, HsOfficeCoopSharesTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsOfficeCoopSharesTransactionResource.class);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.coopShares.repo.getSingleCoopShareTransactionByUuid")
|
||||
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getSingleCoopShareTransactionByUuid(
|
||||
final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
|
||||
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getCoopShareTransactionByUuid(
|
||||
final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var result = coopSharesTransactionRepo.findByUuid(shareTransactionUuid);
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(
|
||||
result.get(),
|
||||
HsOfficeCoopSharesTransactionResource.class,
|
||||
ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
final var result = coopSharesTransactionRepo.findByUuid(shareTransactionUuid);
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeCoopSharesTransactionResource.class));
|
||||
|
||||
}
|
||||
|
||||
@ -148,16 +131,9 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
}
|
||||
|
||||
final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
entity.setMembership(resolve("membership.uuid", resource.getMembershipUuid(), membershipRepo::findByUuid));
|
||||
if (resource.getRevertedShareTxUuid() != null) {
|
||||
entity.setRevertedShareTx(resolve(
|
||||
"revertedShareTx.uuid",
|
||||
resource.getRevertedShareTxUuid(),
|
||||
coopSharesTransactionRepo::findByUuid));
|
||||
if ( resource.getAdjustedShareTxUuid() != null ) {
|
||||
entity.setAdjustedShareTx(coopSharesTransactionRepo.findByUuid(resource.getAdjustedShareTxUuid())
|
||||
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] adjustedShareTxUuid %s not found".formatted(resource.getAdjustedShareTxUuid()))));
|
||||
}
|
||||
};
|
||||
|
||||
final BiConsumer<HsOfficeCoopSharesTransactionEntity, HsOfficeCoopSharesTransactionResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setMembershipUuid(entity.getMembership().getUuid());
|
||||
};
|
||||
}
|
||||
|
@ -7,39 +7,29 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "coopsharetx_rv")
|
||||
@ -58,8 +48,8 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
|
||||
.withProp(HsOfficeCoopSharesTransactionEntity::getShareCount)
|
||||
.withProp(HsOfficeCoopSharesTransactionEntity::getReference)
|
||||
.withProp(HsOfficeCoopSharesTransactionEntity::getComment)
|
||||
.withProp(at -> ofNullable(at.getRevertedShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
|
||||
.withProp(at -> ofNullable(at.getReversalShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
|
||||
.withProp(at -> ofNullable(at.getAdjustedShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
|
||||
.withProp(at -> ofNullable(at.getAdjustmentShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@ -81,7 +71,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
|
||||
* The signed value which directly affects the booking balance.
|
||||
*
|
||||
* <p>This means, that a SUBSCRIPTION is always positive, a CANCELLATION is always negative,
|
||||
* but an REVERSAL can bei either positive or negative.
|
||||
* but an ADJUSTMENT can bei either positive or negative.
|
||||
* See {@link HsOfficeCoopSharesTransactionType} for</p> more information.
|
||||
*/
|
||||
@Column(name = "valuedate")
|
||||
@ -103,14 +93,14 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
|
||||
private String comment;
|
||||
|
||||
/**
|
||||
* Optionally, the UUID of the corresponding transaction for a REVERSAL transaction.
|
||||
* Optionally, the UUID of the corresponding transaction for an adjustment transaction.
|
||||
*/
|
||||
@OneToOne
|
||||
@JoinColumn(name = "revertedsharetxuuid")
|
||||
private HsOfficeCoopSharesTransactionEntity revertedShareTx;
|
||||
@JoinColumn(name = "adjustedsharetxuuid")
|
||||
private HsOfficeCoopSharesTransactionEntity adjustedShareTx;
|
||||
|
||||
@OneToOne(mappedBy = "revertedShareTx")
|
||||
private HsOfficeCoopSharesTransactionEntity reversalShareTx;
|
||||
@OneToOne(mappedBy = "adjustedShareTx")
|
||||
private HsOfficeCoopSharesTransactionEntity adjustmentShareTx;
|
||||
|
||||
@Override
|
||||
public HsOfficeCoopSharesTransactionEntity load() {
|
||||
@ -133,7 +123,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
|
||||
return "%s:%.3s:%+d".formatted(getMemberNumberTagged(), transactionType, shareCount);
|
||||
}
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("coopSharesTransaction", HsOfficeCoopSharesTransactionEntity.class)
|
||||
.withIdentityView(SQL.projection("reference"))
|
||||
.withUpdatableColumns("comment")
|
||||
@ -142,7 +132,6 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
|
||||
// the membership:ADMIN is not to be confused with the member itself, it's an account manager of the coop
|
||||
.toRole("membership", ADMIN).grantPermission(INSERT)
|
||||
.toRole("membership", ADMIN).grantPermission(UPDATE)
|
||||
.toRole("membership", AGENT).grantPermission(SELECT);
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.coopshares;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -11,7 +10,6 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeCoopSharesTransactionRepository extends Repository<HsOfficeCoopSharesTransactionEntity, UUID> {
|
||||
|
||||
@Timed("app.office.coopShares.repo.findByUuid")
|
||||
Optional<HsOfficeCoopSharesTransactionEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
@ -20,14 +18,11 @@ public interface HsOfficeCoopSharesTransactionRepository extends Repository<HsOf
|
||||
AND ( CAST(:fromValueDate AS java.time.LocalDate) IS NULL OR (st.valueDate >= :fromValueDate))
|
||||
AND ( CAST(:toValueDate AS java.time.LocalDate)IS NULL OR (st.valueDate <= :toValueDate))
|
||||
ORDER BY st.membership.memberNumberSuffix, st.valueDate
|
||||
""")
|
||||
@Timed("app.office.coopShares.repo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange")
|
||||
""")
|
||||
List<HsOfficeCoopSharesTransactionEntity> findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(
|
||||
UUID membershipUuid, LocalDate fromValueDate, LocalDate toValueDate);
|
||||
|
||||
@Timed("app.office.coopShares.repo.save")
|
||||
HsOfficeCoopSharesTransactionEntity save(final HsOfficeCoopSharesTransactionEntity entity);
|
||||
|
||||
@Timed("app.office.coopShares.repo.count")
|
||||
long count();
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
|
||||
|
||||
public enum HsOfficeCoopSharesTransactionType {
|
||||
/**
|
||||
* reversal of wrong bookings, with either positive or negative value identical to reversed transaction
|
||||
* correction of wrong bookings, with either positive or negative value
|
||||
*/
|
||||
REVERSAL,
|
||||
ADJUSTMENT,
|
||||
|
||||
/**
|
||||
* shares signed, e.g. with the declaration of accession, value >0
|
||||
|
@ -1,17 +1,14 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitorsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityExistsValidator;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hibernate.Hibernate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -25,11 +22,8 @@ import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
||||
import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
|
||||
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
|
||||
|
||||
@RestController
|
||||
|
||||
@ -39,84 +33,87 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeDebitorRepository debitorRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeRelationRealRepository realRelRepo;
|
||||
private HsOfficeRelationRealRepository relrealRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePersonRealRepository realPersonRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeContactRealRepository realContactRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeBankAccountRepository bankAccountRepo;
|
||||
private EntityExistsValidator entityValidator;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.debitors.api.getListOfDebitors")
|
||||
public ResponseEntity<List<HsOfficeDebitorResource>> getListOfDebitors(
|
||||
public ResponseEntity<List<HsOfficeDebitorResource>> listDebitors(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final String name,
|
||||
final UUID partnerUuid,
|
||||
final String partnerNumber) {
|
||||
final Integer debitorNumber) {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entities = partnerNumber != null
|
||||
? debitorRepo.findDebitorsByPartnerNumber(cropTag("P-", partnerNumber))
|
||||
: partnerUuid != null
|
||||
? debitorRepo.findDebitorsByPartnerUuid(partnerUuid)
|
||||
: debitorRepo.findDebitorsByOptionalNameLike(name);
|
||||
final var entities = debitorNumber != null
|
||||
? debitorRepo.findDebitorByDebitorNumber(debitorNumber)
|
||||
: debitorRepo.findDebitorByOptionalNameLike(name);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var resources = mapper.mapList(entities, HsOfficeDebitorResource.class);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.debitors.api.postNewDebitor")
|
||||
public ResponseEntity<HsOfficeDebitorResource> postNewDebitor(
|
||||
public ResponseEntity<HsOfficeDebitorResource> addDebitor(
|
||||
String currentSubject,
|
||||
String assumedRoles,
|
||||
HsOfficeDebitorInsertResource body) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
Validate.isTrue(
|
||||
body.getDebitorRel() == null || body.getDebitorRelUuid() == null,
|
||||
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRelUuid() == null,
|
||||
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
|
||||
Validate.isTrue(
|
||||
body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
|
||||
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
|
||||
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found none");
|
||||
Validate.isTrue(
|
||||
body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
|
||||
Validate.isTrue(body.getDebitorRel() == null ||
|
||||
body.getDebitorRel().getType() == null || DEBITOR.name().equals(body.getDebitorRel().getType()),
|
||||
"ERROR: [400] debitorRel.type must be '"+DEBITOR.name()+"' or null for default");
|
||||
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
|
||||
"ERROR: [400] debitorRel.mark must be null");
|
||||
|
||||
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
|
||||
if ( body.getDebitorRel() != null ) {
|
||||
body.getDebitorRel().setType(DEBITOR.name());
|
||||
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
|
||||
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
|
||||
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
|
||||
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
|
||||
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
|
||||
} else {
|
||||
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
|
||||
debitorRelOptional.ifPresentOrElse(
|
||||
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
|
||||
() -> { throw new ValidationException("Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());});
|
||||
}
|
||||
|
||||
final var savedEntity = debitorRepo.save(entityToSave).reload(em);
|
||||
final var savedEntity = debitorRepo.save(entityToSave);
|
||||
em.flush();
|
||||
em.refresh(savedEntity);
|
||||
|
||||
final var uri =
|
||||
MvcUriComponentsBuilder.fromController(getClass())
|
||||
.path("/api/hs/office/debitors/{id}")
|
||||
.buildAndExpand(savedEntity.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.debitors.api.getSingleDebitorByUuid")
|
||||
public ResponseEntity<HsOfficeDebitorResource> getSingleDebitorByUuid(
|
||||
public ResponseEntity<HsOfficeDebitorResource> getDebitorByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID debitorUuid) {
|
||||
@ -127,29 +124,11 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.debitors.api.getSingleDebitorByDebitorNumber")
|
||||
public ResponseEntity<HsOfficeDebitorResource> getSingleDebitorByDebitorNumber(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final Integer debitorNumber) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var result = debitorRepo.findDebitorByDebitorNumber(debitorNumber);
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeDebitorResource.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.debitors.api.deleteDebitorByUuid")
|
||||
public ResponseEntity<Void> deleteDebitorByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -166,7 +145,6 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.debitors.api.patchDebitor")
|
||||
public ResponseEntity<HsOfficeDebitorResource> patchDebitor(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -175,49 +153,13 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var current = debitorRepo.findByUuid(debitorUuid).orElseThrow().reload(em);
|
||||
final var current = debitorRepo.findByUuid(debitorUuid).orElseThrow();
|
||||
|
||||
new HsOfficeDebitorEntityPatcher(em, current).apply(body);
|
||||
|
||||
final var saved = debitorRepo.save(current);
|
||||
Hibernate.initialize(saved);
|
||||
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
final BiConsumer<HsOfficeDebitorInsertResource, HsOfficeDebitorEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
if (resource.getDebitorRel() != null) {
|
||||
final var debitorRel = realRelRepo.save(HsOfficeRelationRealEntity.builder()
|
||||
.type(DEBITOR)
|
||||
.anchor(resolve(
|
||||
"debitorRel.anchor.uuid", resource.getDebitorRel().getAnchorUuid(), realPersonRepo::findByUuid))
|
||||
.holder(resolve(
|
||||
"debitorRel.holder.uuid", resource.getDebitorRel().getHolderUuid(), realPersonRepo::findByUuid))
|
||||
.contact(resolve(
|
||||
"debitorRel.contact.uuid", resource.getDebitorRel().getContactUuid(), realContactRepo::findByUuid))
|
||||
.build());
|
||||
entity.setDebitorRel(debitorRel);
|
||||
} else {
|
||||
final var debitorRelOptional = realRelRepo.findByUuid(resource.getDebitorRelUuid());
|
||||
debitorRelOptional.ifPresentOrElse(
|
||||
debitorRel -> {
|
||||
entity.setDebitorRel(realRelRepo.save(debitorRel));
|
||||
},
|
||||
() -> {
|
||||
throw new ValidationException(
|
||||
"Unable to find debitorRel.uuid: " + resource.getDebitorRelUuid());
|
||||
});
|
||||
}
|
||||
|
||||
if (resource.getRefundBankAccountUuid() != null) {
|
||||
entity.setRefundBankAccount(resolve(
|
||||
"refundBankAccount.uuid", resource.getRefundBankAccountUuid(), bankAccountRepo::findByUuid));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
final BiConsumer<HsOfficeDebitorEntity, HsOfficeDebitorResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setDebitorNumber(entity.getTaggedDebitorNumber());
|
||||
resource.getPartner().setPartnerNumber(entity.getPartner().getTaggedPartnerNumber());
|
||||
};
|
||||
}
|
||||
|
@ -7,16 +7,16 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartner;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
import org.hibernate.annotations.JoinFormula;
|
||||
import org.hibernate.annotations.NotFound;
|
||||
import org.hibernate.annotations.NotFoundAction;
|
||||
@ -40,18 +40,18 @@ import static jakarta.persistence.CascadeType.PERSIST;
|
||||
import static jakarta.persistence.CascadeType.REFRESH;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NULLABLE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NULLABLE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "debitor_rv")
|
||||
@ -75,6 +75,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
|
||||
private UUID uuid;
|
||||
|
||||
@Version
|
||||
@ -86,16 +87,16 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
|
||||
value = """
|
||||
(
|
||||
SELECT DISTINCT partner.uuid
|
||||
FROM hs_office.partner partner
|
||||
JOIN hs_office.relation dRel
|
||||
ON dRel.uuid = debitorRelUuid AND dRel.type = 'DEBITOR'
|
||||
JOIN hs_office.relation pRel
|
||||
FROM hs_office.partner_rv partner
|
||||
JOIN hs_office.relation_rv dRel
|
||||
ON dRel.uuid = debitorreluuid AND dRel.type = 'DEBITOR'
|
||||
JOIN hs_office.relation_rv pRel
|
||||
ON pRel.uuid = partner.partnerRelUuid AND pRel.type = 'PARTNER'
|
||||
WHERE pRel.holderUuid = dRel.anchorUuid
|
||||
)
|
||||
""")
|
||||
@NotFound(action = NotFoundAction.EXCEPTION) // TODO.impl: map a simplified raw-PartnerEntity, just for the partner-number
|
||||
private HsOfficePartnerRealEntity partner;
|
||||
@NotFound(action = NotFoundAction.IGNORE) // TODO.impl: map a simplified raw-PartnerEntity, just for the partner-number
|
||||
private HsOfficePartnerEntity partner;
|
||||
|
||||
@Column(name = "debitornumbersuffix", length = 2)
|
||||
@Pattern(regexp = TWO_DECIMAL_DIGITS)
|
||||
@ -131,7 +132,9 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
|
||||
@Override
|
||||
public HsOfficeDebitorEntity load() {
|
||||
BaseEntity.super.load();
|
||||
partner.load();
|
||||
if (partner != null) {
|
||||
partner.load();
|
||||
}
|
||||
debitorRel.load();
|
||||
if (refundBankAccount != null) {
|
||||
refundBankAccount.load();
|
||||
@ -139,14 +142,19 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTaggedDebitorNumber() {
|
||||
private String getDebitorNumberString() {
|
||||
return ofNullable(partner)
|
||||
.filter(partner -> debitorNumberSuffix != null)
|
||||
.map(HsOfficePartner::getPartnerNumber)
|
||||
.map(partnerNumber -> DEBITOR_NUMBER_TAG + partnerNumber + debitorNumberSuffix)
|
||||
.map(HsOfficePartnerEntity::getPartnerNumber)
|
||||
.map(Object::toString)
|
||||
.map(partnerNumber -> partnerNumber + debitorNumberSuffix)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public Integer getDebitorNumber() {
|
||||
return ofNullable(getDebitorNumberString()).map(Integer::parseInt).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
@ -154,10 +162,10 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return getTaggedDebitorNumber();
|
||||
return DEBITOR_NUMBER_TAG + getDebitorNumberString();
|
||||
}
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("debitor", HsOfficeDebitorEntity.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT debitor.uuid AS uuid,
|
||||
|
@ -1,7 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -11,41 +9,28 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEntity, UUID> {
|
||||
|
||||
@Timed("app.office.debitors.repo.findByUuid")
|
||||
Optional<HsOfficeDebitorEntity> findByUuid(UUID id);
|
||||
|
||||
@Timed("app.office.debitors.repo.findDebitorByPartnerUuid")
|
||||
List<HsOfficeDebitorEntity> findDebitorsByPartnerUuid(UUID partnerUuid);
|
||||
|
||||
@Query("""
|
||||
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
||||
JOIN HsOfficePartnerRealEntity partner
|
||||
JOIN HsOfficePartnerEntity partner
|
||||
ON partner.partnerRel.holder = debitor.debitorRel.anchor
|
||||
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
|
||||
WHERE partner.partnerNumber = :partnerNumber
|
||||
AND (:debitorNumberSuffix IS NULL OR debitor.debitorNumberSuffix = :debitorNumberSuffix)
|
||||
""")
|
||||
@Timed("app.office.debitors.repo.findDebitorByPartnerNumberAndDebitorNumberSuffix")
|
||||
List<HsOfficeDebitorEntity> findDebitorByPartnerNumberAndOptionalDebitorNumberSuffix(int partnerNumber, String debitorNumberSuffix);
|
||||
WHERE cast(partner.partnerNumber as integer) = :partnerNumber
|
||||
AND cast(debitor.debitorNumberSuffix as integer) = :debitorNumberSuffix
|
||||
""")
|
||||
List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int partnerNumber, byte debitorNumberSuffix);
|
||||
|
||||
default Optional<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int debitorNumber) {
|
||||
final var partnerNumber = debitorNumber / 100;
|
||||
final String suffix = String.format("%02d", debitorNumber % 100);
|
||||
final var result = findDebitorByPartnerNumberAndOptionalDebitorNumberSuffix(partnerNumber, suffix);
|
||||
return result.stream().reduce(Reducer::toSingleElement);
|
||||
}
|
||||
|
||||
default List<HsOfficeDebitorEntity> findDebitorsByPartnerNumber(int partnerNumber) {
|
||||
final var result = findDebitorByPartnerNumberAndOptionalDebitorNumberSuffix(partnerNumber, null);
|
||||
return result;
|
||||
default List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int debitorNumber) {
|
||||
return findDebitorByDebitorNumber( debitorNumber/100, (byte) (debitorNumber%100));
|
||||
}
|
||||
|
||||
@Query("""
|
||||
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
||||
JOIN HsOfficePartnerRealEntity partner
|
||||
JOIN HsOfficePartnerEntity partner
|
||||
ON partner.partnerRel.holder = debitor.debitorRel.anchor
|
||||
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
|
||||
JOIN HsOfficePersonRealEntity person
|
||||
JOIN HsOfficePersonEntity person
|
||||
ON person.uuid = partner.partnerRel.holder.uuid
|
||||
OR person.uuid = debitor.debitorRel.holder.uuid
|
||||
JOIN HsOfficeContactRealEntity contact
|
||||
@ -58,15 +43,11 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
|
||||
OR person.givenName like concat(cast(:name as text), '%')
|
||||
OR contact.caption like concat(cast(:name as text), '%')
|
||||
""")
|
||||
@Timed("app.office.debitors.repo.findDebitorByOptionalNameLike")
|
||||
List<HsOfficeDebitorEntity> findDebitorsByOptionalNameLike(String name);
|
||||
List<HsOfficeDebitorEntity> findDebitorByOptionalNameLike(String name);
|
||||
|
||||
@Timed("app.office.debitors.repo.save")
|
||||
HsOfficeDebitorEntity save(final HsOfficeDebitorEntity entity);
|
||||
|
||||
@Timed("app.office.debitors.repo.count")
|
||||
long count();
|
||||
|
||||
@Timed("app.office.debitors.repo.deleteByUuid")
|
||||
int deleteByUuid(UUID uuid);
|
||||
}
|
||||
|
@ -1,79 +1,62 @@
|
||||
package net.hostsharing.hsadminng.hs.office.membership;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeMembershipsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipResource;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRbacEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealRepository;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static net.hostsharing.hsadminng.errors.Validate.validate;
|
||||
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
|
||||
|
||||
@RestController
|
||||
|
||||
public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
|
||||
@Autowired
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePartnerRealRepository partnerRepo;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeMembershipRepository membershipRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.membership.api.getListOfMemberships")
|
||||
public ResponseEntity<List<HsOfficeMembershipResource>> getListOfMemberships(
|
||||
public ResponseEntity<List<HsOfficeMembershipResource>> listMemberships(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID partnerUuid,
|
||||
final String partnerNumber) {
|
||||
UUID partnerUuid,
|
||||
Integer memberNumber) {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
validate("partnerUuid, partnerNumber").atMaxOne(partnerUuid, partnerNumber);
|
||||
final var entities = ( memberNumber != null)
|
||||
? List.of(membershipRepo.findMembershipByMemberNumber(memberNumber))
|
||||
: membershipRepo.findMembershipsByOptionalPartnerUuid(partnerUuid);
|
||||
|
||||
final var entities = partnerNumber != null
|
||||
? membershipRepo.findMembershipsByPartnerNumber(
|
||||
cropTag(HsOfficePartnerRbacEntity.PARTNER_NUMBER_TAG, partnerNumber))
|
||||
: partnerUuid != null
|
||||
? membershipRepo.findMembershipsByPartnerUuid(partnerUuid)
|
||||
: membershipRepo.findAll();
|
||||
|
||||
final var resources = mapper.mapList(
|
||||
entities, HsOfficeMembershipResource.class,
|
||||
final var resources = mapper.mapList(entities, HsOfficeMembershipResource.class,
|
||||
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.membership.api.postNewMembership")
|
||||
public ResponseEntity<HsOfficeMembershipResource> postNewMembership(
|
||||
public ResponseEntity<HsOfficeMembershipResource> addMembership(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficeMembershipInsertResource body) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entityToSave = mapper.map(body, HsOfficeMembershipEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
final var entityToSave = mapper.map(body, HsOfficeMembershipEntity.class);
|
||||
|
||||
final var saved = membershipRepo.save(entityToSave);
|
||||
|
||||
@ -82,16 +65,14 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
.path("/api/hs/office/memberships/{id}")
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(
|
||||
saved, HsOfficeMembershipResource.class,
|
||||
final var mapped = mapper.map(saved, HsOfficeMembershipResource.class,
|
||||
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.membership.api.getSingleMembershipByUuid")
|
||||
public ResponseEntity<HsOfficeMembershipResource> getSingleMembershipByUuid(
|
||||
public ResponseEntity<HsOfficeMembershipResource> getMembershipByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID membershipUuid) {
|
||||
@ -102,33 +83,12 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(
|
||||
result.get(), HsOfficeMembershipResource.class,
|
||||
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.membership.api.getSingleMembershipByMembershipNumber")
|
||||
public ResponseEntity<HsOfficeMembershipResource> getSingleMembershipByMembershipNumber(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final Integer membershipNumber) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var result = membershipRepo.findMembershipByMemberNumber(membershipNumber);
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(
|
||||
result.get(), HsOfficeMembershipResource.class,
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeMembershipResource.class,
|
||||
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.membership.api.deleteMembershipByUuid")
|
||||
public ResponseEntity<Void> deleteMembershipByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -145,7 +105,6 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.membership.api.patchMembership")
|
||||
public ResponseEntity<HsOfficeMembershipResource> patchMembership(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -164,17 +123,10 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
}
|
||||
|
||||
final BiConsumer<HsOfficeMembershipEntity, HsOfficeMembershipResource> SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setMemberNumber(entity.getTaggedMemberNumber());
|
||||
// TODO.refa: this should be possible via ModelMapper config
|
||||
resource.setValidFrom(entity.getValidity().lower());
|
||||
if (entity.getValidity().hasUpperBound()) {
|
||||
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
||||
}
|
||||
resource.getPartner().setPartnerNumber(entity.getPartner().getTaggedPartnerNumber()); // TODO.refa: use partner mapper?
|
||||
};
|
||||
|
||||
final BiConsumer<HsOfficeMembershipInsertResource, HsOfficeMembershipEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
entity.setPartner(partnerRepo.findByUuid(resource.getPartnerUuid())
|
||||
.orElseThrow(() -> new EntityNotFoundException(
|
||||
"ERROR: [400] partnerUuid %s not found".formatted(resource.getPartnerUuid()))));
|
||||
};
|
||||
}
|
||||
|
@ -8,14 +8,13 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
@ -39,22 +38,22 @@ import static io.hypersistence.utils.hibernate.type.range.Range.emptyRange;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "membership_rv")
|
||||
@ -64,7 +63,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayAs("Membership")
|
||||
public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEntity>, Stringifyable, WithRoleId {
|
||||
public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEntity>, Stringifyable {
|
||||
|
||||
public static final String MEMBER_NUMBER_TAG = "M-";
|
||||
public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
|
||||
@ -85,7 +84,7 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "partneruuid")
|
||||
private HsOfficePartnerRealEntity partner;
|
||||
private HsOfficePartnerEntity partner;
|
||||
|
||||
@Column(name = "membernumbersuffix", length = 2)
|
||||
@Pattern(regexp = TWO_DECIMAL_DIGITS)
|
||||
@ -131,7 +130,6 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
|
||||
}
|
||||
return validity;
|
||||
}
|
||||
|
||||
public Integer getMemberNumber() {
|
||||
if (partner == null || partner.getPartnerNumber() == null || memberNumberSuffix == null ) {
|
||||
return null;
|
||||
@ -140,10 +138,6 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
|
||||
return getPartner().getPartnerNumber() * 100 + Integer.parseInt(memberNumberSuffix, 10);
|
||||
}
|
||||
|
||||
public String getTaggedMemberNumber() {
|
||||
return MEMBER_NUMBER_TAG + getMemberNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
@ -161,7 +155,7 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
|
||||
}
|
||||
}
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("membership", HsOfficeMembershipEntity.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT m.uuid AS uuid,
|
||||
|
@ -2,18 +2,18 @@ package net.hostsharing.hsadminng.hs.office.membership;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
|
||||
|
||||
private final StrictMapper mapper;
|
||||
private final StandardMapper mapper;
|
||||
private final HsOfficeMembershipEntity entity;
|
||||
|
||||
public HsOfficeMembershipEntityPatcher(
|
||||
final StrictMapper mapper,
|
||||
final StandardMapper mapper,
|
||||
final HsOfficeMembershipEntity entity) {
|
||||
this.mapper = mapper;
|
||||
this.entity = entity;
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.membership;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -11,52 +10,34 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeMembershipRepository extends Repository<HsOfficeMembershipEntity, UUID> {
|
||||
|
||||
@Timed("app.office.membership.repo.findByUuid")
|
||||
Optional<HsOfficeMembershipEntity> findByUuid(UUID id);
|
||||
|
||||
@Timed("app.office.membership.repo.save")
|
||||
HsOfficeMembershipEntity save(final HsOfficeMembershipEntity entity);
|
||||
|
||||
@Timed("app.office.membership.repo.findAll")
|
||||
List<HsOfficeMembershipEntity> findAll();
|
||||
|
||||
@Query("""
|
||||
SELECT membership FROM HsOfficeMembershipEntity membership
|
||||
WHERE membership.partner.uuid = :partnerUuid
|
||||
WHERE ( CAST(:partnerUuid as org.hibernate.type.UUIDCharType) IS NULL
|
||||
OR membership.partner.uuid = :partnerUuid )
|
||||
ORDER BY membership.partner.partnerNumber, membership.memberNumberSuffix
|
||||
""")
|
||||
@Timed("app.office.membership.repo.findMembershipsByOptionalPartnerUuid")
|
||||
List<HsOfficeMembershipEntity> findMembershipsByPartnerUuid(UUID partnerUuid);
|
||||
|
||||
@Query("""
|
||||
SELECT membership FROM HsOfficeMembershipEntity membership
|
||||
WHERE membership.partner.partnerNumber = :partnerNumber
|
||||
ORDER BY membership.partner.partnerNumber, membership.memberNumberSuffix
|
||||
""")
|
||||
@Timed("app.office.membership.repo.findMembershipsByPartnerNumber")
|
||||
List<HsOfficeMembershipEntity> findMembershipsByPartnerNumber(Integer partnerNumber);
|
||||
|
||||
""")
|
||||
List<HsOfficeMembershipEntity> findMembershipsByOptionalPartnerUuid(UUID partnerUuid);
|
||||
@Query("""
|
||||
SELECT membership FROM HsOfficeMembershipEntity membership
|
||||
WHERE (:partnerNumber = membership.partner.partnerNumber)
|
||||
AND (membership.memberNumberSuffix = :suffix)
|
||||
ORDER BY membership.memberNumberSuffix
|
||||
""")
|
||||
@Timed("app.office.membership.repo.findMembershipByMemberNumber")
|
||||
Optional<HsOfficeMembershipEntity> findMembershipByPartnerNumberAndSuffix(
|
||||
HsOfficeMembershipEntity findMembershipByPartnerNumberAndSuffix(
|
||||
@NotNull Integer partnerNumber,
|
||||
@NotNull String suffix);
|
||||
|
||||
default Optional<HsOfficeMembershipEntity> findMembershipByMemberNumber(final Integer memberNumber) {
|
||||
default HsOfficeMembershipEntity findMembershipByMemberNumber(Integer memberNumber) {
|
||||
final var partnerNumber = memberNumber / 100;
|
||||
final String suffix = String.format("%02d", memberNumber % 100);
|
||||
final var result = findMembershipByPartnerNumberAndSuffix(partnerNumber, suffix);
|
||||
return result;
|
||||
final var suffix = memberNumber % 100;
|
||||
return findMembershipByPartnerNumberAndSuffix(partnerNumber, String.format("%02d", suffix));
|
||||
}
|
||||
|
||||
@Timed("app.office.membership.repo.count")
|
||||
long count();
|
||||
|
||||
@Timed("app.office.membership.repo.deleteByUuid")
|
||||
int deleteByUuid(UUID uuid);
|
||||
}
|
||||
|
@ -1,103 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import org.hibernate.annotations.NotFound;
|
||||
import org.hibernate.annotations.NotFoundAction;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.Version;
|
||||
import java.util.UUID;
|
||||
|
||||
import static jakarta.persistence.CascadeType.DETACH;
|
||||
import static jakarta.persistence.CascadeType.MERGE;
|
||||
import static jakarta.persistence.CascadeType.PERSIST;
|
||||
import static jakarta.persistence.CascadeType.REFRESH;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@Setter
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@DisplayAs("Partner")
|
||||
public class HsOfficePartner<T extends HsOfficePartner<?>> implements Stringifyable, BaseEntity<T> {
|
||||
|
||||
public static final String PARTNER_NUMBER_TAG = "P-";
|
||||
|
||||
protected static Stringify<HsOfficePartner> stringify = stringify(HsOfficePartner.class, "partner")
|
||||
.withIdProp(HsOfficePartner::toShortString)
|
||||
.withProp(p -> ofNullable(p.getPartnerRel())
|
||||
.map(HsOfficeRelation::getHolder)
|
||||
.map(HsOfficePerson::toShortString)
|
||||
.orElse(null))
|
||||
.withProp(p -> ofNullable(p.getPartnerRel())
|
||||
.map(HsOfficeRelation::getContact)
|
||||
.map(HsOfficeContact::toShortString)
|
||||
.orElse(null))
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private UUID uuid;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
|
||||
private Integer partnerNumber;
|
||||
|
||||
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "partnerreluuid", nullable = false)
|
||||
private HsOfficeRelationRealEntity partnerRel;
|
||||
|
||||
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "detailsuuid")
|
||||
@NotFound(action = NotFoundAction.IGNORE)
|
||||
private HsOfficePartnerDetailsEntity details;
|
||||
|
||||
@Override
|
||||
public T load() {
|
||||
BaseEntity.super.load();
|
||||
partnerRel.load();
|
||||
if (details != null) {
|
||||
details.load();
|
||||
}
|
||||
//noinspection unchecked
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public String getTaggedPartnerNumber() {
|
||||
return PARTNER_NUMBER_TAG + partnerNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return getTaggedPartnerNumber();
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.errors.ReferenceNotFoundException;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||
@ -9,12 +8,12 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartne
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerRelInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -26,10 +25,8 @@ import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.EX_PARTNER;
|
||||
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
|
||||
|
||||
@RestController
|
||||
|
||||
@ -39,10 +36,10 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePartnerRbacRepository partnerRepo;
|
||||
private HsOfficePartnerRepository partnerRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeRelationRealRepository relationRepo;
|
||||
@ -52,8 +49,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.partners.api.getListOfPartners")
|
||||
public ResponseEntity<List<HsOfficePartnerResource>> getListOfPartners(
|
||||
public ResponseEntity<List<HsOfficePartnerResource>> listPartners(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final String name) {
|
||||
@ -61,14 +57,13 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
|
||||
final var entities = partnerRepo.findPartnerByOptionalNameLike(name);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var resources = mapper.mapList(entities, HsOfficePartnerResource.class);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.partners.api.postNewPartner")
|
||||
public ResponseEntity<HsOfficePartnerResource> postNewPartner(
|
||||
public ResponseEntity<HsOfficePartnerResource> addPartner(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficePartnerInsertResource body) {
|
||||
@ -84,14 +79,13 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
.path("/api/hs/office/partners/{id}")
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsOfficePartnerResource.class);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.partners.api.getSinglePartnerByUuid")
|
||||
public ResponseEntity<HsOfficePartnerResource> getSinglePartnerByUuid(
|
||||
public ResponseEntity<HsOfficePartnerResource> getPartnerByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID partnerUuid) {
|
||||
@ -102,31 +96,11 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
final var mapped = mapper.map(result.get(), HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.partners.api.getSinglePartnerByPartnerNumber")
|
||||
public ResponseEntity<HsOfficePartnerResource> getSinglePartnerByPartnerNumber(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final Integer partnerNumber) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var result = partnerRepo.findPartnerByPartnerNumber(partnerNumber);
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
final var mapped = mapper.map(result.get(), HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(mapped);
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficePartnerResource.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.partners.api.deletePartnerByUuid")
|
||||
public ResponseEntity<Void> deletePartnerByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -147,7 +121,6 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.partners.api.patchPartner")
|
||||
public ResponseEntity<HsOfficePartnerResource> patchPartner(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -164,20 +137,19 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
final var saved = partnerRepo.save(current);
|
||||
optionallyCreateExPartnerRelation(saved, previousPartnerRel);
|
||||
|
||||
final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsOfficePartnerResource.class);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
private void optionallyCreateExPartnerRelation(final HsOfficePartnerRbacEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) {
|
||||
private void optionallyCreateExPartnerRelation(final HsOfficePartnerEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) {
|
||||
if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) {
|
||||
// TODO.impl: we also need to use the new partner-person as the anchor
|
||||
relationRepo.save(previousPartnerRel.toBuilder().uuid(null).type(EX_PARTNER).build());
|
||||
}
|
||||
}
|
||||
|
||||
private HsOfficePartnerRbacEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
|
||||
final var entityToSave = new HsOfficePartnerRbacEntity();
|
||||
entityToSave.setPartnerNumber(cropTag(HsOfficePartnerRbacEntity.PARTNER_NUMBER_TAG, body.getPartnerNumber()));
|
||||
private HsOfficePartnerEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
|
||||
final var entityToSave = new HsOfficePartnerEntity();
|
||||
entityToSave.setPartnerNumber(body.getPartnerNumber());
|
||||
entityToSave.setPartnerRel(persistPartnerRel(body.getPartnerRel()));
|
||||
entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class));
|
||||
return entityToSave;
|
||||
@ -186,8 +158,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
private HsOfficeRelationRealEntity persistPartnerRel(final HsOfficePartnerRelInsertResource resource) {
|
||||
final var entity = new HsOfficeRelationRealEntity();
|
||||
entity.setType(HsOfficeRelationType.PARTNER);
|
||||
entity.setAnchor(ref(HsOfficePersonRealEntity.class, resource.getAnchorUuid()));
|
||||
entity.setHolder(ref(HsOfficePersonRealEntity.class, resource.getHolderUuid()));
|
||||
entity.setAnchor(ref(HsOfficePersonEntity.class, resource.getAnchorUuid()));
|
||||
entity.setHolder(ref(HsOfficePersonEntity.class, resource.getHolderUuid()));
|
||||
entity.setContact(ref(HsOfficeContactRealEntity.class, resource.getContactUuid()));
|
||||
em.persist(entity);
|
||||
return entity;
|
||||
@ -200,8 +172,4 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
throw new ReferenceNotFoundException(entityClass, uuid, exc);
|
||||
}
|
||||
}
|
||||
|
||||
final BiConsumer<HsOfficePartnerRbacEntity, HsOfficePartnerResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setPartnerNumber(entity.getTaggedPartnerNumber());
|
||||
};
|
||||
}
|
||||
|
@ -2,22 +2,22 @@ package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "partner_details_rv")
|
||||
@ -67,7 +67,7 @@ public class HsOfficePartnerDetailsEntity implements BaseEntity<HsOfficePartnerD
|
||||
}
|
||||
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT partnerDetails.uuid as uuid, partner_iv.idName as idName
|
||||
|
@ -0,0 +1,128 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.NotFound;
|
||||
import org.hibernate.annotations.NotFoundAction;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static jakarta.persistence.CascadeType.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "partner_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayAs("Partner")
|
||||
public class HsOfficePartnerEntity implements Stringifyable, BaseEntity<HsOfficePartnerEntity> {
|
||||
|
||||
public static final String PARTNER_NUMBER_TAG = "P-";
|
||||
|
||||
private static Stringify<HsOfficePartnerEntity> stringify = stringify(HsOfficePartnerEntity.class, "partner")
|
||||
.withIdProp(HsOfficePartnerEntity::toShortString)
|
||||
.withProp(p -> ofNullable(p.getPartnerRel())
|
||||
.map(HsOfficeRelation::getHolder)
|
||||
.map(HsOfficePersonEntity::toShortString)
|
||||
.orElse(null))
|
||||
.withProp(p -> ofNullable(p.getPartnerRel())
|
||||
.map(HsOfficeRelation::getContact)
|
||||
.map(HsOfficeContact::toShortString)
|
||||
.orElse(null))
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private UUID uuid;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
|
||||
private Integer partnerNumber;
|
||||
|
||||
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "partnerreluuid", nullable = false)
|
||||
private HsOfficeRelationRealEntity partnerRel;
|
||||
|
||||
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "detailsuuid")
|
||||
@NotFound(action = NotFoundAction.IGNORE)
|
||||
private HsOfficePartnerDetailsEntity details;
|
||||
|
||||
@Override
|
||||
public HsOfficePartnerEntity load() {
|
||||
BaseEntity.super.load();
|
||||
partnerRel.load();
|
||||
details.load();
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTaggedPartnerNumber() {
|
||||
return PARTNER_NUMBER_TAG + partnerNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return getTaggedPartnerNumber();
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("partner", HsOfficePartnerEntity.class)
|
||||
.withIdentityView(SQL.projection("'P-' || partnerNumber"))
|
||||
.withUpdatableColumns("partnerRelUuid")
|
||||
.toRole(GLOBAL, ADMIN).grantPermission(INSERT)
|
||||
|
||||
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationRbacEntity.class,
|
||||
usingDefaultCase(),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
dependsOnColumn("partnerRelUuid"))
|
||||
.createPermission(DELETE).grantedTo("partnerRel", OWNER)
|
||||
.createPermission(UPDATE).grantedTo("partnerRel", ADMIN)
|
||||
.createPermission(SELECT).grantedTo("partnerRel", TENANT)
|
||||
|
||||
.importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class,
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
dependsOnColumn("detailsUuid"))
|
||||
.createPermission("partnerDetails", DELETE).grantedTo("partnerRel", OWNER)
|
||||
.createPermission("partnerDetails", UPDATE).grantedTo("partnerRel", AGENT)
|
||||
.createPermission("partnerDetails", SELECT).grantedTo("partnerRel", AGENT); // not TENANT!
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/504-partner/5043-hs-office-partner-rbac");
|
||||
}
|
||||
}
|
@ -9,10 +9,10 @@ import jakarta.persistence.EntityManager;
|
||||
|
||||
class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatchResource> {
|
||||
private final EntityManager em;
|
||||
private final HsOfficePartnerRbacEntity entity;
|
||||
private final HsOfficePartnerEntity entity;
|
||||
HsOfficePartnerEntityPatcher(
|
||||
final EntityManager em,
|
||||
final HsOfficePartnerRbacEntity entity) {
|
||||
final HsOfficePartnerEntity entity) {
|
||||
this.em = em;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
@ -1,59 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
|
||||
import static jakarta.persistence.CascadeType.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "partner_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@NoArgsConstructor
|
||||
@DisplayAs("RbacPartner")
|
||||
public class HsOfficePartnerRbacEntity extends HsOfficePartner<HsOfficePartnerRbacEntity> {
|
||||
|
||||
public static RbacSpec rbac() {
|
||||
return rbacViewFor("partner", HsOfficePartnerRbacEntity.class)
|
||||
.withIdentityView(SQL.projection("'P-' || partnerNumber"))
|
||||
.withUpdatableColumns("partnerRelUuid")
|
||||
.toRole(GLOBAL, ADMIN).grantPermission(INSERT)
|
||||
|
||||
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationRbacEntity.class,
|
||||
usingDefaultCase(),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
dependsOnColumn("partnerRelUuid"))
|
||||
.createPermission(DELETE).grantedTo("partnerRel", OWNER)
|
||||
.createPermission(UPDATE).grantedTo("partnerRel", ADMIN)
|
||||
.createPermission(SELECT).grantedTo("partnerRel", TENANT)
|
||||
|
||||
.importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class,
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
dependsOnColumn("detailsUuid"))
|
||||
.createPermission("partnerDetails", DELETE).grantedTo("partnerRel", OWNER)
|
||||
.createPermission("partnerDetails", UPDATE).grantedTo("partnerRel", AGENT)
|
||||
.createPermission("partnerDetails", SELECT).grantedTo("partnerRel", AGENT); // not TENANT!
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/504-partner/5043-hs-office-partner-rbac");
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsOfficePartnerRbacRepository extends Repository<HsOfficePartnerRbacEntity, UUID> {
|
||||
|
||||
@Timed("app.office.partners.repo.findByUuid.rbac")
|
||||
Optional<HsOfficePartnerRbacEntity> findByUuid(UUID id);
|
||||
|
||||
@Timed("app.office.partners.repo.findAll.rbac")
|
||||
List<HsOfficePartnerRbacEntity> findAll(); // TODO.refa: move to a repo in test sources
|
||||
|
||||
@Query(value = """
|
||||
select partner.uuid, partner.detailsuuid, partner.partnernumber, partner.partnerreluuid, partner.version
|
||||
from hs_office.partner_rv partner
|
||||
join hs_office.relation partnerRel on partnerRel.uuid = partner.partnerreluuid
|
||||
join hs_office.contact contact on contact.uuid = partnerRel.contactuuid
|
||||
join hs_office.person partnerPerson on partnerPerson.uuid = partnerRel.holderuuid
|
||||
left join hs_office.partner_details_rv partnerDetails on partnerDetails.uuid = partner.detailsuuid
|
||||
where :name is null
|
||||
or (partnerDetails.uuid is not null and partnerDetails.birthname like (cast(:name as text) || '%') escape '')
|
||||
or contact.caption like (cast(:name as text) || '%') escape ''
|
||||
or partnerPerson.tradename like (cast(:name as text) || '%') escape ''
|
||||
or partnerPerson.givenname like (cast(:name as text) || '%') escape ''
|
||||
or partnerPerson.familyname like (cast(:name as text) || '%') escape ''
|
||||
""", nativeQuery = true)
|
||||
@Timed("app.office.partners.repo.findPartnerByOptionalNameLike.rbac")
|
||||
List<HsOfficePartnerRbacEntity> findPartnerByOptionalNameLike(String name);
|
||||
|
||||
@Timed("app.office.partners.repo.findPartnerByPartnerNumber.rbac")
|
||||
Optional<HsOfficePartnerRbacEntity> findPartnerByPartnerNumber(Integer partnerNumber);
|
||||
|
||||
@Timed("app.office.partners.repo.save.rbac")
|
||||
HsOfficePartnerRbacEntity save(final HsOfficePartnerRbacEntity entity);
|
||||
|
||||
@Timed("app.office.partners.repo.count.rbac")
|
||||
long count();
|
||||
|
||||
@Timed("app.office.partners.repo.deleteByUuid.rbac")
|
||||
int deleteByUuid(UUID uuid);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "partner")
|
||||
@Getter
|
||||
@Setter
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@NoArgsConstructor
|
||||
@DisplayAs("RealPartner")
|
||||
public class HsOfficePartnerRealEntity extends HsOfficePartner<HsOfficePartnerRealEntity> {
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsOfficePartnerRealRepository extends Repository<HsOfficePartnerRealEntity, UUID> {
|
||||
|
||||
@Timed("app.office.partners.repo.findByUuid.real")
|
||||
Optional<HsOfficePartnerRealEntity> findByUuid(UUID id);
|
||||
|
||||
@Timed("app.office.partners.repo.findAll.real")
|
||||
List<HsOfficePartnerRbacEntity> findAll(); // TODO.refa: move to a repo in test sources
|
||||
|
||||
@Query(value = """
|
||||
select partner.uuid, partner.detailsuuid, partner.partnernumber, partner.partnerreluuid, partner.version
|
||||
from hs_office.partner partner
|
||||
join hs_office.relation partnerRel on partnerRel.uuid = partner.partnerreluuid
|
||||
join hs_office.contact contact on contact.uuid = partnerRel.contactuuid
|
||||
join hs_office.person partnerPerson on partnerPerson.uuid = partnerRel.holderuuid
|
||||
left join hs_office.partner_details_rv partnerDetails on partnerDetails.uuid = partner.detailsuuid
|
||||
where :name is null
|
||||
or (partnerDetails.uuid is not null and partnerDetails.birthname like (cast(:name as text) || '%') escape '')
|
||||
or contact.caption like (cast(:name as text) || '%') escape ''
|
||||
or partnerPerson.tradename like (cast(:name as text) || '%') escape ''
|
||||
or partnerPerson.givenname like (cast(:name as text) || '%') escape ''
|
||||
or partnerPerson.familyname like (cast(:name as text) || '%') escape ''
|
||||
""", nativeQuery = true)
|
||||
@Timed("app.office.partners.repo.findPartnerByOptionalNameLike.real")
|
||||
List<HsOfficePartnerRealEntity> findPartnerByOptionalNameLike(String name);
|
||||
|
||||
@Timed("app.office.partners.repo.findPartnerByPartnerNumber.real")
|
||||
Optional<HsOfficePartnerRealEntity> findPartnerByPartnerNumber(Integer partnerNumber);
|
||||
|
||||
@Timed("app.office.partners.repo.save.real")
|
||||
HsOfficePartnerRealEntity save(final HsOfficePartnerRealEntity entity);
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEntity, UUID> {
|
||||
|
||||
Optional<HsOfficePartnerEntity> findByUuid(UUID id);
|
||||
|
||||
List<HsOfficePartnerEntity> findAll(); // TODO.impl: move to a repo in test sources
|
||||
|
||||
@Query("""
|
||||
SELECT partner FROM HsOfficePartnerEntity partner
|
||||
JOIN HsOfficeRelationRealEntity rel ON rel.uuid = partner.partnerRel.uuid
|
||||
JOIN HsOfficeContactRealEntity contact ON contact.uuid = rel.contact.uuid
|
||||
JOIN HsOfficePersonEntity person ON person.uuid = rel.holder.uuid
|
||||
WHERE :name is null
|
||||
OR partner.details.birthName like concat(cast(:name as text), '%')
|
||||
OR contact.caption like concat(cast(:name as text), '%')
|
||||
OR person.tradeName like concat(cast(:name as text), '%')
|
||||
OR person.givenName like concat(cast(:name as text), '%')
|
||||
OR person.familyName like concat(cast(:name as text), '%')
|
||||
""")
|
||||
List<HsOfficePartnerEntity> findPartnerByOptionalNameLike(String name);
|
||||
HsOfficePartnerEntity findPartnerByPartnerNumber(Integer partnerNumber);
|
||||
|
||||
HsOfficePartnerEntity save(final HsOfficePartnerEntity entity);
|
||||
|
||||
long count();
|
||||
|
||||
int deleteByUuid(UUID uuid);
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.Version;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@FieldNameConstants
|
||||
@DisplayAs("Person")
|
||||
public class HsOfficePerson<T extends HsOfficePerson<?> & BaseEntity<?>> implements BaseEntity<T>, Stringifyable, WithRoleId {
|
||||
|
||||
private static Stringify<HsOfficePerson> toString = stringify(HsOfficePerson.class, "person")
|
||||
.withProp(Fields.personType, HsOfficePerson::getPersonType)
|
||||
.withProp(Fields.tradeName, HsOfficePerson::getTradeName)
|
||||
.withProp(Fields.salutation, HsOfficePerson::getSalutation)
|
||||
.withProp(Fields.title, HsOfficePerson::getTitle)
|
||||
.withProp(Fields.familyName, HsOfficePerson::getFamilyName)
|
||||
.withProp(Fields.givenName, HsOfficePerson::getGivenName);
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private UUID uuid;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(name = "persontype")
|
||||
private HsOfficePersonType personType;
|
||||
|
||||
@Column(name = "tradename")
|
||||
private String tradeName;
|
||||
|
||||
@Column(name = "salutation")
|
||||
private String salutation;
|
||||
|
||||
@Column(name = "title")
|
||||
private String title;
|
||||
|
||||
@Column(name = "familyname")
|
||||
private String familyName;
|
||||
|
||||
@Column(name = "givenname")
|
||||
private String givenName;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString.apply(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return personType + " " +
|
||||
(!StringUtils.isEmpty(tradeName) ? tradeName : (familyName + ", " + givenName));
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
|
||||
@ -24,21 +23,20 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePersonRbacRepository personRepo;
|
||||
private HsOfficePersonRepository personRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.persons.api.getListOfPersons")
|
||||
public ResponseEntity<List<HsOfficePersonResource>> getListOfPersons(
|
||||
public ResponseEntity<List<HsOfficePersonResource>> listPersons(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final String name) {
|
||||
final String caption) {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entities = personRepo.findPersonByOptionalNameLike(name);
|
||||
final var entities = personRepo.findPersonByOptionalNameLike(caption);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsOfficePersonResource.class);
|
||||
return ResponseEntity.ok(resources);
|
||||
@ -46,15 +44,14 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.persons.api.postNewPerson")
|
||||
public ResponseEntity<HsOfficePersonResource> postNewPerson(
|
||||
public ResponseEntity<HsOfficePersonResource> addPerson(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficePersonInsertResource body) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entityToSave = mapper.map(body, HsOfficePersonRbacEntity.class);
|
||||
final var entityToSave = mapper.map(body, HsOfficePersonEntity.class);
|
||||
|
||||
final var saved = personRepo.save(entityToSave);
|
||||
|
||||
@ -69,8 +66,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.persons.api.getSinglePersonByUuid")
|
||||
public ResponseEntity<HsOfficePersonResource> getSinglePersonByUuid(
|
||||
public ResponseEntity<HsOfficePersonResource> getPersonByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID personUuid) {
|
||||
@ -86,7 +82,6 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.persons.api.deletePersonByUuid")
|
||||
public ResponseEntity<Void> deletePersonByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -103,7 +98,6 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.persons.api.patchPerson")
|
||||
public ResponseEntity<HsOfficePersonResource> patchPerson(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
|
@ -0,0 +1,102 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "person_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@DisplayAs("Person")
|
||||
public class HsOfficePersonEntity implements BaseEntity<HsOfficePersonEntity>, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person")
|
||||
.withProp(Fields.personType, HsOfficePersonEntity::getPersonType)
|
||||
.withProp(Fields.tradeName, HsOfficePersonEntity::getTradeName)
|
||||
.withProp(Fields.salutation, HsOfficePersonEntity::getSalutation)
|
||||
.withProp(Fields.title, HsOfficePersonEntity::getTitle)
|
||||
.withProp(Fields.familyName, HsOfficePersonEntity::getFamilyName)
|
||||
.withProp(Fields.givenName, HsOfficePersonEntity::getGivenName);
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private UUID uuid;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(name = "persontype")
|
||||
private HsOfficePersonType personType;
|
||||
|
||||
@Column(name = "tradename")
|
||||
private String tradeName;
|
||||
|
||||
@Column(name = "salutation")
|
||||
private String salutation;
|
||||
|
||||
@Column(name = "title")
|
||||
private String title;
|
||||
|
||||
@Column(name = "familyname")
|
||||
private String familyName;
|
||||
|
||||
@Column(name = "givenname")
|
||||
private String givenName;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString.apply(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return personType + " " +
|
||||
(!StringUtils.isEmpty(tradeName) ? tradeName : (familyName + ", " + givenName));
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("person", HsOfficePersonEntity.class)
|
||||
.withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)"))
|
||||
.withUpdatableColumns("personType", "title", "salutation", "tradeName", "givenName", "familyName")
|
||||
.toRole(GLOBAL, GUEST).grantPermission(INSERT)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.permission(DELETE);
|
||||
with.owningUser(CREATOR);
|
||||
with.incomingSuperRole(GLOBAL, ADMIN);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(REFERRER, (with) -> {
|
||||
with.permission(SELECT);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/502-person/5023-hs-office-person-rbac");
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user