63 Commits

Author SHA1 Message Date
verboomp
f05b6da0ee added download from excel and zip in frontend 2026-02-23 16:30:21 +01:00
verboomp
746294d640 added download from excel and zip in frontend 2026-02-23 16:19:57 +01:00
verboomp
e0c6a7db5a Added zip export 2026-02-23 12:18:41 +01:00
verboomp
888136e76b Added excel export 2026-02-23 10:43:06 +01:00
verboomp
38be68397a fix test 2026-02-19 14:22:46 +01:00
verboomp
5a868fe27e fix test 2026-02-19 13:55:57 +01:00
verboomp
4b8c41aba7 fix test 2026-02-19 13:25:23 +01:00
verboomp
db58ae079f unit test 2026-02-19 12:09:43 +01:00
verboomp
9b3446685a start quesitonnaire 2026-02-19 11:04:02 +01:00
verboomp
168fc986f2 Tweaking cusomter list 2026-02-10 11:13:16 +01:00
verboomp
a6216f6e81 tweaking download 2026-02-06 14:41:16 +01:00
verboomp
2955a7eb1c tweaking download 2026-02-06 13:45:35 +01:00
verboomp
7819b963f2 Fix image in pdf using the EXIF image header 2026-02-06 12:11:04 +01:00
verboomp
5e941753e2 Tweaking padding in customer list and update look and feel pdf export 2026-02-06 11:09:25 +01:00
verboomp
9c439e21ee Fix bug wrong orientation caused by EXIF 2026-02-06 10:01:45 +01:00
verboomp
b6158d933f Fix bug wrong orientation caused by EXIF 2026-02-06 09:37:03 +01:00
verboomp
bb4d7fbf68 Improved by resolving the last two fixme 2026-02-05 09:25:13 +01:00
verboomp
cb5d0de5b2 Trying to fix image loading on dev 2026-02-03 16:01:31 +01:00
verboomp
bf02f143dd Trying to fix image loading on dev 2026-02-03 15:46:43 +01:00
verboomp
51a05ea372 Trying to fix image loading on dev 2026-02-03 15:23:57 +01:00
verboomp
c7ad119f35 Trying to fix image loading on dev 2026-02-03 15:06:27 +01:00
verboomp
2e120291c7 fix url for https 2026-02-03 14:35:02 +01:00
verboomp
3a1b9a3af6 fix url for https 2026-02-03 14:21:24 +01:00
verboomp
935999bb5a fix url for https 2026-02-03 14:13:10 +01:00
verboomp
1296658645 fix url for https 2026-02-03 14:07:36 +01:00
verboomp
4cd5e12d72 cleanup and unit tests 2026-02-03 13:57:13 +01:00
verboomp
c78560ac3a cleanup and unit tests 2026-02-03 12:27:56 +01:00
verboomp
e3d7716133 cleanup and unit tests 2026-02-03 12:14:55 +01:00
verboomp
508b1b4380 cleanup and unit tests 2026-02-03 12:02:45 +01:00
verboomp
6cb7c768e0 cleanup and unit tests 2026-02-03 11:46:03 +01:00
verboomp
54b6237188 cleanup and unit tests 2026-02-03 10:33:59 +01:00
verboomp
5f1d2d8610 Added download 2026-02-03 09:51:03 +01:00
verboomp
f9ca668b39 Styling tweaking 2026-02-02 14:52:33 +01:00
verboomp
e0279a4d26 image test 2026-02-02 13:11:06 +01:00
verboomp
c7afc52abc image test 2026-02-02 12:54:34 +01:00
verboomp
cbd53ac9a5 fix test 2026-02-02 11:17:51 +01:00
verboomp
5f2a54a5bc Imrpoved image handling since the size of the image is about 5MB. 2026-02-02 10:54:53 +01:00
verboomp
8d329501e4 Imrpoved image handling since the size of the image is about 5MB. 2026-02-02 10:35:22 +01:00
verboomp
33ee33d55c removed auth from picture upload 2026-02-02 08:13:28 +01:00
verboomp
94c6becf9f added unit test 2026-01-30 14:30:53 +01:00
verboomp
d9d64d2daa added unit test 2026-01-29 15:42:28 +01:00
verboomp
71100353fa rework ui 2026-01-29 15:22:05 +01:00
verboomp
0918185a6b rework ui 2026-01-29 15:06:56 +01:00
verboomp
2587a9c3c8 rework ui 2026-01-29 14:50:06 +01:00
verboomp
e062b4c688 rework ui 2026-01-29 12:37:44 +01:00
verboomp
38979c99e5 rework ui 2026-01-29 07:08:44 +01:00
verboomp
ca514eea67 added tests and enabled the schema validation for upload 2026-01-28 10:46:38 +01:00
verboomp
98764dc51e added tests and enabled the schema validation for upload 2026-01-27 15:29:28 +01:00
verboomp
e4b2dd0462 cleanup and added unit tests 2026-01-27 14:09:12 +01:00
verboomp
3d456128b1 First designs for ui 2026-01-27 09:57:53 +01:00
verboomp
f48bfe2107 First designs for ui 2026-01-23 15:09:34 +01:00
verboomp
b3de3eec8c added frontend 2026-01-21 16:08:09 +01:00
verboomp
d2e6f5164a Extended security for web front end and added first rest resources 2026-01-21 14:08:50 +01:00
verboomp
47ee7c3c25 fix code smells 2026-01-21 09:40:43 +01:00
verboomp
2c14caa6ca fix sql script 2026-01-21 09:33:56 +01:00
verboomp
934a5eec51 added init db security setup 2026-01-20 15:50:46 +01:00
verboomp
39580438c2 added security to picture upload resource 2026-01-20 15:28:15 +01:00
verboomp
8ccd98755b tweaking pom.xml 2026-01-20 15:04:13 +01:00
verboomp
db46303a88 tweaking pom.xml 2026-01-20 14:58:06 +01:00
verboomp
f3ba2758af tweaking pom.xml 2026-01-20 14:56:49 +01:00
verboomp
4b4353f133 disable junit for regular build phase 2026-01-20 11:52:32 +01:00
verboomp
05b0ced8dd [P25284-20] Initial commit with setup and first draft rest servcice 2026-01-20 11:45:35 +01:00
verboomp
9d36ea7780 [P25284-20] Initial commit with setup and first draft rest servcice 2026-01-20 11:39:25 +01:00
237 changed files with 22962 additions and 16 deletions

32
.gitignore vendored
View File

@@ -42,9 +42,9 @@ target/
*/.dart_tool */.dart_tool
*/build */build
skillmatrix-frontend/build hartmann-foto-documentation-frontend/build
skillmatrix-frontend/coverage/ hartmann-foto-documentation-frontend/coverage/
skillmatrix-frontend/pubspec.lock hartmann-foto-documentation-frontend/pubspec.lock
# Avoid committing generated Javascript files: # Avoid committing generated Javascript files:
*.dart.js *.dart.js
@@ -59,17 +59,17 @@ skillmatrix-frontend/pubspec.lock
.flutter-plugins-dependencies .flutter-plugins-dependencies
hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-*.war
skillmatrix-docker/src/main/docker/skillmatrix-web-*.war hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-*.war
skillmatrix-web/src/main/webapp/.last_build_id hartmann-foto-documentation-web/src/main/webapp/.last_build_id
skillmatrix-web/src/main/webapp/assets/ hartmann-foto-documentation-web/src/main/webapp/assets/
skillmatrix-web/src/main/webapp/canvaskit/ hartmann-foto-documentation-web/src/main/webapp/canvaskit/
skillmatrix-web/src/main/webapp/favicon.png hartmann-foto-documentation-web/src/main/webapp/favicon.png
skillmatrix-web/src/main/webapp/flutter.js hartmann-foto-documentation-web/src/main/webapp/flutter.js
skillmatrix-web/src/main/webapp/flutter_bootstrap.js hartmann-foto-documentation-web/src/main/webapp/flutter_bootstrap.js
skillmatrix-web/src/main/webapp/flutter_service_worker.js hartmann-foto-documentation-web/src/main/webapp/flutter_service_worker.js
skillmatrix-web/src/main/webapp/icons/ hartmann-foto-documentation-web/src/main/webapp/icons/
skillmatrix-web/src/main/webapp/index.html hartmann-foto-documentation-web/src/main/webapp/index.html
skillmatrix-web/src/main/webapp/manifest.json hartmann-foto-documentation-web/src/main/webapp/manifest.json
skillmatrix-web/src/main/webapp/version.json hartmann-foto-documentation-web/src/main/webapp/version.json

17
.project Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>hartmann-foto_documentation</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

99
CLAUDE.md Normal file
View File

@@ -0,0 +1,99 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
### Backend (Maven)
```bash
# Full build and deploy
mvn deploy -U -f pom.xml
# Run integration tests with Docker
mvn deploy -P docker -f hartmann-foto-documentation-docker/pom.xml
```
### Frontend (Flutter)
```bash
cd hartmann-foto-documentation-frontend
# Install dependencies
flutter pub get
# Build for web
flutter build web --no-tree-shake-icons
# Run tests with coverage
flutter test --coverage --reporter=json
# Run a single test file
flutter test test/path/to/test_file.dart
# Static analysis
flutter analyze
# Generate localization files
flutter gen-l10n
```
## Architecture
### Multi-Module Maven Project
- **hartmann-foto-documentation-app** - Backend REST API (jar)
- **hartmann-foto-documentation-web** - WAR deployment package
- **hartmann-foto-documentation-docker** - Docker/integration tests
- **hartmann-foto-documentation-frontend** - Flutter mobile/web app
### Technology Stack
- **Backend:** Java 21, WildFly 26.1.3, Jakarta EE 10, Hibernate 6.2, PostgreSQL 11, JWT auth
- **Frontend:** Flutter 3.3.0+, Provider for state management, go_router for navigation
- **API Docs:** Swagger/OpenAPI v3
### Backend Layered Architecture
```
REST Resources → Services (@Stateless EJBs) → QueryService → JPA Entities
```
Key packages in `hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/`:
- `core/model/` - JPA entities (User, Right, Customer, Picture, JwtRefreshToken)
- `core/service/` - Business logic with AbstractService base class
- `core/query/` - QueryService for database operations
- `rest/` - JAX-RS resources and value objects
### Frontend Structure
```
lib/
├── main.dart # Entry point (MaterialApp.router)
├── controller/ # Business logic controllers
├── pages/ # Feature-based UI (login/, customer/)
├── dto/ # Data Transfer Objects
├── utils/ # DI container, theme, routing
└── l10n/ # Localization (German supported)
```
### Authentication
- JWT token-based authentication with refresh tokens
- WildFly Elytron security realm
- Bearer token auth for REST endpoints
- User passwords stored with salt-based hashing
### Database Entities
- User ↔ Right (many-to-many via user_to_right)
- Customer → Picture (one-to-many)
- JwtRefreshToken (device/IP tracking)
Named queries pattern: `@NamedQuery` on entities (e.g., `User.BY_USERNAME`)
## Local Development
Docker Compose provides PostgreSQL and WildFly:
```bash
cd hartmann-foto-documentation-docker
docker-compose up
```
Port mappings: WildFly HTTP (8180), Management (9990), SMTP (8280)
## CI/CD
Jenkins pipeline with SonarQube integration. Build runs on macOS agent with JDK 21.

243
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,243 @@
//
// Created by Patrick Verboom on 04.11.2024.
// Copyright © 2024 heyday Marketing GmbH. All rights reserved.
//
def defaultRocketChatChannel = "#builds"
def rocketChatColor = "#e813c8"
def rocketChatEmoji = ":skunk:"
def numOfArtifactsToKeep = env.GIT_BRANCH == "master" ? "10" : "5"
def result = []
pipeline {
agent {
label "macOS"
}
tools {
maven 'Maven Latest'
jdk 'OpenJDK-21.0.5'
}
environment {
PATH = "$PATH:/Users/Shared/jenkins/flutter/bin"
}
options {
// keeps only the atifacts of the last 15 builds
buildDiscarder(
logRotator(
artifactDaysToKeepStr: '',
artifactNumToKeepStr: numOfArtifactsToKeep,
daysToKeepStr: '',
numToKeepStr: numOfArtifactsToKeep
)
)
}
stages {
stage ('Initialize') {
steps {
script {
def msg = "Pipeline ${env.JOB_NAME} ${env.BUILD_NUMBER} has started. \n More info at: ${env.BUILD_URL} "
try {
echo msg
rocketSend channel: defaultRocketChatChannel, message: "jenkins-${env.JOB_NAME}-${env.BUILD_NUMBER}", emoji: rocketChatEmoji, attachments: [[$class: 'MessageAttachment', color: rocketChatColor, text: msg, title: 'Build started']]
} catch (Exception e) {
echo "Exception occurred sending : " + e.toString() + "\nmessage: " + msg
throw e
}
}
sh '''
echo "PATH = ${PATH}"
echo "M2_HOME = ${M2_HOME}"
printenv
'''
}
}
stage ('Build Frontend') {
steps {
echo "running Frontend build for branch ${env.BRANCH_NAME}"
dir("hartmann-foto-documentation-frontend"){
//flutter build web --dart-define=FLUTTER_WEB_CANVASKIT_URL=OURBASEURL/canvaskit/
sh 'flutter pub get'
//sh 'dart run build_runner build'
sh 'flutter build web --no-tree-shake-icons'
dir("build/web"){
sh "cp -R . ../../../hartmann-foto-documentation-web/src/main/webapp/."
}
}
}
post {
always {
script {
def msg = "Build ${env.JOB_NAME} ${env.BUILD_NUMBER} frontend build has finished. Result: ${currentBuild.currentResult}. Took ${currentBuild.duration}.\n More info at: ${env.BUILD_URL} "
result.add( ["Stage: Build ${currentBuild.currentResult}", msg] )
}
}
}
}
stage ('Test Frontend') {
steps {
echo "running Frontend test for branch ${env.BRANCH_NAME}"
dir("hartmann-foto-documentation-frontend"){
// Run tests with JSON output for Jenkins parsing
sh '''
flutter test --coverage --reporter=json --file-reporter=json:test_results.json --reporter=expanded || true
dart test_runner.dart test_results.json test_results.xml
'''
}
}
post {
success {
archiveArtifacts artifacts: 'hartmann-foto-documentation-frontend/coverage/lcov.info', fingerprint: true
}
always {
// Publish test results to Jenkins
junit 'hartmann-foto-documentation-frontend/test_results.xml'
// Archive test artifacts
archiveArtifacts artifacts: 'hartmann-foto-documentation-frontend/test_results.json, hartmann-foto-documentation-frontend/test_results.xml', allowEmptyArchive: true
script {
def msg = "Build ${env.JOB_NAME} ${env.BUILD_NUMBER} frontend test has finished. Result: ${currentBuild.currentResult}. Took ${currentBuild.duration}.\n More info at: ${env.BUILD_URL} "
result.add( ["Stage: Build ${currentBuild.currentResult}", msg] )
}
}
}
}
stage ('Build') {
steps {
echo "running build for branch ${env.BRANCH_NAME}"
//sh 'mvn dependency:purge-local-repository clean -U -f hartmann-foto-documentation/pom.xml '
sh 'mvn deploy -U -f pom.xml'
}
post {
success {
//junit '**/target/surefire-reports/*.xml'
archiveArtifacts artifacts: '**/target/*.war, **/target/*.zip', fingerprint: true
}
always {
script {
def msg = "Build ${env.JOB_NAME} ${env.BUILD_NUMBER} build has finished. Result: ${currentBuild.currentResult}. Took ${currentBuild.duration}.\n More info at: ${env.BUILD_URL} "
result.add( ["Stage: Build ${currentBuild.currentResult}", msg] )
}
}
}
}
stage ('Tests') {
steps {
sh 'mvn deploy -P docker -f hartmann-foto-documentation-docker/pom.xml'
}
post {
success {
junit '**/target/surefire-reports/*.xml'
}
always {
script {
def msg = "Build ${env.JOB_NAME} ${env.BUILD_NUMBER} tests has finished. Result: ${currentBuild.currentResult}. Took ${currentBuild.duration}.\n More info at: ${env.BUILD_URL} "
result.add( ["Stage: Test ${currentBuild.currentResult}", msg])
}
}
}
}
stage ('Code Coverage') {
steps {
sh 'mvn jacoco:report-aggregate -P docker -f pom.xml'
sh 'cp hartmann-foto-documentation-docker/target/site/jacoco-aggregate/jacoco.xml hartmann-foto-documentation-app/target/site/jacoco-aggregate/jacoco.xml'
}
post {
always {
script {
def msg = "Build ${env.JOB_NAME} ${env.BUILD_NUMBER} Code coverage has finished. Result: ${currentBuild.currentResult}. Took ${currentBuild.duration}.\n More info at: ${env.BUILD_URL} "
result.add( ["Stage: Test ${currentBuild.currentResult}", msg])
}
}
}
}
stage ('SonarQube analysis') {
steps {
withSonarQubeEnv('heyday sonar') {
script {
def key = "${env.BRANCH_NAME.replaceAll("/", "_")}"
def projectKey = "\"marketing.heyday.hartmann:hartmann-foto-documentation:${key}\""
echo "running sonar for branch ${projectKey}"
sh "mvn org.sonarsource.scanner.maven:sonar-maven-plugin:5.3.0.6276:sonar -f pom.xml -Dsonar.projectKey=${projectKey}"
}
}
}
post {
always {
script {
def msg = "Build ${env.JOB_NAME} ${env.BUILD_NUMBER} sonar has finished. Result: ${currentBuild.currentResult}. Took ${currentBuild.duration}.\n More info at: ${env.BUILD_URL} "
result.add(["Stage: Sonar ${currentBuild.currentResult}", msg])
}
}
}
}
stage ('Release') {
when {
branch 'master'
}
steps {
sh "mvn -B -U -X -e -Dmaven.test.skip=true -Djava.awt.headless=true release:prepare release:perform -DbambooBuildNumber=${env.BUILD_NUMBER} -Dmaven.javadoc.skip=true -f pom.xml "
}
post {
success {
archiveArtifacts artifacts: '**/target/*.war, **/target/*.zip', fingerprint: true
}
always {
script {
def msg = "Build ${env.JOB_NAME} ${env.BUILD_NUMBER} release has finished. Result: ${currentBuild.currentResult}. Took ${currentBuild.duration}.\n More info at: ${env.BUILD_URL} "
result.add(["Stage: Release ${currentBuild.currentResult}",msg])
}
}
}
}
}
post {
always {
script {
def msg = "Build ${env.JOB_NAME} ${env.BUILD_NUMBER} Pipeline finished. Result: ${currentBuild.currentResult}. Took ${currentBuild.duration}.\n More info at: ${env.BUILD_URL} "
result.add(["Stage: Pipeline ${currentBuild.currentResult}", msg])
try {
//echo msg
def attachements = []
for (elem in result) {
attachements+=([$class: 'MessageAttachment', color: rocketChatColor, text: elem.get(1), title: elem.get(0)])
}
rocketSend channel: defaultRocketChatChannel, message: "jenkins-${env.JOB_NAME}-${env.BUILD_NUMBER}", emoji: rocketChatEmoji, attachments: attachements
} catch (Exception e) {
echo "Exception occurred sending : " + e.toString() + "\nmessage: " + msg
throw e
}
}
}
unsuccessful {
emailext body: "${currentBuild.currentResult}: Job ${env.JOB_NAME} build ${env.BUILD_NUMBER}\n More info at: ${env.BUILD_URL}",
recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']],
subject: "Failed Jenkins Build ${currentBuild.currentResult}: Job ${env.JOB_NAME}"
}
fixed {
emailext body: "${currentBuild.currentResult}: Job ${env.JOB_NAME} build ${env.BUILD_NUMBER}\n More info at: ${env.BUILD_URL}",
recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']],
subject: "Success Jenkins Build ${currentBuild.currentResult}: Job ${env.JOB_NAME}"
}
}
}

View File

