diff --git a/docker/README.md b/docker/README.md index ae1e4f93b..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.2.1 +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.2.1 + docker.elastic.co/elasticsearch/elasticsearch:9.4.3 ``` For OpenSearch: ```bash -docker pull opensearchproject/opensearch:3.0.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.0.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 0aa221209..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.2.1 + 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 fb2d27d27..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.0.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.0.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.0.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 a088fc082..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.2.1 + 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 db97b6d64..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.2.1 + 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 03abe5263..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.0.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.0.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.0.0 + image: opensearchproject/opensearch-dashboards:3.7.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..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.1.3 + 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 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/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/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 +---- 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 ea9eb8fbf..4e20b8ec3 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 @@ -120,43 +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 - - - junit junit @@ -264,6 +232,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 +267,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..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)); @@ -1073,7 +1057,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 +1110,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 +3170,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/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} diff --git a/pom.xml b/pom.xml index dc7fbdf8c..abc524a7c 100644 --- a/pom.xml +++ b/pom.xml @@ -63,11 +63,11 @@ ${java.version} ${java.version} - 4.4.8 - 9.1.4 - 9.2.1 - 3.0.0 - 3.0.0 + 4.4.11 + 9.4.3 + 9.4.3 + 3.7.0 + 3.7.0 5.2.1 2.28.14 1.1.0.Final @@ -79,7 +79,7 @@ 2.5.2.Final 2.3 3.10 - 2.19.0 + 2.24.0 1.7.36 9.12.2 @@ -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 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"