diff --git a/Jenkinsfile.talend b/Jenkinsfile.talend index 07fe2ad2c4649..d4819623fb861 100644 --- a/Jenkinsfile.talend +++ b/Jenkinsfile.talend @@ -17,8 +17,10 @@ org.apache.camel:camel-drill,\ org.apache.camel:camel-langchain4j-core,\ org.apache.camel:camel-langchain4j-chat,\ org.apache.camel:camel-langchain4j-tools,\ +org.apache.camel:camel-neo4j,\ org.apache.camel:camel-salesforce,\ org.apache.camel:camel-spring,\ +org.apache.camel:camel-tika,\ org.apache.camel:camel-componentdsl,\ org.apache.camel:camel-endpointdsl\ ''' diff --git a/components/camel-ai/camel-neo4j/pom.xml b/components/camel-ai/camel-neo4j/pom.xml index 1116042807a3d..82cbbadf0c419 100644 --- a/components/camel-ai/camel-neo4j/pom.xml +++ b/components/camel-ai/camel-neo4j/pom.xml @@ -30,8 +30,10 @@ jar Camel :: AI :: Neo4j Camel Neo4j support + ${revision} + ${camel-neo4j.tesb.version} true 4 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 c1ac6cb0a871f..0e2f0995174c0 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.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 @@ public class Neo4jProducer extends DefaultProducer { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeReference> MAP_TYPE_REF = new TypeReference<>() { + }; + private Driver driver; public Neo4jProducer(Neo4jEndpoint endpoint) { @@ -104,15 +110,24 @@ private void createNode(Exchange exchange) throws InvalidPayloadException { final String databaseName = getEndpoint().getName(); - var query = ""; - Map properties = null; + // Always use parameterized queries to prevent Cypher injection + var query = String.format("CREATE (%s:%s $props)", alias, label); + Map 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 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 @@ private void retrieveNodes(Exchange exchange) throws NoSuchHeaderException { 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 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 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 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 @@ private void deleteNode(Exchange exchange) throws NoSuchHeaderException { 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 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 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 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 45aa62fb8a22e..3f29848a2dd0b 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: 'alice@example.com', age: 30}"; - var expectedCypherQuery = "CREATE (u1:User {name: 'Alice', email: 'alice@example.com', age: 30})"; + var body = "{\"name\": \"Alice\", \"email\": \"alice@example.com\", \"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 @@ void testCreateMultipleNodesAndRelationshipWithCypherQuery() { 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 @@ void testDeleteNode() { // 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 @@ void testDeleteNode() { 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 @@ void testDeleteNodeWithExistingRelationship() { // 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 @@ void testDeleteNodeWithExistingRelationship() { // 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 @@ void testDeleteNodeWithExistingRelationship() { 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 @@ void testDeleteNodeWithCypherQuery() { 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); diff --git a/components/camel-tika/pom.xml b/components/camel-tika/pom.xml index 4dcb85e891bb0..64ae2d76583f9 100644 --- a/components/camel-tika/pom.xml +++ b/components/camel-tika/pom.xml @@ -30,8 +30,11 @@ jar Camel :: Tika This component integrates with Apache Tika to extract content and metadata from thousands of file types. + ${revision} + ${camel-tika.tesb.version} + 3.2.3 @@ -44,17 +47,17 @@ org.apache.tika tika-core - ${tika-version} + ${tika.tesb.version} org.apache.tika - tika-parser-html-commons - ${tika-version} + tika-handler-boilerpipe + ${tika.tesb.version} org.apache.tika tika-parser-text-module - ${tika-version} + ${tika.tesb.version} diff --git a/parent/pom.xml b/parent/pom.xml index 0b820b5b1bed7..b0d0041d5ae8d 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -1948,7 +1948,7 @@ org.apache.camel camel-neo4j - ${upstream.version} + ${camel-neo4j.tesb.version} org.apache.camel @@ -2483,7 +2483,7 @@ org.apache.camel camel-tika - ${upstream.version} + ${camel-tika.tesb.version} org.apache.camel diff --git a/pom.xml b/pom.xml index 12a2b20c86a8a..ece6173f3efa5 100644 --- a/pom.xml +++ b/pom.xml @@ -104,20 +104,22 @@ 4.10.5 - 4.10.5.20251010 - 4.10.5.20251010 - 4.10.5.20251010 - 4.10.5.20251010 - 4.10.5.20251010 - 4.10.5.20251010 - 4.10.5.20251010 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 4.1.3.1 - 4.10.5.20251010 - 4.10.5.20251010 - 4.10.5.20251010 - 4.10.5.20251010 - 4.10.5.20251010 - 4.10.5.20251010 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 + 4.10.5.20251224 UTF-8 @@ -270,6 +272,8 @@ ${camel-langchain4j-chat.tesb.version} ${camel-langchain4j-tools.tesb.version} ${camel-drill.tesb.version} + ${camel-neo4j.tesb.version} + ${camel-tika.tesb.version} ${camel-componentdsl.tesb.version} ${camel-endpointdsl.tesb.version}