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

Commit

Permalink
Use Neo4J-OGM #19
Browse files Browse the repository at this point in the history
We use converters and OGM annotations instead of converting SutStates
and Actions manually.
Open sessions for all state machines and create transactions for the sessions.
  • Loading branch information
tdauth committed Apr 9, 2019
1 parent 9394eac commit 3ee5265
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 160 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ 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.
It uses [Neo4J-OGM](https://neo4j.com/docs/ogm-manual/current/) to map our types to the graph database.

Each state machine is represented by a separate graph database stored in a separate directory.
The nodes all have the property "sutState" which contains the corresponding SUT state serialized as XML.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package de.retest.guistatemachine.api.neo4j

import de.retest.surili.commons.actions.{Action, NavigateRefreshAction}
import org.neo4j.ogm.typeconversion.AttributeConverter

/*
* https://github.com/neo4j/neo4j-ogm/blob/master/neo4j-ogm-docs/src/main/asciidoc/reference/conversion.adoc
*/
class ActionConverter extends AttributeConverter[Action, String] {
def toGraphProperty(value: Action): String = value.toString // TODO #19 convert to XML with element
def toEntityAttribute(value: String): Action = new NavigateRefreshAction // TODO #19 convert from XML to an action
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package de.retest.guistatemachine.api.neo4j
import de.retest.recheck.ui.descriptors.SutState
import de.retest.surili.commons.actions.Action
import org.neo4j.ogm.annotation.typeconversion.Convert
import org.neo4j.ogm.annotation.{EndNode, RelationshipEntity, StartNode}

@RelationshipEntity(`type` = "EXECUTED")
class ActionTransitionEntity(start: SutState, end: SutState) extends Entity {
@RelationshipEntity(`type` = "ACTIONS")
class ActionTransitionEntity(s: SutState, e: SutState, a: Action) extends Entity {

def this() = this(null, null)
def this() = this(null, null, null)

@StartNode val s = start
@StartNode val start: SutState = s

@EndNode val e = end
@EndNode val end: SutState = e

@Convert(classOf[ActionConverter])
val action: Action = a

/// The number of times this action has been executed.
var counter: Int = 1
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ object Example extends App {
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")
//stateMachine.clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ 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

Expand All @@ -16,26 +14,32 @@ class GuiStateMachineApiNeo4J extends GuiStateMachineApi {
// 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)
val uri = getUri(name)
Neo4jSessionFactory.getSessionFactory(uri)
logger.info("Created new graph DB in {}.", uri)

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

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

override def removeStateMachine(name: String): Boolean = stateMachines.remove(name).isDefined // TODO #19 Remove from disk?
override def removeStateMachine(name: String): Boolean = stateMachines.get(name) match {
case Some(_) =>
if (stateMachines.remove(name).isDefined) {
val uri = getUri(name)
Neo4jSessionFactory.getSessionFactory(uri).close() // TODO #19 Removes from disk?
true
} else {
false
}
case None => false
}

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

override def clear(): Unit = stateMachines.clear()
override def clear(): Unit = stateMachines.keySet foreach { name => // TODO #19 keys can be modified concurrently
removeStateMachine(name)
} // TODO #19 Removes from disk?

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() } })
}
private def getUri(name: String): String = new File(name).toURI.toString
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,99 +3,69 @@ 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, ResourceIterator, Transaction}
import org.neo4j.ogm.cypher.{ComparisonOperator, Filter}

import scala.collection.immutable.HashMap

class GuiStateMachineNeo4J(var graphDb: GraphDatabaseService) extends GuiStateMachine {
class GuiStateMachineNeo4J(var uri: String) extends GuiStateMachine {
implicit val session = Neo4jSessionFactory.getSessionFactory(uri).openSession() // TODO #19 Save the session at some point and close it at some point

override def getState(sutState: SutState): State = {
var tx: Option[Transaction] = None
try {
tx = Some(graphDb.beginTx)
Neo4jSessionFactory.transaction {
getNodeBySutState(sutState) match {
case None => {
// Create a new node for the sutState in the graph database.
val node = graphDb.createNode
node.addLabel(SutStateLabel)
// TODO #19 SutState is not a supported property value!
val value = new SutStateConverter().toGraphProperty(sutState)
node.setProperty("sutState", value)
}
case None =>
// Create a new node for the SUT state in the graph database.
session.save(new SutStateEntity(sutState))

// Do nothing if the node for the SUT state does already exist.
case _ =>
}
tx.get.success
} finally {
if (tx.isDefined) { tx.get.close() }
}

new StateNeo4J(sutState, this)
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
try {
tx = Some(graphDb.beginTx)
val allNodes = graphDb.getAllNodes()
override def getAllStates: Map[SutState, State] =
Neo4jSessionFactory.transaction {
val allNodes = session.loadAll(classOf[SutStateEntity])
var result = HashMap[SutState, State]()
val iterator = allNodes.iterator()

while (iterator.hasNext) {
val node = iterator.next()
val sutState = getSutState(node)
result = result + (sutState -> new StateNeo4J(sutState, this))
val sutState = node.sutState
result = result + (sutState -> StateNeo4J(sutState, this))
}
tx.get.success()
result
} finally {
if (tx.isDefined) { tx.get.close() }
}
}

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: Option[Transaction] = None
try {
tx = Some(graphDb.beginTx)
override def clear(): Unit =
Neo4jSessionFactory.transaction {
// Deletes all nodes and relationships.
graphDb.execute("MATCH (n)\nDETACH DELETE n")
tx.get.success
} finally {
if (tx.isDefined) { tx.get.close() }
session.deleteAll(classOf[SutStateEntity])
session.deleteAll(classOf[ActionTransitionEntity])
}
}

override def assignFrom(other: GuiStateMachine): Unit = {
clear()
// TODO #19 Should we delete the old graph database?
val otherStateMachine = other.asInstanceOf[GuiStateMachineNeo4J]
graphDb = otherStateMachine.graphDb
}

private[neo4j] def getSutState(node: Node): SutState = {
val value = node.getProperty("sutState").asInstanceOf[String]
new SutStateConverter().toEntityAttribute(value)
uri = otherStateMachine.uri
}

// 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] = {
var nodes: Option[ResourceIterator[Node]] = None
val first = try {
val value = new SutStateConverter().toGraphProperty(sutState)
nodes = Some(graphDb.findNodes(SutStateLabel, "sutState", value))
nodes.get.stream().findFirst()
} finally {
if (nodes.isDefined) {
nodes.get.close()
}
}
// TODO #19 Should always be used inside of a transaction.
private[neo4j] def getNodeBySutState(sutState: SutState): Option[SutStateEntity] = {
val filter = new Filter("sutState", ComparisonOperator.EQUALS, sutState) // TODO #19 Is this how we filter for an attribute?
val first = session.loadAll(classOf[SutStateEntity], filter).stream().findFirst()
if (first.isPresent) {
Some(first.get())
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
package de.retest.guistatemachine.api.neo4j

import org.neo4j.ogm.session.SessionFactory
import org.neo4j.ogm.config.Configuration
import org.neo4j.ogm.session.{Session, SessionFactory}
import org.neo4j.ogm.transaction.Transaction

import scala.collection.concurrent.TrieMap

// TODO #19 Use sessions to modify the state graph.
object Neo4jSessionFactory {
private val sessionFactory = new SessionFactory("de.retest.guistatemachine.api.neo4j")
private val sessionFactories = TrieMap[String, SessionFactory]()

def getSessionFactory(uri: String): SessionFactory = sessionFactories.get(uri) match {
case Some(sessionFactory) => sessionFactory
case None =>
val conf = new Configuration.Builder().uri(uri).build
val sessionFactory = new SessionFactory(conf, "de.retest.guistatemachine.api.neo4j")
sessionFactories += (uri -> sessionFactory)
sessionFactory
}

def getSession() = sessionFactory.openSession()
def transaction[A](f: => A)(implicit session: Session): A = {
var txn: Option[Transaction] = None
try {
txn = Some(session.beginTransaction())
val r = f
txn.get.commit()
r
} finally {
if (txn.isDefined) { txn.get.close() }
}
}
}

This file was deleted.

97 changes: 35 additions & 62 deletions src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,52 @@ 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 org.neo4j.ogm.cypher.{ComparisonOperator, Filter}

import scala.collection.immutable.HashMap

case class StateNeo4J(sutState: SutState, guiStateMachine: GuiStateMachineNeo4J) extends State {
implicit val session = guiStateMachine.session

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 = guiStateMachine.getSutState(relationship.getEndNode)
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) {
override def getTransitions: Map[Action, ActionTransitions] =
Neo4jSessionFactory.transaction {
val filter = new Filter("start", ComparisonOperator.EQUALS, sutState)
val transitions = session.loadAll(classOf[ActionTransitionEntity], filter)
var result = HashMap[Action, ActionTransitions]()
val iterator = transitions.iterator()
while (iterator.hasNext) {
val relationship = iterator.next()
val sutState = guiStateMachine.getSutState(relationship.getEndNode)
if (to.getSutState == sutState) {
existingRelationship = Some(relationship)
val action = relationship.action
val targetSutState = relationship.end
val counter = relationship.counter
val actionTransitions = if (result.contains(action)) {
val existing = result(action)
ActionTransitions(existing.to ++ Set(StateNeo4J(targetSutState, guiStateMachine)), existing.executionCounter + counter)
} else {
ActionTransitions(Set(StateNeo4J(targetSutState, guiStateMachine)), counter)
}
result = result + (action -> actionTransitions)
}

val counter = if (existingRelationship.isEmpty) {
val other = guiStateMachine.getNodeBySutState(to.getSutState).get // TODO #19 What happens if the node is not found?
val relationship = node.createRelationshipTo(other, relationshipTypeAction)
relationship.setProperty("counter", 1)
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() }
result
}
}

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() }
private[api] override def addTransition(a: Action, to: State): Int = Neo4jSessionFactory.transaction {
val filter = new Filter("start", ComparisonOperator.EQUALS, sutState)
val filter2 = new Filter("action", ComparisonOperator.EQUALS, a)
val targetSutState = to.asInstanceOf[StateNeo4J].sutState
val filter3 = new Filter("end", ComparisonOperator.EQUALS, targetSutState)
val transitions = session.loadAll(classOf[ActionTransitionEntity], filter.and(filter2).and(filter3))
val first = transitions.stream().findFirst()
val counter = if (first.isPresent) {
first.get().counter = first.get().counter + 1
session.save(first.get())
first.get().counter
} else {
val transition = new ActionTransitionEntity(sutState, targetSutState, a)
session.save(transition)
1
}
counter
}
}
Loading

0 comments on commit 3ee5265

Please sign in to comment.