Skip to content
This repository has been archived by the owner on Mar 12, 2020. It is now read-only.

Commit

Permalink
Initial Neo4J support #19
Browse files Browse the repository at this point in the history
Use an embedded database and the Java API.
  • Loading branch information
tdauth committed Apr 2, 2019
1 parent 6047cc2 commit 5c07ee3
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 2 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,20 @@ After running the genetic algorithm, the state machine is then used to create a
## Concurrency

The creation and modification of state machines should be threadsafe.

## Backends

There can be different backends which manage the state machine.

### Neo4J

This backend uses the GraphDB [Neo4J](https://neo4j.com/) (community edition) with an embedded database.
Each state machine is represented by a separate graph database stored in a separate file.

The nodes all have the property "sutState" which contains the corresponding SUT state and can be used as index to query the nodes.
The relationship types correspond to actions.
Each relation has the property "counter" which contains the execution counter of the action.

We need to use an [Object Graph Mapper](https://neo4j.com/docs/ogm-manual/current/introduction/) to store the corresponding SUT states in the nodes.

See also <https://neo4j.com/docs/java-reference/current/tutorials-java-embedded/>
5 changes: 5 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ fork := true

resolvers += "nexus-retest-maven-all" at "https://nexus.retest.org/repository/all/"

resolvers += "sonatype-snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"

// Dependencies to represent states and actions:
libraryDependencies += "de.retest" % "surili-commons" % "0.1.0-SNAPSHOT" withSources () withJavadoc () changing ()

// Dependencies for a graph database:
libraryDependencies += "org.neo4j" % "neo4j" % "3.0.1"

// Dependencies to write GML files for yEd:
libraryDependencies += "com.github.systemdir.gml" % "GMLWriterForYed" % "2.1.0"
libraryDependencies += "org.jgrapht" % "jgrapht-core" % "1.0.1"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.retest.guistatemachine.api
import de.retest.guistatemachine.api.impl.GuiStateMachineApiImpl
import de.retest.guistatemachine.api.neo4j.GuiStateMachineApiNeo4J

/**
* This API allows the creation, modification and deletion of state machines ([[GuiStateMachine]]) which are created
Expand Down Expand Up @@ -37,10 +38,12 @@ trait GuiStateMachineApi {
}

object GuiStateMachineApi {
private val impl = new GuiStateMachineApiImpl
val default = new GuiStateMachineApiImpl

/**
* @return The standard implementaiton of the API.
*/
def apply(): GuiStateMachineApi = impl
def apply(): GuiStateMachineApi = default

val neo4j = new GuiStateMachineApiNeo4J
}
55 changes: 55 additions & 0 deletions src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package de.retest.guistatemachine.api.neo4j
import java.util.Arrays

import de.retest.guistatemachine.api.GuiStateMachineApi
import de.retest.recheck.ui.descriptors._
import de.retest.recheck.ui.image.Screenshot
import de.retest.surili.commons.actions.NavigateToAction

// TODO #19 Replace this example with unit tests when everything works.
object Example extends App {
private val rootElementA = getRootElement("a", 0)
private val rootElementB = getRootElement("b", 0)
private val rootElementC = getRootElement("c", 0)
private val action0 = new NavigateToAction("http://google.com")
private val action1 = new NavigateToAction("http://wikipedia.org")

val stateMachine = GuiStateMachineApi.neo4j.createStateMachine("tmp")
val startState = new SutState(Arrays.asList(rootElementA, rootElementB, rootElementC))
val endState = new SutState(Arrays.asList(rootElementA))
stateMachine.executeAction(startState, action0, endState)

/**
* Creates a new identifying attributes collection which should only match other identifying attributes with the same ID.
*
* @param id The ID is used as value for different attributes.
* @return The identifying attributes.
*/
def getIdentifyingAttributes(id: String): IdentifyingAttributes =
new IdentifyingAttributes(Arrays.asList(new StringAttribute("a", id), new StringAttribute("b", id), new StringAttribute("c", id)))

/**
* The identifying attributes and the contained components specify the equality.
*
* @param id This value is a criteria for equality of the returned element.
* @param numberOfContainedComponents This value is a criteria for equality of the returned element.
* @return A new root element which is equal to itself but not to any other root element.
*/
def getRootElement(id: String, numberOfContainedComponents: Int): RootElement = {
val r = new RootElement(
"retestId",
getIdentifyingAttributes(id),
new Attributes(),
new Screenshot("prefix", Array(1, 2, 3), Screenshot.ImageType.PNG),
"screen0",
0,
"My Window"
)
if (numberOfContainedComponents > 0) {
r.addChildren(scala.collection.JavaConverters.seqAsJavaList[Element](0 to numberOfContainedComponents map { _ =>
getRootElement("x", 0)
}))
}
r
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package de.retest.guistatemachine.api.neo4j

import java.io.File

import com.typesafe.scalalogging.Logger
import de.retest.guistatemachine.api.impl.GuiStateMachineImpl
import de.retest.guistatemachine.api.{GuiStateMachine, GuiStateMachineApi}
import org.neo4j.graphdb.GraphDatabaseService
import org.neo4j.graphdb.factory.GraphDatabaseFactory

import scala.collection.concurrent.TrieMap

class GuiStateMachineApiNeo4J extends GuiStateMachineApi {
private val logger = Logger[GuiStateMachineImpl]
private val stateMachines = TrieMap[String, GuiStateMachine]()
// TODO #19 Load existing state machines from the disk.

override def createStateMachine(name: String): GuiStateMachine = {
var dir = new File(name)
var graphDb = new GraphDatabaseFactory().newEmbeddedDatabase(dir)
registerShutdownHook(graphDb)

logger.info("Created new graph DB in {}.", dir.getAbsolutePath)

val guiStateMachine = new GuiStateMachineNeo4J(graphDb)
stateMachines += (name -> guiStateMachine)
guiStateMachine
}

override def removeStateMachine(name: String): Boolean = stateMachines.remove(name).isDefined // TODO #19 Remove from disk?

override def getStateMachine(name: String): Option[GuiStateMachine] = stateMachines.get(name)

override def clear(): Unit = stateMachines.clear()

private def registerShutdownHook(graphDb: GraphDatabaseService): Unit = { // Registers a shutdown hook for the Neo4j instance so that it
// shuts down nicely when the VM exits (even if you "Ctrl-C" the
// running application).
Runtime.getRuntime.addShutdownHook(new Thread() { override def run(): Unit = { graphDb.shutdown() } })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package de.retest.guistatemachine.api.neo4j

import de.retest.guistatemachine.api.{GuiStateMachine, State}
import de.retest.recheck.ui.descriptors.SutState
import de.retest.surili.commons.actions.Action
import org.neo4j.graphdb.{GraphDatabaseService, Node, Transaction}

import scala.collection.immutable.HashMap

class GuiStateMachineNeo4J(var graphDb: GraphDatabaseService) extends GuiStateMachine {

override def getState(sutState: SutState): State = {
var tx: Option[Transaction] = None
try {
tx = Some(graphDb.beginTx)
getNodeBySutState(sutState) match {
case None => {
val node = graphDb.createNode
// TODO #19 SutState is not a supported property value!
node.setProperty("sutState", sutState)
}
case _ =>
}
tx.get.success
} finally {
if (tx.isDefined) { tx.get.close() }
}

new StateNeo4J(sutState, this)
}

override def executeAction(from: State, a: Action, to: State): State = {
from.addTransition(a, to)
to
}

override def getAllStates: Map[SutState, State] = {
var tx: Option[Transaction] = None
val allNodes = try {
tx = Some(graphDb.beginTx)
val allNodes = graphDb.getAllNodes()
tx.get.success()
allNodes
} finally {
if (tx.isDefined) { tx.get.close() }
}

var result = HashMap[SutState, State]()
val iterator = allNodes.iterator()

while (iterator.hasNext) {
val node = iterator.next()
val sutState = node.getProperty("sutState").asInstanceOf[SutState]
result = result + (sutState -> new StateNeo4J(sutState, this))
}
result
}

override def getAllExploredActions: Set[Action] = Set() // TODO #19 get all relationships in a transaction

override def getActionExecutionTimes: Map[Action, Int] = Map() // TODO #19 get all execution time properties "counter" from all actions

override def clear(): Unit = {
var tx: Transaction = null
try {
tx = graphDb.beginTx
// Deletes all nodes and relationships.
graphDb.execute("MATCH (n)\nDETACH DELETE n")
tx.success
} finally {
if (tx != null) { tx.close() }
}
}

override def assignFrom(other: GuiStateMachine): Unit = {
clear()
val otherStateMachine = other.asInstanceOf[GuiStateMachineNeo4J]
graphDb = otherStateMachine.graphDb
}

// TODO #19 Create an index on the property "sutState": https://neo4j.com/docs/cypher-manual/current/schema/index/#schema-index-create-a-single-property-index
private[neo4j] def getNodeBySutState(sutState: SutState): Option[Node] = {
val nodes = graphDb.findNodes(SutStateLabel, "sutState", sutState)
val first = nodes.stream().findFirst()
if (first.isPresent) {
Some(first.get())
} else {
None
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package de.retest.guistatemachine.api.neo4j

import de.retest.surili.commons.actions.Action
import org.neo4j.graphdb.RelationshipType

case class RelationshipTypeAction(action: Action) extends RelationshipType {
override def name() = action.toString
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package de.retest.guistatemachine.api.neo4j
import de.retest.guistatemachine.api.{ActionTransitions, State}
import de.retest.recheck.ui.descriptors.SutState
import de.retest.surili.commons.actions.Action
import org.neo4j.graphdb.{Direction, Node, Relationship, Transaction}

import scala.collection.immutable.HashMap

case class StateNeo4J(sutState: SutState, guiStateMachine: GuiStateMachineNeo4J) extends State {

override def getSutState: SutState = sutState
override def getTransitions: Map[Action, ActionTransitions] = {
val node = getNode()
val outgoingRelationships = node.getRelationships(Direction.OUTGOING)
var result = HashMap[Action, ActionTransitions]()
val iterator = outgoingRelationships.iterator()
while (iterator.hasNext()) {
val relationship = iterator.next()
val relationshipTypeAction = relationship.getType.asInstanceOf[RelationshipTypeAction]
val action = relationshipTypeAction.action
val sutState = relationship.getEndNode.getProperty("sutState").asInstanceOf[SutState]
val actionTransitions = if (result.contains(action)) {
val existing = result.get(action).get
ActionTransitions(existing.to ++ Set(new StateNeo4J(sutState, guiStateMachine)), existing.executionCounter + 1)
} else {
ActionTransitions(Set(new StateNeo4J(sutState, guiStateMachine)), 1)
}
result = result + (action -> actionTransitions)
}
result
}

private[api] override def addTransition(a: Action, to: State): Int = {
var tx: Option[Transaction] = None
try {
tx = Some(guiStateMachine.graphDb.beginTx)
val node = guiStateMachine.getNodeBySutState(sutState).get // TODO #19 What happens if the node is not found?
val relationshipTypeAction = RelationshipTypeAction(a)
val existingRelationships = node.getRelationships(relationshipTypeAction, Direction.OUTGOING)
var existingRelationship: Option[Relationship] = None
val iterator = existingRelationships.iterator()
while (iterator.hasNext && existingRelationship.isEmpty) {
val relationship = iterator.next()
val sutState = relationship.getEndNode().getProperty("sutState").asInstanceOf[SutState]
if (to.getSutState == sutState) {
existingRelationship = Some(relationship)
}
}

val counter = if (existingRelationship.isEmpty) {
val other = guiStateMachine.getNodeBySutState(to.getSutState).get // TODO #19 What happens if the node is not found?
node.createRelationshipTo(other, relationshipTypeAction)
1
} else {
val r = existingRelationship.get
val counter = r.getProperty("counter").asInstanceOf[Int] + 1
existingRelationship.get.setProperty("counter", counter)
counter
}
tx.get.success()
counter
} finally {
if (tx.isDefined) { tx.get.close() }
}
}

private def getNode(): Node = {
var tx: Option[Transaction] = None

try {
tx = Some(guiStateMachine.graphDb.beginTx)
val node = guiStateMachine.getNodeBySutState(sutState).get // TODO #19 What happens if the node is not found?
tx.get.success()
node
} finally {
if (tx.isDefined) { tx.get.close() }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.retest.guistatemachine.api.neo4j

import org.neo4j.graphdb.Label

object SutStateLabel extends Label {
override def name() = "SutState"
}

0 comments on commit 5c07ee3

Please sign in to comment.