Skip to content

Commit

Permalink
Bugfixes, improvements, a meaningful commit message, A README FILE
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Hofer authored and dahoat committed Apr 21, 2021
1 parent ad7ed98 commit c69fe77
Show file tree
Hide file tree
Showing 12 changed files with 443 additions and 238 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Neo4JDemo
This project is used for a lecture at the Johannes Kepler University Linz (JKU) in Austria.
You are free to using it also if you do not participate in the course.

---

## Running
ATTENTION: This application tries to create the subfolder 'database' in the working directory.
If a folder of the same name is already present, the application will abort.
Otherwise, the folder is deleted on application shutdown.

### Requirements
- Java 11 or above
- A folder without 'database' in it

### Running from the console
1. Download the latest release at the releases page https://github.com/dahoat/neo4jdemo/releases
1. Open a cmd/terminal/... and navigate to the JAR file
1. Make sure no directory 'database' is present in the current directory
1. Execute `java --illegal-access=permit -jar neo4jdemo.jar`
1. Navigate to http://localhost:8080/
1. When you are finished, click into the console and press [Ctrl]+[C] to shutdown the application
1. Cleanup 'database' in case of an error.

### Running from your IDE
Clone the project and set up the Spring Boot Application.
The Angular project located in the folder 'frontend' is only needed, if you would like to change the website with the examples.
In that case, you have to use the command `ng build-spring` which will compile the angular project and store the result in 'resources/static' in the Spring project.

---
79 changes: 75 additions & 4 deletions frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,77 @@ <h2>Basic building blocks</h2>
<h2>Official Cypher Manual</h2>
<a href="https://neo4j.com/docs/cypher-manual/current/" target="_blank">https://neo4j.com/docs/cypher-manual/current/</a>

<h2>Creating our First Entries</h2>

<div class="lecture-part">
We will start by creating our first node
<app-snippset query="CREATE ()"></app-snippset>
This only inserts an empty node into the database without a label or any content.
</div>

<div class="lecture-part">
So next, we create a node with the label <b>Student</b> and a name.
<app-snippset query="CREATE (n:Student{name: 'Alex'}) RETURN n"></app-snippset>
This time, the node is stored in the variable <b>n</b>, so we can <b>RETURN</b> it at the end of the query.
</div>

<div class="lecture-part">
We now also want to add a property <b>study</b> to our student.
What we need is to first get the node out of the database again using <b>MATCH</b> like this:
<app-snippset query="MATCH (n:Student{name: 'Alex'}) RETURN n"></app-snippset>
If this query returns our previously created node, we can enhance the query.
<app-snippset query="MATCH (n:Student{name: 'Alex'}) SET n.study = 'computer science' RETURN n"></app-snippset>
This gets the node out of the database, modifies it by adding the property study and then returns the modified version back to us.
</div>

<div class="lecture-part">
We still have the empty node in the database which we do not need any longer, so we want to remove it.
First, we have to make sure, we get match only the node we want to delete:
<app-snippset query="MATCH (n) RETURN n"></app-snippset>
This has the problem that we also get our students, so we need to filter them out:
<app-snippset query="MATCH (n) WHERE NOT n:Student RETURN n"></app-snippset>
We could also do this one for the case we have multiple labels in the database:
<app-snippset query="MATCH (n) WHERE size(labels(n)) = 0 RETURN n"></app-snippset>
For the actual deletion, we replace the <b>RETURN</b> part with <b>DELETE</b>:
<app-snippset query="MATCH (n) WHERE size(labels(n)) = 0 DELETE n"></app-snippset>
</div>

<div class="lecture-part">
To add some edges, we need more nodes, so we create a lecture, find our student and attach both with an edge.
<app-snippset query="CREATE (l:Lecture{topic: 'GraphDBs'}) WITH l MATCH (s:Student{name: 'Alex'}) WITH l, s CREATE (s)-[:LISTENS]->(l) RETURN *"></app-snippset>
</div>

<div class="lecture-part">
After Alex graduated, the nodes is eventually removed, so we try this query:
<app-snippset query="MATCH (s:Student{name: 'Alex'}) DELETE s" copy="false"></app-snippset>
Which won't execute because as Neo4J says <em>Cannot delete node<0>, because it still has relationships. To delete this node, you must first delete its relationships.</em>
Instead, we exlicilty need to state that we want also the relationships removed by
<app-snippset query="MATCH (s:Student{name: 'Alex'}) DETACH DELETE s"></app-snippset>
</div>

