This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch CAMEL-22719-4.10.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit 4caa2c842f1898084e6ae4a327b8a68003bdb83f 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 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);
