Compare commits

..

7 Commits

Author SHA1 Message Date
Michael Hoennig
1af213a95a handle locally existing branches 2024-09-26 14:20:37 +02:00
Michael Hoennig
0675918362 fix branch recognition 2024-09-26 14:17:41 +02:00
Michael Hoennig
e8c4946111 explicitly call gradlew build 2024-09-26 14:12:03 +02:00
Michael Hoennig
086fb11436 echo checkout command 2024-09-26 14:09:44 +02:00
Michael Hoennig
e7558cdbe8 wait for branch with new commit 2024-09-26 13:59:13 +02:00
Michael Hönnig
c2ea66a87f some comment 2024-09-26 13:45:49 +02:00
Michael Hoennig
4eceb41ebc watch all branches for changes 2024-09-26 12:04:41 +02:00
377 changed files with 1758 additions and 13549 deletions

View File

@ -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"
}
@ -91,9 +83,7 @@ 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-test='. .aliases; ./gradlew test'
alias gw-check='. .aliases; gw test check -x pitest'
alias cas-curl='bin/cas-curl'
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='
@ -105,6 +95,3 @@ if [ ! -f .environment ]; then
cp .tc-environment .environment
fi
source .environment
alias scenario-reports-upload='./gradlew scenarioTests 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'

88
Jenkinsfile vendored
View File

@ -1,88 +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-/Integration/Acceptance-Tests') {
steps {
sh './gradlew check --no-daemon -x pitest -x dependencyCheckAnalyze -x importOfficeData -x importHostingAssets'
}
}
stage('Import-Tests') {
steps {
sh './gradlew importOfficeData importHostingAssets --no-daemon'
}
}
stage ('Scenario-Tests') {
steps {
sh './gradlew scenarioTests --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()
}
}
}

View File

@ -60,48 +60,36 @@ If you have at least Docker and the Java JDK installed in appropriate versions a
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 scenarioTests # compiles and scenario-tests - takes ~1min on a decent machine
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 without CAS-authentication on `localhost:8080`:
export HSADMINNG_CAS_SERVER=
gw bootRun
For using the REST-API with CAS-authentication, see `bin/cas-curl`.
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.
@ -509,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
```
@ -587,7 +565,7 @@ 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.
@ -603,20 +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
```
### 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:
@ -815,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

View File

@ -1,165 +0,0 @@
#!/bin/bash
if [ "$#" -eq 0 ] || [ "$1" == "help" ] || [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
cat <<EOF
curl-wrapper utilizing CAS-authentication for hsadmin-ng
usage: $0 [--trace] <<command>> [parameters]
commands:
EOF
grep '") ''# ' $0
exit
fi
if [ "$1" == "--trace" ]; then
function trace() {
echo "$*" >&2
}
function doCurl() {
set -x
curl --fail-with-body --header "Authorization: $HSADMINNG_CAS_TICKET" "$@"
set +x
}
shift
else
function trace() {
: # noop
}
function doCurl() {
curl --fail-with-body --header "Authorization: $HSADMINNG_CAS_TICKET" "$@"
}
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 casLogout() {
rm -f ~/.cas-login-tgt
}
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
# 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=<<PASSWORD OMITTED>>\" \
$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
fi
echo "$HSADMINNG_CAS_TGT" >~/.cas-login-tgt
trace "$HSADMINNG_CAS_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
"login") # reads username+password and fetches ticket granting ticket (bypasses HSADMINNG_CAS_USERNAME+HSADMINNG_CAS_PASSWORD)
casLogout
export HSADMINNG_CAS_USERNAME=
export HSADMINNG_CAS_PASSWORD=
casLogin
;;
"logout") # logout, deleting ticket granting ticket
casLogout
;;
"validate") # validates ticket granting ticket and prints currently logged in user
casValidate
;;
"get") # HTTP GET, add URL as parameter
shift
casLogin
HSADMINNG_CAS_TICKET=`casTicket`
doCurl "$*"
;;
"post") # HTTP POST, add curl options to specify the request body and the URL as last parameter
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 parameter
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
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

View File

@ -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

View File