@@ -0,0 +1,392 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>marketing.heyday.hartmann.fotodocumentation</groupId>
<artifactId>hartmann-foto-documentation</artifactId>
<version>1.0.1</version>
<relativePath>../hartmann-foto-documentation/pom.xml</relativePath>
</parent>
<artifactId>hartmann-foto-documentation-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>hartmann-foto-documentation app</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<configuration>
<skip>true</skip>
<skipDeploy>true</skipDeploy>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- EXCEL report -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<!-- PDF report -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.5</version>
</dependency>
<!-- EXIF metadata extraction for image orientation -->
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.19.0</version>
</dependency>
<!-- Elytron secrity used for username/password login -->
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron</artifactId>
<version>2.5.2.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-credential</artifactId>
<version>2.5.2.Final</version>
<scope>provided</scope>
</dependency>
<!-- JWT Library -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT Library -->
<!-- Apache POI -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<!-- Apache POI-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>fluent-hc</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2-jakarta</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>3.22.2</version>
</dependency>
<dependency>
<groupId>com.github.spullara.mustache.java</groupId>
<artifactId>compiler</artifactId>
<version>0.9.5</version>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>0.1.2</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${version.commons-logging}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${version.commons-lang3}</version>
</dependency>
<dependency>
<groupId>io.javaslang</groupId>
<artifactId>javaslang</artifactId>
<version>${version.javaslang}</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.jboss.ejb3</groupId>
<artifactId>jboss-ejb3-ext-api</artifactId>
<version>${version.org.jboss.ejb3.ext-api}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Resteasy -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-core-spi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-multipart-provider</artifactId>
<scope>provided</scope>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${version.commons-fileupload}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.21.0</version>
</dependency>
<!-- Apache POI -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<scope>provided</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Test -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>17.0</version>
<scope>test</scope>
</dependency>
<!-- to fix the java.lang.NoClassDefFoundError:
org/w3c/dom/ElementTraversal -->
<dependency>
<groupId>xml-apis</groupId>
<artifactId>xml-apis</artifactId>
<version>1.4.01</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.subethamail</groupId>
<artifactId>subethasmtp-smtp</artifactId>
<version>${subethamail.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.subethamail</groupId>
<artifactId>subethasmtp-wiser</artifactId>
<version>${subethamail.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.subethamail</groupId>
<artifactId>subethasmtp-smtp</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<!-- for jacoco -->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.core</artifactId>
<scope>test</scope>
</dependency>
<!-- Jakarta JSON Processing implementation for unit tests -->
<dependency>
<groupId>org.eclipse.parsson</groupId>
<artifactId>parsson</artifactId>
<version>1.1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,48 @@
package marketing.heyday.hartmann.fotodocumentation.core.db.migration;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;
import jakarta.ejb.TransactionManagement;
import jakarta.ejb.TransactionManagementType;
import javax.sql.DataSource;
import org.flywaydb.core.Flyway;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Singleton
@Startup
@TransactionManagement(TransactionManagementType.BEAN)
public class DbMigrator {
@Resource(lookup = "java:/jdbc/fotoDocumentationDS")
private DataSource dataSource;
@PostConstruct
public void onStartup() {
Flyway flyway = getFlyway();
flyway.setDataSource(dataSource);
flyway.setTable("DB_MIGRATION");
flyway.setLocations(this.getClass().getPackage().getName());
flyway.setBaselineOnMigrate(false);
flyway.setValidateOnMigrate(false);
flyway.setIgnoreFailedFutureMigration(true);
flyway.migrate();
}
protected Flyway getFlyway() {
return new Flyway();
}
}

View File

@@ -0,0 +1,71 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.Date;
import jakarta.persistence.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@MappedSuperclass
public abstract class AbstractDateEntity extends AbstractEntity {
private static final long serialVersionUID = 1L;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "JPA_CREATED", nullable = false)
@JsonIgnore
private Date jpaCreated;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "JPA_UPDATED", nullable = false)
@JsonIgnore
private Date jpaUpdated;
@Column(name = "JPA_ACTIVE", nullable = false)
private boolean active = true;
@Version
@Column(name = "JPA_VERSION", nullable = false)
private int jpaVersion = 0;
@PrePersist
protected void onCreate() {
Date now = new Date();
jpaCreated = now;
jpaUpdated = now;
}
@PreUpdate
protected void onUpdate() {
jpaUpdated = new Date();
}
public Date getJpaCreated() {
return jpaCreated;
}
public Date getJpaUpdated() {
return jpaUpdated;
}
public int getJpaVersion() {
return jpaVersion;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}

View File

@@ -0,0 +1,21 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.io.Serializable;
import jakarta.persistence.MappedSuperclass;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@MappedSuperclass
public abstract class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,139 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang.builder.HashCodeBuilder;
import jakarta.persistence.*;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Entity
@Table(name = "customer")
@NamedQuery(name = Customer.FIND_BY_NUMBER, query = "select c from Customer c where c.customerNumber = :cutomerNumber")
public class Customer extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "customer_seq";
public static final String FIND_BY_NUMBER = "Customer.findByNumber";
public static final String PARAM_NUMBER = "cutomerNumber";
@Id
@Column(name = "customer_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long customerId;
@Column(name = "customer_number", unique = true, nullable = false)
private String customerNumber;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "city")
private String city;
@Column(name = "zip")
private String zip;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Picture> pictures = new HashSet<>();
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getCustomerNumber() {
return customerNumber;
}
public void setCustomerNumber(String customerNumber) {
this.customerNumber = customerNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
public Set<Picture> getPictures() {
return pictures;
}
public void setPictures(Set<Picture> pictures) {
this.pictures = pictures;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(customerNumber).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
return this.customerNumber.equals(((Customer) obj).getCustomerNumber());
}
public static class Builder {
private Customer instance = new Customer();
public Builder customerNumber(String customerNumber) {
instance.setCustomerNumber(customerNumber);
return this;
}
public Builder name(String name) {
instance.setName(name);
return this;
}
public Builder city(String city) {
instance.setCity(city);
return this;
}
public Builder zip(String zip) {
instance.setZip(zip);
return this;
}
public Customer build() {
return instance;
}
}
}

View File

@@ -0,0 +1,191 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@Entity
@Table(name = "jwt_refresh_token")
@NamedQuery(name = JwtRefreshToken.FIND_BY_HASH_REVOKE_NULL, query = "SELECT t FROM JwtRefreshToken t WHERE t.tokenHash = :hash AND t.revokedAt IS NULL")
@NamedQuery(name = JwtRefreshToken.FIND_BY_HASH, query = "SELECT t FROM JwtRefreshToken t WHERE t.tokenHash = :hash")
@NamedQuery(name = JwtRefreshToken.REVOKE_ALL_USER, query = "UPDATE JwtRefreshToken t SET t.revokedAt = :date WHERE t.user.userId = :userId AND t.revokedAt IS NULL")
public class JwtRefreshToken extends AbstractEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "jwt_refresh_token_seq";
public static final String FIND_BY_HASH_REVOKE_NULL = "JwtRefreshToken.forHashRevokeNull";
public static final String FIND_BY_HASH = "JwtRefreshToken.forHash";
public static final String REVOKE_ALL_USER = "JwtRefreshToken.revokeAllUser";
public static final String PARAM_HASH = "hash";
public static final String PARAM_USER_ID = "userId";
public static final String PARAM_DATE = "date";
@Id
@Column(name = "jwt_refresh_token_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long jwsRefreshTokenId;
@Column(name = "token_hash", nullable = false)
private String tokenHash;
@Column(name = "device_info", nullable = false)
private String deviceInfo;
@Column(name = "ip_address", nullable = false)
private String ipAddress;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "issued_at", nullable = false)
@JsonIgnore
private Date issuedAt;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "expires_at", nullable = false)
@JsonIgnore
private Date expiresAt;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "revoked_at", nullable = false)
@JsonIgnore
private Date revokedAt;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "last_used_at", nullable = false)
@JsonIgnore
private Date lastUsedAt;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id_fk", nullable = true)
private User user;
public Long getJwsRefreshTokenId() {
return jwsRefreshTokenId;
}
public void setJwsRefreshTokenId(Long jwsRefreshTokenId) {
this.jwsRefreshTokenId = jwsRefreshTokenId;
}
public String getTokenHash() {
return tokenHash;
}
public void setTokenHash(String tokenHash) {
this.tokenHash = tokenHash;
}
public String getDeviceInfo() {
return deviceInfo;
}
public void setDeviceInfo(String deviceInfo) {
this.deviceInfo = deviceInfo;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public Date getIssuedAt() {
return issuedAt;
}
public void setIssuedAt(Date issuedAt) {
this.issuedAt = issuedAt;
}
public Date getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(Date expiresAt) {
this.expiresAt = expiresAt;
}
public Date getRevokedAt() {
return revokedAt;
}
public void setRevokedAt(Date revokedAt) {
this.revokedAt = revokedAt;
}
public Date getLastUsedAt() {
return lastUsedAt;
}
public void setLastUsedAt(Date lastUsedAt) {
this.lastUsedAt = lastUsedAt;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public static class Builder {
private JwtRefreshToken instance = new JwtRefreshToken();
public Builder tokenHash(String tokenHash) {
instance.setTokenHash(tokenHash);
return this;
}
public Builder deviceInfo(String deviceInfo) {
instance.setDeviceInfo(deviceInfo);
return this;
}
public Builder ipAddress(String ipAddress) {
instance.setIpAddress(ipAddress);
return this;
}
public Builder expiresAt(Date expiresAt) {
instance.setExpiresAt(expiresAt);
return this;
}
public Builder issuedAt(Date issuedAt) {
instance.setIssuedAt(issuedAt);
return this;
}
public Builder revokedAt(Date revokedAt) {
instance.setRevokedAt(revokedAt);
return this;
}
public Builder lastUsedAt(Date lastUsedAt) {
instance.setLastUsedAt(lastUsedAt);
return this;
}
public Builder user(User user) {
instance.setUser(user);
return this;
}
public JwtRefreshToken build() {
return instance;
}
}
}

View File

@@ -0,0 +1,180 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.Date;
import org.apache.commons.lang.builder.HashCodeBuilder;
import jakarta.persistence.*;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Entity
@Table(name = "picture")
public class Picture extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "picture_seq";
@Id
@Column(name = "picture_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long pictureId;
// username from the person that shot the picture
@Column(name = "username")
private String username;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "picture_date", nullable = false)
private Date pictureDate;
@Basic(fetch = FetchType.LAZY)
private String comment;
@Column(name = "evaluation")
private Integer evaluation;
@Column
private String category;
@Column(name = "image")
@Basic(fetch = FetchType.LAZY)
private String image;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id_fk")
private Customer customer;
public Long getPictureId() {
return pictureId;
}
public void setPictureId(Long pictureId) {
this.pictureId = pictureId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getPictureDate() {
return pictureDate;
}
public void setPictureDate(Date pictureDate) {
this.pictureDate = pictureDate;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public Integer getEvaluation() {
return evaluation;
}
public void setEvaluation(Integer evaluation) {
this.evaluation = evaluation;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(pictureId).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || this.getClass() != obj.getClass() || pictureId == null) {
return false;
}
return this.pictureId.equals(((Picture) obj).getPictureId());
}
public static class Builder {
private Picture instance = new Picture();
public Builder(){
instance.evaluation = 0;
}
public Builder username(String username) {
instance.setUsername(username);
return this;
}
public Builder pictureDate(Date pictureDate) {
instance.setPictureDate(pictureDate);
return this;
}
public Builder comment(String comment) {
instance.setComment(comment);
return this;
}
public Builder category(String category) {
instance.setCategory(category);
return this;
}
public Builder image(String image) {
instance.setImage(image);
return this;
}
public Builder evaluation(Integer evaluation) {
instance.setEvaluation(evaluation);
return this;
}
public Builder customer(Customer customer) {
instance.setCustomer(customer);
return this;
}
public Picture build() {
return instance;
}
}
}

View File

@@ -0,0 +1,181 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.Date;
import org.apache.commons.lang.builder.HashCodeBuilder;
import jakarta.persistence.*;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Entity
@Table(name = "questionnaire")
public class Questionnaire extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "questionnaire_seq";
@Id
@Column(name = "questionnaire_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long questionnaireId;
// username from the person that shot the picture
@Column(name = "username")
private String username;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "questionnaire_date", nullable = false)
private Date questionnaireDate;
@Basic(fetch = FetchType.LAZY)
private String comment;
@Column(name = "evaluation")
private Integer evaluation;
@Column
private String category;
@Column(name = "questions")
@Basic(fetch = FetchType.LAZY)
private String questions;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id_fk")
private QuestionnaireCustomer customer;
public Long getQuestionnaireId() {
return questionnaireId;
}
public void setQuestionnaireId(Long questionnaireId) {
this.questionnaireId = questionnaireId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getQuestionnaireDate() {
return questionnaireDate;
}
public void setQuestionnaireDate(Date questionnaireDate) {
this.questionnaireDate = questionnaireDate;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public Integer getEvaluation() {
return evaluation;
}
public void setEvaluation(Integer evaluation) {
this.evaluation = evaluation;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getQuestions() {
return questions;
}
public void setQuestions(String questions) {
this.questions = questions;
}
public QuestionnaireCustomer getCustomer() {
return customer;
}
public void setCustomer(QuestionnaireCustomer customer) {
this.customer = customer;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(questionnaireId).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || this.getClass() != obj.getClass() || questionnaireId == null) {
return false;
}
return this.questionnaireId.equals(((Questionnaire) obj).getQuestionnaireId());
}
public static class Builder {
private Questionnaire instance = new Questionnaire();
public Builder(){
instance.evaluation = 0;
}
public Builder username(String username) {
instance.setUsername(username);
return this;
}
public Builder questionnaireDate(Date date) {
instance.setQuestionnaireDate(date);
return this;
}
public Builder comment(String comment) {
instance.setComment(comment);
return this;
}
public Builder category(String category) {
instance.setCategory(category);
return this;
}
public Builder questions(String questions) {
instance.setQuestions(questions);
return this;
}
public Builder evaluation(Integer evaluation) {
instance.setEvaluation(evaluation);
return this;
}
public Builder customer(QuestionnaireCustomer customer) {
instance.setCustomer(customer);
return this;
}
public Questionnaire build() {
return instance;
}
}
}

View File

@@ -0,0 +1,142 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang.builder.HashCodeBuilder;
import jakarta.persistence.*;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Entity
@Table(name = "questionnaire_customer")
@NamedQuery(name = QuestionnaireCustomer.FIND_ALL, query = "select c from QuestionnaireCustomer c order by c.name")
@NamedQuery(name = QuestionnaireCustomer.FIND_BY_NUMBER, query = "select c from QuestionnaireCustomer c where c.customerNumber = :customerNumber")
public class QuestionnaireCustomer extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "questionnaire_customer_seq";
public static final String FIND_ALL = "QuestionnaireCustomer.findAll";
public static final String FIND_BY_NUMBER = "QuestionnaireCustomer.findByNumber";
public static final String PARAM_NUMBER = "customerNumber";
@Id
@Column(name = "customer_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long customerId;
@Column(name = "customer_number", unique = true, nullable = false)
private String customerNumber;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "city")
private String city;
@Column(name = "zip")
private String zip;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Questionnaire> questionnaires = new HashSet<>();
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getCustomerNumber() {
return customerNumber;
}
public void setCustomerNumber(String customerNumber) {
this.customerNumber = customerNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
public Set<Questionnaire> getQuestionnaires() {
return questionnaires;
}
public void setQuestionnaires(Set<Questionnaire> questionnaires) {
this.questionnaires = questionnaires;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(customerNumber).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
return this.customerNumber.equals(((QuestionnaireCustomer) obj).getCustomerNumber());
}
public static class Builder {
private QuestionnaireCustomer instance = new QuestionnaireCustomer();
public Builder customerNumber(String customerNumber) {
instance.setCustomerNumber(customerNumber);
return this;
}
public Builder name(String name) {
instance.setName(name);
return this;
}
public Builder city(String city) {
instance.setCity(city);
return this;
}
public Builder zip(String zip) {
instance.setZip(zip);
return this;
}
public QuestionnaireCustomer build() {
return instance;
}
}
}

View File

@@ -0,0 +1,95 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import jakarta.persistence.*;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Entity
@Table(name = "x_right")
public class Right extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "right_seq";
@Id
@Column(name = "right_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long rightId;
@Column(nullable = false, unique = true)
private String code;
@Column(nullable = false, unique = true)
private String name;
@ManyToMany(mappedBy = "rights")
private Set<User> roles = new HashSet<>();
public Long getRightId() {
return rightId;
}
public void setRightId(Long rightId) {
this.rightId = rightId;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(code).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
return this.code.equals(((Right) obj).getCode());
}
public static class Builder {
private Right instance = new Right();
public Builder code(String code) {
instance.setCode(code);
return this;
}
public Builder name(String name) {
instance.setName(name);
return this;
}
public Right build() {
return instance;
}
}
}

View File

@@ -0,0 +1,182 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang.builder.HashCodeBuilder;
import jakarta.persistence.*;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Entity
@Table(name = "x_user")
@NamedQuery(name = User.BY_USERNAME, query = "select u from User u where u.username like :username")
public class User extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "user_seq";
public static final String BY_USERNAME = "user.byUsername";
public static final String PARAM_USERNAME = "username";
@Id
@Column(name = "user_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long userId;
@Column(name = "username", unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private String salt;
@Column(name = "title")
private String title;
@Column(nullable = false)
private String firstname;
@Column(nullable = false)
private String lastname;
@Column(nullable = false)
private String email;
@ManyToMany
@JoinTable(name = "user_to_right", joinColumns = { @JoinColumn(name = "user_id_fk") }, inverseJoinColumns = { @JoinColumn(name = "right_id_fk") })
private Set<Right> rights = new HashSet<>();
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Set<Right> getRights() {
return rights;
}
public void setRights(Set<Right> rights) {
this.rights = rights;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(username).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
return this.username.equals(((User) obj).getUsername());
}
public static class Builder {
private User instance = new User();
public Builder email(String email) {
instance.setEmail(email);
return this;
}
public Builder firstname(String firstname) {
instance.setFirstname(firstname);
return this;
}
public Builder lastname(String lastname) {
instance.setLastname(lastname);
return this;
}
public Builder title(String title) {
instance.setTitle(title);
return this;
}
public Builder username(String username) {
instance.setUsername(username);
return this;
}
public Builder password(String password) {
instance.setPassword(password);
return this;
}
public User build() {
return instance;
}
}
}

View File

@@ -0,0 +1,16 @@
package marketing.heyday.hartmann.fotodocumentation.core.query;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
public record Param(String name, Object value) {
}

View File

@@ -0,0 +1,68 @@
package marketing.heyday.hartmann.fotodocumentation.core.query;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.NoResultException;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Stateless
@SuppressWarnings("unchecked")
@PermitAll
public class QueryService {
private static final Log LOG = LogFactory.getLog(QueryService.class);
@PersistenceContext
private EntityManager eManager;
public <T> T callNamedQueryList(String namedQuery, Param... objects) {
Query query = eManager.createNamedQuery(namedQuery);
for (Param param : objects) {
query.setParameter(param.name(), param.value());
}
return (T) query.getResultList();
}
public <T> Optional<T> callNamedQuerySingleResult(String namedQuery, Param... params) {
return singleResult(eManager.createNamedQuery(namedQuery), Arrays.asList(params));
}
private <T> Optional<T> singleResult(Query query, Collection<Param> params) {
try {
for (Param param : params) {
query.setParameter(param.name(), param.value());
}
return Optional.ofNullable((T) query.getSingleResult());
} catch (NoResultException nre) {
LOG.debug("No entity found for query " + query + " with params " + params);
LOG.trace("NoResultException", nre);
return Optional.empty();
}
}
public int callNamedQueryUpdate(String namedQuery, Param... objects) {
Query query = eManager.createNamedQuery(namedQuery);
for (Param param : objects) {
query.setParameter(param.name(), param.value());
}
return query.executeUpdate();
}
}

View File

@@ -0,0 +1,53 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import jakarta.annotation.Resource;
import jakarta.ejb.EJB;
import jakarta.ejb.EJBContext;
import jakarta.ejb.SessionContext;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext;
import marketing.heyday.hartmann.fotodocumentation.core.query.QueryService;
import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
public abstract class AbstractService {
private static final Log LOG = LogFactory.getLog(AbstractService.class);
@Resource
protected EJBContext ejbContext;
@PersistenceContext
protected EntityManager entityManager;
@Resource
protected SessionContext sessionContext;
@EJB
protected QueryService queryService;
protected <T> StorageState delete(Class<T> type, Long id) {
try {
T entity = entityManager.getReference(type, id);
entityManager.remove(entity);
entityManager.flush();
return StorageState.OK;
} catch (EntityNotFoundException e) {
LOG.warn("Failed to delete entity " + type + " not found " + id, e);
ejbContext.setRollbackOnly();
return StorageState.NOT_FOUND;
}
}
}

View File

@@ -0,0 +1,138 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
import marketing.heyday.hartmann.fotodocumentation.core.utils.CalendarUtil;
import marketing.heyday.hartmann.fotodocumentation.core.utils.PdfUtils;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerListValue;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Stateless
@LocalBean
@PermitAll
public class CustomerPictureService extends AbstractService {
@Inject
private PdfUtils pdfUtils;
@Inject
private CalendarUtil calendarUtil;
public boolean addCustomerPicture(CustomerPictureValue customerPictureValue) {
Optional<Customer> customerOpt = queryService.callNamedQuerySingleResult(Customer.FIND_BY_NUMBER, new Param(Customer.PARAM_NUMBER, customerPictureValue.customerNumber()));
Customer customer = customerOpt.orElseGet(() -> new Customer.Builder().customerNumber(customerPictureValue.customerNumber()).name(customerPictureValue.pharmacyName())
.city(customerPictureValue.city()).zip(customerPictureValue.zip())
.build());
customer = entityManager.merge(customer);
Picture picture = new Picture.Builder().customer(customer).username(customerPictureValue.username())
.category(customerPictureValue.category())
.comment(customerPictureValue.comment())
.image(customerPictureValue.base64String())
.pictureDate(customerPictureValue.date()).build();
customer.getPictures().add(picture);
entityManager.persist(picture);
entityManager.flush();
return true;
}
// query = search for name, number and date
public List<CustomerListValue> getAll(String queryStr, String startsWith) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> criteriaQuery = builder.createQuery(Customer.class);
Root<Customer> customerRoot = criteriaQuery.from(Customer.class);
criteriaQuery = criteriaQuery.select(customerRoot).distinct(true);
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.isNotBlank(startsWith)) {
String param = startsWith.toLowerCase() + "%";
var pred = builder.like(builder.lower(customerRoot.get("name")), param);
predicates.add(pred);
}
if (StringUtils.isNotBlank(queryStr)) {
// check if it contains a date
Date date = calendarUtil.parse(queryStr);
if (date != null) {
Date startOfDay = calendarUtil.getStartOfDay(date);
Date endOfDay = calendarUtil.getEndOfDay(date);
Fetch<Customer, Picture> picturesFetch = customerRoot.fetch("pictures", JoinType.LEFT);
@SuppressWarnings("unchecked")
Join<Customer, Picture> pictures = (Join<Customer, Picture>) picturesFetch;
var predicateDate = builder.between(pictures.get("pictureDate"), startOfDay, endOfDay);
predicates.add(predicateDate);
} else {
String param = "%" + StringUtils.trimToEmpty(queryStr).toLowerCase() + "%";
var predicateName = builder.like(builder.lower(customerRoot.get("name")), param);
var predicateNr = builder.like(builder.lower(customerRoot.get("customerNumber")), param);
var pred = builder.or(predicateName, predicateNr);
predicates.add(pred);
}
}
if (predicates.size() == 1) {
criteriaQuery = criteriaQuery.where(predicates.getFirst());
} else if (predicates.size() > 1) {
criteriaQuery = criteriaQuery.where(builder.and(predicates.toArray(new Predicate[0])));
}
TypedQuery<Customer> typedQuery = entityManager.createQuery(criteriaQuery);
List<Customer> customers = typedQuery.getResultList();
customers.forEach(c -> c.getPictures().size());
return customers.parallelStream().map(CustomerListValue::builder).toList();
}
public CustomerValue get(Long id, String baseUrl) {
Customer customer = entityManager.find(Customer.class, id);
if (customer == null) {
return null;
}
return CustomerValue.builder(customer, baseUrl);
}
public byte[] getExport(Long id, Long pictureId) {
Customer customer = entityManager.find(Customer.class, id);
if (customer == null) {
return new byte[0];
}
List<Picture> pictures = customer.getPictures().stream().sorted((x, y) -> x.getPictureDate().compareTo(y.getPictureDate())).toList();
if (pictureId != null) {
Optional<Picture> pictureOpt = customer.getPictures().stream().filter(p -> p.getPictureId().equals(pictureId)).findFirst();
pictures = pictureOpt.map(Arrays::asList).orElse(pictures);
}
return pdfUtils.createPdf(customer, pictures);
}
}

View File

@@ -0,0 +1,101 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import static marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Date;
import java.util.Optional;
import io.jsonwebtoken.Claims;
import jakarta.inject.Inject;
import marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken;
import marketing.heyday.hartmann.fotodocumentation.core.model.Right;
import marketing.heyday.hartmann.fotodocumentation.core.model.User;
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
import marketing.heyday.hartmann.fotodocumentation.core.utils.JwtTokenUtil;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.TokenPairValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
public class JwtTokenService extends AbstractService {
private static final Long EXPIRED_DELTA = 30L * 24 * 60 * 60 * 1000;
@Inject
private JwtTokenUtil jwtTokenUtil;
public TokenPairValue generateTokenPair(User user, String deviceInfo, String ipAddress) {
String accessToken = jwtTokenUtil.generateAccessToken(
user.getUserId(),
user.getUsername(),
user.getRights().stream().map(Right::getCode).toList());
String refreshToken = jwtTokenUtil.generateRefreshToken(user.getUserId());
// Store refresh token (optional - for revocation support)
var refreshTokenEntity = new JwtRefreshToken.Builder()
.user(user)
.tokenHash(hashToken(refreshToken))
.deviceInfo(deviceInfo)
.ipAddress(ipAddress)
.issuedAt(new Date())
.expiresAt(new Date(System.currentTimeMillis() + EXPIRED_DELTA)).build();
entityManager.persist(refreshTokenEntity);
return new TokenPairValue(accessToken, refreshToken);
}
public String refreshAccessToken(String refreshToken) {
Claims claims = jwtTokenUtil.validateAndExtractClaims(refreshToken);
Long userId = Long.parseLong(claims.getSubject());
// Verify refresh token exists and not revoked
String tokenHash = hashToken(refreshToken);
Optional<JwtRefreshToken> tokenEntityOpt = queryService.callNamedQuerySingleResult(FIND_BY_HASH_REVOKE_NULL, new Param(PARAM_HASH, tokenHash));
if (tokenEntityOpt.isEmpty()) {
// FIXME: do error handling
}
var tokenEntity = tokenEntityOpt.get();
tokenEntity.setLastUsedAt(new Date());
entityManager.merge(tokenEntity);
User user = entityManager.find(User.class, userId);
return jwtTokenUtil.generateAccessToken(user.getUserId(), user.getUsername(), user.getRights().stream().map(Right::getCode).toList());
}
public void revokeRefreshToken(String refreshToken) {
String tokenHash = hashToken(refreshToken);
Optional<JwtRefreshToken> tokenEntityOpt = queryService.callNamedQuerySingleResult(FIND_BY_HASH, new Param(PARAM_HASH, tokenHash));
if (tokenEntityOpt.isPresent()) {
JwtRefreshToken tokenEntity = tokenEntityOpt.get();
tokenEntity.setRevokedAt(new Date());
entityManager.merge(tokenEntity);
}
}
public void revokeAllUserTokens(Long userId) {
queryService.callNamedQueryUpdate(REVOKE_ALL_USER, new Param(PARAM_DATE, new Date()), new Param(PARAM_USER_ID, userId));
}
private String hashToken(String token) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,39 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import java.util.Optional;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import marketing.heyday.hartmann.fotodocumentation.core.model.User;
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.TokenPairValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@Stateless
@LocalBean
public class LoginService extends AbstractService{
@Inject
private JwtTokenService jwtTokenService;
public TokenPairValue authenticateUser(String username, String deviceInfo, String ipAddress) {
// Get logged-in user from database
Optional<User> userOpt = queryService.callNamedQuerySingleResult(User.BY_USERNAME, new Param(User.PARAM_USERNAME, username));
if (userOpt.isEmpty()) {
// Should never happen
return null;
}
User user = userOpt.get();
return jwtTokenService.generateTokenPair(user, deviceInfo, ipAddress);
}
}

View File

@@ -0,0 +1,61 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.persistence.EntityNotFoundException;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
import marketing.heyday.hartmann.fotodocumentation.core.utils.ImageUtil;
import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Stateless
@LocalBean
@PermitAll
public class PictureService extends AbstractService {
private static final Log LOG = LogFactory.getLog(PictureService.class);
@Inject
private ImageUtil imageUtil;
public StorageState delete(Long id) {
return super.delete(Picture.class, id);
}
public StorageState updateEvaluationStatus(Long id, Integer value) {
try {
Picture entity = entityManager.getReference(Picture.class, id);
entity.setEvaluation(value);
entityManager.flush();
return StorageState.OK;
} catch (EntityNotFoundException e) {
LOG.warn("Failed to update evaluation value not found " + id, e);
ejbContext.setRollbackOnly();
return StorageState.NOT_FOUND;
}
}
public byte[] getImage(Long id, int size) {
try {
Picture entity = entityManager.getReference(Picture.class, id);
String base64 = entity.getImage();
return imageUtil.getImage(base64, size);
} catch (EntityNotFoundException e) {
LOG.warn("Failed to get image for id " + id, e);
ejbContext.setRollbackOnly();
return new byte[0];
}
}
}

View File

@@ -0,0 +1,127 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
import marketing.heyday.hartmann.fotodocumentation.core.utils.CalendarUtil;
import marketing.heyday.hartmann.fotodocumentation.core.utils.ExcelUtils;
import marketing.heyday.hartmann.fotodocumentation.core.utils.ZipExportUtils;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnaireCustomerListValue;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnaireCustomerValue;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 18 Feb 2026
*/
@Stateless
@LocalBean
@PermitAll
public class QuestionnaireCustomerService extends AbstractService {
@Inject
private ZipExportUtils zipExportUtils;
@Inject
private ExcelUtils excelUtils;
@Inject
private CalendarUtil calendarUtil;
// query = search for name, number and date
public List<QuestionnaireCustomerListValue> getAll(String queryStr, String startsWith) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<QuestionnaireCustomer> criteriaQuery = builder.createQuery(QuestionnaireCustomer.class);
Root<QuestionnaireCustomer> customerRoot = criteriaQuery.from(QuestionnaireCustomer.class);
criteriaQuery = criteriaQuery.select(customerRoot).distinct(true);
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.isNotBlank(startsWith)) {
String param = startsWith.toLowerCase() + "%";
var pred = builder.like(builder.lower(customerRoot.get("name")), param);
predicates.add(pred);
}
if (StringUtils.isNotBlank(queryStr)) {
// check if it contains a date
Date date = calendarUtil.parse(queryStr);
if (date != null) {
Date startOfDay = calendarUtil.getStartOfDay(date);
Date endOfDay = calendarUtil.getEndOfDay(date);
Fetch<QuestionnaireCustomer, Questionnaire> picturesFetch = customerRoot.fetch("questionnaires", JoinType.LEFT);
@SuppressWarnings("unchecked")
Join<QuestionnaireCustomer, Questionnaire> pictures = (Join<QuestionnaireCustomer, Questionnaire>) picturesFetch;
var predicateDate = builder.between(pictures.get("questionnaireDate"), startOfDay, endOfDay);
predicates.add(predicateDate);
} else {
String param = "%" + StringUtils.trimToEmpty(queryStr).toLowerCase() + "%";
var predicateName = builder.like(builder.lower(customerRoot.get("name")), param);
var predicateNr = builder.like(builder.lower(customerRoot.get("customerNumber")), param);
var pred = builder.or(predicateName, predicateNr);
predicates.add(pred);
}
}
if (predicates.size() == 1) {
criteriaQuery = criteriaQuery.where(predicates.getFirst());
} else if (predicates.size() > 1) {
criteriaQuery = criteriaQuery.where(builder.and(predicates.toArray(new Predicate[0])));
}
TypedQuery<QuestionnaireCustomer> typedQuery = entityManager.createQuery(criteriaQuery);
List<QuestionnaireCustomer> customers = typedQuery.getResultList();
customers.forEach(c -> c.getQuestionnaires().size());
return customers.parallelStream().map(QuestionnaireCustomerListValue::builder).toList();
}
public QuestionnaireCustomerValue get(Long id) {
QuestionnaireCustomer customer = entityManager.find(QuestionnaireCustomer.class, id);
if (customer == null) {
return null;
}
return QuestionnaireCustomerValue.builder(customer);
}
public Optional<byte[]> getExport() {
List<QuestionnaireCustomer> customers = queryService.callNamedQueryList(QuestionnaireCustomer.FIND_ALL);
return zipExportUtils.getExport(customers);
}
public Optional<byte[]> getExport(Long id, Long questionnaireId) {
QuestionnaireCustomer customer = entityManager.find(QuestionnaireCustomer.class, id);
if (customer == null) {
return Optional.empty();
}
List<Questionnaire> questionnaires = customer.getQuestionnaires().stream().sorted((x, y) -> x.getQuestionnaireDate().compareTo(y.getQuestionnaireDate())).toList();
if (questionnaireId != null) {
Optional<Questionnaire> pictureOpt = customer.getQuestionnaires().stream().filter(p -> p.getQuestionnaireId().equals(questionnaireId)).findFirst();
questionnaires = pictureOpt.map(Arrays::asList).orElse(questionnaires);
}
return excelUtils.create(customer, questionnaires);
}
}

View File

@@ -0,0 +1,57 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import static marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer.*;
import java.util.Optional;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
import marketing.heyday.hartmann.fotodocumentation.core.utils.QuestionnaireUploadJsonParser;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 18 Feb 2026
*/
@Stateless
@LocalBean
@PermitAll
public class QuestionnairePublishService extends AbstractService {
public boolean publish(String body) {
var parserOpt = QuestionnaireUploadJsonParser.builder(body);
if (parserOpt.isEmpty()) {
return false;
}
var parser = parserOpt.get();
Optional<QuestionnaireCustomer> customerOpt = queryService.callNamedQuerySingleResult(FIND_BY_NUMBER, new Param(PARAM_NUMBER, parser.customerNumber()));
var customer = customerOpt.orElseGet(() -> new QuestionnaireCustomer.Builder().customerNumber(parser.customerNumber()).name(parser.pharmacyName())
.city(parser.city()).zip(parser.zip())
.build());
customer = entityManager.merge(customer);
var questionnaire = new Questionnaire.Builder().customer(customer).username(parser.username())
.category("")// FIXME: remove category
.comment(parser.comment())
.customer(customer)
.questionnaireDate(parser.date())
.questions(parser.questionnair())
.username(parser.username())
.build();
customer.getQuestionnaires().add(questionnaire);
entityManager.persist(questionnaire);
entityManager.flush();
return true;
}
}

View File

@@ -0,0 +1,44 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityNotFoundException;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Stateless
@LocalBean
@PermitAll
public class QuestionnaireService extends AbstractService {
private static final Log LOG = LogFactory.getLog(QuestionnaireService.class);
public StorageState delete(Long id) {
return super.delete(Questionnaire.class, id);
}
public StorageState updateEvaluationStatus(Long id, Integer value) {
try {
Questionnaire entity = entityManager.getReference(Questionnaire.class, id);
entity.setEvaluation(value);
entityManager.flush();
return StorageState.OK;
} catch (EntityNotFoundException e) {
LOG.warn("Failed to update evaluation value not found " + id, e);
ejbContext.setRollbackOnly();
return StorageState.NOT_FOUND;
}
}
}

View File

@@ -0,0 +1,58 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 3 Feb 2026
*/
public class CalendarUtil {
private static final Log LOG = LogFactory.getLog(CalendarUtil.class);
private static final int HOUR_OF_DAY = 23;
private static final int MINUTE = 59;
private static final int SECOND = 59;
private static final int MILLISECOND = 999;
public Date parse(String query) {
try {
return DateUtils.parseDate(query, Locale.GERMAN, "dd.MM.yyyy", "d.M.yyyy", "dd.MM.yy", "d. MMMM yyyy", "dd MMMM yyyy", "dd-MM-yyyy");
} catch (ParseException e) {
LOG.trace("Failed to find date in queryStr " + query);
return null;
}
}
public Date getStartOfDay(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
return cal.getTime();
}
public Date getEndOfDay(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.set(Calendar.HOUR_OF_DAY, HOUR_OF_DAY);
cal.set(Calendar.MINUTE, MINUTE);
cal.set(Calendar.SECOND, SECOND);
cal.set(Calendar.MILLISECOND, MILLISECOND);
return cal.getTime();
}
}

View File

