48 Commits

Author SHA1 Message Date
verboomp
2eaae18ed9 removed not needed file from git 2026-02-03 16:20:38 +01:00
verboomp
f8ad09582a fixed Jenkins file for release 2026-02-03 16:15:09 +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
181 changed files with 15738 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 == "main" ? "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 'main'
}
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,365 @@
<?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>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.5</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>${version.commons-io}</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>
<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,141 @@
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_ALL, query = "select c from Customer c order by c.name")
@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_ALL = "Customer.findAll";
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,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,60 @@
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> 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,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,110 @@
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 {
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 -> original;
case 2 -> normal(original);
case 3 -> thumbnail(original);
default -> 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 {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(original));
if (image == null) {
LOG.error("Failed to read image from byte array");
return original;
}
int originalWidth = image.getWidth();
if (originalWidth <= maxWidth) {
return original;
}
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[] 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,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,299 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.awt.Color;
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.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 {
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 halfWidth = contentWidth / 2F;
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 (50% of content width)
float imageX = PAGE_MARGIN;
float imageY = yPosition;
float imageMaxWidth = halfWidth - 10F;
float imageMaxHeight = pageHeight - 2 * PAGE_MARGIN - 40F;
if (picture.getImage() != null) {
try {
byte[] imageBytes = Base64.getDecoder().decode(picture.getImage());
PDImageXObject pdImage = PDImageXObject.createFromByteArray(document, imageBytes, "picture");
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 (top-aligned with image)
float rightX = PAGE_MARGIN + halfWidth + 10F;
float rightY = imageY - 32F;
// Date (no label, bold, size 44)
String dateStr = picture.getPictureDate() != null ? (DATE_FORMAT.format(picture.getPictureDate()) + " UHR") : "";
cs.setFont(fontBold, 32);
cs.setNonStrokingColor(COLOR_DATE);
cs.beginText();
cs.newLineAtOffset(rightX, rightY);
cs.showText(dateStr);
cs.endText();
rightY -= 54F;
// Customer number
float kundenNummerY = rightY;
rightY = drawLabel(cs, fontBold, "KUNDENNUMMER", rightX, rightY);
rightY = drawValue(cs, fontRegular, nullSafe(customer.getCustomerNumber()), rightX, rightY);
rightY -= 10F;
// Evaluation card with circles
float circlesX = rightX + 140F;
drawEvaluationCard(cs, fontBold, circlesX, kundenNummerY, picture.getEvaluation());
// 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;
// Comment
rightY = drawLabel(cs, fontBold, "KOMMENTAR", rightX, rightY);
drawWrappedText(cs, fontRegular, nullSafe(picture.getComment()), rightX, rightY, halfWidth - 20f);
}
}
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 void drawWrappedText(PDPageContentStream cs, PDFont font, String text, float x, float y, float maxWidth) throws IOException {
if (text == null || text.isEmpty()) {
return;
}
cs.setFont(font, 10);
cs.setNonStrokingColor(COLOR_TEXT_GRAY);
String[] words = text.split("\\s+");
StringBuilder line = new StringBuilder();
float currentY = y;
for (String word : words) {
String testLine = line.isEmpty() ? word : (line + " " + word);
float textWidth = font.getStringWidth(testLine) / 1000F * 10F;
if (textWidth > maxWidth && !line.isEmpty()) {
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()) {
cs.beginText();
cs.newLineAtOffset(x, currentY);
cs.showText(line.toString());
cs.endText();
}
}
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 cardPadding = 10F;
float cardWidth = 2 * CIRCLE_SPACING + 2 * HIGHLIGHT_RADIUS + 2 * cardPadding;
float labelHeight = 14F;
float cardHeight = labelHeight + 2 * HIGHLIGHT_RADIUS + 2 * cardPadding + 4F;
float cardX = x - HIGHLIGHT_RADIUS - cardPadding;
float cardY = y - cardHeight + cardPadding;
// Draw card background (rounded rectangle)
cs.setStrokingColor(new Color(0xDD, 0xDD, 0xDD));
cs.setNonStrokingColor(new Color(0xF8, 0xF8, 0xF8));
cs.setLineWidth(1f);
drawRoundedRect(cs, cardX, cardY, cardWidth, cardHeight, 6F);
cs.fillAndStroke();
// Draw "BEWERTUNG" label above circles
float labelX = x;
float labelY = y - labelHeight;
cs.setFont(fontBold, 9);
cs.setNonStrokingColor(COLOR_CUSTOMER_NAME);
cs.beginText();
cs.newLineAtOffset(labelX, labelY);
cs.showText("BEWERTUNG");
cs.endText();
// Draw circles below the label
float circleY = labelY - cardPadding - HIGHLIGHT_RADIUS - 2F;
for (int i = 0; i < 3; i++) {
float cx = x + 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 drawRoundedRect(PDPageContentStream cs, float x, float y, float w, float h, float r) throws IOException {
cs.moveTo(x + r, y);
cs.lineTo(x + w - r, y);
cs.curveTo(x + w, y, x + w, y, x + w, y + r);
cs.lineTo(x + w, y + h - r);
cs.curveTo(x + w, y + h, x + w, y + h, x + w - r, y + h);
cs.lineTo(x + r, y + h);
cs.curveTo(x, y + h, x, y + h, x, y + h - r);
cs.lineTo(x, y + r);
cs.curveTo(x, y, x, y, x + r, y);
cs.closePath();
}
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,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,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,52 @@
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);
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,119 @@
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.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
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;
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;
/**
*
*
* <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> {
private static final Log LOG = LogFactory.getLog(ValidatedMessageBodyReader.class);
/* (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();
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);
}
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,17 @@
package marketing.heyday.hartmann.fotodocumentation.rest.jackson;
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,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, 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(), 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,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,25 @@
<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>
<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,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,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,240 @@
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]);
}
// --- 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(3, 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,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,123 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.json.Json;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;
import org.apache.http.impl.client.BasicCookieStore;
/**
*
* <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 AbstractRestTest extends AbstractTest {
private static final Log LOG = LogFactory.getLog(AbstractRestTest.class);
public static final String TEXT_PLAIN = "text/plain";
private static Map<String, String> bearerToken = new HashMap<>();
public HttpResponse executeRequest(Request request) throws IOException {
var executor = Executor.newInstance();
return executor.use(new BasicCookieStore()).execute(request).returnResponse();
}
protected String getAuthorization() {
return getAuthorization(username, password);
}
protected String getAuthorization(String user, String pass) {
if (!bearerToken.containsKey(user)) {
String auth = user + ":" + pass;
String encoded = Base64.getEncoder().encodeToString(auth.getBytes());
String authorization = "Basic " + encoded;
bearerToken.put(user, getBearerToken(authorization));
}
return "Bearer " + bearerToken.getOrDefault(user, "");
}
protected String getBasicHeader() {
return getBasicHeader(username, password);
}
protected String getBasicHeader(String user, String pass) {
String auth = user + ":" + pass;
String encoded = Base64.getEncoder().encodeToString(auth.getBytes());
return "Basic " + encoded;
}
protected String getBearerToken(String authorization) {
try {
String path = deploymentURL + "api/login";
Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
String bearerStr = getResponseText(httpResponse);
try (var reader = Json.createReader(new StringReader(bearerStr));) {
return reader.readObject().getString("accessToken");
}
} catch (IOException e) {
LOG.error("Failed to get bearer token " + e.getMessage(), e);
return "";
}
}
protected byte[] getResponse(HttpResponse httpResponse) throws IOException {
try (var output = new ByteArrayOutputStream(); var input = httpResponse.getEntity().getContent()) {
input.transferTo(output);
output.flush();
return output.toByteArray();
}
}
protected String getResponseText(HttpResponse httpResponse) throws IOException {
String text;
try (BufferedReader input = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent()))) {
text = input.lines().collect(Collectors.joining("\n"));
}
return text;
}
protected String getResponseText(HttpResponse httpResponse, Supplier<String> supplier) throws IOException {
String text = getResponseText(httpResponse);
writeJsonFile(text, supplier.get());
return text;
}
protected String getResponseText(HttpResponse httpResponse, String name) throws IOException {
String className = this.getClass().getName();
return getResponseText(httpResponse, () -> className + "-" + name + ".json");
}
protected int customerCount() {
return getCount("select count(*) from customer");
}
protected int pictureCount() {
return getCount("select count(*) from picture");
}
}

View File

@@ -0,0 +1,182 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import java.io.*;
import java.nio.charset.Charset;
import java.sql.*;
import java.util.Date;
import java.util.Properties;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.map.ObjectMapper;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.ext.postgresql.PostgresqlDataTypeFactory;
import org.dbunit.operation.DatabaseOperation;
import org.json.JSONException;
import org.skyscreamer.jsonassert.JSONAssert;
/**
*
* <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 AbstractTest {
private static final Log LOG = LogFactory.getLog(AbstractTest.class);
public String deploymentURL = "http://localhost:8180/";
public String username = "admin";
public String password = "test";
private static Properties props = null;
public static Connection getConnection() throws SQLException {
if (props == null) {
try (FileReader reader = new FileReader(new File("src/test/resources/junit.properties"))) {
Class.forName("org.postgresql.Driver");
props = new Properties();
props.load(reader);
} catch (Exception e) {
throw new SQLException(e.getMessage());
}
}
return DriverManager.getConnection(props.getProperty("connection"), props.getProperty("username"), props.getProperty("password"));
}
public static void initDB() {
initDB("dataset.xml");
}
public static void initDB(String xmlFile) {
try (Connection jdbcConnection = getConnection();) {
initDb(jdbcConnection, xmlFile);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private static void initDb(Connection jdbcConnection, String xmlFile) {
// using manual DBUnit since the persistence plugin has performance problems.
// https://community.jboss.org/thread/235511
// https://issues.jboss.org/browse/ARQ-1440
try {
System.out.println("start dbunit " + new Date(System.currentTimeMillis()));
IDatabaseConnection conn = new org.dbunit.database.DatabaseConnection(jdbcConnection);
DatabaseConfig config = conn.getConfig();
config.setProperty("http://www.dbunit.org/features/datatypeWarning", Boolean.FALSE);
config.setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, Boolean.TRUE);
config.setProperty(DatabaseConfig.FEATURE_BATCHED_STATEMENTS, Boolean.TRUE);
config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new PostgresqlDataTypeFactory());
// initialize your dataset here
IDataSet dataSet = new FlatXmlDataSetBuilder().build(new File("src/test/resources/datasets/" + xmlFile));
ReplacementDataSet repDataSet = new ReplacementDataSet(dataSet);
repDataSet.addReplacementObject("EXP_DATE", DateUtils.addDays(new Date(), 1));
repDataSet.addReplacementObject("NULL_DATE", null);
repDataSet.addReplacementObject("NULL_NUMBER", null);
repDataSet.addReplacementObject("NULL_STRING", null);
try {
DatabaseOperation.CLEAN_INSERT.execute(conn, repDataSet);
} finally {
conn.close();
}
} catch (java.sql.BatchUpdateException e) {
e.printStackTrace();
e.getNextException().printStackTrace();
throw new RuntimeException(e.getMessage(), e);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage(), e);
}
System.out.println("End dbunit " + new Date(System.currentTimeMillis()));
}
public String fileToString(final String fileName) {
try (FileInputStream input = new FileInputStream("src/test/resources/" + fileName)) {
return IOUtils.toString(input, Charset.forName("utf-8"));
} catch (IOException e) {
return "";
}
}
public static void writeJsonFile(final String content, final String fileName) {
if (content.isEmpty()) {
return;
}
File file = new File("target/test/output/");
file.mkdirs();
try (FileOutputStream out = new FileOutputStream(new File(file, fileName))) {
ObjectMapper mapper = new ObjectMapper();
Object json = mapper.readValue(content, Object.class);
String formattedContent = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(json);
System.out.println(formattedContent);
IOUtils.write(formattedContent, out, Charset.forName("utf-8"));
} catch (Exception e) {
LOG.error("Error storing json file", e);
}
}
public static void writeFile(byte[] content, 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 storing binary file", e);
}
}
public static byte[] readFile(String fileName) {
try (FileInputStream in = new FileInputStream("src/test/resources/" + fileName)) {
return IOUtils.toByteArray(in);
} catch (Exception e) {
throw new RuntimeException("Error reading file src/test/resources/" + fileName);
}
}
public void writeFile(InputStream content, String fileName) {
File file = new File("target/test/output/");
file.mkdirs();
try (FileOutputStream out = new FileOutputStream(new File(file, fileName))) {
IOUtils.copy(content, out);
} catch (Exception e) {
LOG.error("Error storing binary file", e);
}
}
public static int getCount(String sql) {
try (Connection conn = getConnection(); PreparedStatement statement = conn.prepareStatement(sql); ResultSet resultSet = statement.executeQuery();) {
if (resultSet.next()) {
return resultSet.getInt(1);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
LOG.error("no result found");
return 0;
}
protected void jsonAssert(String expected, String content) {
jsonAssert(expected, content, false);
}
protected void jsonAssert(String expected, String content, boolean strict) {
try {
JSONAssert.assertEquals(expected, content, strict);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,137 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.io.IOException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;
import org.apache.http.entity.ContentType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
/**
*
* <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
*/
@TestMethodOrder(OrderAnnotation.class)
public class CustomerPictureResourceTest extends AbstractRestTest {
private static final Log LOG = LogFactory.getLog(CustomerPictureResourceTest.class);
private static final String PATH = "api/customer-picture";
private static final String BASE_UPLOAD = "src/test/resources/upload/";
@BeforeAll
public static void init() {
initDB();
}
@Test
@Order(2)
public void doAddCustomerPicture() throws IOException {
LOG.info("doAddCustomerPicture");
assertEquals(3, customerCount());
assertEquals(5, pictureCount());
String path = deploymentURL + PATH;
Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8")
.bodyFile(new File(BASE_UPLOAD + "add.json"), ContentType.APPLICATION_JSON);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
assertEquals(3, customerCount());
assertEquals(6, pictureCount());
}
@Test
@Order(3)
public void doAddCustomerWithPicture() throws IOException {
LOG.info("doAddCustomerWithPicture");
assertEquals(3, customerCount());
assertEquals(6, pictureCount());
String authorization = getBasicHeader();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH;
Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization)
.bodyFile(new File(BASE_UPLOAD + "addNewCustomer.json"), ContentType.APPLICATION_JSON);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
assertEquals(4, customerCount());
assertEquals(7, pictureCount());
}
@Test
@Order(1)
public void doAddCustomerPictureWrongJson() throws IOException {
LOG.info("doAddCustomerPictureWrongJson");
String authorization = getBasicHeader();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH;
Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization)
.bodyFile(new File(BASE_UPLOAD + "addWrong.json"), ContentType.APPLICATION_JSON);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(400, code);
String text = getResponseText(httpResponse, "doGetAll");
System.out.println(text);
}
@Test
@Order(2)
public void doTest() throws IOException {
LOG.info("doAddCustomerPicture");
//String authorization = getBasicHeader();
//LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH;
Request request = Request.Options(path).addHeader("Accept", "application/json; charset=utf-8");
//.addHeader("Authorization", authorization)
//.bodyFile(new File(BASE_UPLOAD + "add.json"), ContentType.APPLICATION_JSON);
HttpResponse httpResponse = executeRequest(request);
var headers = httpResponse.getAllHeaders();
for (var header : headers) {
System.out.println(header.getName() + " " + header.getValue());
}
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
}
public static void main(String[] args) throws IOException {
var test = new CustomerPictureResourceTest();
test.deploymentURL = "http://localhost:8080/";
test.deploymentURL = "https://hartmann-cue.heydevelop.de/";
test.username = "adm";
test.password = "x1t0e7Pb49";
test.doTest();
}
}

View File

@@ -0,0 +1,235 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
/**
*
* <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
*/
@TestMethodOrder(OrderAnnotation.class)
public class CustomerResourceTest extends AbstractRestTest {
private static final Log LOG = LogFactory.getLog(CustomerResourceTest.class);
private static final String PATH = "api/customer";
private static final String BASE_DOWNLOAD = "json/CustomerResourceTest-";
@BeforeAll
public static void init() {
initDB();
}
@Test
@Order(1)
public void doGetAll() throws IOException {
LOG.info("doGetAll");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH;
Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
String text = getResponseText(httpResponse, "doGetAll");
String expected = fileToString(BASE_DOWNLOAD + "doGetAll.json");
jsonAssert(expected, text);
}
@Test
@Order(1)
public void doGetAllStartWith() throws IOException {
LOG.info("doGetAllStartWith");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH + "?startsWith=M";
Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
String text = getResponseText(httpResponse, "doGetAllStartWith");
String expected = fileToString(BASE_DOWNLOAD + "doGetAllStartWith.json");
jsonAssert(expected, text);
}
@Test
@Order(1)
public void doGetAllQueryText() throws IOException {
LOG.info("doGetAllQueryText");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH + "?query=2345";
Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
String text = getResponseText(httpResponse, "doGetAllQueryText");
String expected = fileToString(BASE_DOWNLOAD + "doGetAllQueryText.json");
jsonAssert(expected, text);
}
@Test
@Order(1)
public void doGetAllQueryTextWithStart() throws IOException {
LOG.info("doGetAllQueryTextWithStart");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH + "?query=45&startsWith=M";
Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
String text = getResponseText(httpResponse, "doGetAllQueryTextWithStart");
String expected = fileToString(BASE_DOWNLOAD + "doGetAllQueryTextWithStart.json");
jsonAssert(expected, text);
}
@Test
@Order(1)
public void doGetAllQueryDate1() throws IOException {
LOG.info("doGetAllQueryDate");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH + "?query=12.01.2026";
Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
String text = getResponseText(httpResponse, "doGetAllQueryDate");
String expected = fileToString(BASE_DOWNLOAD + "doGetAllQueryDate.json");
jsonAssert(expected, text);
}
@Test
@Order(1)
public void doGetAllQueryDate2() throws IOException {
LOG.info("doGetAllQueryDate");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String query = URLEncoder.encode("12 Januar 2026", Charset.forName("utf-8"));
String path = deploymentURL + PATH + "?query=" + query;
Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
String text = getResponseText(httpResponse, "doGetAllQueryDate");
String expected = fileToString(BASE_DOWNLOAD + "doGetAllQueryDate.json");
jsonAssert(expected, text);
}
@Test
@Order(1)
public void doGetAllQueryDate3() throws IOException {
LOG.info("doGetAllQueryDate");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String query = URLEncoder.encode("12. Januar 2026", Charset.forName("utf-8"));
String path = deploymentURL + PATH + "?query=" + query;
Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
String text = getResponseText(httpResponse, "doGetAllQueryDate");
String expected = fileToString(BASE_DOWNLOAD + "doGetAllQueryDate.json");
jsonAssert(expected, text);
}
@Test
@Order(1)
public void doGetCustomer() throws IOException {
LOG.info("doGetCustomer");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH + "/1";
Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
String text = getResponseText(httpResponse, "doGetCustomer");
String expected = fileToString(BASE_DOWNLOAD + "doGetCustomer.json");
jsonAssert(expected, text);
}
@Test
@Order(1)
public void doDownload() throws IOException {
LOG.info("doDownload");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH + "/export/1";
Request request = Request.Get(path).addHeader("Accept", "application/pdf")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
byte[] text = getResponse(httpResponse);
writeFile(text, "doDownload.pdf");
}
@Test
@Order(1)
public void doDownloadNotExist() throws IOException {
LOG.info("doDownloadNotExist");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH + "/export/9999";
Request request = Request.Get(path).addHeader("Accept", "application/pdf")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(404, code);
}
}

View File

@@ -0,0 +1,40 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
/**
*
* <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
*/
@TestMethodOrder(OrderAnnotation.class)
public class LoginResourceTest extends AbstractRestTest {
private static final Log LOG = LogFactory.getLog(LoginResourceTest.class);
@Test
@Order(2)
public void doTestLogin() {
LOG.info("doTestLogin");
String token = getBasicHeader();
assertNotNull(token);
}
public static void main(String[] args) {
var test = new LoginResourceTest();
test.deploymentURL = "http://localhost:8080/";
String token = test.getAuthorization("hartmann", "nvlev4YnTi");
System.out.println(token);
}
}

View File

@@ -0,0 +1,34 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;
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: 13 Nov 2024
*/
public class MonitoringResourceTest extends AbstractRestTest {
@Test
public void check() throws IOException {
String path = deploymentURL + "api/monitoring/check/hello";
Request request = Request.Get(path).addHeader("Accept", TEXT_PLAIN);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
}
}

View File

@@ -0,0 +1,181 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
/**
*
* <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
*/
@TestMethodOrder(OrderAnnotation.class)
public class PictureResourceTest extends AbstractRestTest {
private static final Log LOG = LogFactory.getLog(PictureResourceTest.class);
private static final String PATH = "api/picture";
@BeforeAll
public static void init() {
initDB();
}
@Test
@Order(3)
public void doDelete() throws IOException {
LOG.info("doDelete");
assertEquals(5, pictureCount());
String path = deploymentURL + PATH + "/1";
Request request = Request.Delete(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", getAuthorization());
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
assertEquals(4, pictureCount());
}
@Test
@Order(2)
public void doDeleteNotFound() throws IOException {
LOG.info("doDeleteNotFound");
assertEquals(5, pictureCount());
String path = deploymentURL + PATH + "/6000";
Request request = Request.Delete(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", getAuthorization());
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(404, code);
assertEquals(5, pictureCount());
}
@Test
@Order(1)
public void doEvaluation() throws IOException {
LOG.info("doEvaluation");
assertEquals(0, getCount("select count(*) from picture where picture_id = 1 and evaluation = 3"));
String path = deploymentURL + PATH + "/evaluation/1?evaluation=3";
Request request = Request.Put(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", getAuthorization());
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
assertEquals(1, getCount("select count(*) from picture where picture_id = 1 and evaluation = 3"));
}
@Test
@Order(1)
public void doEvaluationNotFound() throws IOException {
LOG.info("doEvaluationNotFound");
String path = deploymentURL + PATH + "/evaluation/6000?evaluation=3";
Request request = Request.Put(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", getAuthorization());
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(404, code);
}
@Test
@Order(1)
public void doEvaluationWrongValue() throws IOException {
LOG.info("doEvaluationWrongValue");
String path = deploymentURL + PATH + "/evaluation/1?evaluation=4";
Request request = Request.Put(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", getAuthorization());
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(400, code);
}
@Test
@Order(1)
public void doEvaluationWrongValue2() throws IOException {
LOG.info("doEvaluationWrongValue2");
String path = deploymentURL + PATH + "/evaluation/1?evaluation=0";
Request request = Request.Put(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", getAuthorization());
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(400, code);
}
@Test
@Order(1)
public void doEvaluationNoValue() throws IOException {
LOG.info("doEvaluationNoValue");
String path = deploymentURL + PATH + "/evaluation/1";
Request request = Request.Put(path).addHeader("Accept", "application/json; charset=utf-8")
.addHeader("Authorization", getAuthorization());
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(400, code);
}
@Test
@Order(1)
public void doGetPicture() throws IOException {
LOG.info("doGetPicture");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH + "/image/1?size=1";
Request request = Request.Get(path).addHeader("Accept", "image/jpg")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
byte[] file = getResponse(httpResponse);
assertTrue(file.length > 0);
writeFile(file, "doGetPicture.jpg");
}
@Test
@Order(1)
public void doGetPictureNotFound() throws IOException {
LOG.info("doGetPicture");
String authorization = getAuthorization();
LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH + "/image/9999?size=1";
Request request = Request.Get(path).addHeader("Accept", "image/jpg")
.addHeader("Authorization", authorization);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(404, code);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
[
{
"id": 3,
"name": "Schmidt Apotheke",
"customerNumber": "3456",
"lastUpdateDate": 1768212570000
}
]

View File

@@ -0,0 +1,8 @@
[
{
"id": 2,
"name": "Meier Apotheke",
"customerNumber": "2345",
"lastUpdateDate": 1767607770000
}
]

View File

@@ -0,0 +1,8 @@
[
{
"id": 2,
"name": "Meier Apotheke",
"customerNumber": "2345",
"lastUpdateDate": 1767607770000
}
]

View File

@@ -0,0 +1,14 @@
[
{
"id": 1,
"name": "Müller Apotheke",
"customerNumber": "1234",
"lastUpdateDate": 1767348570000
},
{
"id": 2,
"name": "Meier Apotheke",
"customerNumber": "2345",
"lastUpdateDate": 1767607770000
}
]

View File

@@ -0,0 +1,31 @@
{
"id": 1,
"name": "Müller Apotheke",
"customerNumber": "1234",
"city": "Hannover",
"zip": "12345",
"pictures": [
{
"id": 1,
"comment": "good looking picture 1",
"category": null,
"pictureDate": 1767262170000,
"username": "verboomp",
"evaluation": 1,
"imageUrl": "http://localhost:8180/api/picture/image/1?size=1",
"normalSizeUrl": "http://localhost:8180/api/picture/image/1?size=2",
"thumbnailSizeUrl": "http://localhost:8180/api/picture/image/1?size=3"
},
{
"id": 2,
"comment": "good looking picture 2",
"category": null,
"pictureDate": 1767348570000,
"username": "verboomp",
"evaluation": 1,
"imageUrl": "http://localhost:8180/api/picture/image/2?size=1",
"normalSizeUrl": "http://localhost:8180/api/picture/image/2?size=2",
"thumbnailSizeUrl": "http://localhost:8180/api/picture/image/2?size=3"
}
]
}

View File

@@ -0,0 +1,20 @@
[
{
"id": 1,
"name": "Müller Apotheke",
"customerNumber": "1234",
"lastUpdateDate": 1767348570000
},
{
"id": 2,
"name": "Meier Apotheke",
"customerNumber": "2345",
"lastUpdateDate": 1767607770000
},
{
"id": 3,
"name": "Schmidt Apotheke",
"customerNumber": "3456",
"lastUpdateDate": 1768212570000
}
]

View File

@@ -0,0 +1,3 @@
connection=jdbc:postgresql://localhost:5430/fotodocumentation
username=fotodocumentation
password=fotodocumentation

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="org.apache.http">
<priority value="INFO"/>
</category>
<category name="STDOUT">
<priority value="DEBUG"/>
</category>
<root>
<appender-ref ref="CONSOLE"/>
</root>
</log4j:configuration>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(flutter test:*)",
"Bash(cat:*)",
"Bash(flutter gen-l10n:*)",
"Bash(flutter analyze:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ac4e799d237041cf905519190471f657b657155a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ac4e799d237041cf905519190471f657b657155a
base_revision: ac4e799d237041cf905519190471f657b657155a
- platform: web
create_revision: ac4e799d237041cf905519190471f657b657155a
base_revision: ac4e799d237041cf905519190471f657b657155a
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "foto-frontend",
"request": "launch",
"type": "dart"
},
{
"name": "foto-frontend (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "foto-frontend (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

View File

@@ -0,0 +1,16 @@
# foto documentation
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

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