@ -1,10 +1,10 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
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.9'
id "org.owasp.dependencycheck" version "10.0.4"
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'
@ -58,22 +58,19 @@ 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.9.2'
implementation 'org.springdoc:springdoc-openapi:2.6.0'
implementation 'org.postgresql:postgresql:42.7.4'
implementation 'org.liquibase:liquibase-core:4.29.2'
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.8.3'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0'
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.12.0'
implementation 'net.java.dev.jna:jna:5.15.0'
implementation 'org.modelmapper:modelmapper:3.2.1'
implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
implementation 'org.webjars:swagger-ui:5.17.14'
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'
@ -88,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:3.0'
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 {
@ -121,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
@ -258,7 +254,7 @@ test {
'net.hostsharing.hsadminng.**.generated.**',
]
useJUnitPlatform {
excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest'
excludeTags 'import'
}
}
jacocoTestReport {
@ -280,7 +276,7 @@ jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.80 // TODO.test: improve instruction coverage
minimum = 0.92
}
}
@ -292,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 {
@ -319,7 +310,7 @@ jacocoTestCoverageVerification {
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.00 // TODO.test: improve branch coverage
minimum = 1.00
}
}
}
@ -347,30 +338,19 @@ tasks.register('importHostingAssets', Test) {
mustRunAfter spotlessJava
}
tasks.register('scenarioTests', 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*']
pitestVersion = '1.17.0'
pitestVersion = '1.15.3'
junit5PluginVersion = '1.1.0'
threads = 4
@ -405,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 scenarioTests
// shortcut for compiling all files
tasks.register('compile') {
dependsOn 'compileJava', 'compileTestJava'
}

View File

@ -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.

View File

@ -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.

View File

@ -64,7 +64,7 @@ classDiagram
}
class partner-MeierGmbH {
+Numeric partnerNumber: P-12345
+Numeric partnerNumber: 12345
+Relation partnerRel
}
partner-MeierGmbH *-- rel-MeierGmbH

View File

@ -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>

View File

@ -1 +0,0 @@
find the generated ScenarioReports in build/doc/scenarios

View File

@ -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.

View File

@ -1,10 +1,8 @@
{
"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": "The Apache Software License, Version 2.0" },
@ -13,8 +11,6 @@
{ "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" },
@ -33,27 +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"
},
{
"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"
}
{ "moduleName": "org.springdoc:springdoc-openapi" }
]
}

View File

