From 80db55047977f6b82e1730499c765cb6b30e24d7 Mon Sep 17 00:00:00 2001 From: Tamino Dauth Date: Tue, 26 Mar 2019 18:06:55 +0100 Subject: [PATCH] Store SutStates as string properties Use the utility class XmlTransformerUtil for the XML conversion. Besides, improve the transaction code. --- README.md | 8 +-- build.sbt | 2 + .../guistatemachine/api/neo4j/Example.scala | 3 ++ .../api/neo4j/GuiStateMachineNeo4J.scala | 54 ++++++++++++------- .../api/neo4j/StateNeo4J.scala | 7 +-- .../api/neo4j/SutStateConverter.scala | 16 ++++++ 6 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateConverter.scala diff --git a/README.md b/README.md index 239fc4e..d96fcdd 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,8 @@ 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. +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. 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 diff --git a/build.sbt b/build.sbt index ecfa4c4..4b1fe56 100644 --- a/build.sbt +++ b/build.sbt @@ -25,6 +25,8 @@ libraryDependencies += "de.retest" % "surili-commons" % "0.1.0-SNAPSHOT" withSou // Dependencies for a graph database: libraryDependencies += "org.neo4j" % "neo4j" % "3.0.1" +libraryDependencies += "org.neo4j" % "neo4j-ogm-core" % "3.1.7" +libraryDependencies += "org.neo4j" % "neo4j-ogm-embedded-driver" % "3.1.7" // Dependencies to write GML files for yEd: libraryDependencies += "com.github.systemdir.gml" % "GMLWriterForYed" % "2.1.0" 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 1e36502..b5a6d01 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala @@ -15,10 +15,13 @@ object Example extends App { private val action1 = new NavigateToAction("http://wikipedia.org") val stateMachine = GuiStateMachineApi.neo4j.createStateMachine("tmp") + //stateMachine.clear() val startState = new SutState(Arrays.asList(rootElementA, rootElementB, rootElementC)) val endState = new SutState(Arrays.asList(rootElementA)) stateMachine.executeAction(startState, action0, endState) + println(s"All states ${stateMachine.getAllStates.size}") + /** * Creates a new identifying attributes collection which should only match other identifying attributes with the same ID. * 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 81e7e9c..82420ac 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala @@ -3,7 +3,7 @@ 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 org.neo4j.graphdb.{GraphDatabaseService, Node, ResourceIterator, Transaction} import scala.collection.immutable.HashMap @@ -15,9 +15,12 @@ class GuiStateMachineNeo4J(var graphDb: GraphDatabaseService) extends GuiStateMa tx = Some(graphDb.beginTx) 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! - node.setProperty("sutState", sutState) + val value = new SutStateConverter().toGraphProperty(sutState) + node.setProperty("sutState", value) } case _ => } @@ -36,24 +39,22 @@ class GuiStateMachineNeo4J(var graphDb: GraphDatabaseService) extends GuiStateMa override def getAllStates: Map[SutState, State] = { var tx: Option[Transaction] = None - val allNodes = try { + try { tx = Some(graphDb.beginTx) val allNodes = graphDb.getAllNodes() + 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)) + } tx.get.success() - allNodes + result } 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 @@ -61,14 +62,14 @@ class GuiStateMachineNeo4J(var graphDb: GraphDatabaseService) extends GuiStateMa 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 + var tx: Option[Transaction] = None try { - tx = graphDb.beginTx + tx = Some(graphDb.beginTx) // Deletes all nodes and relationships. graphDb.execute("MATCH (n)\nDETACH DELETE n") - tx.success + tx.get.success } finally { - if (tx != null) { tx.close() } + if (tx.isDefined) { tx.get.close() } } } @@ -78,10 +79,23 @@ class GuiStateMachineNeo4J(var graphDb: GraphDatabaseService) extends GuiStateMa graphDb = otherStateMachine.graphDb } + private[neo4j] def getSutState(node: Node): SutState = { + val value = node.getProperty("sutState").asInstanceOf[String] + new SutStateConverter().toEntityAttribute(value) + } + // 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() + 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() + } + } if (first.isPresent) { Some(first.get()) } else { 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 da640ea..88ce00f 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala @@ -18,7 +18,7 @@ case class StateNeo4J(sutState: SutState, guiStateMachine: GuiStateMachineNeo4J) val relationship = iterator.next() val relationshipTypeAction = relationship.getType.asInstanceOf[RelationshipTypeAction] val action = relationshipTypeAction.action - val sutState = relationship.getEndNode.getProperty("sutState").asInstanceOf[SutState] + 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) @@ -41,7 +41,7 @@ case class StateNeo4J(sutState: SutState, guiStateMachine: GuiStateMachineNeo4J) val iterator = existingRelationships.iterator() while (iterator.hasNext && existingRelationship.isEmpty) { val relationship = iterator.next() - val sutState = relationship.getEndNode().getProperty("sutState").asInstanceOf[SutState] + val sutState = guiStateMachine.getSutState(relationship.getEndNode) if (to.getSutState == sutState) { existingRelationship = Some(relationship) } @@ -49,7 +49,8 @@ case class StateNeo4J(sutState: SutState, guiStateMachine: GuiStateMachineNeo4J) 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) + val relationship = node.createRelationshipTo(other, relationshipTypeAction) + relationship.setProperty("counter", 1) 1 } else { val r = existingRelationship.get diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateConverter.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateConverter.scala new file mode 100644 index 0000000..e1d81c7 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateConverter.scala @@ -0,0 +1,16 @@ +package de.retest.guistatemachine.api.neo4j +import java.io.ByteArrayInputStream +import java.nio.charset.StandardCharsets + +import de.retest.recheck.XmlTransformerUtil +import de.retest.recheck.ui.descriptors.SutState +import org.neo4j.ogm.typeconversion.AttributeConverter + +/* + * https://github.com/neo4j/neo4j-ogm/blob/master/neo4j-ogm-docs/src/main/asciidoc/reference/conversion.adoc + */ +class SutStateConverter extends AttributeConverter[SutState, String] { + def toGraphProperty(value: SutState): String = XmlTransformerUtil.getXmlTransformer.toXML(value) + def toEntityAttribute(value: String): SutState = + XmlTransformerUtil.getXmlTransformer.fromXML[SutState](new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8))) +}