From b99fee574f9e83e9b0f78ef8de34d3bde79455dd Mon Sep 17 00:00:00 2001 From: Tamino Dauth Date: Wed, 27 Mar 2019 14:43:13 +0100 Subject: [PATCH] Use Neo4J-OGM #19 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. --- README.md | 1 + .../api/neo4j/ActionConverter.scala | 12 +++ .../api/neo4j/ActionTransitionEntity.scala | 18 +++- .../guistatemachine/api/neo4j/Example.scala | 1 - .../api/neo4j/GuiStateMachineApiNeo4J.scala | 34 ++++--- .../api/neo4j/GuiStateMachineNeo4J.scala | 80 +++++---------- .../api/neo4j/Neo4jSessionFactory.scala | 29 +++++- .../api/neo4j/RelationshipTypeAction.scala | 8 -- .../api/neo4j/StateNeo4J.scala | 97 +++++++------------ .../api/neo4j/SutStateEntity.scala | 6 +- .../api/neo4j/SutStateLabel.scala | 7 -- 11 files changed, 133 insertions(+), 160 deletions(-) create mode 100644 src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala delete mode 100644 src/main/scala/de/retest/guistatemachine/api/neo4j/RelationshipTypeAction.scala delete mode 100644 src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateLabel.scala diff --git a/README.md b/README.md index d96fcdd..8ec10a9 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,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. diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala new file mode 100644 index 0000000..0677924 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala @@ -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 +} diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionTransitionEntity.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionTransitionEntity.scala index 2d0e419..52a3763 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionTransitionEntity.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionTransitionEntity.scala @@ -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 } diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala index b5a6d01..39c0985 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala @@ -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() diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala index 47abb13..f651dd1 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala @@ -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 @@ -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 } diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala index 82420ac..338daad 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala @@ -3,33 +3,26 @@ 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 = { @@ -37,65 +30,42 @@ class GuiStateMachineNeo4J(var graphDb: GraphDatabaseService) extends GuiStateMa 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 { diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4jSessionFactory.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4jSessionFactory.scala index aa0fe2f..f74b28e 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4jSessionFactory.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4jSessionFactory.scala @@ -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() } + } + } } diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/RelationshipTypeAction.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/RelationshipTypeAction.scala deleted file mode 100644 index 42f2503..0000000 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/RelationshipTypeAction.scala +++ /dev/null @@ -1,8 +0,0 @@ -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 -} diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala index 88ce00f..ce27633 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala @@ -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 } } diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateEntity.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateEntity.scala index 1d511ce..6000707 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateEntity.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateEntity.scala @@ -1,8 +1,8 @@ package de.retest.guistatemachine.api.neo4j import de.retest.recheck.ui.descriptors.SutState +import org.neo4j.ogm.annotation._ import org.neo4j.ogm.annotation.typeconversion.Convert -import org.neo4j.ogm.annotation.{GeneratedValue, Id, _} // TODO #19 Use this entity and sessions instead of manual transactions. @NodeEntity @@ -10,8 +10,6 @@ class SutStateEntity(state: SutState) extends Entity { def this() = this(null) - @Id @GeneratedValue private val id = 0L - @Convert(classOf[SutStateConverter]) - private val sutState = state + val sutState: SutState = state } diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateLabel.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateLabel.scala deleted file mode 100644 index d11265d..0000000 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateLabel.scala +++ /dev/null @@ -1,7 +0,0 @@ -package de.retest.guistatemachine.api.neo4j - -import org.neo4j.graphdb.Label - -object SutStateLabel extends Label { - override def name() = "SutState" -}