Compare commits
2 Commits
60dc35961a
...
62c13ff0b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62c13ff0b1 | ||
|
|
d85406f0c7 |
292
DEPLOYMENT.md
Normal file
292
DEPLOYMENT.md
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
# Deployment Setup für SKAMP API
|
||||||
|
|
||||||
|
Diese Anleitung beschreibt das vollständige Setup für das automatische Deployment der Spring Boot Applikation auf einen Remote Server.
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Bei einem Push auf den `main` Branch wird automatisch:
|
||||||
|
1. Die Spring Boot Applikation gebaut
|
||||||
|
2. Ein Docker Image erstellt
|
||||||
|
3. Das Image in die Gitea Container Registry gepusht
|
||||||
|
4. Das Image auf dem Remote Server deployed
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
### Auf dem Deployment-Server
|
||||||
|
|
||||||
|
1. **Docker installiert** (Version 20.10+)
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Keine weiteren Dateien notwendig**
|
||||||
|
- Der Workflow deployed direkt via `docker run`
|
||||||
|
- Keine compose.yml oder .env Dateien auf dem Server erforderlich
|
||||||
|
- Alle Konfiguration erfolgt über Gitea Secrets
|
||||||
|
|
||||||
|
## Gitea Configuration
|
||||||
|
|
||||||
|
Im Gitea Repository müssen folgende Variables und Secrets angelegt werden:
|
||||||
|
|
||||||
|
### Repository Settings > Actions > Variables
|
||||||
|
|
||||||
|
| Variable Name | Beschreibung | Beispiel |
|
||||||
|
|--------------|--------------|----------|
|
||||||
|
| `REGISTRY_URL` | Gitea Container Registry URL | `gitea.moz-tech.de` |
|
||||||
|
| `NAMESPACE` | Organisation/Benutzer | `murat` |
|
||||||
|
| `REPO_NAME` | Repository Name | `skamp` |
|
||||||
|
|
||||||
|
### Repository Settings > Actions > Secrets
|
||||||
|
|
||||||
|
| Secret Name | Beschreibung | Beispiel |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `CI_GITEA_USER` | Gitea Benutzername | `murat` |
|
||||||
|
| `CI_GITEA_TOKEN` | Gitea Access Token | `ghp_xxxxxxxxxxxx` |
|
||||||
|
| `SSH_HOST` | Server IP/Hostname | `123.456.789.0` |
|
||||||
|
| `SSH_USERNAME` | SSH Benutzername | `deploy` |
|
||||||
|
| `SSH_PRIVATE_KEY` | SSH Private Key | `-----BEGIN RSA PRIVATE KEY-----...` |
|
||||||
|
| `SSH_PORT` | SSH Port | `22` |
|
||||||
|
| `APP_PORT` | Anwendungs-Port | `8080` |
|
||||||
|
| `SPRING_PROFILES_ACTIVE` | Spring Profil | `prod` |
|
||||||
|
| `POSTGRES_HOST` | PostgreSQL Host | `db.example.com` |
|
||||||
|
| `POSTGRES_PORT` | PostgreSQL Port | `5432` |
|
||||||
|
| `POSTGRES_DB` | Datenbank Name | `skamp` |
|
||||||
|
| `POSTGRES_USER` | Datenbank User | `skamp` |
|
||||||
|
| `POSTGRES_PASSWORD` | Datenbank Passwort | `IhrSicheresPasswort123!` |
|
||||||
|
| `HIBERNATE_DDL_AUTO` | Hibernate DDL | `update` |
|
||||||
|
| `KEYCLOAK_ISSUER_URI` | Keycloak Issuer URI | `https://keycloak.example.com/realms/skamp` |
|
||||||
|
| `CORS_ALLOWED_ORIGINS` | Erlaubte CORS Origins | `https://frontend.example.com` |
|
||||||
|
| `MINIO_ENDPOINT` | MinIO/S3 Endpoint | `https://s3.example.com` |
|
||||||
|
| `MINIO_ACCESS_KEY` | MinIO Access Key | `your_access_key` |
|
||||||
|
| `MINIO_SECRET_KEY` | MinIO Secret Key | `your_secret_key` |
|
||||||
|
| `JAVA_OPTS` | Java Optionen | `-Xmx1024m -Xms512m` |
|
||||||
|
|
||||||
|
### Gitea Token erstellen
|
||||||
|
|
||||||
|
1. Gitea → Einstellungen → Anwendungen → Access Tokens
|
||||||
|
2. Token Name: `deployment-token`
|
||||||
|
3. Berechtigungen: `read:packages`, `write:packages`
|
||||||
|
4. Token generieren und als `CI_GITEA_TOKEN` Secret speichern
|
||||||
|
|
||||||
|
### SSH Key erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf deinem lokalen Rechner
|
||||||
|
ssh-keygen -t ed25519 -C "deployment-key" -f ~/.ssh/skamp_deploy
|
||||||
|
|
||||||
|
# Public Key auf den Server kopieren
|
||||||
|
ssh-copy-id -i ~/.ssh/skamp_deploy.pub user@server
|
||||||
|
|
||||||
|
# Private Key als Secret speichern
|
||||||
|
cat ~/.ssh/skamp_deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Erstes Deployment
|
||||||
|
|
||||||
|
Das erste Deployment erfolgt automatisch beim Push auf `main`. Der Workflow:
|
||||||
|
|
||||||
|
1. Baut die Spring Boot 3 Applikation mit Maven
|
||||||
|
2. Erstellt ein Docker Image
|
||||||
|
3. Pusht das Image zur Gitea Registry
|
||||||
|
4. Loggt sich per SSH auf dem Server ein
|
||||||
|
5. Pulled das Image
|
||||||
|
6. Startet den Container mit allen Environment-Variablen
|
||||||
|
|
||||||
|
Alternativ kannst du das erste Deployment manuell testen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf dem Server
|
||||||
|
# Login zur Container Registry
|
||||||
|
docker login gitea.moz-tech.de -u murat
|
||||||
|
|
||||||
|
# Pull Image (ersetze IMAGE_TAG mit einem Commit-SHA oder 'latest')
|
||||||
|
docker pull gitea.moz-tech.de/murat/skamp:latest
|
||||||
|
|
||||||
|
# Starte Container
|
||||||
|
docker run -d \
|
||||||
|
--name skamp-app \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-e SPRING_PROFILES_ACTIVE=prod \
|
||||||
|
-e SPRING_DATASOURCE_URL=jdbc:postgresql://db.example.com:5432/skamp \
|
||||||
|
-e SPRING_DATASOURCE_USERNAME=skamp \
|
||||||
|
-e SPRING_DATASOURCE_PASSWORD=IhrPasswort \
|
||||||
|
-e SPRING_JPA_HIBERNATE_DDL_AUTO=update \
|
||||||
|
-e KEYCLOAK_ISSUER_URI=https://keycloak.example.com/realms/skamp \
|
||||||
|
-e CORS_ALLOWED_ORIGINS=https://frontend.example.com \
|
||||||
|
-e S3_ACCESS_KEY=your_access_key \
|
||||||
|
-e S3_SECRET_KEY=your_secret_key \
|
||||||
|
-e S3_ENDPOINT=https://s3.example.com \
|
||||||
|
-e JAVA_OPTS="-Xmx1024m -Xms512m" \
|
||||||
|
gitea.moz-tech.de/murat/skamp:latest
|
||||||
|
|
||||||
|
# Prüfe Status
|
||||||
|
docker ps --filter name=skamp-app
|
||||||
|
docker logs -f skamp-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Trigger
|
||||||
|
|
||||||
|
Das Deployment wird automatisch ausgelöst durch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Deploy: Update application"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Logs anzeigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf dem Server
|
||||||
|
|
||||||
|
# Container Logs live anzeigen
|
||||||
|
docker logs -f skamp-app
|
||||||
|
|
||||||
|
# Letzte 100 Zeilen
|
||||||
|
docker logs --tail 100 skamp-app
|
||||||
|
|
||||||
|
# Mit Timestamps
|
||||||
|
docker logs -f --timestamps skamp-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Status prüfen
|
||||||
|
docker ps --filter name=skamp-app
|
||||||
|
|
||||||
|
# Detaillierte Informationen
|
||||||
|
docker inspect skamp-app
|
||||||
|
|
||||||
|
# Resource-Nutzung
|
||||||
|
docker stats skamp-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HTTP Health Check
|
||||||
|
curl http://localhost:8080/actuator/health
|
||||||
|
|
||||||
|
# Container Health Status
|
||||||
|
docker inspect --format='{{.State.Health.Status}}' skamp-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Workflow schlägt fehl
|
||||||
|
|
||||||
|
1. **Build Error**: Prüfe die Java Version und Maven Dependencies in `pom.xml`
|
||||||
|
2. **Registry Login Error**: Prüfe `CI_GITEA_TOKEN` Secret
|
||||||
|
3. **SSH Error**: Prüfe SSH Secrets (`SSH_HOST`, `SSH_USERNAME`, `SSH_PRIVATE_KEY`, `SSH_PORT`)
|
||||||
|
4. **Deployment Error**: Prüfe Server-Logs mit `docker logs skamp-app`
|
||||||
|
|
||||||
|
### Container startet nicht
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe Logs
|
||||||
|
docker logs skamp-app
|
||||||
|
|
||||||
|
# Prüfe Container-Details
|
||||||
|
docker inspect skamp-app
|
||||||
|
|
||||||
|
# Container neu starten
|
||||||
|
docker restart skamp-app
|
||||||
|
|
||||||
|
# Container mit neuen Einstellungen starten
|
||||||
|
docker stop skamp-app
|
||||||
|
docker rm skamp-app
|
||||||
|
# Dann docker run erneut ausführen (siehe Deployment-Sektion)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenbank-Verbindungsfehler
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Prüfe ob PostgreSQL erreichbar ist
|
||||||
|
docker exec skamp-app ping -c 3 $POSTGRES_HOST
|
||||||
|
|
||||||
|
# 2. Prüfe Verbindung zur Datenbank
|
||||||
|
docker exec -it skamp-app sh -c "apt update && apt install -y postgresql-client"
|
||||||
|
docker exec -it skamp-app psql -h $POSTGRES_HOST -U $POSTGRES_USER -d $POSTGRES_DB
|
||||||
|
|
||||||
|
# 3. Prüfe Environment-Variablen im Container
|
||||||
|
docker exec skamp-app env | grep SPRING_DATASOURCE
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port-Konflikte
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe ob Port bereits belegt ist
|
||||||
|
sudo netstat -tlnp | grep :8080
|
||||||
|
# oder
|
||||||
|
sudo lsof -i :8080
|
||||||
|
|
||||||
|
# Anderen Port verwenden (z.B. 8081)
|
||||||
|
docker stop skamp-app
|
||||||
|
docker rm skamp-app
|
||||||
|
# Dann docker run mit -p 8081:8080 statt -p 8080:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
### Automatisches Update
|
||||||
|
|
||||||
|
Jeder Push auf `main` löst ein automatisches Update aus.
|
||||||
|
|
||||||
|
### Manuelles Update auf spezifische Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf dem Server
|
||||||
|
|
||||||
|
# Stoppe aktuellen Container
|
||||||
|
docker stop skamp-app
|
||||||
|
docker rm skamp-app
|
||||||
|
|
||||||
|
# Pull spezifische Version (ersetze COMMIT_SHA)
|
||||||
|
docker pull gitea.moz-tech.de/murat/skamp:COMMIT_SHA
|
||||||
|
|
||||||
|
# Starte Container mit neuer Version
|
||||||
|
docker run -d \
|
||||||
|
--name skamp-app \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-e ... (alle Environment-Variablen) \
|
||||||
|
gitea.moz-tech.de/murat/skamp:COMMIT_SHA
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback zu vorheriger Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe verfügbare Images
|
||||||
|
docker images | grep skamp
|
||||||
|
|
||||||
|
# Stoppe aktuellen Container
|
||||||
|
docker stop skamp-app
|
||||||
|
docker rm skamp-app
|
||||||
|
|
||||||
|
# Starte mit älterer Version
|
||||||
|
docker run -d \
|
||||||
|
--name skamp-app \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-e ... (alle Environment-Variablen) \
|
||||||
|
gitea.moz-tech.de/murat/skamp:OLD_COMMIT_SHA
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Secrets niemals committen** - Nutze `.gitignore` für `.env`
|
||||||
|
2. **Starke Passwörter** - Generiere sichere Passwörter für DB und S3
|
||||||
|
3. **SSH Key Rotation** - Rotiere SSH Keys regelmäßig
|
||||||
|
4. **Firewall** - Beschränke Zugriff auf notwendige Ports
|
||||||
|
5. **Updates** - Halte Docker und Images aktuell
|
||||||
|
6. **Monitoring** - Implementiere Monitoring und Alerting
|
||||||
|
|
||||||
|
## Weitere Informationen
|
||||||
|
|
||||||
|
- Gitea Actions: https://docs.gitea.com/usage/actions/overview
|
||||||
|
- Docker Compose: https://docs.docker.com/compose/
|
||||||
|
- Spring Boot: https://spring.io/projects/spring-boot
|
||||||
181
DEPLOYMENT_SECRETS.md
Normal file
181
DEPLOYMENT_SECRETS.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Deployment Secrets Configuration
|
||||||
|
|
||||||
|
Diese Dokumentation beschreibt die erforderlichen Secrets für den automatischen Deployment-Workflow in Gitea.
|
||||||
|
|
||||||
|
## Erforderliche Secrets und Variablen in Gitea
|
||||||
|
|
||||||
|
### Gitea Variables (nicht-sensitive Daten)
|
||||||
|
|
||||||
|
Navigiere zu: **Repository Settings → Variables → Actions**
|
||||||
|
|
||||||
|
Diese Werte sind nicht sensitiv und können als Variables gespeichert werden:
|
||||||
|
|
||||||
|
| Variable Name | Beschreibung | Beispiel |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `REGISTRY_URL` | URL der Container Registry | `gitea.moz-tech.de` |
|
||||||
|
| `NAMESPACE` | Namespace/Organisation in der Registry | `murat` |
|
||||||
|
| `REPO_NAME` | Repository Name für Docker Image | `skamp` |
|
||||||
|
|
||||||
|
### Gitea Secrets (sensitive Daten)
|
||||||
|
|
||||||
|
Navigiere zu: **Repository Settings → Secrets → Actions**
|
||||||
|
|
||||||
|
Diese Werte sind sensitiv und müssen als Secrets gespeichert werden:
|
||||||
|
|
||||||
|
#### Container Registry Secrets
|
||||||
|
|
||||||
|
| Secret Name | Beschreibung | Beispiel |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `CI_GITEA_USER` | Gitea Benutzername für Registry Login | `murat` |
|
||||||
|
| `CI_GITEA_TOKEN` | Gitea Access Token mit Registry-Rechten | `74a7738116bfb99497a7781291efc5766901f497` |
|
||||||
|
|
||||||
|
#### SSH Server Secrets
|
||||||
|
|
||||||
|
| Secret Name | Beschreibung | Beispiel |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `SSH_HOST` | Hostname oder IP des Deployment-Servers | `server.example.com` |
|
||||||
|
| `SSH_USERNAME` | SSH Benutzername | `deploy` |
|
||||||
|
| `SSH_PRIVATE_KEY` | SSH Private Key (kompletter Inhalt) | `-----BEGIN OPENSSH PRIVATE KEY-----...` |
|
||||||
|
| `SSH_PORT` | SSH Port (optional, Standard: 22) | `22` |
|
||||||
|
| `DEPLOY_PATH` | Pfad zum Deployment-Verzeichnis auf dem Server | `/opt/skamp/docker` |
|
||||||
|
|
||||||
|
### Application Secrets
|
||||||
|
|
||||||
|
| Secret Name | Beschreibung | Beispiel |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `APP_PORT` | Port für die Anwendung | `8082` |
|
||||||
|
| `SPRING_PROFILES_ACTIVE` | Spring Boot Profil | `prod` |
|
||||||
|
| `APPLICATION_NAME` | Name der Anwendung | `skamp-api` |
|
||||||
|
| `CORS_ALLOWED_ORIGINS` | Erlaubte CORS Origins | `https://app.example.com` |
|
||||||
|
|
||||||
|
### PostgreSQL Secrets
|
||||||
|
|
||||||
|
| Secret Name | Beschreibung | Beispiel |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `POSTGRES_HOST` | PostgreSQL Hostname | `postgres` oder `db.example.com` |
|
||||||
|
| `POSTGRES_PORT` | PostgreSQL Port | `5432` |
|
||||||
|
| `POSTGRES_DB` | Datenbank Name | `skamp` |
|
||||||
|
| `POSTGRES_USER` | Datenbank Benutzername | `skamp_user` |
|
||||||
|
| `POSTGRES_PASSWORD` | Datenbank Passwort | `secure_password_here` |
|
||||||
|
| `HIBERNATE_DDL_AUTO` | Hibernate DDL Modus | `update` oder `validate` |
|
||||||
|
|
||||||
|
### Keycloak Secrets
|
||||||
|
|
||||||
|
| Secret Name | Beschreibung | Beispiel |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `KEYCLOAK_URL` | Keycloak Server URL | `https://auth.moz-tech.de/` |
|
||||||
|
| `KEYCLOAK_REALM` | Keycloak Realm | `enerport` |
|
||||||
|
| `KEYCLOAK_ISSUER_URI` | Keycloak Issuer URI | `https://auth.moz-tech.de/realms/enerport` |
|
||||||
|
|
||||||
|
### MinIO (S3) Secrets
|
||||||
|
|
||||||
|
| Secret Name | Beschreibung | Beispiel |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `MINIO_ENDPOINT` | MinIO Endpoint URL | `http://minio:9000` |
|
||||||
|
| `MINIO_ACCESS_KEY` | MinIO Access Key | `minioadmin` |
|
||||||
|
| `MINIO_SECRET_KEY` | MinIO Secret Key | `minioadmin123` |
|
||||||
|
|
||||||
|
### AWS S3 Secrets (Optional)
|
||||||
|
|
||||||
|
| Secret Name | Beschreibung | Beispiel |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `AWS_ACCESS_KEY_ID` | AWS Access Key ID | `AKIAIOSFODNN7EXAMPLE` |
|
||||||
|
| `AWS_SECRET_ACCESS_KEY` | AWS Secret Access Key | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` |
|
||||||
|
| `AWS_S3_BUCKET_NAME` | AWS S3 Bucket Name | `my-bucket` |
|
||||||
|
| `AWS_S3_REGION` | AWS S3 Region | `eu-central-1` |
|
||||||
|
| `AWS_S3_ENDPOINT` | AWS S3 Endpoint (optional) | `https://s3.eu-central-1.amazonaws.com` |
|
||||||
|
|
||||||
|
### Additional Configuration Secrets
|
||||||
|
|
||||||
|
| Secret Name | Beschreibung | Beispiel |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `JAVA_OPTS` | Java JVM Optionen | `-Xmx512m -Xms256m` |
|
||||||
|
|
||||||
|
## Workflow Ablauf
|
||||||
|
|
||||||
|
Der Workflow wird bei jedem Push auf den `main` Branch ausgelöst:
|
||||||
|
|
||||||
|
1. **Checkout Code** - Lädt den Repository-Code
|
||||||
|
2. **Login to Container Registry** - Authentifiziert sich bei der Gitea Registry
|
||||||
|
3. **Extract metadata** - Erstellt Image-Tags (commit SHA + latest)
|
||||||
|
4. **Build and Push Docker Image** - Baut das Docker Image mit Multi-Stage Build (Maven Build + Runtime Image) und pusht es zur Registry
|
||||||
|
5. **Deploy to Remote Server** - Verbindet per SSH zum Server, erstellt .env Datei aus Secrets und deployed die Anwendung
|
||||||
|
|
||||||
|
**Hinweis:** Der Maven Build erfolgt im Docker Multi-Stage Build (siehe Dockerfile), daher ist kein separater Maven-Build-Step im Workflow erforderlich.
|
||||||
|
|
||||||
|
## Server-Vorbereitung
|
||||||
|
|
||||||
|
Auf dem Deployment-Server müssen folgende Voraussetzungen erfüllt sein:
|
||||||
|
|
||||||
|
1. **Docker und Docker Compose installiert**
|
||||||
|
```bash
|
||||||
|
docker --version
|
||||||
|
docker compose version
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deployment-Verzeichnis eingerichtet**
|
||||||
|
```bash
|
||||||
|
mkdir -p /opt/skamp/docker
|
||||||
|
cd /opt/skamp/docker
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Docker Compose Konfiguration**
|
||||||
|
- `compose.yml` - Die Docker Compose Konfiguration
|
||||||
|
- `.env` - Umgebungsvariablen für das Deployment
|
||||||
|
|
||||||
|
4. **SSH-Zugriff konfiguriert**
|
||||||
|
- SSH Public Key des CI/CD Servers muss auf dem Deployment-Server hinterlegt sein
|
||||||
|
- SSH Private Key muss als Secret `SSH_PRIVATE_KEY` in Gitea hinterlegt sein
|
||||||
|
|
||||||
|
## Image Tagging
|
||||||
|
|
||||||
|
Der Workflow erstellt zwei Image-Tags:
|
||||||
|
|
||||||
|
- `${REGISTRY_URL}/${NAMESPACE}/${REPO_NAME}:${COMMIT_SHA}` - Spezifischer Commit
|
||||||
|
- `${REGISTRY_URL}/${NAMESPACE}/${REPO_NAME}:latest` - Immer die neueste Version
|
||||||
|
|
||||||
|
## Deployment auf dem Server
|
||||||
|
|
||||||
|
Das Deployment-Script auf dem Server:
|
||||||
|
|
||||||
|
1. Navigiert zum Deployment-Verzeichnis
|
||||||
|
2. Authentifiziert sich bei der Container Registry
|
||||||
|
3. Setzt die IMAGE_TAG Umgebungsvariable auf den Commit SHA
|
||||||
|
4. Pullt das neueste Image
|
||||||
|
5. Startet die Services neu mit `docker compose up -d`
|
||||||
|
6. Räumt alte Images auf (älter als 7 Tage)
|
||||||
|
7. Zeigt laufende Container an
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Problem: Login zur Registry schlägt fehl
|
||||||
|
- Überprüfe CI_GITEA_TOKEN hat die richtigen Berechtigungen
|
||||||
|
- Stelle sicher, dass REGISTRY_URL korrekt ist (ohne `https://`)
|
||||||
|
|
||||||
|
### Problem: SSH-Verbindung schlägt fehl
|
||||||
|
- Überprüfe SSH_HOST, SSH_USERNAME und SSH_PORT
|
||||||
|
- Stelle sicher, dass SSH_PRIVATE_KEY korrekt formatiert ist (mit Zeilenumbrüchen)
|
||||||
|
- Prüfe, ob der Public Key auf dem Server hinterlegt ist
|
||||||
|
|
||||||
|
### Problem: Docker Compose startet nicht
|
||||||
|
- Überprüfe, ob die .env Datei auf dem Server existiert und korrekt ist
|
||||||
|
- Stelle sicher, dass compose.yml im DEPLOY_PATH vorhanden ist
|
||||||
|
- Prüfe die Logs mit `docker compose logs`
|
||||||
|
|
||||||
|
## Manuelles Deployment
|
||||||
|
|
||||||
|
Falls ein manuelles Deployment erforderlich ist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf dem Server
|
||||||
|
cd /opt/skamp/docker
|
||||||
|
|
||||||
|
# Login zur Registry
|
||||||
|
echo "YOUR_TOKEN" | docker login gitea.moz-tech.de -u murat --password-stdin
|
||||||
|
|
||||||
|
# Image pullen und starten
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Logs prüfen
|
||||||
|
docker compose logs -f app
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Auth
|
name: Auth App
|
||||||
type: http
|
type: http
|
||||||
seq: 2
|
seq: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
47
bruno/propify/DataImport/Auth Bruno.bru
Normal file
47
bruno/propify/DataImport/Auth Bruno.bru
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
meta {
|
||||||
|
name: Auth Bruno
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{KEYCLOAK_BASE_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token
|
||||||
|
body: formUrlEncoded
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
}
|
||||||
|
|
||||||
|
body:form-urlencoded {
|
||||||
|
grant_type: client_credentials
|
||||||
|
client_id: {{KEYCLOAK_BRUNO_CLIENT_ID}}
|
||||||
|
client_secret: {{KEYCLOAK_BRUNO_CLIENT_SECRET}}
|
||||||
|
}
|
||||||
|
|
||||||
|
script:post-response {
|
||||||
|
// Parse die JSON-Antwort
|
||||||
|
const jsonResponse = res.body;
|
||||||
|
|
||||||
|
|
||||||
|
if (jsonResponse.access_token) {
|
||||||
|
// Schreibe den access_token in eine Umgebungsvariable
|
||||||
|
// oder in eine collection-Variable
|
||||||
|
|
||||||
|
// Option 1: In eine Umgebungsvariable schreiben
|
||||||
|
// (z.B. für eine bestimmte Umgebung wie "Development")
|
||||||
|
bru.setEnvVar("BEARER_TOKEN", jsonResponse.access_token);
|
||||||
|
|
||||||
|
// Option 2: In eine Collection-Variable schreiben
|
||||||
|
// (Diese Variable ist global für alle Anfragen in deiner Collection)
|
||||||
|
// bru.setVar("bearerToken", "Bearer " + jsonResponse.access_token);
|
||||||
|
} else {
|
||||||
|
// optional: Error Handling, falls der Token nicht in der Antwort ist
|
||||||
|
console.log("Error: access_token not found in the response.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: false
|
||||||
|
}
|
||||||
39
bruno/propify/DataImport/Upload Organizations.bru
Normal file
39
bruno/propify/DataImport/Upload Organizations.bru
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
meta {
|
||||||
|
name: Upload Organizations
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{API_BASE_URL}}/api/{{API_VERSION}}/data-import/organizations
|
||||||
|
body: multipartForm
|
||||||
|
auth: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: {{BEARER_TOKEN}}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"name": "Bungalow",
|
||||||
|
"street": "Hebbelstraße",
|
||||||
|
"houseNumber": "30",
|
||||||
|
"zipCode": "55127",
|
||||||
|
"city": "Mainz",
|
||||||
|
"country": "DE",
|
||||||
|
"notes": "Lorem ipsum"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
file: @file(/Users/muratoezkorkmaz/projects/misc/skamp/init_data/organizations-17911945-23.xlsx)
|
||||||
|
}
|
||||||
|
|
||||||
|
body:file {
|
||||||
|
file: @file(/Users/murat/Pictures/IMG_0229.jpeg) @contentType(image/jpeg)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
}
|
||||||
39
bruno/propify/DataImport/Upload Persons.bru
Normal file
39
bruno/propify/DataImport/Upload Persons.bru
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
meta {
|
||||||
|
name: Upload Persons
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{API_BASE_URL}}/api/{{API_VERSION}}/data-import/persons
|
||||||
|
body: multipartForm
|
||||||
|
auth: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: {{BEARER_TOKEN}}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"name": "Bungalow",
|
||||||
|
"street": "Hebbelstraße",
|
||||||
|
"houseNumber": "30",
|
||||||
|
"zipCode": "55127",
|
||||||
|
"city": "Mainz",
|
||||||
|
"country": "DE",
|
||||||
|
"notes": "Lorem ipsum"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
file: @file(/Users/muratoezkorkmaz/projects/misc/skamp/init_data/people-17911945-22.xlsx)
|
||||||
|
}
|
||||||
|
|
||||||
|
body:file {
|
||||||
|
file: @file(/Users/murat/Pictures/IMG_0229.jpeg) @contentType(image/jpeg)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
}
|
||||||
8
bruno/propify/DataImport/folder.bru
Normal file
8
bruno/propify/DataImport/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: DataImport
|
||||||
|
seq: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
vars {
|
vars {
|
||||||
API_BASE_URL: http://localhost:8080
|
API_BASE_URL: http://localhost:8180
|
||||||
API_VERSION: v1
|
API_VERSION: v1
|
||||||
KEYCLOAK_BASE_URL: http://localhost:8280
|
KEYCLOAK_BASE_URL: https://kc.dev.localhost
|
||||||
DEV_USERNAME: dev@example.com
|
DEV_USERNAME: dev@example.com
|
||||||
DEV_PASSWORD: dev
|
DEV_PASSWORD: dev
|
||||||
ADMIN_USERNAME: admin@example.com
|
ADMIN_USERNAME: admin@example.com
|
||||||
ADMIN_PASSWORD: admin
|
ADMIN_PASSWORD: admin
|
||||||
KEYCLOAK_CLIENT_ID: skamp-app
|
KEYCLOAK_CLIENT_ID: skamp-app
|
||||||
KEYCLOAK_REALM: skamp
|
KEYCLOAK_REALM: skamp
|
||||||
|
KEYCLOAK_BRUNO_CLIENT_ID: skamp-bruno
|
||||||
|
KEYCLOAK_BRUNO_CLIENT_SECRET: sNQpCVyVckGo5AZw7FqeW0POtgWuXzJt
|
||||||
}
|
}
|
||||||
vars:secret [
|
vars:secret [
|
||||||
BEARER_TOKEN
|
BEARER_TOKEN
|
||||||
|
|||||||
BIN
init_data/organizations-17911945-23.xlsx
Normal file
BIN
init_data/organizations-17911945-23.xlsx
Normal file
Binary file not shown.
BIN
init_data/people-17911945-22.xlsx
Normal file
BIN
init_data/people-17911945-22.xlsx
Normal file
Binary file not shown.
7
pom.xml
7
pom.xml
@@ -110,6 +110,13 @@
|
|||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- EasyExcel for Excel processing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>easyexcel</artifactId>
|
||||||
|
<version>3.3.4</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package de.iwomm.propify_api.controller;
|
||||||
|
|
||||||
|
import de.iwomm.propify_api.service.OrganizationImportService;
|
||||||
|
import de.iwomm.propify_api.service.PersonImportService;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/data-import")
|
||||||
|
public class DataImportController {
|
||||||
|
|
||||||
|
private final PersonImportService personImportService;
|
||||||
|
private final OrganizationImportService organizationImportService;
|
||||||
|
|
||||||
|
public DataImportController(PersonImportService personImportService,
|
||||||
|
OrganizationImportService organizationImportService) {
|
||||||
|
this.personImportService = personImportService;
|
||||||
|
this.organizationImportService = organizationImportService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/persons")
|
||||||
|
public ResponseEntity<?> importPersons(@RequestParam("file") MultipartFile file) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body("Please upload a file");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.getOriginalFilename().endsWith(".xlsx")) {
|
||||||
|
return ResponseEntity.badRequest().body("Only .xlsx files are supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PersonImportService.ImportResult result = personImportService.importPersonsFromExcel(file);
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body("Error processing file: " + e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body("Unexpected error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/organizations")
|
||||||
|
public ResponseEntity<?> importOrganizations(@RequestParam("file") MultipartFile file) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body("Please upload a file");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.getOriginalFilename().endsWith(".xlsx")) {
|
||||||
|
return ResponseEntity.badRequest().body("Only .xlsx files are supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
OrganizationImportService.ImportResult result = organizationImportService.importOrganizationsFromExcel(file);
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body("Error processing file: " + e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body("Unexpected error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package de.iwomm.propify_api.dto;
|
||||||
|
|
||||||
|
import com.alibaba.excel.annotation.ExcelProperty;
|
||||||
|
|
||||||
|
public class OrganizationImportDTO {
|
||||||
|
|
||||||
|
@ExcelProperty(index = 0, value = "Organisation - Name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 1, value = "Organisation - Branche Intern")
|
||||||
|
private String industry;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 2, value = "Organisation - Label")
|
||||||
|
private String labels;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 3, value = "Organisation - Abgeschlossene Deals")
|
||||||
|
private String completedDeals;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 4, value = "Organisation - Offene Deals")
|
||||||
|
private String openDeals;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 5, value = "Organisation - Datum nächste Aktivität")
|
||||||
|
private String nextActivityDate;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 6, value = "Organisation - Besitzer")
|
||||||
|
private String owner;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIndustry() {
|
||||||
|
return industry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIndustry(String industry) {
|
||||||
|
this.industry = industry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabels() {
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabels(String labels) {
|
||||||
|
this.labels = labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCompletedDeals() {
|
||||||
|
return completedDeals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCompletedDeals(String completedDeals) {
|
||||||
|
this.completedDeals = completedDeals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOpenDeals() {
|
||||||
|
return openDeals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOpenDeals(String openDeals) {
|
||||||
|
this.openDeals = openDeals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNextActivityDate() {
|
||||||
|
return nextActivityDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNextActivityDate(String nextActivityDate) {
|
||||||
|
this.nextActivityDate = nextActivityDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOwner() {
|
||||||
|
return owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwner(String owner) {
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/main/java/de/iwomm/propify_api/dto/PersonImportDTO.java
Normal file
117
src/main/java/de/iwomm/propify_api/dto/PersonImportDTO.java
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package de.iwomm.propify_api.dto;
|
||||||
|
|
||||||
|
import com.alibaba.excel.annotation.ExcelProperty;
|
||||||
|
|
||||||
|
public class PersonImportDTO {
|
||||||
|
|
||||||
|
@ExcelProperty(index = 0, value = "Person - Name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 1, value = "Person - Label")
|
||||||
|
private String labels;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 2, value = "Person - Organisation")
|
||||||
|
private String organization;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 3, value = "Person - E-Mail-Adresse - Büro")
|
||||||
|
private String emailOffice;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 4, value = "Person - E-Mail-Adresse - Privat")
|
||||||
|
private String emailPrivate;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 5, value = "Person - E-Mail-Adresse - Sonstiger")
|
||||||
|
private String emailOther;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 6, value = "Person - Telefon - Büro")
|
||||||
|
private String phoneOffice;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 7, value = "Person - Telefon - Privat")
|
||||||
|
private String phonePrivate;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 8, value = "Person - Telefon - Mobil")
|
||||||
|
private String phoneMobile;
|
||||||
|
|
||||||
|
@ExcelProperty(index = 9, value = "Person - Telefon - Sonstiger")
|
||||||
|
private String phoneOther;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabels() {
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabels(String labels) {
|
||||||
|
this.labels = labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrganization() {
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrganization(String organization) {
|
||||||
|
this.organization = organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmailOffice() {
|
||||||
|
return emailOffice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmailOffice(String emailOffice) {
|
||||||
|
this.emailOffice = emailOffice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmailPrivate() {
|
||||||
|
return emailPrivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmailPrivate(String emailPrivate) {
|
||||||
|
this.emailPrivate = emailPrivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmailOther() {
|
||||||
|
return emailOther;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmailOther(String emailOther) {
|
||||||
|
this.emailOther = emailOther;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneOffice() {
|
||||||
|
return phoneOffice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhoneOffice(String phoneOffice) {
|
||||||
|
this.phoneOffice = phoneOffice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhonePrivate() {
|
||||||
|
return phonePrivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhonePrivate(String phonePrivate) {
|
||||||
|
this.phonePrivate = phonePrivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneMobile() {
|
||||||
|
return phoneMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhoneMobile(String phoneMobile) {
|
||||||
|
this.phoneMobile = phoneMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneOther() {
|
||||||
|
return phoneOther;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhoneOther(String phoneOther) {
|
||||||
|
this.phoneOther = phoneOther;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/main/java/de/iwomm/propify_api/entity/Industry.java
Normal file
31
src/main/java/de/iwomm/propify_api/entity/Industry.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package de.iwomm.propify_api.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public class Industry {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public UUID getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(UUID id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/main/java/de/iwomm/propify_api/entity/Organization.java
Normal file
71
src/main/java/de/iwomm/propify_api/entity/Organization.java
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package de.iwomm.propify_api.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public class Organization {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ManyToOne()
|
||||||
|
@JoinColumn(name = "industry_id")
|
||||||
|
private Industry industry;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String owner;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(UUID id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Industry getIndustry() {
|
||||||
|
return industry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIndustry(Industry industry) {
|
||||||
|
this.industry = industry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOwner() {
|
||||||
|
return owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwner(String owner) {
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,199 @@
|
|||||||
package de.iwomm.propify_api.entity;
|
package de.iwomm.propify_api.entity;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public class Person {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String firstName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String lastName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String emailOffice;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String emailPrivate;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String emailOther;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String phoneOfficeCountryCode;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String phoneOfficeAreaCode;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String phoneOfficeNumber;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String phonePrivateCountryCode;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String phonePrivateAreaCode;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String phonePrivateNumber;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String phoneMobile;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
private String keycloakId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
|
||||||
|
@JoinTable(
|
||||||
|
name = "person_labels",
|
||||||
|
joinColumns = @JoinColumn(name = "person_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "label_id"),
|
||||||
|
foreignKey = @ForeignKey(name = "fk_person_labels_person", foreignKeyDefinition = "FOREIGN KEY (person_id) REFERENCES person(id) ON DELETE CASCADE")
|
||||||
|
)
|
||||||
|
private Set<PersonLabel> labels = new HashSet<>();
|
||||||
|
|
||||||
|
public UUID getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(UUID id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getEmailOffice() {
|
||||||
|
return emailOffice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmailOffice(String emailOffice) {
|
||||||
|
this.emailOffice = emailOffice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmailPrivate() {
|
||||||
|
return emailPrivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmailPrivate(String emailPrivate) {
|
||||||
|
this.emailPrivate = emailPrivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmailOther() {
|
||||||
|
return emailOther;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmailOther(String emailOther) {
|
||||||
|
this.emailOther = emailOther;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneOfficeCountryCode() {
|
||||||
|
return phoneOfficeCountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhoneOfficeCountryCode(String phoneOfficeCountryCode) {
|
||||||
|
this.phoneOfficeCountryCode = phoneOfficeCountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneOfficeAreaCode() {
|
||||||
|
return phoneOfficeAreaCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhoneOfficeAreaCode(String phoneOfficeAreaCode) {
|
||||||
|
this.phoneOfficeAreaCode = phoneOfficeAreaCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneOfficeNumber() {
|
||||||
|
return phoneOfficeNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhoneOfficeNumber(String phoneOfficeNumber) {
|
||||||
|
this.phoneOfficeNumber = phoneOfficeNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhonePrivateCountryCode() {
|
||||||
|
return phonePrivateCountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhonePrivateCountryCode(String phonePrivateCountryCode) {
|
||||||
|
this.phonePrivateCountryCode = phonePrivateCountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhonePrivateAreaCode() {
|
||||||
|
return phonePrivateAreaCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhonePrivateAreaCode(String phonePrivateAreaCode) {
|
||||||
|
this.phonePrivateAreaCode = phonePrivateAreaCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhonePrivateNumber() {
|
||||||
|
return phonePrivateNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhonePrivateNumber(String phonePrivateNumber) {
|
||||||
|
this.phonePrivateNumber = phonePrivateNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneMobile() {
|
||||||
|
return phoneMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhoneMobile(String phoneMobile) {
|
||||||
|
this.phoneMobile = phoneMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKeycloakId() {
|
||||||
|
return keycloakId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeycloakId(String keycloakId) {
|
||||||
|
this.keycloakId = keycloakId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<PersonLabel> getLabels() {
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabels(Set<PersonLabel> labels) {
|
||||||
|
this.labels = labels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
31
src/main/java/de/iwomm/propify_api/entity/PersonLabel.java
Normal file
31
src/main/java/de/iwomm/propify_api/entity/PersonLabel.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package de.iwomm.propify_api.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public class PersonLabel {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public UUID getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(UUID id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.iwomm.propify_api.repository;
|
||||||
|
|
||||||
|
import de.iwomm.propify_api.entity.Industry;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface IndustryRepository extends JpaRepository<Industry, UUID> {
|
||||||
|
Optional<Industry> findByName(String name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.iwomm.propify_api.repository;
|
||||||
|
|
||||||
|
import de.iwomm.propify_api.entity.Organization;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface OrganizationRepository extends JpaRepository<Organization, UUID> {
|
||||||
|
Optional<Organization> findByName(String name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.iwomm.propify_api.repository;
|
||||||
|
|
||||||
|
import de.iwomm.propify_api.entity.PersonLabel;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface PersonLabelRepository extends JpaRepository<PersonLabel, UUID> {
|
||||||
|
Optional<PersonLabel> findByName(String name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.iwomm.propify_api.repository;
|
||||||
|
|
||||||
|
import de.iwomm.propify_api.entity.Person;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface PersonRepository extends JpaRepository<Person, UUID> {
|
||||||
|
Optional<Person> findByKeycloakId(String keycloakId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package de.iwomm.propify_api.service;
|
||||||
|
|
||||||
|
import com.alibaba.excel.EasyExcel;
|
||||||
|
import com.alibaba.excel.context.AnalysisContext;
|
||||||
|
import com.alibaba.excel.read.listener.ReadListener;
|
||||||
|
import de.iwomm.propify_api.dto.OrganizationImportDTO;
|
||||||
|
import de.iwomm.propify_api.entity.Industry;
|
||||||
|
import de.iwomm.propify_api.entity.Organization;
|
||||||
|
import de.iwomm.propify_api.repository.IndustryRepository;
|
||||||
|
import de.iwomm.propify_api.repository.OrganizationRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OrganizationImportService {
|
||||||
|
|
||||||
|
private final OrganizationRepository organizationRepository;
|
||||||
|
private final IndustryRepository industryRepository;
|
||||||
|
|
||||||
|
public OrganizationImportService(OrganizationRepository organizationRepository,
|
||||||
|
IndustryRepository industryRepository) {
|
||||||
|
this.organizationRepository = organizationRepository;
|
||||||
|
this.industryRepository = industryRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ImportResult importOrganizationsFromExcel(MultipartFile file) throws IOException {
|
||||||
|
ImportResult result = new ImportResult();
|
||||||
|
|
||||||
|
EasyExcel.read(file.getInputStream(), OrganizationImportDTO.class, new ReadListener<OrganizationImportDTO>() {
|
||||||
|
private int currentRow = 1; // Start at 1 to account for header
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invoke(OrganizationImportDTO data, AnalysisContext context) {
|
||||||
|
currentRow++;
|
||||||
|
try {
|
||||||
|
Organization organization = convertToOrganization(data);
|
||||||
|
organizationRepository.save(organization);
|
||||||
|
result.incrementSuccess();
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addError(currentRow, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||||
|
// Called after all data has been analyzed
|
||||||
|
}
|
||||||
|
}).sheet().doRead();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Organization convertToOrganization(OrganizationImportDTO dto) {
|
||||||
|
Organization organization = new Organization();
|
||||||
|
|
||||||
|
// Name
|
||||||
|
if (dto.getName() != null && !dto.getName().trim().isEmpty()) {
|
||||||
|
organization.setName(dto.getName().trim());
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Name is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Industry (Branche) - create or lookup
|
||||||
|
if (dto.getIndustry() != null && !dto.getIndustry().trim().isEmpty()) {
|
||||||
|
String industryName = dto.getIndustry().trim();
|
||||||
|
Industry industry = industryRepository.findByName(industryName)
|
||||||
|
.orElseGet(() -> {
|
||||||
|
Industry newIndustry = new Industry();
|
||||||
|
newIndustry.setName(industryName);
|
||||||
|
return industryRepository.save(newIndustry);
|
||||||
|
});
|
||||||
|
organization.setIndustry(industry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner
|
||||||
|
organization.setOwner(trimOrNull(dto.getOwner()));
|
||||||
|
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimOrNull(String value) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ImportResult {
|
||||||
|
private int successCount = 0;
|
||||||
|
private int errorCount = 0;
|
||||||
|
private List<ImportError> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
public void incrementSuccess() {
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addError(int row, String message) {
|
||||||
|
errorCount++;
|
||||||
|
errors.add(new ImportError(row, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSuccessCount() {
|
||||||
|
return successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getErrorCount() {
|
||||||
|
return errorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ImportError> getErrors() {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ImportError {
|
||||||
|
private int row;
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public ImportError(int row, String message) {
|
||||||
|
this.row = row;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRow() {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package de.iwomm.propify_api.service;
|
||||||
|
|
||||||
|
import com.alibaba.excel.EasyExcel;
|
||||||
|
import com.alibaba.excel.context.AnalysisContext;
|
||||||
|
import com.alibaba.excel.read.listener.ReadListener;
|
||||||
|
import de.iwomm.propify_api.dto.PersonImportDTO;
|
||||||
|
import de.iwomm.propify_api.entity.Person;
|
||||||
|
import de.iwomm.propify_api.entity.PersonLabel;
|
||||||
|
import de.iwomm.propify_api.repository.OrganizationRepository;
|
||||||
|
import de.iwomm.propify_api.repository.PersonLabelRepository;
|
||||||
|
import de.iwomm.propify_api.repository.PersonRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PersonImportService {
|
||||||
|
|
||||||
|
private final PersonRepository personRepository;
|
||||||
|
private final PersonLabelRepository personLabelRepository;
|
||||||
|
private final OrganizationRepository organizationRepository;
|
||||||
|
|
||||||
|
public PersonImportService(PersonRepository personRepository,
|
||||||
|
PersonLabelRepository personLabelRepository,
|
||||||
|
OrganizationRepository organizationRepository) {
|
||||||
|
this.personRepository = personRepository;
|
||||||
|
this.personLabelRepository = personLabelRepository;
|
||||||
|
this.organizationRepository = organizationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ImportResult importPersonsFromExcel(MultipartFile file) throws IOException {
|
||||||
|
ImportResult result = new ImportResult();
|
||||||
|
|
||||||
|
EasyExcel.read(file.getInputStream(), PersonImportDTO.class, new ReadListener<PersonImportDTO>() {
|
||||||
|
private int currentRow = 1; // Start at 1 to account for header
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invoke(PersonImportDTO data, AnalysisContext context) {
|
||||||
|
currentRow++;
|
||||||
|
try {
|
||||||
|
Person person = convertToPerson(data);
|
||||||
|
personRepository.save(person);
|
||||||
|
result.incrementSuccess();
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addError(currentRow, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||||
|
// Called after all data has been analyzed
|
||||||
|
}
|
||||||
|
}).sheet().doRead();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Person convertToPerson(PersonImportDTO dto) {
|
||||||
|
Person person = new Person();
|
||||||
|
|
||||||
|
// Parse name (format: "firstName lastName")
|
||||||
|
if (dto.getName() != null && !dto.getName().trim().isEmpty()) {
|
||||||
|
String[] nameParts = dto.getName().trim().split("\\s+", 2);
|
||||||
|
person.setFirstName(nameParts[0]);
|
||||||
|
person.setLastName(nameParts.length > 1 ? nameParts[1] : null);
|
||||||
|
} else {
|
||||||
|
person.setFirstName(null);
|
||||||
|
person.setLastName(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse labels (comma-separated)
|
||||||
|
if (dto.getLabels() != null && !dto.getLabels().trim().isEmpty()) {
|
||||||
|
Set<PersonLabel> labels = new HashSet<>();
|
||||||
|
String[] labelNames = dto.getLabels().split(",");
|
||||||
|
for (String labelName : labelNames) {
|
||||||
|
String trimmedName = labelName.trim();
|
||||||
|
if (!trimmedName.isEmpty()) {
|
||||||
|
PersonLabel label = personLabelRepository.findByName(trimmedName)
|
||||||
|
.orElseGet(() -> {
|
||||||
|
PersonLabel newLabel = new PersonLabel();
|
||||||
|
newLabel.setName(trimmedName);
|
||||||
|
return personLabelRepository.save(newLabel);
|
||||||
|
});
|
||||||
|
labels.add(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
person.setLabels(labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization (not yet linked in Person entity)
|
||||||
|
// String organization = dto.getOrganization();
|
||||||
|
|
||||||
|
// Email addresses
|
||||||
|
person.setEmailOffice(trimOrNull(dto.getEmailOffice()));
|
||||||
|
person.setEmailPrivate(trimOrNull(dto.getEmailPrivate()));
|
||||||
|
person.setEmailOther(trimOrNull(dto.getEmailOther()));
|
||||||
|
|
||||||
|
// Office phone
|
||||||
|
if (dto.getPhoneOffice() != null && !dto.getPhoneOffice().trim().isEmpty()) {
|
||||||
|
PhoneNumber parsed = parsePhoneNumber(dto.getPhoneOffice());
|
||||||
|
person.setPhoneOfficeCountryCode(parsed.countryCode);
|
||||||
|
person.setPhoneOfficeAreaCode(parsed.areaCode);
|
||||||
|
person.setPhoneOfficeNumber(parsed.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private phone
|
||||||
|
if (dto.getPhonePrivate() != null && !dto.getPhonePrivate().trim().isEmpty()) {
|
||||||
|
PhoneNumber parsed = parsePhoneNumber(dto.getPhonePrivate());
|
||||||
|
person.setPhonePrivateCountryCode(parsed.countryCode);
|
||||||
|
person.setPhonePrivateAreaCode(parsed.areaCode);
|
||||||
|
person.setPhonePrivateNumber(parsed.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile phone (prefer phoneMobile, fallback to phoneOther)
|
||||||
|
String mobile = dto.getPhoneMobile();
|
||||||
|
String other = dto.getPhoneOther();
|
||||||
|
|
||||||
|
if (mobile != null && !mobile.trim().isEmpty()) {
|
||||||
|
person.setPhoneMobile(mobile.trim());
|
||||||
|
} else if (other != null && !other.trim().isEmpty()) {
|
||||||
|
person.setPhoneMobile(other.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return person;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimOrNull(String value) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PhoneNumber parsePhoneNumber(String phone) {
|
||||||
|
PhoneNumber result = new PhoneNumber();
|
||||||
|
|
||||||
|
// Remove all whitespace and special characters except + and digits
|
||||||
|
String cleaned = phone.replaceAll("[^+\\d]", "");
|
||||||
|
|
||||||
|
// Pattern to match: +CountryCode AreaCode Number
|
||||||
|
// Examples: +493012345678, +4930123456
|
||||||
|
Pattern pattern = Pattern.compile("^(\\+\\d{1,3})(\\d{2,5})(\\d+)$");
|
||||||
|
Matcher matcher = pattern.matcher(cleaned);
|
||||||
|
|
||||||
|
if (matcher.matches()) {
|
||||||
|
result.countryCode = matcher.group(1);
|
||||||
|
result.areaCode = matcher.group(2);
|
||||||
|
result.number = matcher.group(3);
|
||||||
|
} else {
|
||||||
|
// If pattern doesn't match, try simpler parsing
|
||||||
|
if (cleaned.startsWith("+")) {
|
||||||
|
// Assume format: +CC followed by rest
|
||||||
|
int ccEnd = Math.min(4, cleaned.length()); // Max 3 digits + '+'
|
||||||
|
result.countryCode = cleaned.substring(0, ccEnd);
|
||||||
|
|
||||||
|
String rest = cleaned.substring(ccEnd);
|
||||||
|
if (rest.length() > 4) {
|
||||||
|
// Split remaining into area code (2-3 digits) and number
|
||||||
|
result.areaCode = rest.substring(0, Math.min(3, rest.length() - 4));
|
||||||
|
result.number = rest.substring(Math.min(3, rest.length() - 4));
|
||||||
|
} else {
|
||||||
|
result.number = rest;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No country code, store as-is in number field
|
||||||
|
result.number = cleaned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PhoneNumber {
|
||||||
|
String countryCode;
|
||||||
|
String areaCode;
|
||||||
|
String number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ImportResult {
|
||||||
|
private int successCount = 0;
|
||||||
|
private int errorCount = 0;
|
||||||
|
private List<ImportError> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
public void incrementSuccess() {
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addError(int row, String message) {
|
||||||
|
errorCount++;
|
||||||
|
errors.add(new ImportError(row, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSuccessCount() {
|
||||||
|
return successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getErrorCount() {
|
||||||
|
return errorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ImportError> getErrors() {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ImportError {
|
||||||
|
private int row;
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public ImportError(int row, String message) {
|
||||||
|
this.row = row;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRow() {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user