@@ -0,0 +1,20 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 3 Feb 2026
*/
public class EvaluationUtil {
private static final int MIN_VALUE = 1;
private static final int MAX_VALUE = 3;
public boolean isInValid(Integer value) {
return (value == null || value < MIN_VALUE || value > MAX_VALUE);
}
}

View File

@@ -0,0 +1,201 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
import marketing.heyday.hartmann.fotodocumentation.core.utils.QuestionnaireJsonParser.MatrixAnswer;
import marketing.heyday.hartmann.fotodocumentation.core.utils.QuestionnaireJsonParser.QuestionJsonObj;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 2 Feb 2026
*/
@SuppressWarnings({ "java:S818", "squid:S818", "squid:S109" })
public class ExcelUtils {
private static final Log LOG = LogFactory.getLog(ExcelUtils.class);
public Optional<byte[]> create(QuestionnaireCustomer customer, List<Questionnaire> questionnaires) {
LOG.debug("Create excel file for customer " + customer);
if(customer == null || questionnaires.isEmpty()) {
return Optional.empty();
}
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); XSSFWorkbook workbook = new XSSFWorkbook()) {
for (var questionnaire : questionnaires) {
writeSheet(workbook, customer, questionnaire);
}
workbook.write(bos);
return Optional.of(bos.toByteArray());
} catch (IOException e) {
LOG.debug("Failed to export countries", e);
return Optional.empty();
}
}
private void writeSheet(XSSFWorkbook workbook, QuestionnaireCustomer customer, Questionnaire questionnaire) throws IOException {
XSSFSheet sheet = workbook.createSheet();
int rowIndex = writeCustomerData(workbook, sheet, customer, questionnaire);
writeQuestionData(sheet, questionnaire, rowIndex);
for (int i = 0; i < 10; i++) {
sheet.autoSizeColumn(i);
}
}
private int writeCustomerData(XSSFWorkbook workbook, XSSFSheet sheet, QuestionnaireCustomer customer, Questionnaire questionnaire) {
int rowIndex = 0;
XSSFRow row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(customer.getName());
row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(customer.getCustomerNumber());
row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(customer.getCity());
row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(customer.getZip());
row = sheet.createRow(rowIndex++);
CreationHelper createHelper = workbook.getCreationHelper();
short format = createHelper.createDataFormat().getFormat("dd.MM.yyyy h:mm");
CellStyle cellStyle = workbook.createCellStyle();
cellStyle.setDataFormat(format);
var dateCell = row.createCell(0);
dateCell.setCellStyle(cellStyle);
dateCell.setCellValue(questionnaire.getQuestionnaireDate());
rowIndex++;
rowIndex++;
return rowIndex;
}
private void writeQuestionData(XSSFSheet sheet, Questionnaire questionnaire, int rowIndex) throws IOException {
QuestionnaireJsonParser parser = new QuestionnaireJsonParser();
var success = parser.parse(questionnaire.getQuestions());
if (!success) {
throw new IOException("Failed to parse json");
}
var questions = parser.getQuestions();
for (var question : questions) {
XSSFRow row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(question.title());
switch (question.type()) {
case "singleChoice":
writeSingle(row, question);
break;
case "multiplChoice":
int amount1 = writeMultiple(sheet, row, question);
rowIndex = rowIndex + amount1;
break;
case "number":
writeNumber(row, question);
break;
case "matrix":
int amount2 = writeMatrix(sheet, row, question);
rowIndex = rowIndex + amount2;
break;
case "freeText":
writeFreeText(row, question);
break;
default:
break;
}
rowIndex++;
}
}
private void writeSingle(XSSFRow row, QuestionJsonObj question) {
row.createCell(1).setCellValue(question.getSingleAnswer());
}
private int writeMultiple(XSSFSheet sheet, XSSFRow row, QuestionJsonObj question) {
var answers = question.getMultiAnswer();
int count = 0;
for (var answer : answers) {
if (count == 0) {
row.createCell(1).setCellValue(answer);
} else {
sheet.createRow(row.getRowNum() + count).createCell(1).setCellValue(answer);
}
count++;
}
return count - 1;
}
private void writeNumber(XSSFRow row, QuestionJsonObj question) {
var cell = row.createCell(1);
cell.setCellType(CellType.NUMERIC);
cell.setCellValue(question.getNumberAnswer());
}
private void writeFreeText(XSSFRow row, QuestionJsonObj question) {
row.createCell(1).setCellValue(question.getFreeText());
}
private int writeMatrix(XSSFSheet sheet, XSSFRow row, QuestionJsonObj question) {
var questions = question.getMatrixAnswer();
int count = 2;
var answerOpts = questions.getFirst().answers();
answerOpts.sort((i, j) -> i.answer().compareToIgnoreCase(j.answer()));
var headerRow = sheet.createRow(row.getRowNum() + count);
// create header
for (int i = 0; i < answerOpts.size(); i++) {
var answer = answerOpts.get(i);
headerRow.createCell(i + 1).setCellValue(answer.answer());
}
count++;
for (var subQuestion : questions) {
var questionRow = sheet.createRow(row.getRowNum() + count);
questionRow.createCell(0).setCellValue(subQuestion.title());
for (var answer : subQuestion.answers()) {
if (!answer.selected()) {
continue;
}
int index = getMatrixAnswerIndex(answerOpts, answer);
var cell = questionRow.createCell(1 + index);
cell.setCellType(CellType.BOOLEAN);
cell.setCellValue(answer.selected());
}
count++;
}
return count;
}
private int getMatrixAnswerIndex(List<MatrixAnswer> answerOpts, MatrixAnswer answer) {
int index = 0;
for (int i = 0; i < answerOpts.size(); i++) {
if (answer.answer().equalsIgnoreCase(answerOpts.get(i).answer())) {
index = i;
}
}
return index;
}
}

View File

@@ -0,0 +1,123 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import javax.imageio.ImageIO;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifIFD0Directory;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 6 Feb 2026
*/
public interface ImageHandler {
Log LOG = LogFactory.getLog(ImageHandler.class);
/**
* Reads image bytes and returns a BufferedImage with correct EXIF orientation applied.
*/
default BufferedImage readImageWithCorrectOrientation(byte[] imageBytes) throws IOException {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
if (image == null) {
throw new IOException("Failed to read image from byte array");
}
int orientation = getExifOrientation(imageBytes);
return applyOrientation(image, orientation);
}
default int getExifOrientation(byte[] imageBytes) {
try {
Metadata metadata = ImageMetadataReader.readMetadata(new ByteArrayInputStream(imageBytes));
ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
return directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
}
} catch (ImageProcessingException | IOException | MetadataException e) {
LOG.debug("Could not read EXIF orientation: " + e.getMessage());
}
return 1;
}
default BufferedImage applyOrientation(BufferedImage image, int orientation) {
int width = image.getWidth();
int height = image.getHeight();
AffineTransform transform = new AffineTransform();
int newWidth = width;
int newHeight = height;
switch (orientation) {
case 1 -> {
return image;
}
case 2 -> {
transform.scale(-1.0, 1.0);
transform.translate(-width, 0);
}
case 3 -> {
transform.translate(width, height);
transform.rotate(Math.PI);
}
case 4 -> {
transform.scale(1.0, -1.0);
transform.translate(0, -height);
}
case 5 -> {
newWidth = height;
newHeight = width;
transform.rotate(-Math.PI / 2);
transform.scale(-1.0, 1.0);
}
case 6 -> {
newWidth = height;
newHeight = width;
transform.translate(height, 0);
transform.rotate(Math.PI / 2);
}
case 7 -> {
newWidth = height;
newHeight = width;
transform.scale(-1.0, 1.0);
transform.translate(-height, 0);
transform.translate(0, width);
transform.rotate(3 * Math.PI / 2);
}
case 8 -> {
newWidth = height;
newHeight = width;
transform.translate(0, width);
transform.rotate(-Math.PI / 2);
}
default -> {
return image;
}
}
BufferedImage rotated = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = rotated.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.drawImage(image, transform, null);
g2d.dispose();
return rotated;
}
}

View File

@@ -0,0 +1,141 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 2 Feb 2026
*/
public class ImageUtil implements ImageHandler {
private static final Log LOG = LogFactory.getLog(ImageUtil.class);
private static final int NORMAL_MAX_WIDTH = 1200;
private static final float NORMAL_QUALITY = 0.75F;
private static final int THUMBNAIL_MAX_WIDTH = 200;
private static final float THUMBNAIL_QUALITY = 0.6F;
/**
* size 1 is original
* size 2 is normal (web)
* size 3 is thumbnail
*/
public byte[] getImage(String base64, int size) {
byte[] original = Base64.getDecoder().decode(base64);
return switch (size) {
case 1 -> applyExifOrientation(original);
case 2 -> normal(original);
case 3 -> thumbnail(original);
default -> applyExifOrientation(original);
};
}
private byte[] normal(byte[] original) {
return resize(original, NORMAL_MAX_WIDTH, NORMAL_QUALITY);
}
private byte[] thumbnail(byte[] original) {
return resize(original, THUMBNAIL_MAX_WIDTH, THUMBNAIL_QUALITY);
}
private byte[] resize(byte[] original, int maxWidth, float quality) {
try {
int orientation = getExifOrientation(original);
BufferedImage image = ImageIO.read(new ByteArrayInputStream(original));
if (image == null) {
LOG.error("Failed to read image from byte array");
return original;
}
// For rotated images (orientation 5, 6, 7, 8), width and height are swapped
int effectiveWidth = (orientation >= 5 && orientation <= 8) ? image.getHeight() : image.getWidth();
// If no resize needed and no orientation fix needed, return original
if (effectiveWidth <= maxWidth && orientation == 1) {
return original;
}
image = applyOrientation(image, orientation);
int originalWidth = image.getWidth();
if (originalWidth <= maxWidth) {
return writeJpeg(image, quality);
}
double scale = (double) maxWidth / originalWidth;
int targetWidth = maxWidth;
int originalHeight = image.getHeight();
int targetHeight = (int) (originalHeight * scale);
BufferedImage resized = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = resized.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.drawImage(image, 0, 0, targetWidth, targetHeight, null);
g2d.dispose();
return writeJpeg(resized, quality);
} catch (IOException e) {
LOG.error("Failed to resize image", e);
return original;
}
}
private byte[] applyExifOrientation(byte[] original) {
try {
int orientation = getExifOrientation(original);
if (orientation == 1) {
return original;
}
BufferedImage image = ImageIO.read(new ByteArrayInputStream(original));
if (image == null) {
return original;
}
BufferedImage rotated = applyOrientation(image, orientation);
return writeJpeg(rotated, 1.0F);
} catch (IOException e) {
LOG.error("Failed to apply EXIF orientation", e);
return original;
}
}
private byte[] writeJpeg(BufferedImage image, float quality) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(quality);
try (ImageOutputStream ios = ImageIO.createImageOutputStream(output)) {
writer.setOutput(ios);
writer.write(null, new IIOImage(image, null, null), param);
} finally {
writer.dispose();
}
return output.toByteArray();
}
}

View File

@@ -0,0 +1,57 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.ValidationMessage;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 20 Feb 2026
*/
public class JsonSchemaValidator {
private static final Log LOG = LogFactory.getLog(JsonSchemaValidator.class);
public ValidationReply validate(String schemaPath, String jsonData) {
try {
JsonSchema schema = getJsonSchema(schemaPath);
JsonNode node = getJsonNode(jsonData);
Set<ValidationMessage> errors = schema.validate(node);
if (!errors.isEmpty()) {
LOG.error("Failed to validate json to schema " + schemaPath);
errors.forEach(LOG::error);
}
return new ValidationReply(errors.isEmpty(), errors);
} catch (IOException e) {
LOG.error(e.getMessage(), e);
return new ValidationReply(false, new HashSet<>());
}
}
protected JsonSchema getJsonSchema(String name) throws IOException {
JsonSchemaFactory factory = new JsonSchemaFactory();
try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(name);) {
return factory.getSchema(input);
}
}
protected JsonNode getJsonNode(String content) throws IOException {
return new ObjectMapper().readTree(content);
}
}

View File

@@ -0,0 +1,124 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import jakarta.annotation.PostConstruct;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@Stateless
@LocalBean
public class JwtTokenUtil {
private static final Log LOG = LogFactory.getLog(JwtTokenUtil.class);
private static final long ACCESS_TOKEN_VALIDITY = 60 * 60 * 1000L; // 1 hour
private static final long REFRESH_TOKEN_VALIDITY = 30 * 24 * 60 * 60 * 1000L; // 30 days
private static final long TEMP_2FA_TOKEN_VALIDITY = 5 * 60 * 1000L; // 5 minutes
private static final String ISSUER = "foto-jwt-issuer";
private static final String AUDIENCE = "foto-api";
private PrivateKey privateKey;
private PublicKey publicKey;
@PostConstruct
public void init() {
// Load key from wildfly system property
try {
String pem = System.getProperty("jwt.secret.key");
pem = pem.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(pem);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
privateKey = KeyFactory.getInstance("RSA").generatePrivate(spec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
LOG.error("Failed to load JWT PrivateKey " + e.getMessage(), e);
}
}
public String generateAccessToken(Long userId, String username, List<String> groups) {
return Jwts.builder()
.issuer(ISSUER)
.audience().add(AUDIENCE).and()
.subject(userId.toString())
.claim("username", username)
.claim("type", "access")
.claim("groups", groups)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY))
.signWith(privateKey, Jwts.SIG.RS256)
.compact();
}
public String generateRefreshToken(Long userId) {
return Jwts.builder()
.issuer(ISSUER)
.audience().add(AUDIENCE).and()
.subject(userId.toString())
.claim("type", "refresh")
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_VALIDITY))
.signWith(privateKey, Jwts.SIG.RS256)
.compact();
}
public String generateTemp2FAToken(Long userId) {
return Jwts.builder()
.issuer(ISSUER)
.audience().add(AUDIENCE).and()
.subject(userId.toString())
.claim("type", "temp_2fa")
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + TEMP_2FA_TOKEN_VALIDITY))
.signWith(privateKey, Jwts.SIG.RS256)
.compact();
}
public Claims validateAndExtractClaims(String token) {
return Jwts.parser()
.verifyWith(publicKey) // FIXME: not working need public key that we currently didn't load
.build()
.parseUnsecuredClaims(token)
.getPayload();
}
public Long extractUserId(String token) {
Claims claims = validateAndExtractClaims(token);
return Long.parseLong(claims.getSubject());
}
public boolean isTokenExpired(String token) {
try {
Claims claims = validateAndExtractClaims(token);
return claims.getExpiration().before(new Date());
} catch (JwtException e) {
LOG.warn("Failed to get expiration date from token " + e.getMessage());
return true;
}
}
}

View File

@@ -0,0 +1,110 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.Optional;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wildfly.security.auth.principal.NamePrincipal;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.auth.server.SecurityDomain;
import org.wildfly.security.auth.server.SecurityIdentity;
import org.wildfly.security.evidence.PasswordGuessEvidence;
import jakarta.servlet.http.HttpServletRequest;
import javaslang.Tuple;
import javaslang.Tuple2;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 20 Jan 2026
*/
public class LoginUtils {
private static final Log LOG = LogFactory.getLog(LoginUtils.class);
private static final int BASIC_HEADER_SIZE = 6;
private static final int USERPASS_LENGTH = 1;
public Optional<SecurityIdentity> authenticate(HttpServletRequest httpServletRequest) {
Tuple2<String, String> userPass = extractUsernamePassword(httpServletRequest);
if (userPass._1.isBlank() || userPass._2.isBlank()) {
return Optional.empty();
}
return authenticate(userPass._1, userPass._2);
}
private Optional<SecurityIdentity> authenticate(String username, String password) {
try {
LOG.debug("Login with username & password " + username);
Principal principal = new NamePrincipal(username);
PasswordGuessEvidence evidence = new PasswordGuessEvidence(password.toCharArray());
SecurityDomain sd = SecurityDomain.getCurrent();
SecurityIdentity identity = sd.authenticate(principal, evidence);
LOG.debug("Login identity: " + identity);
return Optional.ofNullable(identity);
} catch (RealmUnavailableException | SecurityException e) {
LOG.warn("Failed to authenticate user " + e.getMessage(), e);
return Optional.empty();
}
}
private Tuple2<String, String> extractUsernamePassword(HttpServletRequest httpServletRequest) {
Optional<String[]> userPassOptional = extractAuthHeader(httpServletRequest);
if (userPassOptional.isPresent() && userPassOptional.get().length >= USERPASS_LENGTH) {
String[] userpass = userPassOptional.get();
String username = userpass[0];
String password = userpass.length > USERPASS_LENGTH ? userpass[1] : "";
return Tuple.of(username, password);
}
return Tuple.of("", "");
}
private Optional<String[]> extractAuthHeader(HttpServletRequest httpServletRequest) {
Optional<String[]> retVal = Optional.empty();
String authorization = httpServletRequest.getHeader("Authorization");
if (authorization != null && StringUtils.length(authorization) > BASIC_HEADER_SIZE) {
authorization = authorization.substring(BASIC_HEADER_SIZE);
String decoded = StringUtils.toEncodedString(Base64.decodeBase64(authorization), Charset.forName("utf-8"));
retVal = Optional.of(decoded.split(":"));
}
return retVal;
}
/**
* Extract device information from User-Agent header
*
* @param request HTTP servlet request
* @return Device/browser information
*/
public String extractDeviceInfo(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
return userAgent != null ? userAgent : "Unknown";
}
/**
* Extract client IP address, considering proxies
*
* @param request HTTP servlet request
* @return Client IP address
*/
public String extractIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
// X-Forwarded-For can contain multiple IPs, take the first one
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}

View File

@@ -0,0 +1,308 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.List;
import java.util.Locale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 2 Feb 2026
*/
@SuppressWarnings({ "java:S818", "squid:S818", "squid:S109" })
public class PdfUtils implements ImageHandler {
private static final Log LOG = LogFactory.getLog(PdfUtils.class);
private static final String FONT_PANTON_REGULAR = "fonts/Panton-Regular.ttf";
private static final String FONT_PANTON_BOLD = "fonts/Panton-Bold.ttf";
private static final Color COLOR_CUSTOMER_NAME = new Color(0x00, 0x45, 0xFF);
private static final Color COLOR_DATE = new Color(0x00, 0x16, 0x89);
private static final Color COLOR_TEXT_GRAY = new Color(0x2F, 0x2F, 0x2F);
private static final Color COLOR_GREEN = new Color(76, 175, 80);
private static final Color COLOR_YELLOW = new Color(255, 193, 7);
private static final Color COLOR_RED = new Color(244, 67, 54);
private static final Color COLOR_HIGHLIGHT = new Color(41, 98, 175);
private static final float PAGE_MARGIN = 40F;
private static final float CIRCLE_RADIUS = 8F;
private static final float HIGHLIGHT_RADIUS = 12F;
private static final float CIRCLE_SPACING = 30F;
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.GERMAN);
public byte[] createPdf(Customer customer, List<Picture> pictures) {
try (PDDocument document = new PDDocument()) {
PDFont fontBold = loadFont(document, FONT_PANTON_BOLD, Standard14Fonts.FontName.HELVETICA_BOLD);
PDFont fontRegular = loadFont(document, FONT_PANTON_REGULAR, Standard14Fonts.FontName.HELVETICA);
boolean firstPage = true;
for (Picture picture : pictures) {
PDPage page = new PDPage(new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()));
document.addPage(page);
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
float contentWidth = pageWidth - 2 * PAGE_MARGIN;
float imageWidth = contentWidth * 0.75F;
float metadataWidth = contentWidth * 0.25F;
try (PDPageContentStream cs = new PDPageContentStream(document, page)) {
float yPosition = pageHeight - 50F;
// Customer name on the first page
if (firstPage) {
cs.setFont(fontBold, 50);
cs.setNonStrokingColor(COLOR_CUSTOMER_NAME);
cs.beginText();
cs.newLineAtOffset(PAGE_MARGIN, yPosition);
cs.showText(nullSafe(customer.getName()));
cs.endText();
yPosition -= 60F;
firstPage = false;
}
// Left side: image (75% of content width)
float imageX = PAGE_MARGIN;
float imageY = yPosition;
float imageMaxWidth = imageWidth - 10F;
float imageMaxHeight = pageHeight - 2 * PAGE_MARGIN - 40F;
if (picture.getImage() != null) {
try {
byte[] imageBytes = Base64.getDecoder().decode(picture.getImage());
BufferedImage correctedImage = readImageWithCorrectOrientation(imageBytes);
PDImageXObject pdImage = LosslessFactory.createFromImage(document, correctedImage);
float scale = Math.min(imageMaxWidth / pdImage.getWidth(), imageMaxHeight / pdImage.getHeight());
float drawWidth = pdImage.getWidth() * scale;
float drawHeight = pdImage.getHeight() * scale;
cs.drawImage(pdImage, imageX, imageY - drawHeight, drawWidth, drawHeight);
} catch (Exception e) {
LOG.error("Failed to embed image in PDF", e);
}
}
// Right side: metadata (25% of content width, top-aligned with image)
float rightX = PAGE_MARGIN + imageWidth + 10F;
float rightY = imageY - 10F;
// Date (bold, size 10 - matching labels)
String dateStr = picture.getPictureDate() != null ? (DATE_FORMAT.format(picture.getPictureDate()) + " UHR") : "";
cs.setFont(fontBold, 10);
cs.setNonStrokingColor(COLOR_DATE);
cs.beginText();
cs.newLineAtOffset(rightX, rightY);
cs.showText(dateStr);
cs.endText();
rightY -= 24F;
// Customer number
rightY = drawLabel(cs, fontBold, "KUNDENNUMMER", rightX, rightY);
rightY = drawValue(cs, fontRegular, nullSafe(customer.getCustomerNumber()), rightX, rightY);
rightY -= 10F;
// ZIP
rightY = drawLabel(cs, fontBold, "PLZ", rightX, rightY);
rightY = drawValue(cs, fontRegular, nullSafe(customer.getZip()), rightX, rightY);
rightY -= 10F;
// City
rightY = drawLabel(cs, fontBold, "ORT", rightX, rightY);
rightY = drawValue(cs, fontRegular, nullSafe(customer.getCity()), rightX, rightY);
rightY -= 10F;
// Evaluation card with circles
float evaluationY = rightY;
drawEvaluationCard(cs, fontBold, rightX, evaluationY, picture.getEvaluation());
rightY -= 80F;
// Comment
rightY = drawLabel(cs, fontBold, "KOMMENTAR", rightX, rightY);
String remainingComment = drawWrappedText(cs, fontRegular, nullSafe(picture.getComment()), rightX, rightY, metadataWidth - 20f, 50F);
// Continue comment on additional pages if needed
while (remainingComment != null && !remainingComment.isEmpty()) {
PDPage continuationPage = new PDPage(new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()));
document.addPage(continuationPage);
try (PDPageContentStream continuationCs = new PDPageContentStream(document, continuationPage)) {
float continuationY = continuationPage.getMediaBox().getHeight() - PAGE_MARGIN;
float continuationWidth = continuationPage.getMediaBox().getWidth() - 2 * PAGE_MARGIN;
continuationY = drawLabel(continuationCs, fontBold, "KOMMENTAR (FORTSETZUNG)", PAGE_MARGIN, continuationY);
remainingComment = drawWrappedText(continuationCs, fontRegular, remainingComment, PAGE_MARGIN, continuationY, continuationWidth, 50F);
}
}
}
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
document.save(output);
return output.toByteArray();
} catch (IOException e) {
LOG.error("Failed to create PDF", e);
return new byte[0];
}
}
private PDFont loadFont(PDDocument document, String resourcePath, Standard14Fonts.FontName fallback) {
try (InputStream fontStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath)) {
if (fontStream != null) {
return PDType0Font.load(document, fontStream);
}
} catch (IOException e) {
LOG.warn("Failed to load font " + resourcePath + ", using fallback", e);
}
LOG.info("Font " + resourcePath + " not found, using fallback " + fallback.getName());
return new PDType1Font(fallback);
}
private float drawLabel(PDPageContentStream cs, PDFont font, String label, float x, float y) throws IOException {
cs.setFont(font, 10);
cs.setNonStrokingColor(COLOR_CUSTOMER_NAME);
cs.beginText();
cs.newLineAtOffset(x, y);
cs.showText(label);
cs.endText();
return y - 14F;
}
private float drawValue(PDPageContentStream cs, PDFont font, String value, float x, float y) throws IOException {
cs.setFont(font, 10);
cs.setNonStrokingColor(COLOR_TEXT_GRAY);
cs.beginText();
cs.newLineAtOffset(x, y);
cs.showText(value);
cs.endText();
return y - 14F;
}
private String drawWrappedText(PDPageContentStream cs, PDFont font, String text, float x, float y, float maxWidth, float minY) throws IOException {
if (text == null || text.isEmpty()) {
return null;
}
cs.setFont(font, 10);
cs.setNonStrokingColor(COLOR_TEXT_GRAY);
String[] words = text.split("\\s+");
StringBuilder line = new StringBuilder();
float currentY = y;
int wordIndex = 0;
for (wordIndex = 0; wordIndex < words.length; wordIndex++) {
String word = words[wordIndex];
String testLine = line.isEmpty() ? word : (line + " " + word);
float textWidth = font.getStringWidth(testLine) / 1000F * 10F;
if (textWidth > maxWidth && !line.isEmpty()) {
// Check if we have room for this line
if (currentY < minY) {
// Return remaining text starting from current word
StringBuilder remaining = new StringBuilder(line);
for (int i = wordIndex; i < words.length; i++) {
if (!remaining.isEmpty()) {
remaining.append(" ");
}
remaining.append(words[i]);
}
return remaining.toString();
}
cs.beginText();
cs.newLineAtOffset(x, currentY);
cs.showText(line.toString());
cs.endText();
currentY -= 14F;
line = new StringBuilder(word);
} else {
line = new StringBuilder(testLine);
}
}
if (!line.isEmpty()) {
if (currentY < minY) {
return line.toString();
}
cs.beginText();
cs.newLineAtOffset(x, currentY);
cs.showText(line.toString());
cs.endText();
}
return null;
}
private void drawEvaluationCard(PDPageContentStream cs, PDFont fontBold, float x, float y, Integer evaluation) throws IOException {
int eval = evaluation != null ? evaluation : 0;
Color[] colors = { COLOR_GREEN, COLOR_YELLOW, COLOR_RED };
float labelHeight = 14F;
// Draw "BEWERTUNG" label above circles
float labelY = y - labelHeight;
cs.setFont(fontBold, 10);
cs.setNonStrokingColor(COLOR_CUSTOMER_NAME);
cs.beginText();
cs.newLineAtOffset(x, labelY);
cs.showText("BEWERTUNG");
cs.endText();
// Draw circles below the label
float circleY = labelY - HIGHLIGHT_RADIUS - 10F;
for (int i = 0; i < 3; i++) {
float cx = x + CIRCLE_RADIUS + i * CIRCLE_SPACING;
// Highlight circle if this matches the evaluation (1=green, 2=yellow, 3=red)
if (eval == i + 1) {
cs.setStrokingColor(COLOR_HIGHLIGHT);
cs.setLineWidth(2F);
drawCircle(cs, cx, circleY, HIGHLIGHT_RADIUS);
cs.stroke();
}
// Filled color circle
cs.setNonStrokingColor(colors[i]);
drawCircle(cs, cx, circleY, CIRCLE_RADIUS);
cs.fill();
}
}
private void drawCircle(PDPageContentStream cs, float cx, float cy, float r) throws IOException {
float k = 0.5523f; // Bezier approximation for circle
cs.moveTo(cx - r, cy);
cs.curveTo(cx - r, cy + r * k, cx - r * k, cy + r, cx, cy + r);
cs.curveTo(cx + r * k, cy + r, cx + r, cy + r * k, cx + r, cy);
cs.curveTo(cx + r, cy - r * k, cx + r * k, cy - r, cx, cy - r);
cs.curveTo(cx - r * k, cy - r, cx - r, cy - r * k, cx - r, cy);
cs.closePath();
}
private String nullSafe(String value) {
return value != null ? value : "";
}
}

View File