<div class="lecture-part">
Sometimes, we want a node, but are not sure, whether it is already existing.
For this case, we can use <b>MERGE</b> which tries to lookup a node and nothing is found, creates it as if <b>CREATE</b> was used.
For example, to add another student to our (existing) lecture:
<app-snippset query="MERGE (s:Student{name: 'Kim'}) WITH s MERGE (l:Lecture{topic: 'GraphDBs'}) WITH s, l MERGE (s)-[:LISTENS]->(l) RETURN *"></app-snippset>
ATTENTION: If we used
<app-snippset query="MERGE (s:Student{name: 'Kim'})-[:LISTENS]->(l:Lecture{topic: 'GraphDBs'}) RETURN *"></app-snippset>
We would have got another lecture about graph databases because Neo4J would be looking for a lecture with an incoming <b>LISTENS</b> edge attaching a student named Kim.
Actually if you use the second query, change the name and execute it, you would end up with another new lecture with only one student.
</div>

<div class="lecture-part">
To remove a property of a node, <b>REMOVE</b> is used and not DELETE.
<app-snippset query="MATCH (l:Lecture{topic: 'GraphDBs'}) REMOVE l.topic RETURN l"></app-snippset>
</div>

<div class="lecture-part">
At this point, we finished the basics of creating and deleting nodes and edges and now focus on fetching data.
Before that, we can clean up the database, either by
<app-snippset query="MATCH (n) DETACH DELETE n"></app-snippset>
Matching, detaching and deleting everything in the database, or simply use the button below.
<app-database-control button="clear"></app-database-control>
</div>

<h2>Simple MATCH queries</h2>

Expand Down Expand Up @@ -108,9 +179,9 @@ <h2>Filtering with Properties</h2>

<div class="lecture-part">
We can also filter using all kinds of functions and combinations.
<app-snippset query="MATCH (n:Pokemon) WHERE n.name starts with 'pi' RETURN n"></app-snippset>
<app-snippset query="MATCH (n:Pokemon) WHERE n.name starts with 'pi' or n.name = 'charizard' RETURN n"></app-snippset>
<app-snippset query="MATCH (n:Pokemon) WHERE n.name starts with 'c' and n.pokedexId <= 150 RETURN n"></app-snippset>
<app-snippset query="MATCH (n:Pokemon) WHERE n.name STARTS WITH 'pi' RETURN n"></app-snippset>
<app-snippset query="MATCH (n:Pokemon) WHERE n.name STARTS WITH 'pi' or n.name = 'charizard' RETURN n"></app-snippset>
<app-snippset query="MATCH (n:Pokemon) WHERE n.name STARTS WITH 'c' and n.pokedexId <= 150 RETURN n"></app-snippset>
</div>

<h2>Filtering with Paths</h2>
Expand Down Expand Up @@ -146,7 +217,7 @@ <h2>Filtering with Paths</h2>

<div class="lecture-part">
For Pokemon with three evolutionary levels, we can use an abbreviation:
<app-snippset query="MATCH (p1:Pokemon)-[:EVOLVES_TO*3]->(p3:Pokemon) WHERE NOT (p3)-[:EVOLVES_TO]->() RETURN *"></app-snippset>
<app-snippset query="MATCH (p1:Pokemon)-[:EVOLVES_TO*2]->(p3:Pokemon) WHERE NOT (p3)-[:EVOLVES_TO]->() RETURN *"></app-snippset>
Where we do not see the full path but only start end end.
If we again store the paths in a variable, we get all three Pokemon.
<app-snippset query="MATCH chain=(p1:Pokemon)-[:EVOLVES_TO*2]->(p3:Pokemon) WHERE NOT (p3)-[:EVOLVES_TO]->() RETURN *"></app-snippset>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/components/browser/browser.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ <h2>Neo4J Browser</h2>
<dt>Password</dt>
<dd>&lt;empty&gt;</dd>
</dl>
Basically, just click <button onclick="alert('Not here, in the Neo4J Browser...');">Connect</button>
Basically, just enter <em>localhost</em> in the URL and click <button onclick="alert('Not here, in the Neo4J Browser...');">Connect</button>
</div>
2 changes: 1 addition & 1 deletion frontend/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Frontend</title>
<title>Neo4JDemo - Control Panel</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
Expand Down
6 changes: 0 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package at.jku.faw.neo4jdemo.repository;

import at.jku.faw.neo4jdemo.model.csv.CsvPokemon;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Repository;

import java.io.IOException;
Expand All @@ -12,8 +15,9 @@ public class CsvPokemonRepositoryImpl extends GenericCsvRepositoryImpl implement

private final List<CsvPokemon> csvPokemon = new ArrayList<>();

