This is an automated email from the ASF dual-hosted git repository.
acosentino pushed a commit to branch camel-4.10.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.10.x by this push:
new 723e2cd98ce4 CAMEL-22719 - camel-neo4j - Improve detection of message
body (#20037)
723e2cd98ce4 is described below
commit 723e2cd98ce4b4ceb1dd38837bc113fca0cef170
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Nov 24 11:19:59 2025 +0100
CAMEL-22719 - camel-neo4j - Improve detection of message body (#20037)
Signed-off-by: Andrea Cosentino <[email protected]>
---
components/camel-ai/camel-neo4j/pom.xml | 4 +
.../camel/component/neo4j/Neo4jProducer.java | 131 +++++++++++++++++----
.../camel/component/neo4j/it/Neo4jNodeIT.java | 18 +--
3 files changed, 124 insertions(+), 29 deletions(-)
diff --git a/components/camel-ai/camel-neo4j/pom.xml
b/components/camel-ai/camel-neo4j/pom.xml
index 2914de8d6407..137d0190e74c 100644
--- a/components/camel-ai/camel-neo4j/pom.xml
+++ b/components/camel-ai/camel-neo4j/pom.xml
@@ -47,6 +47,10 @@
<groupId>org.apache.camel</groupId>
<artifactId>camel-support</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-jackson</artifactId>
+ </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
diff --git
a/components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java
b/components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java
index c1ac6cb0a871..0e2f0995174c 100644
---
a/components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java
+++
b/components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java
@@ -21,6 +21,8 @@ import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.camel.Exchange;
import org.apache.camel.InvalidPayloadException;
import org.apache.camel.Message;
@@ -50,6 +52,10 @@ import static
org.apache.camel.component.neo4j.Neo4jConstants.Headers.QUERY_RETR
public class Neo4jProducer extends DefaultProducer {
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final TypeReference<Map<String, Object>> MAP_TYPE_REF = new
TypeReference<>() {
+ };
+
private Driver driver;
public Neo4jProducer(Neo4jEndpoint endpoint) {
@@ -104,15 +110,24 @@ public class Neo4jProducer extends DefaultProducer {
final String databaseName = getEndpoint().getName();
- var query = "";
- Map<String, Object> properties = null;
+ // Always use parameterized queries to prevent Cypher injection
+ var query = String.format("CREATE (%s:%s $props)", alias, label);
+ Map<String, Object> properties;
if (body instanceof String) {
- // Case we get the object in a Json format
- query = String.format("CREATE (%s:%s %s)", alias, label, body);
+ try {
+ // Convert JSON string to Map for parameterized query
+ Map<String, Object> bodyMap = OBJECT_MAPPER.readValue((String)
body, MAP_TYPE_REF);
+ properties = Map.of("props", bodyMap);
+ } catch (Exception e) {
+ exchange.setException(
+ new Neo4jOperationException(
+ Neo4Operation.CREATE_NODE,
+ new IllegalArgumentException("Failed to parse
body as JSON: " + body, e)));
+ return;
+ }
} else {
- // body should be a list of properties
- query = String.format("CREATE (%s:%s $props)", alias, label);
+ // body should be a Map or similar object
properties = Map.of("props", body);
}
@@ -126,17 +141,55 @@ public class Neo4jProducer extends DefaultProducer {
final String alias = getEndpoint().getConfiguration().getAlias();
ObjectHelper.notNull(alias, "alias");
- String matchQuery = exchange.getMessage().getHeader(MATCH_PROPERTIES,
String.class);
- // in this case we search all nodes
- if (matchQuery == null) {
- matchQuery = "";
- }
+ String matchProperties =
exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class);
final String databaseName = getEndpoint().getName();
- var query = String.format("MATCH (%s:%s %s) RETURN %s", alias, label,
matchQuery, alias);
+ String query;
+ Map<String, Object> queryParams = null;
+
+ if (matchProperties == null || matchProperties.isEmpty()) {
+ // Search all nodes
+ query = String.format("MATCH (%s:%s) RETURN %s", alias, label,
alias);
+ } else {
+ try {
+ // Convert JSON string to Map and build WHERE clause with
parameters
+ Map<String, Object> matchMap =
OBJECT_MAPPER.readValue(matchProperties, MAP_TYPE_REF);
+
+ if (!matchMap.isEmpty()) {
+ StringBuilder whereClause = new StringBuilder();
+ queryParams = new java.util.HashMap<>();
+ int paramIndex = 0;
+
+ for (Map.Entry<String, Object> entry :
matchMap.entrySet()) {
+ if (paramIndex > 0) {
+ whereClause.append(" AND ");
+ }
+ String paramName = "param" + paramIndex;
+
whereClause.append(alias).append(".").append(entry.getKey())
+ .append(" = $").append(paramName);
+ queryParams.put(paramName, entry.getValue());
+ paramIndex++;
+ }
+
+ query = String.format("MATCH (%s:%s) WHERE %s RETURN %s",
+ alias, label, whereClause.toString(), alias);
+ } else {
+ // Empty map, match all nodes
+ query = String.format("MATCH (%s:%s) RETURN %s", alias,
label, alias);
+ }
+ } catch (Exception e) {
+ exchange.setException(
+ new Neo4jOperationException(
+ RETRIEVE_NODES,
+ new IllegalArgumentException(
+ "Failed to parse MATCH_PROPERTIES as
JSON: " + matchProperties,
+ e)));
+ return;
+ }
+ }
- queryRetriveNodes(exchange, databaseName, null, query, RETRIEVE_NODES);
+ queryRetriveNodes(exchange, databaseName, queryParams, query,
RETRIEVE_NODES);
}
private void retrieveNodesWithCypherQuery(Exchange exchange) throws
NoSuchHeaderException {
@@ -184,19 +237,57 @@ public class Neo4jProducer extends DefaultProducer {
final String alias = getEndpoint().getConfiguration().getAlias();
ObjectHelper.notNull(alias, "alias");
- String matchQuery = exchange.getMessage().getHeader(MATCH_PROPERTIES,
String.class);
- // in this case we search all nodes
- if (matchQuery == null) {
- matchQuery = "";
- }
+ String matchProperties =
exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class);
final String databaseName = getEndpoint().getName();
final String detached =
getEndpoint().getConfiguration().isDetachRelationship() ? "DETACH" : "";
- var query = String.format("MATCH (%s:%s %s) %s DELETE %s", alias,
label, matchQuery, detached, alias);
+ String query;
+ Map<String, Object> queryParams = null;
+
+ if (matchProperties == null || matchProperties.isEmpty()) {
+ // Delete all nodes of this label
+ query = String.format("MATCH (%s:%s) %s DELETE %s", alias, label,
detached, alias);
+ } else {
+ try {
+ // Convert JSON string to Map and build WHERE clause with
parameters
+ Map<String, Object> matchMap =
OBJECT_MAPPER.readValue(matchProperties, MAP_TYPE_REF);
+
+ if (!matchMap.isEmpty()) {
+ StringBuilder whereClause = new StringBuilder();
+ queryParams = new java.util.HashMap<>();
+ int paramIndex = 0;
+
+ for (Map.Entry<String, Object> entry :
matchMap.entrySet()) {
+ if (paramIndex > 0) {
+ whereClause.append(" AND ");
+ }
+ String paramName = "param" + paramIndex;
+
whereClause.append(alias).append(".").append(entry.getKey())
+ .append(" = $").append(paramName);
+ queryParams.put(paramName, entry.getValue());
+ paramIndex++;
+ }
+
+ query = String.format("MATCH (%s:%s) WHERE %s %s DELETE
%s",
+ alias, label, whereClause.toString(), detached,
alias);
+ } else {
+ // Empty map, delete all nodes of this label
+ query = String.format("MATCH (%s:%s) %s DELETE %s", alias,
label, detached, alias);
+ }
+ } catch (Exception e) {
+ exchange.setException(
+ new Neo4jOperationException(
+ Neo4Operation.DELETE_NODE,
+ new IllegalArgumentException(
+ "Failed to parse MATCH_PROPERTIES as
JSON: " + matchProperties,
+ e)));
+ return;
+ }
+ }
- executeWriteQuery(exchange, query, null, databaseName,
Neo4Operation.DELETE_NODE);
+ executeWriteQuery(exchange, query, queryParams, databaseName,
Neo4Operation.DELETE_NODE);
}
private void createVectorIndex(Exchange exchange) {
diff --git
a/components/camel-ai/camel-neo4j/src/test/java/org/apache/camel/component/neo4j/it/Neo4jNodeIT.java
b/components/camel-ai/camel-neo4j/src/test/java/org/apache/camel/component/neo4j/it/Neo4jNodeIT.java
index 45aa62fb8a22..3f29848a2dd0 100644
---
a/components/camel-ai/camel-neo4j/src/test/java/org/apache/camel/component/neo4j/it/Neo4jNodeIT.java
+++
b/components/camel-ai/camel-neo4j/src/test/java/org/apache/camel/component/neo4j/it/Neo4jNodeIT.java
@@ -41,8 +41,8 @@ public class Neo4jNodeIT extends Neo4jTestSupport {
@Order(0)
void createNodeWithJsonObject() {
- var body = "{name: 'Alice', email: '[email protected]', age: 30}";
- var expectedCypherQuery = "CREATE (u1:User {name: 'Alice', email:
'[email protected]', age: 30})";
+ var body = "{\"name\": \"Alice\", \"email\": \"[email protected]\",
\"age\": 30}";
+ var expectedCypherQuery = "CREATE (u1:User $props)";
Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u1&label=User")
.withBodyAs(body, String.class)
@@ -141,7 +141,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport {
void testRetrieveNode() {
Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User")
.withHeader(Neo4jConstants.Headers.OPERATION,
Neo4Operation.RETRIEVE_NODES)
- .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name:
'Alice'}")
+ .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES,
"{\"name\": \"Alice\"}")
.request(Exchange.class);
assertNotNull(result);
@@ -193,7 +193,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport {
// delete node
Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User")
.withHeader(Neo4jConstants.Headers.OPERATION,
Neo4Operation.DELETE_NODE)
- .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name:
'Alice'}")
+ .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES,
"{\"name\": \"Alice\"}")
.request(Exchange.class);
assertNotNull(result);
@@ -215,7 +215,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport {
result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User")
.withHeader(Neo4jConstants.Headers.OPERATION,
Neo4Operation.RETRIEVE_NODES)
- .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name:
'Alice'}")
+ .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES,
"{\"name\": \"Alice\"}")
.request(Exchange.class);
assertNotNull(result);
@@ -233,7 +233,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport {
// try to delete user named Diana and this should fail as Diana has a
relationship with Ethan
Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User")
.withHeader(Neo4jConstants.Headers.OPERATION,
Neo4Operation.DELETE_NODE)
- .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name:
'Diana'}")
+ .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES,
"{\"name\": \"Diana\"}")
.request(Exchange.class);
assertNotNull(result);
@@ -245,7 +245,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport {
// delete the Diana by detaching its relationship with Ethan -
detachRelationship=true
result =
fluentTemplate.to("neo4j:neo4j?alias=u&label=User&detachRelationship=true")
.withHeader(Neo4jConstants.Headers.OPERATION,
Neo4Operation.DELETE_NODE)
- .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name:
'Diana'}")
+ .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES,
"{\"name\": \"Diana\"}")
.request(Exchange.class);
assertNotNull(result);
assertNull("No exception anymore when deleting relationship at same
time", result.getException());
@@ -269,7 +269,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport {
result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User")
.withHeader(Neo4jConstants.Headers.OPERATION,
Neo4Operation.RETRIEVE_NODES)
- .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name:
'Diana'}")
+ .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES,
"{\"name\": \"Diana\"}")
.request(Exchange.class);
assertNotNull(result);
@@ -311,7 +311,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport {
result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User")
.withHeader(Neo4jConstants.Headers.OPERATION,
Neo4Operation.RETRIEVE_NODES)
- .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name:
'Bob'}")
+ .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES,
"{\"name\": \"Bob\"}")
.request(Exchange.class);
assertNotNull(result);