From 545f6b8af838502c5baab9f4eb7eb84cab750ceb Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Thu, 2 Jul 2026 10:58:59 +0200 Subject: [PATCH 1/8] UNOMI-875: Upgrade Karaf to 4.4.11 Karaf 4.4.11 no longer brings slf4j-api transitively; add an explicit provided dependency in lifecycle-watcher and align setenv.sh. --- lifecycle-watcher/pom.xml | 5 +++++ pom.xml | 2 +- setenv.sh | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lifecycle-watcher/pom.xml b/lifecycle-watcher/pom.xml index 8b608383a..92b28c314 100644 --- a/lifecycle-watcher/pom.xml +++ b/lifecycle-watcher/pom.xml @@ -61,6 +61,11 @@ commons-lang3 provided + + org.slf4j + slf4j-api + provided + diff --git a/pom.xml b/pom.xml index dc7fbdf8c..86d0b241b 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ ${java.version} ${java.version} - 4.4.8 + 4.4.11 9.1.4 9.2.1 3.0.0 diff --git a/setenv.sh b/setenv.sh index 115a5f848..ef5de389e 100755 --- a/setenv.sh +++ b/setenv.sh @@ -27,6 +27,6 @@ if [ -z "$UNOMI_VERSION" ]; then fi rm -f "$_mvn_err" echo "Detected project version=$UNOMI_VERSION" -export KARAF_VERSION=4.4.8 +export KARAF_VERSION=4.4.11 # Uncomment the following line if you need Apache Unomi to start automatically at the first start # export KARAF_OPTS="-Dunomi.autoStart=true" From 324714f5401f4b447771c3af10056ff81e0d9b5b Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Thu, 2 Jul 2026 10:59:16 +0200 Subject: [PATCH 2/8] UNOMI-875: Upgrade Elasticsearch 9.4.2 and OpenSearch 3.9.0 Align Docker compose images and IT fixtures with new server versions. Adapt ES bulk routing API for 9.4 and embed httpclient5 compression dependencies for Karaf OSGi resolution. --- docker/README.md | 8 ++--- .../main/docker/docker-compose-build-es.yml | 2 +- .../main/docker/docker-compose-build-os.yml | 6 ++-- .../main/docker/docker-compose-cluster.yml | 2 +- docker/src/main/docker/docker-compose-es.yml | 2 +- docker/src/main/docker/docker-compose-os.yml | 6 ++-- itests/docker-compose-snapshot-analysis.yml | 2 +- persistence-elasticsearch/core/pom.xml | 33 ++++++++++++++++++- .../ElasticSearchPersistenceServiceImpl.java | 14 ++++++-- pom.xml | 8 ++--- 10 files changed, 62 insertions(+), 21 deletions(-) diff --git a/docker/README.md b/docker/README.md index ae1e4f93b..5c862fc5d 100644 --- a/docker/README.md +++ b/docker/README.md @@ -46,25 +46,25 @@ If you want to run it without docker-compose you should then make sure you setup For ElasticSearch: ```bash -docker pull docker.elastic.co/elasticsearch/elasticsearch:9.2.1 +docker pull docker.elastic.co/elasticsearch/elasticsearch:9.4.2 docker network create unomi docker run -d --name elasticsearch --net unomi -p 9200:9200 -p 9300:9300 \ -e "discovery.type=single-node" \ -e "xpack.security.enabled=false" \ -e cluster.name=contextElasticSearch \ - docker.elastic.co/elasticsearch/elasticsearch:9.2.1 + docker.elastic.co/elasticsearch/elasticsearch:9.4.2 ``` For OpenSearch: ```bash -docker pull opensearchproject/opensearch:3.0.0 +docker pull opensearchproject/opensearch:3.9.0 docker network create unomi export OPENSEARCH_ADMIN_PASSWORD=enter_your_custom_admin_password_here docker run -d --name opensearch --net unomi -p 9200:9200 -p 9300:9300 \ -e "discovery.type=single-node" \ -e OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} \ - opensearchproject/opensearch:3.0.0 + opensearchproject/opensearch:3.9.0 ``` For Unomi (with ElasticSearch): diff --git a/docker/src/main/docker/docker-compose-build-es.yml b/docker/src/main/docker/docker-compose-build-es.yml index 0aa221209..5cbdc6845 100644 --- a/docker/src/main/docker/docker-compose-build-es.yml +++ b/docker/src/main/docker/docker-compose-build-es.yml @@ -17,7 +17,7 @@ version: '2.4' services: elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:9.2.1 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 volumes: - unomi-3-elasticsearch-data:/usr/share/elasticsearch/data environment: diff --git a/docker/src/main/docker/docker-compose-build-os.yml b/docker/src/main/docker/docker-compose-build-os.yml index fb2d27d27..c46d7c933 100644 --- a/docker/src/main/docker/docker-compose-build-os.yml +++ b/docker/src/main/docker/docker-compose-build-os.yml @@ -23,7 +23,7 @@ networks: services: opensearch-node1: - image: opensearchproject/opensearch:3.0.0 + image: opensearchproject/opensearch:3.9.0 container_name: opensearch-node1 environment: - cluster.name=opensearch-cluster @@ -51,7 +51,7 @@ services: - opensearch-node1 opensearch-node2: - image: opensearchproject/opensearch:3.0.0 + image: opensearchproject/opensearch:3.9.0 container_name: opensearch-node2 environment: - cluster.name=opensearch-cluster @@ -76,7 +76,7 @@ services: - opensearch-node2 opensearch-dashboards: - image: opensearchproject/opensearch-dashboards:3.0.0 + image: opensearchproject/opensearch-dashboards:3.9.0 container_name: opensearch-dashboards ports: - 5601:5601 diff --git a/docker/src/main/docker/docker-compose-cluster.yml b/docker/src/main/docker/docker-compose-cluster.yml index a088fc082..49ab47538 100644 --- a/docker/src/main/docker/docker-compose-cluster.yml +++ b/docker/src/main/docker/docker-compose-cluster.yml @@ -17,7 +17,7 @@ version: '2.4' services: elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:9.2.1 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 volumes: - unomi-3-elasticsearch-data:/usr/share/elasticsearch/data environment: diff --git a/docker/src/main/docker/docker-compose-es.yml b/docker/src/main/docker/docker-compose-es.yml index db97b6d64..0e518036a 100644 --- a/docker/src/main/docker/docker-compose-es.yml +++ b/docker/src/main/docker/docker-compose-es.yml @@ -23,7 +23,7 @@ networks: services: elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:9.2.1 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 container_name: elasticsearch volumes: - unomi-3-elasticsearch-data:/usr/share/elasticsearch/data diff --git a/docker/src/main/docker/docker-compose-os.yml b/docker/src/main/docker/docker-compose-os.yml index 03abe5263..adc8b3a26 100644 --- a/docker/src/main/docker/docker-compose-os.yml +++ b/docker/src/main/docker/docker-compose-os.yml @@ -23,7 +23,7 @@ networks: services: opensearch-node1: - image: opensearchproject/opensearch:3.0.0 + image: opensearchproject/opensearch:3.9.0 container_name: opensearch-node1 environment: - cluster.name=opensearch-cluster @@ -51,7 +51,7 @@ services: - opensearch-node1 opensearch-node2: - image: opensearchproject/opensearch:3.0.0 + image: opensearchproject/opensearch:3.9.0 container_name: opensearch-node2 environment: - cluster.name=opensearch-cluster @@ -103,7 +103,7 @@ services: - unomi opensearch-dashboards: - image: opensearchproject/opensearch-dashboards:3.0.0 + image: opensearchproject/opensearch-dashboards:3.9.0 container_name: opensearch-dashboards ports: - 5601:5601 diff --git a/itests/docker-compose-snapshot-analysis.yml b/itests/docker-compose-snapshot-analysis.yml index 7e9f955e8..742b300e0 100644 --- a/itests/docker-compose-snapshot-analysis.yml +++ b/itests/docker-compose-snapshot-analysis.yml @@ -16,7 +16,7 @@ ################################################################################ services: elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:9.1.3 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 container_name: es-snapshot-analysis environment: - discovery.type=single-node diff --git a/persistence-elasticsearch/core/pom.xml b/persistence-elasticsearch/core/pom.xml index ea9eb8fbf..f60982555 100644 --- a/persistence-elasticsearch/core/pom.xml +++ b/persistence-elasticsearch/core/pom.xml @@ -104,6 +104,11 @@ jackson-core provided + + com.fasterxml.jackson.core + jackson-annotations + provided + org.apache.commons commons-lang3 @@ -156,6 +161,19 @@ true + + + com.aayushatharva.brotli4j + brotli4j + 1.20.0 + compile + + + com.github.luben + zstd-jni + 1.5.7-4 + compile + junit @@ -264,6 +282,16 @@ org.conscrypt;resolution:=optional, org.glassfish.hk2.osgiresourcelocator;resolution:=optional, org.brotli.dec;resolution:=optional, + com.aayushatharva.brotli4j.decoder;resolution:=optional, + com.aayushatharva.brotli4j.encoder;resolution:=optional, + com.github.luben.zstd;resolution:=optional, + io.netty.buffer;resolution:=optional, + io.netty.channel;resolution:=optional, + io.netty.util;resolution:=optional, + javax.xml.datatype;resolution:=optional, + javax.xml.namespace;resolution:=optional, + javax.xml.transform.dom;resolution:=optional, + org.newsclub.net.unix;resolution:=optional, io.opentelemetry.opentelemetry-api;resolution:=optional, io.opentelemetry.sdk.autoconfigure;resolution:=optional, io.opentelemetry.opentelemetry-context;resolution:=optional, @@ -289,7 +317,10 @@ co.elastic.clients.util;version="${elasticsearch.version}", co.elastic.clients.elasticsearch._types;version="${elasticsearch.version}" - *;scope=compile|runtime + + !groupId=org.apache.logging.log4j;artifactId=*, + *;scope=compile|runtime + true osgi.service;objectClass:List<String>="org.apache.unomi.persistence.spi.PersistenceService", diff --git a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java index efed6c03c..69dfb6474 100644 --- a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java +++ b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java @@ -1073,7 +1073,7 @@ protected Boolean execute(Object... args) throws Exception { BulkOperation bulkOp = BulkOperation.of(builder -> builder.update( u -> u.index(updateRequest.index()).id(updateRequest.id()).action(b -> b.doc(updateRequest.doc())) .ifSeqNo(updateRequest.ifSeqNo()).ifPrimaryTerm(updateRequest.ifPrimaryTerm()) - .routing(updateRequest.routing()))); + .routing(routingForBulk(updateRequest.routing())))); bulkIngester.add(bulkOp); } logMetadataItemOperation("updated", item); @@ -1126,7 +1126,7 @@ protected List execute(Object... args) throws Exception { BulkOperation bulkOp = BulkOperation.of(builder -> builder.update( u -> u.index(updateRequest.index()).id(updateRequest.id()).action(b -> b.doc(updateRequest.doc())) .ifSeqNo(updateRequest.ifSeqNo()).ifPrimaryTerm(updateRequest.ifPrimaryTerm()) - .routing(updateRequest.routing()))); + .routing(routingForBulk(updateRequest.routing())))); operations.add(bulkOp); }); @@ -3186,4 +3186,14 @@ public void unbindTransformationListener(ServiceReference routing) { + if (routing == null || routing.isEmpty()) { + return null; + } + if (routing.size() == 1) { + return routing.get(0); + } + return String.join(",", routing); + } + } diff --git a/pom.xml b/pom.xml index 86d0b241b..c1e9b539c 100644 --- a/pom.xml +++ b/pom.xml @@ -64,10 +64,10 @@ ${java.version} 4.4.11 - 9.1.4 - 9.2.1 - 3.0.0 - 3.0.0 + 9.4.2 + 9.4.2 + 3.9.0 + 3.9.0 5.2.1 2.28.14 1.1.0.Final From a5e6f35d2e6c5742b2ad82b16231f3693c639b9a Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Thu, 2 Jul 2026 10:59:28 +0200 Subject: [PATCH 3/8] UNOMI-875: Upgrade log4j to 2.24.0 Completes the platform dependency refresh deferred from PR2; ES core bundle excludes embedded log4j jars so Karaf supplies log4j 2.24. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c1e9b539c..e32312e1c 100644 --- a/pom.xml +++ b/pom.xml @@ -79,7 +79,7 @@ 2.5.2.Final 2.3 3.10 - 2.19.0 + 2.24.0 1.7.36 9.12.2 From e7ab67b7c588728c383b252f848396f9e58e31a9 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sun, 5 Jul 2026 09:00:26 +0200 Subject: [PATCH 4/8] docs(manual): add platform upgrade runbooks for maintainers Publish Karaf, ES/OS, Log4j, IT, and dependency-hygiene procedures in the developer manual so upgrade knowledge is versioned with the codebase (UNOMI-875). --- manual/src/main/asciidoc/index.adoc | 2 + .../asciidoc/upgrades/platform-upgrades.adoc | 40 +++ .../upgrades/upgrade-cxf-jackson-jetty.adoc | 181 ++++++++++ .../upgrades/upgrade-dependency-hygiene.adoc | 115 ++++++ .../upgrades/upgrade-elasticsearch.adoc | 183 ++++++++++ .../upgrades/upgrade-integration-tests.adoc | 283 +++++++++++++++ .../main/asciidoc/upgrades/upgrade-java.adoc | 106 ++++++ .../upgrades/upgrade-jira-reference.adoc | 257 +++++++++++++ .../main/asciidoc/upgrades/upgrade-karaf.adoc | 301 ++++++++++++++++ .../main/asciidoc/upgrades/upgrade-log4j.adoc | 113 ++++++ .../asciidoc/upgrades/upgrade-opensearch.adoc | 107 ++++++ .../asciidoc/upgrades/upgrade-overview.adoc | 338 ++++++++++++++++++ 12 files changed, 2026 insertions(+) create mode 100644 manual/src/main/asciidoc/upgrades/platform-upgrades.adoc create mode 100644 manual/src/main/asciidoc/upgrades/upgrade-cxf-jackson-jetty.adoc create mode 100644 manual/src/main/asciidoc/upgrades/upgrade-dependency-hygiene.adoc create mode 100644 manual/src/main/asciidoc/upgrades/upgrade-elasticsearch.adoc create mode 100644 manual/src/main/asciidoc/upgrades/upgrade-integration-tests.adoc create mode 100644 manual/src/main/asciidoc/upgrades/upgrade-java.adoc create mode 100644 manual/src/main/asciidoc/upgrades/upgrade-jira-reference.adoc create mode 100644 manual/src/main/asciidoc/upgrades/upgrade-karaf.adoc create mode 100644 manual/src/main/asciidoc/upgrades/upgrade-log4j.adoc create mode 100644 manual/src/main/asciidoc/upgrades/upgrade-opensearch.adoc create mode 100644 manual/src/main/asciidoc/upgrades/upgrade-overview.adoc diff --git a/manual/src/main/asciidoc/index.adoc b/manual/src/main/asciidoc/index.adoc index 09a9d8843..f5cfa4a44 100644 --- a/manual/src/main/asciidoc/index.adoc +++ b/manual/src/main/asciidoc/index.adoc @@ -147,6 +147,8 @@ include::connectors/salesforce-connector.adoc[] include::building-and-deploying.adoc[] +include::upgrades/platform-upgrades.adoc[] + include::shell-commands.adoc[] include::writing-plugins.adoc[] diff --git a/manual/src/main/asciidoc/upgrades/platform-upgrades.adoc b/manual/src/main/asciidoc/upgrades/platform-upgrades.adoc new file mode 100644 index 000000000..1430bc391 --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/platform-upgrades.adoc @@ -0,0 +1,40 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_platform_upgrades]] +=== Platform upgrades + +This section is for Apache Unomi *maintainers* upgrading runtime platform components: Apache Karaf, JDK, CXF/Jackson/Jetty, Elasticsearch and OpenSearch clients, Log4j, integration tests, and application dependency hygiene. + +Procedures are derived from git history (UNOMI-216, 491, 876, 899, 901, 828, 921, 875) and Unomi 3.1 platform upgrade work, cross-checked against Apache JIRA (see <<_upgrade_jira_reference,JIRA reference>>). + +include::upgrade-overview.adoc[] + +include::upgrade-jira-reference.adoc[] + +include::upgrade-karaf.adoc[] + +include::upgrade-java.adoc[] + +include::upgrade-cxf-jackson-jetty.adoc[] + +include::upgrade-elasticsearch.adoc[] + +include::upgrade-opensearch.adoc[] + +include::upgrade-log4j.adoc[] + +include::upgrade-integration-tests.adoc[] + +include::upgrade-dependency-hygiene.adoc[] diff --git a/manual/src/main/asciidoc/upgrades/upgrade-cxf-jackson-jetty.adoc b/manual/src/main/asciidoc/upgrades/upgrade-cxf-jackson-jetty.adoc new file mode 100644 index 000000000..85f1d6d6b --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/upgrade-cxf-jackson-jetty.adoc @@ -0,0 +1,181 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_upgrade_cxf_jackson_jetty]] +==== CXF, Jackson, and Jetty (Karaf cascade libraries) + +Karaf upgrades change the *runtime* versions of several frameworks even when you only bump `${karaf.version}`. Unomi pins some of these in root `pom.xml` and `kar/src/main/feature/feature.xml` so compile-time, feature verify, and OSGi resolution stay aligned. + +This guide covers libraries that *cascade from Karaf* but are *owned by Unomi* for REST, WAB, GraphQL, and ITs. + +===== Version pins (Unomi 3.1 baseline) + +[cols="1,1,2", options="header"] +|=== +| Property | Value | Primary consumers + +| `cxf.version` +| 3.6.8 +| `kar/.../feature.xml`, `rest/`, `bom/pom.xml`, OpenAPI/Swagger + +| `jackson.version` +| 2.18.3 +| `bom/pom.xml`, `kar/.../feature.xml` (pinned features + extra modules) + +| `jetty.version` +| 9.4.58.v20250814 +| `graphql/karaf-feature/pom.xml`, `itests/pom.xml` (compile/test only) +|=== + +IMPORTANT: `${jetty.version}` must match the Jetty bundles shipped by the Pax Web version bundled in your `${karaf.version}`. Inspect `pax-web-features-*-features.xml` on Maven Central — do not guess from release notes alone. + +===== How Karaf supplies vs how Unomi overrides + +[cols="1,2,2", options="header"] +|=== +| Library | Karaf source | Unomi override + +| CXF +| `standard` features reference `apache-cxf/RELEASE` +| Explicit repo + versioned feature in `feature.xml` (UNOMI-876, UNOMI-899) + +| Jackson +| `specs` features (`jackson`, `jackson-jaxrs`) — multiple versions on 4.4.11+ +| Pin `` — never unversioned (PR3 lesson) + +| Jetty +| Pax Web bundles (runtime) +| `${jetty.version}` for Maven compile/test deps only + +| Log4j (runtime) +| pax-logging-log4j2 bundle +| See <<_upgrade_log4j,Log4j upgrade>> — `${log4j.version}` is compile-time for fragments +|=== + + +===== Karaf supply vs Unomi override (diagram) + +[plantuml] +---- +@startuml +skinparam componentStyle uml2 + +package "Runtime (OSGi inside Karaf)" { + [Pax Web Jetty bundles] + [Karaf jackson features] + [CXF JAX-RS from apache-cxf repo] + [pax-logging-log4j2] +} + +package "Build / verify (Maven)" { + [(jetty.version) compile deps] + [(jackson.version) in feature.xml] + [(cxf.version) feature + bom] + [(log4j.version) fragment compile] +} + +note right of [Karaf jackson features] + 4.4.11+ may expose multiple + jackson feature versions — + always pin version attribute +end note + +note right of [CXF JAX-RS from apache-cxf repo] + CXF 4.x has no OSGi support — + stay on CXF 3.6.x (UNOMI-876) +end note + +[(jetty.version) compile deps] ..> [Pax Web Jetty bundles] : must match bundle version +[(jackson.version) in feature.xml] --> [Karaf jackson features] +[(cxf.version) feature + bom] --> [CXF JAX-RS from apache-cxf repo] +@enduml +---- + +===== CXF upgrade (UNOMI-899, `8cff171ef`) + +*When:* REST/OpenAPI breakage, Karaf major jump (4.2 → 4.4), or explicit CXF security release. + +*Steps:* + +. Bump `` in root `pom.xml` (managed via `bom/pom.xml`). +. Confirm `kar/src/main/feature/feature.xml` still has: + +[source,xml] +---- +mvn:org.apache.cxf.karaf/apache-cxf/${cxf.version}/xml/features +... +cxf-jaxrs +---- + +. Rebuild REST modules and run feature verify (`mvn -pl package -DskipTests package`). +. Regenerate or smoke-test OpenAPI/Swagger UI (`unomi-rest-ui` feature) if JAX-RS annotations changed. + +*History:* UNOMI-876 moved CXF 3.3.11 → 3.6.5 with Karaf 4.4.8. UNOMI-899 bumped 3.6.5 → 3.6.8 independently and switched Jackson delivery to Karaf features. + +===== Jackson upgrade + +*Risk:* Karaf 4.4.11+ ships *multiple* `jackson` feature versions (e.g. 2.18.3 and 2.21.2). An unversioned `jackson` may resolve to the newest, while Unomi still installs datatype/jaxb modules at `${jackson.version}` — causing mixed Jackson on the OSGi classpath. + +*Required pattern* in `kar/src/main/feature/feature.xml`: + +[source,xml] +---- +jackson +jackson-jaxrs +mvn:com.fasterxml.jackson.datatype/jackson-datatype-jsr310/${jackson.version} +mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/${jackson.version} +---- + +*Procedure:* + +. Check which `jackson` feature versions exist for your `${karaf.version}`: + +[source,bash] +---- +curl -sL "https://repo1.maven.org/maven2/org/apache/karaf/features/specs/${KARAF_VERSION}/specs-${KARAF_VERSION}-features.xml" \ + | rg 'feature name="jackson"' +---- + +. Set `${jackson.version}` to a version that exists in Karaf *specs* (not necessarily the highest). +. Bump `bom/pom.xml` managed Jackson artifacts if needed. +. Do **not** embed Jackson in WABs or persistence bundles — `scope=provided` (UNOMI-491, UNOMI-876). + +*Major Karaf migration note:* UNOMI-876 initially listed every Jackson JAR in `feature.xml`; UNOMI-899 moved to Karaf features *with* version pins. Do not revert to unversioned features. + +===== Jetty alignment after Karaf bump + +Pax Web version is tied to `${karaf.version}`. Example: Karaf 4.4.11 → Pax Web 8.0.35 → Jetty 9.4.58.v20250814. + +. Inspect Pax Web features for your Karaf version: + +[source,bash] +---- +curl -sL "https://repo1.maven.org/maven2/org/apache/karaf/features/standard/${KARAF_VERSION}/standard-${KARAF_VERSION}-features.xml" \ + | rg 'pax-web-features' +# then open that pax-web version's features XML and rg 'jetty-server' +---- + +. Update `${jetty.version}` in root `pom.xml` to match Pax Web's Jetty bundle version. +. Modules using `${jetty.version}`: `graphql/karaf-feature/pom.xml`, `itests/pom.xml`. + +Runtime Jetty comes from Pax Web OSGi bundles — no Jetty bundles in Unomi `feature.xml`. + +===== Verification + +[source,bash] +---- +rg 'cxf\.version|jackson\.version|jetty\.version' pom.xml kar/src/main/feature/feature.xml +mvn -pl kar,package -DskipTests package +./build.sh --ci +---- diff --git a/manual/src/main/asciidoc/upgrades/upgrade-dependency-hygiene.adoc b/manual/src/main/asciidoc/upgrades/upgrade-dependency-hygiene.adoc new file mode 100644 index 000000000..f362ea1bc --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/upgrade-dependency-hygiene.adoc @@ -0,0 +1,115 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_upgrade_dependency_hygiene]] +==== Dependency hygiene (non-platform bumps) + +Not every version bump is a Karaf or persistence migration. PR2-style hygiene (UNOMI-875 `#795`, `ef10dfe15`) covers *application* dependencies that must *not* break OSGi rules established in platform upgrades. + +The bulk dependency pass https://issues.apache.org/jira/browse/UNOMI-829[UNOMI-829] defines the maintenance strategy: 30-minute timebox per library, maintenance-branch fallback, and separate tickets for Karaf/CXF/GraphQL — see <<_upgrade_jira_reference,JIRA reference>>. + +===== Scope + +[cols="1,2,2", options="header"] +|=== +| Track | Examples | OSGi impact + +| Frontend / WAB npm +| `wab/`, `graphql-ui/` yarn lockfiles +| Build-time only — verify WAB packaging still works + +| Messaging +| `kafka-clients` in `plugins/kafka-injector` +| Usually embed or declare imports explicitly in bundle + +| Commons / utilities +| `commons-lang3`, `commons-io`, Guava +| Prefer `provided` if Karaf feature supplies them + +| Logging +| `${log4j.version}` +| **High** — never without persistence embed exclusion +|=== + +===== Recommended order (platform stack) + + + +===== Hygiene vs platform upgrade scope + +[plantuml] +---- +@startuml +skinparam activityBackgroundColor #F8F8F8 + +start +:Library bump requested; +if (Touches Karaf features, +CXF, persistence embed, +or log4j OSGi?) then (yes) + :Stop — use platform upgrade guides +not bulk hygiene (UNOMI-829); + stop +else (no) +endif +:Try upgrade + mvn compile +(30 min timebox); +if (Build green?) then (yes) + :Commit in hygiene PR slice; + stop +else (no) + :File follow-up ticket +or use maintenance branch version; + stop +endif +@enduml +---- +. Karaf (+ JDK if major) — <<_upgrade_karaf,Karaf>> +. CXF / Jackson / Jetty alignment — <<_upgrade_cxf_jackson_jetty,CXF/Jackson/Jetty>> +. Elasticsearch + OpenSearch clients — <<_upgrade_elasticsearch,Elasticsearch>>, <<_upgrade_opensearch,OpenSearch>> +. Log4j — <<_upgrade_log4j,Log4j>> (last among platform pins) +. Application hygiene (npm, kafka, commons) — this section +. `./build.sh --ci` + +===== npm / frontend (Track E1) + +. Bump lockfiles in `wab/` and `graphql-ui/` with `yarn upgrade` or targeted version edits. +. Rebuild WAB modules: `mvn -pl wab,graphql-ui -am -DskipTests package` +. Smoke-test tracker and GraphQL UI in a running Karaf if UI behavior changed. + +===== Java libraries (Track E2) + +. Bump property in root `pom.xml`; confirm `bom/pom.xml` manages the artifact. +. Check consuming bundle `Import-Package` / embed instructions — do not pull platform libs into compile scope accidentally. +. Run unit tests for affected modules before full CI. + +===== Log4j hygiene trap (PR2 lesson) + +Bumping `${log4j.version}` alone caused Elasticsearch core to embed `log4j-core` transitively → Felix duplicate activator → feature verify failure. Always pair log4j bumps with: + +[source,xml] +---- +!groupId=org.apache.logging.log4j;artifactId=*, +---- + +in persistence ES/OS `Embed-Dependency` (see <<_upgrade_log4j,Log4j upgrade>>). + +===== Verification + +[source,bash] +---- +mvn -DskipTests compile +mvn -pl package -DskipTests package +./build.sh --ci +---- diff --git a/manual/src/main/asciidoc/upgrades/upgrade-elasticsearch.adoc b/manual/src/main/asciidoc/upgrades/upgrade-elasticsearch.adoc new file mode 100644 index 000000000..8673ee107 --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/upgrade-elasticsearch.adoc @@ -0,0 +1,183 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_upgrade_elasticsearch]] +==== Upgrading the Elasticsearch client + +Unomi does *not* embed the Elasticsearch server. It embeds the *Java API client* and REST transport inside the `unomi-persistence-elasticsearch-core` OSGi bundle, then exports query DSL types for the conditions bundle. + +For *data migration* between Elasticsearch major versions, see also <<_migrate_from_elasticsearch_7_to_elasticsearch_9,ES7 to ES9 migration>> (distinct from https://issues.apache.org/jira/browse/UNOMI-884[UNOMI-884] V3 data migration scripts). + +Client migration is tracked in https://issues.apache.org/jira/browse/UNOMI-901[UNOMI-901] (supersedes https://issues.apache.org/jira/browse/UNOMI-851[UNOMI-851]). JIRA notes an interim ES 8 client on Java 8 for 2.x branches only — Unomi 3 pairs ES 9 with Karaf 4.4 + JDK 17; see <<_upgrade_jira_reference,JIRA reference>>. + +===== Version pins + +[cols="1,2", options="header"] +|=== +| Property | Files + +| `elasticsearch.version` +| Root `pom.xml`, `bom/pom.xml` + +| `elasticsearch.test.version` +| Root `pom.xml`, IT Docker image in `itests/pom.xml` +|=== + +Update in the same pull request: `docker/README.md`, `docker/src/main/docker/docker-compose-*.yml`, `itests/docker-compose-snapshot-analysis.yml`. + +Image tag example: `docker.elastic.co/elasticsearch/elasticsearch:9.4.3`. + +===== Bundle architecture + +[cols="1,2", options="header"] +|=== +| Module | Role + +| `persistence-elasticsearch/core` +| Embeds ES client; exports `co.elastic.clients.*`; provides `PersistenceService` + +| `persistence-elasticsearch/conditions` +| Query builders; imports exported ES types; does not re-embed client +|=== + + +[plantuml] +---- +@startuml +skinparam componentStyle uml2 +skinparam component { + BackgroundColor<> #E8F4E8 + BackgroundColor<> #E8EEF8 + BackgroundColor<> #F0F0F0 +} + +package "Karaf platform" <> { + [slf4j-api provided] + [Jackson 2 provided] +} + +package "unomi-persistence-elasticsearch-core" <> { + [elasticsearch-java embedded] + [httpclient5 / parsson embedded] + [Exports co.elastic.clients.*] +} + +package "unomi-persistence-elasticsearch-conditions" <> { + [Imports exported ES types] + [No client re-embed] +} + +cloud "Elasticsearch cluster" <> { + [Docker / external ES (elasticsearch.version)] +} + +[unomi-persistence-elasticsearch-core] --> [Elasticsearch cluster] : REST +[unomi-persistence-elasticsearch-conditions] --> [unomi-persistence-elasticsearch-core] : Import-Package +[unomi-persistence-elasticsearch-core] ..> [slf4j-api provided] : provided +[unomi-persistence-elasticsearch-core] ..> [Jackson 2 provided] : provided + +note bottom of [unomi-persistence-elasticsearch-core] + Embed-Dependency excludes log4j:* + Optional Import-Package for codecs +end note +@enduml +---- + +OpenSearch follows the same pattern in `persistence-opensearch/core` — see <<_upgrade_opensearch,OpenSearch upgrade>>. + +*Embed rules* (`core/pom.xml`): + +[source,xml] +---- + + !groupId=org.apache.logging.log4j;artifactId=*, + *;scope=compile|runtime + +true +---- + +*Embed:* `elasticsearch-java`, `elasticsearch-rest5-client` (9.4+), httpclient5, parsson, OpenTelemetry, etc. + +*Do not embed:* log4j JARs (see <<_upgrade_log4j,Log4j>>); optional codecs (brotli, zstd, netty) — use optional Import-Package only. + +*Provided:* `slf4j-api`, Jackson 2, commons, Unomi API modules. + +*Export from core:* `co.elastic.clients.elasticsearch`, `_types`, `_types.query_dsl`, `util`. + +===== Git history (OSGi embedding) + +[cols="1,1,2", options="header"] +|=== +| Commit | Issue | Fix + +| `96a018ecb` (UNOMI-70) +| ES 5 monolithic embed +| REST client only; optional codec imports + +| `838fc1cb9` / `a605e2731` (UNOMI-225) +| ES 7 resolution +| Rewrote Import-Package iteratively + +| `2e6a6e8f1` (UNOMI-584) +| Test dep pollution +| Exclude lucene-core from lucene-test-framework + +| `d3d2d16c4` (UNOMI-901) +| HLRC → Java API +| Export co.elastic.clients.*; remove ChildFirstClassLoader + +| PR2 #795 +| log4j 2.24 embed +| OSGi failure — exclude log4j from embed in PR3 + +| PR3 #796 +| ES 9.4.3 / httpclient5 5.6 +| Routing API `List`; optional brotli/zstd/netty imports +|=== + +===== httpclient5 5.6+ optional codec imports + +ES 9.4+ uses httpclient5 5.6.x. Add to `Import-Package` in core (before `*`): + +[source] +---- +com.aayushatharva.brotli4j.decoder;resolution:=optional, +com.aayushatharva.brotli4j.encoder;resolution:=optional, +com.github.luben.zstd;resolution:=optional, +io.netty.buffer;resolution:=optional, +io.netty.channel;resolution:=optional, +io.netty.util;resolution:=optional, +javax.xml.datatype;resolution:=optional, +javax.xml.namespace;resolution:=optional, +javax.xml.transform.dom;resolution:=optional, +org.newsclub.net.unix;resolution:=optional, +---- + +Do **not** add compile Maven dependencies for `brotli4j` / `zstd-jni`. + +===== Java API checklist + +. `mvn -pl persistence-elasticsearch/core,persistence-elasticsearch/conditions -am -DskipTests compile` +. Fix API breaks in `ElasticSearchPersistenceServiceImpl.java` (e.g. `UpdateRequest.routing()` → `List` in 9.4). +. ES 9.4 embeds Jackson 3 (`tools.jackson.core`) inside the bundle; Unomi app code uses Jackson 2 — do not add Jackson 3 as a root dependency. +. Audit manifest — expect no embedded log4j or native codec JARs. + +===== Verification + +[source,bash] +---- +mvn -pl persistence-elasticsearch/core install -DskipTests +mvn -pl extensions/groovy-actions/karaf-kar -DskipTests package 2>&1 | rg -i 'Unable to resolve|persistence-elasticsearch' +./build.sh --ci +---- diff --git a/manual/src/main/asciidoc/upgrades/upgrade-integration-tests.adoc b/manual/src/main/asciidoc/upgrades/upgrade-integration-tests.adoc new file mode 100644 index 000000000..2e839c36f --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/upgrade-integration-tests.adoc @@ -0,0 +1,283 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_upgrade_integration_tests]] +==== Integration tests and Docker + +Platform upgrades are validated by *Pax Exam* integration tests plus *Docker-backed* search engines. Failures often appear in Maven Failsafe output, not in `karaf.log`. + +See also: `itests/README.md` and <<_using_the_build_script,build.sh>>. + + + +===== Validation pipeline after a platform bump + +[plantuml] +---- +@startuml +skinparam activityBackgroundColor #F8F8F8 + +|Maintainer| +start +:mvn -DskipTests compile; +:mvn -pl kar,package -DskipTests package +(feature verify); +|Docker| +:docker pull ES image +(elasticsearch.test.version); +:docker pull OS image +(opensearch.version); +|Integration tests| +:./build.sh --ci +(or --integration-tests ×2 profiles); +if (All ITs fail in ~5–10 s +Failsafe CNFE?) then (yes) + :Static init in BaseIT? — +<<_upgrade_integration_tests_pax_exam_probe,Pax Exam probe & static init>>; + :Use lazy getObjectMapper(); + :Re-run ITs; +else (no) +endif +if (karaf.log clean +but tests timeout after unomi:start?) then (yes) + :Docker ES/OS not up — +use integration-tests profile / build.sh; +else (no) +endif +if (BundleException +in karaf.log at startup?) then (yes) + :OSGi / embed issue +<<_upgrade_elasticsearch,ES/OS>> or <<_upgrade_log4j,Log4j>>; +else (no) +endif +:Green CI → merge; +stop +@enduml +---- +===== Architecture + +[cols="1,2,2", options="header"] +|=== +| Layer | Technology | Location + +| Test runner +| Pax Exam + JUnit 4 +| `itests/.../BaseIT.java` + +| Karaf under test +| Unomi distribution tar.gz +| `itests/pom.xml` — `${karaf.version}` + +| Search engine +| docker-maven-plugin +| ES port 9400 / OS port 9401 + +| JSON in IT helpers +| `BaseIT.getObjectMapper()` → `CustomObjectMapper.getCustomInstance()` (lazy, on first use) +| `itests/.../BaseIT.java` + +| Exam logs +| Karaf instance +| `itests/target/exam/*/data/log/karaf.log` + +| Reports +| Failsafe +| `itests/target/failsafe-reports/` +|=== + +===== Docker IT (UNOMI-921) + +https://issues.apache.org/jira/browse/UNOMI-921[UNOMI-921] motivation: stop downloading ES binaries in Maven, remove the `default_template` workaround required with elasticsearch-maven-plugin on ES 8/9, and match the OpenSearch Docker pattern. + +Shipped: `elasticsearch-maven-plugin` replaced with `io.fabric8:docker-maven-plugin` (`5a5aedba7`, #767). Related stabilization: https://issues.apache.org/jira/browse/UNOMI-937[UNOMI-937], tooling https://issues.apache.org/jira/browse/UNOMI-944[UNOMI-944]. + +[cols="1,2,1", options="header"] +|=== +| Engine | Image | Property + +| Elasticsearch +| `docker.elastic.co/elasticsearch/elasticsearch:${elasticsearch.test.version}` +| `elasticsearch.test.version` + +| OpenSearch +| `opensearchproject/opensearch:${opensearch.version}` +| `opensearch.version` +|=== + +Always run `docker pull` for both images before bumping version properties. + +[[_upgrade_integration_tests_pax_exam_probe]] +===== Pax Exam probe and static initialization (Karaf 4.4.11 / Pax Exam 4.14) + +After a Karaf bump, integration tests may fail *before Unomi starts*. `karaf.log` can look healthy or incomplete while Failsafe reports hundreds of errors in a few seconds — that pattern almost always means the *PAXEXAM-PROBE* bundle failed to load test classes, not a Karaf startup crash. + +*How the probe works* + +* Pax Exam 4.14 builds the probe with `rawBuilder()`: compiled test classes under `target/test-classes` plus `DynamicImport-Package: *` — *not* the full Maven test classpath. +* Maven `` on `itests/pom.xml` are for *compile-time* only. They are **not** automatically embedded in the probe. +* Types the probe needs at runtime must be exported by *already resolved* OSGi bundles in the Karaf container, unless you explicitly embed them on `Bundle-ClassPath` (avoid — see <<_upgrade_integration_tests_do_not_embed,Do not embed>>). + +*Why static initializers break (UNOMI-875 PR3 lesson)* + +A `static { … }` block (or `static final` field initialized with `new …()`) runs when the JVM *loads* the probe class — typically when Pax Exam installs `PAXEXAM-PROBE`, **before** `@Before`, **before** `unomi:start`, and often **before** Unomi features finish installing. + +That is too early for types that live in Unomi features, not in Karaf boot: + +* `com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule` — **not** in Karaf's `jackson` feature; shipped in Unomi's `unomi-base` (`kar/src/main/feature/feature.xml`). +* `CustomObjectMapper` and its Jackson modules — same: available only after `unomi-base` bundles are resolved. + +On Karaf 4.4.8 this often *appeared* to work because feature boot timing usually left `unomi-base` ready before the probe loaded `BaseIT`. On 4.4.11 (heavier/async feature boot, dual Jackson stacks) the same static block fails reliably with: + +[source] +---- +ClassNotFoundException: com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule + not found by PAXEXAM-PROBE-… +---- + +*Historical mistake:* `BaseIT` had a static block since UNOMI-568 (May 2022) for a shared `ObjectMapper`; UNOMI-569 (June 2022) added `JaxbAnnotationModule` to mirror production JSON handling. That duplicated `CustomObjectMapper` and forced Jackson types to load at probe install time. + +*Do not do this* + +[source,java] +---- +// BAD — runs when probe class loads, before Unomi features are up +protected static final ObjectMapper objectMapper; +static { + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JaxbAnnotationModule()); +} +---- + +*Shipped fix (UNOMI-875 PR3, current)* + +Use *on-first-use* access after Unomi is running. Delegate to the same mapper as the server: + +[source,java] +---- +/** JSON mapper for IT HTTP/JSON helpers — lazy, after Unomi features are up. */ +protected ObjectMapper getObjectMapper() { + return CustomObjectMapper.getCustomInstance(); +} +---- + +Call sites use `getObjectMapper()` in test methods (after `@Before` / `waitForStartup()`), not a static field. `CustomObjectMapper.getCustomInstance()` returns the holder singleton — the first call happens when HTTP/JSON helpers run, when `unomi-base` (including `jackson-module-jaxb-annotations`) is already active, so `DynamicImport-Package: *` can wire the types. + +[[_upgrade_integration_tests_do_not_embed]] +*Do not embed (removed anti-patterns)* + +An earlier PR3 attempt embedded test JARs on the probe `Bundle-ClassPath` (`copy-probe-lib`, `probe-bundle-classpath.properties`, `getOsgiService()` JDK proxy). That was **more complex than necessary** and caused new failures (`ClassCastException`, Maven phase ordering bugs, `Require-Bundle` wiring races). **Do not reintroduce:** + +* `maven-dependency-plugin` copying Unomi JARs into `target/test-classes/lib/` +* `@ProbeBuilder` setting explicit `Bundle-ClassPath` for embedded libs +* `Require-Bundle` on the probe for Unomi bundles +* JDK dynamic proxy on `getOsgiService()` to paper over duplicate embedded API classes + +If ITs fail with `ClassNotFoundException` on Jackson or Unomi types, fix *when* the probe touches those types (move off static init), not by embedding the whole test classpath into the probe. + +*Rules for probe-safe IT code* + +* **No static initializers** in `BaseIT` or IT base classes that reference Jackson modules, `CustomObjectMapper`, or other types from Unomi/Karaf features not in vanilla Karaf boot. +* **Lazy/on-first-use** for JSON mappers, parsers, and helpers that need Unomi or Jackson JAXB types — call from test methods after `@Before` has started Unomi (or after the relevant OSGi service is known to be up). +* **Do not** duplicate production mapper setup in the probe; use `CustomObjectMapper.getCustomInstance()` via `BaseIT.getObjectMapper()` (or OSGi services from the running container). +* **Do not** run `mvn failsafe:integration-test` on `itests` without the `integration-tests` profile unless Docker ES/OS is already listening on the IT port (`9400` / `9401`). A fixed probe can still hang in `waitForStartup()` if persistence is unreachable. + +*Symptom → diagnosis* + +[cols="2,2,2", options="header"] +|=== +| Failsafe symptom | Typical cause | Fix + +| `ClassNotFoundException: …JaxbAnnotationModule` (all ITs, ~5–10 s) +| Static init or static field touching Jackson JAXB before `unomi-base` is up +| Remove static block; use lazy instance `getObjectMapper()` → `CustomObjectMapper.getCustomInstance()` + +| `ClassNotFoundException: …PersistenceService` (or other Unomi type) at class load +| Static init or early reference before Unomi features resolve +| Move work out of static blocks; ensure `@Before` starts Unomi first + +| All tests error in ~5–10 s; `karaf.log` short or still installing features +| Probe class loading failed early (static init or missing dynamic import) +| Failsafe + `itests/target/it-run-operator-note.txt`; check for static blocks + +| Tests reach `unomi:start` then timeout (~10–20 min) +| Docker ES/OS not running or persistence blueprint failed +| `karaf.log` (`elasticSearchPersistenceService`, port `9400`); `docker ps` + +| `EOFException` / RMI `Connection refused` after long wait +| Karaf container died during startup (often ES connectivity) +| Tail of `karaf.log` in the exam folder +|=== + +===== Reading logs + +[cols="1,2,2", options="header"] +|=== +| Artifact | Healthy signal | Failure signal + +| `karaf.log` +| Features install, persistence blueprints start, Pax Web binds +| `BundleException`, `Unable to resolve`, `elasticSearchPersistenceService` errors + +| Failsafe reports +| Tests pass +| ClassNotFound in probe (static init), timeouts + +| `itests/target/it-run-operator-note.txt` +| Normal run duration, memory samples +| ~7 s run with 292 errors → probe failure + +| docker-maven-plugin +| Container starts, HTTP 200 on `:9400` / `:9401` +| `Unable to pull`, `manifest not found`, connection refused from Unomi +|=== + +TIP: A clean or truncated `karaf.log` with all ITs failing in seconds usually indicates probe class loading (often a `static` block in `BaseIT`). A clean log with tests stuck after `unomi:start` usually indicates Docker / persistence — not the probe. + +===== Running ITs locally + +[source,bash] +---- +./build.sh --integration-tests +./build.sh --integration-tests --use-opensearch +./build.sh --ci --integration-tests --no-karaf +---- + +Use Maven profile `ci-build-itests` when building the `itests` module directly. The `integration-tests` profile starts the Docker search container; ad-hoc `mvn -pl itests verify` without that profile (or without `./build.sh --integration-tests`) skips Docker and ITs will hang waiting for Elasticsearch/OpenSearch. + +===== Karaf bump side effects (UNOMI-875 PR3) + +[cols="1,2,2", options="header"] +|=== +| Symptom | Cause | Fix + +| All ITs fail in ~5–10 s; `ClassNotFoundException: …JaxbAnnotationModule` +| Static `ObjectMapper` init in `BaseIT` before `unomi-base` is up +| Lazy instance `getObjectMapper()` → `CustomObjectMapper.getCustomInstance()` — <<_upgrade_integration_tests_pax_exam_probe,Pax Exam probe & static init>> + +| IT compile uses Jetty websocket client +| `${jetty.version}` drift vs Pax Web runtime +| Align Jetty pin after Karaf bump — <<_upgrade_cxf_jackson_jetty,CXF/Jackson/Jetty>> + +| Clean `karaf.log`, tests timeout after `unomi:start` +| Docker ES/OS not started or wrong image tag +| `./build.sh --integration-tests`; fix `elasticsearch.test.version` / `opensearch.version` +|=== + +===== Build script (UNOMI-887) + +Prefer `./build.sh --ci` over ad-hoc Maven invocations — it runs the same gates as GitHub Actions (compile, feature verify path, IT profiles). See <<_using_the_build_script,build.sh>> in the manual. + +===== IT developer tooling (UNOMI-944) + +For cross-run comparison and live Karaf inspection during upgrade debugging, see `itests/README.md` (run archival, log diff workflows). diff --git a/manual/src/main/asciidoc/upgrades/upgrade-java.adoc b/manual/src/main/asciidoc/upgrades/upgrade-java.adoc new file mode 100644 index 000000000..6838d3f1d --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/upgrade-java.adoc @@ -0,0 +1,106 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_upgrade_java]] +==== Upgrading the JDK + +A JDK upgrade is almost always bundled with a *major* Karaf migration. Unomi 3.x baseline is *Java 17* (UNOMI-876, `da9c57fd2`). + +===== Version pin + +|=== +| Property | Value | Files + +| `java.version` +| 17 +| Root `pom.xml` (`maven.compiler.source/target/release`, `karaf-maven-plugin` `javase` in `package/pom.xml`) +|=== + +Also update CI workflows, `build.sh` preflight checks, and contributor docs when raising the minimum JDK. + + + +===== JDK upgrade coupled to Karaf (diagram) + +Java version changes are not done in isolation on Unomi 3.x — they ride the major Karaf migration path (UNOMI-876). + +[plantuml] +---- +@startuml +skinparam activityBackgroundColor #F8F8F8 + +start +:Read Karaf release notes +minimum Java version; +:Bump java.version in pom.xml; +:Bump karaf.version + setenv.sh +(same PR / commit slice); +:Update package/pom.xml javase ++ Pax Exam IT config; +:Mass POM provided-scope fixes +(platform libs); +:WAB servlet → WebConfig +(Pax Web 8); +:./build.sh --ci; +stop +@enduml +---- +===== UNOMI-876 checklist (Java 11 → 17) + +[cols="1,2", options="header"] +|=== +| Area | Action + +| Maven compiler +| `17`; remove obsolete Java 11 `--add-opens` workarounds + +| Karaf distribution +| Karaf 4.4.x requires Java 17 — bump `${karaf.version}` in the same effort + +| Module POMs +| ~187 modules: platform libraries → `scope=provided`; fix `cannot find symbol` for removed JDK internals + +| WAB / Pax Web +| Blueprint servlet registration → programmatic `WebConfig.java` (Pax Web 8) + +| IT harness +| Pax Exam `javase` 17; Jacoco 0.8.13+; timeout property renames + +| Removed APIs +| OGNL scripting removed from defaults; javax → jakarta where applicable +|=== + +===== Procedure + +. Confirm target Karaf version's minimum Java from https://karaf.apache.org/download.html[Karaf download page]. +. Bump `java.version` and `${karaf.version}` together (see <<_upgrade_karaf,Karaf upgrade>>). +. Full compile: `mvn -DskipTests compile` +. Feature verify + `./build.sh --ci` +. Update GitHub Actions Java version matrix if present. + +===== Anti-patterns + +[cols="1,2", options="header"] +|=== +| Don't | Do instead + +| Bump JDK without Karaf compatibility check +| Read Karaf release notes; 4.4.x needs 17 + +| Embed JDK-specific libraries in bundles +| Use Karaf specs / provided scope + +| Skip ITs after JDK change +| Run full `./build.sh --ci` — bytecode and Pax Exam both shift +|=== diff --git a/manual/src/main/asciidoc/upgrades/upgrade-jira-reference.adoc b/manual/src/main/asciidoc/upgrades/upgrade-jira-reference.adoc new file mode 100644 index 000000000..954d4ad65 --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/upgrade-jira-reference.adoc @@ -0,0 +1,257 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_upgrade_jira_reference]] +==== JIRA reference (platform upgrades) + +This section maps Apache Unomi JIRA tickets to the upgrade runbooks in this chapter. Use it when you need *why* a constraint exists (Cellar removal, CXF 3.x ceiling, ES client decoupling) or when tracing work under the Unomi 3 epic. + +JIRA: https://issues.apache.org/jira/projects/UNOMI[UNOMI project] + +===== Unomi 3 epic + +https://issues.apache.org/jira/browse/UNOMI-875[UNOMI-875] (*Unomi Version 3*) groups most 3.x platform and product work. Karaf was initially deferred from the first 3.0 release because the cascade (Java 17, CXF, Cellar removal, WAB/Pax Web) would delay shipping again — see comments on https://issues.apache.org/jira/browse/UNOMI-898[UNOMI-898]. + +Platform-relevant subtasks include: + +[cols="1,2,2", options="header"] +|=== +| Ticket | Summary | Upgrade guide + +| https://issues.apache.org/jira/browse/UNOMI-876[UNOMI-876] +| Karaf 4.4.7+ (major migration) +| <<_upgrade_karaf,Karaf>>, <<_upgrade_java,JDK>> + +| https://issues.apache.org/jira/browse/UNOMI-877[UNOMI-877] +| Remove Karaf Cellar / Hazelcast → PersistenceService clustering +| <<_upgrade_karaf,Karaf>> (prerequisite) + +| https://issues.apache.org/jira/browse/UNOMI-828[UNOMI-828] +| OpenSearch persistence module +| <<_upgrade_opensearch,OpenSearch>> + +| https://issues.apache.org/jira/browse/UNOMI-901[UNOMI-901] +| HLRC → `elasticsearch-java` client +| <<_upgrade_elasticsearch,Elasticsearch>> + +| https://issues.apache.org/jira/browse/UNOMI-921[UNOMI-921] +| Docker-based ES in ITs (replace elasticsearch-maven-plugin) +| <<_upgrade_integration_tests,Integration tests>> + +| https://issues.apache.org/jira/browse/UNOMI-887[UNOMI-887] +| Enhanced `build.sh` +| <<_upgrade_integration_tests,Integration tests>> + +| https://issues.apache.org/jira/browse/UNOMI-884[UNOMI-884] +| V3 *data* migration scripts (audit fields, multi-tenancy) +| Manual migration chapter — *not* Maven version pins + +| https://issues.apache.org/jira/browse/UNOMI-829[UNOMI-829] +| General dependency upgrade strategy +| <<_upgrade_dependency_hygiene,Dependency hygiene>> +|=== + +IMPORTANT: https://issues.apache.org/jira/browse/UNOMI-884[UNOMI-884] covers *index/data migration* for existing deployments. Platform upgrade guides cover *runtime and client versions* (Karaf, ES/OS clients, Log4j). Do not conflate the two. + +===== Ticket map by component + +[cols="1,1,2,2", options="header"] +|=== +| Ticket | Status (2026) | Topic | Maintainer takeaway + +| https://issues.apache.org/jira/browse/UNOMI-526[UNOMI-526] +| Closed +| Karaf 4.3 / CXF 3.4.5 / Jackson 2.12 +| Blocked by https://issues.apache.org/jira/browse/CXF-8593[CXF-8593] and Karaf Cellar incompatibility with Karaf 4.3 + +| https://issues.apache.org/jira/browse/UNOMI-829[UNOMI-829] +| Closed +| Dependency hygiene +| 30-minute timebox per library; maintenance-branch strategy; Karaf blocked by https://issues.apache.org/jira/browse/KARAF-7526[KARAF-7526] until Cellar removed + +| https://issues.apache.org/jira/browse/UNOMI-838[UNOMI-838] +| Closed +| CXF + jakarta.servlet +| Superseded by Karaf 4.4 migration; GraphQL still on `javax` — see https://issues.apache.org/jira/browse/UNOMI-667[UNOMI-667] + +| https://issues.apache.org/jira/browse/UNOMI-851[UNOMI-851] +| Closed +| ES HLRC → 7.17 +| Duplicate of https://issues.apache.org/jira/browse/UNOMI-901[UNOMI-901]; shipped as ES 9 + `elasticsearch-java` + +| https://issues.apache.org/jira/browse/UNOMI-898[UNOMI-898] +| Closed +| Karaf 4.2.15 → 4.4.7 +| Duplicate of https://issues.apache.org/jira/browse/UNOMI-876[UNOMI-876] + +| https://issues.apache.org/jira/browse/UNOMI-70[UNOMI-70] +| Closed +| ES 5.x externalization +| Start of external ES server; no embedded ES in modern Unomi + +| https://issues.apache.org/jira/browse/UNOMI-225[UNOMI-225] +| Closed +| ES 7 / REST HLRC +| Predecessor path to UNOMI-901 + +| https://issues.apache.org/jira/browse/UNOMI-531[UNOMI-531] +| Open +| IT matrix for multiple ES versions +| Partially addressed by Docker IT properties; see <<_upgrade_integration_tests,Integration tests>> + +| https://issues.apache.org/jira/browse/UNOMI-667[UNOMI-667] +| In Progress +| GraphQL OSGi / `java.io` imports +| Future hygiene risk when bumping graphql-java — not a Karaf blocker today + +| https://issues.apache.org/jira/browse/UNOMI-889[UNOMI-889] +| Sub-task of 875 +| Remove OGNL scripting +| Called out in https://issues.apache.org/jira/browse/UNOMI-876[UNOMI-876] Karaf plan (Camel 4.x line) +|=== + +===== Historical blockers (Karaf) + + + +===== Karaf upgrade prerequisites (timeline) + +Historical JIRA blockers explain why Karaf 4.3+ waited until Unomi 3 clustering changed: + +[plantuml] +---- +@startuml +skinparam activityBackgroundColor #F8F8F8 + +title Karaf upgrade path (JIRA milestones) + +|UNOMI-526| +:CXF-8593 + Cellar block Karaf 4.3; +|UNOMI-829| +:Dependency pass blocked by KARAF-7526; +|UNOMI-877| +:Remove Cellar/Hazelcast +→ PersistenceService clustering; +|UNOMI-876| +:Karaf 4.4.8 + JDK 17 lands; +|UNOMI-875 PR3| +:Patch Karaf 4.4.11 ++ Jackson/Jetty cascade pins; +stop +@enduml +---- +Before Unomi 3 could move to Karaf 4.3+, two recurring blockers appeared in JIRA: + +. *Karaf Cellar* — https://issues.apache.org/jira/browse/KARAF-7526[KARAF-7526] / Cellar 4.2.1 not compatible with Karaf 4.3 (https://issues.apache.org/jira/browse/UNOMI-526[UNOMI-526], https://issues.apache.org/jira/browse/UNOMI-829[UNOMI-829]). Resolved by https://issues.apache.org/jira/browse/UNOMI-877[UNOMI-877] (PersistenceService-based clustering). +. *CXF OSGi* — https://issues.apache.org/jira/browse/CXF-8593[CXF-8593] blocked Jackson/CXF/Karaf alignment on the 4.3 line. Unomi 3 landed on Karaf 4.4 with *CXF 3.6.x* pinned in `feature.xml` (https://issues.apache.org/jira/browse/UNOMI-899[UNOMI-899]). + +https://issues.apache.org/jira/browse/UNOMI-876[UNOMI-876] documents a critical decision repeated in JIRA comments: *CXF 4.x removes OSGi support* and is unsuitable for Karaf. Target CXF 3.6.5+ (Unomi currently pins 3.6.8), not CXF 4. + +===== UNOMI-876 planned vs shipped + +Serge Huber's Karaf upgrade report on https://issues.apache.org/jira/browse/UNOMI-876[UNOMI-876] compares Karaf 4.2.15 vs 4.4.7 binary contents. Use it as a *checklist*, then verify against the *current* `${karaf.version}` feature XMLs (see <<_upgrade_karaf,Karaf>>). + +[cols="1,1,1,2", options="header"] +|=== +| Component | JIRA target (4.4.7 era) | Unomi 3.1 baseline | Notes + +| Java +| 17 +| 17 +| Shipped — commit `da9c57fd2` + +| Karaf +| 4.4.7 → later 4.4.11 +| 4.4.11 +| Patch bumps need cascade pins (PR3) + +| CXF +| 3.6.5 (not 4.x) +| 3.6.8 +| Repo override in `feature.xml` + +| Pax Web / Jetty +| 8.0.30 / Jetty 9.4.x +| Pax Web 8.0.35 / Jetty 9.4.58 +| Align `${jetty.version}` after each Karaf bump + +| Camel +| 4.9.0 (bundled with Karaf 4.4.7) +| *2.23.1* in `extensions/router/pom.xml` +| Router extension still ships Camel 2.x OSGi routes; Karaf may expose Camel 4 features — do not assume JIRA matrix matches router code without checking + +| Jackson +| ~2.15.x in report +| 2.18.3 pinned +| Pin `` on 4.4.11+ + +| Servlet API +| 4.0.1 (`javax`, not jakarta) +| javax.servlet in WAB/Pax Web 8 +| https://issues.apache.org/jira/browse/UNOMI-838[UNOMI-838] jakarta migration deferred +|=== + +===== Elasticsearch client strategy (UNOMI-901) + +https://issues.apache.org/jira/browse/UNOMI-901[UNOMI-901] acceptance criteria: remove HLRC, adopt `co.elastic.clients:elasticsearch-java`, refactor persistence code, green ITs. + +JIRA comment (decoupling option): if JDK 17 + Karaf upgrade is too heavy for a *2.x* maintenance branch, Elastic's ES 8 Java API client (Java 8+) can talk to ES 9 clusters as an interim step — but *Karaf upgrades belong on Unomi 3*, not 2.x. + +For Unomi 3 platform work, treat ES client, IT Docker tag, and compose files as one atomic bump (<<_upgrade_elasticsearch,Elasticsearch>>). + +===== Dependency upgrade strategy (UNOMI-829) + +When doing non-platform library bumps (<<_upgrade_dependency_hygiene,Dependency hygiene>>), follow the process documented on https://issues.apache.org/jira/browse/UNOMI-829[UNOMI-829]: + +. Prefer removing unused libraries over upgrading. +. Timebox each failed upgrade to *30 minutes*; if harder, file a follow-up and move on. +. Prefer latest release; if breaking, use latest on same major maintenance branch and open a ticket for the next major. +. Do *not* mix complex upgrades (Karaf, CXF, GraphQL OSGi) into a bulk hygiene pass. + +===== Integration test tickets + +[cols="1,2,2", options="header"] +|=== +| Ticket | Intent | Runbook + +| https://issues.apache.org/jira/browse/UNOMI-921[UNOMI-921] +| Replace elasticsearch-maven-plugin; remove `default_template` workaround; Docker parity with OpenSearch +| <<_upgrade_integration_tests,Integration tests>> + +| https://issues.apache.org/jira/browse/UNOMI-937[UNOMI-937] +| Stabilize ITs; align GitHub Actions with `build.sh` +| <<_upgrade_integration_tests,Integration tests>> + +| https://issues.apache.org/jira/browse/UNOMI-944[UNOMI-944] +| IT developer tooling (archival, diff, live Karaf) +| `itests/README.md` + +| https://issues.apache.org/jira/browse/UNOMI-946[UNOMI-946] +| Cluster health / replica checks in IT harness +| Migration and dual-engine IT matrix +|=== + +===== Searching JIRA for upgrade context + +Useful JQL examples (Apache JIRA → *Advanced* search): + +[source,text] +---- +project = UNOMI AND summary ~ "Karaf" ORDER BY updated DESC +project = UNOMI AND summary ~ "upgrade" AND status != Closed +project = UNOMI AND parent = UNOMI-875 +project = UNOMI AND text ~ "elasticsearch-maven-plugin" +---- + +When a ticket and git history disagree, prefer *git commits* for exact file changes and *JIRA* for intent, blockers, and acceptance criteria. diff --git a/manual/src/main/asciidoc/upgrades/upgrade-karaf.adoc b/manual/src/main/asciidoc/upgrades/upgrade-karaf.adoc new file mode 100644 index 000000000..3a31e4110 --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/upgrade-karaf.adoc @@ -0,0 +1,301 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_upgrade_karaf]] +==== Upgrading Apache Karaf + +Karaf upgrades touch the *whole distribution*, not a single module. Scope depends on whether you are doing a *major*, *minor*, or *patch* Karaf bump. + +TIP: JIRA context for prerequisites (Cellar removal, CXF 3.x ceiling, UNOMI-876 version matrix): <<_upgrade_jira_reference,JIRA reference>>. + +[cols="1,1,2", options="header"] +|=== +| Bump type | Example | Typical Unomi effort + +| *Major* +| 4.2.15 → 4.4.8 (UNOMI-876) +| JDK, feature namespace, WAB/Pax Web, ~187 POMs, CXF/Jackson/Jetty, full IT matrix + +| *Minor / patch* +| 4.4.8 → 4.4.11 (UNOMI-875 PR3) +| `karaf.version`, cascade library pins, compile fallout (e.g. slf4j), feature verify, IT probe fixes + +| *Config-only* +| UNOMI-919 startFeatures feature +| Feature graph / cfg — not a Karaf version bump +|=== + + +===== Choosing major vs patch Karaf path + +[plantuml] +---- +@startuml +skinparam activityBackgroundColor #F8F8F8 + +start +:New target Karaf version; +if (Crossing Karaf minor/major line? +e.g. 4.2 → 4.4) then (yes) + :Use UNOMI-876 major template; + :Bump java.version to 17; + :feature.xml v1.6 + specs repo; + :WAB → Pax Web 8 WebConfig; + :~187 POMs → provided; + :Full CXF/Jackson/Jetty alignment; +else (no) + :Use UNOMI-875 PR3 patch template; + :Bump karaf.version + setenv.sh; + :Compare feature XML cascade only; + :Fix compile fallout +(e.g. slf4j-api); + :Pin jackson + jetty versions; +endif +:Feature verify +mvn -pl package -DskipTests package; +:./build.sh --ci; +stop +@enduml +---- + +===== Framework cascade from Karaf feature XMLs + +When `${karaf.version}` changes, inspect upstream feature repositories before merging — patch releases move Pax Web, pax-logging, and Jackson specs independently of Unomi code. + +[plantuml] +---- +@startuml +skinparam componentStyle uml2 + +rectangle "Karaf (karaf.version) features" { + component "standard/features" as std + component "specs/features" as specs + component "framework/features" as fw +} + +component "Pax Web version" as paxweb +component "Jetty bundles" as jetty +component "pax-logging-log4j2" as plog +component "jackson feature versions" as jackson +component "CXF RELEASE pointer" as cxfref + +component "Unomi pom.xml" as pom +component "kar/feature.xml" as feat +component "setenv.sh" as env + +std --> paxweb +paxweb --> jetty +fw --> plog +specs --> jackson +std --> cxfref + +jetty ..> pom : (jetty.version) +jackson ..> pom : (jackson.version) +jackson ..> feat : version-pinned features +cxfref ..> feat : (cxf.version) repo +pom --> env : KARAF_VERSION +plog ..> pom : runtime Log4j ≠ (log4j.version) +@enduml +---- + +===== Framework cascade (every Karaf bump) + +Even patch releases can move platform libraries inside Karaf feature XMLs: + +[cols="1,1,1,2", options="header"] +|=== +| Component | 4.4.8 example | 4.4.11 example | Unomi action + +| Pax Web +| 8.0.33 +| 8.0.35 +| Align `${jetty.version}` — see <<_upgrade_cxf_jackson_jetty,CXF/Jackson/Jetty>> + +| pax-logging +| 2.3.0 +| 2.3.3 +| Runtime Log4j version changes — see <<_upgrade_log4j,Log4j>> + +| Jackson specs +| single 2.18.2 feature +| 2.18.3 *and* 2.21.2 features +| Pin `${jackson.version}` on `` — see <<_upgrade_cxf_jackson_jetty,CXF/Jackson/Jetty>> + +| CXF +| RELEASE pointer +| RELEASE pointer +| Keep `${cxf.version}` repo override (UNOMI-899) +|=== + +*Compare feature XMLs before merging:* + +[source,bash] +---- +KVER=4.4.11 +for repo in framework standard specs enterprise; do + echo "=== $repo ===" + curl -sL "https://repo1.maven.org/maven2/org/apache/karaf/features/${repo}/${KVER}/${repo}-${KVER}-features.xml" \ + | rg -i 'pax-web|pax-logging|jackson-core|jetty-server' | head -8 +done +---- + +===== Version pins to update + +*Required (every Karaf bump):* + +[cols="1,2", options="header"] +|=== +| File | Property / variable + +| Root `pom.xml` +| `` + +| `setenv.sh` +| `export KARAF_VERSION=…` +|=== + +Everything else should reference `${karaf.version}`: `bom/pom.xml`, `kar/pom.xml`, `package/pom.xml`, `distribution/pom.xml`, `itests/pom.xml`, extension and GraphQL Karaf feature modules. + +Search for hardcoded old versions: + +[source,bash] +---- +rg '4\.4\.[0-9]+' --glob '!**/target/**' pom.xml setenv.sh package kar itests extensions graphql +---- + +*Co-bump after comparing Karaf feature XMLs (patch/minor):* + +* `${jetty.version}` — match Pax Web Jetty bundles +* `${jackson.version}` — must exist as a Karaf `specs` feature version; pin in `feature.xml` +* `${cxf.version}` — only if REST/Karaf release notes require it (major jumps usually do) + +*Co-bump on major Karaf migration only:* + +* `java.version` — see <<_upgrade_java,JDK upgrade>> +* Feature namespace (`v1.6.0`), specs repository, WAB servlet wiring — UNOMI-876 + +===== Prerequisites (JIRA) + +Karaf bumps were blocked for years until clustering moved off Karaf Cellar: + +* https://issues.apache.org/jira/browse/UNOMI-877[UNOMI-877] — replace Cellar/Hazelcast with PersistenceService-backed sync (required before 4.3+). +* https://issues.apache.org/jira/browse/KARAF-7526[KARAF-7526] — Cellar incompatibility cited in https://issues.apache.org/jira/browse/UNOMI-526[UNOMI-526] and https://issues.apache.org/jira/browse/UNOMI-829[UNOMI-829]. +* https://issues.apache.org/jira/browse/CXF-8593[CXF-8593] — blocked Karaf 4.3 / Jackson 2.12 alignment until the 4.4 + CXF 3.6 path. + +Do not target CXF 4.x for Unomi on Karaf — https://issues.apache.org/jira/browse/UNOMI-876[UNOMI-876] notes OSGi support was removed in CXF 4. + +===== Major Karaf migration (UNOMI-876 template) + +Use this checklist when crossing Karaf minor/major lines (e.g. 4.2 → 4.4): + +. Read Karaf release notes and Java requirement. +. Bump `${karaf.version}` + `java.version` + `setenv.sh`. +. Update `kar/src/main/feature/feature.xml`: +** xmlns features `v1.6.0` +** `mvn:org.apache.karaf.features/specs/${karaf.version}/xml/features` +** CXF repo + versioned `cxf-jaxrs` +** Version-pinned `jackson` / `jackson-jaxrs` +. Migrate WAB servlets to Pax Web 8 programmatic registration (`WebConfig.java`). +. Mass POM update: platform deps → `scope=provided` (~187 modules in UNOMI-876). +. Update `package/pom.xml` boot/verify feature lists if graph changed (UNOMI-919: `unomi-startup` feature). +. IT harness: Pax Exam `instanceStartupTimeout`, Jacoco, `javase` in verify config. +. Full <<_upgrade_cxf_jackson_jetty,CXF/Jackson/Jetty>> alignment. +. `./build.sh --ci` + +===== Minor / patch Karaf migration (UNOMI-875 PR3 template) + +Use when staying on the same Karaf line (e.g. 4.4.8 → 4.4.11): + +. Bump `${karaf.version}` and `setenv.sh` only as the first commit slice. +. Run framework cascade comparison (Pax Web, pax-logging, Jackson specs). +. Update `${jetty.version}` and `${jackson.version}` + pin Jackson features in `feature.xml`. +. Compile all modules — fix missing `provided` deps (PR3: `slf4j-api` in `lifecycle-watcher`): + +[source,bash] +---- +mvn -DskipTests compile 2>&1 | rg 'cannot find symbol|package .* does not exist|slf4j' +---- + +. Feature verify (mandatory): + +[source,bash] +---- +mvn -pl extensions/groovy-actions/karaf-kar -am -DskipTests package +mvn -pl package -DskipTests package +mvn -pl kar,distribution -DskipTests package +---- + +. Check <<_upgrade_integration_tests,Integration tests>> for probe static-init / Docker image issues. +. `./build.sh --ci` + +Do **not** skip cascade library alignment — patch Karaf releases have changed Pax Web, pax-logging, and Jackson specs without changing Unomi application code. + +===== Lessons from past upgrades + +*UNOMI-216 (`b873b35a2`) — Karaf 4.1.7:* `${env:…}` placeholders in `custom.system.properties` need Karaf >= 4.1.6. Smoke-test property interpolation after any bump. + +*UNOMI-491 (`dda24b66e`) — Karaf 4.2.11:* Moved Jackson YAML out of WAB; reactivated features in package verify. Do not embed platform Jackson in web modules. + +*UNOMI-876 (`da9c57fd2`) — Karaf 4.4.8 + JDK 17:* Feature namespace v1.6; removed manual Jackson/Guava/SnakeYAML bundles; WAB → `WebConfig.java`; ~187 POMs to `provided`; CXF 3.3 → 3.6.5. + +*UNOMI-899 (`8cff171ef`) — CXF 3.6.8 + Karaf Jackson features:* Independent CXF bump; switched to Karaf `jackson` features (must be version-pinned on 4.4.11+). + +*UNOMI-919 (`a0a08e4ad`) — startFeatures:* Refactored startup to a Karaf feature; adjust `package/pom.xml` verify lists when changing boot order. + +*PR3 / UNOMI-875 (`545f6b8af`) — 4.4.8 → 4.4.11:* `lifecycle-watcher` lost transitive `slf4j-api`; Pax Web 8.0.35 → Jetty 9.4.58; Jackson specs duplication → pin `${jackson.version}`; Pax Exam IT fix — remove static `ObjectMapper` init in `BaseIT`, use lazy instance `getObjectMapper()` → `CustomObjectMapper.getCustomInstance()` (types from `unomi-base`, not Karaf boot) — see <<_upgrade_integration_tests_pax_exam_probe,Pax Exam probe & static init>>. + +===== Files to inspect + +[cols="1,2", options="header"] +|=== +| Area | Paths + +| Feature graphs +| `kar/src/main/feature/feature.xml`, `distribution/src/main/feature/feature.xml`, extension Karaf features + +| Package verify +| `package/pom.xml` — karaf-maven-plugin verify feature list + +| Runtime config +| `package/src/main/resources/etc/*.cfg`, `custom.system.properties` + +| Logging +| `org.ops4j.pax.logging.cfg`, `extensions/log4j-extension/` + +| IT container +| `itests/pom.xml`, `itests/.../BaseIT.java` +|=== + +===== Anti-patterns + +[cols="1,2,1", options="header"] +|=== +| Don't | Do instead | History + +| Embed platform libs in every bundle +| `scope=provided` + Karaf features +| UNOMI-876, PR2 log4j + +| Use unversioned `jackson` on 4.4.11+ +| `jackson` +| PR3 cascade + +| Assume a Karaf patch is "just pom.xml" +| Cascade pins + compile + feature verify +| PR3 lifecycle-watcher, Jetty, Jackson + +| Skip ITs after Karaf bump +| Run `./build.sh --ci` +| UNOMI-876 +|=== diff --git a/manual/src/main/asciidoc/upgrades/upgrade-log4j.adoc b/manual/src/main/asciidoc/upgrades/upgrade-log4j.adoc new file mode 100644 index 000000000..073effd11 --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/upgrade-log4j.adoc @@ -0,0 +1,113 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_upgrade_log4j]] +==== Upgrading Log4j + +Unomi uses *pax-logging-log4j2* as the platform logging implementation. Custom appenders live in `extensions/log4j-extension` as an OSGi *fragment* on pax-logging. Persistence bundles must *never embed* log4j JARs and must not call Log4j APIs at runtime. + +===== Version pin vs runtime version + +[cols="1,2,2", options="header"] +|=== +| Pin | Purpose | Notes + +| `${log4j.version}` in `pom.xml` +| Compile-time for `log4j-extension` (`provided`) +| Managed in `bom/pom.xml` + +| pax-logging-log4j2 bundle +| *Runtime* Log4j inside Karaf +| Version follows `${karaf.version}` — inspect bundle manifest, do not assume `${log4j.version}` matches +|=== + +Example: Karaf 4.4.8 ships pax-logging 2.3.0 (Log4j 2.24.3 inside the bundle). Karaf 4.4.11 ships pax-logging 2.3.3 (Log4j 2.25.4 inside). Unomi `${log4j.version}` 2.24.0 is still valid for fragment compilation if APIs are compatible — but always verify after a <<_upgrade_karaf,Karaf bump>>. + +===== Architecture + +[plantuml] +---- +@startuml +skinparam componentStyle uml2 + +package "Karaf runtime" { + [pax-logging-log4j2 host] as host + [unomi-log4j-extension fragment] as frag +} + +package "Persistence bundles" { + [ES core] as escore + [OS core] as oscore +} + +frag ..> host : Fragment-Host +escore ..> host : platform logging +oscore ..> host : platform logging + +note right of escore + Do not embed log4j JARs + in persistence bundles +end note +@enduml +---- + +Persistence ES/OS cores must *not* call Log4j APIs at runtime. REST client verbosity is configured in `etc/org.ops4j.pax.logging.cfg` (see <<_upgrade_log4j_rest_client,REST client log levels>>). + +[[_upgrade_log4j_rest_client]] +===== REST client log levels + +Elasticsearch and OpenSearch Java clients log through Apache Commons Logging → SLF4J → pax-logging. Do *not* set levels in persistence `start()` methods or persistence CM configs. + +*Deploy-time:* `etc/custom.system.properties` (or env vars): + +* `org.apache.unomi.elasticsearch.logLevelRestClient` / `UNOMI_ELASTICSEARCH_LOG_LEVEL_REST_CLIENT` (default `ERROR`) +* `org.apache.unomi.opensearch.logLevelRestClient` / `UNOMI_OPENSEARCH_LOG_LEVEL_REST_CLIENT` (default `ERROR`) +* `org.apache.unomi.logs.restClientTracer.level` / `UNOMI_LOG_LEVEL_REST_CLIENT_TRACER` — logger `tracer` (curl-style trace; default `ERROR`) + +*Runtime:* Karaf shell `log:set org.elasticsearch.client.RestClient DEBUG` (or edit `org.ops4j.pax.logging.cfg` and reload logging). + +`etc/org.ops4j.pax.logging.cfg` maps those system properties to `log4j2.logger.*` entries for `org.elasticsearch.client.RestClient`, `org.opensearch.client.RestClient`, and `tracer`. + +PR3 embed exclusion removed embedded `log4j-1.2-api`; legacy code that called `org.apache.log4j.Logger` in `ElasticSearchPersistenceServiceImpl.start()` then failed with `ClassNotFoundException` — fixed by moving control to pax-logging (not by re-embedding log4j). + +Optional Maven compile deps on log4j in the ES client POM are excluded from embed and unused at runtime. Felix must not see duplicate `Bundle-Activator` entries from embedded `log4j-core`. + +===== Git history + +* PR2 #795 (`ef10dfe15`): bumped to 2.24.0 — *reverted* after ES OSGi bundle failed feature verify (embedded log4j conflict). +* PR3 #796 (`a5e6f35d2`): re-applied 2.24.0 *with* persistence embed exclusion. +* UNOMI-936 (`e9a330052`): log format aligned with Karaf/pax-logging patterns — config in `org.ops4j.pax.logging.cfg`. + +Embed exclusion (both persistence cores): + +[source,xml] +---- + + !groupId=org.apache.logging.log4j;artifactId=*, + *;scope=compile|runtime + +---- + +===== Procedure + +. Never bump log4j alone without persistence OSGi verification. +. After a Karaf bump, inspect pax-logging version change — runtime Log4j may jump even if `${log4j.version}` is unchanged. +. Update `${log4j.version}` in root `pom.xml`. +. Confirm ES and OS core embed exclusion (see <<_upgrade_elasticsearch,Elasticsearch>> and <<_upgrade_opensearch,OpenSearch>>). +. Inspect manifests — no `log4j-*.jar` in `Embedded-Artifacts`. +. `mvn -pl package -DskipTests package` then `./build.sh --ci`. + +===== log4j-extension fragment + +`extensions/log4j-extension/pom.xml`: `Fragment-Host` = `org.ops4j.pax.logging.pax-logging-log4j2`; `log4j-core` scope = `provided`. diff --git a/manual/src/main/asciidoc/upgrades/upgrade-opensearch.adoc b/manual/src/main/asciidoc/upgrades/upgrade-opensearch.adoc new file mode 100644 index 000000000..7219c3667 --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/upgrade-opensearch.adoc @@ -0,0 +1,107 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_upgrade_opensearch]] +==== Upgrading the OpenSearch client + +OpenSearch persistence mirrors Elasticsearch: a fat OSGi bundle embeds the Java client and HTTP stack (UNOMI-828, `078ebceda`). + +For switching backends at runtime, see <<_migrate_from_elasticsearch_to_opensearch,Elasticsearch to OpenSearch migration>>. + +===== Version pins + +[cols="1,2", options="header"] +|=== +| Property | Files + +| `opensearch.version` +| Root `pom.xml`, `bom/pom.xml`, Docker compose, IT image + +| `opensearch.rest.client.version` +| Root `pom.xml` — keep in sync with `opensearch.version` +|=== + +IMPORTANT: `${opensearch.version}` must match a published Docker Hub tag. Verify before bumping: + +[source,bash] +---- +docker pull opensearchproject/opensearch:${opensearch.version} +---- + +Latest OpenSearch 3.x line on Docker Hub (as of Unomi 3.1 work): *3.7.0*. Do not assume every Maven Central `opensearch-java` version has a matching Docker image. + +===== Dual-engine verification (diagram) + +Unomi supports Elasticsearch and OpenSearch persistence modules; platform bumps must keep both IT profiles green. + +[plantuml] +---- +@startuml +skinparam componentStyle uml2 + +component "Unomi Karaf distribution" as unomi +component "persistence-elasticsearch" as es +component "persistence-opensearch" as os +cloud "ES Docker IT image" as esd +cloud "OS Docker IT image" as osd + +unomi --> es +unomi --> os +es --> esd +os --> osd + +note bottom of unomi + Run both IT profiles after platform bumps +end note +@enduml +---- + +===== Bundle architecture + +Module: `persistence-opensearch/core` (+ `conditions`). + +Same embed rules as Elasticsearch core — including log4j embed exclusion and optional codec imports after httpclient5 5.6. + +*Export-Package:* `org.opensearch.*`, `org.opensearch.index.query.*`, `org.apache.unomi.persistence.opensearch`. + +===== Step-by-step + +. Bump `opensearch.version` and `opensearch.rest.client.version` in root `pom.xml`. +. Update Docker compose files and `docker/README.md`. +. `mvn -pl persistence-opensearch/core,persistence-opensearch/conditions -am -DskipTests compile` +. Fix Java API breaks in `OpenSearchPersistenceServiceImpl.java` if any. +. Sync optional Import-Package with ES core when httpclient version jumps. +. Confirm log4j embed exclusion on OS core. +. Feature verify + `./build.sh --ci`; run OpenSearch IT profile. + +===== ES vs OS coupling + +[cols="1,1", options="header"] +|=== +| Concern | Keep in sync? + +| httpclient5 optional imports +| **Yes** + +| log4j embed exclusion +| **Yes** + +| Docker image tags +| No — independent server versions + +| Java API code changes +| No — separate impl classes +|=== + +You may upgrade ES and OS in one pull request but use separate commits for review clarity. diff --git a/manual/src/main/asciidoc/upgrades/upgrade-overview.adoc b/manual/src/main/asciidoc/upgrades/upgrade-overview.adoc new file mode 100644 index 000000000..75dc91e89 --- /dev/null +++ b/manual/src/main/asciidoc/upgrades/upgrade-overview.adoc @@ -0,0 +1,338 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +[[_platform_upgrade_overview]] +==== Overview and shared rules + +===== Guide index + +[cols="1,2,2", options="header"] +|=== +| Component | Guide | When to use + +| Overview +| This section +| PR planning, shared OSGi rules, version pins, upgrade ordering + +| Apache Karaf +| <<_upgrade_karaf,Karaf upgrade>> +| Major vs patch Karaf bumps, feature verify, framework cascade + +| JDK +| <<_upgrade_java,JDK upgrade>> +| Java version changes (with major Karaf migrations) + +| CXF / Jackson / Jetty +| <<_upgrade_cxf_jackson_jetty,CXF/Jackson/Jetty>> +| Libraries that cascade from Karaf but are Unomi-pinned + +| Elasticsearch +| <<_upgrade_elasticsearch,Elasticsearch upgrade>> +| `elasticsearch.version` bump, OSGi embed, Java API changes + +| OpenSearch +| <<_upgrade_opensearch,OpenSearch upgrade>> +| `opensearch.version` bump, Docker tag verification + +| Log4j +| <<_upgrade_log4j,Log4j upgrade>> +| `${log4j.version}` — always with persistence embed checks + +| Integration tests +| <<_upgrade_integration_tests,Integration tests>> +| IT/Docker failures, Pax Exam static init, log reading + +| Dependency hygiene +| <<_upgrade_dependency_hygiene,Dependency hygiene>> +| npm, kafka, commons — after platform pins are stable + +| JIRA reference +| <<_upgrade_jira_reference,JIRA reference>> +| Epic UNOMI-875, blockers, ticket-to-guide map, acceptance criteria +|=== + + +===== Platform upgrade workflow + +Recommended order when bumping multiple platform components in one release cycle: + +[plantuml] +---- +@startuml +skinparam activityBackgroundColor #F8F8F8 +skinparam activityBorderColor #333333 + +start +:Identify bump scope +(Karaf major / patch, ES, OS, hygiene); +if (Karaf version change?) then (yes) + :<<_upgrade_karaf,Karaf>> +Bump karaf.version + setenv.sh; + :Compare Karaf feature XMLs +(Pax Web, pax-logging, Jackson specs); + :Align jetty.version + jackson.version ++ feature.xml pins; + :Compile + feature verify +(kar, package, distribution); +else (no) +endif +if (JDK / major Karaf line?) then (yes) + :<<_upgrade_java,JDK>> +java.version with Karaf; +else (no) +endif +if (REST / CXF / Jackson drift?) then (yes) + :<<_upgrade_cxf_jackson_jetty,CXF/Jackson/Jetty>>; +else (no) +endif +if (Search client bump?) then (yes) + :<<_upgrade_elasticsearch,Elasticsearch>> ++ <<_upgrade_opensearch,OpenSearch>>; + :Sync Docker compose + IT image tags +docker pull both engines; +else (no) +endif +:<<_upgrade_log4j,Log4j>> +Last among platform pins +(embed exclusion verified); +if (Application libs only?) then (yes) + :<<_upgrade_dependency_hygiene,Dependency hygiene>>; +else (no) +endif +:./build.sh --ci; +if (Green?) then (yes) + :Open PR with focused commits; + stop +else (no) + :Read karaf.log + failsafe reports +<<_upgrade_integration_tests,Integration tests>>; + stop +endif +@enduml +---- + +===== Platform component relationships + +Karaf is the hub: most runtime libraries come from Karaf features, while Unomi Maven properties pin compile-time and feature-verify alignment. + +[plantuml] +---- +@startuml +skinparam componentStyle uml2 +skinparam component { + BackgroundColor<> #E8F4E8 + BackgroundColor<> #FFF4E6 + BackgroundColor<> #E8EEF8 + BackgroundColor<> #F0F0F0 +} + +package "Apache Karaf (karaf.version)" <> { + [Pax Web / Jetty runtime] + [pax-logging-log4j2] + [specs: jackson features] + [standard: CXF RELEASE ref] +} + +package "Unomi Maven pins" <> { + [cxf.version] + [jackson.version] + [jetty.version] + [log4j.version compile] +} + +package "kar/feature.xml overrides" <> { + [Versioned cxf-jaxrs] + [Versioned jackson features] +} + +package "Persistence OSGi bundles" <> { + [ES core embeds client] + [OS core embeds client] +} + +cloud "External search engines" <> { + [Elasticsearch Docker] + [OpenSearch Docker] +} + +[Pax Web / Jetty runtime] ..> [jetty.version] : align compile/test +[specs: jackson features] ..> [jackson.version] : pin feature version +[standard: CXF RELEASE ref] ..> [cxf.version] : repo override +[pax-logging-log4j2] ..> [log4j.version compile] : fragment compiles against +[ES core embeds client] --> [Elasticsearch Docker] : REST +[OS core embeds client] --> [OpenSearch Docker] : REST +[kar/feature.xml overrides] --> [Pax Web / Jetty runtime] +@enduml +---- + +===== Current version pins (Unomi 3.1 baseline) + +[cols="1,1,2", options="header"] +|=== +| Property | Value | Primary files + +| `java.version` +| 17 +| Root `pom.xml`, `package/pom.xml` (`javase`) + +| `karaf.version` +| 4.4.11 +| `pom.xml`, `setenv.sh`, `itests/pom.xml`, `*/karaf-kar/pom.xml` + +| `cxf.version` +| 3.6.8 +| `pom.xml`, `bom/pom.xml`, `kar/.../feature.xml` + +| `jackson.version` +| 2.18.3 +| `pom.xml`, `bom/pom.xml`, `kar/.../feature.xml` + +| `jetty.version` +| 9.4.58.v20250814 +| `pom.xml`, `graphql/karaf-feature/pom.xml`, `itests/pom.xml` + +| `elasticsearch.version` +| 9.4.3 +| `pom.xml`, `bom/pom.xml`, Docker ES compose files + +| `elasticsearch.test.version` +| 9.4.3 +| `pom.xml`, IT Docker ES image in `itests/pom.xml` + +| `opensearch.version` +| 3.7.0 +| `pom.xml`, `bom/pom.xml`, OS Docker compose, IT OS image + +| `opensearch.rest.client.version` +| 3.7.0 +| `pom.xml` (keep in sync with `opensearch.version`) + +| `log4j.version` +| 2.24.0 +| `pom.xml` (compile-time for fragments; runtime from pax-logging) +|=== + +IMPORTANT: Maven client version, IT Docker image, and `docker/` compose tags must match for each search engine. Verify with `docker pull` before opening a pull request. + +IMPORTANT: `${log4j.version}`, `${jackson.version}`, and `${jetty.version}` are *Unomi Maven pins*. Runtime versions for logging and HTTP come from Karaf/pax-logging/Pax Web — reconcile after every Karaf bump. + +===== Shared OSGi rules (Elasticsearch and OpenSearch persistence) + +. *Embed* the client and HTTP/JSON stack: `Embed-Dependency *;scope=compile|runtime`, `Embed-Transitive true`. +. *Exclude log4j from embed:* `!groupId=org.apache.logging.log4j;artifactId=*` (see <<_upgrade_log4j,Log4j upgrade>>). +. *Optional Import-Package* for codecs (brotli, zstd, netty) after httpclient5 5.6 — do **not** add compile Maven dependencies for native codecs. +. *Provided* for platform libraries: `slf4j-api`, Jackson 2 (`com.fasterxml.jackson.*`), commons, Unomi API modules. +. *Export* Elasticsearch query DSL types from the core bundle only; the conditions bundle imports exports — do not re-embed the client. +. Verify OSGi resolution, not just compilation: + +[source,bash] +---- +mvn -pl persistence-elasticsearch/core,persistence-opensearch/core install -DskipTests +mvn -pl package -DskipTests package +./build.sh --ci +---- + +===== Recommended pull request structure + +Split platform work into focused commits when possible: + +[cols="1,2,1", options="header"] +|=== +| Slice | Contents | Rationale + +| Karaf + cascade libs +| `karaf.version`, `setenv.sh`, `jetty.version`, `jackson.version`, `feature.xml` pins, compile-fix POMs +| Distribution-wide; isolate from search clients + +| ES + OS + Docker +| Version pins, persistence POMs, compose files, Java API fixes +| Large OSGi and Docker surface + +| Log4j last +| `${log4j.version}` after embed exclusion verified +| Log4j-only bump breaks ES bundle without embed fix + +| Hygiene +| npm, kafka, commons (Track E1/E2) +| Lower OSGi risk once platform is green +|=== + +===== Git history reference + +[cols="1,1,1,2", options="header"] +|=== +| Ticket / PR | Commit | Era | Key lesson + +| UNOMI-216 | `b873b35a2` +| Karaf 4.1.7 +| Property placeholders in etc require Karaf >= 4.1.6 + +| UNOMI-491 | `dda24b66e` +| Karaf 4.2.11 +| Do not duplicate Jackson in WAB; reactivate features after graph changes + +| UNOMI-876 | `da9c57fd2` +| Karaf 4.4.8 + JDK 17 +| feature.xml v1.6; platform deps `provided`; ~187 POM changes; WAB/Pax Web + +| UNOMI-899 | `8cff171ef` +| CXF 3.6.8 +| Karaf Jackson features; OpenAPI/Swagger path; pin CXF version in feature.xml + +| UNOMI-901 | `d3d2d16c4` +| ES 9 client +| Export `co.elastic.clients.*`; optional httpclient5 imports + +| UNOMI-828 | `078ebceda` +| OpenSearch module +| Lean embed + optional imports; dual-engine IT matrix + +| UNOMI-921 | `5a5aedba7` +| Docker IT +| Replace elasticsearch-maven-plugin with docker-maven-plugin + +| UNOMI-919 | `a0a08e4ad` +| Startup feature +| Boot feature graph changes — update package verify lists + +| PR2 #795 | `ef10dfe15` +| log4j 2.24 +| Embed without exclusion breaks Felix — reverted + +| PR3 #796 | `545f6b8af`… +| Karaf 4.4.11 / ES 9.4 / OS 3.7 +| slf4j-api; Jackson/Jetty cascade; codec optional imports; IT lazy JSON mapper (no static init in probe) +|=== + +===== Pre-merge checklist + +* [ ] Compared Karaf feature XMLs for Pax Web, pax-logging, Jackson specs (patch/minor Karaf bumps) +* [ ] `${jetty.version}` matches Pax Web Jetty bundles for `${karaf.version}` +* [ ] Jackson features version-pinned in `feature.xml`; `${jackson.version}` exists in Karaf specs +* [ ] `./build.sh --ci` green (or unit tests + both IT profiles) +* [ ] After Karaf bump: no static initializers in `BaseIT` that touch Jackson/Unomi types before features are up; IT JSON via lazy instance `getObjectMapper()` (see <<_upgrade_integration_tests_pax_exam_probe,Pax Exam probe & static init>>) +* [ ] `docker pull` succeeds for ES and OS IT images at pinned versions +* [ ] ES/OS core manifests: no embedded log4j / brotli4j / zstd-jni JARs +* [ ] `setenv.sh` `KARAF_VERSION` matches `pom.xml` +* [ ] Docker README and compose tags match version properties + +===== Quick diagnostics + +[source,bash] +---- +rg 'karaf\.version|jackson\.version|jetty\.version|cxf\.version|elasticsearch\.version|opensearch\.version|log4j\.version' pom.xml setenv.sh kar/src/main/feature/feature.xml +mvn -pl package -DskipTests package 2>&1 | rg -i 'Unable to resolve|BUILD FAILURE' +docker pull docker.elastic.co/elasticsearch/elasticsearch:9.4.3 +docker pull opensearchproject/opensearch:3.7.0 +---- From c3857fb09e694aec2ba97e9bcb1e6e88ea40006a Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sun, 5 Jul 2026 09:00:33 +0200 Subject: [PATCH 5/8] fix(persistence): configure REST client logging via pax-logging Stop using legacy log4j 1.x APIs in ES/OS persistence bundles. Route REST client log levels through Karaf org.ops4j.pax.logging.cfg instead. --- .../resources/etc/custom.system.properties | 4 +- .../resources/etc/org.ops4j.pax.logging.cfg | 11 ++++ persistence-elasticsearch/core/pom.xml | 50 ------------------- .../ElasticSearchPersistenceServiceImpl.java | 16 ------ .../OSGI-INF/blueprint/blueprint.xml | 3 -- ...apache.unomi.persistence.elasticsearch.cfg | 2 - persistence-opensearch/core/pom.xml | 16 +++++- .../OpenSearchPersistenceServiceImpl.java | 6 --- .../OSGI-INF/blueprint/blueprint.xml | 3 -- ...rg.apache.unomi.persistence.opensearch.cfg | 3 -- 10 files changed, 29 insertions(+), 85 deletions(-) diff --git a/package/src/main/resources/etc/custom.system.properties b/package/src/main/resources/etc/custom.system.properties index f6385658b..fa98f7979 100644 --- a/package/src/main/resources/etc/custom.system.properties +++ b/package/src/main/resources/etc/custom.system.properties @@ -151,8 +151,9 @@ org.apache.unomi.elasticsearch.password=${env:UNOMI_ELASTICSEARCH_PASSWORD:-} org.apache.unomi.elasticsearch.sslEnable=${env:UNOMI_ELASTICSEARCH_SSL_ENABLE:-false} org.apache.unomi.elasticsearch.sslTrustAllCertificates=${env:UNOMI_ELASTICSEARCH_SSL_TRUST_ALL_CERTIFICATES:-false} -# ES logging +# REST client log levels (applied in etc/org.ops4j.pax.logging.cfg; runtime: log:set) org.apache.unomi.elasticsearch.logLevelRestClient=${env:UNOMI_ELASTICSEARCH_LOG_LEVEL_REST_CLIENT:-ERROR} +org.apache.unomi.logs.restClientTracer.level=${env:UNOMI_LOG_LEVEL_REST_CLIENT_TRACER:-ERROR} ####################################################################################################################### ## OpenSearch settings ## @@ -220,6 +221,7 @@ org.apache.unomi.opensearch.username=${env:UNOMI_OPENSEARCH_USERNAME:-admin} org.apache.unomi.opensearch.password=${env:UNOMI_OPENSEARCH_PASSWORD:-} org.apache.unomi.opensearch.sslEnable=${env:UNOMI_OPENSEARCH_SSL_ENABLE:-true} org.apache.unomi.opensearch.sslTrustAllCertificates=${env:UNOMI_OPENSEARCH_SSL_TRUST_ALL_CERTIFICATES:-true} +org.apache.unomi.opensearch.logLevelRestClient=${env:UNOMI_OPENSEARCH_LOG_LEVEL_REST_CLIENT:-ERROR} org.apache.unomi.opensearch.minimalClusterState=${env:UNOMI_OPENSEARCH_MINIMAL_CLUSTER_STATE:-YELLOW} ####################################################################################################################### diff --git a/package/src/main/resources/etc/org.ops4j.pax.logging.cfg b/package/src/main/resources/etc/org.ops4j.pax.logging.cfg index 0069c4132..c00bd0414 100644 --- a/package/src/main/resources/etc/org.ops4j.pax.logging.cfg +++ b/package/src/main/resources/etc/org.ops4j.pax.logging.cfg @@ -131,3 +131,14 @@ log4j2.logger.jsonSchema.level = ${org.apache.unomi.logs.jsonschema.level:-INFO} # Enable debug logging for Karaf Deployer to understand which bundles are stopped and why log4j2.logger.karafDeployer.name = org.apache.karaf.features.core log4j2.logger.karafDeployer.level = ${org.apache.unomi.logs.deployer.level:-DEBUG} + +# Elasticsearch / OpenSearch REST clients (commons-logging → SLF4J → pax-logging) +# Suppress noisy response logs (e.g. deprecated ignore_throttled warnings). Tune via custom.system.properties or log:set. +log4j2.logger.esRestClient.name = org.elasticsearch.client.RestClient +log4j2.logger.esRestClient.level = ${org.apache.unomi.elasticsearch.logLevelRestClient:-ERROR} +log4j2.logger.osRestClient.name = org.opensearch.client.RestClient +log4j2.logger.osRestClient.level = ${org.apache.unomi.opensearch.logLevelRestClient:-ERROR} +# curl-style request/response trace (RestClient javadoc: logger name "tracer") +log4j2.logger.restClientTracer.name = tracer +log4j2.logger.restClientTracer.level = ${org.apache.unomi.logs.restClientTracer.level:-ERROR} + diff --git a/persistence-elasticsearch/core/pom.xml b/persistence-elasticsearch/core/pom.xml index f60982555..4e20b8ec3 100644 --- a/persistence-elasticsearch/core/pom.xml +++ b/persistence-elasticsearch/core/pom.xml @@ -125,56 +125,6 @@ joda-time provided - - - org.apache.logging.log4j - log4j-api - compile - true - - - org.apache.logging.log4j - log4j-core - compile - - - org.apache.logging.log4j - log4j-api - - - true - - - org.apache.logging.log4j - log4j-1.2-api - compile - - - org.apache.logging.log4j - log4j-api - - - org.apache.logging.log4j - log4j-core - - - true - - - - - com.aayushatharva.brotli4j - brotli4j - 1.20.0 - compile - - - com.github.luben - zstd-jni - 1.5.7-4 - compile - - junit junit diff --git a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java index 69dfb6474..3f62cb884 100644 --- a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java +++ b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java @@ -61,7 +61,6 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; -import org.apache.log4j.Level; import org.apache.unomi.api.*; import org.apache.unomi.api.conditions.Condition; import org.apache.unomi.api.query.DateRange; @@ -166,7 +165,6 @@ public class ElasticSearchPersistenceServiceImpl implements PersistenceService, private MetricsService metricsService; private boolean useBatchingForSave = false; private boolean useBatchingForUpdate = true; - private String logLevelRestClient = "ERROR"; private boolean alwaysOverwrite = true; private boolean aggQueryThrowOnMissingDocs = false; private Integer aggQueryMaxResponseSizeHttp = null; @@ -370,10 +368,6 @@ public void setAlwaysOverwrite(boolean alwaysOverwrite) { this.alwaysOverwrite = alwaysOverwrite; } - public void setLogLevelRestClient(String logLevelRestClient) { - this.logLevelRestClient = logLevelRestClient; - } - public void setTaskWaitingTimeout(String taskWaitingTimeout) { if (StringUtils.isNumeric(taskWaitingTimeout)) { this.taskWaitingTimeout = Integer.parseInt(taskWaitingTimeout); @@ -457,15 +451,6 @@ private String validateTenantAndGetId(String permission) { public void start() throws Exception { - // Work around to avoid ES Logs regarding the deprecated [ignore_throttled] parameter - try { - Level lvl = Level.toLevel(logLevelRestClient, Level.ERROR); - //TODO ensure this is necessary - org.apache.log4j.Logger.getLogger("org.elasticsearch.client.RestClient").setLevel(lvl); - } catch (Exception e) { - // Never fail because of the set of the logger - } - // on startup new InClassLoaderExecute<>(null, null, this.bundleContext, this.fatalIllegalStateErrors, throwExceptions) { public Object execute(Object... args) throws Exception { @@ -698,7 +683,6 @@ public void updated(Dictionary properties) { propertyMappings.put("aggQueryThrowOnMissingDocs", ConfigurationUpdateHelper.booleanProperty(this::setAggQueryThrowOnMissingDocs)); // String properties - propertyMappings.put("logLevelRestClient", ConfigurationUpdateHelper.stringProperty(this::setLogLevelRestClient)); propertyMappings.put("clientSocketTimeout", ConfigurationUpdateHelper.stringProperty(this::setClientSocketTimeout)); propertyMappings.put("taskWaitingTimeout", ConfigurationUpdateHelper.stringProperty(this::setTaskWaitingTimeout)); propertyMappings.put("taskWaitingPollingInterval", ConfigurationUpdateHelper.stringProperty(this::setTaskWaitingPollingInterval)); diff --git a/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml index a71474bb7..3df1b5b6e 100644 --- a/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml +++ b/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -68,8 +68,6 @@ - - @@ -160,7 +158,6 @@ - - *;scope=compile|runtime + + !groupId=org.apache.logging.log4j;artifactId=*, + *;scope=compile|runtime + true osgi.service;objectClass:List<String>="org.apache.unomi.persistence.spi.PersistenceService", diff --git a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java index 42e95e543..48ca106d7 100644 --- a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java +++ b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java @@ -159,7 +159,6 @@ public class OpenSearchPersistenceServiceImpl implements PersistenceService, Syn private MetricsService metricsService; private boolean useBatchingForSave = false; private boolean useBatchingForUpdate = true; - private String logLevelRestClient = "ERROR"; private boolean alwaysOverwrite = true; private boolean aggQueryThrowOnMissingDocs = false; private Integer aggQueryMaxResponseSizeHttp = null; @@ -354,10 +353,6 @@ public void setAlwaysOverwrite(boolean alwaysOverwrite) { this.alwaysOverwrite = alwaysOverwrite; } - public void setLogLevelRestClient(String logLevelRestClient) { - this.logLevelRestClient = logLevelRestClient; - } - public void setTaskWaitingTimeout(String taskWaitingTimeout) { if (StringUtils.isNumeric(taskWaitingTimeout)) { this.taskWaitingTimeout = Integer.parseInt(taskWaitingTimeout); @@ -590,7 +585,6 @@ public void updated(Dictionary properties) { propertyMappings.put("aggQueryThrowOnMissingDocs", ConfigurationUpdateHelper.booleanProperty(this::setAggQueryThrowOnMissingDocs)); // String properties - propertyMappings.put("logLevelRestClient", ConfigurationUpdateHelper.stringProperty(this::setLogLevelRestClient)); propertyMappings.put("clientSocketTimeout", ConfigurationUpdateHelper.stringProperty(this::setClientSocketTimeout)); propertyMappings.put("taskWaitingTimeout", ConfigurationUpdateHelper.stringProperty(this::setTaskWaitingTimeout)); propertyMappings.put("taskWaitingPollingInterval", ConfigurationUpdateHelper.stringProperty(this::setTaskWaitingPollingInterval)); diff --git a/persistence-opensearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/persistence-opensearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml index dd08748a7..3255eb984 100644 --- a/persistence-opensearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml +++ b/persistence-opensearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -68,8 +68,6 @@ - - @@ -158,7 +156,6 @@ - diff --git a/persistence-opensearch/core/src/main/resources/org.apache.unomi.persistence.opensearch.cfg b/persistence-opensearch/core/src/main/resources/org.apache.unomi.persistence.opensearch.cfg index 4fb927e5c..0505768a5 100644 --- a/persistence-opensearch/core/src/main/resources/org.apache.unomi.persistence.opensearch.cfg +++ b/persistence-opensearch/core/src/main/resources/org.apache.unomi.persistence.opensearch.cfg @@ -106,7 +106,4 @@ throwExceptions=${org.apache.unomi.opensearch.throwExceptions:-false} alwaysOverwrite=${org.apache.unomi.opensearch.alwaysOverwrite:-true} useBatchingForUpdate=${org.apache.unomi.opensearch.useBatchingForUpdate:-true} -# OpenSearch logging -logLevelRestClient=${org.apache.unomi.opensearch.logLevelRestClient:-ERROR} - minimalClusterState=${org.apache.unomi.opensearch.minimalClusterState:-GREEN} From 8f8f4d0d87ffa22a9c18f3f36f0793429f4dfae3 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sun, 5 Jul 2026 09:00:34 +0200 Subject: [PATCH 6/8] fix(itests): stabilize Pax Exam harness for Karaf 4.4.11 Use lazy CustomObjectMapper in BaseIT, unify IT JSON helpers, and suppress Pax Exam debug noise in the test JVM via DefaultServiceLog.level=INFO. --- itests/docker-compose-snapshot-analysis.yml | 2 +- itests/pom.xml | 13 ++ .../java/org/apache/unomi/itests/BaseIT.java | 42 ++++--- .../java/org/apache/unomi/itests/BasicIT.java | 24 ++-- .../apache/unomi/itests/ContextServletIT.java | 119 +++++++++--------- .../unomi/itests/CopyPropertiesActionIT.java | 3 +- .../unomi/itests/EventsCollectorIT.java | 4 +- .../unomi/itests/GroovyActionsServiceIT.java | 3 +- .../apache/unomi/itests/HealthCheckIT.java | 2 +- .../unomi/itests/InputValidationIT.java | 2 +- .../itests/LegacyQueryBuilderMappingIT.java | 3 +- .../java/org/apache/unomi/itests/PatchIT.java | 11 +- .../org/apache/unomi/itests/PersonaIT.java | 5 +- .../itests/PropertiesUpdateActionIT.java | 3 +- .../unomi/itests/RestCreateValidationIT.java | 2 +- .../apache/unomi/itests/RuleServiceIT.java | 3 +- .../org/apache/unomi/itests/SchedulerIT.java | 2 +- .../org/apache/unomi/itests/TenantIT.java | 20 ++- .../org/apache/unomi/itests/TestUtils.java | 18 +-- .../unomi/itests/V2CompatibilityModeIT.java | 60 ++++----- .../Migrate16xToCurrentVersionIT.java | 24 ++-- .../itests/shell/ShellCommandsBaseIT.java | 18 +-- 22 files changed, 190 insertions(+), 193 deletions(-) diff --git a/itests/docker-compose-snapshot-analysis.yml b/itests/docker-compose-snapshot-analysis.yml index 742b300e0..af517f1fc 100644 --- a/itests/docker-compose-snapshot-analysis.yml +++ b/itests/docker-compose-snapshot-analysis.yml @@ -16,7 +16,7 @@ ################################################################################ services: elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.3 container_name: es-snapshot-analysis environment: - discovery.type=single-node diff --git a/itests/pom.xml b/itests/pom.xml index 3e8c79ee0..d0051bb63 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -261,6 +261,17 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + + + INFO + + + @@ -293,6 +304,7 @@ elasticsearch ${elasticsearch.port} ${karaf.heap} + INFO @@ -414,6 +426,7 @@ foo opensearch localhost:${opensearch.port} + INFO diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java index 479cc110b..e5d9e76a4 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java @@ -17,11 +17,8 @@ package org.apache.unomi.itests; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; import org.apache.camel.CamelContext; import org.apache.camel.Route; import org.apache.camel.ServiceStatus; @@ -143,15 +140,15 @@ public abstract class BaseIT extends KarafTestSupport { protected static final String ENABLE_LOG_CHECKING_PROPERTY = "it.unomi.log.checking.enabled"; protected static final String CAMEL_DEBUG_PROPERTY = "it.unomi.camel.debug"; - protected final static ObjectMapper objectMapper; protected static boolean unomiStarted = false; protected static String searchEngine = SEARCH_ENGINE_ELASTICSEARCH; - static { - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new JaxbAnnotationModule()); - objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + /** + * JSON mapper for IT HTTP/JSON helpers. Initialized on first use (after Unomi features are up), + * delegating to the same mapper as the running server. + */ + protected ObjectMapper getObjectMapper() { + return CustomObjectMapper.getCustomInstance(); } protected PersistenceService persistenceService; @@ -193,6 +190,9 @@ public abstract class BaseIT extends KarafTestSupport { protected LogChecker logChecker; private String currentTestName; + + + public enum AuthType { NONE, // No authentication PUBLIC_KEY, // X-Unomi-Api-Key header with public key @@ -207,6 +207,20 @@ public enum AuthType { * This method should be called early, before any test setup, to ensure * the correct search engine is detected and any necessary fixes are applied. */ + + + protected TestUtils.RequestResponse executeContextJSONRequest(org.apache.http.client.methods.HttpUriRequest request, String sessionId) throws IOException { + return TestUtils.executeContextJSONRequest(request, sessionId, getObjectMapper()); + } + + protected TestUtils.RequestResponse executeContextJSONRequest(org.apache.http.client.methods.HttpUriRequest request, String sessionId, int expectedStatusCode, boolean withAuth) throws IOException { + return TestUtils.executeContextJSONRequest(request, sessionId, expectedStatusCode, withAuth, getObjectMapper()); + } + + protected TestUtils.RequestResponse executeContextJSONRequest(org.apache.http.client.methods.HttpPost request) throws IOException { + return TestUtils.executeContextJSONRequest(request, null, getObjectMapper()); + } + protected void checkSearchEngine() { searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH); } @@ -893,8 +907,8 @@ protected String getValidatedBundleJSON(final String resourcePath, Map T get(final String url, Class clazz) { final HttpGet httpGet = new HttpGet(getFullUrl(url)); response = executeHttpRequest(httpGet); if (response.getStatusLine().getStatusCode() == 200) { - return objectMapper.readValue(response.getEntity().getContent(), clazz); + return getObjectMapper().readValue(response.getEntity().getContent(), clazz); } else { return null; } @@ -1248,8 +1262,8 @@ protected CloseableHttpResponse executeHttpRequest(HttpUriRequest request, AuthT protected String resourceAsString(final String resource) { final java.net.URL url = bundleContext.getBundle().getResource(resource); try (InputStream stream = url.openStream()) { - JsonNode node = objectMapper.readTree(stream); - String value = objectMapper.writeValueAsString(node); + JsonNode node = getObjectMapper().readTree(stream); + String value = getObjectMapper().writeValueAsString(node); return value; } catch (final Exception e) { throw new RuntimeException(e); diff --git a/itests/src/test/java/org/apache/unomi/itests/BasicIT.java b/itests/src/test/java/org/apache/unomi/itests/BasicIT.java index cd42fcd1b..9d0408352 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BasicIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BasicIT.java @@ -32,7 +32,6 @@ import org.apache.unomi.api.conditions.ConditionType; import org.apache.unomi.api.rules.Rule; import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -132,7 +131,7 @@ public void testContextJSONWithUrlParameter() throws Exception { LOGGER.info("Start test testContextJSONWithUrlParameter"); ContextRequest contextRequest = new ContextRequest(); HttpPost request = new HttpPost(getFullUrl("/cxs/context.json?sessionId=" + SESSION_ID_1)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.create("application/json"))); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.create("application/json"))); executeContextJSONRequest(request, SESSION_ID_1); LOGGER.info("End test testContextJSONWithUrlParameter"); @@ -144,7 +143,7 @@ public void testContextJSON() throws Exception { ContextRequest contextRequest = new ContextRequest(); contextRequest.setSessionId(SESSION_ID_2); HttpPost request = new HttpPost(getFullUrl("/cxs/context.json")); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.create("application/json"))); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.create("application/json"))); executeContextJSONRequest(request, SESSION_ID_2); LOGGER.info("End test testContextJSON"); @@ -155,7 +154,7 @@ public void testMultipleLoginOnSameBrowser() throws Exception { LOGGER.info("Start test testMultipleLoginOnSameBrowser"); // Add login event condition - ConditionType conditionType = CustomObjectMapper.getObjectMapper().readValue( + ConditionType conditionType = getObjectMapper().readValue( new File("data/tmp/testLoginEventCondition.json").toURI().toURL(), ConditionType.class); definitionsService.setConditionType(conditionType); @@ -171,7 +170,7 @@ public void testMultipleLoginOnSameBrowser() throws Exception { ); // Add login rule - Rule rule = CustomObjectMapper.getObjectMapper().readValue(new File("data/tmp/testLogin.json").toURI().toURL(), + Rule rule = getObjectMapper().readValue(new File("data/tmp/testLogin.json").toURI().toURL(), Rule.class); createAndWaitForRule(rule); @@ -181,7 +180,7 @@ public void testMultipleLoginOnSameBrowser() throws Exception { // First page view with the first visitor aka VISITOR_1 and SESSION_ID_3 ContextRequest contextRequestPageViewSession1 = getContextRequestWithPageViewEvent(sourceSite, SESSION_ID_3); HttpPost requestPageView1 = new HttpPost(getFullUrl("/cxs/context.json")); - requestPageView1.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequestPageViewSession1), + requestPageView1.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequestPageViewSession1), ContentType.create("application/json"))); TestUtils.RequestResponse requestResponsePageView1 = executeContextJSONRequest(requestPageView1, SESSION_ID_3); String profileIdVisitor1 = requestResponsePageView1.getContextResponse().getProfileId(); @@ -201,7 +200,7 @@ public void testMultipleLoginOnSameBrowser() throws Exception { HttpPost requestLoginVisitor1 = new HttpPost(getFullUrl("/cxs/context.json")); requestLoginVisitor1.addHeader("Cookie", requestResponsePageView1.getCookieHeaderValue()); requestLoginVisitor1.addHeader("X-Unomi-Api-Key", testPublicKey.getKey()); - requestLoginVisitor1.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequestLoginVisitor1), + requestLoginVisitor1.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequestLoginVisitor1), ContentType.create("application/json"))); TestUtils.RequestResponse requestResponseLoginVisitor1 = executeContextJSONRequest(requestLoginVisitor1, SESSION_ID_3); Assert.assertEquals("Context profile id should be the same", profileIdVisitor1, @@ -214,7 +213,7 @@ public void testMultipleLoginOnSameBrowser() throws Exception { // Lets add a page view with VISITOR_1 to simulate reloading the page after login and be able to check the profile properties HttpPost requestPageView2 = new HttpPost(getFullUrl("/cxs/context.json")); requestPageView2.addHeader("Cookie", requestResponsePageView1.getCookieHeaderValue()); - requestPageView2.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequestPageViewSession1), + requestPageView2.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequestPageViewSession1), ContentType.create("application/json"))); TestUtils.RequestResponse requestResponsePageView2 = executeContextJSONRequest(requestPageView2, SESSION_ID_3); Assert.assertEquals("Context profile id should be the same", profileIdVisitor1, @@ -229,7 +228,7 @@ public void testMultipleLoginOnSameBrowser() throws Exception { ContextRequest contextRequestPageViewSession2 = getContextRequestWithPageViewEvent(sourceSite, SESSION_ID_4); HttpPost requestPageView3 = new HttpPost(getFullUrl("/cxs/context.json")); requestPageView3.addHeader("Cookie", requestResponsePageView1.getCookieHeaderValue()); - requestPageView3.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequestPageViewSession2), + requestPageView3.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequestPageViewSession2), ContentType.create("application/json"))); TestUtils.RequestResponse requestResponsePageView3 = executeContextJSONRequest(requestPageView3, SESSION_ID_4); Assert.assertEquals("Context profile id should be the same", profileIdVisitor1, @@ -255,7 +254,7 @@ public void testMultipleLoginOnSameBrowser() throws Exception { HttpPost requestLoginVisitor2 = new HttpPost(getFullUrl("/cxs/context.json")); requestLoginVisitor2.addHeader("Cookie", requestResponsePageView1.getCookieHeaderValue()); requestLoginVisitor2.addHeader("X-Unomi-Api-Key", testPublicKey.getKey()); - requestLoginVisitor2.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequestLoginVisitor2), + requestLoginVisitor2.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequestLoginVisitor2), ContentType.create("application/json"))); TestUtils.RequestResponse requestResponseLoginVisitor2 = executeContextJSONRequest(requestLoginVisitor2, SESSION_ID_4); // We should have a new profile id so the session should have been moved from VISITOR_1 to VISITOR_2 @@ -268,7 +267,7 @@ public void testMultipleLoginOnSameBrowser() throws Exception { // Lets add a page view with VISITOR_2 to simulate reloading the page after login HttpPost requestPageView4 = new HttpPost(getFullUrl("/cxs/context.json")); requestPageView4.addHeader("Cookie", requestResponseLoginVisitor2.getCookieHeaderValue()); - requestPageView4.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequestPageViewSession2), + requestPageView4.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequestPageViewSession2), ContentType.create("application/json"))); TestUtils.RequestResponse requestResponsePageView4 = executeContextJSONRequest(requestPageView4, SESSION_ID_4); Assert.assertEquals("Context profile id should be the same", profileIdVisitor2, @@ -341,9 +340,6 @@ private ContextRequest getContextRequestWithPageViewEvent(CustomItem sourceSite, return contextRequest; } - private TestUtils.RequestResponse executeContextJSONRequest(HttpPost request, String sessionId) throws IOException { - return TestUtils.executeContextJSONRequest(request, sessionId); - } private void checkVisitor1ResponseProperties(Map profileProperties) { checkVisitorResponseProperties(profileProperties, FIRST_NAME_VISITOR_1, LAST_NAME_VISITOR_1, EMAIL_VISITOR_1); diff --git a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java index 03a314a7d..0c8beb2eb 100644 --- a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java @@ -39,7 +39,6 @@ import org.apache.unomi.api.tenants.ApiKey; import org.apache.unomi.api.tenants.Tenant; import org.apache.unomi.itests.TestUtils.RequestResponse; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -172,8 +171,8 @@ public void testUpdateEventFromContextAuthorizedThirdParty_Success() throws Exce contextRequest.setEvents(Arrays.asList(event)); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); addPrivateTenantAuth(request, testTenant, testPrivateKey); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.executeContextJSONRequest(request, sessionId, -1, false); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + executeContextJSONRequest(request, sessionId, -1, false); event = keepTrying("Event " + eventId + " not updated in the required time", () -> eventService.getEvent(eventId), savedEvent -> Objects.nonNull(savedEvent) && TEST_EVENT_TYPE.equals(savedEvent.getEventType()), DEFAULT_TRYING_TIMEOUT, @@ -204,8 +203,8 @@ public void testCallingContextWithSessionCreation() throws Exception { contextRequest.setEvents(Collections.singletonList(event)); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); addPublicTenantAuth(request); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.executeContextJSONRequest(request, sessionId); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + executeContextJSONRequest(request, sessionId); Session session = keepTrying("Session with the id " + sessionId + " not saved in the required time", () -> profileService.loadSession(sessionId), Objects::nonNull, DEFAULT_TRYING_TIMEOUT, @@ -244,8 +243,8 @@ public void testUpdateEventFromContextUnAuthorizedThirdParty_Fail() throws Excep contextRequest.setSessionId(session.getItemId()); contextRequest.setEvents(Arrays.asList(event)); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.executeContextJSONRequest(request, sessionId); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + executeContextJSONRequest(request, sessionId); // Check event type did not changed event = shouldBeTrueUntilEnd("Event type should not have changed", () -> eventService.getEvent(eventId), @@ -276,8 +275,8 @@ public void testUpdateEventFromContextAuthorizedThirdPartyNoItemID_Fail() throws contextRequest.setSessionId(session.getItemId()); contextRequest.setEvents(Arrays.asList(event)); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.executeContextJSONRequest(request, sessionId); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + executeContextJSONRequest(request, sessionId); // Check event type did not changed event = shouldBeTrueUntilEnd("Event type should not have changed", () -> eventService.getEvent(eventId), @@ -301,8 +300,8 @@ public void testCreateEventsWithNoTimestampParam_profileAddedToSegment() throws contextRequest.setRequireSegments(true); contextRequest.setEvents(Arrays.asList(event)); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - String cookieHeaderValue = TestUtils.executeContextJSONRequest(request, sessionId).getCookieHeaderValue(); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + String cookieHeaderValue = executeContextJSONRequest(request, sessionId).getCookieHeaderValue(); refreshPersistence(Event.class); @@ -316,8 +315,8 @@ public void testCreateEventsWithNoTimestampParam_profileAddedToSegment() throws secondContextRequest.setEvents(Arrays.asList(secondEvent)); HttpPost secondRequest = new HttpPost(getFullUrl(CONTEXT_URL)); secondRequest.addHeader("Cookie", cookieHeaderValue); - secondRequest.setEntity(new StringEntity(objectMapper.writeValueAsString(secondContextRequest), ContentType.APPLICATION_JSON)); - TestUtils.executeContextJSONRequest(secondRequest, sessionId); + secondRequest.setEntity(new StringEntity(getObjectMapper().writeValueAsString(secondContextRequest), ContentType.APPLICATION_JSON)); + executeContextJSONRequest(secondRequest, sessionId); // Wait for profile to be saved with updated past event counts and segments // The SetEventOccurenceCountAction updates pastEvents, then EvaluateProfileSegmentsAction @@ -334,8 +333,8 @@ public void testCreateEventsWithNoTimestampParam_profileAddedToSegment() throws ContextRequest retryContextRequest = new ContextRequest(); retryContextRequest.setSessionId(sessionId); retryContextRequest.setRequireSegments(true); - retryRequest.setEntity(new StringEntity(objectMapper.writeValueAsString(retryContextRequest), ContentType.APPLICATION_JSON)); - ContextResponse response = (TestUtils.executeContextJSONRequest(retryRequest, sessionId)).getContextResponse(); + retryRequest.setEntity(new StringEntity(getObjectMapper().writeValueAsString(retryContextRequest), ContentType.APPLICATION_JSON)); + ContextResponse response = (executeContextJSONRequest(retryRequest, sessionId)).getContextResponse(); // Also refresh to ensure profile is loaded from persistence refreshPersistence(Profile.class); return response; @@ -372,13 +371,13 @@ public void testCreateEventWithTimestampParam_pastEvent_profileIsNotAddedToSegme contextRequest.setRequireSegments(true); contextRequest.setEvents(Arrays.asList(event)); HttpPost request = new HttpPost(regularURI); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); //The first event is with a default timestamp (now) - String cookieHeaderValue = TestUtils.executeContextJSONRequest(request, sessionId).getCookieHeaderValue(); + String cookieHeaderValue = executeContextJSONRequest(request, sessionId).getCookieHeaderValue(); //The second event is with a customized timestamp request.setURI(URI.create(customTimestampURI)); request.addHeader("Cookie", cookieHeaderValue); - ContextResponse response = (TestUtils.executeContextJSONRequest(request, sessionId)).getContextResponse(); //second event + ContextResponse response = (executeContextJSONRequest(request, sessionId)).getContextResponse(); //second event shouldBeTrueUntilEnd("Profile " + response.getProfileId() + " not found in the required time", () -> profileService.load(response.getProfileId()), @@ -404,14 +403,14 @@ public void testCreateEventWithTimestampParam_futureEvent_profileIsNotAddedToSeg contextRequest.setRequireSegments(true); contextRequest.setEvents(Arrays.asList(event)); HttpPost request = new HttpPost(regularURI); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); //The first event is with a default timestamp (now) - String cookieHeaderValue = TestUtils.executeContextJSONRequest(request, sessionId).getCookieHeaderValue(); + String cookieHeaderValue = executeContextJSONRequest(request, sessionId).getCookieHeaderValue(); //The second event is with a customized timestamp request.setURI(URI.create(customTimestampURI)); request.addHeader("Cookie", cookieHeaderValue); - ContextResponse response = TestUtils.executeContextJSONRequest(request, sessionId).getContextResponse(); //second event + ContextResponse response = executeContextJSONRequest(request, sessionId).getContextResponse(); //second event shouldBeTrueUntilEnd("Profile " + response.getProfileId() + " not found in the required time", () -> profileService.load(response.getProfileId()), @@ -435,8 +434,8 @@ public void testCreateEventWithProfileId_Success() throws Exception { //Act HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); addPublicTenantAuth(request); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.executeContextJSONRequest(request); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + executeContextJSONRequest(request); keepTrying("Profile " + TEST_PROFILE_ID + " not found in the required time", () -> profileService.load(TEST_PROFILE_ID), Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); @@ -462,8 +461,8 @@ public void testCreateEventWithPropertiesValidation_Success() throws Exception { //Act HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); addPrivateTenantAuth(request, testTenant, testPrivateKey); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.executeContextJSONRequest(request, null, -1, false); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + executeContextJSONRequest(request, null, -1, false); //Assert event = keepTrying("Event not found", () -> eventService.getEvent(eventId), Objects::nonNull, DEFAULT_TRYING_TIMEOUT, @@ -492,8 +491,8 @@ public void testCreateEventWithPropertyValueValidation_Failure() throws Exceptio //Act HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); addPrivateTenantAuth(request, testTenant, testPrivateKey); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.executeContextJSONRequest(request); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + executeContextJSONRequest(request); //Assert shouldBeTrueUntilEnd("Event should be null", () -> eventService.getEvent(eventId), Objects::isNull, DEFAULT_TRYING_TIMEOUT, @@ -519,8 +518,8 @@ public void testCreateEventWithPropertyNameValidation_Failure() throws Exception //Act HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); addPrivateTenantAuth(request, testTenant, testPrivateKey); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.executeContextJSONRequest(request); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + executeContextJSONRequest(request); //Assert shouldBeTrueUntilEnd("Event should be null", () -> eventService.getEvent(eventId), Objects::isNull, DEFAULT_TRYING_TIMEOUT, @@ -542,7 +541,7 @@ public void testMVELVulnerability() throws Exception { HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); request.setEntity( new StringEntity(getValidatedBundleJSON("security/mvel-payload-1.json", parameters), ContentType.APPLICATION_JSON)); - RequestResponse response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + RequestResponse response = executeContextJSONRequest(request, TEST_SESSION_ID); shouldBeTrueUntilEnd("Vulnerability successfully executed ! File created at " + vulnFileCanonicalPath, vulnFile::exists, exists -> exists == Boolean.FALSE, DEFAULT_TRYING_TIMEOUT, DEFAULT_SHOULDBETRUE_TRIES); @@ -562,7 +561,7 @@ public void testMVELVulnerabilityWithListPropertyValues() throws Exception { HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); request.setEntity( new StringEntity(getValidatedBundleJSON("security/mvel-payload-list.json", parameters), ContentType.APPLICATION_JSON)); - TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + executeContextJSONRequest(request, TEST_SESSION_ID); shouldBeTrueUntilEnd("Vulnerability successfully executed ! File created at " + vulnFileCanonicalPath, vulnFile::exists, exists -> exists == Boolean.FALSE, DEFAULT_TRYING_TIMEOUT, DEFAULT_SHOULDBETRUE_TRIES); @@ -574,7 +573,7 @@ public void testPersonalization() throws Exception { Map parameters = new HashMap<>(); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); request.setEntity(new StringEntity(getValidatedBundleJSON("personalization.json", parameters), ContentType.APPLICATION_JSON)); - RequestResponse response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + RequestResponse response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("Invalid response code", 200, response.getStatusCode()); } @@ -583,7 +582,7 @@ public void testScorePersonalizationStrategy_Interests() throws Exception { // Test request before adding interests to current profile. HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); request.setEntity(new StringEntity(getValidatedBundleJSON("personalization-score-interests.json", null), ContentType.APPLICATION_JSON)); - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request); + TestUtils.RequestResponse response = executeContextJSONRequest(request); ContextResponse contextResponse = response.getContextResponse(); List variants = contextResponse.getPersonalizations().get("perso-by-interest"); assertEquals("Invalid response code", 200, response.getStatusCode()); @@ -601,7 +600,7 @@ public void testScorePersonalizationStrategy_Interests() throws Exception { // check results of the perso now request = new HttpPost(getFullUrl(CONTEXT_URL)); request.setEntity(new StringEntity(getValidatedBundleJSON("personalization-score-interests.json", null), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request); + response = executeContextJSONRequest(request); contextResponse = response.getContextResponse(); variants = contextResponse.getPersonalizations().get("perso-by-interest"); assertEquals("Invalid response code", 200, response.getStatusCode()); @@ -638,7 +637,7 @@ public void testScorePersonalizationStrategy_Interests() throws Exception { // re test now that profiles has interests request = new HttpPost(getFullUrl(CONTEXT_URL)); request.setEntity(new StringEntity(getValidatedBundleJSON("personalization-score-interests.json", null), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request); + response = executeContextJSONRequest(request); contextResponse = response.getContextResponse(); variants = contextResponse.getPersonalizations().get("perso-by-interest"); assertEquals("Invalid response code", 200, response.getStatusCode()); @@ -666,7 +665,7 @@ public void testRequireScoring() throws Exception { Map parameters = new HashMap<>(); String scoringSource = getValidatedBundleJSON("score1.json", parameters); - Scoring scoring = CustomObjectMapper.getObjectMapper().readValue(scoringSource, Scoring.class); + Scoring scoring = getObjectMapper().readValue(scoringSource, Scoring.class); segmentService.setScoringDefinition(scoring); keepTrying("Profile does not contains scores in the required time", () -> profileService.load(TEST_PROFILE_ID), storedProfile -> @@ -676,7 +675,7 @@ public void testRequireScoring() throws Exception { parameters = new HashMap<>(); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); request.setEntity(new StringEntity(getValidatedBundleJSON("withoutRequireScores.json", parameters), ContentType.APPLICATION_JSON)); - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request); + TestUtils.RequestResponse response = executeContextJSONRequest(request); assertEquals("Invalid response code", 200, response.getStatusCode()); assertNotNull("Context response should not be null", response.getContextResponse()); @@ -687,7 +686,7 @@ public void testRequireScoring() throws Exception { parameters = new HashMap<>(); request = new HttpPost(getFullUrl(CONTEXT_URL)); request.setEntity(new StringEntity(getValidatedBundleJSON("withRequireScores.json", parameters), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request); + response = executeContextJSONRequest(request); assertEquals("Invalid response code", 200, response.getStatusCode()); assertNotNull("Context response should not be null", response.getContextResponse()); @@ -868,8 +867,8 @@ public void testContextEndpointAuthentication() throws Exception { contextRequest.setSessionId(TEST_SESSION_ID); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID, 401, false); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + TestUtils.RequestResponse response = executeContextJSONRequest(request, TEST_SESSION_ID, 401, false); Assert.assertEquals("Unauthenticated request should be rejected", 401, response.getStatusCode()); // Test with JAAS authentication (should succeed) @@ -887,7 +886,7 @@ public void testContextEndpointAuthentication() throws Exception { .build(); request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); // We need to specify which tenant we want to access since we are using the system administrator. request.addHeader(UNOMI_TENANT_ID_HEADER, TEST_TENANT_ID); CloseableHttpResponse jaasResponse = adminClient.execute(request); @@ -896,15 +895,15 @@ public void testContextEndpointAuthentication() throws Exception { // Test with public API key (should succeed) contextRequest.setPublicApiKey(publicKey.getKey()); request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + response = executeContextJSONRequest(request, TEST_SESSION_ID); Assert.assertEquals("Public API key request should succeed", 200, response.getStatusCode()); // Test with private API key (should fail for public endpoint) request = new HttpPost(getFullUrl(CONTEXT_URL)); addPrivateTenantAuth(request, tenant, privateKey); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + response = executeContextJSONRequest(request, TEST_SESSION_ID); Assert.assertEquals("Private API key should be accepted for public endpoint to be able to update events and send restricted events", 200, response.getStatusCode()); // Cleanup @@ -928,7 +927,7 @@ private void performPersonalizationWithControlGroup(Map controlG request.setEntity(new StringEntity(getValidatedBundleJSON("personalization-no-control-group.json", null), ContentType.APPLICATION_JSON)); } - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request); + TestUtils.RequestResponse response = executeContextJSONRequest(request); ContextResponse contextResponse = response.getContextResponse(); // Check variants @@ -978,23 +977,23 @@ public void testConcealedProperties() throws Exception { contextRequest.setProfileId(profile.getItemId()); contextRequest.setSessionId(sessionId); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - assertEquals(TestUtils.executeContextJSONRequest(request, sessionId).getContextResponse().getProfileProperties().get("customProperty"), ("concealedValue")); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + assertEquals(executeContextJSONRequest(request, sessionId).getContextResponse().getProfileProperties().get("customProperty"), ("concealedValue")); // set the property as concealed customPropertyType.getMetadata().getSystemTags().add("concealed"); profileService.deletePropertyType(customPropertyType.getItemId()); profileService.setPropertyType(customPropertyType); // Not in all properties - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - assertNull(TestUtils.executeContextJSONRequest(request, sessionId).getContextResponse().getProfileProperties().get("customProperty")); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + assertNull(executeContextJSONRequest(request, sessionId).getContextResponse().getProfileProperties().get("customProperty")); // Got it explicitly contextRequest.setRequiredProfileProperties(Arrays.asList("customProperty")); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - assertEquals(TestUtils.executeContextJSONRequest(request, sessionId).getContextResponse().getProfileProperties().get("customProperty"), ("concealedValue")); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + assertEquals(executeContextJSONRequest(request, sessionId).getContextResponse().getProfileProperties().get("customProperty"), ("concealedValue")); // Got it with all contextRequest.setRequiredProfileProperties(Arrays.asList("*", "customProperty")); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - assertEquals(TestUtils.executeContextJSONRequest(request, sessionId).getContextResponse().getProfileProperties().get("customProperty"), ("concealedValue")); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + assertEquals(executeContextJSONRequest(request, sessionId).getContextResponse().getProfileProperties().get("customProperty"), ("concealedValue")); // remove the concealed tag on the property type customPropertyType.getMetadata().getSystemTags().remove("concealed"); @@ -1003,8 +1002,8 @@ public void testConcealedProperties() throws Exception { // Got it from all properties contextRequest.setRequiredProfileProperties(Arrays.asList("*")); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - assertEquals(TestUtils.executeContextJSONRequest(request, sessionId).getContextResponse().getProfileProperties().get("customProperty"), ("concealedValue")); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + assertEquals(executeContextJSONRequest(request, sessionId).getContextResponse().getProfileProperties().get("customProperty"), ("concealedValue")); } @Test @@ -1020,8 +1019,8 @@ public void testContextRequestWithPublicApiKey() throws Exception { // Send request HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + TestUtils.RequestResponse response = executeContextJSONRequest(request, TEST_SESSION_ID); // Verify response ContextResponse contextResponse = response.getContextResponse(); @@ -1031,8 +1030,8 @@ public void testContextRequestWithPublicApiKey() throws Exception { request = new HttpPost(getFullUrl(CONTEXT_URL)); contextRequest.setPublicApiKey("invalid-key"); request.addHeader(UNOMI_API_KEY_HTTP_HEADER_KEY, "invalid-key"); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID, 401, false); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + response = executeContextJSONRequest(request, TEST_SESSION_ID, 401, false); // Verify error response for invalid key assertEquals("Should receive unauthorized response", 401, response.getStatusCode()); diff --git a/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java b/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java index 571b0ae94..5205ffc94 100644 --- a/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java @@ -24,7 +24,6 @@ import org.apache.unomi.api.rules.Rule; import org.apache.unomi.itests.tools.LogChecker; import org.apache.unomi.api.services.EventService; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -163,7 +162,7 @@ private void initializePropertyTypeWithDifferentSystemTag() throws InterruptedEx } private void createRule(String filename) throws IOException, InterruptedException { - Rule rule = CustomObjectMapper.getObjectMapper().readValue(new File(filename).toURI().toURL(), Rule.class); + Rule rule = getObjectMapper().readValue(new File(filename).toURI().toURL(), Rule.class); createAndWaitForRule(rule); } diff --git a/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java b/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java index 79971ef05..2553c7c59 100644 --- a/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java @@ -104,13 +104,13 @@ public void testEventsCollectorWithPublicApiKey() throws Exception { // Send request with public API key HttpPost request = new HttpPost(getFullUrl(EVENTS_URL)); request.addHeader("Content-Type", "application/json"); - String requestBody = objectMapper.writeValueAsString(eventsCollectorRequest); + String requestBody = getObjectMapper().writeValueAsString(eventsCollectorRequest); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); // Execute request and verify response try (CloseableHttpResponse response = HttpClientThatWaitsForUnomi.doRequest(request, 200)) { String responseContent = EntityUtils.toString(response.getEntity()); - EventCollectorResponse eventResponse = objectMapper.readValue(responseContent, EventCollectorResponse.class); + EventCollectorResponse eventResponse = getObjectMapper().readValue(responseContent, EventCollectorResponse.class); Assert.assertNotNull("Event collector response should not be null", eventResponse); // Check that the response indicates the session and profile were updated diff --git a/itests/src/test/java/org/apache/unomi/itests/GroovyActionsServiceIT.java b/itests/src/test/java/org/apache/unomi/itests/GroovyActionsServiceIT.java index cd889fbd4..7731fa74d 100644 --- a/itests/src/test/java/org/apache/unomi/itests/GroovyActionsServiceIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/GroovyActionsServiceIT.java @@ -23,7 +23,6 @@ import org.apache.unomi.api.Profile; import org.apache.unomi.api.actions.ActionType; import org.apache.unomi.api.rules.Rule; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -74,7 +73,7 @@ private String loadGroovyAction(String pathname) throws IOException { } private void createRule(String filename) throws IOException, InterruptedException { - Rule rule = CustomObjectMapper.getObjectMapper().readValue(new File(filename).toURI().toURL(), Rule.class); + Rule rule = getObjectMapper().readValue(new File(filename).toURI().toURL(), Rule.class); createAndWaitForRule(rule); } diff --git a/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java b/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java index fd15aada7..9fe11829d 100644 --- a/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java @@ -115,7 +115,7 @@ protected T get(final String url, TypeReference typeReference) { final HttpGet httpGet = new HttpGet(getFullUrl(url)); response = executeHttpRequest(httpGet, AuthType.CUSTOM_BASIC, HEALTHCHECK_AUTH_USER_NAME, HEALTHCHECK_AUTH_PASSWORD); if (response.getStatusLine().getStatusCode() == 200 || response.getStatusLine().getStatusCode() == 206) { - return objectMapper.readValue(response.getEntity().getContent(), typeReference); + return getObjectMapper().readValue(response.getEntity().getContent(), typeReference); } else { return null; } diff --git a/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java b/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java index dd9ac69a7..ab7ea7269 100644 --- a/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java @@ -332,7 +332,7 @@ private void assertInvalidClientDataResponse(String responseBody) throws IOExcep if (ERROR_MESSAGE_INVALID_DATA_RECEIVED.equals(responseBody)) { return; } - JsonNode json = objectMapper.readTree(responseBody); + JsonNode json = getObjectMapper().readTree(responseBody); JsonNode errorNode = json.get("errorMessage"); assertNotNull("Response JSON missing 'errorMessage' field. Body: " + responseBody, errorNode); assertEquals("badRequest", errorNode.asText()); diff --git a/itests/src/test/java/org/apache/unomi/itests/LegacyQueryBuilderMappingIT.java b/itests/src/test/java/org/apache/unomi/itests/LegacyQueryBuilderMappingIT.java index 14c001277..7a7597712 100644 --- a/itests/src/test/java/org/apache/unomi/itests/LegacyQueryBuilderMappingIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/LegacyQueryBuilderMappingIT.java @@ -22,7 +22,6 @@ import org.apache.unomi.api.Profile; import org.apache.unomi.api.conditions.Condition; import org.apache.unomi.api.conditions.ConditionType; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -141,7 +140,7 @@ public void testPropertyConditionLegacyMapping() throws IOException { * @throws IOException if the JSON file cannot be read */ private void testLegacyMapping(String jsonFilePath, Map parameters) throws IOException { - ConditionType customConditionType = CustomObjectMapper.getObjectMapper().readValue( + ConditionType customConditionType = getObjectMapper().readValue( new File(jsonFilePath).toURI().toURL(), ConditionType.class); definitionsService.setConditionType(customConditionType); diff --git a/itests/src/test/java/org/apache/unomi/itests/PatchIT.java b/itests/src/test/java/org/apache/unomi/itests/PatchIT.java index 5a068ea4a..71795f0ea 100644 --- a/itests/src/test/java/org/apache/unomi/itests/PatchIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/PatchIT.java @@ -20,7 +20,6 @@ import org.apache.unomi.api.PropertyType; import org.apache.unomi.api.actions.ActionType; import org.apache.unomi.api.conditions.ConditionType; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import java.util.Objects; import org.junit.Assert; import org.junit.Test; @@ -43,7 +42,7 @@ public void testPatch() throws IOException, InterruptedException { PropertyType company = profileService.getPropertyType("company"); try { - Patch patch = CustomObjectMapper.getObjectMapper().readValue(bundleContext.getBundle().getResource("patch1.json"), Patch.class); + Patch patch = getObjectMapper().readValue(bundleContext.getBundle().getResource("patch1.json"), Patch.class); PropertyType newCompany = (PropertyType) patchService.patch(patch); Assert.assertEquals("foo", newCompany.getDefaultValue()); @@ -65,7 +64,7 @@ public void testOverride() throws IOException, InterruptedException { PropertyType gender = profileService.getPropertyType("gender"); try { - Patch patch = CustomObjectMapper.getObjectMapper().readValue(bundleContext.getBundle().getResource("patch2.json"), Patch.class); + Patch patch = getObjectMapper().readValue(bundleContext.getBundle().getResource("patch2.json"), Patch.class); PropertyType newGender = (PropertyType) patchService.patch(patch); Assert.assertEquals("foo", newGender.getDefaultValue()); @@ -91,7 +90,7 @@ public void testRemove() throws IOException, InterruptedException { executionContextManager.executeAsSystem(() -> { Patch patch = null; try { - patch = CustomObjectMapper.getObjectMapper().readValue(bundleContext.getBundle().getResource("patch3.json"), Patch.class); + patch = getObjectMapper().readValue(bundleContext.getBundle().getResource("patch3.json"), Patch.class); } catch (IOException e) { throw new RuntimeException(e); } @@ -130,7 +129,7 @@ public void testPatchOnConditionType() throws IOException, InterruptedException Assert.assertTrue(formCondition.getMetadata().getSystemTags().contains("profileTags")); try { - Patch patch = CustomObjectMapper.getObjectMapper().readValue(bundleContext.getBundle().getResource("patch4.json"), Patch.class); + Patch patch = getObjectMapper().readValue(bundleContext.getBundle().getResource("patch4.json"), Patch.class); patchService.patch(patch); @@ -157,7 +156,7 @@ public void testPatchOnActionType() throws IOException, InterruptedException { Assert.assertTrue(mailAction.getMetadata().getSystemTags().contains("availableToEndUser")); try { - Patch patch = CustomObjectMapper.getObjectMapper().readValue(bundleContext.getBundle().getResource("patch5.json"), Patch.class); + Patch patch = getObjectMapper().readValue(bundleContext.getBundle().getResource("patch5.json"), Patch.class); patchService.patch(patch); diff --git a/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java b/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java index a5754bcbe..80d32f630 100644 --- a/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/PersonaIT.java @@ -25,7 +25,6 @@ import org.apache.unomi.api.PartialList; import org.apache.unomi.api.PersonaSession; import org.apache.unomi.api.PersonaWithSessions; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -101,7 +100,7 @@ public void testSavePersonaWithSessionsAndRetrieveSessions() throws Exception { Assert.assertEquals("Persona creation should return 200 OK", 200, statusCode); String responseBody = EntityUtils.toString(createResponse.getEntity()); - createdPersona = CustomObjectMapper.getObjectMapper().readValue(responseBody, PersonaWithSessions.class); + createdPersona = getObjectMapper().readValue(responseBody, PersonaWithSessions.class); } Assert.assertNotNull("Created persona should not be null", createdPersona); @@ -120,7 +119,7 @@ public void testSavePersonaWithSessionsAndRetrieveSessions() throws Exception { try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl(sessionsUrl)), AuthType.JAAS_ADMIN)) { if (response.getStatusLine().getStatusCode() == 200) { String responseBody = EntityUtils.toString(response.getEntity()); - PartialList result = CustomObjectMapper.getObjectMapper().readValue( + PartialList result = getObjectMapper().readValue( responseBody, new TypeReference>() {}); // Check if the test session is present if (result != null && result.getList() != null && !result.getList().isEmpty()) { diff --git a/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java b/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java index 70a732e15..ad2206a6b 100644 --- a/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java @@ -20,7 +20,6 @@ import org.apache.unomi.api.Event; import org.apache.unomi.api.Profile; import org.apache.unomi.api.rules.Rule; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.apache.unomi.plugins.baseplugin.actions.UpdatePropertiesAction; import org.junit.Assert; import org.junit.Before; @@ -317,7 +316,7 @@ public void testSetPropertyActionDates() throws InterruptedException, IOExceptio dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); // register test rule - Rule rule = CustomObjectMapper.getObjectMapper() + Rule rule = getObjectMapper() .readValue(getValidatedBundleJSON("testSetPropertyActionRule.json", new HashMap<>()), Rule.class); createAndWaitForRule(rule); diff --git a/itests/src/test/java/org/apache/unomi/itests/RestCreateValidationIT.java b/itests/src/test/java/org/apache/unomi/itests/RestCreateValidationIT.java index cff021b3a..e40ac5970 100644 --- a/itests/src/test/java/org/apache/unomi/itests/RestCreateValidationIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/RestCreateValidationIT.java @@ -126,7 +126,7 @@ private void assertBadRequestResponse(String url, String responseBody) throws IO assertNotNull("Expected a response body for 400 on POST " + url, responseBody); JsonNode json; try { - json = objectMapper.readTree(responseBody); + json = getObjectMapper().readTree(responseBody); } catch (Exception e) { fail("Expected JSON in 400 response for POST " + url + " but got: " + responseBody); return; diff --git a/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java b/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java index f59afe50e..5187810a1 100644 --- a/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java @@ -24,7 +24,6 @@ import org.apache.unomi.api.services.EventService; import org.apache.unomi.api.services.RulesService; import org.apache.unomi.api.utils.ConditionBuilder; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -296,7 +295,7 @@ private Event generateViewEvent(Session session, Profile profile) { public void testGetTrackedConditions() throws InterruptedException, IOException { // Add custom condition with parameter try { - ConditionType conditionType = CustomObjectMapper.getObjectMapper().readValue( + ConditionType conditionType = getObjectMapper().readValue( new File("data/tmp/testClickEventCondition.json").toURI().toURL(), ConditionType.class); definitionsService.setConditionType(conditionType); refreshPersistence(Rule.class); diff --git a/itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java b/itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java index f5d98681b..9d0a96973 100644 --- a/itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/SchedulerIT.java @@ -137,7 +137,7 @@ public void testRetryTask() throws Exception { try (CloseableHttpResponse response = post("/cxs/tasks/" + testTaskId + "/retry?resetFailureCount=true", null)) { assertEquals("Response should be OK", 200, response.getStatusLine().getStatusCode()); String responseBody = EntityUtils.toString(response.getEntity()); - ScheduledTask retried = objectMapper.readValue(responseBody, ScheduledTask.class); + ScheduledTask retried = getObjectMapper().readValue(responseBody, ScheduledTask.class); assertNotNull("Task should not be null", retried); assertEquals("Task should be scheduled", ScheduledTask.TaskStatus.SCHEDULED, retried.getStatus()); assertEquals("Failure count should be reset", 0, retried.getFailureCount()); diff --git a/itests/src/test/java/org/apache/unomi/itests/TenantIT.java b/itests/src/test/java/org/apache/unomi/itests/TenantIT.java index bfdf693f1..88df8b429 100644 --- a/itests/src/test/java/org/apache/unomi/itests/TenantIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/TenantIT.java @@ -33,7 +33,6 @@ import org.apache.unomi.api.tenants.ApiKey; import org.apache.unomi.api.tenants.ResourceQuota; import org.apache.unomi.api.tenants.Tenant; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -51,12 +50,9 @@ public class TenantIT extends BaseIT { private static final String REST_ENDPOINT = "/cxs/tenants"; - private CustomObjectMapper objectMapper; @Before public void setUp() throws InterruptedException { - objectMapper = new CustomObjectMapper(); - // Wait for tenant REST endpoint to be available keepTrying("Couldn't find tenant endpoint", () -> { try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl(REST_ENDPOINT)), AuthType.JAAS_ADMIN)) { @@ -78,13 +74,13 @@ public void testRestEndpoint() throws Exception { requestBody.put("properties", properties); HttpPost createRequest = new HttpPost(getFullUrl(REST_ENDPOINT)); - createRequest.setEntity(new StringEntity(objectMapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)); + createRequest.setEntity(new StringEntity(getObjectMapper().writeValueAsString(requestBody), ContentType.APPLICATION_JSON)); String createResponse; Tenant createdTenant; try (CloseableHttpResponse response = executeHttpRequest(createRequest, AuthType.JAAS_ADMIN)) { createResponse = EntityUtils.toString(response.getEntity()); - createdTenant = objectMapper.readValue(createResponse, Tenant.class); + createdTenant = getObjectMapper().readValue(createResponse, Tenant.class); } Assert.assertNotNull("Created tenant should not be null", createdTenant); @@ -99,7 +95,7 @@ public void testRestEndpoint() throws Exception { Tenant retrievedTenant; try (CloseableHttpResponse response = executeHttpRequest(new HttpGet(getFullUrl(REST_ENDPOINT + "/" + createdTenant.getItemId())), AuthType.JAAS_ADMIN)) { getResponse = EntityUtils.toString(response.getEntity()); - retrievedTenant = objectMapper.readValue(getResponse, Tenant.class); + retrievedTenant = getObjectMapper().readValue(getResponse, Tenant.class); } Assert.assertEquals("Retrieved tenant should match created tenant", createdTenant.getItemId(), retrievedTenant.getItemId()); @@ -112,13 +108,13 @@ public void testRestEndpoint() throws Exception { retrievedTenant.setResourceQuota(quota); HttpPut updateRequest = new HttpPut(getFullUrl(REST_ENDPOINT + "/" + retrievedTenant.getItemId())); - updateRequest.setEntity(new StringEntity(objectMapper.writeValueAsString(retrievedTenant), ContentType.APPLICATION_JSON)); + updateRequest.setEntity(new StringEntity(getObjectMapper().writeValueAsString(retrievedTenant), ContentType.APPLICATION_JSON)); String updateResponse; Tenant updatedTenant; try (CloseableHttpResponse response = executeHttpRequest(updateRequest, AuthType.JAAS_ADMIN)) { updateResponse = EntityUtils.toString(response.getEntity()); - updatedTenant = objectMapper.readValue(updateResponse, Tenant.class); + updatedTenant = getObjectMapper().readValue(updateResponse, Tenant.class); } Assert.assertEquals("Tenant name should be updated", "Updated Rest Test Tenant", updatedTenant.getName()); @@ -133,7 +129,7 @@ public void testRestEndpoint() throws Exception { ApiKey newApiKey; try (CloseableHttpResponse response = executeHttpRequest(generateKeyRequest, AuthType.JAAS_ADMIN)) { generateKeyResponse = EntityUtils.toString(response.getEntity()); - newApiKey = objectMapper.readValue(generateKeyResponse, ApiKey.class); + newApiKey = getObjectMapper().readValue(generateKeyResponse, ApiKey.class); } Assert.assertNotNull("New API key should not be null", newApiKey); @@ -197,13 +193,13 @@ public void testTenantEndpointAuthentication() throws Exception { requestBody.put("properties", Collections.emptyMap()); HttpPost createRequest = new HttpPost(getFullUrl(REST_ENDPOINT)); - createRequest.setEntity(new StringEntity(objectMapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)); + createRequest.setEntity(new StringEntity(getObjectMapper().writeValueAsString(requestBody), ContentType.APPLICATION_JSON)); String createResponse; Tenant tenant; try (CloseableHttpResponse response = adminClient.execute(createRequest)) { createResponse = EntityUtils.toString(response.getEntity()); - tenant = objectMapper.readValue(createResponse, Tenant.class); + tenant = getObjectMapper().readValue(createResponse, Tenant.class); } try { diff --git a/itests/src/test/java/org/apache/unomi/itests/TestUtils.java b/itests/src/test/java/org/apache/unomi/itests/TestUtils.java index 5b2aa5c01..71b4300c0 100644 --- a/itests/src/test/java/org/apache/unomi/itests/TestUtils.java +++ b/itests/src/test/java/org/apache/unomi/itests/TestUtils.java @@ -34,7 +34,6 @@ import org.apache.unomi.api.tenants.Tenant; import org.apache.unomi.api.tenants.TenantService; import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.apache.unomi.persistence.spi.PersistenceService; import org.junit.Assert; import org.slf4j.Logger; @@ -61,7 +60,7 @@ public class TestUtils { * @return The deserialized resource object, or null if the response or entity is null * @throws IOException if there is an error reading or parsing the response */ - public static T retrieveResourceFromResponse(HttpResponse response, Class clazz) throws IOException { + public static T retrieveResourceFromResponse(HttpResponse response, Class clazz, ObjectMapper objectMapper) throws IOException { if (response == null) { return null; } @@ -69,7 +68,7 @@ public static T retrieveResourceFromResponse(HttpResponse response, Class return null; } String jsonFromResponse = EntityUtils.toString(response.getEntity()); - ObjectMapper mapper = CustomObjectMapper.getObjectMapper(); + ObjectMapper mapper = objectMapper; try { T value = mapper.readValue(jsonFromResponse, clazz); return value; @@ -89,8 +88,8 @@ public static T retrieveResourceFromResponse(HttpResponse response, Class * @return A RequestResponse object containing the response details * @throws IOException if there is an error executing the request or processing the response */ - public static RequestResponse executeContextJSONRequest(HttpUriRequest request, String sessionId) throws IOException { - return executeContextJSONRequest(request, sessionId, -1, true); + public static RequestResponse executeContextJSONRequest(HttpUriRequest request, String sessionId, ObjectMapper objectMapper) throws IOException { + return executeContextJSONRequest(request, sessionId, -1, true, objectMapper); } /** @@ -101,10 +100,11 @@ public static RequestResponse executeContextJSONRequest(HttpUriRequest request, * @param sessionId The session ID to use for cookie handling, or null if not needed * @param expectedStatusCode The expected status code of the response, or -1 if not needed * @param withAuth Whether to include authentication headers in the request + * @param objectMapper JSON mapper (use {@link BaseIT#getObjectMapper()} from integration tests) * @return A RequestResponse object containing the response details * @throws IOException if there is an error executing the request or processing the response */ - public static RequestResponse executeContextJSONRequest(HttpUriRequest request, String sessionId, int expectedStatusCode, boolean withAuth) throws IOException { + public static RequestResponse executeContextJSONRequest(HttpUriRequest request, String sessionId, int expectedStatusCode, boolean withAuth, ObjectMapper objectMapper) throws IOException { try (CloseableHttpResponse response = HttpClientThatWaitsForUnomi.doRequest(request, expectedStatusCode, withAuth, false)) { // validate mimeType HttpEntity entity = response.getEntity(); @@ -131,7 +131,7 @@ public static RequestResponse executeContextJSONRequest(HttpUriRequest request, ContextResponse contextResponse = null; if (responseCode == 200) { - contextResponse = CustomObjectMapper.getObjectMapper().readValue(responseContent, ContextResponse.class); + contextResponse = objectMapper.readValue(responseContent, ContextResponse.class); } return new RequestResponse(cookieHeader, responseCode, contextResponse); @@ -146,8 +146,8 @@ public static RequestResponse executeContextJSONRequest(HttpUriRequest request, * @return A RequestResponse object containing the response details * @throws IOException if there is an error executing the request or processing the response */ - public static RequestResponse executeContextJSONRequest(HttpPost request) throws IOException { - return executeContextJSONRequest(request, null); + public static RequestResponse executeContextJSONRequest(HttpPost request, String sessionId, ObjectMapper objectMapper) throws IOException { + return executeContextJSONRequest(request, sessionId, -1, true, objectMapper); } private static boolean removeAllItems(DefinitionsService definitionsService, PersistenceService persistenceService, diff --git a/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java b/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java index 691514eff..04b41414d 100644 --- a/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java @@ -201,28 +201,28 @@ private void testV3ModeBehavior() throws Exception { contextRequest.setSessionId(TEST_SESSION_ID); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID, 401, false); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + TestUtils.RequestResponse response = executeContextJSONRequest(request, TEST_SESSION_ID, 401, false); assertEquals("V2-style request should be rejected in V3 mode", 401, response.getStatusCode()); // Test V3-style request with public API key - should work request = new HttpPost(getFullUrl(CONTEXT_URL)); request.addHeader(UNOMI_API_KEY_HEADER, testPublicKey.getKey()); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("V3-style request with public API key should work in V3 mode", 200, response.getStatusCode()); // Test V3-style request with private API key - should work request = new HttpPost(getFullUrl(CONTEXT_URL)); addPrivateTenantAuth(request, testTenant, testPrivateKey); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("V3-style request with private API key should work in V3 mode", 200, response.getStatusCode()); // Test V3-style request with JAAS authentication - should work request = new HttpPost(getFullUrl(CONTEXT_URL)); request.addHeader(UNOMI_TENANT_ID_HEADER, testTenant.getItemId()); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); BasicCredentialsProvider credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("karaf", "karaf")); @@ -252,30 +252,30 @@ private void testV2ModeBehavior() throws Exception { contextRequest.setSessionId(TEST_SESSION_ID); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + TestUtils.RequestResponse response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("V2-style request should work in V2 compatibility mode", 200, response.getStatusCode()); // Test V2-style request with X-Unomi-Peer header (V2 third-party auth) - should work request = new HttpPost(getFullUrl(CONTEXT_URL)); request.addHeader(UNOMI_PEER_HEADER, "670c26d1cc413346c3b2fd9ce65dab41"); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("V2-style request with X-Unomi-Peer should work in V2 compatibility mode", 200, response.getStatusCode()); // Test V3-style request with public API key - in V2 mode, V3 API keys are ignored (request succeeds but no events processed) request = new HttpPost(getFullUrl(CONTEXT_URL)); request.addHeader(UNOMI_API_KEY_HEADER, testPublicKey.getKey()); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("V3-style request with public API key should return 200 in V2 compatibility mode", 200, response.getStatusCode()); assertEquals("V3-style request with public API key should have 0 processed events in V2 mode", 0, response.getContextResponse().getProcessedEvents()); // Test V3-style request with private API key - in V2 mode, V3 API keys are ignored (request succeeds but no events processed) request = new HttpPost(getFullUrl(CONTEXT_URL)); addPrivateTenantAuth(request, testTenant, testPrivateKey); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("V3-style request with private API key should return 200 in V2 compatibility mode", 200, response.getStatusCode()); assertEquals("V3-style request with private API key should have 0 processed events in V2 mode", 0, response.getContextResponse().getProcessedEvents()); @@ -323,24 +323,24 @@ public void testV2CompatibilityModeWithProtectedEvents() throws Exception { contextRequest.setEvents(Arrays.asList(loginEvent)); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID, 200, false); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + TestUtils.RequestResponse response = executeContextJSONRequest(request, TEST_SESSION_ID, 200, false); assertEquals("Protected event without V2 auth should return 200", 200, response.getStatusCode()); assertEquals("Protected event without V2 auth should have 0 processed events", 0, response.getContextResponse().getProcessedEvents()); // Test protected event with V2 third-party authentication - should work request = new HttpPost(getFullUrl(CONTEXT_URL)); request.addHeader(UNOMI_PEER_HEADER, "670c26d1cc413346c3b2fd9ce65dab41"); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("Protected event with V2 auth should work", 200, response.getStatusCode()); assertEquals("Protected event with V2 auth should have 1 processed event", 1, response.getContextResponse().getProcessedEvents()); // Test protected event with empty X-Unomi-Peer header - should be rejected request = new HttpPost(getFullUrl(CONTEXT_URL)); request.addHeader(UNOMI_PEER_HEADER, ""); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("Protected event with empty X-Unomi-Peer should return 200", 200, response.getStatusCode()); assertEquals("Protected event with empty X-Unomi-Peer should have 0 processed events", 0, response.getContextResponse().getProcessedEvents()); @@ -354,7 +354,7 @@ public void testV2CompatibilityModeWithProtectedEvents() throws Exception { request = new HttpPost(getFullUrl(CONTEXT_URL)); request.setEntity(new StringEntity(contextRequestJson, ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("Non-protected event without auth should work in V2 mode", 200, response.getStatusCode()); assertEquals("Non-protected event without auth should have 1 processed event", 1, response.getContextResponse().getProcessedEvents()); } @@ -385,8 +385,8 @@ public void testV2CompatibilityModeDefaultTenant() throws Exception { contextRequest.setSessionId(TEST_SESSION_ID); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + TestUtils.RequestResponse response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("V2-style request should work with BaseIT tenant as default", 200, response.getStatusCode()); } @@ -420,8 +420,8 @@ public void testV2CompatibilityModeConfigurationPersistence() throws Exception { contextRequest.setSessionId(TEST_SESSION_ID); HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); - request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + request.setEntity(new StringEntity(getObjectMapper().writeValueAsString(contextRequest), ContentType.APPLICATION_JSON)); + TestUtils.RequestResponse response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("V2-style request should still work after service update", 200, response.getStatusCode()); } @@ -440,13 +440,13 @@ public void testV2CompatibilityProtectedEventNegativeCases() throws Exception { ContextRequest contextRequest = new ContextRequest(); contextRequest.setSessionId(TEST_SESSION_ID); contextRequest.setEvents(Arrays.asList(loginEvent)); - String requestBody = objectMapper.writeValueAsString(contextRequest); + String requestBody = getObjectMapper().writeValueAsString(contextRequest); // Case 1: protected event with an unknown provider key → rejected (0 processed events) HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL)); request.addHeader(UNOMI_PEER_HEADER, "unknownkey000000000000000000000000"); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); - TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + TestUtils.RequestResponse response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("Protected event with unknown provider key should return 200", 200, response.getStatusCode()); assertEquals("Protected event with unknown provider key should have 0 processed events", 0, response.getContextResponse().getProcessedEvents()); @@ -467,7 +467,7 @@ public void testV2CompatibilityProtectedEventNegativeCases() throws Exception { request = new HttpPost(getFullUrl(CONTEXT_URL)); request.addHeader(UNOMI_PEER_HEADER, testProviderKey); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("Protected event with valid key but wrong source IP should return 200", 200, response.getStatusCode()); assertEquals("Protected event with valid key but wrong source IP should have 0 processed events", 0, response.getContextResponse().getProcessedEvents()); @@ -486,7 +486,7 @@ public void testV2CompatibilityProtectedEventNegativeCases() throws Exception { request = new HttpPost(getFullUrl(CONTEXT_URL)); request.addHeader(UNOMI_PEER_HEADER, limitedKey); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); - response = TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID); + response = executeContextJSONRequest(request, TEST_SESSION_ID); assertEquals("Protected login event with key that only allows updateProperties should return 200", 200, response.getStatusCode()); assertEquals("Protected login event with key that only allows updateProperties should have 0 processed events", 0, response.getContextResponse().getProcessedEvents()); } finally { diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java index 3f7a84361..402c3f549 100644 --- a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java @@ -498,7 +498,7 @@ private void countNumberOfSessionIndices() { } private void getScopeFromEvents(CloseableHttpClient httpClient, String eventIndex) throws IOException { String requestBody = resourceAsString(RESOURCE_MATCH_ALL_LOGIN_EVENT); - JsonNode jsonNode = objectMapper.readTree(HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + "/" + eventIndex + "/_search", requestBody, null)); + JsonNode jsonNode = getObjectMapper().readTree(HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + "/" + eventIndex + "/_search", requestBody, null)); if (jsonNode.has("hits") && jsonNode.get("hits").has("hits") && !jsonNode.get("hits").get("hits").isEmpty()) { jsonNode.get("hits").get("hits").forEach(doc -> { JsonNode event = doc.get("_source"); @@ -522,7 +522,7 @@ private int countItems(CloseableHttpClient httpClient, String index, String requ if (requestBody == null) { requestBody = resourceAsString(RESOURCE_MUST_NOT_MATCH_EVENTTYPE); } - JsonNode jsonNode = objectMapper.readTree(HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + "/" + index + "/_count", requestBody, null)); + JsonNode jsonNode = getObjectMapper().readTree(HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + "/" + index + "/_count", requestBody, null)); return jsonNode.get("count").asInt(); } @@ -577,7 +577,7 @@ private void checkTenantIdsApplied() throws IOException { */ private void checkDocumentsInIndex(String indexName, String expectedTenantId, boolean isSystemIndex) throws IOException { String query = HttpUtils.executeGetRequest(httpClient, getEsBaseUrl() + "/" + indexName + "/_search?size=10", null); - JsonNode jsonNode = objectMapper.readTree(query); + JsonNode jsonNode = getObjectMapper().readTree(query); if (jsonNode.has("hits") && jsonNode.get("hits").has("hits") && !jsonNode.get("hits").get("hits").isEmpty()) { for (JsonNode hit : jsonNode.get("hits").get("hits")) { JsonNode source = hit.get("_source"); @@ -667,7 +667,7 @@ private void checkDefaultTenantCreated() throws Exception { if (defaultTenant == null) { // Check if tenant exists in Elasticsearch directly String query = HttpUtils.executeGetRequest(httpClient, getEsBaseUrl() + "/" + INDEX_PREFIX_CONTEXT + "tenant/_search?q=itemId:" + tenantId, null); - JsonNode jsonNode = objectMapper.readTree(query); + JsonNode jsonNode = getObjectMapper().readTree(query); if (jsonNode.has("hits") && jsonNode.get("hits").has("hits") && !jsonNode.get("hits").get("hits").isEmpty()) { JsonNode tenantDoc = jsonNode.get("hits").get("hits").get(0).get("_source"); Assert.assertEquals("Default tenant should have correct itemId", tenantId, tenantDoc.get("itemId").asText()); @@ -722,8 +722,8 @@ private void checkDefinitionsServiceObjects(String itemType, String itemTypeDesc query.set("query", queryWrapper); query.put("size", 1000); - String response = HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_search", objectMapper.writeValueAsString(query), null); - JsonNode jsonNode = objectMapper.readTree(response); + String response = HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_search", getObjectMapper().writeValueAsString(query), null); + JsonNode jsonNode = getObjectMapper().readTree(response); Set itemIds = new HashSet<>(); if (jsonNode.has("hits") && jsonNode.get("hits").has("hits")) { @@ -839,8 +839,8 @@ private void checkLegacyQueryBuilderMigration() throws Exception { query.set("query", queryWrapper); query.put("size", 1000); - String response = HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_search", objectMapper.writeValueAsString(query), null); - JsonNode jsonNode = objectMapper.readTree(response); + String response = HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_search", getObjectMapper().writeValueAsString(query), null); + JsonNode jsonNode = getObjectMapper().readTree(response); int conditionTypesChecked = 0; int conditionTypesWithLegacyIds = 0; @@ -928,8 +928,8 @@ private void checkRulesAndSegmentsForEmbeddedConditionTypes() throws Exception { query.put("_source", "condition"); String response = HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + "/" + rulesIndex + "/_search", - objectMapper.writeValueAsString(query), null); - JsonNode jsonNode = objectMapper.readTree(response); + getObjectMapper().writeValueAsString(query), null); + JsonNode jsonNode = getObjectMapper().readTree(response); int rulesChecked = 0; int rulesWithEmbeddedConditionTypes = 0; @@ -977,8 +977,8 @@ private void checkRulesAndSegmentsForEmbeddedConditionTypes() throws Exception { query.put("_source", "condition"); String response = HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + "/" + segmentsIndex + "/_search", - objectMapper.writeValueAsString(query), null); - JsonNode jsonNode = objectMapper.readTree(response); + getObjectMapper().writeValueAsString(query), null); + JsonNode jsonNode = getObjectMapper().readTree(response); int segmentsChecked = 0; int segmentsWithEmbeddedConditionTypes = 0; diff --git a/itests/src/test/java/org/apache/unomi/itests/shell/ShellCommandsBaseIT.java b/itests/src/test/java/org/apache/unomi/itests/shell/ShellCommandsBaseIT.java index 9f2288157..78d36ce17 100644 --- a/itests/src/test/java/org/apache/unomi/itests/shell/ShellCommandsBaseIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/shell/ShellCommandsBaseIT.java @@ -16,9 +16,7 @@ */ package org.apache.unomi.itests.shell; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.unomi.itests.BaseIT; -import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,18 +38,6 @@ public abstract class ShellCommandsBaseIT extends BaseIT { protected static final Logger LOGGER = LoggerFactory.getLogger(ShellCommandsBaseIT.class); - /** - * Get ObjectMapper for JSON parsing. - * Uses CustomObjectMapper for consistency with Unomi's JSON handling. - * This ensures proper deserialization of Unomi Item types and maintains - * the same date formatting and configuration as the rest of the system. - * - * Note: This is lazy-initialized to avoid class loading issues before OSGi is ready. - */ - protected ObjectMapper getJsonMapper() { - return CustomObjectMapper.getObjectMapper(); - } - /** * Execute a shell command and capture its output as a string. * @@ -93,10 +79,10 @@ protected Map parseJsonOutput(String output) { int jsonEnd = output.lastIndexOf('}'); if (jsonStart >= 0 && jsonEnd > jsonStart) { String jsonStr = output.substring(jsonStart, jsonEnd + 1); - return (Map) getJsonMapper().readValue(jsonStr, Map.class); + return (Map) getObjectMapper().readValue(jsonStr, Map.class); } // If no JSON found, try parsing the whole output - return (Map) getJsonMapper().readValue(output, Map.class); + return (Map) getObjectMapper().readValue(output, Map.class); } catch (Exception e) { // Don't log here - any logging can be captured by command output stream causing StackOverflow // Just throw exception without logging From 3eef8a9232deb28740aef4c1d14ac8c7cbc67b4d Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sun, 5 Jul 2026 09:00:45 +0200 Subject: [PATCH 7/8] chore(platform): align Jackson, Jetty, and search engine patch versions Pin jackson features to ${jackson.version}, bump ES to 9.4.3 and Jetty to 9.4.58, and sync Docker compose image tags with pom properties. --- docker/README.md | 8 ++++---- docker/src/main/docker/docker-compose-build-es.yml | 2 +- docker/src/main/docker/docker-compose-build-os.yml | 6 +++--- docker/src/main/docker/docker-compose-cluster.yml | 2 +- docker/src/main/docker/docker-compose-es.yml | 2 +- docker/src/main/docker/docker-compose-os.yml | 6 +++--- kar/src/main/feature/feature.xml | 4 ++-- pom.xml | 12 ++++++------ 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docker/README.md b/docker/README.md index 5c862fc5d..8cfa6c048 100644 --- a/docker/README.md +++ b/docker/README.md @@ -46,25 +46,25 @@ If you want to run it without docker-compose you should then make sure you setup For ElasticSearch: ```bash -docker pull docker.elastic.co/elasticsearch/elasticsearch:9.4.2 +docker pull docker.elastic.co/elasticsearch/elasticsearch:9.4.3 docker network create unomi docker run -d --name elasticsearch --net unomi -p 9200:9200 -p 9300:9300 \ -e "discovery.type=single-node" \ -e "xpack.security.enabled=false" \ -e cluster.name=contextElasticSearch \ - docker.elastic.co/elasticsearch/elasticsearch:9.4.2 + docker.elastic.co/elasticsearch/elasticsearch:9.4.3 ``` For OpenSearch: ```bash -docker pull opensearchproject/opensearch:3.9.0 +docker pull opensearchproject/opensearch:3.7.0 docker network create unomi export OPENSEARCH_ADMIN_PASSWORD=enter_your_custom_admin_password_here docker run -d --name opensearch --net unomi -p 9200:9200 -p 9300:9300 \ -e "discovery.type=single-node" \ -e OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} \ - opensearchproject/opensearch:3.9.0 + opensearchproject/opensearch:3.7.0 ``` For Unomi (with ElasticSearch): diff --git a/docker/src/main/docker/docker-compose-build-es.yml b/docker/src/main/docker/docker-compose-build-es.yml index 5cbdc6845..03f265a7b 100644 --- a/docker/src/main/docker/docker-compose-build-es.yml +++ b/docker/src/main/docker/docker-compose-build-es.yml @@ -17,7 +17,7 @@ version: '2.4' services: elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.3 volumes: - unomi-3-elasticsearch-data:/usr/share/elasticsearch/data environment: diff --git a/docker/src/main/docker/docker-compose-build-os.yml b/docker/src/main/docker/docker-compose-build-os.yml index c46d7c933..97d38cfe4 100644 --- a/docker/src/main/docker/docker-compose-build-os.yml +++ b/docker/src/main/docker/docker-compose-build-os.yml @@ -23,7 +23,7 @@ networks: services: opensearch-node1: - image: opensearchproject/opensearch:3.9.0 + image: opensearchproject/opensearch:3.7.0 container_name: opensearch-node1 environment: - cluster.name=opensearch-cluster @@ -51,7 +51,7 @@ services: - opensearch-node1 opensearch-node2: - image: opensearchproject/opensearch:3.9.0 + image: opensearchproject/opensearch:3.7.0 container_name: opensearch-node2 environment: - cluster.name=opensearch-cluster @@ -76,7 +76,7 @@ services: - opensearch-node2 opensearch-dashboards: - image: opensearchproject/opensearch-dashboards:3.9.0 + image: opensearchproject/opensearch-dashboards:3.7.0 container_name: opensearch-dashboards ports: - 5601:5601 diff --git a/docker/src/main/docker/docker-compose-cluster.yml b/docker/src/main/docker/docker-compose-cluster.yml index 49ab47538..75aed4628 100644 --- a/docker/src/main/docker/docker-compose-cluster.yml +++ b/docker/src/main/docker/docker-compose-cluster.yml @@ -17,7 +17,7 @@ version: '2.4' services: elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.3 volumes: - unomi-3-elasticsearch-data:/usr/share/elasticsearch/data environment: diff --git a/docker/src/main/docker/docker-compose-es.yml b/docker/src/main/docker/docker-compose-es.yml index 0e518036a..edfada591 100644 --- a/docker/src/main/docker/docker-compose-es.yml +++ b/docker/src/main/docker/docker-compose-es.yml @@ -23,7 +23,7 @@ networks: services: elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.3 container_name: elasticsearch volumes: - unomi-3-elasticsearch-data:/usr/share/elasticsearch/data diff --git a/docker/src/main/docker/docker-compose-os.yml b/docker/src/main/docker/docker-compose-os.yml index adc8b3a26..579d3e2bc 100644 --- a/docker/src/main/docker/docker-compose-os.yml +++ b/docker/src/main/docker/docker-compose-os.yml @@ -23,7 +23,7 @@ networks: services: opensearch-node1: - image: opensearchproject/opensearch:3.9.0 + image: opensearchproject/opensearch:3.7.0 container_name: opensearch-node1 environment: - cluster.name=opensearch-cluster @@ -51,7 +51,7 @@ services: - opensearch-node1 opensearch-node2: - image: opensearchproject/opensearch:3.9.0 + image: opensearchproject/opensearch:3.7.0 container_name: opensearch-node2 environment: - cluster.name=opensearch-cluster @@ -103,7 +103,7 @@ services: - unomi opensearch-dashboards: - image: opensearchproject/opensearch-dashboards:3.9.0 + image: opensearchproject/opensearch-dashboards:3.7.0 container_name: opensearch-dashboards ports: - 5601:5601 diff --git a/kar/src/main/feature/feature.xml b/kar/src/main/feature/feature.xml index 9d59b3022..9165caf52 100644 --- a/kar/src/main/feature/feature.xml +++ b/kar/src/main/feature/feature.xml @@ -39,8 +39,8 @@ feature spifly shell-compat - jackson - jackson-jaxrs + jackson + jackson-jaxrs mvn:commons-collections/commons-collections/${commons-collections.version} mvn:org.apache.commons/commons-lang3/${commons-lang3.version} mvn:commons-beanutils/commons-beanutils/${commons-beanutils.version} diff --git a/pom.xml b/pom.xml index e32312e1c..abc524a7c 100644 --- a/pom.xml +++ b/pom.xml @@ -64,10 +64,10 @@ ${java.version} 4.4.11 - 9.4.2 - 9.4.2 - 3.9.0 - 3.9.0 + 9.4.3 + 9.4.3 + 3.7.0 + 3.7.0 5.2.1 2.28.14 1.1.0.Final @@ -89,7 +89,7 @@ 1.3 1.2 1.10 - 2.18.2 + 2.18.3 1.10 16.0.1 16.0.1 @@ -125,7 +125,7 @@ 14.0 8.0 1.0 - 9.4.57.v20241219 + 9.4.58.v20250814 2.0.0 2.1.3 1.1 From 414659007d5439e12261f5857c59ce58206b2f96 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sun, 5 Jul 2026 09:21:21 +0200 Subject: [PATCH 8/8] fix(persistence): mark commons-compress imports optional in OpenSearch bundle OpenSearch 3.7 client embeds references to org.apache.commons.compress packages that Karaf does not provide. Match the Elasticsearch bundle and resolve optional imports so unomi-opensearch-core features install in ITs. --- persistence-opensearch/core/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/persistence-opensearch/core/pom.xml b/persistence-opensearch/core/pom.xml index 6c04e1aaa..efff4c998 100644 --- a/persistence-opensearch/core/pom.xml +++ b/persistence-opensearch/core/pom.xml @@ -156,6 +156,8 @@ jdk.net;resolution:=optional, org.apache.avalon.framework.logger;resolution:=optional, org.apache.log;resolution:=optional, + org.apache.commons.compress.compressors;resolution:=optional, + org.apache.commons.compress.utils;resolution:=optional, org.brotli.dec;resolution:=optional, com.aayushatharva.brotli4j.decoder;resolution:=optional, com.aayushatharva.brotli4j.encoder;resolution:=optional,