@ -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/*

View File

@ -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,10 +14,4 @@
<packageUrl regex="true">^pkg:maven/org\.pitest/pitest\-command\-line@.*$</packageUrl>
<cpe>cpe:/a:line:line</cpe>
</suppress>
<suppress>
<notes><![CDATA[
Malicious HTTP redirect in JAXB on a REST-endpoint is not that dangerous.
]]></notes>
<cve>CVE-2024-9329</cve>
</suppress>
</suppressions>

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -1,8 +0,0 @@
package net.hostsharing.hsadminng.config;
import jakarta.servlet.http.HttpServletRequest;
public interface Authenticator {
String authenticate(final HttpServletRequest httpRequest);
}

View File

@ -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 {
System.err.println("CasAuthenticator.casValidation using CAS-server: " + casServerUrl);
final var ticket = httpRequest.getHeader("Authorization");
final var url = casServerUrl + "/p3/serviceValidate" +
"?service=" + serviceUrl +
"&ticket=" + ticket;
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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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 + ")");
}
}
}

View File

@ -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"),

View File

@ -5,8 +5,8 @@ import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.hostsharing.hsadminng.errors.DisplayAs;
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;
@ -14,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

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.booking.debitor;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import java.util.List;
@ -9,9 +8,7 @@ import java.util.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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -1,15 +0,0 @@
package net.hostsharing.hsadminng.hs.booking.item;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import java.util.UUID;
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);
}

View File

@ -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

View File

@ -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,7 +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.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
@ -26,7 +22,6 @@ 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
@ -38,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) {
@ -67,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) {
@ -76,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()
@ -85,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())
@ -97,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) {
@ -115,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,
@ -130,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,
@ -148,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()) {
@ -169,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()));
};

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.booking.item;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import java.util.List;
@ -10,21 +9,15 @@ import java.util.UUID;
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();
}

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.booking.item;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import java.util.List;
@ -10,21 +9,15 @@ import java.util.UUID;
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();
}

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.booking.item;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

View File

@ -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");

View File

@ -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();

View File

@ -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");
}
}

View File

@ -1,6 +1,5 @@
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;
@ -36,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) {
@ -51,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) {
@ -74,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,
@ -91,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,
@ -106,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,

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import java.util.List;
@ -10,21 +9,14 @@ import java.util.UUID;
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();
}

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import java.util.List;
@ -10,21 +9,14 @@ import java.util.UUID;
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();
}

View File

@ -1,28 +1,19 @@
package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
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();
}

View File

@ -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;
@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();

View File

@ -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;
@ -49,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,
@ -67,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) {
@ -96,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) {
@ -113,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) {
@ -128,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,

View File

@ -1,6 +1,5 @@
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;
@ -15,8 +14,7 @@ import java.util.Map;
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();
@ -24,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);

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
@ -11,10 +10,8 @@ import java.util.UUID;
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 = """
@ -35,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();
}

View File

@ -1,35 +1,18 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed;
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;
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,
@ -50,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();
}

View File

@ -1,32 +1,24 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
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();
}

View File

@ -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.StandardMapper;
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 StandardMapper standardMapper) {
super(emw, newBookingItemRealEntity, asset, standardMapper);
}
@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;
}
}

View File

@ -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.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
@RequiredArgsConstructor
abstract class HostingAssetFactory {
final EntityManagerWrapper emw;
final HsBookingItemRealEntity fromBookingItem;
final HsHostingAssetAutoInsertResource asset;
final StandardMapper standardMapper;
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();
}
}

View File

@ -1,76 +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.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
@Autowired
private EntityManagerWrapper emw;
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private StandardMapper standardMapper;
@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, standardMapper);
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
};
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 StandardMapper standardMapper
) {
return new HostingAssetFactory(emw, fromBookingItem, asset, standardMapper) {
@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());
}
};
}
}

View File

@ -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.StandardMapper;
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 StandardMapper standardMapper) {
super(emw, newBookingItemRealEntity, asset, standardMapper);
}
@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 = standardMapper.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);
}
}

View File

@ -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");

View File

@ -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(

View File

@ -1,6 +1,5 @@
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;
@ -32,8 +31,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@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,

View File

@ -3,10 +3,10 @@ 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.object.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
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.*;
import java.io.IOException;
@ -16,7 +16,7 @@ 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.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "bankaccount_rv")

View File

@ -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();
}

View File

@ -11,9 +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.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;
@ -27,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
@ -54,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)
@ -81,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,

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.office.contact;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
@ -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
@ -35,18 +33,13 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@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,

View File

@ -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())

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}
}
};

View File

@ -8,10 +8,10 @@ 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.object.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
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 org.hibernate.annotations.GenericGenerator;
import jakarta.persistence.*;
@ -31,7 +31,7 @@ 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.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "coopassettx_rv")
@ -50,10 +50,8 @@ 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
@ -79,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")
@ -97,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() {

View File

@ -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();
}

View File

@ -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

View File

@ -1,8 +1,6 @@
package net.hostsharing.hsadminng.hs.office.coopshares;
import jakarta.persistence.EntityNotFoundException;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource;
@ -40,9 +38,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@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,
@ -61,8 +57,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@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) {
@ -85,8 +80,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override
@Transactional(readOnly = true)
@Timed("app.office.coopShares.repo.getSingleCoopShareTransactionByUuid")
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getSingleCoopShareTransactionByUuid(
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getCoopShareTransactionByUuid(
final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
context.define(currentSubject, assumedRoles);
@ -137,9 +131,9 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
}
final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if ( resource.getRevertedShareTxUuid() != null ) {
entity.setRevertedShareTx(coopSharesTransactionRepo.findByUuid(resource.getRevertedShareTxUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] revertedShareTxUuid %s not found".formatted(resource.getRevertedShareTxUuid()))));
if ( resource.getAdjustedShareTxUuid() != null ) {
entity.setAdjustedShareTx(coopSharesTransactionRepo.findByUuid(resource.getAdjustedShareTxUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] adjustedShareTxUuid %s not found".formatted(resource.getAdjustedShareTxUuid()))));
}
};
}

View File

@ -8,10 +8,10 @@ import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
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.*;
import java.io.IOException;
@ -29,7 +29,7 @@ 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.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "coopsharetx_rv")
@ -48,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
@ -71,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")
@ -93,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() {

View File

@ -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();
}

View File

@ -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

View File

@ -1,6 +1,5 @@
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.generated.api.v1.api.HsOfficeDebitorsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorInsertResource;
@ -23,10 +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.repr.TaggedNumber.cropTag;
@RestController
@ -52,29 +49,24 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@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) {
@ -85,13 +77,16 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
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().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);
if (body.getDebitorRel() != null) {
if ( body.getDebitorRel() != null ) {
body.getDebitorRel().setType(DEBITOR.name());
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
debitorRel.setType(DEBITOR);
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
@ -100,10 +95,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
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());
});
() -> { throw new ValidationException("Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());});
}
final var savedEntity = debitorRepo.save(entityToSave);
@ -115,14 +107,13 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
.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) {
@ -133,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,
@ -172,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,
@ -187,11 +159,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
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<HsOfficeDebitorEntity, HsOfficeDebitorResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setDebitorNumber(entity.getTaggedDebitorNumber());
};
}

View File

@ -11,11 +11,11 @@ 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.object.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
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 org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.JoinFormula;
import org.hibernate.annotations.NotFound;
@ -51,7 +51,7 @@ 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.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "debitor_rv")
@ -142,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(HsOfficePartnerEntity::getPartnerNumber)
.map(partnerNumber -> DEBITOR_NUMBER_TAG + partnerNumber + debitorNumberSuffix)
.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);
@ -157,7 +162,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
@Override
public String toShortString() {
return getTaggedDebitorNumber();
return DEBITOR_NUMBER_TAG + getDebitorNumberString();
}
public static RbacView rbac() {

View File

@ -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,33 +9,20 @@ 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 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("""
@ -45,7 +30,7 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
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);
}

View File

@ -1,12 +1,10 @@
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.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@ -18,10 +16,8 @@ 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
@ -35,33 +31,25 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@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(HsOfficePartnerEntity.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) {
@ -77,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) {
@ -97,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,
@ -140,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,
@ -159,7 +123,7 @@ 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));

View File

@ -9,12 +9,12 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
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.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;
import jakarta.persistence.Column;
@ -53,7 +53,7 @@ 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.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "membership_rv")
@ -130,7 +130,6 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
}
return validity;
}
public Integer getMemberNumber() {
if (partner == null || partner.getPartnerNumber() == null || memberNumberSuffix == null ) {
return null;
@ -139,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);

View File

@ -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);
}

View File

@ -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.StandardMapper;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -28,7 +27,6 @@ import java.util.List;
import java.util.UUID;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.EX_PARTNER;
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController
@ -51,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) {
@ -66,8 +63,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@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) {
@ -89,8 +85,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@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) {
@ -104,26 +99,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
return ResponseEntity.ok(mapper.map(result.get(), HsOfficePartnerResource.class));
}
@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();
}
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,
@ -144,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,
@ -167,14 +143,13 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
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 HsOfficePartnerEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
final var entityToSave = new HsOfficePartnerEntity();
entityToSave.setPartnerNumber(cropTag(HsOfficePartnerEntity.PARTNER_NUMBER_TAG, body.getPartnerNumber()));
entityToSave.setPartnerNumber(body.getPartnerNumber());
entityToSave.setPartnerRel(persistPartnerRel(body.getPartnerRel()));
entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class));
return entityToSave;
@ -183,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;

View File

@ -2,11 +2,11 @@ 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.object.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
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.*;
import java.io.IOException;
@ -17,7 +17,7 @@ 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.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "partner_details_rv")

View File

@ -7,15 +7,15 @@ 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.HsOfficePerson;
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.persistence.BaseEntity;
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.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
@ -33,7 +33,7 @@ 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.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "partner_rv")
@ -51,7 +51,7 @@ public class HsOfficePartnerEntity implements Stringifyable, BaseEntity<HsOffice
.withIdProp(HsOfficePartnerEntity::toShortString)
.withProp(p -> ofNullable(p.getPartnerRel())
.map(HsOfficeRelation::getHolder)
.map(HsOfficePerson::toShortString)
.map(HsOfficePersonEntity::toShortString)
.orElse(null))
.withProp(p -> ofNullable(p.getPartnerRel())
.map(HsOfficeRelation::getContact)

View File

@ -1,6 +1,5 @@
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;
@ -10,17 +9,15 @@ import java.util.UUID;
public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEntity, UUID> {
@Timed("app.office.partners.repo.findByUuid")
Optional<HsOfficePartnerEntity> findByUuid(UUID id);
@Timed("app.office.partners.repo.findAll")
List<HsOfficePartnerEntity> findAll(); // TODO.refa: move to a repo in test sources
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 HsOfficePersonRealEntity person ON person.uuid = rel.holder.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), '%')
@ -28,18 +25,12 @@ public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEnt
OR person.givenName like concat(cast(:name as text), '%')
OR person.familyName like concat(cast(:name as text), '%')
""")
@Timed("app.office.partners.repo.findPartnerByOptionalNameLike")
List<HsOfficePartnerEntity> findPartnerByOptionalNameLike(String name);
HsOfficePartnerEntity findPartnerByPartnerNumber(Integer partnerNumber);
@Timed("app.office.partners.repo.findPartnerByPartnerNumber")
Optional<HsOfficePartnerEntity> findPartnerByPartnerNumber(Integer partnerNumber);
@Timed("app.office.partners.repo.save")
HsOfficePartnerEntity save(final HsOfficePartnerEntity entity);
@Timed("app.office.partners.repo.count")
long count();
@Timed("app.office.partners.repo.deleteByUuid")
int deleteByUuid(UUID uuid);
}

View File

@ -1,79 +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.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 {
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));
}
}

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.office.person;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
@ -27,18 +26,17 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
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,

View File

@ -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");
}
}

View File

@ -9,9 +9,9 @@ import java.util.Optional;
class HsOfficePersonEntityPatcher implements EntityPatcher<HsOfficePersonPatchResource> {
private final HsOfficePersonRbacEntity entity;
private final HsOfficePersonEntity entity;
HsOfficePersonEntityPatcher(final HsOfficePersonRbacEntity entity) {
HsOfficePersonEntityPatcher(final HsOfficePersonEntity entity) {
this.entity = entity;
}

View File

@ -1,52 +0,0 @@
package net.hostsharing.hsadminng.hs.office.person;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
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.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 = "person_rv")
@Getter
@Setter
@NoArgsConstructor
@SuperBuilder(toBuilder = true)
@FieldNameConstants
@DisplayAs("RbacPerson")
public class HsOfficePersonRbacEntity extends HsOfficePerson<HsOfficePersonRbacEntity> {
public static RbacView rbac() {
return rbacViewFor("person", HsOfficePersonRbacEntity.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");
}
}

View File

@ -1,34 +0,0 @@
package net.hostsharing.hsadminng.hs.office.person;
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 HsOfficePersonRbacRepository extends Repository<HsOfficePersonRbacEntity, UUID> {
@Timed("app.office.persons.repo.findByUuid.rbac")
Optional<HsOfficePersonRbacEntity> findByUuid(UUID personUuid);
@Query("""
SELECT p FROM HsOfficePersonRbacEntity p
WHERE :name is null
OR p.tradeName like concat(cast(:name as text), '%')
OR p.givenName like concat(cast(:name as text), '%')
OR p.familyName like concat(cast(:name as text), '%')
""")
@Timed("app.office.persons.repo.findPersonByOptionalNameLike.rbac")
List<HsOfficePersonRbacEntity> findPersonByOptionalNameLike(String name);
@Timed("app.office.persons.repo.save.rbac")
HsOfficePersonRbacEntity save(final HsOfficePersonRbacEntity entity);
@Timed("app.office.persons.repo.deleteByUuid.rbac")
int deleteByUuid(final UUID personUuid);
@Timed("app.office.persons.repo.count.rbac")
long count();
}

View File

@ -1,23 +0,0 @@
package net.hostsharing.hsadminng.hs.office.person;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(schema = "hs_office", name = "person")
@Getter
@Setter
@NoArgsConstructor
@SuperBuilder(toBuilder = true)
@FieldNameConstants
@DisplayAs("RealPerson")
public class HsOfficePersonRealEntity extends HsOfficePerson<HsOfficePersonRealEntity> {
}

View File

@ -1,31 +0,0 @@
package net.hostsharing.hsadminng.hs.office.person;
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 HsOfficePersonRealRepository extends Repository<HsOfficePersonRealEntity, UUID> {
@Timed("app.office.persons.repo.findByUuid.real")
Optional<HsOfficePersonRealEntity> findByUuid(UUID personUuid);
@Query("""
SELECT p FROM HsOfficePersonRealEntity p
WHERE :name is null
OR p.tradeName like concat(cast(:name as text), '%')
OR p.givenName like concat(cast(:name as text), '%')
OR p.familyName like concat(cast(:name as text), '%')
""")
@Timed("app.office.persons.repo.findPersonByOptionalNameLike.real")
List<HsOfficePersonRealEntity> findPersonByOptionalNameLike(String name);
@Timed("app.office.persons.repo.save.real")
HsOfficePersonRealEntity save(final HsOfficePersonRealEntity entity);
@Timed("app.office.persons.repo.count.real")
long count();
}

View File

@ -0,0 +1,28 @@
package net.hostsharing.hsadminng.hs.office.person;
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 HsOfficePersonRepository extends Repository<HsOfficePersonEntity, UUID> {
Optional<HsOfficePersonEntity> findByUuid(UUID personUuid);
@Query("""
SELECT p FROM HsOfficePersonEntity p
WHERE :name is null
OR p.tradeName like concat(cast(:name as text), '%')
OR p.givenName like concat(cast(:name as text), '%')
OR p.familyName like concat(cast(:name as text), '%')
""")
List<HsOfficePersonEntity> findPersonByOptionalNameLike(String name);
HsOfficePersonEntity save(final HsOfficePersonEntity entity);
int deleteByUuid(final UUID personUuid);
long count();
}

View File

@ -4,7 +4,6 @@ import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.stream.Stream;
// HOWTO: convert data types for exchange between PostgreSQL and Java/Hibernate/JPA-Entities
@Converter(autoApply = true)
public class HsOfficePersonTypeConverter implements AttributeConverter<HsOfficePersonType, String> {

View File

@ -4,16 +4,16 @@ import lombok.*;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import jakarta.persistence.*;
import jakarta.persistence.Column;
import java.util.UUID;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@MappedSuperclass
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ -45,11 +45,11 @@ public class HsOfficeRelation implements BaseEntity<HsOfficeRelation>, Stringify
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "anchoruuid")
private HsOfficePersonRealEntity anchor;
private HsOfficePersonEntity anchor;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "holderuuid")
private HsOfficePersonRealEntity holder;
private HsOfficePersonEntity holder;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "contactuuid")

View File

@ -1,14 +1,10 @@
package net.hostsharing.hsadminng.hs.office.relation;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.Validate;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelationsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@ -23,9 +19,9 @@ import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
@RestController
public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Autowired
@ -35,35 +31,28 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
private StandardMapper mapper;
@Autowired
private HsOfficeRelationRbacRepository rbacRelationRepo;
private HsOfficeRelationRbacRepository relationRbacRepo;
@Autowired
private HsOfficePersonRealRepository realPersonRepo;
private HsOfficePersonRepository holderRepo;
@Autowired
private HsOfficeContactRealRepository realContactRepo;
private HsOfficeContactRealRepository contactrealRepo;
@PersistenceContext
private EntityManager em;
@Override
@Transactional(readOnly = true)
@Timed("app.office.relations.api.getListOfRelations")
public ResponseEntity<List<HsOfficeRelationResource>> getListOfRelations(
public ResponseEntity<List<HsOfficeRelationResource>> listRelations(
final String currentSubject,
final String assumedRoles,
final UUID personUuid,
final HsOfficeRelationTypeResource relationType,
final String mark,
final String personData,
final String contactData) {
final HsOfficeRelationTypeResource relationType) {
context.define(currentSubject, assumedRoles);
final List<HsOfficeRelationRbacEntity> entities =
rbacRelationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(
personUuid,
relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name()),
mark, personData, contactData);
final var entities = relationRbacRepo.findRelationRelatedToPersonUuidAndRelationType(personUuid,
mapper.map(relationType, HsOfficeRelationType.class));
final var resources = mapper.mapList(entities, HsOfficeRelationResource.class,
RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
@ -72,8 +61,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Override
@Transactional
@Timed("app.office.relations.api.postNewRelation")
public ResponseEntity<HsOfficeRelationResource> postNewRelation(
public ResponseEntity<HsOfficeRelationResource> addRelation(
final String currentSubject,
final String assumedRoles,
final HsOfficeRelationInsertResource body) {
@ -83,34 +71,17 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
final var entityToSave = new HsOfficeRelationRbacEntity();
entityToSave.setType(HsOfficeRelationType.valueOf(body.getType()));
entityToSave.setMark(body.getMark());
entityToSave.setAnchor(realPersonRepo.findByUuid(body.getAnchorUuid()).orElseThrow(
entityToSave.setAnchor(holderRepo.findByUuid(body.getAnchorUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Person by anchorUuid: " + body.getAnchorUuid())
));
entityToSave.setHolder(holderRepo.findByUuid(body.getHolderUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Person by holderUuid: " + body.getHolderUuid())
));
entityToSave.setContact(contactrealRepo.findByUuid(body.getContactUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Contact by contactUuid: " + body.getContactUuid())
));
Validate.validate("anchor, anchor.uuid").exactlyOne(body.getHolder(), body.getHolderUuid());
if ( body.getHolderUuid() != null) {
entityToSave.setHolder(realPersonRepo.findByUuid(body.getHolderUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Person by holderUuid: " + body.getHolderUuid())
));
} else {
entityToSave.setHolder(realPersonRepo.save(
mapper.map(body.getHolder(), HsOfficePersonRealEntity.class)
) );
}
Validate.validate("contact, contact.uuid").exactlyOne(body.getContact(), body.getContactUuid());
if ( body.getContactUuid() != null) {
entityToSave.setContact(realContactRepo.findByUuid(body.getContactUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Contact by contactUuid: " + body.getContactUuid())
));
} else {
entityToSave.setContact(realContactRepo.save(
mapper.map(body.getContact(), HsOfficeContactRealEntity.class, CONTACT_RESOURCE_TO_ENTITY_POSTMAPPER)
) );
}
final var saved = rbacRelationRepo.save(entityToSave);
final var saved = relationRbacRepo.save(entityToSave);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@ -124,15 +95,14 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Override
@Transactional(readOnly = true)
@Timed("app.office.relations.api.getSingleRelationByUuid")
public ResponseEntity<HsOfficeRelationResource> getSingleRelationByUuid(
public ResponseEntity<HsOfficeRelationResource> getRelationByUuid(
final String currentSubject,
final String assumedRoles,
final UUID relationUuid) {
context.define(currentSubject, assumedRoles);
final var result = rbacRelationRepo.findByUuid(relationUuid);
final var result = relationRbacRepo.findByUuid(relationUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
@ -141,14 +111,13 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Override
@Transactional
@Timed("apprelations.api..deleteRelationByUuid")
public ResponseEntity<Void> deleteRelationByUuid(
final String currentSubject,
final String assumedRoles,
final UUID relationUuid) {
context.define(currentSubject, assumedRoles);
final var result = rbacRelationRepo.deleteByUuid(relationUuid);
final var result = relationRbacRepo.deleteByUuid(relationUuid);
if (result == 0) {
return ResponseEntity.notFound().build();
}
@ -158,7 +127,6 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Override
@Transactional
@Timed("app.office.relations.api.patchRelation")
public ResponseEntity<HsOfficeRelationResource> patchRelation(
final String currentSubject,
final String assumedRoles,
@ -167,25 +135,19 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
context.define(currentSubject, assumedRoles);
final var current = rbacRelationRepo.findByUuid(relationUuid).orElseThrow();
final var current = relationRbacRepo.findByUuid(relationUuid).orElseThrow();
new HsOfficeRelationEntityPatcher(em, current).apply(body);
final var saved = rbacRelationRepo.save(current);
final var saved = relationRbacRepo.save(current);
final var mapped = mapper.map(saved, HsOfficeRelationResource.class);
return ResponseEntity.ok(mapped);
}
final BiConsumer<HsOfficeRelationRbacEntity, HsOfficeRelationResource> RELATION_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setAnchor(mapper.map(entity.getAnchor(), HsOfficePersonResource.class));
resource.setHolder(mapper.map(entity.getHolder(), HsOfficePersonResource.class));
resource.setContact(mapper.map(entity.getContact(), HsOfficeContactResource.class));
};
@SuppressWarnings("unchecked")
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRealEntity> CONTACT_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
};
}

View File

@ -6,7 +6,7 @@ import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
@ -52,11 +52,11 @@ public class HsOfficeRelationRbacEntity extends HsOfficeRelation {
.withRestrictedViewOrderBy(SQL.expression(
"(select idName from hs_office.person_iv p where p.uuid = target.holderUuid)"))
.withUpdatableColumns("contactUuid")
.importEntityAlias("anchorPerson", HsOfficePersonRbacEntity.class, usingDefaultCase(),
.importEntityAlias("anchorPerson", HsOfficePersonEntity.class, usingDefaultCase(),
dependsOnColumn("anchorUuid"),
directlyFetchedByDependsOnColumn(),
NOT_NULL)
.importEntityAlias("holderPerson", HsOfficePersonRbacEntity.class, usingDefaultCase(),
.importEntityAlias("holderPerson", HsOfficePersonEntity.class, usingDefaultCase(),
dependsOnColumn("holderUuid"),
directlyFetchedByDependsOnColumn(),
NOT_NULL)

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.office.relation;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
@ -11,76 +10,28 @@ import java.util.UUID;
public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelationRbacEntity, UUID> {
@Timed("app.office.relations.repo.findByUuid.rbac")
Optional<HsOfficeRelationRbacEntity> findByUuid(UUID id);
default List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) {
return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType.toString());
}
@Query(value = """
SELECT p.* FROM hs_office.relation_rv AS p
WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid
""", nativeQuery = true)
@Timed("app.office.relations.repo.findRelationRelatedToPersonUuid.rbac")
""", nativeQuery = true)
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuid(@NotNull UUID personUuid);
/**
* Finds relations by a conjunction of optional criteria, including anchorPerson, holderPerson and contact data.
* *
* @param personUuid the optional UUID of the anchorPerson or holderPerson
* @param relationType the type of the relation
* @param mark the mark (use '%' for wildcard), case ignored
* @param personData a string to match the persons tradeName, familyName or givenName (use '%' for wildcard), case ignored
* @param contactData a string to match the contacts caption, postalAddress, emailAddresses or phoneNumbers (use '%' for wildcard), case ignored
* @return a list of (accessible) relations which match all given criteria
*/
default List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(
final UUID personUuid,
final HsOfficeRelationType relationType,
final String mark,
final String personData,
final String contactData) {
return findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl(
personUuid, toStringOrNull(relationType),
toSqlLikeOperand(mark), toSqlLikeOperand(personData), toSqlLikeOperand(contactData));
}
// TODO: use ELIKE instead of lower(...) LIKE ...? Or use jsonb_path with RegEx like emailAddressRegEx in ContactRepo?
@Query(value = """
SELECT rel FROM HsOfficeRelationRbacEntity AS rel
WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType)
AND ( :personUuid IS NULL
OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid )
AND ( :mark IS NULL OR lower(rel.mark) LIKE :mark )
AND ( :personData IS NULL
OR lower(rel.anchor.tradeName) LIKE :personData OR lower(rel.holder.tradeName) LIKE :personData
OR lower(rel.anchor.familyName) LIKE :personData OR lower(rel.holder.familyName) LIKE :personData
OR lower(rel.anchor.givenName) LIKE :personData OR lower(rel.holder.givenName) LIKE :personData )
AND ( :contactData IS NULL
OR lower(rel.contact.caption) LIKE :contactData
OR lower(CAST(rel.contact.postalAddress AS String)) LIKE :contactData
OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData
OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData )
""")
@Timed("app.office.relations.repo.findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl.rbac")
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl(
final UUID personUuid,
final String relationType,
final String mark,
final String personData,
final String contactData);
SELECT p.* FROM hs_office.relation_rv AS p
WHERE (:relationType IS NULL OR p.type = cast(:relationType AS hs_office.RelationType))
AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid)
""", nativeQuery = true)
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType);
@Timed("app.office.relations.repo.save.rbac")
HsOfficeRelationRbacEntity save(final HsOfficeRelationRbacEntity entity);
@Timed("app.office.relations.repo.count.rbac")
long count();
@Timed("app.office.relations.repo.deleteByUuid.rbac")
int deleteByUuid(UUID uuid);
private static String toSqlLikeOperand(final String text) {
return text == null ? null : ("%" + text.toLowerCase() + "%");
}
private static String toStringOrNull(final HsOfficeRelationType relationType) {
return relationType == null ? null : relationType.name();
}
}

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.office.relation;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
@ -11,18 +10,16 @@ import java.util.UUID;
public interface HsOfficeRelationRealRepository extends Repository<HsOfficeRelationRealEntity, UUID> {
@Timed("app.repo.relations.findByUuid.real")
Optional<HsOfficeRelationRealEntity> findByUuid(UUID id);
default List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) {
return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType == null ? null : relationType.toString());
return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType.toString());
}
@Query(value = """
SELECT p.* FROM hs_office.relation AS p
WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid
""", nativeQuery = true)
@Timed("app.repo.relations.findRelationRelatedToPersonUuid.real")
List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuid(@NotNull UUID personUuid);
@Query(value = """
@ -30,15 +27,11 @@ public interface HsOfficeRelationRealRepository extends Repository<HsOfficeRelat
WHERE (:relationType IS NULL OR p.type = cast(:relationType AS hs_office.RelationType))
AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid)
""", nativeQuery = true)
@Timed("app.repo.relations.findRelationRelatedToPersonUuidAndRelationTypeString.real")
List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType);
@Timed("app.repo.relations.save.real")
HsOfficeRelationRealEntity save(final HsOfficeRelationRealEntity entity);
@Timed("app.repo.relations.count.real")
long count();
@Timed("app.repo.relations.deleteByUuid.real")
int deleteByUuid(UUID uuid);
}

View File

@ -8,6 +8,5 @@ public enum HsOfficeRelationType {
VIP_CONTACT,
DEBITOR,
OPERATIONS,
OPERATIONS_ALERT,
SUBSCRIBER
}

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.office.sepamandate;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMandatesApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateInsertResource;
@ -39,8 +38,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Override
@Transactional(readOnly = true)
@Timed("app.office.sepaMandates.api.getListOfSepaMandates")
public ResponseEntity<List<HsOfficeSepaMandateResource>> getListOfSepaMandates(
public ResponseEntity<List<HsOfficeSepaMandateResource>> listSepaMandatesByIban(
final String currentSubject,
final String assumedRoles,
final String iban) {
@ -55,8 +53,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Override
@Transactional
@Timed("app.office.sepaMandates.api.postNewSepaMandate")
public ResponseEntity<HsOfficeSepaMandateResource> postNewSepaMandate(
public ResponseEntity<HsOfficeSepaMandateResource> addSepaMandate(
final String currentSubject,
final String assumedRoles,
final HsOfficeSepaMandateInsertResource body) {
@ -79,8 +76,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Override
@Transactional(readOnly = true)
@Timed("app.office.sepaMandates.api.getSingleSepaMandateByUuid")
public ResponseEntity<HsOfficeSepaMandateResource> getSingleSepaMandateByUuid(
public ResponseEntity<HsOfficeSepaMandateResource> getSepaMandateByUuid(
final String currentSubject,
final String assumedRoles,
final UUID sepaMandateUuid) {
@ -97,7 +93,6 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Override
@Transactional
@Timed("app.office.sepaMandates.api.deleteSepaMandateByUuid")
public ResponseEntity<Void> deleteSepaMandateByUuid(
final String currentSubject,
final String assumedRoles,
@ -114,7 +109,6 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Override
@Transactional
@Timed("app.office.sepaMandates.api.patchSepaMandate")
public ResponseEntity<HsOfficeSepaMandateResource> patchSepaMandate(
final String currentSubject,
final String assumedRoles,
@ -137,7 +131,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
if (entity.getValidity().hasUpperBound()) {
resource.setValidTo(entity.getValidity().upper().minusDays(1));
}
resource.getDebitor().setDebitorNumber(entity.getDebitor().getTaggedDebitorNumber());
resource.getDebitor().setDebitorNumber(entity.getDebitor().getDebitorNumber());
};
final BiConsumer<HsOfficeSepaMandateInsertResource, HsOfficeSepaMandateEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {

View File

@ -7,10 +7,10 @@ import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
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 org.hibernate.annotations.Type;
import jakarta.persistence.*;
@ -30,7 +30,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectRefer
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "sepamandate_rv")

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.office.sepamandate;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
@ -10,7 +9,6 @@ import java.util.UUID;
public interface HsOfficeSepaMandateRepository extends Repository<HsOfficeSepaMandateEntity, UUID> {
@Timed("app.office.sepaMandates.repo.findByUuid")
Optional<HsOfficeSepaMandateEntity> findByUuid(UUID id);
@Query("""
@ -18,16 +16,12 @@ public interface HsOfficeSepaMandateRepository extends Repository<HsOfficeSepaMa
WHERE :iban is null
OR mandate.bankAccount.iban like concat(cast(:iban as text), '%')
ORDER BY mandate.bankAccount.iban
""")
@Timed("app.office.sepaMandates.repo.findSepaMandateByOptionalIban")
""")
List<HsOfficeSepaMandateEntity> findSepaMandateByOptionalIban(String iban);
@Timed("app.office.sepaMandates.repo.save")
HsOfficeSepaMandateEntity save(final HsOfficeSepaMandateEntity entity);
@Timed("app.office.sepaMandates.repo.count")
long count();
@Timed("app.office.sepaMandates.repo.deleteByUuid")
int deleteByUuid(UUID uuid);
}

View File

@ -56,10 +56,6 @@ public class IntegerProperty<P extends IntegerProperty<P>> extends ValidatablePr
return unit;
}
public Integer min() {
return min;
}
public Integer max() {
return max;
}

Some files were not shown because too many files have changed in this diff Show More