@@ -0,0 +1,175 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import jakarta.json.*;
import jakarta.json.JsonValue.ValueType;
import jakarta.json.stream.JsonParsingException;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 20 Feb 2026
*/
public class QuestionnaireJsonParser {
private static final Log LOG = LogFactory.getLog(QuestionnaireJsonParser.class);
private final List<QuestionJsonObj> questions = new ArrayList<>();
public List<QuestionJsonObj> getQuestions() {
return questions;
}
public boolean parse(String body) {
questions.clear();
if (body == null) {
return false;
}
try (var stringReader = new StringReader(body); JsonReader reader = Json.createReader(stringReader);) {
JsonArray root = reader.readArray();
for (var object : root) {
var question = object.asJsonObject();
String id = question.getString("id");
String title = question.getString("title");
int order = question.getInt("order");
String type = question.getString("type");
String data = toData(question.getJsonObject("data"));
questions.add(new QuestionJsonObj(id, title, order, type, data));
}
return true;
} catch (IOException | JsonParsingException | NullPointerException ioe) {
LOG.warn("Failed to parse json " + ioe.getMessage(), ioe);
return false;
}
}
private String toData(JsonObject obj) throws IOException {
try (StringWriter writer = new StringWriter(); JsonWriter jsonWriter = Json.createWriter(writer);) {
jsonWriter.writeObject(obj);
jsonWriter.close();
return writer.toString();
}
}
public record QuestionJsonObj(String id, String title, int order, String type, String data) {
public List<MatrixQuestion> getMatrixAnswer() {
return getValue("questions", () -> null, matrixQuestions -> {
var retVal = new ArrayList<MatrixQuestion>();
for (var question : matrixQuestions) {
var questionObj = question.asJsonObject();
var id = questionObj.getString("id");
var title = questionObj.getString("title");
var order = questionObj.getInt("order");
var answerList = new ArrayList<MatrixAnswer>();
var answers = questionObj.getJsonArray("answers");
for (var answer : answers) {
var answerObj = answer.asJsonObject();
var answerId = answerObj.getString("id");
var answerStr = answerObj.getString("answer");
var selected = answerObj.getBoolean("selected");
answerList.add(new MatrixAnswer(answerId, answerStr, selected));
}
retVal.add(new MatrixQuestion(id, title, order, answerList));
}
return retVal;
});
}
public Integer getNumberAnswer() {
return getValue(() -> null, answers -> {
for (var answer : answers) {
var answerObj = answer.asJsonObject();
if (answerObj.getBoolean("selected")) {
var value = answerObj.get("answer");
if (value.getValueType() == ValueType.NUMBER) {
return answerObj.getInt("answer");
}
return Integer.parseInt(answerObj.getString("answer"));
}
}
return null;
});
}
public String getSingleAnswer() {
return getValue(() -> "", answers -> {
for (var answer : answers) {
var answerObj = answer.asJsonObject();
if (answerObj.getBoolean("selected")) {
return answerObj.getString("answer");
}
}
return "";
});
}
public List<String> getMultiAnswer() {
return getValue(() -> List.of(), answers -> {
List<String> retVal = new ArrayList<>();
for (var answer : answers) {
var answerObj = answer.asJsonObject();
if (answerObj.getBoolean("selected")) {
retVal.add(answerObj.getString("answer"));
}
}
return retVal;
});
}
public String getFreeText() {
return getValue(() -> "", (answers) -> answers.getFirst().asJsonObject().getString("answer"));
}
private <T> T getValue(Supplier<T> emptyValue, Function<JsonArray, T> function) {
return getValue("answers", emptyValue, function);
}
private <T> T getValue(String element, Supplier<T> emptyValue, Function<JsonArray, T> function) {
try (var stringReader = new StringReader(data); JsonReader reader = Json.createReader(stringReader);) {
JsonObject root = reader.readObject();
JsonArray answers = root.getJsonArray(element);
if (answers.isEmpty()) {
return emptyValue.get();
}
return function.apply(answers);
} catch (JsonParsingException | NullPointerException ioe) {
LOG.warn("Failed to parse json " + ioe.getMessage(), ioe);
return emptyValue.get();
}
}
}
public record MatrixQuestion(String id, String title, int order, List<MatrixAnswer> answers) {
}
public record MatrixAnswer(String id, String answer, boolean selected) {
}
}

View File

@@ -0,0 +1,74 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import jakarta.json.*;
import jakarta.json.stream.JsonParsingException;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 20 Feb 2026
*/
public record QuestionnaireUploadJsonParser(
String username,
String pharmacyName,
String customerNumber,
Date date,
String comment,
String city,
String zip,
String questionnair) {
private static final Log LOG = LogFactory.getLog(QuestionnaireUploadJsonParser.class);
public static Optional<QuestionnaireUploadJsonParser> builder(String body) {
if (body == null) {
return Optional.empty();
}
try (var stringReader = new StringReader(body); JsonReader reader = Json.createReader(stringReader);) {
JsonObject root = reader.readObject();
String username = root.getString("username");
String pharmacyName = root.getString("pharmacyName");
String customerNumber = root.getString("customerNumber");
String dateStr = root.getString("date");
var dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
Date date = dateFormat.parse(dateStr);
String comment = root.getString("comment");
String city = root.getString("city");
String zip = root.getString("zip");
JsonArray questionnaireArr = root.getJsonArray("questionnaire");
String questionnaire = "";
try (StringWriter writer = new StringWriter(); JsonWriter jsonWriter = Json.createWriter(writer);) {
jsonWriter.writeArray(questionnaireArr);
jsonWriter.close();
questionnaire = writer.toString();
}
return Optional.of(new QuestionnaireUploadJsonParser(username, pharmacyName, customerNumber, date, comment, city, zip, questionnaire));
} catch (IOException | ParseException | JsonParsingException | NullPointerException ioe) {
LOG.warn("Failed to parse json " + ioe.getMessage(), ioe );
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,23 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 27 Jan 2026
*/
public class StorageUtils {
public enum StorageState {
OK,
DUPLICATE,
FORBIDDEN,
NOT_FOUND,
ERROR,
;
}
}

View File

@@ -0,0 +1,17 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.util.Set;
import com.networknt.schema.ValidationMessage;
/**
*
* <p>Copyright: Copyright (c) 2017</p>
* <p>Company: heyday marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: Feb 14, 2017
*/
public record ValidationReply(boolean success, Set<ValidationMessage> errors) {
}

View File

@@ -0,0 +1,55 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import jakarta.inject.Inject;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 23 Feb 2026
*/
public class ZipExportUtils {
private static final Log LOG = LogFactory.getLog(ZipExportUtils.class);
@Inject
private ExcelUtils excelUtils;
public Optional<byte[]> getExport(List<QuestionnaireCustomer> customers) {
if (customers.isEmpty()) {
return Optional.empty();
}
try (ZipUtils zipUtils = new ZipUtils()) {
boolean hasContent = false;
for (var customer : customers) {
List<Questionnaire> questionnaires = customer.getQuestionnaires().stream().sorted((x, y) -> x.getQuestionnaireDate().compareTo(y.getQuestionnaireDate())).toList();
var file = excelUtils.create(customer, questionnaires);
if (file.isPresent()) {
zipUtils.addFile(customer.getName() + ".xlsx", file.get());
hasContent = true;
}
}
if (!hasContent) {
return Optional.empty();
}
return Optional.of(zipUtils.create());
} catch (IOException ioe) {
LOG.error("Failed to create zip file " + ioe.getMessage(), ioe);
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,41 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 23 Feb 2026
*/
public class ZipUtils implements AutoCloseable {
private ByteArrayOutputStream baos = new ByteArrayOutputStream();
private ZipOutputStream zos = new ZipOutputStream(baos);
public void addFile(String name, byte[] file) throws IOException {
ZipEntry entry = new ZipEntry(name);
zos.putNextEntry(entry);
zos.write(file);
zos.closeEntry();
}
public byte[] create() throws IOException {
zos.flush();
zos.close();
return baos.toByteArray();
}
@Override
public void close() throws IOException {
zos.close();
baos.close();
}
}

View File

@@ -0,0 +1,45 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import org.jboss.resteasy.annotations.GZIP;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import marketing.heyday.hartmann.fotodocumentation.core.service.CustomerPictureService;
import marketing.heyday.hartmann.fotodocumentation.rest.jackson.JsonSchemaValidate;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@RequestScoped
@Path("customer-picture")
public class CustomerPictureResource {
@EJB
private CustomerPictureService customerPictureService;
@GZIP
@POST
@Path("")
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Add Customer Image to database")
@ApiResponse(responseCode = "200", description = "Add successfull")
public Response doAddCustomerPicture(@JsonSchemaValidate("schema/customer_picture_add.json") CustomerPictureValue customerPictureValue) {
boolean success = customerPictureService.addCustomerPicture(customerPictureValue);
return success ? Response.ok().build() : Response.status(Status.BAD_REQUEST).build();
}
}

View File

@@ -0,0 +1,94 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT;
import java.io.OutputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.resteasy.annotations.GZIP;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import jakarta.ws.rs.core.UriInfo;
import jakarta.ws.rs.core.Response.Status;
import marketing.heyday.hartmann.fotodocumentation.core.service.CustomerPictureService;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerListValue;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@RequestScoped
@Path("customer")
public class CustomerResource {
private static final Log LOG = LogFactory.getLog(CustomerResource.class);
@Context
private UriInfo uriInfo;
@EJB
private CustomerPictureService customerPictureService;
@GZIP
@GET
@Path("")
@Produces(JSON_OUT)
@Operation(summary = "Get customer list")
@ApiResponse(responseCode = "200", description = "Successfully retrieved customer list", content = @Content(mediaType = JSON_OUT, array = @ArraySchema(schema = @Schema(implementation = CustomerListValue.class))))
public Response doGetCustomerList(@QueryParam("query") String query, @QueryParam("startsWith") String startsWith) {
LOG.debug("Query customers for query " + query + " startsWith: " + startsWith);
var retVal = customerPictureService.getAll(query, startsWith);
return Response.ok().entity(retVal).build();
}
@GZIP
@GET
@Path("{id}")
@Produces(JSON_OUT)
@Operation(summary = "Get customer value")
@ApiResponse(responseCode = "200", description = "Successfully retrieved customer value", content = @Content(mediaType = JSON_OUT, array = @ArraySchema(schema = @Schema(implementation = CustomerValue.class))))
public Response doGetDetailCustomer(@PathParam("id") Long id) {
LOG.debug("Get Customer details for id " + id);
String baseUrl = uriInfo.getBaseUri().toString();
var retVal = customerPictureService.get(id, baseUrl);
return Response.ok().entity(retVal).build();
}
@GZIP
@GET
@Path("export/{id}")
@Produces("application/pdf")
@Operation(summary = "Get Export")
@ApiResponse(responseCode = "200", description = "Successfully retrieved export")
public Response doExport(@PathParam("id") Long id, @QueryParam("picture") Long pictureId) {
LOG.debug("Create export for customer " + id + " with optional param " + pictureId);
byte[] pdf = customerPictureService.getExport(id, pictureId);
if (pdf.length == 0) {
return Response.status(Status.NOT_FOUND).build();
}
StreamingOutput streamingOutput = (OutputStream output) -> {
LOG.debug("Start writing content to OutputStream available bytes");
output.write(pdf);
};
return Response.status(Status.OK).entity(streamingOutput).build();
}
}

View File

@@ -0,0 +1,73 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wildfly.security.auth.server.SecurityIdentity;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import marketing.heyday.hartmann.fotodocumentation.core.service.LoginService;
import marketing.heyday.hartmann.fotodocumentation.core.utils.LoginUtils;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.TokenPairValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@RequestScoped
@Path("login")
@PermitAll
public class LoginResource {
private static final Log LOG = LogFactory.getLog(LoginResource.class);
@EJB
private LoginService loginService;
@Inject
private LoginUtils loginUtils;
@GET
@Path("/")
@Produces(JSON_OUT)
@Operation(summary = "Get logged in user")
@ApiResponse(responseCode = "200", description = "Successfully retrieved logged in user", content = @Content(mediaType = JSON_OUT, schema = @Schema(implementation = TokenPairValue.class)))
@ApiResponse(responseCode = "500", description = "Internal server error")
public Response doLogin(@Context HttpServletRequest httpServletRequest) {
Optional<SecurityIdentity> identity = loginUtils.authenticate(httpServletRequest);
if (identity.isEmpty()) {
LOG.debug("identity empty login invalid");
return Response.status(Status.UNAUTHORIZED).build();
}
String username = identity.get().getPrincipal().getName();
LOG.debug("Login valid returning jwt");
String deviceInfo = loginUtils.extractDeviceInfo(httpServletRequest);
String ipAddress = loginUtils.extractIpAddress(httpServletRequest);
var tokenPairValue = loginService.authenticateUser(username, deviceInfo, ipAddress);
return Response.ok(tokenPairValue).build();
}
}

View File

@@ -0,0 +1,36 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 14 Nov 2024
*/
@Path("monitoring")
@RequestScoped
public class MonitoringResource {
@GET
@Path("check/{text}")
@Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Monitoring service for testing if the server is up and running.")
@ApiResponse(responseCode = "200", description = "ok with as body the given path param text. ")
public Response check(@PathParam("text") String text) {
return Response.status(Status.OK).entity(text).build();
}
}

View File

@@ -0,0 +1,95 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import java.io.OutputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.resteasy.annotations.GZIP;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.StreamingOutput;
import marketing.heyday.hartmann.fotodocumentation.core.service.PictureService;
import marketing.heyday.hartmann.fotodocumentation.core.utils.EvaluationUtil;
import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@RequestScoped
@Path("picture")
public class PictureResource {
private static final Log LOG = LogFactory.getLog(PictureResource.class);
@EJB
private PictureService pictureService;
@Inject
private EvaluationUtil evaluationUtil;
@DELETE
@Path("{id}")
@Operation(summary = "Delete picture from database")
@ApiResponse(responseCode = "200", description = "Task successfully deleted")
@ApiResponse(responseCode = "404", description = "Task not found")
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
public Response doDelete(@PathParam("id") Long id) {
LOG.debug("Delete picture with id " + id);
var state = pictureService.delete(id);
return deleteResponse(state).build();
}
@PUT
@Path("evaluation/{id}")
@Operation(summary = "Update evaluation for picture data to database")
@ApiResponse(responseCode = "200", description = "Task successfully updated")
public Response doUpdateEvaluation(@PathParam("id") Long id, @QueryParam("evaluation") Integer value) {
if (evaluationUtil.isInValid(value)) {
return Response.status(Status.BAD_REQUEST).build();
}
StorageState state = pictureService.updateEvaluationStatus(id, value);
return deleteResponse(state).build();
}
@GZIP
@GET
@Path("image/{id}")
@Produces({ "image/png", "image/jpg" })
@Operation(summary = "Get picture")
@ApiResponse(responseCode = "200", description = "Successfully retrieved picture")
public Response doGetPictureImage(@PathParam("id") Long id, @QueryParam("size") int size) {
LOG.debug("Get Picture for id " + id + " with size " + size);
byte[] retVal = pictureService.getImage(id, size);
if (retVal.length == 0) {
return Response.status(Status.NOT_FOUND).build();
}
StreamingOutput streamingOutput = (OutputStream output) -> {
LOG.debug("Start writing content to OutputStream available bytes");
output.write(retVal);
};
return Response.status(Status.OK).entity(streamingOutput).build();
}
protected ResponseBuilder deleteResponse(StorageState state) {
return switch (state) {
case OK -> Response.status(Status.OK);
case NOT_FOUND -> Response.status(Status.NOT_FOUND);
default -> Response.status(Status.INTERNAL_SERVER_ERROR);
};
}
}

View File

@@ -0,0 +1,111 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT;
import java.io.OutputStream;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.resteasy.annotations.GZIP;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.StreamingOutput;
import marketing.heyday.hartmann.fotodocumentation.core.service.QuestionnaireCustomerService;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnaireCustomerListValue;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnaireCustomerValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@RequestScoped
@Path("questionnairecustomer")
public class QuestionnaireCustomerResource {
private static final Log LOG = LogFactory.getLog(QuestionnaireCustomerResource.class);
@EJB
private QuestionnaireCustomerService questionnaireCustomerService;
@GZIP
@GET
@Path("")
@Produces(JSON_OUT)
@Operation(summary = "Get customer list")
@ApiResponse(responseCode = "200", description = "Successfully retrieved customer list", content = @Content(mediaType = JSON_OUT, array = @ArraySchema(schema = @Schema(implementation = QuestionnaireCustomerListValue.class))))
public Response doGetCustomerList(@QueryParam("query") String query, @QueryParam("startsWith") String startsWith) {
LOG.debug("Query customers for query " + query + " startsWith: " + startsWith);
var retVal = questionnaireCustomerService.getAll(query, startsWith);
return Response.ok().entity(retVal).build();
}
@GZIP
@GET
@Path("{id}")
@Produces(JSON_OUT)
@Operation(summary = "Get customer value")
@ApiResponse(responseCode = "200", description = "Successfully retrieved customer value", content = @Content(mediaType = JSON_OUT, array = @ArraySchema(schema = @Schema(implementation = QuestionnaireCustomerValue.class))))
public Response doGetDetailCustomer(@PathParam("id") Long id) {
LOG.debug("Get Customer details for id " + id);
var retVal = questionnaireCustomerService.get(id);
return Response.ok().entity(retVal).build();
}
@GZIP
@GET
@Path("exportall")
@Produces("application/zip")
@Operation(summary = "Get Export")
@ApiResponse(responseCode = "200", description = "Successfully retrieved export")
public Response doExport() {
LOG.debug("Create export for customer ");
Optional<byte[]> pdfOpt = questionnaireCustomerService.getExport();
if (pdfOpt.isEmpty()) {
return Response.status(Status.NOT_FOUND).build();
}
StreamingOutput streamingOutput = (OutputStream output) -> {
LOG.debug("Start writing content to OutputStream available bytes");
output.write(pdfOpt.get());
};
return Response.status(Status.OK).entity(streamingOutput).build();
}
@GZIP
@GET
@Path("export/{id}")
@Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
@Operation(summary = "Get Export")
@ApiResponse(responseCode = "200", description = "Successfully retrieved export")
public Response doExport(@PathParam("id") Long id, @QueryParam("questionnaire") Long questionnaireId) {
LOG.debug("Create export for customer " + id + " with optional param " + questionnaireId);
Optional<byte[]> pdfOpt = questionnaireCustomerService.getExport(id, questionnaireId);
if (pdfOpt.isEmpty()) {
return Response.status(Status.NOT_FOUND).build();
}
StreamingOutput streamingOutput = (OutputStream output) -> {
LOG.debug("Start writing content to OutputStream available bytes");
output.write(pdfOpt.get());
};
return Response.status(Status.OK).entity(streamingOutput).build();
}
}

View File

@@ -0,0 +1,67 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import org.jboss.resteasy.annotations.GZIP;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import marketing.heyday.hartmann.fotodocumentation.core.service.QuestionnairePublishService;
import marketing.heyday.hartmann.fotodocumentation.core.utils.JsonSchemaValidator;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 18 Feb 2026
*/
@RequestScoped
@Path("questionnaire-publish")
public class QuestionnairePublishResource {
@EJB
private QuestionnairePublishService questionnairePublishService;
@Inject
private JsonSchemaValidator jsonSchemaValidator;
@GZIP
@POST
@Path("")
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Add questionnaire to database")
@ApiResponse(responseCode = "200", description = "Add successfull")
public Response doAddQuestionnaire(InputStream inputStream) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String body = reader.lines().collect(Collectors.joining("\n"));
var reply = jsonSchemaValidator.validate("schema/questionnaire_publish.json", body);
if (!reply.success()) {
return Response.status(Status.BAD_REQUEST).entity(reply.errors()).build();
}
questionnairePublishService.publish(body);
return Response.ok().build();
} catch (IOException e) {
return Response.status(Status.BAD_REQUEST).build();
}
}
}

View File

@@ -0,0 +1,70 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status;
import marketing.heyday.hartmann.fotodocumentation.core.service.QuestionnaireService;
import marketing.heyday.hartmann.fotodocumentation.core.utils.EvaluationUtil;
import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@RequestScoped
@Path("questionnaire")
public class QuestionnaireResource {
private static final Log LOG = LogFactory.getLog(QuestionnaireResource.class);
@EJB
private QuestionnaireService questionnaireService;
@Inject
private EvaluationUtil evaluationUtil;
@DELETE
@Path("{id}")
@Operation(summary = "Delete questionnaire from database")
@ApiResponse(responseCode = "200", description = "Task successfully deleted")
@ApiResponse(responseCode = "404", description = "Task not found")
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
public Response doDelete(@PathParam("id") Long id) {
LOG.debug("Delete questionnaire with id " + id);
var state = questionnaireService.delete(id);
return deleteResponse(state).build();
}
@PUT
@Path("evaluation/{id}")
@Operation(summary = "Update evaluation for questionnaire data to database")
@ApiResponse(responseCode = "200", description = "Task successfully updated")
public Response doUpdateEvaluation(@PathParam("id") Long id, @QueryParam("evaluation") Integer value) {
if (evaluationUtil.isInValid(value)) {
return Response.status(Status.BAD_REQUEST).build();
}
StorageState state = questionnaireService.updateEvaluationStatus(id, value);
return deleteResponse(state).build();
}
protected ResponseBuilder deleteResponse(StorageState state) {
return switch (state) {
case OK -> Response.status(Status.OK);
case NOT_FOUND -> Response.status(Status.NOT_FOUND);
default -> Response.status(Status.INTERNAL_SERVER_ERROR);
};
}
}

View File

@@ -0,0 +1,56 @@
package marketing.heyday.hartmann.fotodocumentation.rest.jackson;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.servers.Server;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.MediaType;
import marketing.heyday.hartmann.fotodocumentation.rest.*;
/**
*
*
* <p>Copyright: Copyright (c) 2017</p>
* <p>Company: heyday marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 18 Oct 2017
*/
@OpenAPIDefinition(info = @Info(title = "Hartmann Photo upload API", version = "1.0", description = "All available routes for the Hartmann Photo upload API", contact = @Contact(url = "https://localhost", name = "Patrick Verboom", email = "p.verboom@heyday.marketing")), servers = {
@Server(description = "development", url = "http://localhost"),
@Server(description = "integration", url = "http://localhost"),
@Server(description = "production", url = "http://localhost")
})
@ApplicationPath("/api")
public class ApplicationConfigApi extends Application {
private static final Log LOG = LogFactory.getLog(ApplicationConfigApi.class);
public static final String JSON_OUT = MediaType.APPLICATION_JSON + "; charset=utf-8";
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> retVal = new HashSet<>();
retVal.add(OpenApiResource.class);
retVal.add(ValidatedMessageBodyReader.class);
retVal.add(LoginResource.class);
retVal.add(MonitoringResource.class);
retVal.add(CustomerPictureResource.class);
retVal.add(CustomerResource.class);
retVal.add(PictureResource.class);
retVal.add(QuestionnairePublishResource.class);
retVal.add(QuestionnaireCustomerResource.class);
retVal.add(QuestionnaireResource.class);
LOG.info("returning rest api classes " + retVal);
return retVal;
}
}

View File

@@ -0,0 +1,21 @@
package marketing.heyday.hartmann.fotodocumentation.rest.jackson;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
* <p>Copyright: Copyright (c) 2017</p>
* <p>Company: heyday marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: Feb 10, 2017
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.PARAMETER})
public @interface JsonSchemaValidate {
String value();
}

View File

@@ -0,0 +1,14 @@
package marketing.heyday.hartmann.fotodocumentation.rest.jackson;
/**
*
* <p>Copyright: Copyright (c) 2017</p>
* <p>Company: heyday marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: Feb 10, 2017
*/
public interface SchemaValidated {
}

View File

@@ -0,0 +1,88 @@
package marketing.heyday.hartmann.fotodocumentation.rest.jackson;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.Provider;
import marketing.heyday.hartmann.fotodocumentation.core.utils.JsonSchemaValidator;
import marketing.heyday.hartmann.fotodocumentation.core.utils.ValidationReply;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 6 Dec 2024
*/
@Provider
@Consumes(value = {
MediaType.APPLICATION_JSON, "application/json",
MediaType.APPLICATION_JSON, "application/json; charset=utf-8",
})
public class ValidatedMessageBodyReader implements MessageBodyReader<SchemaValidated> {
/* (non-Javadoc)
* @see jakarta.ws.rs.ext.MessageBodyReader#isReadable(java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], jakarta.ws.rs.core.MediaType)
*/
@Override
public boolean isReadable(Class<?> classType, Type type, Annotation[] annotations, MediaType mediaType) {
if (mediaType.getType().contains("application/json")) {
return false;
}
return Arrays.stream(annotations).anyMatch(a -> a.annotationType() == JsonSchemaValidate.class);
}
/* (non-Javadoc)
* @see jakarta.ws.rs.ext.MessageBodyReader#readFrom(java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], jakarta.ws.rs.core.MediaType, jakarta.ws.rs.core.MultivaluedMap, java.io.InputStream)
*/
@Override
public SchemaValidated readFrom(Class<SchemaValidated> classType, Type type, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream input) throws IOException {
final String jsonData = read(input);
Optional<JsonSchemaValidate> annotation = Arrays.stream(annotations).filter(a -> a.annotationType() == JsonSchemaValidate.class).map(a -> (JsonSchemaValidate) a).findAny();
if (annotation.isPresent()) {
ValidationReply reply = validate(annotation.get(), jsonData);
if (!reply.success()) {
var response = Response.status(Status.BAD_REQUEST).entity(reply.errors()).build();
throw new WebApplicationException(response);
}
}
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(new StringReader(jsonData), classType);
}
/**
* @param jsonSchema
* @param jsonData
* @return
*/
private ValidationReply validate(JsonSchemaValidate jsonSchema, String jsonData) {
String schemaPath = jsonSchema.value();
return new JsonSchemaValidator().validate(schemaPath, jsonData);
}
private String read(InputStream input) throws IOException {
try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input, Charset.forName("UTF-8")))) {
return buffer.lines().collect(Collectors.joining("\n"));
}
}
}

View File

@@ -0,0 +1,29 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Schema(name = "CustomerList")
public record CustomerListValue(Long id, String name, String customerNumber, String zip, String city, Date lastUpdateDate) {
public static CustomerListValue builder(Customer customer) {
if (customer == null) {
return null;
}
Date date = customer.getPictures().stream().map(Picture::getPictureDate).sorted((p1, p2) -> p2.compareTo(p1)).findFirst().orElse(null);
return new CustomerListValue(customer.getCustomerId(), customer.getName(), customer.getCustomerNumber(), customer.getZip(), customer.getCity(), date);
}
}

View File

@@ -0,0 +1,20 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import marketing.heyday.hartmann.fotodocumentation.rest.jackson.SchemaValidated;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Schema(name = "CustomerPictureUpload")
public record CustomerPictureValue(String username, String pharmacyName, String customerNumber, Date date, String zip, String city, String comment, String category, String base64String) implements SchemaValidated {
}

View File

@@ -0,0 +1,27 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 22 Jan 2026
*/
@Schema(name = "Customer")
public record CustomerValue(Long id, String name, String customerNumber, String city, String zip, List<PictureValue> pictures) {
public static CustomerValue builder(Customer customer, String baseUrl) {
if (customer == null) {
return null;
}
List<PictureValue> pictures = customer.getPictures().parallelStream().map(p -> PictureValue.builder(p, baseUrl)).filter(p -> p != null).toList();
return new CustomerValue(customer.getCustomerId(), customer.getName(), customer.getCustomerNumber(), customer.getCity(), customer.getZip(), pictures);
}
}

View File

@@ -0,0 +1,39 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 22 Jan 2026
*/
@Schema(name = "Picture")
public record PictureValue(Long id, String comment, String category, Date pictureDate, String username, Integer evaluation, String imageUrl, String normalSizeUrl, String thumbnailSizeUrl) {
public static PictureValue builder(Picture picture, String baseUrl) {
if (picture == null) {
return null;
}
String sizeUrl = baseUrl;
// we need to rewrite the url for dev/integ/prod since the Wildfly doesn't know we are running the nginx on https. Without the https the images are not shown
if(baseUrl.startsWith("http://") && !baseUrl.startsWith("http://localhost")){
sizeUrl = "https://" + baseUrl.substring(7);
}
sizeUrl = sizeUrl + "picture/image/" + picture.getPictureId() + "?size=";
String imageUrl = sizeUrl + "1";
String normalSizeUrl = sizeUrl + "2";
String thumbnailSizeUrl = sizeUrl + "3";
return new PictureValue(picture.getPictureId(), picture.getComment(), picture.getCategory(), picture.getPictureDate(), picture.getUsername(), picture.getEvaluation(), imageUrl, normalSizeUrl, thumbnailSizeUrl);
}
}

View File

