diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index b49b05b75..c2a1b1cf6 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -11,6 +11,7 @@ and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Version === Added - Include TCK tests +- Include support to Neo4J === Changed diff --git a/README.adoc b/README.adoc index cc3204997..64e32b15a 100644 --- a/README.adoc +++ b/README.adoc @@ -1830,8 +1830,82 @@ SolrTemplate template; List people = template.solr("age:@age AND type:@type AND _entity:@entity", params); ---- +== Neo4J -=== Graph (Apache Tinkerpop) +image::https://jnosql.github.io/img/logos/neo4j.png[Neo4J Project,align="center",width=25%,height=25%] +https://neo4j.com/[Neo4J] is a highly scalable, native graph database designed to manage complex relationships in data. It enables developers to build applications that leverage the power of graph traversal, pattern matching, and high-performance querying using the **Cypher** query language. + +This API provides support for **Graph** database operations, including entity persistence, query execution via Cypher, and relationship traversal. + +=== How To Install + +You can use either the Maven or Gradle dependencies: + +[source,xml] +---- + + org.eclipse.jnosql.databases + jnosql-neo4j + 1.1.4 + +---- + +=== Configuration + +This API provides the `Neo4JDatabaseConfigurations` class to programmatically establish the credentials. You can configure Neo4J properties using the https://microprofile.io/microprofile-config/[MicroProfile Config] specification. + +[cols="2,4"] +|=== +| Configuration Property | Description + +| `jnosql.neo4j.uri` | The connection URI for the Neo4J database. Example: `bolt://localhost:7687` +| `jnosql.neo4j.username` | The username for authentication. +| `jnosql.neo4j.password` | The password for authentication. +| `jnosql.neo4j.database` | The target database name. +|=== + +==== Example Using MicroProfile Config + +[source,properties] +---- +jnosql.neo4j.uri=bolt://localhost:7687 +jnosql.neo4j.username=neo4j +jnosql.neo4j.password=yourpassword +jnosql.neo4j.database=neo4j +---- + +=== Template API + +The `Neo4JTemplate` interface extends `GraphTemplate` and allows for dynamic Cypher execution. + +[source,java] +---- +@Inject +private Neo4JTemplate template; + +List people = template.cypherQuery("MATCH (p:Person) WHERE p.name = $name RETURN p", params); +var edge = template.edge(otavio, "FRIENDS_WITH", ada); +---- + +=== Repository Support + +The `Neo4JRepository` interface extends the `NoSQLRepository` interface and enables query execution using the `@Cypher` annotation. + +[source,java] +---- +@Repository +interface PersonRepository extends Neo4JRepository { + + @Cypher("MATCH (p:Person) RETURN p") + List findAll(); + + @Cypher("MATCH (p:Person) WHERE p.name = $name RETURN p") + List findByName(@Param("name") String name); +} +---- + + +== Graph (Apache Tinkerpop) Currently, the Jakarta NoSQL doesn't define an API for Graph database types but Eclipse JNoSQL provides a Graph template to explore the specific behavior of this NoSQL type. diff --git a/jnosql-mongodb/src/main/java/org/eclipse/jnosql/databases/mongodb/communication/DocumentQueryConversor.java b/jnosql-mongodb/src/main/java/org/eclipse/jnosql/databases/mongodb/communication/DocumentQueryConversor.java index e64cbf787..21840cc2a 100644 --- a/jnosql-mongodb/src/main/java/org/eclipse/jnosql/databases/mongodb/communication/DocumentQueryConversor.java +++ b/jnosql-mongodb/src/main/java/org/eclipse/jnosql/databases/mongodb/communication/DocumentQueryConversor.java @@ -57,15 +57,15 @@ public static Bson convert(CriteriaCondition condition) { } case LIKE -> Filters.regex(document.name(), Pattern.compile(prepareRegexValue(value.toString()))); case AND -> { - List andList = condition.element().value().get(new TypeReference<>() { + List andConditions = condition.element().value().get(new TypeReference<>() { }); - yield Filters.and(andList.stream() + yield Filters.and(andConditions.stream() .map(DocumentQueryConversor::convert).toList()); } case OR -> { - List orList = condition.element().value().get(new TypeReference<>() { + List orConditions = condition.element().value().get(new TypeReference<>() { }); - yield Filters.or(orList.stream() + yield Filters.or(orConditions.stream() .map(DocumentQueryConversor::convert).toList()); } case BETWEEN -> { diff --git a/jnosql-neo4j/pom.xml b/jnosql-neo4j/pom.xml new file mode 100644 index 000000000..74c6599ef --- /dev/null +++ b/jnosql-neo4j/pom.xml @@ -0,0 +1,56 @@ + + + + + 4.0.0 + + org.eclipse.jnosql.databases + jnosql-databases-parent + 1.1.5-SNAPSHOT + + + jnosql-neo4j + + + 5.27.0 + + + + + org.neo4j.driver + neo4j-java-driver + ${neo4j.driver} + + + ${project.groupId} + jnosql-database-commons + ${project.version} + + + org.eclipse.jnosql.mapping + jnosql-mapping-semistructured + ${project.version} + + + org.testcontainers + neo4j + ${testcontainers.version} + test + + + + diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/DefaultNeo4JDatabaseManager.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/DefaultNeo4JDatabaseManager.java new file mode 100644 index 000000000..1dc0eb51f --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/DefaultNeo4JDatabaseManager.java @@ -0,0 +1,312 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + +import org.eclipse.jnosql.communication.CommunicationException; +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.DeleteQuery; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.communication.semistructured.SelectQuery; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.Values; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Logger; +import java.util.stream.Stream; + +class DefaultNeo4JDatabaseManager implements Neo4JDatabaseManager { + + private static final Logger LOGGER = Logger.getLogger(DefaultNeo4JDatabaseManager.class.getName()); + public static final String ID = "_id"; + + private final Session session; + private final String database; + + public DefaultNeo4JDatabaseManager(Session session, String database) { + this.session = session; + this.database = database; + } + + @Override + public String name() { + return database; + } + + public CommunicationEntity insert(CommunicationEntity entity) { + Objects.requireNonNull(entity, "entity is required"); + return insertEntities(Collections.singletonList(entity)).iterator().next(); + } + + @Override + public Iterable insert(Iterable entities) { + Objects.requireNonNull(entities, "entities is required"); + return insertEntities(entities); + } + + @Override + public Iterable insert(Iterable entities, Duration ttl) { + throw new UnsupportedOperationException("This operation is not supported in Neo4J"); + } + + @Override + public CommunicationEntity insert(CommunicationEntity entity, Duration ttl) { + throw new UnsupportedOperationException("This operation is not supported in Neo4J"); + } + + @Override + public CommunicationEntity update(CommunicationEntity entity) { + Objects.requireNonNull(entity, "entity is required"); + + if (!entity.contains(ID)) { + throw new Neo4JCommunicationException("Cannot update entity without an _id field, entity: " + entity); + } + + Map entityMap = entity.toMap(); + StringBuilder cypher = new StringBuilder("MATCH (e) WHERE elementId(e) = $elementId SET "); + + entityMap.entrySet().stream() + .filter(entry -> !ID.equals(entry.getKey())) + .forEach(entry -> cypher.append("e.").append(entry.getKey()).append(" = $").append(entry.getKey()).append(", ")); + + if (cypher.toString().endsWith(", ")) { + cypher.setLength(cypher.length() - 2); + } + + LOGGER.finest(() -> "Executing Cypher query to update entity: " + cypher); + + try (Transaction tx = session.beginTransaction()) { + var elementId = entity.find(ID) + .orElseThrow(() -> new CommunicationException("Entity must have an ID")) + .get(String.class); + + Map params = new HashMap<>(entityMap); + params.put("elementId", elementId); + + tx.run(cypher.toString(), Values.parameters(flattenMap(params))); + tx.commit(); + } + + LOGGER.fine("Updated entity: " + entity.name() + " with elementId: " + entity.find(ID).orElseThrow().get(String.class)); + return entity; + } + + + @Override + public Iterable update(Iterable entities) { + Objects.requireNonNull(entities, "entities is required"); + entities.forEach(this::update); + return entities; + } + + @Override + public void delete(DeleteQuery query) { + Objects.requireNonNull(query, "query is required"); + Map parameters = new HashMap<>(); + String cypher = Neo4JQueryBuilder.INSTANCE.buildQuery(query, parameters); + + LOGGER.fine("Executing Delete Cypher Query: " + cypher); + try (Transaction tx = session.beginTransaction()) { + tx.run(cypher, Values.parameters(flattenMap(parameters))); + tx.commit(); + } + } + + @Override + public Stream select(SelectQuery query) { + Objects.requireNonNull(query, "query is required"); + Map parameters = new HashMap<>(); + String cypher = Neo4JQueryBuilder.INSTANCE.buildQuery(query, parameters); + + LOGGER.fine("Executing Cypher Query for select entities: " + cypher); + try (Transaction tx = session.beginTransaction()) { + return tx.run(cypher, Values.parameters(flattenMap(parameters))) + .list(record -> extractEntity(query.name(), record, query.columns().isEmpty())) + .stream(); + } + } + + @Override + public long count(String entity) { + Objects.requireNonNull(entity, "entity is required"); + try (Transaction tx = session.beginTransaction()) { + String cypher = "MATCH (e:" + entity + ") RETURN count(e) AS count"; + LOGGER.fine("Executing Cypher Query for counting: " + cypher); + long count = tx.run(cypher).single().get("count").asLong(); + tx.commit(); + return count; + } catch (Exception e) { + LOGGER.severe("Error executing count query: " + e.getMessage()); + throw new CommunicationException("Error executing count query", e); + } + } + + @Override + public Stream executeQuery(String cypher, Map parameters) { + Objects.requireNonNull(cypher, "Cypher query is required"); + Objects.requireNonNull(parameters, "Parameters map is required"); + + try (Transaction tx = session.beginTransaction()) { + Stream result = tx.run(cypher, Values.parameters(flattenMap(parameters))) + .list(record -> extractEntity("QueryResult", record, false)) + .stream(); + LOGGER.fine("Executed Cypher query: " + cypher); + tx.commit(); + return result; + } catch (Exception e) { + throw new CommunicationException("Error executing Cypher query", e); + } + } + + @Override + public Stream traverse(String startNodeId, String relationship, int depth) { + Objects.requireNonNull(startNodeId, "Start node ID is required"); + Objects.requireNonNull(relationship, "Relationship type is required"); + + String cypher = "MATCH (startNode) WHERE elementId(startNode) = $elementId " + + "MATCH (startNode)-[r:" + relationship + "*1.." + depth + "]-(endNode) " + + "RETURN endNode"; + + try (Transaction tx = session.beginTransaction()) { + Stream result = tx.run(cypher, Values.parameters("elementId", startNodeId)) + .list(record -> extractEntity("TraversalResult", record, false)) + .stream(); + LOGGER.fine("Executed traversal query: " + cypher); + tx.commit(); + return result; + } + } + + @Override + public void edge(CommunicationEntity source, String relationshipType, CommunicationEntity target) { + Objects.requireNonNull(source, "Source entity is required"); + Objects.requireNonNull(target, "Target entity is required"); + Objects.requireNonNull(relationshipType, "Relationship type is required"); + + String cypher = "MATCH (s) WHERE elementId(s) = $sourceElementId " + + "MATCH (t) WHERE elementId(t) = $targetElementId " + + "CREATE (s)-[r:" + relationshipType + "]->(t)"; + + try (Transaction tx = session.beginTransaction()) { + var sourceId = source.find(ID).orElseThrow(() -> + new EdgeCommunicationException("The source entity should have the " + ID + " property")).get(); + var targetId = target.find(ID).orElseThrow(() -> + new EdgeCommunicationException("The target entity should have the " + ID + " property")).get(); + + tx.run(cypher, Values.parameters( + "sourceElementId", sourceId, + "targetElementId", targetId + )); + + LOGGER.fine("Created edge: " + cypher); + tx.commit(); + } + } + + @Override + public void remove(CommunicationEntity source, String relationshipType, CommunicationEntity target) { + Objects.requireNonNull(source, "Source entity is required"); + Objects.requireNonNull(target, "Target entity is required"); + Objects.requireNonNull(relationshipType, "Relationship type is required"); + + String cypher = "MATCH (s) WHERE elementId(s) = $sourceElementId " + + "MATCH (t) WHERE elementId(t) = $targetElementId " + + "MATCH (s)-[r:" + relationshipType + "]-(t) DELETE r"; + + var sourceId = source.find(ID).orElseThrow(() -> + new EdgeCommunicationException("The source entity should have the " + ID + " property")).get(); + var targetId = target.find(ID).orElseThrow(() -> + new EdgeCommunicationException("The target entity should have the " + ID + " property")).get(); + + try (Transaction tx = session.beginTransaction()) { + tx.run(cypher, Values.parameters( + "sourceElementId", sourceId, + "targetElementId", targetId + )); + LOGGER.fine("Removed edge: " + cypher); + tx.commit(); + } + } + + + + @Override + public void close() { + LOGGER.fine("Closing the Neo4J session, the database name is: " + database); + this.session.close(); + } + + private Object[] flattenMap(Map map) { + return map.entrySet().stream() + .flatMap(entry -> java.util.stream.Stream.of(entry.getKey(), entry.getValue())) + .toArray(); + } + + private Iterable insertEntities(Iterable entities) { + List entitiesResult = new ArrayList<>(); + try (Transaction tx = session.beginTransaction()) { + for (CommunicationEntity entity : entities) { + + Map properties = entity.toMap(); + StringBuilder cypher = new StringBuilder("CREATE (e:"); + cypher.append(entity.name()).append(" {"); + + properties.keySet().forEach(key -> cypher.append(key).append(": $").append(key).append(", ")); + + if (!properties.isEmpty()) { + cypher.setLength(cypher.length() - 2); + } + cypher.append("}) RETURN e"); + LOGGER.fine("Executing Cypher Query to insert entities: " + cypher); + + var result = tx.run(cypher.toString(), Values.parameters(flattenMap(properties))); + var record = result.hasNext() ? result.next() : null; + var insertedNode = record.get("e").asNode(); + entity.add(ID, insertedNode.elementId()); + entitiesResult.add(entity); + } + tx.commit(); + } + LOGGER.fine("Inserted entities: " + entitiesResult.size()); + return entitiesResult; + } + + private CommunicationEntity extractEntity(String entityName, org.neo4j.driver.Record record, boolean isFullNode) { + List elements = new ArrayList<>(); + + for (String key : record.keys()) { + var value = record.get(key); + + if (value.hasType(org.neo4j.driver.types.TypeSystem.getDefault().NODE())) { + var node = value.asNode(); + node.asMap().forEach((k, v) -> elements.add(Element.of(k, v))); // Extract properties + elements.add(Element.of(ID, node.elementId())); + } else { + String fieldName = key.contains(".") ? key.substring(key.indexOf('.') + 1) : key; + elements.add(Element.of(fieldName, value.asObject())); + } + } + + return CommunicationEntity.of(entityName, elements); + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/EdgeCommunicationException.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/EdgeCommunicationException.java new file mode 100644 index 000000000..e298f116a --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/EdgeCommunicationException.java @@ -0,0 +1,50 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + +/** + * Exception thrown when an issue occurs with edge (relationship) operations in Neo4j. + * This exception is raised in cases where an edge cannot be created or removed + * due to missing nodes, constraint violations, or other graph-related inconsistencies. + * + *
    + *
  • Attempting to create an edge where either the source or target entity does not exist.
  • + *
  • Removing an edge that is not found in the graph.
  • + *
  • Trying to enforce an invalid relationship constraint.
  • + *
