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 37d00fa53bce CAMEL-26200: camel-ldif - bind the connection for every
message, give the result code and DN of a failed change, and an empty body for
an empty body (#29027)
37d00fa53bce is described below
commit 37d00fa53bce70170dae879400cfa736100cca04
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 11:44:54 2026 +0100
CAMEL-26200: camel-ldif - bind the connection for every message, give the
result code and DN of a failed change, and an empty body for an empty body
(#29027)
… the result code and DN of a failed change, and an empty body for an empty
body
The producer closes the LdapConnection after each message. A connection bean
that is not a prototype then reconnected without a bind, so every later
message failed with "modifications require authentication". The producer now
binds a connection that is not authenticated with its configured name and
credentials, and closes the connection and readers in a finally block.
A failed change now gives the LDAP result code and the DN of the entry, as
OpenLDAP sends no message for some failures. An empty or null body gives an
empty String instead of null, and a missing connection bean fails with a
NoSuchBeanException instead of a NullPointerException.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/catalog/components/ldif.json | 4 +-
.../apache/camel/catalog/docs/ldif-component.adoc | 26 ++-
.../org/apache/camel/component/ldif/ldif.json | 4 +-
.../camel-ldif/src/main/docs/ldif-component.adoc | 26 ++-
.../apache/camel/component/ldif/LdifEndpoint.java | 10 +-
.../apache/camel/component/ldif/LdifProducer.java | 82 ++++++---
.../camel/component/ldif/LdifConnectionTest.java | 198 +++++++++++++++++++++
.../apache/camel/component/ldif/LdifModDnTest.java | 4 +
.../apache/camel/component/ldif/LdifRouteIT.java | 36 +++-
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 14 ++
.../builder/endpoint/StaticEndpointBuilders.java | 8 +-
.../endpoint/dsl/LdifEndpointBuilderFactory.java | 8 +-
12 files changed, 367 insertions(+), 53 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ldif.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ldif.json
index 0f9640e447c8..b044f21c6025 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ldif.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ldif.json
@@ -29,10 +29,10 @@
},
"bodyTypes": {
"producerIn": { "carriers": [ "text", "bytes", "stream", "file" ],
"description": "The LDIF text, starting with version: 1, or bytes, a stream or
a file of it. With allowUrlBody, a text that is not LDIF is read as the URL of
the LDIF file; otherwise it fails, as does a Map or a POJO" },
- "producerOut": { "carriers": [ "list", "null" ], "description": "A List of
one status String per LDIF entry: success, or the error message from the
server, which can be empty. No body or an empty body gives null" }
+ "producerOut": { "carriers": [ "list", "text" ], "description": "A List of
one status String per LDIF entry: success, or the LDAP result code, the DN and
the message from the server, such as entryAlreadyExists (68) on
uid=a,dc=example,dc=org. No body or an empty body gives an empty String" }
},
"properties": {
- "ldapConnectionName": { "index": 0, "kind": "path", "displayName": "Ldap
Connection Name", "group": "producer", "label": "", "required": true, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "The
name of the LdapConnection bean to pull from the registry. Note that this must
be of scope prototype to avoid it being shared among threads or using a
connection that has timed out." },
+ "ldapConnectionName": { "index": 0, "kind": "path", "displayName": "Ldap
Connection Name", "group": "producer", "label": "", "required": true, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "The
name of the LdapConnection bean to pull from the registry. Note that this must
be of scope prototype to avoid it being shared among threads or using a
connection that has timed out. The connecti [...]
"lazyStartProducer": { "index": 1, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
"allowUrlBody": { "index": 2, "kind": "parameter", "displayName": "Allow
Url Body", "group": "security", "label": "security", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "security": "insecure:dev", "defaultValue": false,
"description": "Whether to allow a message body that is not LDIF content to be
dereferenced as a URL and fetched. When disabled (default), a body that does
not start with version: 1 is rejecte [...]
}
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ldif-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ldif-component.adoc
index 9b4018358e5d..c18b66fe06f5 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ldif-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ldif-component.adoc
@@ -76,7 +76,17 @@ By default, such a body is rejected.
=== Result
The result is returned in the Out body as a `ArrayList<java.lang.String>`
object.
-This contains either `success` or the error message for each LDIF entry.
+This contains a status for each LDIF entry: either `success`, or for a failed
change the LDAP result code,
+the DN of the entry and the message from the server, such as:
+
+----
+entryAlreadyExists (68) on uid=test1,ou=test,dc=example,dc=org
+noSuchObject (32) on uid=test2,ou=test,dc=example,dc=org: no such entry
+----
+
+The message is left out when the server sends none (OpenLDAP sends none for an
entry that already exists).
+
+An empty or null body is not sent to the server, and the result is an empty
String.
=== LdapConnection
@@ -86,6 +96,10 @@ The URI, `ldif:ldapConnectionName`, references a bean with
the ID,
`prototype` to avoid the connection being shared or picking up a
stale connection.
+The component closes the connection after each message. When the bean is not a
prototype,
+the next message uses the same connection: the component then binds it again,
+with the name and credentials of its `LdapConnectionConfig`, before the
changes are applied.
+
The `LdapConnection` bean may be defined as follows in Spring XML:
[source,xml]
@@ -123,9 +137,9 @@ In YAML, a Groovy script can make the connection:
new
org.apache.directory.ldap.client.api.DefaultLdapConnectionFactory(config).newLdapConnection()
----
-A YAML bean is created once, and the component closes the connection after
each message.
-The YAML route in the examples below therefore binds the connection again
before each message.
-This works when the route processes one message at a time.
+A YAML bean is created once, and the component closes the connection after
each message,
+and binds it again for the next message.
+As the connection is shared, this works when the route processes one message
at a time.
== Examples
@@ -157,10 +171,6 @@ and logs the result:
from:
uri: file:ldif
steps:
- # the component closes the connection after each message, so bind it
again
- - bean:
- ref: ldapConnection
- method: "bind()"
- to:
uri: ldif:ldapConnection
- log:
diff --git
a/components/camel-ldif/src/generated/resources/META-INF/org/apache/camel/component/ldif/ldif.json
b/components/camel-ldif/src/generated/resources/META-INF/org/apache/camel/component/ldif/ldif.json
index 0f9640e447c8..b044f21c6025 100644
---
a/components/camel-ldif/src/generated/resources/META-INF/org/apache/camel/component/ldif/ldif.json
+++
b/components/camel-ldif/src/generated/resources/META-INF/org/apache/camel/component/ldif/ldif.json
@@ -29,10 +29,10 @@
},
"bodyTypes": {
"producerIn": { "carriers": [ "text", "bytes", "stream", "file" ],
"description": "The LDIF text, starting with version: 1, or bytes, a stream or
a file of it. With allowUrlBody, a text that is not LDIF is read as the URL of
the LDIF file; otherwise it fails, as does a Map or a POJO" },
- "producerOut": { "carriers": [ "list", "null" ], "description": "A List of
one status String per LDIF entry: success, or the error message from the
server, which can be empty. No body or an empty body gives null" }
+ "producerOut": { "carriers": [ "list", "text" ], "description": "A List of
one status String per LDIF entry: success, or the LDAP result code, the DN and
the message from the server, such as entryAlreadyExists (68) on
uid=a,dc=example,dc=org. No body or an empty body gives an empty String" }
},
"properties": {
- "ldapConnectionName": { "index": 0, "kind": "path", "displayName": "Ldap
Connection Name", "group": "producer", "label": "", "required": true, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "The
name of the LdapConnection bean to pull from the registry. Note that this must
be of scope prototype to avoid it being shared among threads or using a
connection that has timed out." },
+ "ldapConnectionName": { "index": 0, "kind": "path", "displayName": "Ldap
Connection Name", "group": "producer", "label": "", "required": true, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "The
name of the LdapConnection bean to pull from the registry. Note that this must
be of scope prototype to avoid it being shared among threads or using a
connection that has timed out. The connecti [...]
"lazyStartProducer": { "index": 1, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
"allowUrlBody": { "index": 2, "kind": "parameter", "displayName": "Allow
Url Body", "group": "security", "label": "security", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "security": "insecure:dev", "defaultValue": false,
"description": "Whether to allow a message body that is not LDIF content to be
dereferenced as a URL and fetched. When disabled (default), a body that does
not start with version: 1 is rejecte [...]
}
diff --git a/components/camel-ldif/src/main/docs/ldif-component.adoc
b/components/camel-ldif/src/main/docs/ldif-component.adoc
index 9b4018358e5d..c18b66fe06f5 100644
--- a/components/camel-ldif/src/main/docs/ldif-component.adoc
+++ b/components/camel-ldif/src/main/docs/ldif-component.adoc
@@ -76,7 +76,17 @@ By default, such a body is rejected.
=== Result
The result is returned in the Out body as a `ArrayList<java.lang.String>`
object.
-This contains either `success` or the error message for each LDIF entry.
+This contains a status for each LDIF entry: either `success`, or for a failed
change the LDAP result code,
+the DN of the entry and the message from the server, such as:
+
+----
+entryAlreadyExists (68) on uid=test1,ou=test,dc=example,dc=org
+noSuchObject (32) on uid=test2,ou=test,dc=example,dc=org: no such entry
+----
+
+The message is left out when the server sends none (OpenLDAP sends none for an
entry that already exists).
+
+An empty or null body is not sent to the server, and the result is an empty
String.
=== LdapConnection
@@ -86,6 +96,10 @@ The URI, `ldif:ldapConnectionName`, references a bean with
the ID,
`prototype` to avoid the connection being shared or picking up a
stale connection.
+The component closes the connection after each message. When the bean is not a
prototype,
+the next message uses the same connection: the component then binds it again,
+with the name and credentials of its `LdapConnectionConfig`, before the
changes are applied.
+
The `LdapConnection` bean may be defined as follows in Spring XML:
[source,xml]
@@ -123,9 +137,9 @@ In YAML, a Groovy script can make the connection:
new
org.apache.directory.ldap.client.api.DefaultLdapConnectionFactory(config).newLdapConnection()
----
-A YAML bean is created once, and the component closes the connection after
each message.
-The YAML route in the examples below therefore binds the connection again
before each message.
-This works when the route processes one message at a time.
+A YAML bean is created once, and the component closes the connection after
each message,
+and binds it again for the next message.
+As the connection is shared, this works when the route processes one message
at a time.
== Examples
@@ -157,10 +171,6 @@ and logs the result:
from:
uri: file:ldif
steps:
- # the component closes the connection after each message, so bind it
again
- - bean:
- ref: ldapConnection
- method: "bind()"
- to:
uri: ldif:ldapConnection
- log:
diff --git
a/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifEndpoint.java
b/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifEndpoint.java
index 190174b34fbd..99fcf44b71a9 100644
---
a/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifEndpoint.java
+++
b/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifEndpoint.java
@@ -38,9 +38,10 @@ import org.apache.camel.support.DefaultEndpoint;
description = "The LDIF text, starting with
version: 1, or bytes, a stream or a file of it."
+ " With allowUrlBody, a text
that is not LDIF is read as the URL of the LDIF"
+ " file; otherwise it fails,
as does a Map or a POJO"),
- producerOut = @BodyType(value = { Carrier.LIST, Carrier.NULL },
- description = "A List of one status String
per LDIF entry: success, or the error message from"
- + " the server, which can be
empty. No body or an empty body gives null"))
+ producerOut = @BodyType(value = { Carrier.LIST, Carrier.TEXT },
+ description = "A List of one status String
per LDIF entry: success, or the LDAP result code,"
+ + " the DN and the message
from the server, such as entryAlreadyExists (68) on"
+ + " uid=a,dc=example,dc=org.
No body or an empty body gives an empty String"))
public class LdifEndpoint extends DefaultEndpoint {
@UriPath
@Metadata(required = true)
@@ -69,7 +70,8 @@ public class LdifEndpoint extends DefaultEndpoint {
/**
* The name of the LdapConnection bean to pull from the registry. Note
that this must be of scope "prototype" to
- * avoid it being shared among threads or using a connection that has
timed out.
+ * avoid it being shared among threads or using a connection that has
timed out. The connection is closed after each
+ * message, and bound again with its configured name and credentials when
it is used for the next message.
*/
public void setLdapConnectionName(String ldapConnectionName) {
this.ldapConnectionName = ldapConnectionName;
diff --git
a/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifProducer.java
b/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifProducer.java
index 0476b13821b8..3cc3958a6245 100644
---
a/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifProducer.java
+++
b/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifProducer.java
@@ -28,12 +28,15 @@ import java.util.List;
import org.apache.camel.CamelException;
import org.apache.camel.Exchange;
import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.support.CamelContextHelper;
import org.apache.camel.support.DefaultProducer;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapOperationException;
import org.apache.directory.api.ldap.model.ldif.LdifEntry;
import org.apache.directory.api.ldap.model.ldif.LdifReader;
+import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.ldap.client.api.LdapConnection;
@@ -68,7 +71,7 @@ public class LdifProducer extends DefaultProducer {
@Override
public void process(Exchange exchange) throws Exception {
String body = exchange.getIn().getBody(String.class);
- List<String> result = null;
+ List<String> result;
// Pass through everything
exchange.setMessage(exchange.getIn());
@@ -76,7 +79,10 @@ public class LdifProducer extends DefaultProducer {
// If nothing to do, then return an empty body
if (ObjectHelper.isEmpty(body)) {
exchange.getMessage().setBody("");
- } else if (body.startsWith(LDIF_HEADER)) {
+ return;
+ }
+
+ if (body.startsWith(LDIF_HEADER)) {
LOG.debug("Reading from LDIF body");
result = processLdif(new StringReader(body));
} else if (((LdifEndpoint) getEndpoint()).isAllowUrlBody()) {
@@ -107,33 +113,44 @@ public class LdifProducer extends DefaultProducer {
* @return The created LDAP connection.
*/
protected LdapConnection getLdapConnection() {
- return (LdapConnection)
getEndpoint().getCamelContext().getRegistry().lookupByName(ldapConnectionName);
+ return
CamelContextHelper.mandatoryLookup(getEndpoint().getCamelContext(),
ldapConnectionName, LdapConnection.class);
}
/**
* Process an LDIF file from a reader.
*/
private List<String> processLdif(Reader reader) throws CamelException {
- LdapConnection conn = getLdapConnection();
- LdifReader ldifReader;
-
- List<String> results = new ArrayList<>();
-
- // Create the reader
+ LdapConnection conn = null;
+ LdifReader ldifReader = null;
try {
- ldifReader = new LdifReader(reader);
- } catch (LdapException e) {
- throw new CamelException("Unable to create LDIF reader", e);
- }
-
- // Process each entry
- for (LdifEntry e : ldifReader) {
- results.add(processLdifEntry(conn, e));
- }
+ conn = getLdapConnection();
+ // the connection is closed after each message. A connection bean
that is looked up again (not a prototype)
+ // then reconnects without a bind, so bind it again with its
configured name and credentials
+ if (!conn.isAuthenticated()) {
+ LOG.debug("Binding the LDAP connection {}",
ldapConnectionName);
+ try {
+ conn.bind();
+ } catch (LdapException e) {
+ throw new CamelException("Unable to bind the LDAP
connection " + ldapConnectionName, e);
+ }
+ }
- IOHelper.close(conn, ldifReader, reader);
+ // Create the reader
+ try {
+ ldifReader = new LdifReader(reader);
+ } catch (LdapException e) {
+ throw new CamelException("Unable to create LDIF reader", e);
+ }
- return results;
+ // Process each entry
+ List<String> results = new ArrayList<>();
+ for (LdifEntry e : ldifReader) {
+ results.add(processLdifEntry(conn, e));
+ }
+ return results;
+ } finally {
+ IOHelper.close(conn, ldifReader, reader);
+ }
}
/**
@@ -178,14 +195,33 @@ public class LdifProducer extends DefaultProducer {
return "success";
} catch (LdapException e) {
LOG.debug("failed to apply ldif", e);
- return getRootCause(e);
+ return getFailureStatus(ldifEntry, e);
+ }
+ }
+
+ /**
+ * The status of a failed change: the LDAP result code (when the server
sent one), the DN of the entry, and the
+ * message of the root cause (when there is one). OpenLDAP sends no
message for some failures, such as an entry that
+ * already exists, so the result code tells what went wrong.
+ */
+ private static String getFailureStatus(LdifEntry ldifEntry, LdapException
e) {
+ StringBuilder sb = new StringBuilder();
+ if (e instanceof LdapOperationException ope && ope.getResultCode() !=
null) {
+ ResultCodeEnum code = ope.getResultCode();
+ sb.append(code.getMessage()).append("
(").append(code.getResultCode()).append(") on ");
+ }
+ sb.append(ldifEntry.getDn());
+ String message = getRootCause(e).getMessage();
+ if (ObjectHelper.isNotEmpty(message)) {
+ sb.append(": ").append(message);
}
+ return sb.toString();
}
/**
* Get the root cause of an exception
*/
- private String getRootCause(LdapException e) {
+ private static Throwable getRootCause(LdapException e) {
Throwable oldt;
Throwable thist;
@@ -194,6 +230,6 @@ public class LdifProducer extends DefaultProducer {
oldt = thist;
thist = thist.getCause();
}
- return oldt.getMessage();
+ return oldt;
}
}
diff --git
a/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifConnectionTest.java
b/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifConnectionTest.java
new file mode 100644
index 000000000000..61f91a2fceaa
--- /dev/null
+++
b/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifConnectionTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.ldif;
+
+import java.lang.reflect.Proxy;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.Exchange;
+import org.apache.camel.NoSuchBeanException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import
org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException;
+import org.apache.directory.api.ldap.model.exception.LdapNoSuchObjectException;
+import org.apache.directory.api.ldap.model.exception.LdapOperationException;
+import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.ldap.client.api.LdapConnection;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The producer closes the connection after each message. A connection bean
that is looked up again (not a prototype)
+ * must be bound again for the next message, a failed change gives the LDAP
result code and the DN, and an empty body
+ * gives an empty body. The connection is a fake that behaves like an LDAP
connection to a server that refuses anonymous
+ * changes, so this test needs no LDAP server.
+ */
+class LdifConnectionTest extends CamelTestSupport {
+
+ private final FakeConnection fake = new FakeConnection();
+
+ @BindToRegistry("conn")
+ private final LdapConnection conn = (LdapConnection)
Proxy.newProxyInstance(
+ LdapConnection.class.getClassLoader(), new Class<?>[] {
LdapConnection.class },
+ (proxy, method, args) -> fake.invoke(proxy, method.getName(),
args));
+
+ @Test
+ void everyMessageRunsWithTheBindOfTheConnection() {
+ assertEquals(List.of("success"), send(add("uid=a,dc=example,dc=org")));
+ assertEquals(List.of("success"), send(add("uid=b,dc=example,dc=org")));
+ assertEquals(List.of("success"),
send(delete("uid=a,dc=example,dc=org")));
+
+ assertEquals(Set.of("uid=b,dc=example,dc=org"), fake.entries);
+ assertEquals(3, fake.closed, "the connection is closed after each
message");
+ assertFalse(fake.connected);
+ }
+
+ @Test
+ void failureStatusHasTheResultCodeAndTheDn() {
+ send(add("uid=a,dc=example,dc=org"));
+
+ assertEquals(List.of("entryAlreadyExists (68) on
uid=a,dc=example,dc=org"), send(add("uid=a,dc=example,dc=org")));
+ assertEquals(List.of("noSuchObject (32) on uid=x,dc=example,dc=org: no
such entry"),
+ send(delete("uid=x,dc=example,dc=org")));
+ }
+
+ @Test
+ void emptyBodyGivesEmptyBody() {
+ assertEquals("", template.requestBody("direct:ldif", ""));
+ assertEquals("", template.requestBody("direct:ldif", (Object) null));
+ assertEquals(0, fake.closed, "the connection is not used");
+ }
+
+ @Test
+ void connectionIsClosedWhenTheChangeFails() {
+ Exchange out = template.request("direct:ldif", e ->
e.getIn().setBody(add("uid=boom,dc=example,dc=org")));
+
+ assertInstanceOf(IllegalStateException.class, out.getException());
+ assertEquals(1, fake.closed);
+ }
+
+ @Test
+ void missingConnectionBean() {
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () -> template.requestBody("direct:missing",
add("uid=a,dc=example,dc=org")));
+
+ NoSuchBeanException cause =
assertInstanceOf(NoSuchBeanException.class, e.getCause());
+ assertTrue(cause.getMessage().contains("missing"), cause.getMessage());
+ }
+
+ private List<?> send(String ldif) {
+ return template.requestBody("direct:ldif", ldif, List.class);
+ }
+
+ private static String add(String dn) {
+ return "version: 1\n"
+ + "dn: " + dn + "\n"
+ + "changetype: add\n"
+ + "objectClass: top\n"
+ + "objectClass: person\n"
+ + "cn: test\n"
+ + "sn: test\n";
+ }
+
+ private static String delete(String dn) {
+ return "version: 1\n"
+ + "dn: " + dn + "\n"
+ + "changetype: delete\n";
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:ldif").to("ldif:conn");
+ from("direct:missing").to("ldif:missing");
+ }
+ };
+ }
+
+ /**
+ * Behaves like LdapNetworkConnection to a server that refuses anonymous
changes: the bean starts bound (as made by
+ * DefaultLdapConnectionFactory), close() drops the bind, a change on a
closed connection reconnects without a bind,
+ * and the server sends no message for an entry that already exists.
+ */
+ private static final class FakeConnection {
+ private final Set<String> entries = new HashSet<>();
+ private boolean connected = true;
+ private boolean authenticated = true;
+ private int closed;
+
+ Object invoke(Object proxy, String method, Object[] args) throws
Exception {
+ switch (method) {
+ case "hashCode":
+ return System.identityHashCode(proxy);
+ case "equals":
+ return proxy == args[0];
+ case "toString":
+ return "LdapConnection fake";
+ case "isConnected":
+ return connected;
+ case "isAuthenticated":
+ return connected && authenticated;
+ case "bind":
+ connected = true;
+ authenticated = true;
+ return null;
+ case "close":
+ closed++;
+ connected = false;
+ authenticated = false;
+ return null;
+ case "add":
+ change();
+ String dn = ((Entry) args[0]).getDn().getName();
+ if (dn.startsWith("uid=boom")) {
+ throw new IllegalStateException("boom");
+ }
+ if (!entries.add(dn)) {
+ throw new LdapEntryAlreadyExistsException("");
+ }
+ return null;
+ case "delete":
+ change();
+ if (!entries.remove(((Dn) args[0]).getName())) {
+ throw new LdapNoSuchObjectException("no such entry");
+ }
+ return null;
+ default:
+ throw new UnsupportedOperationException(method);
+ }
+ }
+
+ private void change() throws LdapOperationException {
+ if (!connected) {
+ // the client reconnects on its own, without a bind
+ connected = true;
+ }
+ if (!authenticated) {
+ throw new
LdapOperationException(ResultCodeEnum.STRONG_AUTH_REQUIRED, "modifications
require authentication");
+ }
+ }
+ }
+}
diff --git
a/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifModDnTest.java
b/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifModDnTest.java
index 43ea3dea3346..8eecade0baad 100644
---
a/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifModDnTest.java
+++
b/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifModDnTest.java
@@ -60,6 +60,10 @@ class LdifModDnTest extends CamelTestSupport {
}
case "moveAndRename" ->
renames.add(args[1] + " deleteOldRdn=" + args[2]);
+ case "isAuthenticated" -> {
+ // the connection is bound, so the producer does not
bind it again
+ return Boolean.TRUE;
+ }
case "close" -> {
// the producer closes the connection after the LDIF
is processed
}
diff --git
a/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifRouteIT.java
b/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifRouteIT.java
index 81159d936b64..f0727320cb94 100644
---
a/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifRouteIT.java
+++
b/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifRouteIT.java
@@ -55,6 +55,7 @@ import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -202,7 +203,37 @@ public class LdifRouteIT extends LdifTestSupport {
List<String> ldifResults = defaultLdapModuleOutAssertions(out);
assertThat(ldifResults, notNullValue());
assertThat(ldifResults.size(), equalTo(1));
- assertThat(ldifResults.get(0), not(equalTo("success")));
+ // OpenLDAP sends no message for an entry that already exists, so the
status has the result code and the DN
+ assertThat(ldifResults.get(0), equalTo("entryAlreadyExists (68) on
uid=test3,ou=test,dc=example,dc=org"));
+ }
+
+ @Test
+ public void twoMessagesWithTheSameConnection() throws Exception {
+ camel.addRoutes(createRouteBuilder(ENDPOINT_LDIF));
+ camel.start();
+
+ // the connection bean is a singleton, which the producer closes after
the first message: the second message
+ // must run with the bind of the connection as well
+ URL loc =
this.getClass().getResource("/org/apache/camel/component/ldif/AddDuplicateSetup.ldif");
+ List<?> first = template.requestBody(ENDPOINT_START, readUrl(loc),
List.class);
+ assertThat(first.size(), equalTo(2));
+ first.forEach(result -> assertThat(String.valueOf(result),
+ anyOf(equalTo("success"), startsWith("entryAlreadyExists
(68)"))));
+
+ String ldif = "version: 1\n"
+ + "dn: uid=test8,ou=test,dc=example,dc=org\n"
+ + "changetype: add\n"
+ + "objectClass: top\n"
+ + "objectClass: organizationalPerson\n"
+ + "objectclass: inetOrgPerson\n"
+ + "cn: Test 8\n"
+ + "uid: test8\n"
+ + "sn: 8\n";
+ List<?> second = template.requestBody(ENDPOINT_START, ldif,
List.class);
+ assertThat(second, equalTo(List.of("success")));
+
+ NamingEnumeration<SearchResult> searchResults =
ldapContext.search("dc=example,dc=org", "(uid=test8)", SEARCH_CONTROLS);
+ checkDN("uid=test8", searchResults);
}
@Test
@@ -376,7 +407,8 @@ public class LdifRouteIT extends LdifTestSupport {
Exchange setupOut = setupTemplate.send(endpoint, exchange);
List<String> setupResults = defaultLdapModuleOutAssertions(setupOut);
- setupResults.forEach(result -> assertThat(result,
anyOf(equalTo("success"), equalTo(""))));
+ // the entries of the setup may exist from an earlier test
+ setupResults.forEach(result -> assertThat(result,
anyOf(equalTo("success"), startsWith("entryAlreadyExists (68)"))));
setupCamel.stop();
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 2a70b4f9ea07..5583d4f1d573 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -3325,6 +3325,20 @@ silently can use the `upsert` operation, or handle the
exception, for example wi
The producer also closes the Kudu session of each operation. The sessions
stayed registered with the Kudu client until
the client was closed, one more for every exchange.
+=== camel-ldif - the connection is bound for every message, and the status of
a failed change
+
+The producer closes the `LdapConnection` after each message. A connection bean
that is not a prototype then
+reconnected without a bind for the next message, so its changes failed with
`modifications require authentication`.
+The producer now binds such a connection again, with the name and credentials
of its `LdapConnectionConfig`.
+A route that called `bind()` on the connection before each message can drop
that step.
+
+The status of a failed change is now the LDAP result code, the DN of the entry
and the message from the server, such as
+`entryAlreadyExists (68) on uid=test1,ou=test,dc=example,dc=org`, instead of
only the message (which OpenLDAP leaves
+empty for some failures). A successful change is still `success`.
+
+An empty or null body now gives an empty String as the result, instead of
`null`. A missing connection bean fails with
+a `NoSuchBeanException` instead of a `NullPointerException`.
+
=== camel-mllp - the producer verifies the host name of the server over TLS
When `sslContextParameters` is set (on the endpoint, or with
`useGlobalSslContextParameters` on the component), the
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
index 623b7b0a0a11..cc1a6ef5ae39 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
@@ -11473,7 +11473,9 @@ public class StaticEndpointBuilders {
* Path parameter: ldapConnectionName (required)
* The name of the LdapConnection bean to pull from the registry. Note that
* this must be of scope prototype to avoid it being shared among threads
or
- * using a connection that has timed out.
+ * using a connection that has timed out. The connection is closed after
+ * each message, and bound again with its configured name and credentials
+ * when it is used for the next message.
*
* @param path ldapConnectionName
* @return the dsl builder
@@ -11494,7 +11496,9 @@ public class StaticEndpointBuilders {
* Path parameter: ldapConnectionName (required)
* The name of the LdapConnection bean to pull from the registry. Note that
* this must be of scope prototype to avoid it being shared among threads
or
- * using a connection that has timed out.
+ * using a connection that has timed out. The connection is closed after
+ * each message, and bound again with its configured name and credentials
+ * when it is used for the next message.
*
* @param componentName to use a custom component name for the endpoint
* instead of the default name
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LdifEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LdifEndpointBuilderFactory.java
index 293173ed2898..6e1df2e771e4 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LdifEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LdifEndpointBuilderFactory.java
@@ -158,7 +158,9 @@ public interface LdifEndpointBuilderFactory {
* Path parameter: ldapConnectionName (required)
* The name of the LdapConnection bean to pull from the registry. Note
* that this must be of scope prototype to avoid it being shared among
- * threads or using a connection that has timed out.
+ * threads or using a connection that has timed out. The connection is
+ * closed after each message, and bound again with its configured name
+ * and credentials when it is used for the next message.
*
* @param path ldapConnectionName
* @return the dsl builder
@@ -179,7 +181,9 @@ public interface LdifEndpointBuilderFactory {
* Path parameter: ldapConnectionName (required)
* The name of the LdapConnection bean to pull from the registry. Note
* that this must be of scope prototype to avoid it being shared among
- * threads or using a connection that has timed out.
+ * threads or using a connection that has timed out. The connection is
+ * closed after each message, and bound again with its configured name
+ * and credentials when it is used for the next message.
*
* @param componentName to use a custom component name for the endpoint
* instead of the default name