@@ -0,0 +1,29 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Schema(name = "QuestionnaireCustomerList")
public record QuestionnaireCustomerListValue(Long id, String name, String customerNumber, String zip, String city, Date lastUpdateDate) {
public static QuestionnaireCustomerListValue builder(QuestionnaireCustomer customer) {
if (customer == null) {
return null;
}
Date date = customer.getQuestionnaires().stream().map(Questionnaire::getQuestionnaireDate).sorted((p1, p2) -> p2.compareTo(p1)).findFirst().orElse(null);
return new QuestionnaireCustomerListValue(customer.getCustomerId(), customer.getName(), customer.getCustomerNumber(), customer.getZip(), customer.getCity(), date);
}
}

View File

@@ -0,0 +1,27 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 22 Jan 2026
*/
@Schema(name = "QuestionnaireCustomer")
public record QuestionnaireCustomerValue(Long id, String name, String customerNumber, String city, String zip, List<QuestionnaireValue> questionnaires) {
public static QuestionnaireCustomerValue builder(QuestionnaireCustomer customer) {
if (customer == null) {
return null;
}
List<QuestionnaireValue> questionnaires = customer.getQuestionnaires().parallelStream().map(QuestionnaireValue::builder).filter(p -> p != null).toList();
return new QuestionnaireCustomerValue(customer.getCustomerId(), customer.getName(), customer.getCustomerNumber(), customer.getCity(), customer.getZip(), questionnaires);
}
}

View File

@@ -0,0 +1,21 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import marketing.heyday.hartmann.fotodocumentation.rest.jackson.SchemaValidated;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 18 Feb 2026
*/
@Schema(name = "QuestionnairePublish")
public record QuestionnairePublishValue(String username, String pharmacyName, String customerNumber, Date date, String zip, String city, String comment, String category, String base64String) implements SchemaValidated {
}

View File

@@ -0,0 +1,28 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 22 Jan 2026
*/
@Schema(name = "Questionnaire")
public record QuestionnaireValue(Long id, String comment, String category, Date questionnaireDate, String username, Integer evaluation) {
public static QuestionnaireValue builder(Questionnaire questionnaire) {
if (questionnaire == null) {
return null;
}
return new QuestionnaireValue(questionnaire.getQuestionnaireId(), questionnaire.getComment(), questionnaire.getCategory(), questionnaire.getQuestionnaireDate(), questionnaire.getUsername(), questionnaire.getEvaluation());
}
}

View File

@@ -0,0 +1,23 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import io.swagger.v3.oas.annotations.media.Schema;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@Schema(name = "TokenPair")
public record TokenPairValue(String accessToken, String refreshToken) {
@Override
public String toString() {
return "TokenPair{" + "accessToken='" + (accessToken != null ? "[REDACTED]" : "null") + '\'' + ", refreshToken='" + (refreshToken != null ? "[REDACTED]" : "null") + '\'' + '}';
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
version="4.0" bean-discovery-mode="all">
</beans>

View File

@@ -0,0 +1,28 @@
<persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd">
<persistence-unit name="auth" transaction-type="JTA">
<jta-data-source>java:/jdbc/fotoDocumentationDS</jta-data-source>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Right</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.User</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Customer</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Picture</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire</class>
<properties>
<property name="hibernate.format_sql" value="false" />
<property name="hibernate.show_sql" value="false" />
<!-- <property name="hibernate.archive.autodetection" value="class" /> -->
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
<property name="hibernate.hbm2ddl.auto" value="none" />
<property name="hibernate.jpa.compliance.query" value="false" />
</properties>
</persistence-unit>
</persistence>

View File

@@ -0,0 +1,120 @@
-- Right
create sequence right_seq start 25;
create table x_right (
right_id bigint PRIMARY KEY,
code varchar(50) NOT NULL,
name varchar(150) NOT NULL,
jpa_active boolean NOT NULL,
jpa_created timestamp NOT NULL,
jpa_updated timestamp NOT NULL,
jpa_version integer NOT NULL,
CONSTRAINT unq_x_right_code UNIQUE(code)
);
-- user
create sequence user_seq start 25;
create table x_user (
user_id bigint PRIMARY KEY,
username varchar(150) NOT NULL,
password varchar(150) NOT NULL,
salt varchar(150) NOT NULL,
title varchar(15) ,
firstname varchar(150) NOT NULL,
lastname varchar(150) NOT NULL,
email varchar(150) NOT NULL,
jpa_active boolean NOT NULL,
jpa_created timestamp NOT NULL,
jpa_updated timestamp NOT NULL,
jpa_version integer NOT NULL,
CONSTRAINT unq_x_user_username UNIQUE(username)
);
create table user_to_right (
user_id_fk bigint REFERENCES x_user,
right_id_fk bigint REFERENCES x_right,
PRIMARY KEY(user_id_fk, right_id_fk)
);
-- jwt_refresh_token
CREATE SEQUENCE jwt_refresh_token_seq START 1 INCREMENT 1;
CREATE TABLE jwt_refresh_token (
jwt_refresh_token_id BIGINT PRIMARY KEY,
token_hash VARCHAR(255) NOT NULL, -- SHA-256 hash of refresh token
device_info VARCHAR(255), -- Browser/device identifier
ip_address VARCHAR(45),
issued_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP,
last_used_at TIMESTAMP,
user_id_fk BIGINT NOT NULL REFERENCES x_user(user_id),
UNIQUE (token_hash)
);
CREATE INDEX idx_jwt_refresh_token_user ON jwt_refresh_token(user_id_fk);
CREATE INDEX idx_jwt_refresh_token_expires ON jwt_refresh_token(expires_at);
-- customer
create sequence customer_seq start 25;
create table customer (
customer_id bigint PRIMARY KEY,
customer_number varchar(150) NOT NULL,
name varchar(150) NOT NULL,
jpa_active boolean NOT NULL,
jpa_created timestamp NOT NULL,
jpa_updated timestamp NOT NULL,
jpa_version integer NOT NULL,
CONSTRAINT unq_customer_number UNIQUE(customer_number)
);
-- picture
create sequence picture_seq start 25;
create table picture (
picture_id bigint PRIMARY KEY,
username varchar(150),
picture_date timestamp NOT NULL,
comment TEXT,
image TEXT,
jpa_active boolean NOT NULL,
jpa_created timestamp NOT NULL,
jpa_updated timestamp NOT NULL,
jpa_version integer NOT NULL,
customer_id_fk bigint REFERENCES customer
);
-- initial users
insert into x_right (right_id, code, name,jpa_active,jpa_created,jpa_updated,jpa_version) VALUES
(1, 'ADMIN', 'Admin Right', true,TIMESTAMP '2026-01-20 10:09:30.009',TIMESTAMP '2026-01-20 10:09:30.009',0),
(2, 'USER', 'User Right', true,TIMESTAMP '2026-01-20 10:09:52.797',TIMESTAMP '2026-01-20 10:09:52.797',0)
;
-- nvlev4YnTi
-- x1t0e7Pb49
INSERT INTO x_user (user_id,username,password,salt,title,firstname,lastname,email,jpa_active,jpa_created,jpa_updated,jpa_version)
VALUES
(1,'hartmann','vPsg/G5xQWoJTOA0r9b9HPTEAzMktKg7fKCrnmHYcyQ=', '9bARmw6zzbXPg4qdbj5RAe2OlJ9mz0Lpq3ZKJlg8Iug=','Herr','Hartmann','Admin','admin@heyday.marketing',true,TIMESTAMP '2026-01-20 10:09:52.000',TIMESTAMP '2026-01-20 10:09:52.000',0),
(2,'adm','eXlSEtLDfqos/w0DqPQiVoJHVEQaqLwD7qeDx74Onmk=','vajK924ZRXNWmt9GkcK/BO/Oc1bYp582MJ47HzsXyzA=','Herr','Hartmann','adm','adm@heyday.marketing',true,TIMESTAMP '2026-01-20 10:09:52.000',TIMESTAMP '2026-01-20 10:09:52.000',0);
INSERT INTO user_to_right (user_id_fk,right_id_fk)
VALUES
(1,1),
(2,2);

View File

@@ -0,0 +1,4 @@
-- picture
alter table picture add column category varchar(250);

View File

@@ -0,0 +1,8 @@
-- picture
alter table picture add column evaluation bigint;
update picture set evaluation = 0;
alter table picture alter column evaluation set not null;

View File

@@ -0,0 +1,7 @@
-- customer
alter table customer add column zip varchar(150);
alter table customer add column city varchar(150);

View File

@@ -0,0 +1,37 @@
create sequence IF NOT EXISTS questionnaire_customer_seq start 25;
CREATE TABLE IF NOT EXISTS questionnaire_customer
(
customer_id bigint PRIMARY KEY,
customer_number varchar(150) NOT NULL,
name varchar(150) NOT NULL,
zip varchar(150),
city varchar(150),
jpa_active boolean NOT NULL,
jpa_created timestamp NOT NULL,
jpa_updated timestamp NOT NULL,
jpa_version integer NOT NULL,
CONSTRAINT unq_questionnaire_customer_number UNIQUE(customer_number)
);
create sequence IF NOT EXISTS questionnaire_seq start 25;
CREATE TABLE IF NOT EXISTS questionnaire
(
questionnaire_id bigint PRIMARY KEY,
username varchar(150),
questionnaire_date timestamp NOT NULL,
comment text,
questions text,
category varchar(250),
evaluation bigint NOT NULL,
jpa_active boolean NOT NULL,
jpa_created timestamp NOT NULL,
jpa_updated timestamp NOT NULL,
jpa_version integer NOT NULL,
customer_id_fk bigint REFERENCES questionnaire_customer
);

View File

@@ -0,0 +1,48 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Add Customer Picture",
"description": "Add a Customer Picture to the system",
"type": "object",
"properties": {
"username": {
"description": "The username from the user who uploads the picture",
"type": "string"
},
"pharmacyName": {
"description": "The Name from the pharmacy customer ",
"type": "string"
},
"customerNumber": {
"description": "The unique number from the pharmacy customer ",
"type": "string"
},
"date": {
"description": "The date when the picture is taken ",
"type": "string"
},
"comment": {
"description": "A free text comment field ",
"type": "string"
},
"zip": {
"description": "The zip from the customer",
"type": "string"
},
"city": {
"description": "The city from the customer",
"type": "string"
},
"base64String": {
"description": "The Picture content as base64 ",
"type": "string"
}
},
"required": [
"username",
"pharmacyName",
"customerNumber",
"date",
"comment",
"base64String"
]
}

View File

@@ -0,0 +1,163 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Publish Questionnaire",
"description": "Publish a completed questionnaire to the system",
"type": "object",
"properties": {
"username": {
"description": "The username from the user who submits the questionnaire",
"type": "string"
},
"pharmacyName": {
"description": "The name of the pharmacy customer",
"type": "string"
},
"customerNumber": {
"description": "The unique number of the pharmacy customer",
"type": "string"
},
"date": {
"description": "The date when the questionnaire was filled in (ISO 8601)",
"type": "string"
},
"comment": {
"description": "A free text comment field",
"type": "string"
},
"city": {
"description": "The city of the customer",
"type": "string"
},
"zip": {
"description": "The zip code of the customer",
"type": "string"
},
"questionnaire": {
"description": "The list of questions and answers",
"type": "array",
"items": {
"$ref": "#/definitions/question"
},
"minItems": 1
}
},
"required": [
"username",
"pharmacyName",
"customerNumber",
"date",
"questionnaire"
],
"definitions": {
"answer": {
"type": "object",
"properties": {
"id": {
"description": "The unique identifier of the answer",
"type": "string"
},
"answer": {
"description": "The answer text",
"type": "string"
},
"selected": {
"description": "Whether this answer is selected",
"type": "boolean"
}
},
"required": [
"id",
"answer",
"selected"
]
},
"matrixSubQuestion": {
"type": "object",
"properties": {
"id": {
"description": "The unique identifier of the sub-question",
"type": "string"
},
"title": {
"description": "The title of the sub-question",
"type": "string"
},
"order": {
"description": "The display order of the sub-question",
"type": "integer"
},
"answers": {
"description": "The list of answers for the sub-question",
"type": "array",
"items": {
"$ref": "#/definitions/answer"
},
"minItems": 1
}
},
"required": [
"id",
"title",
"order",
"answers"
]
},
"questionData": {
"type": "object",
"properties": {
"answers": {
"description": "The list of answers (for singleChoice, multiplChoice, number, freeText)",
"type": "array",
"items": {
"$ref": "#/definitions/answer"
}
},
"questions": {
"description": "The list of sub-questions (for matrix type)",
"type": "array",
"items": {
"$ref": "#/definitions/matrixSubQuestion"
}
}
}
},
"question": {
"type": "object",
"properties": {
"id": {
"description": "The unique identifier of the question",
"type": "string"
},
"title": {
"description": "The title/text of the question",
"type": "string"
},
"order": {
"description": "The display order of the question",
"type": "integer"
},
"type": {
"description": "The type of question",
"type": "string",
"enum": [
"singleChoice",
"multiplChoice",
"number",
"freeText",
"matrix"
]
},
"data": {
"$ref": "#/definitions/questionData"
}
},
"required": [
"id",
"title",
"order",
"type",
"data"
]
}
}
}

View File

@@ -0,0 +1,16 @@
{
"api_key": {
"type": "apiKey",
"name": "api_key",
"in": "header"
},
"petstore_auth": {
"type": "oauth2",
"authorizationUrl": "http://swagger.io/api/oauth/dialog",
"flow": "implicit",
"scopes": {
"write:pets": "modify pets in your account",
"read:pets": "read your pets"
}
}
}

View File

