This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch neo4j-body-improvements in repository https://gitbox.apache.org/repos/asf/camel.git
commit 7d07736fbdd02c12c2161880c7a39d50f467ce00 Author: Andrea Cosentino <[email protected]> AuthorDate: Mon Nov 24 10:07:04 2025 +0100 CAMEL-22719 - camel-neo4j - Improve detection of message body 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 e0f0aadfaf96..c380d017a45e 100644 --- a/components/camel-ai/camel-neo4j/pom.xml +++ b/components/camel-ai/camel-neo4j/pom.xml @@ -45,6 +45,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 273cb7c590b2..48ce18fdfd7e 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.Neo4jHeaders.QUERY_RETRIEVE_SIZE; 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 0d90551d20f3..41d3ad8e1dd7 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 @@ -42,8 +42,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) @@ -144,7 +144,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport { void testRetrieveNode() { Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); Assertions.assertNotNull(result); @@ -196,7 +196,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport { // delete node Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); Assertions.assertNotNull(result); @@ -218,7 +218,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); Assertions.assertNotNull(result); @@ -236,7 +236,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(Neo4jHeaders.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); Assertions.assertNotNull(result); @@ -247,7 +247,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(Neo4jHeaders.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); Assertions.assertNotNull(result); Assertions.assertNull(result.getException(), "No exception anymore when deleting relationship at same time"); @@ -270,7 +270,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); Assertions.assertNotNull(result); @@ -312,7 +312,7 @@ public class Neo4jNodeIT extends Neo4jTestSupport { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Bob'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Bob\"}") .request(Exchange.class); Assertions.assertNotNull(result);
