This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 1a9e9980fdcb CAMEL-25658: camel-bonita - the producer uses one JAX-RS 
client and closes it when it stops (#27855)
1a9e9980fdcb is described below

commit 1a9e9980fdcb4e72329d98158871a876708bcd54
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Oct 15 11:17:15 2026 +0530

    CAMEL-25658: camel-bonita - the producer uses one JAX-RS client and closes 
it when it stops (#27855)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../component/bonita/api/BonitaAPIBuilder.java     |  20 +-
 .../bonita/api/filter/BonitaAuthFilter.java        |  35 ++--
 .../bonita/producer/BonitaStartProducer.java       |  35 +++-
 .../bonita/BonitaStartProducerClientTest.java      | 208 +++++++++++++++++++++
 4 files changed, 275 insertions(+), 23 deletions(-)

diff --git 
a/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/api/BonitaAPIBuilder.java
 
b/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/api/BonitaAPIBuilder.java
index f4c740b3b73e..143975a92640 100644
--- 
a/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/api/BonitaAPIBuilder.java
+++ 
b/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/api/BonitaAPIBuilder.java
@@ -31,13 +31,27 @@ public class BonitaAPIBuilder {
 
     }
 
+    /**
+     * Builds an API that sends its requests with a new JAX-RS client. Nothing 
closes that client; a caller that uses
+     * the API for a while should use {@link #build(BonitaAPIConfig, Client)} 
with a client that it closes.
+     */
     public static BonitaAPI build(BonitaAPIConfig bonitaAPIConfig) {
         if (bonitaAPIConfig == null) {
             throw new IllegalArgumentException("bonitaApiConfig is null");
         }
-        ClientBuilder clientBuilder = ClientBuilder.newBuilder();
-        clientBuilder.register(JacksonJsonProvider.class);
-        Client client = clientBuilder.build();
+        return build(bonitaAPIConfig, ClientBuilder.newBuilder().build());
+    }
+
+    /**
+     * Builds an API that sends its requests with the given JAX-RS client. The 
JSON provider and the login filter of the
+     * configuration are registered on the client, and the file inputs of a 
case are uploaded with it too. The client
+     * stays with the caller, which closes it when the API is no longer used.
+     */
+    public static BonitaAPI build(BonitaAPIConfig bonitaAPIConfig, Client 
client) {
+        if (bonitaAPIConfig == null) {
+            throw new IllegalArgumentException("bonitaApiConfig is null");
+        }
+        client.register(JacksonJsonProvider.class);
         client.register(new BonitaAuthFilter(bonitaAPIConfig));
         WebTarget webTarget = 
client.target(bonitaAPIConfig.getBaseBonitaURI()).path("/API/bpm");
         // the file inputs are uploaded with the client of this API, so they 
go to its server with its account
diff --git 
a/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/api/filter/BonitaAuthFilter.java
 
b/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/api/filter/BonitaAuthFilter.java
index 25fef8c7b054..4574a4e0052f 100644
--- 
a/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/api/filter/BonitaAuthFilter.java
+++ 
b/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/api/filter/BonitaAuthFilter.java
@@ -57,23 +57,28 @@ public class BonitaAuthFilter implements 
ClientRequestFilter {
             }
             ClientBuilder clientBuilder = ClientBuilder.newBuilder();
             Client client = clientBuilder.build();
-            WebTarget webTarget = 
client.target(bonitaApiConfig.getBaseBonitaURI()).path("loginservice");
-            MultivaluedMap<String, String> form = new MultivaluedHashMap<>();
-            form.add("username", username);
-            form.add("password", password);
-            form.add("redirect", "false");
-            Response response = 
webTarget.request().accept(MediaType.APPLICATION_FORM_URLENCODED)
-                    .post(Entity.form(form));
-            Map<String, NewCookie> cr = response.getCookies();
-            ArrayList<Object> cookies = new ArrayList<>();
-            for (NewCookie cookie : cr.values()) {
-                if ("X-Bonita-API-Token".equals(cookie.getName())) {
-                    bonitaApiToken = cookie.getValue();
-                    requestContext.getHeaders().add("X-Bonita-API-Token", 
bonitaApiToken);
+            // a client for this login only: it is closed when the cookies of 
the answer are read
+            try {
+                WebTarget webTarget = 
client.target(bonitaApiConfig.getBaseBonitaURI()).path("loginservice");
+                MultivaluedMap<String, String> form = new 
MultivaluedHashMap<>();
+                form.add("username", username);
+                form.add("password", password);
+                form.add("redirect", "false");
+                Response response = 
webTarget.request().accept(MediaType.APPLICATION_FORM_URLENCODED)
+                        .post(Entity.form(form));
+                Map<String, NewCookie> cr = response.getCookies();
+                ArrayList<Object> cookies = new ArrayList<>();
+                for (NewCookie cookie : cr.values()) {
+                    if ("X-Bonita-API-Token".equals(cookie.getName())) {
+                        bonitaApiToken = cookie.getValue();
+                        requestContext.getHeaders().add("X-Bonita-API-Token", 
bonitaApiToken);
+                    }
+                    cookies.add(cookie.toString());
                 }
-                cookies.add(cookie.toString());
+                requestContext.getHeaders().put("Cookie", cookies);
+            } finally {
+                client.close();
             }
-            requestContext.getHeaders().put("Cookie", cookies);
 
         }
     }
diff --git 
a/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/producer/BonitaStartProducer.java
 
b/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/producer/BonitaStartProducer.java
index ed10b5b765bb..34827b503018 100644
--- 
a/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/producer/BonitaStartProducer.java
+++ 
b/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/producer/BonitaStartProducer.java
@@ -18,6 +18,9 @@ package org.apache.camel.component.bonita.producer;
 
 import java.util.Map;
 
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+
 import org.apache.camel.Exchange;
 import org.apache.camel.component.bonita.BonitaConfiguration;
 import org.apache.camel.component.bonita.BonitaEndpoint;
@@ -28,22 +31,44 @@ import 
org.apache.camel.component.bonita.api.util.BonitaAPIConfig;
 
 public class BonitaStartProducer extends BonitaProducer {
 
+    // one JAX-RS client for all the exchanges of this producer, created on 
start and closed on stop
+    private volatile Client client;
+    private volatile BonitaAPI bonitaApi;
+
     public BonitaStartProducer(BonitaEndpoint endpoint, BonitaConfiguration 
configuration) {
         super(endpoint, configuration);
     }
 
     @Override
-    public void process(Exchange exchange) throws Exception {
+    protected void doStart() throws Exception {
+        super.doStart();
         // Setup access type (HTTP on local host)
         String hostname = this.configuration.getHostname();
         String port = this.configuration.getPort();
-        String processName = this.configuration.getProcessName();
         String username = this.configuration.getUsername();
         String password = this.configuration.getPassword();
         BonitaAPIConfig bonitaAPIConfig = new BonitaAPIConfig(hostname, port, 
username, password);
-        BonitaAPI bonitaApi = BonitaAPIBuilder.build(bonitaAPIConfig);
-        ProcessDefinitionResponse processDefinition = 
bonitaApi.getProcessDefinition(processName);
-        bonitaApi.startCase(processDefinition, 
exchange.getIn().getBody(Map.class));
+        client = ClientBuilder.newBuilder().build();
+        bonitaApi = BonitaAPIBuilder.build(bonitaAPIConfig, client);
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        bonitaApi = null;
+        Client toClose = client;
+        client = null;
+        if (toClose != null) {
+            toClose.close();
+        }
+        super.doStop();
+    }
+
+    @Override
+    public void process(Exchange exchange) throws Exception {
+        String processName = this.configuration.getProcessName();
+        BonitaAPI api = bonitaApi;
+        ProcessDefinitionResponse processDefinition = 
api.getProcessDefinition(processName);
+        api.startCase(processDefinition, exchange.getIn().getBody(Map.class));
     }
 
 }
diff --git 
a/components/camel-bonita/src/test/java/org/apache/camel/component/bonita/BonitaStartProducerClientTest.java
 
b/components/camel-bonita/src/test/java/org/apache/camel/component/bonita/BonitaStartProducerClientTest.java
new file mode 100644
index 000000000000..05552976cf02
--- /dev/null
+++ 
b/components/camel-bonita/src/test/java/org/apache/camel/component/bonita/BonitaStartProducerClientTest.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.camel.component.bonita;
+
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.bonita.api.model.FileInput;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Counts the JAX-RS clients that a bonita producer creates and closes: one 
client for all the exchanges of the
+ * producer, closed when the producer stops, and the client of each login 
closed after the login.
+ * <p/>
+ * The clients are seen through {@link ClientBuilder#newBuilder()}, which the 
component calls for each client. The
+ * static mock only records what the real builder builds; it works on the 
thread of the test, which is the thread that
+ * starts the route and sends the exchanges (direct endpoint, synchronous 
requests). Without a file input the requests
+ * of an exchange are the process lookup and the case creation, each after a 
login; with a file input the file is
+ * uploaded in between, after one more login, through the client of the 
producer.
+ */
+class BonitaStartProducerClientTest {
+
+    private static final String LOGIN = "/bonita/loginservice";
+    private static final String PROCESS = "/bonita/API/bpm/process";
+    private static final String UPLOAD = 
"/bonita/portal/resource/process/orders/1.0/API/formFileUpload";
+    private static final String INSTANTIATION = 
"/bonita/API/bpm/process/7/instantiation";
+
+    private final List<Client> clients = new ArrayList<>();
+    private WireMockServer server;
+    private MockedStatic<ClientBuilder> builders;
+    private CamelContext context;
+
+    @BeforeEach
+    void setup() throws Exception {
+        server = new 
WireMockServer(WireMockConfiguration.options().dynamicPort());
+        server.start();
+        server.stubFor(post(urlEqualTo(LOGIN))
+                .willReturn(aResponse().withHeader("Set-Cookie", 
"JSESSIONID=session", "X-Bonita-API-Token=token")));
+        server.stubFor(get(urlPathEqualTo(PROCESS)).withQueryParam("s", 
equalTo("orders"))
+                .willReturn(aResponse().withHeader("Content-Type", 
"application/json")
+                        
.withBody("[{\"id\":\"7\",\"name\":\"orders\",\"version\":\"1.0\"}]")));
+        server.stubFor(post(urlEqualTo(UPLOAD))
+                .willReturn(aResponse().withHeader("Content-Type", 
"text/plain;charset=UTF-8")
+                        
.withBody("{\"filename\":\"report.txt\",\"tempPath\":\"tmp.doc\","
+                                  + "\"contentType\":\"text/plain\"}")));
+        server.stubFor(post(urlEqualTo(INSTANTIATION))
+                .willReturn(aResponse().withHeader("Content-Type", 
"application/json").withBody("{\"caseId\":\"1\"}")));
+
+        builders = Mockito.mockStatic(ClientBuilder.class, 
Mockito.CALLS_REAL_METHODS);
+        builders.when(ClientBuilder::newBuilder).thenAnswer(invocation -> {
+            ClientBuilder builder = Mockito.spy((ClientBuilder) 
invocation.callRealMethod());
+            Mockito.doAnswer(build -> {
+                Client client = (Client) build.callRealMethod();
+                clients.add(client);
+                return client;
+            }).when(builder).build();
+            return builder;
+        });
+
+        context = new DefaultCamelContext();
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").routeId("bonita")
+                        .to("bonita:startCase?hostname=localhost&port=" + 
server.port()
+                            + 
"&processName=orders&username=user&password=pass");
+            }
+        });
+        context.start();
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (context != null) {
+            context.stop();
+        }
+        if (builders != null) {
+            builders.close();
+        }
+        if (server != null) {
+            server.stop();
+        }
+    }
+
+    @Test
+    void oneClientForAllTheExchangesOfTheProducerClosedOnStop() {
+        startCases(3);
+
+        assertEquals(3, 
server.findAll(anyRequestedFor(urlEqualTo(INSTANTIATION))).size(), "cases 
started");
+        assertEquals(1, openClients(), "clients still open after three 
exchanges, of " + clients.size() + " created");
+
+        context.stop();
+
+        assertEquals(0, openClients(), "clients still open after the stop, of 
" + clients.size() + " created");
+    }
+
+    @Test
+    void fileInputIsUploadedWithTheClientOfTheProducer() {
+        startCases(3, true);
+
+        assertEquals(3, 
server.findAll(anyRequestedFor(urlEqualTo(UPLOAD))).size(), "files uploaded");
+        assertEquals(3, 
server.findAll(anyRequestedFor(urlEqualTo(INSTANTIATION))).size(), "cases 
started");
+        assertEquals(1, openClients(), "clients still open after three 
exchanges with a file input, of "
+                                       + clients.size() + " created");
+
+        context.stop();
+
+        assertEquals(0, openClients(), "clients still open after the stop, of 
" + clients.size() + " created");
+    }
+
+    @Test
+    void routeStartedAgainGetsAClientOfItsOwn() throws Exception {
+        startCases(1);
+        context.getRouteController().stopRoute("bonita");
+
+        assertEquals(0, openClients(), "clients still open after the route 
stopped, of " + clients.size() + " created");
+
+        context.getRouteController().startRoute("bonita");
+        startCases(2);
+
+        assertEquals(3, 
server.findAll(anyRequestedFor(urlEqualTo(INSTANTIATION))).size(), "cases 
started");
+        assertEquals(1, openClients(), "clients still open after the route 
started again, of " + clients.size()
+                                       + " created");
+    }
+
+    private void startCases(int count) {
+        startCases(count, false);
+    }
+
+    private void startCases(int count, boolean withFile) {
+        ProducerTemplate template = context.createProducerTemplate();
+        try {
+            for (int i = 0; i < count; i++) {
+                Exchange exchange = template.send("direct:start", e -> 
e.getIn().setBody(inputs(withFile)));
+                assertNull(exchange.getException(), "exception of the 
exchange");
+            }
+        } finally {
+            template.stop();
+        }
+    }
+
+    private static Map<String, Serializable> inputs(boolean withFile) {
+        Map<String, Serializable> inputs = new HashMap<>();
+        inputs.put("amount", 1);
+        if (withFile) {
+            inputs.put("document", new FileInput("report.txt", 
"content".getBytes(StandardCharsets.UTF_8)));
+        }
+        return inputs;
+    }
+
+    private long openClients() {
+        return clients.stream().filter(client -> !isClosed(client)).count();
+    }
+
+    /**
+     * A closed JAX-RS client throws {@link IllegalStateException} from its 
methods.
+     */
+    private static boolean isClosed(Client client) {
+        try {
+            client.getConfiguration();
+            return false;
+        } catch (IllegalStateException e) {
+            return true;
+        }
+    }
+}

Reply via email to