@@ -0,0 +1,107 @@
#{{#info}}{{title}}
## {{join schemes " | "}}://{{host}}{{basePath}}
{{description}}
{{#contact}}
[**Contact the developer**](mailto:{{email}})
{{/contact}}
**Version** {{version}}
[**Terms of Service**]({{termsOfService}})
{{#license}}[**{{name}}**]({{url}}){{/license}}
{{/info}}
{{#if consumes}}**Consumes:** {{join consumes ", "}}{{/if}}
{{#if produces}}**Produces:** {{join produces ", "}}{{/if}}
{{#if securityDefinitions}}
# Security Definitions
{{/if}}
{{> security}}
# APIs
{{#each paths}}
## {{@key}}
{{#this}}
{{#get}}
### GET
{{> operation}}
{{/get}}
{{#put}}
### PUT
{{> operation}}
{{/put}}
{{#post}}
### POST
{{> operation}}
{{/post}}
{{#delete}}
### DELETE
{{> operation}}
{{/delete}}
{{#option}}
### OPTION
{{> operation}}
{{/option}}
{{#patch}}
### PATCH
{{> operation}}
{{/patch}}
{{#head}}
### HEAD
{{> operation}}
{{/head}}
{{/this}}
{{/each}}
# Definitions
{{#each definitions}}
## <a name="/definitions/{{key}}">{{@key}}</a>
<table border="1">
<tr>
<th>name</th>
<th>type</th>
<th>required</th>
<th>description</th>
<th>example</th>
</tr>
{{#each this.properties}}
<tr>
<td>{{@key}}</td>
<td>
{{#ifeq type "array"}}
{{#items.$ref}}
{{type}}[<a href="{{items.$ref}}">{{basename items.$ref}}</a>]
{{/items.$ref}}
{{^items.$ref}}{{type}}[{{items.type}}]{{/items.$ref}}
{{else}}
{{#$ref}}<a href="{{$ref}}">{{basename $ref}}</a>{{/$ref}}
{{^$ref}}{{type}}{{#format}} ({{format}}){{/format}}{{/$ref}}
{{/ifeq}}
</td>
<td>{{#required}}required{{/required}}{{^required}}optional{{/required}}</td>
<td>{{#description}}{{{description}}}{{/description}}{{^description}}-{{/description}}</td>
<td>{{example}}</td>
</tr>
{{/each}}
</table>
{{/each}}

View File

@@ -0,0 +1,81 @@
{{#deprecated}}-deprecated-{{/deprecated}}
<a id="{{operationId}}">{{summary}}</a>
{{{description}}}
{{#if externalDocs.url}}{{externalDocs.description}}. [See external documents for more details]({{externalDocs.url}})
{{/if}}
{{#if security}}
#### Security
{{/if}}
{{#security}}
{{#each this}}
* {{@key}}
{{#this}} * {{this}}
{{/this}}
{{/each}}
{{/security}}
#### Request
{{#if consumes}}
**Content-Type: ** {{join consumes ", "}}{{/if}}
##### Parameters
{{#if parameters}}
<table border="1">
<tr>
<th>Name</th>
<th>Located in</th>
<th>Required</th>
<th>Description</th>
<th>Default</th>
<th>Schema</th>
<th>Example</th>
</tr>
{{/if}}
{{#parameters}}
<tr>
<th>{{name}}</th>
<td>{{in}}</td>
<td>{{#if required}}yes{{else}}no{{/if}}</td>
<td>{{description}}{{#if pattern}} (**Pattern**: `{{pattern}}`){{/if}}</td>
<td> - </td>
{{#ifeq in "body"}}
<td>
{{#ifeq schema.type "array"}}Array[<a href="{{schema.items.$ref}}">{{basename schema.items.$ref}}</a>]{{/ifeq}}
{{#schema.$ref}}<a href="{{schema.$ref}}">{{basename schema.$ref}}</a> {{/schema.$ref}}
</td>
{{else}}
{{#ifeq type "array"}}
<td>Array[{{items.type}}] ({{collectionFormat}})</td>
{{else}}
<td>{{type}} {{#format}}({{format}}){{/format}}</td>
{{/ifeq}}
{{/ifeq}}
<td>
{{#each examples}}
{{{this}}}
{{/each}}
</td>
</tr>
{{/parameters}}
{{#if parameters}}
</table>
{{/if}}
#### Response
{{#if produces}}**Content-Type: ** {{join produces ", "}}{{/if}}
| Status Code | Reason | Response Model |
|-------------|-------------|----------------|
{{#each responses}}| {{@key}} | {{description}} | {{#schema.$ref}}<a href="{{schema.$ref}}">{{basename schema.$ref}}</a>{{/schema.$ref}}{{^schema.$ref}}{{#ifeq schema.type "array"}}Array[<a href="{{schema.items.$ref}}">{{basename schema.items.$ref}}</a>]{{else}}{{schema.type}}{{/ifeq}}{{/schema.$ref}}{{^schema}} - {{/schema}}|
{{/each}}

View File

@@ -0,0 +1,88 @@
{{#each securityDefinitions}}
### {{@key}}
{{#this}}
{{#ifeq type "oauth2"}}
<table>
<tr>
<th>type</th>
<th colspan="2">{{type}}</th>
</tr>
{{#if description}}
<tr>
<th>description</th>
<th colspan="2">{{description}}</th>
</tr>
{{/if}}
{{#if authorizationUrl}}
<tr>
<th>authorizationUrl</th>
<th colspan="2">{{authorizationUrl}}</th>
</tr>
{{/if}}
{{#if flow}}
<tr>
<th>flow</th>
<th colspan="2">{{flow}}</th>
</tr>
{{/if}}
{{#if tokenUrl}}
<tr>
<th>tokenUrl</th>
<th colspan="2">{{tokenUrl}}</th>
</tr>
{{/if}}
{{#if scopes}}
<tr>
<td rowspan="3">scopes</td>
{{#each scopes}}
<td>{{@key}}</td>
<td>{{this}}</td>
</tr>
<tr>
{{/each}}
</tr>
{{/if}}
</table>
{{/ifeq}}
{{#ifeq type "apiKey"}}
<table>
<tr>
<th>type</th>
<th colspan="2">{{type}}</th>
</tr>
{{#if description}}
<tr>
<th>description</th>
<th colspan="2">{{description}}</th>
</tr>
{{/if}}
{{#if name}}
<tr>
<th>name</th>
<th colspan="2">{{name}}</th>
</tr>
{{/if}}
{{#if in}}
<tr>
<th>in</th>
<th colspan="2">{{in}}</th>
</tr>
{{/if}}
</table>
{{/ifeq}}
{{#ifeq type "basic"}}
<table>
<tr>
<th>type</th>
<th colspan="2">{{type}}</th>
</tr>
{{#if description}}
<tr>
<th>description</th>
<th colspan="2">{{description}}</th>
</tr>
{{/if}}
</table>
{{/ifeq}}
{{/this}}
{{/each}}

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<title>API Document</title>
<xmp theme="united" style="display:none;">
{{>markdown}}
</xmp>
<script src="http://strapdownjs.com/v/0.2/strapdown.js"></script>
<!-- code for TOC (jquery plugin) (see http://projects.jga.me/toc/#toc0) -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://rawgit.com/jgallen23/toc/master/dist/toc.min.js"></script>
<script src="https://rawgit.com/zipizap/strapdown_template/master/js/init_TOC.js"></script>
</html>

View File

@@ -0,0 +1,72 @@
package marketing.heyday.hartmann.fotodocumentation;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Base64.Encoder;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Test;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 20 Jan 2026
*/
public class SecurityGenerator {
private static final int GENERATE_LENGTH = 10;
private static final int SALT_LENGTH = 32;
@java.lang.SuppressWarnings("java:S2245")
public String generatePassword() {
return RandomStringUtils.randomAlphanumeric(GENERATE_LENGTH);
}
public byte[] createSalt() {
byte[] salt = new byte[SALT_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
return salt;
}
public byte[] createPassword(String password, byte[] salt) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] passwordBytes = password.getBytes(Charset.forName("utf-8"));
md.update(passwordBytes);
md.update(salt);
return md.digest();
}
public String encode(byte[] hash) {
Encoder encoder = Base64.getEncoder();
return encoder.encodeToString(hash);
}
@Test
public void test() throws NoSuchAlgorithmException {
String password = generatePassword();
assertNotNull(password);
byte[] salt = createSalt();
String saltHash = encode(salt);
byte[] digest = createPassword(password, salt);
String passwordHash = encode(digest);
System.out.println("Password " + password);
System.out.println("PasswordHash " + passwordHash);
System.out.println("saltHash " + saltHash);
}
}

View File

@@ -0,0 +1,230 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Calendar;
import java.util.Date;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 3 Feb 2026
*/
class CalendarUtilTest {
private CalendarUtil calendarUtil;
@BeforeEach
void setUp() {
calendarUtil = new CalendarUtil();
}
// --- parse ---
@Test
void parse_ddMMyyyyDot_returnsDate() {
Date result = calendarUtil.parse("15.03.2026");
assertNotNull(result);
Calendar cal = toCalendar(result);
assertEquals(15, cal.get(Calendar.DAY_OF_MONTH));
assertEquals(Calendar.MARCH, cal.get(Calendar.MONTH));
assertEquals(2026, cal.get(Calendar.YEAR));
}
@Test
void parse_dMyyyyDot_returnsDate() {
Date result = calendarUtil.parse("5.3.2026");
assertNotNull(result);
Calendar cal = toCalendar(result);
assertEquals(5, cal.get(Calendar.DAY_OF_MONTH));
assertEquals(Calendar.MARCH, cal.get(Calendar.MONTH));
assertEquals(2026, cal.get(Calendar.YEAR));
}
@Test
void parse_ddMMyy_returnsDate() {
Date result = calendarUtil.parse("15.03.26");
assertNotNull(result);
Calendar cal = toCalendar(result);
assertEquals(15, cal.get(Calendar.DAY_OF_MONTH));
assertEquals(Calendar.MARCH, cal.get(Calendar.MONTH));
}
@Test
void parse_ddMMyyyyDash_returnsDate() {
Date result = calendarUtil.parse("15-03-2026");
assertNotNull(result);
Calendar cal = toCalendar(result);
assertEquals(15, cal.get(Calendar.DAY_OF_MONTH));
assertEquals(Calendar.MARCH, cal.get(Calendar.MONTH));
assertEquals(2026, cal.get(Calendar.YEAR));
}
@Test
void parse_germanMonthName_dMMMMyyyy_returnsDate() {
Date result = calendarUtil.parse("5. Januar 2026");
assertNotNull(result);
Calendar cal = toCalendar(result);
assertEquals(5, cal.get(Calendar.DAY_OF_MONTH));
assertEquals(Calendar.JANUARY, cal.get(Calendar.MONTH));
assertEquals(2026, cal.get(Calendar.YEAR));
}
@Test
void parse_germanMonthName_ddMMMMyyyy_returnsDate() {
Date result = calendarUtil.parse("15 Dezember 2025");
assertNotNull(result);
Calendar cal = toCalendar(result);
assertEquals(15, cal.get(Calendar.DAY_OF_MONTH));
assertEquals(Calendar.DECEMBER, cal.get(Calendar.MONTH));
assertEquals(2025, cal.get(Calendar.YEAR));
}
@Test
void parse_invalidString_returnsNull() {
assertNull(calendarUtil.parse("not a date"));
}
@Test
void parse_emptyString_returnsNull() {
assertNull(calendarUtil.parse(""));
}
@Test
void parse_null_throwsException() {
assertThrows(IllegalArgumentException.class, () -> calendarUtil.parse(null));
}
// --- getStartOfDay ---
@Test
void getStartOfDay_returnsDateAtMidnight() {
Calendar input = Calendar.getInstance();
input.set(2026, Calendar.MARCH, 15, 14, 30, 45);
input.set(Calendar.MILLISECOND, 500);
Date result = calendarUtil.getStartOfDay(input.getTime());
Calendar cal = toCalendar(result);
assertEquals(2026, cal.get(Calendar.YEAR));
assertEquals(Calendar.MARCH, cal.get(Calendar.MONTH));
assertEquals(15, cal.get(Calendar.DAY_OF_MONTH));
assertEquals(0, cal.get(Calendar.HOUR_OF_DAY));
assertEquals(0, cal.get(Calendar.MINUTE));
assertEquals(0, cal.get(Calendar.SECOND));
assertEquals(0, cal.get(Calendar.MILLISECOND));
}
@Test
void getStartOfDay_alreadyMidnight_returnsSameTime() {
Calendar input = Calendar.getInstance();
input.set(2026, Calendar.JANUARY, 1, 0, 0, 0);
input.set(Calendar.MILLISECOND, 0);
Date result = calendarUtil.getStartOfDay(input.getTime());
Calendar cal = toCalendar(result);
assertEquals(0, cal.get(Calendar.HOUR_OF_DAY));
assertEquals(0, cal.get(Calendar.MINUTE));
assertEquals(0, cal.get(Calendar.SECOND));
assertEquals(0, cal.get(Calendar.MILLISECOND));
}
@Test
void getStartOfDay_endOfDay_returnsStartOfSameDay() {
Calendar input = Calendar.getInstance();
input.set(2026, Calendar.JUNE, 20, 23, 59, 59);
input.set(Calendar.MILLISECOND, 999);
Date result = calendarUtil.getStartOfDay(input.getTime());
Calendar cal = toCalendar(result);
assertEquals(20, cal.get(Calendar.DAY_OF_MONTH));
assertEquals(0, cal.get(Calendar.HOUR_OF_DAY));
assertEquals(0, cal.get(Calendar.MINUTE));
assertEquals(0, cal.get(Calendar.SECOND));
assertEquals(0, cal.get(Calendar.MILLISECOND));
}
// --- getEndOfDay ---
@Test
void getEndOfDay_returnsDateAt235959999() {
Calendar input = Calendar.getInstance();
input.set(2026, Calendar.MARCH, 15, 10, 0, 0);
input.set(Calendar.MILLISECOND, 0);
Date result = calendarUtil.getEndOfDay(input.getTime());
Calendar cal = toCalendar(result);
assertEquals(2026, cal.get(Calendar.YEAR));
assertEquals(Calendar.MARCH, cal.get(Calendar.MONTH));
assertEquals(15, cal.get(Calendar.DAY_OF_MONTH));
assertEquals(23, cal.get(Calendar.HOUR_OF_DAY));
assertEquals(59, cal.get(Calendar.MINUTE));
assertEquals(59, cal.get(Calendar.SECOND));
assertEquals(999, cal.get(Calendar.MILLISECOND));
}
@Test
void getEndOfDay_fromMidnight_returnsEndOfSameDay() {
Calendar input = Calendar.getInstance();
input.set(2026, Calendar.JANUARY, 1, 0, 0, 0);
input.set(Calendar.MILLISECOND, 0);
Date result = calendarUtil.getEndOfDay(input.getTime());
Calendar cal = toCalendar(result);
assertEquals(1, cal.get(Calendar.DAY_OF_MONTH));
assertEquals(23, cal.get(Calendar.HOUR_OF_DAY));
assertEquals(59, cal.get(Calendar.MINUTE));
assertEquals(59, cal.get(Calendar.SECOND));
assertEquals(999, cal.get(Calendar.MILLISECOND));
}
// --- startOfDay / endOfDay consistency ---
@Test
void startOfDay_isBeforeEndOfDay() {
Date now = new Date();
Date start = calendarUtil.getStartOfDay(now);
Date end = calendarUtil.getEndOfDay(now);
assertTrue(start.before(end));
}
@Test
void startAndEnd_preserveSameDay() {
Calendar input = Calendar.getInstance();
input.set(2026, Calendar.JULY, 4, 12, 0, 0);
Date start = calendarUtil.getStartOfDay(input.getTime());
Date end = calendarUtil.getEndOfDay(input.getTime());
Calendar startCal = toCalendar(start);
Calendar endCal = toCalendar(end);
assertEquals(startCal.get(Calendar.YEAR), endCal.get(Calendar.YEAR));
assertEquals(startCal.get(Calendar.MONTH), endCal.get(Calendar.MONTH));
assertEquals(startCal.get(Calendar.DAY_OF_MONTH), endCal.get(Calendar.DAY_OF_MONTH));
}
private Calendar toCalendar(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return cal;
}
}

View File

@@ -0,0 +1,60 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 3 Feb 2026
*/
class EvaluationUtilTest {
private EvaluationUtil evaluationUtil;
@BeforeEach
void setUp() {
evaluationUtil = new EvaluationUtil();
}
@Test
void isInValid_null_returnsTrue() {
assertTrue(evaluationUtil.isInValid(null));
}
@Test
void isInValid_zero_returnsTrue() {
assertTrue(evaluationUtil.isInValid(0));
}
@Test
void isInValid_negativeValue_returnsTrue() {
assertTrue(evaluationUtil.isInValid(-1));
}
@Test
void isInValid_one_returnsFalse() {
assertFalse(evaluationUtil.isInValid(1));
}
@Test
void isInValid_two_returnsFlse() {
assertFalse(evaluationUtil.isInValid(2));
}
@Test
void isInValid_three_returnsFalse() {
assertFalse(evaluationUtil.isInValid(3));
}
@Test
void isInValid_largeValue_returnsTrue() {
assertTrue(evaluationUtil.isInValid(100));
}
}

View File

@@ -0,0 +1,200 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Feb 2026
*/
class ExcelUtilsTest implements TestAble {
private ExcelUtils excelUtils;
@BeforeEach
void setUp() {
excelUtils = new ExcelUtils();
}
// --- create: basic output ---
@Test
void create_singleQuestionnaire_returnsPresent() {
QuestionnaireCustomer customer = createCustomer("Müller GmbH", "C-001", "Berlin", "10115");
Questionnaire questionnaire = createQuestionnaire(new Date());
Optional<byte[]> result = excelUtils.create(customer, List.of(questionnaire));
assertTrue(result.isPresent());
}
@Test
void create_singleQuestionnaire_returnsValidXlsx() throws IOException {
QuestionnaireCustomer customer = createCustomer("Müller GmbH", "C-001", "Berlin", "10115");
Questionnaire questionnaire = createQuestionnaire(new Date());
byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow();
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
assertNotNull(workbook);
}
}
@Test
void create_emptyQuestionnaires_returnsEmptyWorkbook() {
QuestionnaireCustomer customer = createCustomer("Müller GmbH", "C-001", "Berlin", "10115");
Optional<byte[]> result = excelUtils.create(customer, Collections.emptyList());
assertFalse(result.isPresent());
}
// --- create: sheet count ---
@Test
void create_singleQuestionnaire_createsOneSheet() throws IOException {
QuestionnaireCustomer customer = createCustomer("Müller GmbH", "C-001", "Berlin", "10115");
Questionnaire questionnaire = createQuestionnaire(new Date());
byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow();
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
assertEquals(1, workbook.getNumberOfSheets());
}
}
@Test
void create_multipleQuestionnaires_createsSheetPerQuestionnaire() throws IOException {
QuestionnaireCustomer customer = createCustomer("Müller GmbH", "C-001", "Berlin", "10115");
Questionnaire q1 = createQuestionnaire(new Date());
Questionnaire q2 = createQuestionnaire(new Date());
Questionnaire q3 = createQuestionnaire(new Date());
byte[] bytes = excelUtils.create(customer, List.of(q1, q2, q3)).orElseThrow();
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
assertEquals(3, workbook.getNumberOfSheets());
}
writeToFile(bytes, "create_multipleQuestionnaires_createsSheetPerQuestionnaire.xlsx");
}
// --- create: customer data (each field on its own row) ---
@Test
void create_writesCustomerNameInRow0() throws IOException {
QuestionnaireCustomer customer = createCustomer("Hartmann AG", "C-100", "München", "80331");
Questionnaire questionnaire = createQuestionnaire(new Date());
byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow();
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
assertEquals("Hartmann AG", workbook.getSheetAt(0).getRow(0).getCell(0).getStringCellValue());
assertEquals("C-100", workbook.getSheetAt(0).getRow(1).getCell(0).getStringCellValue());
assertEquals("München", workbook.getSheetAt(0).getRow(2).getCell(0).getStringCellValue());
assertEquals("80331", workbook.getSheetAt(0).getRow(3).getCell(0).getStringCellValue());
}
}
// --- create: question data in row 7 (after 5 customer rows + 2 blank rows) ---
@Test
void create_writesFirstQuestionTitleInRow7() throws IOException {
QuestionnaireCustomer customer = createCustomer("Test", "C-001", "Berlin", "10115");
Questionnaire questionnaire = createQuestionnaire(new Date());
byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow();
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
XSSFSheet sheet = workbook.getSheetAt(0);
// Row 7 should contain the title of the first question from testJson1
XSSFRow row = sheet.getRow(7);
assertNotNull(row);
assertNotNull(row.getCell(0));
}
}
// --- create: null handling ---
@Test
void create_nullCustomerFields_writesNullsWithoutError() {
QuestionnaireCustomer customer = createCustomer(null, null, null, null);
Questionnaire questionnaire = createQuestionnaire(null);
Optional<byte[]> result = excelUtils.create(customer, List.of(questionnaire));
assertTrue(result.isPresent());
}
// --- create: each sheet gets same customer data ---
@Test
void create_multipleSheets_eachSheetHasCustomerData() throws IOException {
QuestionnaireCustomer customer = createCustomer("Hartmann AG", "C-100", "München", "80331");
Questionnaire q1 = createQuestionnaire(new Date());
Questionnaire q2 = createQuestionnaire(new Date());
byte[] bytes = excelUtils.create(customer, List.of(q1, q2)).orElseThrow();
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
XSSFSheet sheet = workbook.getSheetAt(i);
assertEquals("Hartmann AG", sheet.getRow(0).getCell(0).getStringCellValue());
assertEquals("C-100", sheet.getRow(1).getCell(0).getStringCellValue());
}
}
}
@Test
void create_multipleSheets_eachSheetHasQuestionData() throws IOException {
QuestionnaireCustomer customer = createCustomer("Test", "C-001", "Berlin", "10115");
Questionnaire q1 = createQuestionnaire(new Date());
Questionnaire q2 = createQuestionnaire(new Date());
byte[] bytes = excelUtils.create(customer, List.of(q1, q2)).orElseThrow();
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
XSSFRow row = workbook.getSheetAt(i).getRow(7);
assertNotNull(row);
}
}
}
// --- helpers ---
private QuestionnaireCustomer createCustomer(String name, String number, String city, String zip) {
return new QuestionnaireCustomer.Builder()
.name(name)
.customerNumber(number)
.city(city)
.zip(zip)
.build();
}
private Questionnaire createQuestionnaire(Date date) {
return new Questionnaire.Builder()
.questionnaireDate(date)
.questions(QuestionnaireJsonParserTest.TEST_JSON_1)
.build();
}
}

View File

@@ -0,0 +1,428 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import javax.imageio.ImageIO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 3 Feb 2026
*/
class ImageUtilTest {
private ImageUtil imageUtil;
@BeforeEach
void setUp() {
imageUtil = new ImageUtil();
}
// --- Size 1: original ---
@Test
void getImage_size1_returnsOriginalBytes() {
String base64 = createTestImageBase64(800, 600);
byte[] original = Base64.getDecoder().decode(base64);
byte[] result = imageUtil.getImage(base64, 1);
assertArrayEquals(original, result);
}
@Test
void getImage_size1_largeImage_returnsOriginalBytes() {
String base64 = createTestImageBase64(2000, 1500);
byte[] original = Base64.getDecoder().decode(base64);
byte[] result = imageUtil.getImage(base64, 1);
assertArrayEquals(original, result);
}
// --- Size 2: normal (web, max 1200px) ---
@Test
void getImage_size2_largeImage_resizesToMaxWidth1200() throws IOException {
String base64 = createTestImageBase64(2400, 1600);
byte[] result = imageUtil.getImage(base64, 2);
BufferedImage resized = ImageIO.read(new ByteArrayInputStream(result));
assertEquals(1200, resized.getWidth());
}
@Test
void getImage_size2_largeImage_preservesAspectRatio() throws IOException {
String base64 = createTestImageBase64(2400, 1600);
byte[] result = imageUtil.getImage(base64, 2);
BufferedImage resized = ImageIO.read(new ByteArrayInputStream(result));
assertEquals(1200, resized.getWidth());
assertEquals(800, resized.getHeight());
}
@Test
void getImage_size2_largeImage_resultIsSmallerThanOriginal() {
String base64 = createNoisyImageBase64(2400, 1600);
byte[] original = Base64.getDecoder().decode(base64);
byte[] result = imageUtil.getImage(base64, 2);
assertTrue(result.length < original.length);
}
@Test
void getImage_size2_smallImage_returnsOriginal() {
String base64 = createTestImageBase64(800, 600);
byte[] original = Base64.getDecoder().decode(base64);
byte[] result = imageUtil.getImage(base64, 2);
assertArrayEquals(original, result);
}
@Test
void getImage_size2_exactMaxWidth_returnsOriginal() {
String base64 = createTestImageBase64(1200, 900);
byte[] original = Base64.getDecoder().decode(base64);
byte[] result = imageUtil.getImage(base64, 2);
assertArrayEquals(original, result);
}
// --- Size 3: thumbnail (max 200px) ---
@Test
void getImage_size3_largeImage_resizesToMaxWidth200() throws IOException {
String base64 = createTestImageBase64(2400, 1600);
byte[] result = imageUtil.getImage(base64, 3);
BufferedImage resized = ImageIO.read(new ByteArrayInputStream(result));
assertEquals(200, resized.getWidth());
}
@Test
void getImage_size3_largeImage_preservesAspectRatio() throws IOException {
String base64 = createTestImageBase64(2400, 1600);
byte[] result = imageUtil.getImage(base64, 3);
BufferedImage resized = ImageIO.read(new ByteArrayInputStream(result));
assertEquals(200, resized.getWidth());
assertEquals(133, resized.getHeight());
}
@Test
void getImage_size3_largeImage_resultIsSmallerThanNormal() {
String base64 = createTestImageBase64(2400, 1600);
byte[] normal = imageUtil.getImage(base64, 2);
byte[] thumbnail = imageUtil.getImage(base64, 3);
assertTrue(thumbnail.length < normal.length);
}
@Test
void getImage_size3_smallImage_returnsOriginal() {
String base64 = createTestImageBase64(150, 100);
byte[] original = Base64.getDecoder().decode(base64);
byte[] result = imageUtil.getImage(base64, 3);
assertArrayEquals(original, result);
}
// --- Default size ---
@Test
void getImage_unknownSize_returnsOriginalBytes() {
String base64 = createTestImageBase64(800, 600);
byte[] original = Base64.getDecoder().decode(base64);
byte[] result = imageUtil.getImage(base64, 99);
assertArrayEquals(original, result);
}
// --- Output format ---
@Test
void getImage_size2_outputIsJpeg() {
String base64 = createTestImageBase64(2400, 1600);
byte[] result = imageUtil.getImage(base64, 2);
// JPEG files start with FF D8 FF
assertEquals((byte) 0xFF, result[0]);
assertEquals((byte) 0xD8, result[1]);
assertEquals((byte) 0xFF, result[2]);
}
@Test
void getImage_size3_outputIsJpeg() {
String base64 = createTestImageBase64(2400, 1600);
byte[] result = imageUtil.getImage(base64, 3);
assertEquals((byte) 0xFF, result[0]);
assertEquals((byte) 0xD8, result[1]);
assertEquals((byte) 0xFF, result[2]);
}
// --- EXIF orientation: applyOrientation ---
@Test
void applyOrientation_orientation1_returnsSameInstance() {
BufferedImage image = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 1);
assertSame(image, result);
}
@Test
void applyOrientation_orientation2_horizontalFlip_preservesDimensions() {
BufferedImage image = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 2);
assertEquals(800, result.getWidth());
assertEquals(600, result.getHeight());
}
@Test
void applyOrientation_orientation3_rotate180_preservesDimensions() {
BufferedImage image = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 3);
assertEquals(800, result.getWidth());
assertEquals(600, result.getHeight());
}
@Test
void applyOrientation_orientation4_verticalFlip_preservesDimensions() {
BufferedImage image = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 4);
assertEquals(800, result.getWidth());
assertEquals(600, result.getHeight());
}
@Test
void applyOrientation_orientation5_swapsDimensions() {
BufferedImage image = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 5);
assertEquals(600, result.getWidth());
assertEquals(800, result.getHeight());
}
@Test
void applyOrientation_orientation6_rotate90cw_swapsDimensions() {
BufferedImage image = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 6);
assertEquals(600, result.getWidth());
assertEquals(800, result.getHeight());
}
@Test
void applyOrientation_orientation7_swapsDimensions() {
BufferedImage image = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 7);
assertEquals(600, result.getWidth());
assertEquals(800, result.getHeight());
}
@Test
void applyOrientation_orientation8_rotate90ccw_swapsDimensions() {
BufferedImage image = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 8);
assertEquals(600, result.getWidth());
assertEquals(800, result.getHeight());
}
@Test
void applyOrientation_unknownOrientation_returnsSameInstance() {
BufferedImage image = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 99);
assertSame(image, result);
}
@Test
void applyOrientation_orientation2_flipsPixelsHorizontally() {
BufferedImage image = new BufferedImage(4, 2, BufferedImage.TYPE_INT_RGB);
image.setRGB(0, 0, 0xFF0000); // red at top-left
BufferedImage result = imageUtil.applyOrientation(image, 2);
assertEquals(0xFF0000, result.getRGB(3, 0) & 0xFFFFFF); // red at top-right
}
@Test
void applyOrientation_orientation3_rotates180() {
BufferedImage image = new BufferedImage(4, 2, BufferedImage.TYPE_INT_RGB);
image.setRGB(0, 0, 0xFF0000); // red at top-left
BufferedImage result = imageUtil.applyOrientation(image, 3);
assertEquals(0xFF0000, result.getRGB(3, 1) & 0xFFFFFF); // red at bottom-right
}
@Test
void applyOrientation_orientation6_rotates90cw_returnsNewImage() {
BufferedImage image = new BufferedImage(4, 2, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 6);
assertNotSame(image, result);
assertEquals(2, result.getWidth());
assertEquals(4, result.getHeight());
}
@Test
void applyOrientation_orientation8_rotates90ccw_returnsNewImage() {
BufferedImage image = new BufferedImage(4, 2, BufferedImage.TYPE_INT_RGB);
BufferedImage result = imageUtil.applyOrientation(image, 8);
assertNotSame(image, result);
assertEquals(2, result.getWidth());
assertEquals(4, result.getHeight());
}
// --- EXIF orientation: getExifOrientation ---
@Test
void getExifOrientation_pngWithNoExif_returns1() {
String base64 = createTestImageBase64(100, 100);
byte[] imageBytes = Base64.getDecoder().decode(base64);
int orientation = imageUtil.getExifOrientation(imageBytes);
assertEquals(1, orientation);
}
@Test
void getExifOrientation_invalidBytes_returns1() {
byte[] garbage = new byte[] { 0x00, 0x01, 0x02, 0x03 };
int orientation = imageUtil.getExifOrientation(garbage);
assertEquals(1, orientation);
}
// --- EXIF orientation through getImage (size 1 / default) ---
@Test
void getImage_size1_noExif_returnsOriginalUnchanged() {
String base64 = createTestImageBase64(400, 300);
byte[] original = Base64.getDecoder().decode(base64);
byte[] result = imageUtil.getImage(base64, 1);
assertArrayEquals(original, result);
}
@Test
void getImage_defaultSize_noExif_returnsOriginalUnchanged() {
String base64 = createTestImageBase64(400, 300);
byte[] original = Base64.getDecoder().decode(base64);
byte[] result = imageUtil.getImage(base64, 42);
assertArrayEquals(original, result);
}
// --- EXIF orientation with resize ---
@Test
void getImage_size2_rotatedImage_usesEffectiveWidthForResizeDecision() {
// A tall image (600x800) with no EXIF won't be resized since width (600) < 1200
String base64 = createTestImageBase64(600, 800);
byte[] original = Base64.getDecoder().decode(base64);
byte[] result = imageUtil.getImage(base64, 2);
assertArrayEquals(original, result);
}
// --- Edge cases ---
@Test
void getImage_size2_squareImage_preservesAspectRatio() throws IOException {
String base64 = createTestImageBase64(2000, 2000);
byte[] result = imageUtil.getImage(base64, 2);
BufferedImage resized = ImageIO.read(new ByteArrayInputStream(result));
assertEquals(1200, resized.getWidth());
assertEquals(1200, resized.getHeight());
}
@Test
void getImage_size2_veryWideImage_preservesAspectRatio() throws IOException {
String base64 = createTestImageBase64(4000, 500);
byte[] result = imageUtil.getImage(base64, 2);
BufferedImage resized = ImageIO.read(new ByteArrayInputStream(result));
assertEquals(1200, resized.getWidth());
assertEquals(150, resized.getHeight());
}
private String createTestImageBase64(int width, int height) {
try {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
return Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String createNoisyImageBase64(int width, int height) {
try {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
java.util.Random random = new java.util.Random(42);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
image.setRGB(x, y, random.nextInt(0xFFFFFF));
}
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
return Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,293 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.imageio.ImageIO;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 2 Feb 2026
*/
class PdfUtilsTest {
private static final Log LOG = LogFactory.getLog(PdfUtilsTest.class);
private PdfUtils pdfUtils;
private Customer customer;
@BeforeEach
void setUp() {
pdfUtils = new PdfUtils();
customer = new Customer.Builder()
.name("Apotheke Musterstadt")
.customerNumber("KD-12345")
.zip("50667")
.city("Köln")
.build();
}
@Test
void createPdf_singlePicture_returnsValidPdf() throws IOException {
Picture picture = createPicture(new Date(), "Schaufenster Dekoration", 1);
List<Picture> pictures = List.of(picture);
byte[] pdfBytes = pdfUtils.createPdf(customer, pictures);
assertNotNull(pdfBytes);
assertTrue(pdfBytes.length > 0);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
writeToFile(pdfBytes, "createPdf_singlePicture_returnsValidPdf.pdf");
}
@Test
void createPdf_multiplePictures_createsOnPagePerPicture() throws IOException {
List<Picture> pictures = List.of(
createPicture(new Date(), "Bild 1", 1),
createPicture(new Date(), longComment, 2),
createPicture(new Date(), "Bild 3", 3));
byte[] pdfBytes = pdfUtils.createPdf(customer, pictures);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(4, document.getNumberOfPages());
}
writeToFile(pdfBytes, "createPdf_multiplePictures_createsOnPagePerPicture.pdf");
}
@Test
void createPdf_containsCustomerName() throws IOException {
Picture picture = createPicture(new Date(), "Test", 1);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
String text = extractText(pdfBytes);
assertTrue(text.contains("Apotheke Musterstadt"));
}
@Test
void createPdf_containsCustomerNumber() throws IOException {
Picture picture = createPicture(new Date(), "Test", 1);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
String text = extractText(pdfBytes);
assertTrue(text.contains("KD-12345"));
}
@Test
void createPdf_containsLabels() throws IOException {
Picture picture = createPicture(new Date(), "Test Kommentar", 2);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
String text = extractText(pdfBytes);
assertTrue(text.contains("KUNDENNUMMER"));
assertTrue(text.contains("PLZ"));
assertTrue(text.contains("ORT"));
assertTrue(text.contains("KOMMENTAR"));
}
@Test
void createPdf_containsZipAndCity() throws IOException {
Picture picture = createPicture(new Date(), "Test", 1);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
String text = extractText(pdfBytes);
assertTrue(text.contains("50667"));
assertTrue(text.contains("Köln"));
}
@Test
void createPdf_containsComment() throws IOException {
Picture picture = createPicture(new Date(), "Wichtiger Kommentar zum Bild", 1);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
String text = extractText(pdfBytes);
assertTrue(text.contains("Wichtiger Kommentar zum Bild"));
}
@Test
void createPdf_emptyPictureList_returnsValidEmptyPdf() throws IOException {
byte[] pdfBytes = pdfUtils.createPdf(customer, Collections.emptyList());
assertNotNull(pdfBytes);
assertTrue(pdfBytes.length > 0);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(0, document.getNumberOfPages());
}
}
@Test
void createPdf_nullImage_doesNotThrow() throws IOException {
Picture picture = new Picture.Builder()
.pictureDate(new Date())
.comment("Ohne Bild")
.customer(customer)
.build();
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
assertNotNull(pdfBytes);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
}
@Test
void createPdf_nullComment_doesNotThrow() throws IOException {
Picture picture = new Picture.Builder()
.pictureDate(new Date())
.image(createTestImageBase64())
.customer(customer)
.build();
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
assertNotNull(pdfBytes);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
}
@Test
void createPdf_nullDate_doesNotThrow() throws IOException {
Picture picture = new Picture.Builder()
.comment("Kein Datum")
.image(createTestImageBase64())
.customer(customer)
.build();
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
assertNotNull(pdfBytes);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
}
@Test
void createPdf_nullEvaluation_doesNotThrow() {
Picture picture = new Picture.Builder()
.pictureDate(new Date())
.comment("Test")
.image(createTestImageBase64())
.customer(customer)
.build();
// evaluation defaults to 0 in Builder
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
assertNotNull(pdfBytes);
assertTrue(pdfBytes.length > 0);
}
@Test
void createPdf_allEvaluationValues_produceValidPdf() throws IOException {
for (int eval = 1; eval <= 3; eval++) {
Picture picture = createPicture(new Date(), "Eval " + eval, eval);
byte[] pdfBytes = pdfUtils.createPdf(customer, List.of(picture));
assertNotNull(pdfBytes);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
}
}
@Test
void createPdf_nullFieldsOnCustomer_doesNotThrow() throws IOException {
Customer emptyCustomer = new Customer.Builder()
.name("Test")
.customerNumber("000")
.build();
Picture picture = createPicture(new Date(), "Test", 1);
byte[] pdfBytes = pdfUtils.createPdf(emptyCustomer, List.of(picture));
assertNotNull(pdfBytes);
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
assertEquals(1, document.getNumberOfPages());
}
}
private Picture createPicture(Date date, String comment, int evaluation) {
return new Picture.Builder()
.pictureDate(date)
.comment(comment)
.evaluation(evaluation)
.image(createTestImageBase64())
.customer(customer)
.build();
}
private String createTestImageBase64() {
try {
BufferedImage image = new BufferedImage(100, 80, BufferedImage.TYPE_INT_RGB);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
return Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String extractText(byte[] pdfBytes) throws IOException {
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
PDFTextStripper stripper = new PDFTextStripper();
return stripper.getText(document);
}
}
public void writeToFile(final byte[] content, final String fileName) {
File file = new File("target/test/output/");
file.mkdirs();
try (FileOutputStream out = new FileOutputStream(new File(file, fileName))) {
IOUtils.write(content, out);
} catch (Exception e) {
LOG.error("Error saveing pdf file", e);
}
}
String longComment = "This is a sample text used for unit testing purposes. It contains multiple sentences with different structures, punctuation marks, and line breaks. The goal is to simulate realistic content that a program might process during normal execution. Developers often need such text to verify that parsing, searching, filtering, or transformation logic behaves as expected.\n"
+ "\n"
+ "The text includes numbers like 12345, special characters such as @, #, and %, and mixed casing to ensure case-insensitive comparisons can be tested properly. It also contains repeated keywords like skillmatrix and SkillMatrix to validate string matching and normalization features.\n"
+ "\n"
+ "Additionally, this paragraph spans several lines to test newline handling and formatting behavior. Unit tests may check whether the system correctly reads files, counts words, trims whitespace, or handles empty lines without errors.\n"
+ "\n"
+ "Overall, this content is intentionally generic but sufficiently detailed to serve as stable input data for automated tests.";
}

View File

@@ -0,0 +1,349 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 20 Feb 2026
*/
public class QuestionnaireJsonParserTest {
@Test
public void testJson1() {
var parser = new QuestionnaireJsonParser();
boolean retVal = parser.parse(TEST_JSON_1);
assertTrue(retVal);
var questions = parser.getQuestions();
assertEquals(10, questions.size());
}
public static final String TEST_JSON_1 = """
[
{
"id": "question1",
"title": "",
"order": 1,
"type": "singleChoice",
"data":
{
"answers":
[
{
"id": "answer1",
"answer": "Nicht-kaufender Kunde",
"selected": true
},
{
"id": "answer2",
"answer": "Bestandskunde",
"selected": false
}
]
}
},
{
"id": "question2",
"title": "Haben Sie Rezeptpatienten(GKV) für Inko?",
"order": 2,
"type": "singleChoice",
"data":
{
"answers":
[
{
"id": "answer1",
"answer": "Ja",
"selected": false
},
{
"id": "answer1",
"answer": "Nein",
"selected": true
}
]
}
},
{
"id": "question3",
"title": "Warum nicht?",
"order": 3,
"type": "multiplChoice",
"data":
{
"answers":
[
{
"id": "answer1",
"answer": "Lagergründe",
"selected": false
},
{
"id": "answer2",
"answer": "Wirtschaftlichkeitsgründe",
"selected": true
},
{
"id": "answer3",
"answer": "Administrativer Aufwand",
"selected": true
},
{
"id": "answer4",
"answer": "Personeller Aufwand",
"selected": false
}
]
}
},
{
"id": "question4",
"title": "Haben Sie Privatrezeptpatienten für inko?",
"order": 4,
"type": "singleChoice",
"data":
{
"answers":
[
{
"id": "answer1",
"answer": "Ja",
"selected": false
},
{
"id": "answer2",
"answer": "Nein",
"selected": true
}
]
}
},
{
"id": "question5",
"title": "Wie viele Patienten versorgen Sie regelmäßig? (Privat) un GKV",
"order": 5,
"type": "number",
"data":
{
"answers":
[
{
"id": "answer1",
"answer": "47",
"selected": true
}
]
}
},
{
"id": "question6",
"title": "Mit welchem Herstellern arbeiten Sie zusammen?",
"order": 6,
"type": "multiplChoice",
"data":
{
"answers":
[
{
"id": "answer1",
"answer": "HARTMANN",
"selected": false
},
{
"id": "answer2",
"answer": "TZMO",
"selected": true
},
{
"id": "answer3",
"answer": "Essity",
"selected": true
},
{
"id": "answer4",
"answer": "Ontex",
"selected": false
},
{
"id": "answer5",
"answer": "Param",
"selected": false
},
{
"id": "answer6",
"answer": "Andere",
"selected": false
}
]
}
},
{
"id": "question7",
"title": "Was sind Ihre Gründe für die Zusammenarbeit?",
"order": 7,
"type": "matrix",
"data":
{
"questions":
[
{
"id": "subq1",
"title": "Preis",
"order": 1,
"answers":
[
{
"id": "answer1",
"answer": "HARTMANN",
"selected": false
},
{
"id": "answer2",
"answer": "Essity",
"selected": true
},
{
"id": "answer3",
"answer": "Ontex",
"selected": false
}
]
},
{
"id": "subq2",
"title": "Einkaufskondition",
"order": 2,
"answers":
[
{
"id": "answer1",
"answer": "HARTMANN",
"selected": false
},
{
"id": "answer2",
"answer": "Essity",
"selected": true
},
{
"id": "answer3",
"answer": "Ontex",
"selected": false
}
]
},
{
"id": "subq3",
"title": "Qualität",
"order": 3,
"answers":
[
{
"id": "answer1",
"answer": "HARTMANN",
"selected": false
},
{
"id": "answer2",
"answer": "Essity",
"selected": true
},
{
"id": "answer3",
"answer": "Ontex",
"selected": false
}
]
}
]
}
},
{
"id": "question8",
"title": "Beziehen Sie Produkte direkt oder über den Großhandel?",
"order": 8,
"type": "singleChoice",
"data":
{
"answers":
[
{
"id": "answer1",
"answer": "primär direkt",
"selected": false
},
{
"id": "answer2",
"answer": "primär Großhandel",
"selected": true
},
{
"id": "answer3",
"answer": "ptimär teils. Großhandel",
"selected": true
},
{
"id": "answer4",
"answer": "unterschiedlich",
"selected": false
}
]
}
},
{
"id": "question9",
"title": "Gründe für Bezug?",
"order": 10,
"type": "singleChoice",
"data":
{
"answers":
[
{
"id": "answer1",
"answer": "Umsatzziel mit Händler",
"selected": false
},
{
"id": "answer2",
"answer": "Warenverfügbarkeit/ Liefergeschwindigkeit und Frequent",
"selected": true
},
{
"id": "answer3",
"answer": "Einkaufskondition",
"selected": true
}
]
}
},
{
"id": "question10",
"title": "Weiter/Kommentare Hinweise?",
"order": 11,
"type": "freeText",
"data":
{
"answers":
[
{
"id": "answer1",
"answer": "Kommentar eintragen",
"selected": true
}
]
}
}
]
""";
}

View File

@@ -0,0 +1,270 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Optional;
import org.junit.jupiter.api.Test;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 20 Feb 2026
*/
class QuestionnaireUploadJsonParserTest {
// --- builder: successful parsing ---
@Test
void builder_validJson_returnsPresent() {
String json = createValidJson();
Optional<QuestionnaireUploadJsonParser> result = QuestionnaireUploadJsonParser.builder(json);
assertTrue(result.isPresent());
}
@Test
void builder_validJson_parsesUsername() {
String json = createValidJson();
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertEquals("verboomp", parser.username());
}
@Test
void builder_validJson_parsesPharmacyName() {
String json = createValidJson();
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertEquals("Müller Apotheke", parser.pharmacyName());
}
@Test
void builder_validJson_parsesCustomerNumber() {
String json = createValidJson();
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertEquals("1234", parser.customerNumber());
}
@Test
void builder_validJson_parsesDate() {
String json = createValidJson();
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertNotNull(parser.date());
}
@Test
void builder_validJson_parsesComment() {
String json = createValidJson();
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertEquals("Some long text", parser.comment());
}
@Test
void builder_validJson_parsesCity() {
String json = createValidJson();
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertEquals("Hannover", parser.city());
}
@Test
void builder_validJson_parsesZip() {
String json = createValidJson();
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertEquals("12345", parser.zip());
}
@Test
void builder_validJson_parsesQuestionnaireAsJsonString() {
String json = createValidJson();
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertNotNull(parser.questionnair());
assertTrue(parser.questionnair().contains("question1"));
assertTrue(parser.questionnair().contains("singleChoice"));
}
@Test
void builder_validJson_questionnaireContainsAnswers() {
String json = createValidJson();
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertTrue(parser.questionnair().contains("answer1"));
assertTrue(parser.questionnair().contains("Ja"));
}
// --- builder: multiple questions ---
@Test
void builder_multipleQuestions_allQuestionsInOutput() {
String json = createJsonWithMultipleQuestions();
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertTrue(parser.questionnair().contains("question1"));
assertTrue(parser.questionnair().contains("question2"));
}
// --- builder: invalid input ---
@Test
void builder_invalidJson_returnsEmpty() {
String json = "not valid json";
Optional<QuestionnaireUploadJsonParser> result = QuestionnaireUploadJsonParser.builder(json);
assertTrue(result.isEmpty());
}
@Test
void builder_emptyObject_returnsEmpty() {
String json = "{}";
Optional<QuestionnaireUploadJsonParser> result = QuestionnaireUploadJsonParser.builder(json);
assertTrue(result.isEmpty());
}
@Test
void builder_missingRequiredField_returnsEmpty() {
String json = """
{
"username": "verboomp",
"pharmacyName": "Test"
}""";
Optional<QuestionnaireUploadJsonParser> result = QuestionnaireUploadJsonParser.builder(json);
assertTrue(result.isEmpty());
}
@Test
void builder_invalidDateFormat_returnsEmpty() {
String json = """
{
"username": "verboomp",
"pharmacyName": "Test",
"customerNumber": "1234",
"date": "not-a-date",
"comment": "test",
"city": "Berlin",
"zip": "10115",
"questionnaire": [{"id": "q1", "title": "Q", "order": 1, "type": "freeText", "data": {"answers": []}}]
}""";
Optional<QuestionnaireUploadJsonParser> result = QuestionnaireUploadJsonParser.builder(json);
assertTrue(result.isEmpty());
}
// --- builder: special characters ---
@Test
void builder_unicodeCharacters_parsedCorrectly() {
String json = createJsonWithValues("user1", "Löwen Apotheke", "5678", "Köln", "50667");
QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow();
assertEquals("Löwen Apotheke", parser.pharmacyName());
assertEquals("Köln", parser.city());
}
// --- helpers ---
private String createValidJson() {
return """
{
"username": "verboomp",
"pharmacyName": "Müller Apotheke",
"customerNumber": "1234",
"date": "2026-01-20T11:06:00+01:00",
"comment": "Some long text",
"city": "Hannover",
"zip": "12345",
"questionnaire": [
{
"id": "question1",
"title": "Kundentyp",
"order": 1,
"type": "singleChoice",
"data": {
"answers": [
{"id": "answer1", "answer": "Ja", "selected": true},
{"id": "answer2", "answer": "Nein", "selected": false}
]
}
}
]
}""";
}
private String createJsonWithMultipleQuestions() {
return """
{
"username": "verboomp",
"pharmacyName": "Test Apotheke",
"customerNumber": "9999",
"date": "2026-01-20T11:06:00+01:00",
"comment": "comment",
"city": "Berlin",
"zip": "10115",
"questionnaire": [
{
"id": "question1",
"title": "First",
"order": 1,
"type": "singleChoice",
"data": {"answers": [{"id": "a1", "answer": "Yes", "selected": true}]}
},
{
"id": "question2",
"title": "Second",
"order": 2,
"type": "freeText",
"data": {"answers": [{"id": "a1", "answer": "Some text", "selected": true}]}
}
]
}""";
}
private String createJsonWithValues(String username, String pharmacyName, String customerNumber, String city, String zip) {
return """
{
"username": "%s",
"pharmacyName": "%s",
"customerNumber": "%s",
"date": "2026-01-20T11:06:00+01:00",
"comment": "test",
"city": "%s",
"zip": "%s",
"questionnaire": [
{
"id": "q1",
"title": "Q",
"order": 1,
"type": "freeText",
"data": {"answers": [{"id": "a1", "answer": "text", "selected": true}]}
}
]
}""".formatted(username, pharmacyName, customerNumber, city, zip);
}
}

View File

@@ -0,0 +1,33 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.io.File;
import java.io.FileOutputStream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 23 Feb 2026
*/
public interface TestAble {
public static final Log LOG = LogFactory.getLog(ExcelUtilsTest.class);
default void writeToFile(final byte[] content, final String fileName) {
File file = new File("target/test/output/");
file.mkdirs();
try (FileOutputStream out = new FileOutputStream(new File(file, fileName))) {
IOUtils.write(content, out);
} catch (Exception e) {
LOG.error("Error saveing pdf file", e);
}
}
}

View File

@@ -0,0 +1,224 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 23 Feb 2026
*/
class ZipExportUtilsTest implements TestAble {
private ZipExportUtils zipExportUtils;
private ExcelUtils excelUtils;
@BeforeEach
void setUp() throws Exception {
zipExportUtils = new ZipExportUtils();
excelUtils = mock(ExcelUtils.class);
Field field = ZipExportUtils.class.getDeclaredField("excelUtils");
field.setAccessible(true);
field.set(zipExportUtils, excelUtils);
}
// --- getExport: empty input ---
@Test
void getExport_emptyList_returnsEmpty() {
Optional<byte[]> result = zipExportUtils.getExport(Collections.emptyList());
assertTrue(result.isEmpty());
}
// --- getExport: single customer ---
@Test
void getExport_singleCustomer_returnsPresent() {
QuestionnaireCustomer customer = createCustomerWithQuestionnaires("Müller Apotheke", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(new byte[] { 1, 2, 3 }));
Optional<byte[]> result = zipExportUtils.getExport(List.of(customer));
assertTrue(result.isPresent());
}
@Test
void getExport_singleCustomer_returnsValidZip() throws IOException {
QuestionnaireCustomer customer = createCustomerWithQuestionnaires("Müller Apotheke", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(new byte[] { 1, 2, 3 }));
byte[] bytes = zipExportUtils.getExport(List.of(customer)).orElseThrow();
List<String> entries = getZipEntryNames(bytes);
assertEquals(1, entries.size());
}
@Test
void getExport_singleCustomer_zipEntryNamedAfterCustomer() throws IOException {
QuestionnaireCustomer customer = createCustomerWithQuestionnaires("Müller Apotheke", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(new byte[] { 1, 2, 3 }));
byte[] bytes = zipExportUtils.getExport(List.of(customer)).orElseThrow();
List<String> entries = getZipEntryNames(bytes);
assertEquals("Müller Apotheke.xlsx", entries.get(0));
}
@Test
void getExport_singleCustomer_zipEntryContainsExcelContent() throws IOException {
byte[] excelContent = new byte[] { 10, 20, 30, 40, 50 };
QuestionnaireCustomer customer = createCustomerWithQuestionnaires("Test", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(excelContent));
byte[] bytes = zipExportUtils.getExport(List.of(customer)).orElseThrow();
byte[] entryContent = getZipEntryContent(bytes, "Test.xlsx");
assertArrayEquals(excelContent, entryContent);
}
// --- getExport: multiple customers ---
@Test
void getExport_multipleCustomers_createsEntryPerCustomer() throws IOException {
QuestionnaireCustomer c1 = createCustomerWithQuestionnaires("Apotheke A", 1);
QuestionnaireCustomer c2 = createCustomerWithQuestionnaires("Apotheke B", 1);
QuestionnaireCustomer c3 = createCustomerWithQuestionnaires("Apotheke C", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(new byte[] { 1 }));
byte[] bytes = zipExportUtils.getExport(List.of(c1, c2, c3)).orElseThrow();
List<String> entries = getZipEntryNames(bytes);
assertEquals(3, entries.size());
assertTrue(entries.contains("Apotheke A.xlsx"));
assertTrue(entries.contains("Apotheke B.xlsx"));
assertTrue(entries.contains("Apotheke C.xlsx"));
writeToFile(bytes, "getExport_multipleCustomers_createsEntryPerCustomer.zip");
}
// --- getExport: excel creation fails ---
@Test
void getExport_excelReturnsEmpty_returnsEmpty() {
QuestionnaireCustomer customer = createCustomerWithQuestionnaires("Test", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.empty());
Optional<byte[]> result = zipExportUtils.getExport(List.of(customer));
assertTrue(result.isEmpty());
}
@Test
void getExport_someExcelsFail_onlyIncludesSuccessful() throws IOException {
QuestionnaireCustomer c1 = createCustomerWithQuestionnaires("Success", 1);
QuestionnaireCustomer c2 = createCustomerWithQuestionnaires("Fail", 1);
when(excelUtils.create(eq(c1), anyList())).thenReturn(Optional.of(new byte[] { 1 }));
when(excelUtils.create(eq(c2), anyList())).thenReturn(Optional.empty());
byte[] bytes = zipExportUtils.getExport(List.of(c1, c2)).orElseThrow();
List<String> entries = getZipEntryNames(bytes);
assertEquals(1, entries.size());
assertEquals("Success.xlsx", entries.get(0));
}
// --- getExport: questionnaire sorting ---
@Test
void getExport_questionnairesPassedSortedByDate() {
QuestionnaireCustomer customer = new QuestionnaireCustomer.Builder()
.name("Test").customerNumber("C-001").build();
Questionnaire q1 = new Questionnaire.Builder().questionnaireDate(new Date(2000)).build();
Questionnaire q2 = new Questionnaire.Builder().questionnaireDate(new Date(1000)).build();
Questionnaire q3 = new Questionnaire.Builder().questionnaireDate(new Date(3000)).build();
customer.getQuestionnaires().addAll(Set.of(q1, q2, q3));
when(excelUtils.create(any(), anyList())).thenAnswer(invocation -> {
List<Questionnaire> questionnaires = invocation.getArgument(1);
for (int i = 0; i < questionnaires.size() - 1; i++) {
assertTrue(questionnaires.get(i).getQuestionnaireDate()
.compareTo(questionnaires.get(i + 1).getQuestionnaireDate()) <= 0,
"Questionnaires should be sorted by date");
}
return Optional.of(new byte[] { 1 });
});
zipExportUtils.getExport(List.of(customer));
verify(excelUtils).create(eq(customer), anyList());
}
// --- getExport: calls excelUtils correctly ---
@Test
void getExport_callsExcelUtilsForEachCustomer() {
QuestionnaireCustomer c1 = createCustomerWithQuestionnaires("A", 1);
QuestionnaireCustomer c2 = createCustomerWithQuestionnaires("B", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(new byte[] { 1 }));
zipExportUtils.getExport(List.of(c1, c2));
verify(excelUtils).create(eq(c1), anyList());
verify(excelUtils).create(eq(c2), anyList());
}
// --- helpers ---
private QuestionnaireCustomer createCustomerWithQuestionnaires(String name, int questionnaireCount) {
QuestionnaireCustomer customer = new QuestionnaireCustomer.Builder()
.name(name).customerNumber("C-" + name.hashCode()).build();
for (int i = 0; i < questionnaireCount; i++) {
Questionnaire q = new Questionnaire.Builder()
.questionnaireDate(new Date(1000L * (i + 1)))
.questions("[]")
.build();
customer.getQuestionnaires().add(q);
}
return customer;
}
private List<String> getZipEntryNames(byte[] zipBytes) throws IOException {
List<String> names = new ArrayList<>();
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
names.add(entry.getName());
}
}
return names;
}
private byte[] getZipEntryContent(byte[] zipBytes, String entryName) throws IOException {
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().equals(entryName)) {
return zis.readAllBytes();
}
}
}
throw new IOException("Entry not found: " + entryName);
}
}

View File

@@ -0,0 +1,19 @@
package org.mockito.configuration;
/**
*
* <p>Copyright: Copyright (c) 2016</p>
* <p>Company: heyday marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: Oct 14, 2016
*/
public class MockitoConfiguration extends DefaultMockitoConfiguration {
@Override
public boolean enableClassCache() {
return false;
}
}

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
<appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out"/>
<param name="Threshold" value="DEBUG"/>
<layout class="org.apache.log4j.PatternLayout">
<!-- The default pattern: Date Priority [Category] Message\n -->
<param name="ConversionPattern" value="%d{ABSOLUTE} %-5p [%c] %m%n"/>
</layout>
</appender>
<!-- ================ -->
<!-- Limit categories -->
<!-- ================ -->
<!-- Limit the org.apache category to INFO as its DEBUG is verbose -->
<category name="org.apache">
<priority value="INFO"/>
</category>
<category name="com.bm">
<priority value="INFO"/>
</category>
<category name="com.bm.introspectors">
<priority value="ERROR"/>
</category>
<category name="org.hibernate.cfg.annotations">
<priority value="WARN"/>
</category>
<category name="org.hibernate.cfg">
<priority value="WARN"/>
</category>
<category name="org.hibernate.tool">
<priority value="WARN"/>
</category>
<category name="org.hibernate.validator">
<priority value="WARN"/>
</category>
<category name="org.hibernate">
<priority value="ERROR"/>
</category>
<category name="org.dbunit">
<priority value="DEBUG"/>
</category>
<category name="de.juwimm">
<priority value="DEBUG"/>
</category>
<category name="STDOUT">
<priority value="DEBUG"/>
</category>
<root>
<appender-ref ref="CONSOLE"/>
</root>
</log4j:configuration>

View File

@@ -0,0 +1,530 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>marketing.heyday.hartmann.fotodocumentation</groupId>
<artifactId>hartmann-foto-documentation</artifactId>
<version>1.0.1</version>
<relativePath>../hartmann-foto-documentation/pom.xml</relativePath>
</parent>
<artifactId>hartmann-foto-documentation-docker</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>hartmann-foto-documentation docker</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<configuration>
<skip>true</skip>
<skipDeploy>true</skipDeploy>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M1</version>
<configuration>
<skipTests>true</skipTests>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<sonar.java.libraries>${project.build.directory}/lib/*.jar</sonar.java.libraries>
<sonar.libraries>${project.build.directory}/lib/*.jar</sonar.libraries>
</properties>
<profiles>
<profile>
<id>docker</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.1</version>
<configuration>
<skip>true</skip>
<skipDeploy>true</skipDeploy>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M1</version>
<configuration>
<skip>true</skip>
<skipTests>true</skipTests>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>test</goal>
</goals>
<phase>integration-test</phase>
<configuration>
<skip>false</skip>
<skipTests>false</skipTests>
<argLine />
<forkMode>once</forkMode>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.45.1</version>
<extensions>true</extensions>
<configuration>
<autoPull>true</autoPull>
<startParallel>false</startParallel>
<showLogs>true</showLogs>
<autoCreateCustomNetworks>true</autoCreateCustomNetworks>
<images>
<image>
<alias>hartmann_postgres</alias>
<name>postgres:11</name>
<run>
<ports>
<port>5430:5432</port>
</ports>
<network>
<name>hartmann_nw</name>
<alias>hartmann_postgres</alias>
</network>
<env>
<POSTGRES_HOST_AUTH_METHOD>trust</POSTGRES_HOST_AUTH_METHOD>
</env>
<volumes>
<bind>
<volume>${basedir}/src/main/docker/sql:/docker-entrypoint-initdb.d</volume>
</bind>
</volumes>
<wait>
<log>database system is ready to accept connections</log>
<time>2500000</time>
</wait>
</run>
</image>
<image>
<!-- Artifact Image -->
<name>${project.artifactId}</name>
<alias>hartmann</alias>
<build>
<dockerFile>Dockerfile</dockerFile>
<args>
<deploymentDir>maven</deploymentDir>
</args>
<tags>
<tag>latest</tag>
</tags>
</build>
<registry>hub.heyday.marketing</registry>
<run>
<cmd>
/srv/wait-for-it.sh
hartmann_postgres:5432" --
/srv/wildfly/bin/standalone-jacoco.sh
-b 0.0.0.0 -c test-standalone.xml
</cmd>
<ports>
<port>8180:8080</port>
</ports>
<dependsOn>
<container>hartmann_postgres</container>
</dependsOn>
<network>
<name>hartmann_nw</name>
<alias>hartmann</alias>
</network>
<volumes>
<bind>
<volume>${basedir}/target:/srv/target</volume>
</bind>
</volumes>
<wait>
<!-- The plugin waits until this URL
is reachable via HTTP ... -->
<http>
<url>http://${docker.host.address}:8180/api/monitoring/check/hello</url>
</http>
<time>300000</time>
</wait>
</run>
</image>
</images>
</configuration>
<executions>
<execution>
<id>build</id>
<phase>package</phase>
<goals>
<goal>build</goal>
<!-- <goal>push</goal> -->
</goals>
</execution>
<execution>
<id>start</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
<executions>
<execution>
<id>copy-resources</id>
<!-- here the phase you need -->
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/src/main/docker</outputDirectory>
<resources>
<resource>
<directory>${basedir}/../hartmann-foto-documentation-web/target/</directory>
<includes>
<include>*.war</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-resources2</id>
<!-- here the phase you need -->
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.basedir}/target/classes</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/../hartmann-foto-documentation-app/target/classes</directory>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-jacoco-app</id>
<!-- here the phase you need -->
<phase>verify</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.basedir}/../hartmann-foto-documentation-app/target</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/target</directory>
<filtering>false</filtering>
<includes>
<include>jacoco-it.exec</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version> <!-- was 3.11.0 -->
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-edge-driver</artifactId>
</exclusion>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-firefox-driver</artifactId>
</exclusion>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-ie-driver</artifactId>
</exclusion>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-opera-driver</artifactId>
</exclusion>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-safari-driver</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-api</artifactId>
<version>3.141.59</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-chrome-driver</artifactId>
<version>3.141.59</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-remote-driver</artifactId>
<version>3.141.59</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-support</artifactId>
<version>3.141.59</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hartmann-foto-documentation-web</artifactId>
<version>${project.version}</version>
<type>war</type>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hartmann-foto-documentation-app</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Websocket test api's -->
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-client-api</artifactId>
<version>1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-client</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-container-grizzly</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.json</groupId>
<artifactId>javax.json-api</artifactId>
<version>1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
<!-- Websocket test api's -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${version.commons-lang3}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.7.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<version>${version.org.jboss.resteasy}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<version>1.9.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${version.commons-io}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${version.commons-logging}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>fluent-hc</artifactId>
<version>4.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.5</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,52 @@
############################################################
# Dockerfile to build wildfly Hartmann Foto Documentation docker image
# Based on jboss/wildfly
############################################################
# Pull base image.
FROM hub.heyday.marketing/verboomp/wildfly:33.0.2-Final
# File Author / Maintainer
LABEL org.opencontainers.image.authors="Patrick Verboom <p.verboom@heyday.marketing>"
USER root
RUN apt-get update && apt-get install -y unzip
# install the postgres driver
RUN wget 'https://nexus.heyday.marketing/repository/maven-releases/com/ayeqbenu/wildfly/database/modules/postgres-jdbc/1.1.0/postgres-jdbc-1.1.0.zip' -O /srv/postgres-jdbc.zip \
&& unzip /srv/postgres-jdbc.zip -d ${JBOSS_HOME}/modules \
&& rm -rf /srv/postgres-jdbc.zip
# install the startup wait script to make sure postgres is booted before wildfly
COPY wait-for-it.sh /srv/wait-for-it.sh
RUN chmod 777 /srv/wait-for-it.sh
# copy the jacoco agent for code coverage
COPY jacoco/jacocoagent.jar /srv/jacocoagent.jar
run chmod 777 /srv/jacocoagent.jar
COPY standalone-jacoco.sh ${JBOSS_HOME}/bin/standalone-jacoco.sh
RUN chmod 777 ${JBOSS_HOME}/bin/standalone-jacoco.sh
# install the wildfly config xml file
# COPY test-standalone.xml ${JBOSS_HOME}/standalone/configuration/test-standalone.xml
COPY standalone-fotodocumentation.xml ${JBOSS_HOME}/standalone/configuration/test-standalone.xml
# install the web war file
COPY hartmann-foto-documentation-web-*.war ${JBOSS_HOME}/standalone/deployments
# Expose the ports we're interested in
EXPOSE 8080
EXPOSE 9990
# Set the default command to run on boot
# This will boot WildFly in the standalone mode and bind to all interface
CMD ["/srv/wildfly/bin/standalone.sh", "-b", "0.0.0.0", "-c", "test-standalone.xml"]

View File

@@ -0,0 +1,40 @@
services:
smtp-server:
image: andreptb/smtp-server-for-it:0.2.0
ports:
- "8280:8080"
networks:
- hartmann_nw
environment:
- smtp.port=26
hartmann_postgres:
image: postgres:11
volumes:
- ./sql:/docker-entrypoint-initdb.d
ports:
- "5430:5432"
networks:
- hartmann_nw
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
hartmann:
build: .
ports:
- "8180:8080"
- "9990:9990"
depends_on:
- hartmann_postgres
networks:
- hartmann_nw
command: ["/srv/wait-for-it.sh", "cue_postgres:5432", "--", "/srv/wildfly/bin/standalone-jacoco.sh", "-b", "0.0.0.0", "-c", "test-standalone.xml"]
volumes:
- ./../../../target:/srv/target:z
networks:
hartmann_nw:
volumes:
jacoco:

View File

@@ -0,0 +1,80 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 9.4.5
-- Dumped by pg_dump version 9.6.5
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: fotodocumentation; Type: DATABASE; Schema: -; Owner: postgres
--
CREATE ROLE fotodocumentation WITH
LOGIN
NOSUPERUSER
NOCREATEDB
NOCREATEROLE
INHERIT
NOREPLICATION
CONNECTION LIMIT -1
PASSWORD 'fotodocumentation';
CREATE DATABASE fotodocumentation WITH TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8';
ALTER DATABASE fotodocumentation OWNER TO postgres;
\connect fotodocumentation
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
--
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
--
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner:
--
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
SET search_path = public, pg_catalog;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- Name: public; Type: ACL; Schema: -; Owner: postgres
--
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM postgres;
GRANT ALL ON SCHEMA public TO postgres;
GRANT ALL ON SCHEMA public TO PUBLIC;
--
-- PostgreSQL database dump complete
--

View File

@@ -0,0 +1,666 @@
<?xml version="1.0" encoding="UTF-8"?>
<server xmlns="urn:jboss:domain:20.0">
<extensions>
<extension module="org.jboss.as.clustering.infinispan"/>
<extension module="org.jboss.as.connector"/>
<extension module="org.jboss.as.deployment-scanner"/>
<extension module="org.jboss.as.ee"/>
<extension module="org.jboss.as.ejb3"/>
<extension module="org.jboss.as.jaxrs"/>
<extension module="org.jboss.as.jdr"/>
<extension module="org.jboss.as.jmx"/>
<extension module="org.jboss.as.jpa"/>
<extension module="org.jboss.as.jsf"/>
<extension module="org.jboss.as.logging"/>
<extension module="org.jboss.as.mail"/>
<extension module="org.jboss.as.naming"/>
<extension module="org.jboss.as.pojo"/>
<extension module="org.jboss.as.remoting"/>
<extension module="org.jboss.as.sar"/>
<extension module="org.jboss.as.transactions"/>
<extension module="org.jboss.as.webservices"/>
<extension module="org.jboss.as.weld"/>
<extension module="org.wildfly.extension.batch.jberet"/>
<extension module="org.wildfly.extension.bean-validation"/>
<extension module="org.wildfly.extension.clustering.ejb"/>
<extension module="org.wildfly.extension.clustering.web"/>
<extension module="org.wildfly.extension.core-management"/>
<extension module="org.wildfly.extension.discovery"/>
<extension module="org.wildfly.extension.ee-security"/>
<extension module="org.wildfly.extension.elytron"/>
<extension module="org.wildfly.extension.elytron-oidc-client"/>
<extension module="org.wildfly.extension.health"/>
<extension module="org.wildfly.extension.io"/>
<extension module="org.wildfly.extension.metrics"/>
<extension module="org.wildfly.extension.microprofile.config-smallrye"/>
<extension module="org.wildfly.extension.microprofile.jwt-smallrye"/>
<extension module="org.wildfly.extension.request-controller"/>
<extension module="org.wildfly.extension.security.manager"/>
<extension module="org.wildfly.extension.undertow"/>
</extensions>
<system-properties>
<property name="jwt.secret.key" value="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCK1EvBSGUg/+Id
TNnlqXWkWtLypRDW5YtQ1ilT046AQfPyTCK9WGJUqqtxfiOxQ0qKVWsXZd/3JwHP
nqpgORxOlkSpCMJo4syflqSwJ/Zqg6nNEQXErNg1L2/6tM7DW3KnNfW5yujvIDm+
UJJCbmJtQ6tYaNqygQhL6nvDiP5jMwPmdAgg/dUyHJKrNOvF9znUF0360wNG8x/Q
WsmGKPddMD4k7fttA5/GRszRM/WRMbPogsz43PlERchTrYF+nY/4lnD3fNqppJ5u
uskj/G86ux4hmb/C9W+uf5NxTtURTv3H5TJW/jPZ424MoGvzQCeQa0lgNDLAIC+W
Y81e+ZT9AgMBAAECggEAOu3LbDNZLe0/4zUMZvaMB6Q/15xmbfmIrdsCNuFdoyab
sJVNx7adIphBZs7mwqcwHFEOwKNPMp9dnu4YHvkPAXK6mU+tCg1/UxyEMnv8FpFl
wbSAkM/XhJfqve4CuBz4qW53rBIr1tkEebrEoqstX3jyYfg8ILoxtdvGBiV/6cYN
Oyy01xd0NMz+JwHuk9l9ADHDqMJiPkJ1zsHqoqOsblIHSiFSyqRAJU1ns6/If/pV
DI17G2j16lqK9S+fMltJXDfEGhz0A/c9R60nvQObEOtpUCFWS0DP75oZT2AvNqkY
/7aLcBu7FPr522+zFzQrIGON1rJXS/qIBQu0x151JwKBgQDCQGAd+t3qn5Pi63xq
4GFEMZHqn3wyXhhcwg6yQFKrIr8wP3Mmr8utXC0HdncXb80bavkbV8Ii6ttItWDo
eRv3KhJe+83bS0nJc2QQtfKBt1Dg+1TBSsTwQHYKvKSThNcG3ijqtv6Juac+WqfB
H/G5lD5yI4xMquWDyuK6hRNkhwKBgQC29dKdDogFg9cdhpArChduVVGaMu0Ifvt+
oOsy3IVPeOPlXyeDIINi7tw17+WSwm3gS0TVqamcefYIhaBlmRwrYS8wHiyQiMfX
tgthWXtX5z+lw2MdUfAwW6oDRQLVf7YIsas1Loe7KZRoXEbuBeUP/XdpRO1gtUDR
gGL6e3OfWwKBgBlGJPthm6QeVTCOMSb6wM0Nog2j6JXpFkRjX2Qj6F2p7LRLXSEo
eFi7CITTDhW3jzlFBtpe5byDUDq6lrxInbHgAHnpS1SADD6wy9E8yyvDfTt4mAN6
Rft4d6NX/hXPj+at2ycG3kFvLWp4gyEmld3ugt148JU9GxW1vSBFlktbAoGAN1wO
TEN3WOPZlS+AM+WrzVC3jkbWffmeM2SRhiQ/mhpkKqUuGXkfCDJqI0/hURTPlkxw
GY5qqdQlY9K7A8LeSSnw00huB5W7kkOdEem3bpOkKI4EUXzXhmpV+QNKpjssY1kP
Ctp3a2RbaXBybdcOxlXVad7XTKnLYRjN2ii8hX0CgYEApRFzPDU+leHXGIjPZwjA
WcZ7IN+B5pdwJUfqulzx73WtOCfuf2J7HQ0pcaOkG2BOxBY1AGtgPDl7071uYvfR
hbZlR027QB9GpO8pQKZ98UquAmQNTOBI0k0RX9XZAK2ae60SM8NXFFF1TDZMoKud
eZlo8cWlAC5welD3dz1qxEo=
-----END PRIVATE KEY-----"/>
</system-properties>
<management>
<audit-log>
<formatters>
<json-formatter name="json-formatter"/>
</formatters>
<handlers>
<file-handler name="file" formatter="json-formatter" path="audit-log.log" relative-to="jboss.server.data.dir"/>
</handlers>
<logger log-boot="true" log-read-only="false" enabled="false">
<handlers>
<handler name="file"/>
</handlers>
</logger>
</audit-log>
<management-interfaces>
<http-interface http-authentication-factory="management-http-authentication">
<http-upgrade enabled="true" sasl-authentication-factory="management-sasl-authentication"/>
<socket-binding http="management-http"/>
</http-interface>
</management-interfaces>
<access-control provider="simple">
<role-mapping>
<role name="SuperUser">
<include>
<user name="$local"/>
</include>
</role>
</role-mapping>
</access-control>
</management>
<profile>
<subsystem xmlns="urn:jboss:domain:logging:8.0">
<console-handler name="CONSOLE">
<level name="DEBUG"/>
<formatter>
<named-formatter name="COLOR-PATTERN"/>
</formatter>
</console-handler>
<periodic-rotating-file-handler name="FILE" autoflush="true">
<formatter>
<named-formatter name="PATTERN"/>
</formatter>
<file relative-to="jboss.server.log.dir" path="server.log"/>
<suffix value=".yyyy-MM-dd"/>
<append value="true"/>
</periodic-rotating-file-handler>
<logger category="com.arjuna">
<level name="WARN"/>
</logger>
<logger category="com.networknt.schema">
<level name="WARN"/>
</logger>
<logger category="io.jaegertracing.Configuration">
<level name="WARN"/>
</logger>
<logger category="org.jboss.as.config">
<level name="DEBUG"/>
</logger>
<logger category="org.jboss">
<level name="INFO"/>
</logger>
<logger category="org.hibernate">
<level name="INFO"/>
</logger>
<logger category="marketing.heyday.hartmann">
<level name="DEBUG"/>
</logger>
<logger category="sun.rmi">
<level name="WARN"/>
</logger>
<root-logger>
<level name="DEBUG"/>
<handlers>
<handler name="CONSOLE"/>
<handler name="FILE"/>
</handlers>
</root-logger>
<formatter name="PATTERN">
<pattern-formatter pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
</formatter>
<formatter name="COLOR-PATTERN">
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
</formatter>
</subsystem>
<subsystem xmlns="urn:jboss:domain:batch-jberet:3.0">
<default-job-repository name="in-memory"/>
<default-thread-pool name="batch"/>
<security-domain name="ApplicationDomain"/>
<job-repository name="in-memory">
<in-memory/>
</job-repository>
<thread-pool name="batch">
<max-threads count="10"/>
<keepalive-time time="30" unit="seconds"/>
</thread-pool>
</subsystem>
<subsystem xmlns="urn:jboss:domain:bean-validation:1.0"/>
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
<subsystem xmlns="urn:jboss:domain:datasources:7.1">
<datasources>
<!-- Patrick -->
<datasource jndi-name="java:/jdbc/fotoDocumentationDS" pool-name="fotoDocumentationDS" enabled="true" use-java-context="true" use-ccm="false">
<connection-url>jdbc:postgresql://hartmann_postgres:5432/fotodocumentation</connection-url>
<driver>postgres</driver>
<transaction-isolation>TRANSACTION_READ_COMMITTED</transaction-isolation>
<pool>
<min-pool-size>1</min-pool-size>
<max-pool-size>10</max-pool-size>
</pool>
<security user-name="fotodocumentation" password="fotodocumentation"/>
<validation>
<valid-connection-checker class-name="org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker"/>
<exception-sorter class-name="org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter"/>
</validation>
<statement>
<prepared-statement-cache-size>32</prepared-statement-cache-size>
<share-prepared-statements>true</share-prepared-statements>
</statement>
</datasource>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=${wildfly.h2.compatibility.mode:REGULAR}</connection-url>
<driver>h2</driver>
<security user-name="sa" password="sa"/>
</datasource>
<drivers>
<!-- Patrick -->
<driver name="postgres" module="org.postgresql.jdbc">
<xa-datasource-class>org.postgresql.xa.PGXADataSource</xa-datasource-class>
</driver>
<driver name="h2" module="com.h2database.h2">
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
</driver>
</drivers>
</datasources>
</subsystem>
<subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
<deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:discovery:1.0"/>
<subsystem xmlns="urn:jboss:domain:distributable-ejb:1.0" default-bean-management="default">
<infinispan-bean-management name="default" max-active-beans="10000" cache-container="ejb" cache="passivation"/>
<local-client-mappings-registry/>
<infinispan-timer-management name="persistent" cache-container="ejb" cache="persistent" max-active-timers="10000"/>
<infinispan-timer-management name="transient" cache-container="ejb" cache="transient" max-active-timers="10000"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:distributable-web:4.0" default-session-management="default" default-single-sign-on-management="default">
<infinispan-session-management name="default" cache-container="web" granularity="SESSION">
<local-affinity/>
</infinispan-session-management>
<infinispan-single-sign-on-management name="default" cache-container="web" cache="sso"/>
<local-routing/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:ee:6.0">
<spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
<concurrent>
<context-services>
<context-service name="default" jndi-name="java:jboss/ee/concurrency/context/default"/>
</context-services>
<managed-thread-factories>
<managed-thread-factory name="default" jndi-name="java:jboss/ee/concurrency/factory/default" context-service="default"/>
</managed-thread-factories>
<managed-executor-services>
<managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-termination-period="0" hung-task-threshold="60000" keepalive-time="5000"/>
</managed-executor-services>
<managed-scheduled-executor-services>
<managed-scheduled-executor-service name="default" jndi-name="java:jboss/ee/concurrency/scheduler/default" context-service="default" hung-task-termination-period="0" hung-task-threshold="60000" keepalive-time="3000"/>
</managed-scheduled-executor-services>
</concurrent>
<default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/ExampleDS" managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default" managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:ee-security:1.0"/>
<subsystem xmlns="urn:jboss:domain:ejb3:10.0">
<session-bean>
<stateless>
<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
</stateless>
<stateful default-access-timeout="5000" cache-ref="simple" passivation-disabled-cache-ref="simple"/>
<singleton default-access-timeout="5000"/>
</session-bean>
<pools>
<bean-instance-pools>
<strict-max-pool name="slsb-strict-max-pool" derive-size="from-worker-pools" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
<strict-max-pool name="mdb-strict-max-pool" derive-size="from-cpu-count" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
</bean-instance-pools>
</pools>
<caches>
<simple-cache name="simple"/>
<distributable-cache name="distributable"/>
</caches>
<async thread-pool-name="default"/>
<timer-service thread-pool-name="default" default-data-store="default-file-store">
<data-stores>
<file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
</data-stores>
</timer-service>
<remote connectors="http-remoting-connector" thread-pool-name="default">
<channel-creation-options>
<option name="MAX_OUTBOUND_MESSAGES" value="1234" type="remoting"/>
</channel-creation-options>
</remote>
<thread-pools>
<thread-pool name="default">
<max-threads count="10"/>
<keepalive-time time="60" unit="seconds"/>
</thread-pool>
</thread-pools>
<default-security-domain value="other"/>
<application-security-domains>
<application-security-domain name="other" security-domain="ApplicationDomain"/>
<!--patrick-->
<application-security-domain name="fotoDocumentationSecurity" security-domain="fotoDocumentationDomain"/>
</application-security-domains>
<default-missing-method-permissions-deny-access value="true"/>
<statistics enabled="${wildfly.ejb3.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
<log-system-exceptions value="true"/>
</subsystem>
<subsystem xmlns="urn:wildfly:elytron:community:18.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
<providers>
<aggregate-providers name="combined-providers">
<providers name="elytron"/>
<providers name="openssl"/>
</aggregate-providers>
<provider-loader name="elytron" module="org.wildfly.security.elytron"/>
<provider-loader name="openssl" module="org.wildfly.openssl"/>
</providers>
<audit-logging>
<file-audit-log name="local-audit" path="audit.log" relative-to="jboss.server.log.dir" format="JSON"/>
</audit-logging>
<security-domains>
<security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
<realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
<realm name="local"/>
</security-domain>
<security-domain name="ManagementDomain" default-realm="ManagementRealm" permission-mapper="default-permission-mapper">
<realm name="ManagementRealm" role-decoder="groups-to-roles"/>
<realm name="local" role-mapper="super-user-mapper"/>
</security-domain>
<!-- patrick -->
<security-domain name="fotoDocumentationDomain" default-realm="fotoDocumentationRealm" permission-mapper="default-permission-mapper">
<realm name="fotoDocumentationRealm" role-decoder="groups-to-roles"/>
</security-domain>
</security-domains>
<security-realms>
<identity-realm name="local" identity="$local"/>
<properties-realm name="ApplicationRealm">
<users-properties path="application-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ApplicationRealm"/>
<groups-properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
</properties-realm>
<properties-realm name="ManagementRealm">
<users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
<groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
</properties-realm>
<!-- patrick -->
<distributed-realm name="fotoDocumentationRealm" realms="fotoDocumentationJwtRealm fotoDocumentationJdbcRealm" />
<!-- patrick -->
<token-realm name="fotoDocumentationJwtRealm" principal-claim="username">
<jwt issuer="foto-jwt-issuer" audience="foto-api"
public-key="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAitRLwUhlIP/iHUzZ5al1
pFrS8qUQ1uWLUNYpU9OOgEHz8kwivVhiVKqrcX4jsUNKilVrF2Xf9ycBz56qYDkc
TpZEqQjCaOLMn5aksCf2aoOpzREFxKzYNS9v+rTOw1typzX1ucro7yA5vlCSQm5i
bUOrWGjasoEIS+p7w4j+YzMD5nQIIP3VMhySqzTrxfc51BdN+tMDRvMf0FrJhij3
XTA+JO37bQOfxkbM0TP1kTGz6ILM+Nz5REXIU62Bfp2P+JZw93zaqaSebrrJI/xv
OrseIZm/wvVvrn+TcU7VEU79x+UyVv4z2eNuDKBr80AnkGtJYDQywCAvlmPNXvmU
/QIDAQAB
-----END PUBLIC KEY-----"
/>
</token-realm>
<!-- patrick -->
<jdbc-realm name="fotoDocumentationJdbcRealm" >
<principal-query data-source="fotoDocumentationDS" sql="select password, salt, ri.code as Role from x_user u left join user_to_right rtr on rtr.user_id_fk = u.user_id left join x_right ri on rtr.right_id_fk = ri.right_id where username = ?;">
<salted-simple-digest-mapper algorithm="password-salt-digest-sha-256" password-index="1" salt-index="2" />
<attribute-mapping>
<attribute to="groups" index="3"/>
</attribute-mapping>
</principal-query>
</jdbc-realm>
</security-realms>
<mappers>
<simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
<permission-mapping>
<principal name="anonymous"/>
<permission-set name="default-permissions"/>
</permission-mapping>
<permission-mapping match-all="true">
<permission-set name="login-permission"/>
<permission-set name="default-permissions"/>
</permission-mapping>
</simple-permission-mapper>
<constant-realm-mapper name="local" realm-name="local"/>
<simple-role-decoder name="groups-to-roles" attribute="groups"/>
<constant-role-mapper name="super-user-mapper">
<role name="SuperUser"/>
</constant-role-mapper>
</mappers>
<permission-sets>
<permission-set name="login-permission">
<permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
</permission-set>
<permission-set name="default-permissions">
<permission class-name="org.wildfly.transaction.client.RemoteTransactionPermission" module="org.wildfly.transaction.client"/>
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
<permission class-name="org.wildfly.extension.batch.jberet.deployment.BatchPermission" module="org.wildfly.extension.batch.jberet" target-name="*"/>
</permission-set>
</permission-sets>
<http>
<http-authentication-factory name="application-http-authentication" security-domain="ApplicationDomain" http-server-mechanism-factory="global">
<mechanism-configuration>
<mechanism mechanism-name="BASIC">
<mechanism-realm realm-name="ApplicationRealm"/>
</mechanism>
</mechanism-configuration>
</http-authentication-factory>
<http-authentication-factory name="management-http-authentication" security-domain="ManagementDomain" http-server-mechanism-factory="global">
<mechanism-configuration>
<mechanism mechanism-name="DIGEST">
<mechanism-realm realm-name="ManagementRealm"/>
</mechanism>
</mechanism-configuration>
</http-authentication-factory>
<!-- patrick -->
<http-authentication-factory name="fotoDocumentation-http-authentication" security-domain="fotoDocumentationDomain" http-server-mechanism-factory="global">
<mechanism-configuration>
<mechanism mechanism-name="BEARER_TOKEN">
<mechanism-realm realm-name="fotoDocumentationRealm"/>
</mechanism>
</mechanism-configuration>
</http-authentication-factory>
<provider-http-server-mechanism-factory name="global"/>
</http>
<sasl>
<sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
<mechanism-configuration>
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
<mechanism mechanism-name="DIGEST-MD5">
<mechanism-realm realm-name="ApplicationRealm"/>
</mechanism>
</mechanism-configuration>
</sasl-authentication-factory>
<sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
<mechanism-configuration>
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
<mechanism mechanism-name="DIGEST-MD5">
<mechanism-realm realm-name="ManagementRealm"/>
</mechanism>
</mechanism-configuration>
</sasl-authentication-factory>
<configurable-sasl-server-factory name="configured" sasl-server-factory="elytron">
<properties>
<property name="wildfly.sasl.local-user.default-user" value="$local"/>
<property name="wildfly.sasl.local-user.challenge-path" value="${jboss.server.temp.dir}/auth"/>
</properties>
</configurable-sasl-server-factory>
<mechanism-provider-filtering-sasl-server-factory name="elytron" sasl-server-factory="global">
<filters>
<filter provider-name="WildFlyElytron"/>
</filters>
</mechanism-provider-filtering-sasl-server-factory>
<provider-sasl-server-factory name="global"/>
</sasl>
<tls>
<key-stores>
<key-store name="applicationKS">
<credential-reference clear-text="password"/>
<implementation type="JKS"/>
<file path="application.keystore" relative-to="jboss.server.config.dir"/>
</key-store>
</key-stores>
<key-managers>
<key-manager name="applicationKM" key-store="applicationKS" generate-self-signed-certificate-host="localhost">
<credential-reference clear-text="password"/>
</key-manager>
</key-managers>
<server-ssl-contexts>
<server-ssl-context name="applicationSSC" key-manager="applicationKM"/>
</server-ssl-contexts>
</tls>
<policy name="jacc">
<jacc-policy/>
</policy>
</subsystem>
<subsystem xmlns="urn:wildfly:elytron-oidc-client:2.0"/>
<subsystem xmlns="urn:wildfly:health:1.0" security-enabled="false"/>
<subsystem xmlns="urn:jboss:domain:infinispan:14.0">
<cache-container name="hibernate" marshaller="JBOSS" modules="org.infinispan.hibernate-cache">
<local-cache name="entity">
<heap-memory size="10000"/>
<expiration max-idle="100000"/>
</local-cache>
<local-cache name="local-query">
<heap-memory size="10000"/>
<expiration max-idle="100000"/>
</local-cache>
<local-cache name="timestamps">
<expiration interval="0"/>
</local-cache>
<local-cache name="pending-puts">
<expiration max-idle="60000"/>
</local-cache>
</cache-container>
<cache-container name="ejb" default-cache="passivation" marshaller="PROTOSTREAM" aliases="sfsb" modules="org.wildfly.clustering.ejb.infinispan">
<local-cache name="passivation">
<expiration interval="0"/>
<file-store passivation="true"/>
</local-cache>
<local-cache name="persistent">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<expiration interval="0"/>
<file-store preload="true"/>
</local-cache>
<local-cache name="transient">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<expiration interval="0"/>
<file-store passivation="true" purge="true"/>
</local-cache>
</cache-container>
<cache-container name="web" default-cache="passivation" marshaller="PROTOSTREAM" modules="org.wildfly.clustering.web.infinispan">
<local-cache name="passivation">
<expiration interval="0"/>
<file-store passivation="true"/>
</local-cache>
<local-cache name="sso">
<expiration interval="0"/>
</local-cache>
</cache-container>
</subsystem>
<subsystem xmlns="urn:jboss:domain:io:4.0" default-worker="default">
<worker name="default"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jaxrs:3.0"/>
<subsystem xmlns="urn:jboss:domain:jca:6.0">
<archive-validation enabled="true" fail-on-error="true" fail-on-warn="false"/>
<bean-validation enabled="true"/>
<default-workmanager>
<short-running-threads>
<core-threads count="50"/>
<queue-length count="50"/>
<max-threads count="50"/>
<keepalive-time time="10" unit="seconds"/>
</short-running-threads>
<long-running-threads>
<core-threads count="50"/>
<queue-length count="50"/>
<max-threads count="50"/>
<keepalive-time time="10" unit="seconds"/>
</long-running-threads>
</default-workmanager>
<cached-connection-manager/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jdr:1.0"/>
<subsystem xmlns="urn:jboss:domain:jmx:1.3">
<expose-resolved-model/>
<expose-expression-model/>
<remoting-connector/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jpa:1.1">
<jpa default-extended-persistence-inheritance="DEEP"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jsf:1.1"/>
<subsystem xmlns="urn:jboss:domain:mail:4.0">
<mail-session name="default" jndi-name="java:jboss/mail/Default">
<smtp-server outbound-socket-binding-ref="mail-smtp"/>
</mail-session>
<!-- Patrick -->
<mail-session name="fotoDocumentationMail" debug="true" jndi-name="java:/mail/fotoDocumentation-mail">
<smtp-server outbound-socket-binding-ref="mail-smtp"/>
</mail-session>
</subsystem>
<subsystem xmlns="urn:wildfly:metrics:1.0" security-enabled="false" exposed-subsystems="*" prefix="${wildfly.metrics.prefix:wildfly}"/>
<subsystem xmlns="urn:wildfly:microprofile-config-smallrye:2.0"/>
<subsystem xmlns="urn:wildfly:microprofile-jwt-smallrye:1.0"/>
<subsystem xmlns="urn:jboss:domain:naming:2.0">
<remote-naming/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:pojo:1.0"/>
<subsystem xmlns="urn:jboss:domain:remoting:7.0">
<endpoint worker="default"/>
<http-connector name="http-remoting-connector" connector-ref="default" sasl-authentication-factory="application-sasl-authentication"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:request-controller:1.0"/>
<subsystem xmlns="urn:jboss:domain:resource-adapters:7.1"/>
<subsystem xmlns="urn:jboss:domain:sar:1.0"/>
<subsystem xmlns="urn:jboss:domain:security-manager:1.0">
<deployment-permissions>
<maximum-set>
<permission class="java.security.AllPermission"/>
</maximum-set>
</deployment-permissions>
</subsystem>
<subsystem xmlns="urn:jboss:domain:transactions:6.0">
<core-environment node-identifier="${jboss.tx.node.id:1}">
<process-id>
<uuid/>
</process-id>
</core-environment>
<recovery-environment socket-binding="txn-recovery-environment" status-socket-binding="txn-status-manager"/>
<coordinator-environment statistics-enabled="${wildfly.transactions.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
<object-store path="tx-object-store" relative-to="jboss.server.data.dir"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:undertow:14.0" default-virtual-host="default-host" default-servlet-container="default" default-server="default-server" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}" default-security-domain="other">
<byte-buffer-pool name="default"/>
<buffer-cache name="default"/>
<server name="default-server">
<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"/>
<https-listener name="https" socket-binding="https" ssl-context="applicationSSC" enable-http2="true"/>
<host name="default-host" alias="localhost">
<location name="/" handler="welcome-content"/>
<http-invoker http-authentication-factory="application-http-authentication"/>
<!-- patrick -->
<filter-ref name="server-header"/>
<filter-ref name="x-powered-by-header"/>
<filter-ref name="Access-Control-Allow-Origin"/>
<filter-ref name="Access-Control-Allow-Methods"/>
<filter-ref name="Access-Control-Allow-Headers"/>
<filter-ref name="Access-Control-Allow-Credentials"/>
<filter-ref name="Access-Control-Max-Age"/>
</host>
</server>
<servlet-container name="default">
<jsp-config/>
<websockets/>
</servlet-container>
<handlers>
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
</handlers>
<application-security-domains>
<application-security-domain name="other" security-domain="ApplicationDomain"/>
<!-- patrick -->
<application-security-domain name="fotoDocumentationSecurity" http-authentication-factory="fotoDocumentation-http-authentication"/>
</application-security-domains>
<!-- patrick -->
<filters>
<response-header name="server-header" header-name="Server" header-value="WildFly/10"/>
<response-header name="x-powered-by-header" header-name="X-Powered-By" header-value="Undertow/1"/>
<response-header name="Access-Control-Allow-Origin" header-name="Access-Control-Allow-Origin" header-value="http://localhost:1234"/>
<response-header name="Access-Control-Allow-Methods" header-name="Access-Control-Allow-Methods" header-value="GET, POST, OPTIONS, PUT, DELETE"/>
<response-header name="Access-Control-Allow-Headers" header-name="Access-Control-Allow-Headers" header-value="accept, authorization, content-type, x-requested-with, xsrf-token"/>
<response-header name="Access-Control-Expose-Headers" header-name="Access-Control-Expose-Headers" header-value="xsrf-token"/>
<response-header name="Access-Control-Allow-Credentials" header-name="Access-Control-Allow-Credentials" header-value="true"/>
<response-header name="Access-Control-Max-Age" header-name="Access-Control-Max-Age" header-value="1"/>
</filters>
</subsystem>
<subsystem xmlns="urn:jboss:domain:webservices:2.0" statistics-enabled="${wildfly.webservices.statistics-enabled:${wildfly.statistics-enabled:false}}">
<wsdl-host>${jboss.bind.address:127.0.0.1}</wsdl-host>
<endpoint-config name="Standard-Endpoint-Config"/>
<endpoint-config name="Recording-Endpoint-Config">
<pre-handler-chain name="recording-handlers" protocol-bindings="##SOAP11_HTTP ##SOAP11_HTTP_MTOM ##SOAP12_HTTP ##SOAP12_HTTP_MTOM">
<handler name="RecordingHandler" class="org.jboss.ws.common.invocation.RecordingServerHandler"/>
</pre-handler-chain>
</endpoint-config>
<client-config name="Standard-Client-Config"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:weld:5.0"/>
</profile>
<interfaces>
<interface name="management">
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
</interface>
<interface name="public">
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
</interface>
</interfaces>
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
<socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>
<socket-binding name="http" port="${jboss.http.port:8080}"/>
<socket-binding name="https" port="${jboss.https.port:8443}"/>
<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
<socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
<socket-binding name="txn-recovery-environment" port="4712"/>
<socket-binding name="txn-status-manager" port="4713"/>
<outbound-socket-binding name="mail-smtp">
<remote-destination host="smtp-server" port="26"/>
</outbound-socket-binding>
</socket-binding-group>
</server>

View File

@@ -0,0 +1,3 @@
#!/bin/bash
export JAVA_OPTS="$JAVA_OPTS -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true -javaagent:/srv/jacocoagent.jar=destfile=/srv/target/jacoco-it.exec,includes=marketing/heyday/hartmann/fotodocumentation/*,output=file "
exec `dirname $0`/standalone.sh $@

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available
cmdname=$(basename $0)
echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $TIMEOUT -gt 0 ]]; then
echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT"
else
echoerr "$cmdname: waiting for $HOST:$PORT without a timeout"
fi
start_ts=$(date +%s)
while :
do
if [[ $ISBUSY -eq 1 ]]; then
nc -z $HOST $PORT
result=$?
else
(echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1
result=$?
fi
if [[ $result -eq 0 ]]; then
end_ts=$(date +%s)
echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds"
break
fi
sleep 1
done
return $result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $QUIET -eq 1 ]]; then
timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
else
timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
fi
PID=$!
trap "kill -INT -$PID" INT
wait $PID
RESULT=$?
if [[ $RESULT -ne 0 ]]; then
echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT"
fi
return $RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
hostport=(${1//:/ })
HOST=${hostport[0]}
PORT=${hostport[1]}
shift 1
;;
--child)
CHILD=1
shift 1
;;
-q | --quiet)
QUIET=1
shift 1
;;
-s | --strict)
STRICT=1
shift 1
;;
-h)
HOST="$2"
if [[ $HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
HOST="${1#*=}"
shift 1
;;
-p)
PORT="$2"
if [[ $PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
PORT="${1#*=}"
shift 1
;;
-t)
TIMEOUT="$2"
if [[ $TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$HOST" == "" || "$PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
TIMEOUT=${TIMEOUT:-15}
STRICT=${STRICT:-0}
CHILD=${CHILD:-0}
QUIET=${QUIET:-0}
# check to see if timeout is from busybox?
# check to see if timeout is from busybox?
TIMEOUT_PATH=$(realpath $(which timeout))
if [[ $TIMEOUT_PATH =~ "busybox" ]]; then
ISBUSY=1
BUSYTIMEFLAG="-t"
else
ISBUSY=0
BUSYTIMEFLAG=""
fi
if [[ $CHILD -gt 0 ]]; then
wait_for
RESULT=$?
exit $RESULT
else
if [[ $TIMEOUT -gt 0 ]]; then
wait_for_wrapper
RESULT=$?
else
wait_for
RESULT=$?
fi
fi
if [[ $CLI != "" ]]; then
if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then
echoerr "$cmdname: strict mode, refusing to execute subprocess"
exit $RESULT
fi
exec "${CLI[@]}"
else
exit $RESULT
fi

View File

@@ -0,0 +1,23 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 22 Nov 2024
*/
@Suite
@SuiteDisplayName("Rest Resource Suite")
@SelectPackages("marketing.heyday.hartmann.fotodocumentation.rest")
public class AAAResourceTestSuite {
}

View File

@@ -0,0 +1,20 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 13 Nov 2024
*/
public abstract class AbstractFotoTest extends AbstractRestTest {
protected int customerCount() {
return getCount("select count(*) from customer");
}
protected int pictureCount() {
return getCount("select count(*) from picture");
}
}

View File

@@ -0,0 +1,20 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 13 Nov 2024
*/
public abstract class AbstractQuestionnaireTest extends AbstractRestTest {
protected int customerCount() {
return getCount("select count(*) from questionnaire_customer");
}
protected int questionnaireCount() {
return getCount("select count(*) from questionnaire");
}
}

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