diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e66dbc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Intellij +.idea/ +*.iml +*.iws + +# Mac +.DS_Store + +# Maven +log/ +target/ diff --git a/README.md b/README.md index 7effc43..d351cac 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -spring-data-neo4j-showcase -========================== +Our Spring Data Neo4j showcase application for comSysto Blog Article Spring Data Neo4j (>>Link here<<). -Showcase for our blog entry about Spring Data Neo4j. +It uses Spring, Spring Data Neo4j and Maven to demonstrate how to recommend products based on other users' views. + +Please mail comments to Roger.Kowalewski@comsysto.com or Elisabeth.Engel@comsysto.com! diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2f7d142 --- /dev/null +++ b/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + spring-data-neo4j-showcase + spring-data-neo4j-showcase + 1.0-SNAPSHOT + + + 2.2.0.RELEASE + 1.9 + 3.2.1.RELEASE + 1.9.M04 + + + + + org.springframework + spring-core + ${spring.core.version} + + + + org.springframework.data + spring-data-neo4j + ${spring.data.neo4j.version} + + + + org.neo4j + neo4j + ${neo4j.kernel.version} + + + + org.neo4j + neo4j-cypher + ${neo4j.kernel.version} + + + + org.neo4j + neo4j-cypher-dsl + ${neo4j.cypher.dsl.version} + + + + org.neo4j + neo4j-kernel + ${neo4j.kernel.version} + test-jar + + + + junit + junit + 4.11 + + + org.springframework + spring-test + ${spring.core.version} + + + + cglib + cglib + 2.2.2 + + + + javax.validation + validation-api + 1.1.0.Final + + + + + + org.springframework.maven.release + Spring Maven Release Repository + http://maven.springsource.org/release + + true + + false + + + neo4j-public-release-repository + http://m2.neo4j.org/releases + + false + + + true + + + + + + + + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + \ No newline at end of file diff --git a/src/main/java/com/comsysto/springDataNeo4j/showcase/ClickedRelationship.java b/src/main/java/com/comsysto/springDataNeo4j/showcase/ClickedRelationship.java new file mode 100644 index 0000000..ff42a00 --- /dev/null +++ b/src/main/java/com/comsysto/springDataNeo4j/showcase/ClickedRelationship.java @@ -0,0 +1,59 @@ +package com.comsysto.springDataNeo4j.showcase; + +import org.springframework.data.neo4j.annotation.*; + + +@RelationshipEntity(type = RelationshipTypes.CLICKED) +public class ClickedRelationship +{ + @GraphId + private Long graphId; + + @StartNode + private User user; + + @EndNode + @Fetch + private Product product; + + private Integer count; + + public ClickedRelationship() {/* NOOP */} + + public ClickedRelationship(User user, Product product) + { + this.user = user; + this.product = product; + } + + + public Integer getCount() { + return this.count; + } + + public void setCount(Integer count) { + this.count = count; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClickedRelationship that = (ClickedRelationship) o; + + if (product != null ? !product.equals(that.product) : that.product != null) return false; + if (count != null ? !count.equals(that.count) : that.count != null) return false; + if (user != null ? !user.equals(that.user) : that.user != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = user != null ? user.hashCode() : 0; + result = 31 * result + (product != null ? product.hashCode() : 0); + result = 31 * result + (count != null ? count.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/com/comsysto/springDataNeo4j/showcase/IdentifiableEntity.java b/src/main/java/com/comsysto/springDataNeo4j/showcase/IdentifiableEntity.java new file mode 100644 index 0000000..2f9f422 --- /dev/null +++ b/src/main/java/com/comsysto/springDataNeo4j/showcase/IdentifiableEntity.java @@ -0,0 +1,25 @@ +package com.comsysto.springDataNeo4j.showcase; + +import org.springframework.data.neo4j.annotation.GraphId; + +/** + * @author: rkowalewski + */ +public abstract class IdentifiableEntity { + @GraphId + private Long graphId; + + public Long getGraphId() { + return graphId; + } + + @Override + public boolean equals( Object obj ) { + return obj instanceof IdentifiableEntity && graphId.equals( ((IdentifiableEntity) obj).getGraphId() ); + } + + @Override + public int hashCode() { + return 0; + } +} diff --git a/src/main/java/com/comsysto/springDataNeo4j/showcase/Product.java b/src/main/java/com/comsysto/springDataNeo4j/showcase/Product.java new file mode 100644 index 0000000..52dd19e --- /dev/null +++ b/src/main/java/com/comsysto/springDataNeo4j/showcase/Product.java @@ -0,0 +1,96 @@ +package com.comsysto.springDataNeo4j.showcase; + +import org.springframework.data.neo4j.annotation.Indexed; +import org.springframework.data.neo4j.annotation.NodeEntity; +import org.springframework.data.neo4j.annotation.RelatedToVia; +import org.springframework.data.neo4j.support.index.IndexType; + +import java.util.HashSet; +import java.util.Set; + +@NodeEntity +public class Product extends IdentifiableEntity { + + @Indexed(indexName = "productId") + private String productId; + + @Indexed(indexType = IndexType.FULLTEXT, indexName = "productName") + private String productName; + + @RelatedToVia(type = RelationshipTypes.VIEWED) + private Set productsViewed = new HashSet(); + + + public Product() {/* NOOP */} + + public Product(String productId, String productName) { + super(); + + this.productId = productId; + this.productName = productName; + + } + + public String getProductId() { + return productId; + } + + public void setProductId(String productId) { + this.productId = productId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public Set getProductsViewed() { + return productsViewed; + } + + public void setProductsViewed(Set productsViewed) { + this.productsViewed = productsViewed; + } + + public void addProductViewed(Product productViewed) + { + productsViewed.add(productViewed); + } + + + @Override + public String toString() { + return "Product{" + + "graphId=" + this.getGraphId() + + ", productId=" + productId + + ", productName=" + productName + + //", #productsViewed=" + productsViewed.size() + + //", #userClicked=" + usersClicked.size() + + '}'; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + Product product = (Product) o; + + if (productId != null ? !productId.equals(product.productId) : product.productId != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (productId != null ? productId.hashCode() : 0); + return result; + } + +} diff --git a/src/main/java/com/comsysto/springDataNeo4j/showcase/ProductRepository.java b/src/main/java/com/comsysto/springDataNeo4j/showcase/ProductRepository.java new file mode 100644 index 0000000..1139c57 --- /dev/null +++ b/src/main/java/com/comsysto/springDataNeo4j/showcase/ProductRepository.java @@ -0,0 +1,36 @@ +package com.comsysto.springDataNeo4j.showcase; + +import org.springframework.data.neo4j.annotation.Query; +import org.springframework.data.neo4j.repository.GraphRepository; +import org.springframework.data.repository.query.Param; + +import java.util.List; + + +public interface ProductRepository extends GraphRepository { + + Product findByProductId(String productId); + + List findByProductNameLike(String productName); + + @Query("START product=node:Product(productId='{productId}') " + + "MATCH product-[viewed:VIEWED]->otherProduct " + + "RETURN distinct otherProduct " + + "ORDER BY viewed.count desc " + + "LIMIT 5") + List findOtherUsersAlsoViewedProducts(@Param("productId") String productId); + + @Query("START product=node:Product(productId='*') " + + "RETURN distinct product " + + "ORDER BY product.productName") + List findAllProductsSortedByName(); + + @Query("START product=node:Product(productId='{productId}'), user=node:User(userId='{userId}') " + + "MATCH user-[clicked:CLICKED]->product-[viewed:VIEWED]->otherProduct " + + "WHERE not(user-[:CLICKED]->otherProduct) " + + "RETURN distinct otherProduct " + + "ORDER BY viewed.count DESC " + + "LIMIT 5") + List findOtherUsersAlsoViewedProductsWithoutAlreadyViewed(@Param("productId") String productId, @Param("userId") String userId); + +} diff --git a/src/main/java/com/comsysto/springDataNeo4j/showcase/RelationshipTypes.java b/src/main/java/com/comsysto/springDataNeo4j/showcase/RelationshipTypes.java new file mode 100644 index 0000000..8e1f515 --- /dev/null +++ b/src/main/java/com/comsysto/springDataNeo4j/showcase/RelationshipTypes.java @@ -0,0 +1,12 @@ +package com.comsysto.springDataNeo4j.showcase; + +/** + * @author: rkowalewski + */ +public final class RelationshipTypes { + public static final String MEMBER_OF = "MEMBEROF"; + + public static final String CLICKED = "CLICKED"; + public static final String VIEWED = "VIEWED"; + +} diff --git a/src/main/java/com/comsysto/springDataNeo4j/showcase/User.java b/src/main/java/com/comsysto/springDataNeo4j/showcase/User.java new file mode 100644 index 0000000..f501828 --- /dev/null +++ b/src/main/java/com/comsysto/springDataNeo4j/showcase/User.java @@ -0,0 +1,97 @@ +package com.comsysto.springDataNeo4j.showcase; + +import org.springframework.data.neo4j.annotation.*; +import org.springframework.data.neo4j.support.index.IndexType; + +import java.util.HashSet; +import java.util.Set; + +@NodeEntity +public class User extends IdentifiableEntity { + + @Indexed(indexName = "userId") + private String userId; + + @Indexed(indexType = IndexType.FULLTEXT, indexName = "userName") + private String userName; + + private Product clickedBofore = null; + + @RelatedToVia(type = RelationshipTypes.CLICKED) + private Set clickedProducts = new HashSet(); + + + public User() {/* NOOP */} + + public User(String userId, String name) { + super(); + + this.userId = userId; + this.userName = name; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public Set getClickedProducts() { + return clickedProducts; + } + + public void addClickedProduct(Product clickedProduct) + { + + if (this.clickedBofore != null) { + this.clickedBofore.addProductViewed(clickedProduct); + } + + this.clickedProducts.add(clickedProduct); + + //clickedProduct.addUserClicked(this); + + this.clickedBofore = clickedProduct; + + } + + @Override + public String toString() { + return "User{" + + "graphId=" + this.getGraphId() + + ", userId=" + this.userId + + ", userName=" + this.userName + + //", #clickedProducts=" + this.clickedProducts.size() + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + User user = (User) o; + + if (userId != null ? !userId.equals(user.userId) : user.userId != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (userId != null ? userId.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/com/comsysto/springDataNeo4j/showcase/UserRepository.java b/src/main/java/com/comsysto/springDataNeo4j/showcase/UserRepository.java new file mode 100644 index 0000000..c3150e7 --- /dev/null +++ b/src/main/java/com/comsysto/springDataNeo4j/showcase/UserRepository.java @@ -0,0 +1,11 @@ +package com.comsysto.springDataNeo4j.showcase; + +import org.springframework.data.neo4j.repository.GraphRepository; + + +public interface UserRepository extends GraphRepository{ + + User findByUserId(String userId); + + Iterable findByUserNameLike(String userName); +} diff --git a/src/main/java/com/comsysto/springDataNeo4j/showcase/ViewedRelationship.java b/src/main/java/com/comsysto/springDataNeo4j/showcase/ViewedRelationship.java new file mode 100644 index 0000000..7a0a500 --- /dev/null +++ b/src/main/java/com/comsysto/springDataNeo4j/showcase/ViewedRelationship.java @@ -0,0 +1,60 @@ +package com.comsysto.springDataNeo4j.showcase; + +import org.springframework.data.neo4j.annotation.*; + + +@RelationshipEntity(type = RelationshipTypes.VIEWED) +public class ViewedRelationship +{ + @GraphId + private Long graphId; + + @StartNode + private Product productStart; + + @EndNode + @Fetch + private Product productEnd; + + private Integer count; + + public ViewedRelationship() {/* NOOP */} + + public ViewedRelationship(Product productStart, Product productEnd) + { + this.productStart = productStart; + this.productEnd = productEnd; + } + + + public Integer getCount() { + return this.count; + } + + public void setCount(Integer count) { + this.count = count; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ViewedRelationship that = (ViewedRelationship) o; + + if (productStart != null ? !productStart.equals(that.productStart) : that.productStart != null) return false; + if (productEnd != null ? !productEnd.equals(that.productEnd) : that.productEnd != null) return false; + if (count != null ? !count.equals(that.count) : that.count != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = productStart != null ? productStart.hashCode() : 0; + result = 31 * result + (productEnd != null ? productEnd.hashCode() : 0); + result = 31 * result + (count != null ? count.hashCode() : 0); + return result; + } + +} diff --git a/src/test/java/com/comsysto/springDataNeo4j/showcase/SpringDataNeo4jProductUserTest.java b/src/test/java/com/comsysto/springDataNeo4j/showcase/SpringDataNeo4jProductUserTest.java new file mode 100644 index 0000000..3ec1b51 --- /dev/null +++ b/src/test/java/com/comsysto/springDataNeo4j/showcase/SpringDataNeo4jProductUserTest.java @@ -0,0 +1,111 @@ +package com.comsysto.springDataNeo4j.showcase; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.neo4j.graphdb.GraphDatabaseService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.neo4j.support.node.Neo4jHelper; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.*; + + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = {"classpath:com/comsysto/springDataNeo4j/showcase/related-to-via-test-context.xml"}) +@Transactional +public class SpringDataNeo4jProductUserTest { + @Autowired + private UserRepository userRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + GraphDatabaseService graphDatabaseService; + + User jordan, pippen, miller; + Product pizzaMargarita, pizzaFungi, pizzaSalami, pizzaVegitarian, pizzaRustica; + + public void createSzenario () { + jordan = createUser("MJ", "Monika Jordan"); + pippen = createUser("SP", "Sandra Pippen"); + miller = createUser("JM", "John Miller"); + + pizzaMargarita = createProduct("Pizza_1", "Pizza Margarita"); + pizzaFungi = createProduct("Pizza_1", "Pizza Fungi"); + pizzaSalami = createProduct("Pizza_1", "Pizza Salami"); + pizzaVegitarian = createProduct("Pizza_1", "Pizza Vegitarian"); + pizzaRustica = createProduct("Pizza_1", "Pizza Rustica"); + + jordan.addClickedProduct(pizzaMargarita); + jordan.addClickedProduct(pizzaFungi); + jordan.addClickedProduct(pizzaSalami); + + pippen.addClickedProduct(pizzaMargarita); + pippen.addClickedProduct(pizzaVegitarian); + pippen.addClickedProduct(pizzaRustica); + + miller.addClickedProduct(pizzaFungi); + + } + + @Test + public void testClickedRelationships() { + + createSzenario(); + + //Load and check relations + + List allProducts = productRepository.findAll().as(List.class); + assertEquals("there should be three products in the products repository", 5, allProducts.size()); + + assertTrue("saved and loaded products should be equal", + allProducts.contains(pizzaMargarita) && allProducts.contains(pizzaFungi) && + allProducts.contains(pizzaSalami) && allProducts.contains(pizzaVegitarian) && + allProducts.contains(pizzaRustica)); + + + List allUsers = userRepository.findAll().as(List.class); + assertEquals("there should be three users in the user repository", 1, allUsers.size()); + + Set clickedProducts = allUsers.get(0).getClickedProducts(); + assertEquals("Monika Jordan should have three clicked products", 3, clickedProducts.size()); + assertTrue("The two products Monika Jordan clicked on should be pizza margarita and pizza fungi", + clickedProducts.contains(pizzaMargarita) && clickedProducts.contains(pizzaFungi) && clickedProducts.contains(pizzaSalami)); + } + + @Test + public void testNamedCypherQuerys() { + + createSzenario(); + + List alsoViewedProducts = productRepository.findOtherUsersAlsoViewedProducts(pizzaMargarita.getProductId()); + assertTrue("using this cypher query should return a list with also viewed products", + alsoViewedProducts.contains(pizzaFungi) && alsoViewedProducts.contains(pizzaVegitarian)); + + List alsoViewedProductsWithoutAlreadyViewed = productRepository.findOtherUsersAlsoViewedProductsWithoutAlreadyViewed(pizzaMargarita.getProductId(), miller.getUserId()); + assertTrue("using this cypher query should return a list with also viewed products without the ones Miller already viewed", + alsoViewedProducts.contains(pizzaVegitarian)); + + + } + + private Product createProduct(String id, String name) { + return productRepository.save(new Product(id, name)); + } + + private User createUser(String id, String name) { + return userRepository.save(new User(id, name)); + } + + @After + public void cleanDB() { + Neo4jHelper.cleanDb(graphDatabaseService); + } +} diff --git a/src/test/resources/com/comsysto/springDataNeo4j/showcase/related-to-via-test-context.xml b/src/test/resources/com/comsysto/springDataNeo4j/showcase/related-to-via-test-context.xml new file mode 100644 index 0000000..12cddd0 --- /dev/null +++ b/src/test/resources/com/comsysto/springDataNeo4j/showcase/related-to-via-test-context.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file