+ */ +public class EdgeCommunicationException extends Neo4JCommunicationException{ + + /** + * Constructs a new exception with the specified detail message. + * + * @param message the detail message + */ + public EdgeCommunicationException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message the detail message + * @param exception the cause + */ + public EdgeCommunicationException(String message, Throwable exception) { + super(message, exception); + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JCommunicationException.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JCommunicationException.java new file mode 100644 index 000000000..a1eef8d2f --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JCommunicationException.java @@ -0,0 +1,53 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + +import org.eclipse.jnosql.communication.CommunicationException; + +/** + * Exception representing general communication errors when interacting with a Neo4j database. + * This exception is used as a base for more specific exceptions related to Neo4j operations, + * such as Cypher syntax errors, transaction failures, or connectivity issues. + * + *
    + *
  • Invalid Cypher query syntax.
  • + *
  • Transaction failures during read or write operations.
  • + *
  • Connection timeouts or authentication failures.
  • + *
  • Inconsistent data constraints within the graph.
  • + *
+ */ +public class Neo4JCommunicationException extends CommunicationException { + + /** + * Constructs a new exception with the specified detail message. + * + * @param message the detail message + */ + public Neo4JCommunicationException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message the detail message + * @param exception the cause + */ + public Neo4JCommunicationException(String message, Throwable exception) { + super(message, exception); + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JConfiguration.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JConfiguration.java new file mode 100644 index 000000000..7385820e0 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JConfiguration.java @@ -0,0 +1,71 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + +import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.communication.semistructured.DatabaseConfiguration; + +import java.util.logging.Logger; + +/** + * Class Neo4JConfiguration + * This class provides the configuration for the Neo4j database using the Eclipse jNoSQL framework. + * It implements the {@link DatabaseConfiguration} interface to set up a connection to a Neo4j database. + * + *

The configuration retrieves the following settings from the provided {@link Settings}: + *

    + *
  • {@link Neo4JConfigurations#URI}: The connection URI for the Neo4j database (e.g., "bolt://localhost:7687").
  • + *
  • {@link Neo4JConfigurations#USERNAME}: The username for authentication (optional).
  • + *
  • {@link Neo4JConfigurations#PASSWORD}: The password for authentication (optional).
  • + *
+ * + *

If no URI is provided, a default URI ("bolt://localhost:7687") is used. + * + *

Usage example: + *

+ * Settings settings = Settings.builder()
+ *         .put(Neo4JConfigurations.URI, "bolt://custom-host:7687")
+ *         .put(Neo4JConfigurations.USERNAME, "neo4j")
+ *         .put(Neo4JConfigurations.PASSWORD, "password123")
+ *         .build();
+ *
+ * Neo4JConfiguration configuration = new Neo4JConfiguration();
+ * Neo4JDatabaseManagerFactory factory = configuration.apply(settings);
+ * 
+ */ +public final class Neo4JConfiguration implements DatabaseConfiguration { + + private static final Logger LOGGER = Logger.getLogger(Neo4JConfiguration.class.getName()); + + private static final String DEFAULT_BOLT = "bolt://localhost:7687"; + + /** + * Applies the provided settings to the Neo4j database configuration. + * + * @param settings the settings to apply + * @return a new {@link Neo4JDatabaseManagerFactory} instance + */ + @Override + public Neo4JDatabaseManagerFactory apply(Settings settings) { + var uri = settings.getOrDefault(Neo4JConfigurations.URI, DEFAULT_BOLT); + var user = settings.get(Neo4JConfigurations.USERNAME, String.class).orElse(null); + var password = settings.get(Neo4JConfigurations.PASSWORD, String.class).orElse(null); + LOGGER.info("Starting configuration to Neo4J database, the uri: " + uri); + var neo4Property = new Neo4Property(uri, user, password); + return Neo4JDatabaseManagerFactory.of(neo4Property); + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JConfigurations.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JConfigurations.java new file mode 100644 index 000000000..a4ee35362 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JConfigurations.java @@ -0,0 +1,82 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + +import java.util.function.Supplier; + +/** + * Enum Neo4JConfigurations + * This enum defines the configuration keys used in the Eclipse jNoSQL driver for Neo4j. + * Each configuration key is associated with a specific property required to connect to a Neo4j database. + * + *
    + *
  • URI: The connection URI for the Neo4j database (e.g., bolt://localhost:7687).
  • + *
  • USERNAME: The username used for authentication (e.g., "neo4j").
  • + *
  • PASSWORD: The password used for authentication (e.g., "password123").
  • + *
+ *

+ * This enum implements the Supplier interface, providing a method to retrieve the configuration key as a string. + * Usage example: + *

+ * String uri = Neo4JConfigurations.URI.get(); // Returns "jnosql.neo4j.uri"
+ * String username = Neo4JConfigurations.USERNAME.get(); // Returns "jnosql.neo4j.username"
+ * String password = Neo4JConfigurations.PASSWORD.get(); // Returns "jnosql.neo4j.password"
+ *
+ * // Example of configuring a connection:
+ * Settings settings = Settings.builder().put(Neo4JConfigurations.URI, "bolt://localhost:7687")
+ * .put(Neo4JConfigurations.USERNAME, "neo4j")
+ * .put(Neo4JConfigurations.PASSWORD, "password123")
+ * .build();
+ * 
+ */ +public enum Neo4JConfigurations implements Supplier { + + /** + * The URI of the Neo4j database. + * Example: bolt://localhost:7687 + */ + URI("jnosql.neo4j.uri"), + + /** + * The username for authentication. + * Example: "neo4j" + */ + USERNAME("jnosql.neo4j.username"), + + /** + * The password for authentication. + * Example: "password123" + */ + PASSWORD("jnosql.neo4j.password"), + + /** + * The database name. + * Example: "library" + */ + DATABASE("jnosql.neo4j.database"); + + private final String value; + + Neo4JConfigurations(String value) { + this.value = value; + } + + @Override + public String get() { + return value; + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManager.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManager.java new file mode 100644 index 000000000..493dc1bcd --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManager.java @@ -0,0 +1,95 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.DatabaseManager; + +import java.util.Map; +import java.util.stream.Stream; + +/** + * This specialization of {@link DatabaseManager} is designed for Neo4j databases. + * + *

Neo4j does not natively support Time-To-Live (TTL) operations, but TTL can be managed + * using the APOC library. Implementations of this interface should handle TTL-related methods + * accordingly—either by integrating APOC's TTL features or throwing {@link UnsupportedOperationException} + * if TTL is not supported.

+ *

+ * All write operations, including {@code insert} and {@code update}, will be executed within a transaction. + * When performing batch inserts using an iterable, the entire operation will be executed as a single transaction + * to ensure consistency. + * + *

+ * Neo4JDatabaseManager manager = ...; // Obtain an instance
+ * CommunicationEntity entity = CommunicationEntity.of("User");
+ * entity.add("name", "Alice");
+ * entity.add("age", 30);
+ *
+ * manager.insert(entity); // Insert into Neo4j
+ * 
+ *

+ * Ensure proper transaction and session management when implementing this interface. + * Unsupported TTL operations should result in an {@link UnsupportedOperationException}. + * + * @see DatabaseManager + */ +public interface Neo4JDatabaseManager extends DatabaseManager { + + /** + * Executes a custom Cypher query with parameters and returns a stream of {@link CommunicationEntity}. + * + * @param cypher the Cypher query to execute. + * @param parameters the parameters to bind to the query. + * @return a stream of {@link CommunicationEntity} representing the query result. + * @throws NullPointerException if {@code cypher} or {@code parameters} is null. + */ + Stream executeQuery(String cypher, Map parameters); + + /** + * Traverses the graph starting from a node and follows the specified relationship type up to a given depth. + * + * @param startNodeId the ID of the starting node. + * @param relationship the type of relationship to traverse. + * @param depth the traversal depth limit. + * @return a stream of {@link CommunicationEntity} representing related nodes. + * @throws NullPointerException if {@code startNodeId}, {@code relationship}, or {@code depth} is null. + */ + Stream traverse(String startNodeId, String relationship, int depth); + + /** + * Creates a relationship (edge) between two {@link CommunicationEntity} nodes. + * + * @param source the source entity. + * @param target the target entity. + * @param relationshipType the type of relationship to create. + * @throws EdgeCommunicationException if either the source or target entity does not exist in the database. + * @throws NullPointerException if {@code source}, {@code target}, or {@code relationshipType} is null. + */ + void edge(CommunicationEntity source, String relationshipType, CommunicationEntity target); + + /** + * Removes an existing relationship (edge) between two {@link CommunicationEntity} nodes. + * + * @param source the source entity, which must already exist in the database. + * @param target the target entity, which must already exist in the database. + * @param relationshipType the type of relationship to remove. + * @throws EdgeCommunicationException if either the source or target entity does not exist in the database. + * @throws NullPointerException if {@code source}, {@code target}, or {@code relationshipType} is null. + */ + void remove(CommunicationEntity source, String relationshipType, CommunicationEntity target); +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManagerFactory.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManagerFactory.java new file mode 100644 index 000000000..87b3a94dd --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManagerFactory.java @@ -0,0 +1,114 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + +import org.eclipse.jnosql.communication.CommunicationException; +import org.eclipse.jnosql.communication.semistructured.DatabaseManagerFactory; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Result; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; + +import java.util.Objects; +import java.util.logging.Logger; + +/** + * This class provides a factory for creating and managing instances of {@link DefaultNeo4JDatabaseManager}, + * enabling communication with a Neo4j database. It implements the {@link DatabaseManagerFactory} interface + * and handles the lifecycle of the Neo4j {@link Driver}. + * + *

Main Responsibilities:

+ *
    + *
  • Establishing a connection to a Neo4j database using the provided URI, username, and password.
  • + *
  • Creating new instances of {@link DefaultNeo4JDatabaseManager} for a specific database.
  • + *
  • Managing the lifecycle of the Neo4j driver, including proper resource cleanup.
  • + *
+ *>Thread Safety: + * The factory is thread-safe as long as it is used properly. Ensure to close the factory + * when it is no longer needed to release resources. + * At the close method, the factory closes the Neo4j {@link Driver} and releases resources. + */ +public class Neo4JDatabaseManagerFactory implements DatabaseManagerFactory { + + private static final Logger LOGGER = Logger.getLogger(Neo4JDatabaseManagerFactory.class.getName()); + + private final Driver driver; + + private Neo4JDatabaseManagerFactory(Driver driver) { + this.driver = driver; + } + + /** + * Closes the Neo4j driver and releases resources. + */ + @Override + public void close() { + LOGGER.info("Closing the Neo4J driver"); + this.driver.close(); + } + + @Override + public DefaultNeo4JDatabaseManager apply(String database) { + Objects.requireNonNull(database, "database is required"); + LOGGER.fine(() -> "Creating a new instance of Neo4JDatabaseManager with the database: " + database); + boolean databaseExists = databaseExists(database); + LOGGER.fine(() -> "Database " + database + " exists: " + databaseExists); + if (!databaseExists) { + LOGGER.fine(() -> "Database " + database + " does not exist. Creating it..."); + createDatabase(database); + } + var session = driver.session(SessionConfig.builder().withDatabase(database).build()); + return new DefaultNeo4JDatabaseManager(session, database); + } + + static Neo4JDatabaseManagerFactory of(Neo4Property property) { + Objects.requireNonNull(property, "property is required"); + LOGGER.fine(() -> "Creating a new instance of Neo4JDatabaseManagerFactory with the uri: " + property.uri()); + if(property.user() == null && property.password() == null) { + LOGGER.fine("Creating a new instance of Neo4JDatabaseManagerFactory without authentication"); + return new Neo4JDatabaseManagerFactory(GraphDatabase.driver(property.uri())); + } + var basic = AuthTokens.basic(property.user(), property.password()); + return new Neo4JDatabaseManagerFactory(GraphDatabase.driver(property.uri(), basic)); + } + + private boolean databaseExists(String database) { + try (Session session = driver.session()) { + Result result = session.run("SHOW DATABASES"); + while (result.hasNext()) { + if (database.equals(result.next().get("name").asString())) { + return true; + } + } + } catch (Exception e) { + LOGGER.severe("Failed to check database existence: " + e.getMessage()); + } + return false; + } + + private void createDatabase(String database) { + try (Session session = driver.session()) { + session.run("CREATE DATABASE " + database); + LOGGER.info("Database created: " + database); + } catch (Exception e) { + LOGGER.severe("Failed to create database: " + e.getMessage()); + throw new CommunicationException("Could not create database: " + database, e); + } + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JQueryBuilder.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JQueryBuilder.java new file mode 100644 index 000000000..195d12c6d --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JQueryBuilder.java @@ -0,0 +1,163 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + + +import org.eclipse.jnosql.communication.CommunicationException; +import org.eclipse.jnosql.communication.Condition; +import org.eclipse.jnosql.communication.TypeReference; +import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; +import org.eclipse.jnosql.communication.semistructured.DeleteQuery; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.communication.semistructured.SelectQuery; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +enum Neo4JQueryBuilder { + + INSTANCE; + + private static final String INTERNAL_ID = "_id"; + + String buildQuery(DeleteQuery query, Map parameters) { + StringBuilder cypher = new StringBuilder("MATCH (e:"); + cypher.append(query.name()).append(")"); + + query.condition().ifPresent(c -> { + cypher.append(" WHERE "); + createWhereClause(cypher, c, parameters); + }); + + List columns = query.columns(); + if (!columns.isEmpty()) { + cypher.append(" SET "); + cypher.append(columns.stream() + .map(this::translateField) + .map(col -> col + " = NULL") + .collect(Collectors.joining(", "))); + } else { + cypher.append(" DELETE e"); + } + + return cypher.toString(); + } + + String buildQuery(SelectQuery query, Map parameters) { + StringBuilder cypher = new StringBuilder("MATCH (e:"); + cypher.append(query.name()).append(")"); + + query.condition().ifPresent(c -> { + cypher.append(" WHERE "); + createWhereClause(cypher, c, parameters); + }); + + if (!query.sorts().isEmpty()) { + cypher.append(" ORDER BY "); + cypher.append(query.sorts().stream() + .map(sort -> "e." + sort.property() + (sort.isAscending() ? " ASC" : " DESC")) + .collect(Collectors.joining(", "))); // Fix double "e." + } + if (query.skip() > 0) { + cypher.append(" SKIP ").append(query.skip()); + } + + if (query.limit() > 0) { + cypher.append(" LIMIT ").append(query.limit()); + } + + cypher.append(" RETURN "); + List columns = query.columns(); + if (columns.isEmpty()) { + cypher.append("e"); + } else { + cypher.append(columns.stream() + .map(this::translateField) + .collect(Collectors.joining(", "))); + } + + return cypher.toString(); + } + + private void createWhereClause(StringBuilder cypher, CriteriaCondition condition, Map parameters) { + Element element = condition.element(); + String fieldName = element.name(); + String queryField = translateField(fieldName); + + switch (condition.condition()) { + case EQUALS: + case GREATER_THAN: + case GREATER_EQUALS_THAN: + case LESSER_THAN: + case LESSER_EQUALS_THAN: + case LIKE: + case IN: + String paramName = INTERNAL_ID.equals(fieldName) ? "id" : fieldName; // Ensure valid parameter name + parameters.put(paramName, element.get()); + cypher.append(queryField).append(" ") + .append(getConditionOperator(condition.condition())) + .append(" $").append(paramName); + break; + case NOT: + cypher.append("NOT ("); + createWhereClause(cypher, element.get(CriteriaCondition.class), parameters); + cypher.append(")"); + break; + case AND: + case OR: + cypher.append("("); + List conditions = element.get(new TypeReference<>() {}); + for (int index = 0; index < conditions.size(); index++) { + if (index > 0) { + cypher.append(" ").append(getConditionOperator(condition.condition())).append(" "); + } + createWhereClause(cypher, conditions.get(index), parameters); + } + cypher.append(")"); + break; + default: + throw new CommunicationException("Unsupported condition: " + condition.condition()); + } + } + + private String translateField(String field) { + if (INTERNAL_ID.equals(field)) { + return "elementId(e)"; + } + if (field.startsWith("e.")) { + return field; + } + return "e." + field; + } + + + private String getConditionOperator(Condition condition) { + return switch (condition) { + case EQUALS -> "="; + case GREATER_THAN -> ">"; + case GREATER_EQUALS_THAN -> ">="; + case LESSER_THAN -> "<"; + case LESSER_EQUALS_THAN -> "<="; + case LIKE -> "CONTAINS"; + case IN -> "IN"; + case AND -> "AND"; + case OR -> "OR"; + default -> throw new CommunicationException("Unsupported operator: " + condition); + }; + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4Property.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4Property.java new file mode 100644 index 000000000..e429b74ad --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4Property.java @@ -0,0 +1,20 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + +record Neo4Property(String uri, String user, String password) { +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/package-info.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/package-info.java new file mode 100644 index 000000000..a4626be6c --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/package-info.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ + +/** + * This package contains the communication layer for Neo4j integration with Eclipse JNoSQL. + + * It provides implementations for managing communication with a Neo4j database, + * including inserting, updating, deleting, and querying entities. Additionally, + * it supports handling relationships (edges) between nodes and executing custom Cypher queries. + */ +package org.eclipse.jnosql.databases.neo4j.communication; \ No newline at end of file diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Cypher.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Cypher.java new file mode 100644 index 000000000..80f26c066 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Cypher.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Annotation for defining Cypher queries in Neo4J repositories. + * This annotation allows users to specify a Cypher query directly on repository methods, + * enabling custom query execution within {@code Neo4JRepository}. + * + * Example usage: + *
+ * {@code
+ * @Cypher("MATCH (n:Person) WHERE n.name = $name RETURN n")
+ * List findByName(@Param("name") String name);
+ * }
+ * 
+ * + * The {@code value} attribute should contain a valid Cypher query. Query parameters + * can be defined using the {@code $parameterName} syntax, which will be replaced by + * method parameters annotated with {@code @Param}. + * + * @see Neo4JRepository + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Cypher { + + /** + * The Cypher query to be executed. + * + * @return The Cypher query string. + */ + + String value(); +} \ No newline at end of file diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/DefaultNeo4JTemplate.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/DefaultNeo4JTemplate.java new file mode 100644 index 000000000..4bb80a618 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/DefaultNeo4JTemplate.java @@ -0,0 +1,184 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Typed; +import jakarta.inject.Inject; +import org.eclipse.jnosql.communication.semistructured.DatabaseManager; +import org.eclipse.jnosql.databases.neo4j.communication.Neo4JDatabaseManager; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.semistructured.AbstractSemiStructuredTemplate; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.eclipse.jnosql.mapping.semistructured.EventPersistManager; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.logging.Logger; +import java.util.stream.Stream; + +@ApplicationScoped +@Typed(Neo4JTemplate.class) +class DefaultNeo4JTemplate extends AbstractSemiStructuredTemplate implements Neo4JTemplate { + + private static final Logger LOGGER = Logger.getLogger(DefaultNeo4JTemplate.class.getName()); + + private Instance manager; + + private EntityConverter converter; + + private EntitiesMetadata entities; + + private Converters converters; + + private EventPersistManager persistManager; + + + @Inject + DefaultNeo4JTemplate(Instance manager, + EntityConverter converter, + EntitiesMetadata entities, + Converters converters, + EventPersistManager persistManager) { + this.manager = manager; + this.converter = converter; + this.entities = entities; + this.converters = converters; + this.persistManager = persistManager; + } + + @SuppressWarnings("unchecked") + @Override + public Stream cypher(String cypher, Map parameters) { + Objects.requireNonNull(cypher, "cypher is required"); + Objects.requireNonNull(parameters, "parameters is required"); + return manager.get().executeQuery(cypher, parameters) + .map(e -> (T) converter.toEntity(e)); + } + + @Override + public Stream traverse(String startNodeId, Supplier relationship, int depth) { + Objects.requireNonNull(startNodeId, "startNodeId is required"); + Objects.requireNonNull(relationship, "relationship is required"); + return traverse(startNodeId, relationship.get(), depth); + } + + + @SuppressWarnings("unchecked") + @Override + public Stream traverse(String startNodeId, String relationship, int depth) { + Objects.requireNonNull(startNodeId, "startNodeId is required"); + Objects.requireNonNull(relationship, "relationship is required"); + return manager.get().traverse(startNodeId, relationship, depth) + .map(e -> (T) converter.toEntity(e)); + } + + @Override + public Edge edge(T source, Supplier relationship, E target) { + Objects.requireNonNull(source, "source is required"); + Objects.requireNonNull(relationship, "relationship is required"); + Objects.requireNonNull(target, "target is required"); + return edge(source, relationship.get(), target); + } + + @SuppressWarnings("unchecked") + @Override + public Edge edge(T source, String relationshipType, E target) { + Objects.requireNonNull(relationshipType, "relationshipType is required"); + Objects.requireNonNull(source, "source is required"); + Objects.requireNonNull(target, "target is required"); + + T findSource = this.find((Class)source.getClass(), source).orElseGet(() ->{ + LOGGER.fine("There is not entity to source: " + source + " inserting the entity"); + return this.insert(source); + }); + + E findTarget = this.find((Class)target.getClass(), source).orElseGet(() ->{ + LOGGER.fine("There is not entity to target: " + target + " inserting the entity"); + return this.insert(target); + }); + + var sourceCommunication = this.converter.toCommunication(findSource); + var targetCommunication = this.converter.toCommunication(findTarget); + LOGGER.fine(() -> "creating an edge from " + sourceCommunication + " to " + targetCommunication + " with the relationship: " + relationshipType); + manager.get().edge(sourceCommunication, relationshipType, targetCommunication); + return Edge.of(findSource, relationshipType, findTarget); + } + + + + @Override + public void remove(T source, Supplier relationship, E target) { + Objects.requireNonNull(source, "source is required"); + Objects.requireNonNull(relationship, "relationship is required"); + Objects.requireNonNull(target, "target is required"); + this.remove(source, relationship.get(), target); + } + + @SuppressWarnings("unchecked") + @Override + public void remove(T source, String relationshipType, E target) { + Objects.requireNonNull(source, "source is required"); + Objects.requireNonNull(relationshipType, "relationshipType is required"); + Objects.requireNonNull(target, "target is required"); + + T findSource = this.find((Class)source.getClass(), source).orElseGet(() ->{ + LOGGER.fine("There is not entity to source: " + source + " inserting the entity"); + return this.insert(source); + }); + + E findTarget = this.find((Class)target.getClass(), source).orElseGet(() ->{ + LOGGER.fine("There is not entity to target: " + target + " inserting the entity"); + return this.insert(target); + }); + + var sourceCommunication = this.converter.toCommunication(findSource); + var targetCommunication = this.converter.toCommunication(findTarget); + + LOGGER.fine(() -> "removing an edge from " + sourceCommunication + " to " + targetCommunication + " with the relationship: " + relationshipType); + manager.get().remove(sourceCommunication, relationshipType, targetCommunication); + + } + + @Override + protected EntityConverter converter() { + return converter; + } + + @Override + protected DatabaseManager manager() { + return manager.get(); + } + + @Override + protected EventPersistManager eventManager() { + return persistManager; + } + + @Override + protected EntitiesMetadata entities() { + return entities; + } + + @Override + protected Converters converters() { + return converters; + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Edge.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Edge.java new file mode 100644 index 000000000..ba61cdf51 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Edge.java @@ -0,0 +1,98 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import java.util.Objects; + +/** + * Represents an edge in a graph database, linking a source entity to a target entity + * through a specified relationship type. + * + *

This class models relationships between nodes in a Neo4J database, where each + * edge is defined by a source node, a target node, and a relationship type.

+ * + *

Edges are immutable and ensure that a valid relationship exists between two entities.

+ * + * @param The entity type representing the source node. + * @param The entity type representing the target node. + */ +public class Edge { + + private final T source; + private final E target; + private final String relationship; + + private Edge(T source, E target, String relationship) { + this.source = source; + this.target = target; + this.relationship = relationship; + } + + /** + * Retrieves the source entity of the edge. + * + * @return The source entity. + */ + public T source() { + return source; + } + + /** + * Retrieves the target entity of the edge. + * + * @return The target entity. + */ + public E target() { + return target; + } + + /** + * Retrieves the relationship type of the edge. + * + * @return The relationship type. + */ + public String relationship() { + return relationship; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Edge edge = (Edge) o; + return Objects.equals(source, edge.source) && Objects.equals(target, edge.target) && Objects.equals(relationship, edge.relationship); + } + + @Override + public int hashCode() { + return Objects.hash(source, target, relationship); + } + + @Override + public String toString() { + return "Edge{" + + "source=" + source + + ", target=" + target + + ", relationship='" + relationship + '\'' + + '}'; + } + + static Edge of(T source, String relationship, E target) { + return new Edge<>(source, target, relationship); + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/GraphManagerSupplier.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/GraphManagerSupplier.java new file mode 100644 index 000000000..ea51a8230 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/GraphManagerSupplier.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Disposes; +import jakarta.enterprise.inject.Produces; +import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.databases.neo4j.communication.Neo4JConfiguration; +import org.eclipse.jnosql.databases.neo4j.communication.Neo4JDatabaseManager; +import org.eclipse.jnosql.databases.neo4j.communication.Neo4JDatabaseManagerFactory; +import org.eclipse.jnosql.mapping.core.config.MicroProfileSettings; + +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ApplicationScoped +class GraphManagerSupplier implements Supplier { + + private static final String DATABASE_DEFAULT = "neo4j"; + + private static final Logger LOGGER = Logger.getLogger(GraphManagerSupplier.class.getName()); + + @Override + @Produces + @ApplicationScoped + public Neo4JDatabaseManager get() { + LOGGER.fine(() -> "Creating a Neo4JDatabaseManager bean"); + Settings settings = MicroProfileSettings.INSTANCE; + var configuration = new Neo4JConfiguration(); + Neo4JDatabaseManagerFactory managerFactory = configuration.apply(settings); + var database = settings.getOrDefault("database", DATABASE_DEFAULT); + LOGGER.fine(() -> "Creating a Neo4JDatabaseManager bean with database: " + database); + return managerFactory.apply(database); + } + + public void close(@Disposes Neo4JDatabaseManager manager) { + LOGGER.log(Level.FINEST, "Closing Neo4JDatabaseManager resource, database name: " + manager.name()); + manager.close(); + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JExtension.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JExtension.java new file mode 100644 index 000000000..48f2a6052 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JExtension.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; +import org.eclipse.jnosql.mapping.metadata.ClassScanner; + +import java.util.Set; +import java.util.logging.Logger; + +/** + * CDI extension for Cassandra integration. + */ +public class Neo4JExtension implements Extension { + + private static final Logger LOGGER = Logger.getLogger(Neo4JExtension.class.getName()); + + /** + * Observes the AfterBeanDiscovery event to add Cassandra repository beans. + * + * @param afterBeanDiscovery the AfterBeanDiscovery event + */ + void onAfterBeanDiscovery(@Observes final AfterBeanDiscovery afterBeanDiscovery) { + ClassScanner scanner = ClassScanner.load(); + Set> crudTypes = scanner.repositories(Neo4JRepository.class); + + LOGGER.info("Starting the onAfterBeanDiscovery with elements number: " + crudTypes.size()); + + crudTypes.forEach(type -> afterBeanDiscovery.addBean(new Neo4JRepositoryBean<>(type))); + + LOGGER.info("Finished the onAfterBeanDiscovery"); + } +} \ No newline at end of file diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepository.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepository.java new file mode 100644 index 000000000..1ab4244d4 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepository.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + + +import org.eclipse.jnosql.mapping.NoSQLRepository; + +/** + * A repository interface for interacting with Neo4J databases using the Jakarta Data API. + *

+ * This interface extends {@link NoSQLRepository}, providing + * generic CRUD operations for entities stored in a Neo4J database. + * It also allows defining custom Cypher queries using the {@link Cypher} annotation. + *

+ * + *

Example usage:

+ *
+ * {@code
+ * @Repository
+ * public interface PersonRepository extends Neo4JRepository {
+ *
+ *     @Cypher("MATCH (p:Person) WHERE p.name = $name RETURN p")
+ *     List findByName(@Param("name") String name);
+ * }
+ * }
+ * 
+ * + * @param the entity type representing nodes in the Neo4J database. + * @param the entity ID type, typically a {@link String} corresponding to the element ID. + * + * @see NoSQLRepository + * @see Cypher + */ +public interface Neo4JRepository extends NoSQLRepository { + +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryBean.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryBean.java new file mode 100644 index 000000000..7957cc277 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryBean.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.util.AnnotationLiteral; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.AbstractBean; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Set; + + +class Neo4JRepositoryBean extends AbstractBean> { + + private final Class type; + + private final Set types; + + private final Set qualifiers = Collections.singleton(new AnnotationLiteral() { + }); + + Neo4JRepositoryBean(Class type) { + this.type = type; + this.types = Collections.singleton(type); + } + + @Override + public Class getBeanClass() { + return type; + } + + @SuppressWarnings("unchecked") + @Override + public Neo4JRepository create(CreationalContext> creationalContext) { + Neo4JTemplate template = getInstance(Neo4JTemplate.class); + Converters converters = getInstance(Converters.class); + EntitiesMetadata entitiesMetadata = getInstance(EntitiesMetadata.class); + Neo4JRepositoryProxy handler = new Neo4JRepositoryProxy<>(template, type, + converters, entitiesMetadata); + return (Neo4JRepository) Proxy.newProxyInstance(type.getClassLoader(), + new Class[]{type}, + handler); + } + + + @Override + public Set getTypes() { + return types; + } + + @Override + public Set getQualifiers() { + return qualifiers; + } + + @Override + public String getId() { + return type.getName() + "@neo4j"; + } + +} \ No newline at end of file diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryProxy.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryProxy.java new file mode 100644 index 000000000..c6dc39218 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryProxy.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.query.AbstractRepository; +import org.eclipse.jnosql.mapping.core.repository.DynamicReturn; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.metadata.EntityMetadata; +import org.eclipse.jnosql.mapping.semistructured.query.AbstractSemiStructuredRepositoryProxy; +import org.eclipse.jnosql.mapping.semistructured.query.SemiStructuredRepositoryProxy; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import static org.eclipse.jnosql.mapping.core.repository.DynamicReturn.toSingleResult; + +class Neo4JRepositoryProxy extends AbstractSemiStructuredRepositoryProxy { + + private final Class typeClass; + + private final Neo4JTemplate template; + + private final AbstractRepository repository; + + private final Converters converters; + + private final EntityMetadata entityMetadata; + + private final Class repositoryType; + + Neo4JRepositoryProxy(Neo4JTemplate template, Class repositoryType, + Converters converters, EntitiesMetadata entitiesMetadata) { + + this.template = template; + this.typeClass = Class.class.cast(ParameterizedType.class.cast(repositoryType.getGenericInterfaces()[0]) + .getActualTypeArguments()[0]); + + this.converters = converters; + this.entityMetadata = entitiesMetadata.get(typeClass); + this.repositoryType = repositoryType; + this.repository = SemiStructuredRepositoryProxy.SemiStructuredRepository.of(template, entityMetadata); + } + + @Override + protected AbstractRepository repository() { + return repository; + } + + @Override + protected Converters converters() { + return converters; + } + + @Override + protected Class repositoryType() { + return repositoryType; + } + + @Override + protected EntityMetadata entityMetadata() { + return entityMetadata; + } + + @Override + protected Neo4JTemplate template() { + return template; + } + + @Override + public Object invoke(Object instance, Method method, Object[] args) throws Throwable { + + Cypher cql = method.getAnnotation(Cypher.class); + if (Objects.nonNull(cql)) { + + Stream result; + Map values = ParamConverterUtils.getValues(args, method); + if (!values.isEmpty()) { + result = template.cypher(cql.value(), values); + } else { + result = template.cypher(cql.value(), Collections.emptyMap()); + } + + return DynamicReturn.builder() + .classSource(typeClass) + .methodSource(method) + .result(() -> result) + .singleResult(toSingleResult(method).apply(() -> result)) + .build().execute(); + } + + return super.invoke(instance, method, args); + } +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JTemplate.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JTemplate.java new file mode 100644 index 000000000..084ca0355 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JTemplate.java @@ -0,0 +1,117 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import org.eclipse.jnosql.mapping.semistructured.SemiStructuredTemplate; + +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * A template interface for executing queries, traversing relationships, + * and managing edges in a Neo4J database. + * This interface provides methods for executing Cypher queries, + * traversing relationships, and handling edges between entities. + * + */ +public interface Neo4JTemplate extends SemiStructuredTemplate { + /** + * Executes a Cypher query and returns a stream of results mapped to the given entity type. + * + * @param cypher The Cypher query string. + * @param parameters The query parameters. + * @param The entity type representing nodes or relationships within the graph database. + * @return A stream of entities representing the query result. + * @throws NullPointerException if {@code cypher} or {@code parameters} is null. + */ + Stream cypher(String cypher, Map parameters); + + /** + * Traverses relationships from a given start node up to a specified depth. + * + * @param startNodeId The unique identifier of the starting node. + * @param relationship The relationship type to traverse. + * @param depth The depth of traversal. + * @param The entity type representing nodes or relationships within the graph database. + * @return A stream of entities resulting from the traversal. + * @throws NullPointerException if {@code startNodeId} or {@code relationship} is null. + */ + Stream traverse(String startNodeId, String relationship, int depth); + + /** + * Traverses relationships dynamically using a relationship supplier. + * + * @param startNodeId The unique identifier of the starting node. + * @param relationship A supplier providing the relationship type dynamically. + * @param depth The depth of traversal. + * @param The entity type representing nodes or relationships within the graph database. + * @return A stream of entities resulting from the traversal. + * @throws NullPointerException if {@code startNodeId} or {@code relationship} is null. + */ + Stream traverse(String startNodeId, Supplier relationship, int depth); + + /** + * Creates an edge between two entities with the specified relationship type. + * + * @param source The source entity. + * @param relationshipType The relationship type to establish. + * @param target The target entity. + * @param The entity type representing the source node. + * @param The entity type representing the target node. + * @return The created {@link Edge} representing the relationship. + * @throws NullPointerException if {@code source}, {@code relationshipType}, or {@code target} is null. + */ + Edge edge(T source, String relationshipType, E target); + + /** + * Creates an edge between two entities using a dynamically provided relationship type. + * + * @param source The source entity. + * @param relationship A supplier providing the relationship type dynamically. + * @param target The target entity. + * @param The entity type representing the source node. + * @param The entity type representing the target node. + * @return The created {@link Edge} representing the relationship. + * @throws NullPointerException if {@code source}, {@code relationship}, or {@code target} is null. + */ + Edge edge(T source, Supplier relationship, E target); + + /** + * Removes an edge between two entities with the specified relationship type. + * + * @param source The source entity. + * @param relationshipType The relationship type to remove. + * @param target The target entity. + * @param The entity type representing the source node. + * @param The entity type representing the target node. + * @throws NullPointerException if {@code source}, {@code relationshipType}, or {@code target} is null. + */ + void remove(T source, String relationshipType, E target); + + /** + * Removes an edge between two entities using a dynamically provided relationship type. + * + * @param source The source entity. + * @param relationship A supplier providing the relationship type dynamically. + * @param target The target entity. + * @param The entity type representing the source node. + * @param The entity type representing the target node. + * @throws NullPointerException if {@code source}, {@code relationship}, or {@code target} is null. + */ + void remove(T source, Supplier relationship, E target); +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/ParamConverterUtils.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/ParamConverterUtils.java new file mode 100644 index 000000000..846f04f36 --- /dev/null +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/ParamConverterUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import jakarta.data.repository.Param; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +final class ParamConverterUtils { + + private ParamConverterUtils() { + } + + static Map getValues(Object[] args, Method method) { + + Map map = new HashMap<>(); + Annotation[][] annotations = method.getParameterAnnotations(); + + for (int index = 0; index < annotations.length; index++) { + + final Object arg = args[index]; + + Optional param = Stream.of(annotations[index]) + .filter(Param.class::isInstance) + .map(Param.class::cast) + .findFirst(); + param.ifPresent(p -> map.put(p.value(), arg)); + + } + + return map; + } +} diff --git a/jnosql-neo4j/src/main/resources/META-INF/beans.xml b/jnosql-neo4j/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..0340ecdbc --- /dev/null +++ b/jnosql-neo4j/src/main/resources/META-INF/beans.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/jnosql-neo4j/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension b/jnosql-neo4j/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension new file mode 100644 index 000000000..02761ccdc --- /dev/null +++ b/jnosql-neo4j/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension @@ -0,0 +1 @@ +org.eclipse.jnosql.databases.neo4j.mapping.Neo4JExtension \ No newline at end of file diff --git a/jnosql-neo4j/src/main/resources/META-INF/services/org.eclipse.jnosql.communication.semistructured.DatabaseConfiguration b/jnosql-neo4j/src/main/resources/META-INF/services/org.eclipse.jnosql.communication.semistructured.DatabaseConfiguration new file mode 100644 index 000000000..2c3e81f80 --- /dev/null +++ b/jnosql-neo4j/src/main/resources/META-INF/services/org.eclipse.jnosql.communication.semistructured.DatabaseConfiguration @@ -0,0 +1 @@ +org.eclipse.jnosql.databases.neo4j.communication.Neo4JConfiguration \ No newline at end of file diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/communication/DatabaseContainer.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/communication/DatabaseContainer.java new file mode 100644 index 000000000..022012da1 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/communication/DatabaseContainer.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ + +package org.eclipse.jnosql.databases.neo4j.communication; + +import org.eclipse.jnosql.communication.Settings; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.utility.DockerImageName; + +import java.util.Objects; + +public enum DatabaseContainer { + + INSTANCE; + + private final Neo4jContainer neo4jContainer = new Neo4jContainer<>(DockerImageName.parse("neo4j:5.26.3")) + .withoutAuthentication(); + + { + neo4jContainer.start(); + } + + + public String host() { + return neo4jContainer.getBoltUrl(); + } + public Neo4JDatabaseManager get(String database) { + Objects.requireNonNull(database, "database is required"); + Settings settings = Settings.builder().put(Neo4JConfigurations.URI, neo4jContainer.getBoltUrl()).build(); + var configuration = new Neo4JConfiguration(); + var managerFactory = configuration.apply(settings); + return managerFactory.apply(database); + } + +} diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManagerTest.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManagerTest.java new file mode 100644 index 000000000..b3c866d69 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManagerTest.java @@ -0,0 +1,588 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + +import net.datafaker.Faker; +import org.assertj.core.api.SoftAssertions; +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.communication.semistructured.Elements; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static org.eclipse.jnosql.communication.driver.IntegrationTest.MATCHES; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.NAMED; +import static org.eclipse.jnosql.communication.semistructured.DeleteQuery.delete; +import static org.eclipse.jnosql.communication.semistructured.SelectQuery.select; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnabledIfSystemProperty(named = NAMED, matches = MATCHES) +class Neo4JDatabaseManagerTest { + + public static final String COLLECTION_NAME = "person"; + private static Neo4JDatabaseManager entityManager; + + @BeforeAll + public static void setUp() { + entityManager = DatabaseContainer.INSTANCE.get("neo4j"); + } + + @BeforeEach + void beforeEach() { + removeAllEdges(); + delete().from(COLLECTION_NAME).delete(entityManager); + } + + @Test + void shouldInsert() { + var entity = getEntity(); + var communicationEntity = entityManager.insert(entity); + assertTrue(communicationEntity.elements().stream().map(Element::name).anyMatch(s -> s.equals("_id"))); + } + + @Test + void shouldInsertEntities() { + var entities = List.of(getEntity(), getEntity()); + var result = entityManager.insert(entities); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(result).hasSize(2); + softly.assertThat(result).allMatch(e -> e.elements().stream().map(Element::name).anyMatch(s -> s.equals("_id"))); + }); + } + + @Test + void shouldCount() { + var entity = getEntity(); + entityManager.insert(entity); + long count = entityManager.count(COLLECTION_NAME); + assertTrue(count > 0); + } + + @Test + void shouldUpdate() { + var entity = getEntity(); + var communicationEntity = entityManager.insert(entity); + var id = communicationEntity.find("_id").orElseThrow().get(); + var update = CommunicationEntity.of(COLLECTION_NAME); + update.add("_id", id); + update.add("name", "Lucas"); + + var result = entityManager.update(update); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(result).isNotNull(); + softly.assertThat(result.find("name").orElseThrow().get()).isEqualTo("Lucas"); + }); + assertTrue(result.find("name").isPresent()); + } + + @Test + void shouldUpdateEntities() { + var entity = getEntity(); + var communicationEntity = entityManager.insert(entity); + var id = communicationEntity.find("_id").orElseThrow().get(); + var update = CommunicationEntity.of(COLLECTION_NAME); + update.add("_id", id); + update.add("name", "Lucas"); + + var result = entityManager.update(List.of(update)); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(result).hasSize(1); + softly.assertThat(result).allMatch(e -> e.find("name").orElseThrow().get().equals("Lucas")); + }); + } + + @Test + void shouldSelectById() { + var entity = getEntity(); + var communicationEntity = entityManager.insert(entity); + var id = communicationEntity.find("_id").orElseThrow().get(); + var query = select().from(COLLECTION_NAME).where("_id").eq(id).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(1); + softly.assertThat(entities).allMatch(e -> e.find("_id").isPresent()); + }); + } + + @Test + void shouldSelectByIdWithName() { + var entity = getEntity(); + var communicationEntity = entityManager.insert(entity); + var id = communicationEntity.find("_id").orElseThrow().get(); + var query = select("name", "city").from(COLLECTION_NAME).where("_id").eq(id).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(1); + softly.assertThat(entities.get(0).elements()).hasSize(2); + softly.assertThat(entities.get(0).contains("name")).isTrue(); + softly.assertThat(entities.get(0).contains("city")).isTrue(); + }); + } + + @Test + void shouldSelectLimit() { + for (int index = 0; index < 10; index++) { + entityManager.insert(getEntity()); + } + var query = select().from(COLLECTION_NAME).limit(5).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(5); + }); + } + + @Test + void shouldSelectStart() { + for (int index = 0; index < 10; index++) { + entityManager.insert(getEntity()); + } + var query = select().from(COLLECTION_NAME).skip(5).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(5); + }); + } + + @Test + void shouldSelectStartAndLimit() { + for (int index = 0; index < 10; index++) { + entityManager.insert(getEntity()); + } + var query = select().from(COLLECTION_NAME).skip(5).limit(2).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(2); + }); + } + + @Test + void shouldSelectOrderAsc() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + + var query = select().from(COLLECTION_NAME).orderBy("index").asc().build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(10); + softly.assertThat(entities).allMatch(e -> e.find("index").isPresent()); + softly.assertThat(entities).extracting(e -> e.find("index").orElseThrow().get()) + .containsExactly(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L); + }); + } + + @Test + void shouldSelectOrderDesc() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var query = select().from(COLLECTION_NAME).orderBy("index").desc().build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(10); + softly.assertThat(entities).allMatch(e -> e.find("index").isPresent()); + softly.assertThat(entities).extracting(e -> e.find("index").orElseThrow().get()) + .containsExactly(9L, 8L, 7L, 6L, 5L, 4L, 3L, 2L, 1L, 0L); + }); + } + + @Test + void shouldSelectFindEquals() { + var entity = getEntity(); + entityManager.insert(entity); + var query = select().from(COLLECTION_NAME).where("name").eq(entity.find("name").orElseThrow().get()).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(1); + softly.assertThat(entities).allMatch(e -> e.find("name").isPresent()); + }); + } + + @Test + void shouldSelectFindNotEquals() { + var entity = getEntity(); + entityManager.insert(entity); + entityManager.insert(getEntity()); + Object name = entity.find("name").orElseThrow().get(); + var query = select().from(COLLECTION_NAME).where("name").not() + .eq(name).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(1); + softly.assertThat(entities).allMatch(e -> !e.find("name").orElseThrow().get().equals(name)); + }); + } + + @Test + void shouldSelectGreaterThan() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var index = 4; + var query = select().from(COLLECTION_NAME).where("index").gt(index).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(5); + softly.assertThat(entities).allMatch(e -> e.find("index").orElseThrow().get(Integer.class) > index); + }); + } + + @Test + void shouldSelectGreaterThanEqual() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var index = 4; + var query = select().from(COLLECTION_NAME).where("index").gte(index).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(6); + softly.assertThat(entities).allMatch(e -> e.find("index").orElseThrow().get(Integer.class) >= index); + }); + } + + @Test + void shouldSelectLesserThan() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var index = 4; + var query = select().from(COLLECTION_NAME).where("index").lt(index).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(4); + softly.assertThat(entities).allMatch(e -> e.find("index").orElseThrow().get(Integer.class) < index); + }); + } + + @Test + void shouldSelectLesserThanEqual() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var index = 4; + var query = select().from(COLLECTION_NAME).where("index").lte(index).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(5); + softly.assertThat(entities).allMatch(e -> e.find("index").orElseThrow().get(Integer.class) <= index); + }); + } + + @Test + void shouldSelectFindIn() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var query = select().from(COLLECTION_NAME).where("index").in(List.of(1, 2, 3)).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(3); + softly.assertThat(entities).allMatch(e -> Stream.of(1, 2, 3) + .anyMatch(i -> i.equals(e.find("index").orElseThrow().get(Integer.class)))); + }); + } + + @Test + void shouldSelectLike() { + var entity = getEntity(); + entity.add("name", "Ada Lovelace"); + entityManager.insert(entity); + var query = select().from(COLLECTION_NAME).where("name").like("Love").build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(1); + softly.assertThat(entities).allMatch(e -> e.find("name").orElseThrow().get().toString().contains("Love")); + }); + } + + @Test + void shouldSelectAnd() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var index = 4; + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + var query = select().from(COLLECTION_NAME).where("index") + .gte(index).and("name") + .eq(entity.find("name") + .orElseThrow().get()).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(1); + softly.assertThat(entities).allMatch(e -> e.find("index").orElseThrow().get(Integer.class) <= index); + }); + } + + @Test + void shouldSelectOr() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var index = 4; + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + var name = entity.find("name") + .orElseThrow().get(String.class); + var query = select().from(COLLECTION_NAME).where("index") + .gte(index).or("name") + .eq(name).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(7); + Predicate get = e -> e.find("index").orElseThrow().get(Integer.class) >= index; + Predicate eq = e -> e.find("name").orElseThrow().get(String.class).equals(name); + softly.assertThat(entities).allMatch(get.or(eq)); + }); + } + + @Test + void shouldDeleteById() { + var entity = getEntity(); + var communicationEntity = entityManager.insert(entity); + var id = communicationEntity.find("_id").orElseThrow().get(); + var deleteQuery = delete().from(COLLECTION_NAME).where("_id").eq(id).build(); + entityManager.delete(deleteQuery); + var query = select().from(COLLECTION_NAME).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).isEmpty(); + }); + } + + @Test + void shouldDeleteGreaterThan() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var index = 4; + var deleteQuery = delete().from(COLLECTION_NAME).where("index").gt(index).build(); + entityManager.delete(deleteQuery); + var query = select().from(COLLECTION_NAME).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(5); + softly.assertThat(entities).allMatch(e -> e.find("index").orElseThrow().get(Integer.class) <= index); + }); + } + + @Test + void shouldDeleteGreaterThanEqual() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var index = 4; + var deleteQuery = delete().from(COLLECTION_NAME).where("index").gte(index).build(); + entityManager.delete(deleteQuery); + var query = select().from(COLLECTION_NAME).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(4); + softly.assertThat(entities).allMatch(e -> e.find("index").orElseThrow().get(Integer.class) < index); + }); + } + + @Test + void shouldDeleteLesserThan() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var index = 4; + var deleteQuery = delete().from(COLLECTION_NAME).where("index").lt(index).build(); + entityManager.delete(deleteQuery); + var query = select().from(COLLECTION_NAME).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(6); + softly.assertThat(entities).allMatch(e -> e.find("index").orElseThrow().get(Integer.class) >= index); + }); + } + + @Test + void shouldDeleteLesserThanEqual() { + for (int index = 0; index < 10; index++) { + var entity = getEntity(); + entity.add("index", index); + entityManager.insert(entity); + } + var index = 4; + var deleteQuery = delete().from(COLLECTION_NAME).where("index").lte(index).build(); + entityManager.delete(deleteQuery); + var query = select().from(COLLECTION_NAME).build(); + var entities = entityManager.select(query).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(entities).hasSize(5); + softly.assertThat(entities).allMatch(e -> e.find("index").orElseThrow().get(Integer.class) > index); + }); + } + + @Test + void shouldExecuteCustomQuery() { + var entity = getEntity(); + entityManager.insert(entity); + + String cypher = "MATCH (e:person) RETURN e"; + var result = entityManager.executeQuery(cypher, new HashMap<>()).toList(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(result).isNotEmpty(); + softly.assertThat(result.get(0).find("name")).isPresent(); + softly.assertThat(result.get(0).find("city")).isPresent(); + softly.assertThat(result.get(0).find("_id")).isPresent(); // Ensuring _id exists + }); + } + + @Test + void shouldTraverseGraph() { + var person1 = getEntity(); + var person2 = getEntity(); + var person3 = getEntity(); + + entityManager.insert(person1); + entityManager.insert(person2); + entityManager.insert(person3); + + entityManager.edge(person1, "FRIEND", person2); + entityManager.edge(person2, "FRIEND", person3); + + var startNodeId = person1.find("_id").orElseThrow().get(); + var result = entityManager.traverse(startNodeId.toString(), "FRIEND", 2).toList(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(result).hasSize(2); + softly.assertThat(result).allMatch(e -> e.find("_id").isPresent()); + }); + entityManager.remove(person1, "FRIEND", person2); + entityManager.remove(person2, "FRIEND", person3); + } + + @Test + void shouldCreateEdge() { + var person1 = entityManager.insert(getEntity()); + var person2 = entityManager.insert(getEntity()); + + String person1Id = entityManager.select(select().from(COLLECTION_NAME) + .where("_id").eq(person1.find("_id").orElseThrow().get()).build()) + .findFirst().orElseThrow().find("_id").orElseThrow().get(String.class); + + String person2Id = entityManager.select(select().from(COLLECTION_NAME) + .where("_id").eq(person2.find("_id").orElseThrow().get()).build()) + .findFirst().orElseThrow().find("_id").orElseThrow().get(String.class); + + person1.add("_id", person1Id); + person2.add("_id", person2Id); + + entityManager.edge(person1, "FRIEND", person2); + + String cypher = "MATCH (p1:person) WHERE elementId(p1) = $id1 " + + "MATCH (p2:person) WHERE elementId(p2) = $id2 " + + "MATCH (p1)-[r:FRIEND]-(p2) RETURN r"; + + Map parameters = Map.of( + "id1", person1Id, + "id2", person2Id + ); + + var result = entityManager.executeQuery(cypher, parameters).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(result).isNotEmpty(); + }); + + entityManager.remove(person1, "FRIEND", person2); + } + + @Test + void shouldRemoveEdge() { + var person1 = getEntity(); + var person2 = getEntity(); + + entityManager.insert(person1); + entityManager.insert(person2); + + entityManager.edge(person1, "FRIEND", person2); + + var startNodeId = person1.find("_id").orElseThrow().get(); + var targetNodeId = person2.find("_id").orElseThrow().get(); + + String cypher = "MATCH (p1:person { _id: $_id1 })-[r:FRIEND]-(p2:person { _id: $_id2 }) RETURN r"; + Map parameters = Map.of("_id1", startNodeId, "_id2", targetNodeId); + + var result = entityManager.executeQuery(cypher, parameters).toList(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(result).isEmpty(); + }); + } + + + private CommunicationEntity getEntity() { + Faker faker = new Faker(); + + CommunicationEntity entity = CommunicationEntity.of(COLLECTION_NAME); + Map map = new HashMap<>(); + map.put("name", faker.name().fullName()); + map.put("city", faker.address().city()); + map.put("age", faker.number().randomNumber()); + List documents = Elements.of(map); + documents.forEach(entity::add); + return entity; + } + + private void removeAllEdges() { + String cypher = "MATCH ()-[r]-() DELETE r"; + + try { + entityManager.executeQuery(cypher, new HashMap<>()).toList(); + } catch (Exception e) { + throw new RuntimeException("Failed to remove edges before node deletion", e); + } + } +} diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JQueryBuilderTest.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JQueryBuilderTest.java new file mode 100644 index 000000000..cb9cf9ac1 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JQueryBuilderTest.java @@ -0,0 +1,116 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.communication; + +import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; +import org.eclipse.jnosql.communication.semistructured.DeleteQuery; +import org.eclipse.jnosql.communication.semistructured.SelectQuery; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class Neo4JQueryBuilderTest { + + @Test + void shouldBuildDeleteQueryForNode() { + DeleteQuery query = mock(DeleteQuery.class); + when(query.name()).thenReturn("Person"); + when(query.condition()).thenReturn(java.util.Optional.empty()); + when(query.columns()).thenReturn(List.of()); + + Map parameters = new HashMap<>(); + String cypher = Neo4JQueryBuilder.INSTANCE.buildQuery(query, parameters); + + assertThat(cypher).isEqualTo("MATCH (e:Person) DELETE e"); + } + + @Test + void shouldBuildDeleteQueryForSpecificColumns() { + DeleteQuery query = mock(DeleteQuery.class); + when(query.name()).thenReturn("Person"); + when(query.condition()).thenReturn(java.util.Optional.empty()); + when(query.columns()).thenReturn(List.of("name", "age")); + + Map parameters = new HashMap<>(); + String cypher = Neo4JQueryBuilder.INSTANCE.buildQuery(query, parameters); + + assertThat(cypher).isEqualTo("MATCH (e:Person) SET e.name = NULL, e.age = NULL"); + } + + @Test + void shouldBuildSelectQueryWithCondition() { + SelectQuery query = mock(SelectQuery.class); + when(query.name()).thenReturn("Person"); + + CriteriaCondition condition = mock(CriteriaCondition.class); + Element element = mock(Element.class); + when(condition.element()).thenReturn(element); + when(element.name()).thenReturn("age"); + when(element.get()).thenReturn(30); + when(condition.condition()).thenReturn(org.eclipse.jnosql.communication.Condition.EQUALS); + when(query.condition()).thenReturn(java.util.Optional.of(condition)); + when(query.columns()).thenReturn(List.of("name", "age")); + + Map parameters = new HashMap<>(); + String cypher = Neo4JQueryBuilder.INSTANCE.buildQuery(query, parameters); + + assertThat(cypher).isEqualTo("MATCH (e:Person) WHERE e.age = $age RETURN e.name, e.age"); + assertThat(parameters).containsEntry("age", 30); + } + + @Test + void shouldBuildSelectQueryWithoutCondition() { + SelectQuery query = mock(SelectQuery.class); + when(query.name()).thenReturn("Person"); + when(query.condition()).thenReturn(java.util.Optional.empty()); + when(query.columns()).thenReturn(List.of("name", "age")); + + Map parameters = new HashMap<>(); + String cypher = Neo4JQueryBuilder.INSTANCE.buildQuery(query, parameters); + + assertThat(cypher).isEqualTo("MATCH (e:Person) RETURN e.name, e.age"); + } + + @Test + void shouldTranslateIdToElementId() { + SelectQuery query = mock(SelectQuery.class); + when(query.name()).thenReturn("Person"); + + CriteriaCondition condition = mock(CriteriaCondition.class); + Element element = mock(Element.class); + when(condition.element()).thenReturn(element); + when(element.name()).thenReturn("_id"); + when(element.get()).thenReturn("12345"); + when(condition.condition()).thenReturn(org.eclipse.jnosql.communication.Condition.EQUALS); + when(query.condition()).thenReturn(java.util.Optional.of(condition)); + when(query.columns()).thenReturn(List.of("name", "_id")); + + Map parameters = new HashMap<>(); + String cypher = Neo4JQueryBuilder.INSTANCE.buildQuery(query, parameters); + + assertThat(cypher).isEqualTo("MATCH (e:Person) WHERE elementId(e) = $id RETURN e.name, elementId(e)"); + assertThat(parameters).containsEntry("id", "12345"); + } + +} \ No newline at end of file diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/Magazine.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/Magazine.java new file mode 100644 index 000000000..cf3cd78b5 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/Magazine.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.integration; + +import jakarta.nosql.Column; +import jakarta.nosql.Entity; +import jakarta.nosql.Id; + +@Entity +public record Magazine(@Id String id, @Column("title") String title, @Column("edition") int edition) { + + +} diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/MagazineRepository.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/MagazineRepository.java new file mode 100644 index 000000000..1a87a4d71 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/MagazineRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.neo4j.integration; + +import jakarta.data.repository.Repository; +import org.eclipse.jnosql.databases.neo4j.mapping.Neo4JRepository; +import org.eclipse.jnosql.mapping.NoSQLRepository; + +@Repository +public interface MagazineRepository extends Neo4JRepository { +} diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/RepositoryIntegrationTest.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/RepositoryIntegrationTest.java new file mode 100644 index 000000000..51f0201c1 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/RepositoryIntegrationTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.integration; + + +import jakarta.inject.Inject; +import org.eclipse.jnosql.databases.neo4j.communication.DatabaseContainer; +import org.eclipse.jnosql.databases.neo4j.communication.Neo4JConfigurations; +import org.eclipse.jnosql.databases.neo4j.mapping.Neo4JExtension; +import org.eclipse.jnosql.databases.neo4j.mapping.Neo4JTemplate; +import org.eclipse.jnosql.mapping.Database; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.EntityMetadataExtension; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.MATCHES; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.NAMED; + +@EnableAutoWeld +@AddPackages(value = {Database.class, EntityConverter.class, Neo4JTemplate.class}) +@AddPackages(Magazine.class) +@AddPackages(Reflections.class) +@AddPackages(Converters.class) +@AddExtensions({EntityMetadataExtension.class, Neo4JExtension.class}) +@EnabledIfSystemProperty(named = NAMED, matches = MATCHES) +public class RepositoryIntegrationTest { + + static { + DatabaseContainer.INSTANCE.host(); + System.setProperty(Neo4JConfigurations.URI.get(), DatabaseContainer.INSTANCE.host()); + System.setProperty(Neo4JConfigurations.DATABASE.get(), "neo4j"); + } + + @Inject + private MagazineRepository repository; + + @Test + void shouldSave() { + Magazine magazine = new Magazine(null, "Effective Java", 1); + assertThat(repository.save(magazine)) + .isNotNull(); + + } + +} diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/TemplateIntegrationTest.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/TemplateIntegrationTest.java new file mode 100644 index 000000000..85cfe17af --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/TemplateIntegrationTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.integration; + +import jakarta.inject.Inject; +import org.eclipse.jnosql.databases.neo4j.communication.DatabaseContainer; +import org.eclipse.jnosql.databases.neo4j.communication.Neo4JConfigurations; +import org.eclipse.jnosql.databases.neo4j.mapping.Neo4JTemplate; +import org.eclipse.jnosql.mapping.Database; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.EntityMetadataExtension; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.MATCHES; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.NAMED; + +@EnableAutoWeld +@AddPackages(value = {Database.class, EntityConverter.class, Neo4JTemplate.class}) +@AddPackages(Magazine.class) +@AddPackages(Reflections.class) +@AddPackages(Converters.class) +@AddExtensions({EntityMetadataExtension.class}) +@EnabledIfSystemProperty(named = NAMED, matches = MATCHES) +public class TemplateIntegrationTest { + + static { + DatabaseContainer.INSTANCE.host(); + System.setProperty(Neo4JConfigurations.URI.get(), DatabaseContainer.INSTANCE.host()); + System.setProperty(Neo4JConfigurations.DATABASE.get(), "neo4j"); + } + + @Inject + private Neo4JTemplate template; + + @BeforeEach + void setUp() { + template.delete(Magazine.class).execute(); + } + + @Test + void shouldFindById() { + Magazine magazine = template.insert(new Magazine(null, "Effective Java", 1)); + + assertThat(template.find(Magazine.class, magazine.id())) + .isNotNull().get().isEqualTo(magazine); + } + + @Test + void shouldInsert() { + Magazine magazine = template.insert(new Magazine(null, "Effective Java", 1)); + + Optional optional = template.find(Magazine.class, magazine.id()); + assertThat(optional).isNotNull().isNotEmpty() + .get().isEqualTo(magazine); + } + + @Test + void shouldUpdate() { + Magazine magazine = template.insert(new Magazine(null, "Effective Java", 1)); + + Magazine updated = new Magazine(magazine.id(), magazine.title() + " updated", 2); + + assertThat(template.update(updated)) + .isNotNull() + .isNotEqualTo(magazine); + + assertThat(template.find(Magazine.class, magazine.id())) + .isNotNull().get().isEqualTo(updated); + + } + + @Test + void shouldDeleteById() { + Magazine magazine = template.insert(new Magazine(null, "Effective Java", 1)); + + template.delete(Magazine.class, magazine.id()); + assertThat(template.find(Magazine.class, magazine.id())) + .isNotNull().isEmpty(); + } + + @Test + void shouldDeleteAll(){ + for (int index = 0; index < 20; index++) { + Magazine magazine = template.insert(new Magazine(null, "Effective Java", 1)); + assertThat(magazine).isNotNull(); + } + + template.delete(Magazine.class).execute(); + assertThat(template.select(Magazine.class).result()).isEmpty(); + } +} diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Birthday.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Birthday.java new file mode 100644 index 000000000..6ce88cc85 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Birthday.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + + +import jakarta.nosql.Column; +import jakarta.nosql.Entity; +import jakarta.nosql.Id; + +import java.util.Objects; + +@Entity +public class Birthday { + + @Id + private String name; + + @Column + private Integer age; + + + public String getName() { + return name; + } + + public Integer getAge() { + return age; + } + + public Birthday(String name, Integer age) { + this.name = name; + this.age = age; + } + + public Birthday() { + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Birthday birthday = (Birthday) o; + return Objects.equals(name, birthday.name) && + Objects.equals(age, birthday.age); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + + @Override + public String toString() { + return "Person{" + "name='" + name + '\'' + + ", age=" + age + + '}'; + } +} diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Contact.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Contact.java new file mode 100644 index 000000000..13ce3d27c --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Contact.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + + +import jakarta.nosql.Column; +import jakarta.nosql.Entity; +import jakarta.nosql.Id; + +import java.util.Objects; + +@Entity +public class Contact { + + @Id("name") + private String name; + + @Column + private Integer age; + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Contact(String name, Integer age) { + this.name = name; + this.age = age; + } + + public Contact() { + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Contact contact = (Contact) o; + return Objects.equals(name, contact.name) && + Objects.equals(age, contact.age); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + + @Override + public String toString() { + return "Person{" + + "name='" + name + '\'' + + ", age=" + age + + '}'; + } +} diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/DefaultNeo4JTemplateTest.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/DefaultNeo4JTemplateTest.java new file mode 100644 index 000000000..771360bb8 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/DefaultNeo4JTemplateTest.java @@ -0,0 +1,154 @@ +/* + * + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + * + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import net.datafaker.Faker; +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.databases.neo4j.communication.Neo4JDatabaseManager; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.EntityMetadataExtension; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.eclipse.jnosql.mapping.semistructured.EventPersistManager; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@EnableAutoWeld +@AddPackages(value = {Converters.class, EntityConverter.class, Neo4JTemplate.class}) +@AddPackages(Music.class) +@AddPackages(Reflections.class) +@AddExtensions({EntityMetadataExtension.class}) +class DefaultNeo4JTemplateTest { + + @Inject + private EntityConverter converter; + + @Inject + private EventPersistManager persistManager; + + @Inject + private EntitiesMetadata entities; + + @Inject + private Converters converters; + + private DefaultNeo4JTemplate template; + + private Neo4JDatabaseManager manager; + + @BeforeEach + void setUp() { + this.manager = mock(Neo4JDatabaseManager.class); + Instance instance = mock(Instance.class); + when(instance.get()).thenReturn(manager); + template = new DefaultNeo4JTemplate(instance, converter, entities, converters, persistManager); + } + + @Test + void shouldCypher() { + String cypher = "MATCH (n:Music) RETURN n"; + Map parameters = Collections.emptyMap(); + CommunicationEntity entity = CommunicationEntity.of("Music"); + entity.add(Element.of("name", "Ada")); + when(manager.executeQuery(cypher, parameters)).thenReturn(Stream.of(entity)); + + Stream result = template.cypher(cypher, parameters); + assertNotNull(result); + assertTrue(result.findFirst().isPresent()); + } + + @Test + void shouldThrowExceptionWhenQueryIsNull() { + assertThrows(NullPointerException.class, () -> template.cypher(null, Collections.emptyMap())); + assertThrows(NullPointerException.class, () -> template.cypher("MATCH (n) RETURN n", null)); + } + + @Test + void shouldCreateEdge() { + var faker = new Faker(); + Music source = new Music(UUID.randomUUID().toString(), faker.funnyName().name(), 10); + Music target = new Music(UUID.randomUUID().toString(), faker.funnyName().name(), 15); + String relationshipType = "FRIENDS"; + Edge edge = Edge.of(source, relationshipType, target); + + + Mockito.when(manager.insert(Mockito.any(CommunicationEntity.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + Edge result = template.edge(source, relationshipType, target); + + assertNotNull(result); + assertEquals(edge, result); + verify(manager).edge(any(), eq(relationshipType), any()); + } + + @Test + void shouldThrowExceptionWhenCreatingEdgeWithNullValues() { + var faker = new Faker(); + Music source = new Music(UUID.randomUUID().toString(), faker.funnyName().name(), 10); + Music target = new Music(UUID.randomUUID().toString(), faker.funnyName().name(), 15); + assertThrows(NullPointerException.class, () -> template.edge(null, "FRIENDS", target)); + assertThrows(NullPointerException.class, () -> template.edge(source, (Supplier) null, target)); + assertThrows(NullPointerException.class, () -> template.edge(source, "FRIENDS", null)); + } + + @Test + void shouldRemoveEdge() { + var faker = new Faker(); + Music source = new Music(UUID.randomUUID().toString(), faker.funnyName().name(), 10); + Music target = new Music(UUID.randomUUID().toString(), faker.funnyName().name(), 15); + String relationshipType = "FRIENDS"; + + doNothing().when(manager).remove(any(), anyString(), any()); + Mockito.when(manager.insert(Mockito.any(CommunicationEntity.class))).thenAnswer(invocation -> invocation.getArgument(0)); + template.remove(source, relationshipType, target); + verify(manager).remove(any(), eq(relationshipType), any()); + } + + @Test + void shouldThrowExceptionWhenRemovingEdgeWithNullValues() { + var faker = new Faker(); + Music source = new Music(UUID.randomUUID().toString(), faker.funnyName().name(), 10); + Music target = new Music(UUID.randomUUID().toString(), faker.funnyName().name(), 15); + assertThrows(NullPointerException.class, () -> template.remove(null, "FRIENDS", target)); + assertThrows(NullPointerException.class, () -> template.remove(source, (String) null, target)); + assertThrows(NullPointerException.class, () -> template.remove(source, "FRIENDS", null)); + } +} \ No newline at end of file diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Music.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Music.java new file mode 100644 index 000000000..24ab933a6 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Music.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ + +package org.eclipse.jnosql.databases.neo4j.mapping; + + +import jakarta.nosql.Column; +import jakarta.nosql.Convert; +import jakarta.nosql.Entity; +import jakarta.nosql.Id; + +@Entity +public class Music { + + @Id + private String id; + + @Column + private String name; + + @Column + private int year; + + Music() { + } + + + Music(String id, String name, int year) { + this.id = id; + this.name = name; + this.year = year; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public int getYear() { + return year; + } + + @Override + public String toString() { + return "Music{" + + "id='" + id + '\'' + + ", name='" + name + '\'' + + ", year=" + year + + '}'; + } +} diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/MusicRepository.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/MusicRepository.java new file mode 100644 index 000000000..dc7884430 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/MusicRepository.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + + +import jakarta.data.repository.Repository; + +@Repository +public interface MusicRepository extends Neo4JRepository { +} diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JTemplateTest.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JTemplateTest.java new file mode 100644 index 000000000..fff5f1b10 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JTemplateTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import jakarta.inject.Inject; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.EntityMetadataExtension; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +@EnableAutoWeld +@AddPackages(value = {Converters.class, EntityConverter.class, Neo4JTemplate.class}) +@AddPackages(Music.class) +@AddPackages(Reflections.class) +@AddExtensions({EntityMetadataExtension.class}) +class Neo4JTemplateTest { + + @Inject + private Neo4JTemplate template; + + @Test + void shouldInjectMongoDBTemplate() { + Assertions.assertNotNull(template); + } +} \ No newline at end of file diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4jExtensionTest.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4jExtensionTest.java new file mode 100644 index 000000000..b41249aaa --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4jExtensionTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import jakarta.inject.Inject; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.EntityMetadataExtension; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@EnableAutoWeld +@AddPackages(value = {Converters.class, Neo4JRepository.class, EntityConverter.class}) +@AddExtensions({EntityMetadataExtension.class, Neo4JExtension.class}) +@AddPackages(Reflections.class) +public class Neo4jExtensionTest { + + + @Inject + private MusicRepository repository; + + @Test + public void shouldCreteNeo4j() { + Assertions.assertNotNull(repository); + } +} \ No newline at end of file diff --git a/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4jRepositoryProxyTest.java b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4jRepositoryProxyTest.java new file mode 100644 index 000000000..3a425b4f4 --- /dev/null +++ b/jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4jRepositoryProxyTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.databases.neo4j.mapping; + +import jakarta.data.repository.Param; +import jakarta.inject.Inject; +import org.eclipse.jnosql.communication.semistructured.DeleteQuery; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.EntityMetadataExtension; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.lang.reflect.Proxy; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@EnableAutoWeld +@AddPackages(value = {Converters.class, Neo4JRepository.class, EntityConverter.class}) +@AddPackages(Reflections.class) +@AddExtensions({EntityMetadataExtension.class, Neo4JExtension.class}) +public class Neo4jRepositoryProxyTest { + + private Neo4JTemplate template; + + @Inject + private Converters converters; + + @Inject + private EntitiesMetadata entitiesMetadata; + + private HumanRepository personRepository; + + @BeforeEach + public void setUp() { + this.template = Mockito.mock(Neo4JTemplate.class); + Neo4JRepositoryProxy handler = new Neo4JRepositoryProxy(template, + HumanRepository.class, converters, entitiesMetadata); + + when(template.insert(any(Contact.class))).thenReturn(new Contact()); + when(template.insert(any(Contact.class), any(Duration.class))).thenReturn(new Contact()); + when(template.update(any(Contact.class))).thenReturn(new Contact()); + this.personRepository = (HumanRepository) Proxy.newProxyInstance(HumanRepository.class.getClassLoader(), + new Class[]{HumanRepository.class}, + handler); + } + + + @Test + public void shouldFindByName() { + personRepository.findByName("Ada"); + verify(template).cypher("MATCH (p:Person) WHERE p.name = $1 RETURN p", Map.of("name", "Ada")); + } + + @Test + public void shouldDeleteByName() { + personRepository.deleteByName("Ada"); + verify(template).cypher("MATCH (p:Person {name: $name}) DELETE p", Collections.singletonMap("name", "Ada")); + } + + @Test + public void shouldFindAll() { + personRepository.findAllQuery(); + verify(template).cypher("MATCH (p:Person) RETURN p", Collections.emptyMap()); + } + + @Test + public void shouldFindByNameCQL() { + personRepository.findByName("Ada"); + verify(template).cypher("MATCH (p:Person) WHERE p.name = $1 RETURN p", Collections.singletonMap("name", "Ada")); + } + + @Test + public void shouldFindByName2CQL() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Map.class); + + personRepository.findByName2("Ada"); + verify(template).cypher(Mockito.eq("MATCH (p:Person) WHERE p.name = $name RETURN p"), captor.capture()); + Map map = captor.getValue(); + assertEquals("Ada", map.get("name")); + } + + @Test + public void shouldSaveUsingInsert() { + Contact contact = new Contact("Ada", 10); + personRepository.save(contact); + verify(template).insert(eq(contact)); + } + + @Test + public void shouldSaveUsingUpdate() { + Contact contact = new Contact("Ada-2", 10); + when(template.find(Contact.class, "Ada-2")).thenReturn(Optional.of(contact)); + personRepository.save(contact); + verify(template).update(eq(contact)); + } + + @Test + public void shouldDelete(){ + personRepository.deleteById("id"); + verify(template).delete(Contact.class, "id"); + } + + + @Test + public void shouldDeleteEntity(){ + Contact contact = new Contact("Ada", 10); + personRepository.delete(contact); + verify(template).delete(Contact.class, contact.getName()); + } + + interface HumanRepository extends Neo4JRepository { + + @Cypher("MATCH (p:Person {name: $name}) DELETE p") + void deleteByName(@Param("name") String name); + + @Cypher("MATCH (p:Person) RETURN p") + List findAllQuery(); + + @Cypher("MATCH (p:Person) WHERE p.name = $1 RETURN p") + List findByName(@Param("name") String name); + + @Cypher("MATCH (p:Person) WHERE p.name = $name RETURN p") + List findByName2(@Param("name") String name); + } + + +} \ No newline at end of file diff --git a/jnosql-neo4j/src/test/resources/META-INF/beans.xml b/jnosql-neo4j/src/test/resources/META-INF/beans.xml new file mode 100644 index 000000000..2a29afc00 --- /dev/null +++ b/jnosql-neo4j/src/test/resources/META-INF/beans.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/jnosql-neo4j/src/test/resources/META-INF/microprofile-config.properties b/jnosql-neo4j/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..166d90edd --- /dev/null +++ b/jnosql-neo4j/src/test/resources/META-INF/microprofile-config.properties @@ -0,0 +1,4 @@ +graph=graph +graph.settings.key=value +graph.settings.key2=value2 +graph.provider=configuration.graph.mapping.org.eclipse.jnosql.databases.tinkerpop.GraphConfigurationMock diff --git a/jnosql-neo4j/src/test/resources/META-INF/services/org.eclipse.jnosql.databases.neo4j.communication.GraphConfiguration b/jnosql-neo4j/src/test/resources/META-INF/services/org.eclipse.jnosql.databases.neo4j.communication.GraphConfiguration new file mode 100644 index 000000000..eef0dd915 --- /dev/null +++ b/jnosql-neo4j/src/test/resources/META-INF/services/org.eclipse.jnosql.databases.neo4j.communication.GraphConfiguration @@ -0,0 +1 @@ +org.eclipse.jnosql.databases.tinkerpop.mapping.configuration.GraphConfigurationMock2 diff --git a/pom.xml b/pom.xml index 45a61bb51..9f8b8743e 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,7 @@ jnosql-riak jnosql-solr jnosql-oracle-nosql + jnosql-neo4j jnosql-tinkerpop