public CsvPokemonRepositoryImpl() throws IOException {
csvPokemon.addAll(getCsvEntities("data/pokemon_species.csv", CsvPokemon.class));
public CsvPokemonRepositoryImpl(ResourceLoader resourceLoader) throws IOException {
Resource csvFile = resourceLoader.getResource("classpath:data/pokemon_species.csv");
csvPokemon.addAll(getCsvEntities(csvFile, CsvPokemon.class));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,12 @@

import at.jku.faw.neo4jdemo.model.csv.CsvType;
import at.jku.faw.neo4jdemo.model.csv.CsvTypeMapping;
import com.opencsv.CSVReader;
import com.opencsv.bean.CsvToBean;
import com.opencsv.bean.CsvToBeanBuilder;
import com.opencsv.bean.HeaderColumnNameMappingStrategy;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Repository;
import org.springframework.util.ResourceUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Repository
Expand All @@ -25,9 +16,12 @@ public class CsvTypeRepositoryImpl extends GenericCsvRepositoryImpl implements C
private final Set<CsvTypeMapping> typeMappings = new HashSet<>();
private final Set<CsvType> types = new HashSet<>();

public CsvTypeRepositoryImpl() throws IOException {
this.typeMappings.addAll(getCsvEntities("data/pokemon_types.csv", CsvTypeMapping.class));
this.types.addAll(getCsvEntities("data/types.csv", CsvType.class));
public CsvTypeRepositoryImpl(ResourceLoader resourceLoader) throws IOException {
Resource csvPokemonTypesResource = resourceLoader.getResource("classpath:data/pokemon_types.csv");
Resource csvTypesResource = resourceLoader.getResource("classpath:data/types.csv");

this.typeMappings.addAll(getCsvEntities(csvPokemonTypesResource, CsvTypeMapping.class));
this.types.addAll(getCsvEntities(csvTypesResource, CsvType.class));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
package at.jku.faw.neo4jdemo.repository;

import at.jku.faw.neo4jdemo.model.csv.CsvType;
import com.opencsv.CSVReader;
import com.opencsv.bean.CsvToBean;
import com.opencsv.bean.CsvToBeanBuilder;
import com.opencsv.bean.CsvToBeanFilter;
import com.opencsv.bean.HeaderColumnNameMappingStrategy;
import com.opencsv.enums.CSVReaderNullFieldIndicator;
import org.springframework.util.ResourceUtils;
import org.springframework.core.io.Resource;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;

public abstract class GenericCsvRepositoryImpl {
private File getCsvFile(String path) throws FileNotFoundException {
return ResourceUtils.getFile("classpath:" + path);
}

protected <T> List<T> getCsvEntities(String csvFilePath, Class<T> entity) throws IOException {
try(CSVReader reader = new CSVReader(new BufferedReader(new InputStreamReader(new FileInputStream(getCsvFile(csvFilePath)))))) {
protected <T> List<T> getCsvEntities(Resource csvResource, Class<T> entity) throws IOException {
try(CSVReader reader = new CSVReader(new BufferedReader(new InputStreamReader(csvResource.getInputStream())))) {

HeaderColumnNameMappingStrategy<T> ms = new HeaderColumnNameMappingStrategy<>();
ms.setType(entity);
Expand Down
23 changes: 18 additions & 5 deletions src/main/java/at/jku/faw/neo4jdemo/runner/Neo4JRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;

import javax.annotation.PreDestroy;
import java.io.File;
Expand All @@ -21,15 +20,20 @@
public class Neo4JRunner implements ApplicationRunner {
private static final Logger logger = LoggerFactory.getLogger(Neo4JRunner.class);

private boolean cleanup = false;
private File dbDir;
private DatabaseManagementService dbms;

@Override
public void run(ApplicationArguments args) throws Exception {
dbDir = new File("database");
if (!dbDir.exists()) {
Files.createDirectory(dbDir.toPath());
if (dbDir.exists()) {
logger.error("The directory 'database' already exists. This application deletes this folder on shutdown. If it is your folder, move it somewhere else, if it belongs to a previous execution of this application, delete it.");
System.exit(1);
return;
}
cleanup = true;
Files.createDirectory(dbDir.toPath());

logger.warn("Database location is {}", dbDir.toPath().toAbsolutePath());

Expand All @@ -41,13 +45,22 @@ public void run(ApplicationArguments args) throws Exception {
if(db.isAvailable(10*1000)) {
logger.info("Neo4J is now available.");
}

logger.warn("Press [CTRL]+[C] to shutdown.");
}

@PreDestroy
public void onExit() {
logger.warn("Shutting database down.");
dbms.shutdown();
if (!cleanup) {
return;
}

try {
logger.warn("Shutting database down.");
dbms.shutdown();
} catch (Exception e) {
e.printStackTrace();
}

logger.warn("Deleting database directory.");
try {
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Frontend</title>
<title>Neo4JDemo - Control Panel</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
Expand Down
Loading

0 comments on commit c69fe77

Please sign in to comment.