diff --git a/docker-compose/multiwar/.env b/docker-compose/multiwar/.env
new file mode 100644
index 00000000..2633c881
--- /dev/null
+++ b/docker-compose/multiwar/.env
@@ -0,0 +1,6 @@
+SHOULD_INIT_GEO_DATA=true
+SHOULD_INIT_LINES_CATALOG=true
+
+COMPOSE_PROJECT_NAME=gridstudy
+
+TOMCAT_PORT=12345
diff --git a/docker-compose/multiwar/README.md b/docker-compose/multiwar/README.md
new file mode 100644
index 00000000..895e5eaa
--- /dev/null
+++ b/docker-compose/multiwar/README.md
@@ -0,0 +1,58 @@
+# WAR Deploy Tool
+
+Generated with little supervision by opus4.6.
+
+Builds GridSuite microservices as WAR files for docker-compose deployment, **without modifying any original source files**.
+
+## How it works
+
+Instead of patching server sources, this tool generates **wrapper Maven projects** that:
+1. Depend on the original server JAR (classes artifact)
+2. Add a generated `SpringBootServletInitializer` class that delegates to the original Application class
+3. Package everything as a WAR with a clean context-path name
+
+```
+Original server (unmodified) Wrapper project (generated)
+┌─────────────────────────┐ ┌─────────────────────────┐
+│ report-server/ │ │ war-wrappers/report-server/
+│ pom.xml (jar) │◄─────────│ pom.xml (war) │
+│ ReportApplication.java│ │ WarInitializer.java │
+└─────────────────────────┘ └─────────────────────────┘
+ │ │
+ ▼ ▼
+ gridsuite-report-server.jar report-server.war
+```
+
+## Quick Start
+
+```bash
+# Build all WARs and prepare for docker-compose
+./wars.sh
+
+# Start the stack
+docker compose up
+```
+
+## Enabling/disabling servers
+
+Edit the `MANIFEST` array in `war-deploy.sh`. Comment lines with `#` to disable:
+
+```bash
+"actions-server|actions-server||..." # enabled
+#"timeseries-server|timeseries-server||..." # disabled
+```
+
+## Prerequisites
+
+- Java 21+
+- Maven 3.8+
+- Server submodules checked out and buildable in the layout of Gridsuite Aggregator
+
+## Excluded (Boot WebFlux — incompatible(??) with WAR)
+
+- config-server
+- config-notification-server
+- directory-notification-server
+- merge-notification-server
+- study-notification-server
+- gateway
diff --git a/docker-compose/multiwar/docker-compose.multiwar.yml b/docker-compose/multiwar/docker-compose.multiwar.yml
new file mode 100644
index 00000000..8f9e1ab8
--- /dev/null
+++ b/docker-compose/multiwar/docker-compose.multiwar.yml
@@ -0,0 +1,252 @@
+services:
+
+ # ===========================================================================
+ # SINGLE TOMCAT — all WAR-deployed microservices (33 webmvc servers)
+ # ===========================================================================
+ tomcat:
+ # JDK 21 required: servers are compiled with Java 21 (class file version 65)
+ image: tomcat:10.1-jdk21
+ ports:
+ - "${TOMCAT_PORT:-12345}:8080"
+ volumes:
+ # Custom server.xml: sets Host startStopThreads="10" for parallel WAR deployment
+ - ./server.xml:/usr/local/tomcat/conf/server.xml:Z
+ - ./gen/wars:/usr/local/tomcat/webapps:Z
+ # Per-WAR config overrides (base-uri rewrites) loaded via spring.config.additional-location
+ # in each WAR's SpringBootServletInitializer. Only contains service base-uri mappings.
+ - ./gen/externalized-war-configs:/config:Z
+ # Merged powsybl itools config (loadflow params, writeReferenceTerminals: false, etc.)
+ - ./itools-config.yml:/root/.itools/config.yml:Z
+ environment:
+ # spring.profiles.active=default: disables "local" profile so application-local.yml
+ # files embedded in server JARs are NOT loaded. Infrastructure defaults (rabbitmq,
+ # postgres, elasticsearch hosts) come from powsybl-ws-commons.jar application.yaml
+ # which already has the correct docker service names.
+ - CATALINA_OPTS=-Dspring.profiles.active=default -Xmx2g
+ depends_on:
+ - postgres
+ - rabbitmq
+ - elasticsearch
+ - s3-storage
+ # Wait for elasticsearch before starting Tomcat: WARs that depend on elasticsearch
+ # (study-server, directory-server, case-server, network-conversion-server) fail
+ # immediately if ES is not reachable at startup (no built-in retry).
+ entrypoint: ["/bin/bash", "-c", "until curl -sf http://elasticsearch:9200/_cluster/health; do echo 'Waiting for elasticsearch...'; sleep 3; done && catalina.sh run"]
+ restart: unless-stopped
+ memswap_limit: 5g
+ deploy:
+ resources:
+ limits:
+ memory: 5g
+ # Future: move shared JARs (Spring, Hibernate, powsybl-*) to Tomcat lib/ so
+ # they are loaded once by the common classloader, reducing Metaspace usage.
+ # Current setup: each WAR bundles ~200MB of dependencies, many identical across
+ # WARs. Shared classloading could cut memory by 30-50%.
+
+ # ===========================================================================
+ # NON-WAR SERVICES (WebFlux / notification servers)
+ # Future: avoid redeclaring these services here — find a way to reuse the
+ # definitions from docker-compose.base.yml directly.
+ # ===========================================================================
+ config-server:
+ image: gridsuite/config-server:latest
+ ports:
+ - 5025:80
+ volumes:
+ - ../../k8s/resources/common/config/config-server-application.yml:/config/specific/application.yml:Z
+ - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z
+ restart: unless-stopped
+ environment:
+ - JAVA_TOOL_OPTIONS=-Xmx96m
+ command: --server.port=80 --spring.config.additional-location=/config/
+ sysctls:
+ - net.ipv4.ip_unprivileged_port_start=0
+ memswap_limit: 384m
+ deploy:
+ resources:
+ limits:
+ memory: 384m
+
+ config-notification-server:
+ image: gridsuite/config-notification-server:latest
+ ports:
+ - 5024:80
+ volumes:
+ - ../../k8s/resources/common/config/config-notification-server-application.yml:/config/specific/application.yml:Z
+ - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z
+ restart: unless-stopped
+ environment:
+ - JAVA_TOOL_OPTIONS=-Xmx96m
+ command: --server.port=80 --spring.config.additional-location=/config/
+ sysctls:
+ - net.ipv4.ip_unprivileged_port_start=0
+ memswap_limit: 384m
+ deploy:
+ resources:
+ limits:
+ memory: 384m
+
+ study-notification-server:
+ image: gridsuite/study-notification-server:latest
+ ports:
+ - 5009:80
+ volumes:
+ - ../../k8s/resources/study/config/study-notification-server-application.yml:/config/specific/application.yml:Z
+ - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z
+ restart: unless-stopped
+ environment:
+ - JAVA_TOOL_OPTIONS=-Xmx96m
+ command: --server.port=80 --spring.config.additional-location=/config/
+ sysctls:
+ - net.ipv4.ip_unprivileged_port_start=0
+ memswap_limit: 384m
+ deploy:
+ resources:
+ limits:
+ memory: 384m
+
+ directory-notification-server:
+ image: gridsuite/directory-notification-server:latest
+ ports:
+ - 5004:80
+ volumes:
+ - ../../k8s/resources/study/config/directory-notification-server-application.yml:/config/specific/application.yml:Z
+ - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z
+ restart: unless-stopped
+ environment:
+ - JAVA_TOOL_OPTIONS=-Xmx96m
+ command: --server.port=80 --spring.config.additional-location=/config/
+ sysctls:
+ - net.ipv4.ip_unprivileged_port_start=0
+ memswap_limit: 384m
+ deploy:
+ resources:
+ limits:
+ memory: 384m
+
+ gateway:
+ image: gridsuite/gateway:latest
+ ports:
+ - 9000:80
+ volumes:
+ - ../../k8s/resources/common/config/gateway-application.yml:/config/specific/application.yml:Z
+ - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z
+ - ../allowed-issuers.yml:/config/allowed-issuers.yml:Z
+ restart: unless-stopped
+ environment:
+ - JAVA_TOOL_OPTIONS=-Xmx96m
+ command: --server.port=80 --spring.config.additional-location=/config/
+ sysctls:
+ - net.ipv4.ip_unprivileged_port_start=0
+ memswap_limit: 384m
+ deploy:
+ resources:
+ limits:
+ memory: 384m
+
+ # ===========================================================================
+ # FRONTEND APPS
+ # ===========================================================================
+ gridstudy-app:
+ image: gridsuite/gridstudy-app:latest
+ ports:
+ - 84:8080
+ volumes:
+ - ../study/gridstudy-app-idpSettings.json:/opt/bitnami/apache/htdocs/gridstudy/idpSettings.json:Z
+ - ../env.json:/opt/bitnami/apache/htdocs/gridstudy/env.json:Z
+ memswap_limit: 128m
+ deploy:
+ resources:
+ limits:
+ memory: 128m
+ restart: unless-stopped
+
+ gridexplore-app:
+ image: gridsuite/gridexplore-app:latest
+ ports:
+ - 80:8080
+ volumes:
+ - ../study/gridexplore-app-idpSettings.json:/opt/bitnami/apache/htdocs/gridexplore/idpSettings.json:Z
+ - ../env.json:/opt/bitnami/apache/htdocs/gridexplore/env.json:Z
+ memswap_limit: 128m
+ deploy:
+ resources:
+ limits:
+ memory: 128m
+ restart: unless-stopped
+
+ gridadmin-app:
+ image: gridsuite/gridadmin-app:latest
+ ports:
+ - 82:8080
+ volumes:
+ - ../study/gridadmin-app-idpSettings.json:/opt/bitnami/apache/htdocs/gridadmin/idpSettings.json:Z
+ - ../env.json:/opt/bitnami/apache/htdocs/gridadmin/env.json:Z
+ memswap_limit: 128m
+ deploy:
+ resources:
+ limits:
+ memory: 128m
+ restart: unless-stopped
+
+ # ===========================================================================
+ # MOCK & METADATA
+ # ===========================================================================
+ mock-user-service:
+ image: gridsuite/oidc-mock-server
+ ports:
+ - 9090:9090
+ environment:
+ - PORT=9090
+ - ISSUER_HOST=172.17.0.1:9090
+ - USERS_PROFILE=UTILISATEURS|ADMIN|ADMIN_EXPLORE
+ - CLIENT_COUNT=8
+ - CLIENT_ID=gridexplore-client
+ - CLIENT_REDIRECT_URI=http://localhost:80/sign-in-callback
+ - CLIENT_LOGOUT_REDIRECT_URI=http://localhost:80/logout-callback
+ - CLIENT_SILENT_REDIRECT_URI=http://localhost:80/silent-renew-callback
+ - CLIENT_ID_2=gridadmin-client
+ - CLIENT_REDIRECT_URI_2=http://localhost:82/sign-in-callback
+ - CLIENT_LOGOUT_REDIRECT_URI_2=http://localhost:82/logout-callback
+ - CLIENT_SILENT_REDIRECT_URI_2=http://localhost:82/silent-renew-callback
+ - CLIENT_ID_3=griddyna-client
+ - CLIENT_REDIRECT_URI_3=http://localhost:83/sign-in-callback
+ - CLIENT_LOGOUT_REDIRECT_URI_3=http://localhost:83/logout-callback
+ - CLIENT_SILENT_REDIRECT_URI_3=http://localhost:83/silent-renew-callback
+ - CLIENT_ID_4=gridstudy-client
+ - CLIENT_REDIRECT_URI_4=http://localhost:84/sign-in-callback
+ - CLIENT_LOGOUT_REDIRECT_URI_4=http://localhost:84/logout-callback
+ - CLIENT_SILENT_REDIRECT_URI_4=http://localhost:84/silent-renew-callback
+ - CLIENT_ID_5=gridexplore-local
+ - CLIENT_REDIRECT_URI_5=http://localhost:3000/sign-in-callback
+ - CLIENT_LOGOUT_REDIRECT_URI_5=http://localhost:3000/logout-callback
+ - CLIENT_SILENT_REDIRECT_URI_5=http://localhost:3000/silent-renew-callback
+ - CLIENT_ID_6=gridadmin-local
+ - CLIENT_REDIRECT_URI_6=http://localhost:3002/sign-in-callback
+ - CLIENT_LOGOUT_REDIRECT_URI_6=http://localhost:3002/logout-callback
+ - CLIENT_SILENT_REDIRECT_URI_6=http://localhost:3002/silent-renew-callback
+ - CLIENT_ID_7=griddyna-local
+ - CLIENT_REDIRECT_URI_7=http://localhost:3003/sign-in-callback
+ - CLIENT_LOGOUT_REDIRECT_URI_7=http://localhost:3003/logout-callback
+ - CLIENT_SILENT_REDIRECT_URI_7=http://localhost:3003/silent-renew-callback
+ - CLIENT_ID_8=gridstudy-local
+ - CLIENT_REDIRECT_URI_8=http://localhost:3004/sign-in-callback
+ - CLIENT_LOGOUT_REDIRECT_URI_8=http://localhost:3004/logout-callback
+ - CLIENT_SILENT_REDIRECT_URI_8=http://localhost:3004/silent-renew-callback
+ restart: unless-stopped
+
+ apps-metadata-server:
+ image: bitnami/apache:2.4.55-debian-11-r3@sha256:bbe50190eb3bbf3be6f61318004480b3230846bfd52dec9286bd1862254c1719
+ ports:
+ - 8070:8080
+ volumes:
+ - ../apps-metadata.json:/opt/bitnami/apache/htdocs/apps-metadata.json:Z
+ - ../../k8s/resources/common/config/apps-metadata-base-voltages.json:/opt/bitnami/apache/htdocs/apps-metadata-base-voltages.json:Z
+ - ../version.json:/opt/bitnami/apache/htdocs/version.json:Z
+ - ../gridapps-metadata-httpd.conf:/opt/bitnami/apache/conf/bitnami/bitnami.conf:Z
+ memswap_limit: 128m
+ deploy:
+ resources:
+ limits:
+ memory: 128m
+ restart: unless-stopped
diff --git a/docker-compose/multiwar/docker-compose.yml b/docker-compose/multiwar/docker-compose.yml
new file mode 100644
index 00000000..ceb04af5
--- /dev/null
+++ b/docker-compose/multiwar/docker-compose.yml
@@ -0,0 +1,3 @@
+include:
+ - ../technical/docker-compose.technical.yml
+ - docker-compose.multiwar.yml
diff --git a/docker-compose/multiwar/itools-config.yml b/docker-compose/multiwar/itools-config.yml
new file mode 100644
index 00000000..ff2716b4
--- /dev/null
+++ b/docker-compose/multiwar/itools-config.yml
@@ -0,0 +1,66 @@
+# Merged itools config for all WAR-deployed servers in Tomcat.
+# Mounted at /root/.itools/config.yml (Tomcat container runs as root).
+#
+# In k8s, each server has its own config.yml. Here we merge them all because
+# all 33 WARs share a single JVM. Key settings:
+# - writeReferenceTerminals: false — workaround for ReferenceTerminals extension
+# not implemented in network-store (causes InvalidClassException without it)
+# - network.default-impl-name: NetworkStore — forces iidm to use network-store
+# instead of in-memory impl (which is on classpath due to cvg-extension)
+# - computation-local.available-core: 4 — limits concurrent computation processes
+#
+# Source files merged:
+# k8s/resources/common/config/loadflow-server-config.yml
+# k8s/resources/common/config/network-conversion-server-config.yml
+# k8s/resources/study/config/network-modification-server-config.yml
+# k8s/resources/study/config/dynawo-itools-config.yml
+# k8s/resources/study/config/voltage-init-server-config.yml
+# k8s/resources/study/config/sensitivity-analysis-server-config.yml
+# k8s/resources/study/config/security-analysis-server-config.yml
+# k8s/resources/study/config/shortcircuit-server-config.yml
+# k8s/resources/monitor/config/monitor-lf-worker-server-config.yml
+# k8s/resources/monitor/config/monitor-sa-worker-server-config.yml
+
+hades2:
+ homeDir: /hades2
+ debug: false
+
+dynaflow:
+ homeDir: /dynaflow-launcher
+ debug: false
+
+dynawo:
+ homeDir: /dynaflow-launcher
+ debug: false
+
+dynawo-algorithms:
+ homeDir: /dynaflow-launcher
+ debug: false
+
+network:
+ default-impl-name: NetworkStore
+
+dynaflow-default-parameters:
+ mergeLoads: false
+
+computation-local:
+ available-core: 4
+
+load-flow-default-parameters:
+ voltageInitMode: DC_VALUES
+ phaseShifterRegulationOn: true
+ twtSplitShuntAdmittance: true
+ dcUseTransformerRatio: false
+ countriesToBalance:
+ - FR
+
+open-loadflow-default-parameters:
+ writeReferenceTerminals: false
+ slackDistributionFailureBehavior: FAIL
+
+open-security-analysis-default-parameters:
+ threadCount: 2
+
+import-export-parameters-default-value:
+ iidm.import.cgmes.cgm-with-subnetworks: false
+ ucte.import.create-areas: false
diff --git a/docker-compose/multiwar/nonwars-to-tomcat-config/common-application.yml b/docker-compose/multiwar/nonwars-to-tomcat-config/common-application.yml
new file mode 100644
index 00000000..45cb98c0
--- /dev/null
+++ b/docker-compose/multiwar/nonwars-to-tomcat-config/common-application.yml
@@ -0,0 +1,56 @@
+# Common application config for non-WAR services in multiwar deployment.
+# Points service base-uris to the single Tomcat instance.
+
+powsybl:
+ services:
+ case-server:
+ base-uri: http://tomcat:8080/case-server/
+ network-store-server:
+ base-uri: http://tomcat:8080/network-store-server/
+ network-conversion-server:
+ base-uri: http://tomcat:8080/network-conversion-server/
+ single-line-diagram-server:
+ base-uri: http://tomcat:8080/single-line-diagram-server/
+
+gridsuite:
+ services:
+ directory-server:
+ base-uri: http://tomcat:8080/directory-server/
+ explore-server:
+ base-uri: http://tomcat:8080/explore-server/
+ study-server:
+ base-uri: http://tomcat:8080/study-server/
+ actions-server:
+ base-uri: http://tomcat:8080/actions-server/
+ filter-server:
+ base-uri: http://tomcat:8080/filter-server/
+ user-admin-server:
+ base-uri: http://tomcat:8080/user-admin-server/
+ config-server:
+ base-uri: http://config-server:80
+ report-server:
+ base-uri: http://tomcat:8080/report-server/
+ network-modification-server:
+ base-uri: http://tomcat:8080/network-modification-server/
+ geo-data-server:
+ base-uri: http://tomcat:8080/geo-data-server/
+ network-map-server:
+ base-uri: http://tomcat:8080/network-map-server/
+ loadflow-server:
+ base-uri: http://tomcat:8080/loadflow-server/
+ security-analysis-server:
+ base-uri: http://tomcat:8080/security-analysis-server/
+ sensitivity-analysis-server:
+ base-uri: http://tomcat:8080/sensitivity-analysis-server/
+ shortcircuit-server:
+ base-uri: http://tomcat:8080/shortcircuit-server/
+ voltage-init-server:
+ base-uri: http://tomcat:8080/voltage-init-server/
+ dynamic-simulation-server:
+ base-uri: http://tomcat:8080/dynamic-simulation-server/
+ timeseries-server:
+ base-uri: http://tomcat:8080/timeseries-server/
+ study-config-server:
+ base-uri: http://tomcat:8080/study-config-server/
+ user-identity-server:
+ base-uri: http://tomcat:8080/user-identity-server/
diff --git a/docker-compose/multiwar/server.xml b/docker-compose/multiwar/server.xml
new file mode 100644
index 00000000..957a5a26
--- /dev/null
+++ b/docker-compose/multiwar/server.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docker-compose/multiwar/wars.sh b/docker-compose/multiwar/wars.sh
new file mode 100755
index 00000000..3a5f26cf
--- /dev/null
+++ b/docker-compose/multiwar/wars.sh
@@ -0,0 +1,516 @@
+#!/usr/bin/env bash
+#
+# war-deploy.sh — Build GridSuite microservices as WAR files for docker-compose.
+# Zero modifications to original server source files.
+#
+# Word of Caution, the whole script with comments (except this sentence)
+# has been generated by opus4.6 without much look at the details.
+#
+# Usage:
+# ./war-deploy.sh [OPTIONS]
+#
+# Options:
+# --only SRV Only process this server (repeatable)
+# --help Show this help
+#
+# Strategy ("wrapper POM" approach):
+# Instead of modifying original Spring Boot server sources to produce WAR files,
+# we generate lightweight wrapper Maven projects that:
+# 1. Depend on the original server's classes JAR (available because powsybl-parent-ws
+# configures spring-boot-maven-plugin with exec, so
+# `mvn install` installs both the executable fat JAR and a plain classes JAR)
+# 2. Provide a SpringBootServletInitializer subclass (required for WAR deployment)
+# 3. Set spring.config.additional-location for per-WAR config overrides
+# 4. Package as WAR with = desired context path
+#
+# This keeps all server submodules UNMODIFIED while producing deployable WARs.
+#
+# Future improvements:
+# - Move shared JARs (Spring, Hibernate, powsybl-commons, etc.) into Tomcat's
+# lib/ directory so they are loaded once by the common classloader. This would
+# significantly reduce Metaspace usage (each WAR currently loads its own copy
+# of every dependency class).
+# - Reduce duplication in config override generation: many servers share the same
+# service references (e.g. network-store, report-server). A template or merge
+# system could replace per-server YAML generation.
+#
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+AGGREGATOR_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
+SERVERS_DIR="$AGGREGATOR_ROOT/backend/servers"
+WRAPPERS_DIR="$SCRIPT_DIR/gen/war-wrappers"
+WEBAPPS_DIR="$SCRIPT_DIR/gen/wars"
+CONFIG_DIR="$SCRIPT_DIR/gen/externalized-war-configs"
+
+# Base URL used inside docker network (container-to-container)
+TOMCAT_BASE_URL="http://tomcat:8080"
+
+# Defaults
+ONLY_SERVERS=()
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --only) ONLY_SERVERS+=("$2"); shift 2 ;;
+ --help) sed -n '2,/^$/p' "$0" | sed 's/^# \?//'; exit 0 ;;
+ *) echo "Unknown option: $1"; exit 1 ;;
+ esac
+done
+
+log() { echo -e "\033[1;34m[WAR]\033[0m $*"; }
+err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; }
+
+# ===========================================================================
+# SERVER MANIFEST
+# Format: context_name|server_folder|pom_subpath|app_class_fqn
+#
+# context_name: WAR filename / Tomcat context path
+# server_folder: folder under backend/servers/
+# pom_subpath: submodule path within server_folder (empty = root)
+# app_class_fqn: fully qualified Application class name
+# Lines starting with # are disabled. Uncomment to include in the build.
+# ===========================================================================
+MANIFEST=(
+"actions-server|actions-server||org.gridsuite.actions.server.ActionsApplication"
+#"balances-adjustment-server|balances-adjustment-server||org.gridsuite.balances.adjustment.server.BalancesAdjustmentApplication"
+#"case-import-server|case-import-server||org.gridsuite.caseimport.server.CaseImportApplication"
+#"case-validation-server|case-validation-server||org.gridsuite.casevalidation.server.CaseValidationApplication"
+#"cgmes-boundary-server|cgmes-boundary-server||org.gridsuite.cgmes.boundary.server.CgmesBoundaryApplication"
+#"cgmes-gl-server|cgmes-gl-server||org.gridsuite.cgmes.gl.server.CgmesGlApplication"
+"directory-server|directory-server||org.gridsuite.directory.server.DirectoryApplication"
+#"dynamic-mapping-server|dynamic-mapping-server||org.gridsuite.mapping.server.MappingApplication"
+#"dynamic-margin-calculation-server|dynamic-margin-calculation-server||org.gridsuite.dynamicmargincalculation.server.DynamicMarginCalculationApplication"
+#"dynamic-security-analysis-server|dynamic-security-analysis-server||org.gridsuite.dynamicsecurityanalysis.server.DynamicSecurityAnalysisApplication"
+#"dynamic-simulation-server|dynamic-simulation-server||org.gridsuite.ds.server.DynamicSimulationApplication"
+"explore-server|explore-server||org.gridsuite.explore.server.ExploreApplication"
+"filter-server|filter-server||org.gridsuite.filter.server.FilterApplication"
+"geo-data-server|geo-data-server||org.gridsuite.geodata.server.GeoDataApplication"
+"loadflow-server|loadflow-server||org.gridsuite.loadflow.server.LoadFlowApplication"
+#"merge-orchestrator-server|merge-orchestrator-server||org.gridsuite.merge.orchestrator.server.MergeOrchestratorApplication"
+"network-map-server|network-map-server||org.gridsuite.network.map.NetworkMapApplication"
+"network-modification-server|network-modification-server||org.gridsuite.modification.server.NetworkModificationApplication"
+"network-store-server|powsybl-network-store-server|network-store-server|com.powsybl.network.store.server.NetworkStoreApplication"
+#"odre-server|odre-server||org.gridsuite.odre.server.OdreApplication"
+"case-server|powsybl-case-server||com.powsybl.caseserver.CaseApplication"
+"network-conversion-server|powsybl-network-conversion-server||com.powsybl.network.conversion.server.NetworkConversionApplication"
+"single-line-diagram-server|powsybl-single-line-diagram-server||com.powsybl.sld.server.SingleLineDiagramApplication"
+"report-server|report-server||org.gridsuite.report.server.ReportApplication"
+"security-analysis-server|security-analysis-server||org.gridsuite.securityanalysis.server.SecurityAnalysisApplication"
+"sensitivity-analysis-server|sensitivity-analysis-server||org.gridsuite.sensitivityanalysis.server.SensitivityAnalysisApplication"
+"shortcircuit-server|shortcircuit-server||org.gridsuite.shortcircuit.server.ShortCircuitApplication"
+"study-config-server|study-config-server||org.gridsuite.studyconfig.server.StudyConfigApplication"
+"study-server|study-server||org.gridsuite.study.server.StudyApplication"
+#"timeseries-server|timeseries-server||org.gridsuite.timeseries.server.TimeSeriesApplication"
+"user-admin-server|user-admin-server||org.gridsuite.useradmin.server.UserAdminApplication"
+"user-identity-server|user-identity-oidc-replication-server||org.gridsuite.useridentity.oidcreplication.server.UserIdentityOidcReplicationApplication"
+"voltage-init-server|voltage-init-server||org.gridsuite.voltageinit.server.VoltageInitApplication"
+)
+
+# Filter out commented entries (lines starting with #)
+_filtered=()
+for _e in "${MANIFEST[@]}"; do
+ [[ "$_e" =~ ^# ]] || _filtered+=("$_e")
+done
+MANIFEST=("${_filtered[@]}")
+unset _filtered _e
+
+should_process() {
+ local ctx="$1"
+ if [[ ${#ONLY_SERVERS[@]} -gt 0 ]]; then
+ for s in "${ONLY_SERVERS[@]}"; do
+ [[ "$s" == "$ctx" ]] && return 0
+ done
+ return 1
+ fi
+ return 0
+}
+
+# Resolve a service key to its base-uri.
+# Trailing slash is required: clients use UriComponentsBuilder.fromUriString(baseUri)
+# which concatenates paths like "v1/networks/..." directly after the base URI.
+# Without trailing slash: "http://tomcat:8080/network-store-serverv1/..." (broken)
+# With trailing slash: "http://tomcat:8080/network-store-server/v1/..." (correct)
+resolve_base_uri() {
+ local svc_key="$1"
+ echo "${TOMCAT_BASE_URL}/${svc_key}/"
+}
+
+# Generate per-WAR config override with base-uri values pointing to Tomcat.
+# Since we disable the "local" profile, we only need base-uri overrides in application.yml.
+# Reads the original application-local file to discover which services are referenced,
+# then generates a clean application.yml with only the base-uri section.
+generate_local_config_override() {
+ local ctx="$1"
+ local server_folder="$2"
+ local pom_subpath="$3"
+ local wrapper_dir="$4"
+
+ local module_dir="$SERVERS_DIR/$server_folder"
+ [[ -n "$pom_subpath" ]] && module_dir="$module_dir/$pom_subpath"
+
+ # Find the original application-local file to discover service references
+ local orig_file=""
+ for ext in yaml yml; do
+ if [[ -f "$module_dir/src/main/resources/application-local.$ext" ]]; then
+ orig_file="$module_dir/src/main/resources/application-local.$ext"
+ break
+ fi
+ done
+ [[ -z "$orig_file" ]] && return 0
+
+ # Detect which format gridsuite: services: uses (list-style vs map-style)
+ local uses_list_style=false
+ if grep -qE '^\s+-\s*(name:|$)' "$orig_file" 2>/dev/null; then
+ uses_list_style=true
+ fi
+
+ # Extract service keys referenced in the file
+ local powsybl_services=() gridsuite_services=()
+ local current_section=""
+
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ if [[ "$line" =~ ^powsybl: ]]; then
+ current_section="powsybl"
+ elif [[ "$line" =~ ^gridsuite: ]]; then
+ current_section="gridsuite"
+ elif [[ ! "$line" =~ ^[[:space:]] && ! "$line" =~ ^# && -n "$line" ]]; then
+ current_section="other"
+ fi
+
+ # Map-style key: " server-name:"
+ if [[ "$line" =~ ^[[:space:]]{4}([a-z][a-z0-9-]+):$ ]]; then
+ local key="${BASH_REMATCH[1]}"
+ if [[ "$current_section" == "powsybl" ]]; then
+ powsybl_services+=("$key")
+ elif [[ "$current_section" == "gridsuite" ]]; then
+ gridsuite_services+=("$key")
+ fi
+ fi
+ # List-style: "- name: xxx" or " name: xxx"
+ if [[ "$line" =~ ^[[:space:]]+-[[:space:]]*name:[[:space:]]*(.+) ]]; then
+ local key="${BASH_REMATCH[1]}"
+ key="${key%%[[:space:]]}"
+ [[ "$current_section" == "gridsuite" ]] && gridsuite_services+=("$key")
+ elif [[ "$line" =~ ^[[:space:]]+name:[[:space:]]*(.+) && "$current_section" == "gridsuite" ]]; then
+ local key="${BASH_REMATCH[1]}"
+ key="${key%%[[:space:]]}"
+ gridsuite_services+=("$key")
+ fi
+ done < "$orig_file"
+
+ # If no services referenced, no config needed
+ if [[ ${#powsybl_services[@]} -eq 0 && ${#gridsuite_services[@]} -eq 0 ]]; then
+ return 0
+ fi
+
+ local out_file="$CONFIG_DIR/$ctx/application.yml"
+ mkdir -p "$CONFIG_DIR/$ctx"
+
+ local output=""
+
+ # Generate powsybl: services: section (always map-style)
+ if [[ ${#powsybl_services[@]} -gt 0 ]]; then
+ output+="powsybl:"$'\n'
+ output+=" services:"$'\n'
+ for svc in "${powsybl_services[@]}"; do
+ local uri
+ uri=$(resolve_base_uri "$svc")
+ output+=" ${svc}:"$'\n'
+ output+=" base-uri: ${uri}"$'\n'
+ done
+ fi
+
+ # Generate gridsuite: services: section
+ if [[ ${#gridsuite_services[@]} -gt 0 ]]; then
+ [[ -n "$output" ]] && output+=$'\n'
+ output+="gridsuite:"$'\n'
+ output+=" services:"$'\n'
+ if $uses_list_style; then
+ for svc in "${gridsuite_services[@]}"; do
+ local uri
+ uri=$(resolve_base_uri "$svc")
+ output+=" -"$'\n'
+ output+=" name: ${svc}"$'\n'
+ output+=" base-uri: ${uri}"$'\n'
+ done
+ else
+ for svc in "${gridsuite_services[@]}"; do
+ local uri
+ uri=$(resolve_base_uri "$svc")
+ output+=" ${svc}:"$'\n'
+ output+=" base-uri: ${uri}"$'\n'
+ done
+ fi
+ fi
+
+ echo -n "$output" > "$out_file"
+}
+
+# ===========================================================================
+# PHASE 1: GENERATE WRAPPER PROJECTS
+# ===========================================================================
+generate_wrappers() {
+ log "=== Generating wrapper projects ==="
+ rm -rf "$WRAPPERS_DIR"
+ mkdir -p "$WRAPPERS_DIR"
+
+ # Detect gridsuite-dependencies BOM version
+ local first_entry="${MANIFEST[0]}"
+ IFS='|' read -r _ctx first_folder first_sub _ <<< "$first_entry"
+ local first_dir="$SERVERS_DIR/$first_folder"
+ [[ -n "$first_sub" ]] && first_dir="$first_dir/$first_sub"
+ local gridsuite_deps_version
+ gridsuite_deps_version=$(grep -m1 "gridsuite-dependencies.version" "$first_dir/pom.xml" 2>/dev/null | sed 's/.*>\(.*\)<.*/\1/' || true)
+ [[ -z "$gridsuite_deps_version" ]] && gridsuite_deps_version="50.0.0"
+ log " gridsuite-dependencies version: $gridsuite_deps_version"
+
+ local modules=""
+
+ for entry in "${MANIFEST[@]}"; do
+ IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry"
+ should_process "$ctx" || continue
+
+ local module_dir="$SERVERS_DIR/$server_folder"
+ [[ -n "$pom_subpath" ]] && module_dir="$module_dir/$pom_subpath"
+
+ # Get groupId, artifactId, version from the server's pom
+ local group_id artifact_id version parent_group parent_version
+
+ parent_group=$(sed -n '//,/<\/parent>/p' "$module_dir/pom.xml" | grep '' | sed 's/.*//;s/<.*//' | tr -d ' ')
+ parent_version=$(sed -n '//,/<\/parent>/p' "$module_dir/pom.xml" | grep '' | sed 's/.*//;s/<.*//' | tr -d ' ')
+ artifact_id=$(sed -n '/<\/parent>/,$ p' "$module_dir/pom.xml" | grep -m1 '' | sed 's/.*//;s/<.*//' | tr -d ' ')
+ version=$(sed -n '/<\/parent>/,/<\(build\|dependencies\|dependencyManagement\|properties\)>/ p' "$module_dir/pom.xml" | grep -m1 '' | sed 's/.*//;s/<.*//' | tr -d ' ' || true)
+ group_id=$(sed -n '/<\/parent>/,/<\(build\|dependencies\|dependencyManagement\|properties\)>/ p' "$module_dir/pom.xml" | grep -m1 '' | sed 's/.*//;s/<.*//' | tr -d ' ' || true)
+
+ # Fall back to parent values if not specified at project level
+ [[ -z "$group_id" ]] && group_id="$parent_group"
+ [[ -z "$version" ]] && version="$parent_version"
+
+ local wrapper_dir="$WRAPPERS_DIR/$ctx"
+ mkdir -p "$wrapper_dir/src/main/java/org/gridsuite/war"
+
+ # Generate WarInitializer.java
+ # Sets spring.config.additional-location to load per-WAR base-uri overrides from
+ # an external directory (/config//application.yml). "optional:" prefix means
+ # servers with no service references (e.g. report-server) won't fail if the
+ # directory doesn't exist. This is the Spring Boot guaranteed way to override
+ # config — additional-location properties always win over classpath configs.
+ local simple_class="${app_class##*.}"
+ local initializer_class="${simple_class/Application/WarInitializer}"
+ cat > "$wrapper_dir/src/main/java/org/gridsuite/war/${initializer_class}.java" << JAVA
+package org.gridsuite.war;
+
+import ${app_class};
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+public class ${initializer_class} extends SpringBootServletInitializer {
+ @Override
+ protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+ return application.sources(${simple_class}.class)
+ .properties("spring.config.additional-location=optional:file:/config/${ctx}/");
+ }
+}
+JAVA
+
+ # Generate wrapper pom.xml
+ local extra_deps=""
+ # antlr4 version override: these servers depend on powsybl-open-loadflow which
+ # pulls in graphviz-builder → antlr4:4.5.1. But Spring Data JPA's HqlLexer was
+ # compiled with antlr4 4.13.0 (ATN version 4). The old antlr4-4.5.1.jar contains
+ # ATNDeserializer that only understands ATN version 3, causing:
+ # InvalidClassException: org.antlr.v4.runtime.atn.ATN; Could not deserialize ATN with version 4
+ # Fix: force antlr4 (the full tool jar, which includes the runtime) to 4.13.0.
+ case "$ctx" in
+ loadflow-server|security-analysis-server|sensitivity-analysis-server|network-modification-server)
+ extra_deps='
+
+ org.antlr
+ antlr4
+ 4.13.0
+ '
+ ;;
+ esac
+
+ cat > "$wrapper_dir/pom.xml" << POM
+
+
+ 4.0.0
+
+
+ org.gridsuite.war
+ war-wrappers
+ 1.0.0
+
+
+ ${ctx}-war
+ war
+
+
+
+ ${group_id}
+ ${artifact_id}
+ ${version}
+ ${extra_deps}
+
+
+
+ ${ctx}
+
+
+POM
+
+ modules="$modules $ctx\n"
+ log " ${ctx}-war -> $module_dir"
+ done
+
+ # Generate reactor parent pom
+ cat > "$WRAPPERS_DIR/pom.xml" << POM
+
+
+ 4.0.0
+
+ org.gridsuite.war
+ war-wrappers
+ 1.0.0
+ pom
+
+
+ 21
+ 21
+
+
+
+
+
+
+ org.gridsuite
+ gridsuite-dependencies
+ ${gridsuite_deps_version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+ provided
+
+
+
+
+$(echo -e "$modules")
+
+POM
+
+ # Generate per-WAR config overrides directly into gen/externalized-war-configs/
+ rm -rf "$CONFIG_DIR"
+ log "=== Generating externalized WAR additional-configs ==="
+ for entry in "${MANIFEST[@]}"; do
+ IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry"
+ should_process "$ctx" || continue
+ local wrapper_dir="$WRAPPERS_DIR/$ctx"
+ generate_local_config_override "$ctx" "$server_folder" "$pom_subpath" "$wrapper_dir"
+ done
+ (cd "$SCRIPT_DIR" && find gen/externalized-war-configs -type f 2>/dev/null) || true
+
+
+}
+
+# ===========================================================================
+# PHASE 2: BUILD ALL (SINGLE REACTOR)
+# Builds ALL server source modules + wrapper projects in one Maven invocation.
+# Server source modules are added to the reactor so that their classes JARs
+# are available in the local repo for wrapper dependency resolution.
+# -T2.0C = 2 threads per CPU core for parallel module builds.
+# ===========================================================================
+build_wars() {
+ # Prefer mvnd (Maven Daemon) for faster builds; fall back to mvn -q
+ local mvn_cmd
+ if command -v mvnd &>/dev/null; then
+ mvn_cmd="mvnd"
+ log "=== Building all with mvnd ==="
+ else
+ mvn_cmd="mvn -T2.0C -q"
+ log "=== Building all with mvn (mvnd not found, using -T2.0C -q) ==="
+ fi
+
+ # Add server source modules to reactor POM for a single build
+ local server_modules=""
+ for entry in "${MANIFEST[@]}"; do
+ IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry"
+ should_process "$ctx" || continue
+
+ local rel_path="../../../../../../backend/servers/$server_folder"
+ [[ -n "$pom_subpath" ]] && rel_path="$rel_path/$pom_subpath"
+ server_modules+=" ${rel_path}\n"
+ done
+
+ # Inject server modules into reactor POM (before wrapper modules)
+ local wrapper_pom="$WRAPPERS_DIR/pom.xml"
+ sed -i "s| | \n \n${server_modules} |" "$wrapper_pom"
+
+ log "Running: $mvn_cmd package -DskipTests -f $WRAPPERS_DIR/pom.xml"
+ (cd "$WRAPPERS_DIR" && $mvn_cmd package -DskipTests) || {
+ err "Reactor build failed"
+ return 1
+ }
+
+ # Verify
+ for entry in "${MANIFEST[@]}"; do
+ IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry"
+ should_process "$ctx" || continue
+ if [[ ! -f "$WRAPPERS_DIR/$ctx/target/${ctx}.war" ]]; then
+ err "Missing: $WRAPPERS_DIR/$ctx/target/${ctx}.war"
+ fi
+ done
+}
+
+# ===========================================================================
+# PHASE 3: COPY WARS TO DOCKER-COMPOSE MOUNT DIR
+# ===========================================================================
+deploy_to_compose_dirs() {
+ log "=== Copying WARs to gen/wars/ ==="
+ rm -f "$WEBAPPS_DIR"/*.war 2>/dev/null || true
+ mkdir -p "$WEBAPPS_DIR"
+
+ for entry in "${MANIFEST[@]}"; do
+ IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry"
+ should_process "$ctx" || continue
+
+ local war="$WRAPPERS_DIR/$ctx/target/${ctx}.war"
+ if [[ -f "$war" ]]; then
+ cp "$war" "$WEBAPPS_DIR/"
+ else
+ err "Missing: $war"
+ fi
+ done
+
+ (cd "$SCRIPT_DIR" && find gen/wars -maxdepth 1 -name '*.war' -type f 2>/dev/null) || true
+ echo ""
+ echo "Folders gen/wars/ and gen/externalized-war-configs/ ready for \$ docker compose up"
+}
+
+# ===========================================================================
+# MAIN
+# ===========================================================================
+generate_wrappers
+build_wars
+deploy_to_compose_dirs