diff --git a/build.sbt b/build.sbt
index ad03dbd..d7c138d 100644
--- a/build.sbt
+++ b/build.sbt
@@ -28,6 +28,7 @@ libraryDependencies += "javax.xml.bind" % "jaxb-api" % "2.3.0"
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"
+libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "1.1.1"
// 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/ActionConverter.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala
index 703ecd2..e430873 100644
--- a/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala
+++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala
@@ -1,9 +1,113 @@
package de.retest.guistatemachine.api.neo4j
-import de.retest.surili.commons.actions.{Action, NavigateRefreshAction}
+import de.retest.recheck.ui.descriptors.{Element, SutState}
+import de.retest.surili.commons.actions._
import org.neo4j.ogm.typeconversion.AttributeConverter
-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
+import scala.xml._
+
+/**
+ * We do not want to store the whole target element as XML again. Hence, we store its retest ID which is unique and
+ * matches the element in the SUT state and only the additional attributes.
+ */
+class ActionConverter(val sutState: Option[SutState]) extends AttributeConverter[Action, String] {
+
+ def this() = this(None)
+
+ def toGraphProperty(value: Action): String = {
+ val nodeBuffer = new NodeBuffer
+
+ nodeBuffer += {value.getClass.getSimpleName}
+
+ if (value.getTargetElement.isPresent) {
+ val retestId = value.getTargetElement.get().getRetestId
+ nodeBuffer += {retestId}
+ }
+
+ value match {
+ case a: ChangeValueOfAction =>
+ val sequences = a.getKeysToSend map { sequence =>
+ {sequence.toString}
+ }
+ nodeBuffer += {sequences}
+ case a: NavigateToAction =>
+ nodeBuffer += {a.getUrl}
+ case a: SwitchToWindowAction =>
+ nodeBuffer += {a.getWindowName}
+ case _ =>
+ }
+
+ val topLevelNode = {nodeBuffer}
+ val stringBuilder = new StringBuilder("\n")
+ val prettyPrinter = new PrettyPrinter(0, 2)
+ prettyPrinter.formatNodes(topLevelNode, TopScope, stringBuilder)
+ stringBuilder.toString()
+ }
+ def toEntityAttribute(value: String): Action = sutState match {
+ case Some(state) =>
+ val node = scala.xml.XML.loadString(value)
+ val typeNode = getNodeByTag(node, "type")
+ typeNode.text match {
+ case "ChangeValueOfAction" =>
+ val element = getElement(node, state)
+ val keys = getNodeByTag(node, "keys")
+ val sequences = keys.child map { c =>
+ c.text
+ }
+ new ChangeValueOfAction(element, sequences.toArray)
+ case "ClickOnAction" =>
+ val element = getElement(node, state)
+ new ClickOnAction(element)
+ case "NavigateToAction" =>
+ val urlNode = getNodeByTag(node, "url")
+ new NavigateToAction(urlNode.text)
+ case "NavigateBackAction" => new NavigateBackAction
+ case "NavigateForwardAction" => new NavigateForwardAction
+ case "NavigateRefreshAction" => new NavigateRefreshAction
+ case "SwitchToWindowAction" =>
+ val windowNode = getNodeByTag(node, "window")
+ new SwitchToWindowAction(windowNode.text)
+ case _ => throw new RuntimeException("Unknown type.")
+ }
+
+ case None => throw new RuntimeException("We need the SutState to reconstruct the action")
+ }
+
+ private def getNodeByTag(node: Node, tag: String): Node = {
+ val matchingNodes = node \\ tag
+ if (matchingNodes.isEmpty) {
+ throw new RuntimeException(s"Missing node with tag $tag.")
+ } else {
+ matchingNodes.head
+ }
+ }
+
+ private def getElement(node: Node, sutState: SutState): Element = {
+ val retestId = getNodeByTag(node, "retestId").text
+ getElementByRetestId(retestId, sutState) match {
+ case Some(element) => element
+ case None => throw new RuntimeException(s"Missing element with retestId $retestId")
+ }
+ }
+
+ private def getElementByRetestId(retestId: String, sutState: SutState): Option[Element] = {
+ val elements = scala.collection.mutable.Set[Element]()
+ val iterator = sutState.getRootElements.iterator()
+ var result: Option[Element] = None
+
+ while (iterator.hasNext && result.isEmpty) {
+ val element = iterator.next()
+ result = if (element.getRetestId == retestId) {
+ Some(element)
+ } else { None }
+
+ val nestedIterator = element.getContainedElements.iterator()
+
+ while (nestedIterator.hasNext) {
+ elements += nestedIterator.next()
+ }
+ }
+
+ result
+ }
}
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 52a3763..2e16baa 100644
--- a/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionTransitionEntity.scala
+++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionTransitionEntity.scala
@@ -1,20 +1,23 @@
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}
+import org.neo4j.ogm.annotation.{EndNode, Index, RelationshipEntity, StartNode}
@RelationshipEntity(`type` = "ACTIONS")
class ActionTransitionEntity(s: SutState, e: SutState, a: Action) extends Entity {
def this() = this(null, null, null)
+ @Index
@StartNode val start: SutState = s
+ @Index
@EndNode val end: SutState = e
- @Convert(classOf[ActionConverter])
- val action: Action = a
+ @Index
+ // TODO #19 We need the previous SutState for the conversion back to the action since we rely on the retest ID only to keep the action small.
+ //@Convert(classOf[ActionConverter])
+ val actionXML: String = new ActionConverter().toGraphProperty(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/GuiStateMachineApiNeo4J.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala
index f651dd1..4afc579 100644
--- a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala
+++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala
@@ -37,7 +37,7 @@ class GuiStateMachineApiNeo4J extends GuiStateMachineApi {
override def getStateMachine(name: String): Option[GuiStateMachine] = stateMachines.get(name)
- override def clear(): Unit = stateMachines.keySet foreach { name => // TODO #19 keys can be modified concurrently
+ override def clear(): Unit = stateMachines.keySet foreach { name => // TODO #19 keys can be modified concurrently. So we might not remove all state machines?
removeStateMachine(name)
} // TODO #19 Removes from disk?
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 338daad..a0f225f 100644
--- a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala
+++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala
@@ -8,7 +8,7 @@ import org.neo4j.ogm.cypher.{ComparisonOperator, Filter}
import scala.collection.immutable.HashMap
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
+ implicit val session = Neo4jSessionFactory.getSessionFactory(uri).openSession() // TODO #19 Close the session at some point?
override def getState(sutState: SutState): State = {
Neo4jSessionFactory.transaction {
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 f74b28e..c5c4f5a 100644
--- a/src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4jSessionFactory.scala
+++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4jSessionFactory.scala
@@ -6,7 +6,6 @@ import org.neo4j.ogm.transaction.Transaction
import scala.collection.concurrent.TrieMap
-// TODO #19 Use sessions to modify the state graph.
object Neo4jSessionFactory {
private val sessionFactories = TrieMap[String, SessionFactory]()
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 339c93d..534fec3 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 iterator = transitions.iterator()
while (iterator.hasNext) {
val relationship = iterator.next()
- val action = relationship.action
+ val action = new ActionConverter(Some(relationship.start)).toEntityAttribute(relationship.actionXML)
val targetSutState = relationship.end
val counter = relationship.counter
val actionTransitions = if (result.contains(action)) {
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 6000707..fbe6d6f 100644
--- a/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateEntity.scala
+++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateEntity.scala
@@ -4,12 +4,12 @@ import de.retest.recheck.ui.descriptors.SutState
import org.neo4j.ogm.annotation._
import org.neo4j.ogm.annotation.typeconversion.Convert
-// TODO #19 Use this entity and sessions instead of manual transactions.
@NodeEntity
class SutStateEntity(state: SutState) extends Entity {
def this() = this(null)
+ @Index(unique = true)
@Convert(classOf[SutStateConverter])
val sutState: SutState = state
}
diff --git a/src/test/scala/de/retest/guistatemachine/api/neo4j/ActionConverterSpec.scala b/src/test/scala/de/retest/guistatemachine/api/neo4j/ActionConverterSpec.scala
new file mode 100644
index 0000000..c0d8e0b
--- /dev/null
+++ b/src/test/scala/de/retest/guistatemachine/api/neo4j/ActionConverterSpec.scala
@@ -0,0 +1,109 @@
+package de.retest.guistatemachine.api.neo4j
+
+import java.util
+
+import de.retest.guistatemachine.api.AbstractApiSpec
+import de.retest.surili.commons.actions._
+import org.scalatest.BeforeAndAfterEach
+
+class ActionConverterSpec extends AbstractApiSpec with BeforeAndAfterEach {
+ private val rootElement = getRootElement("a", 0)
+ private val sutState = createSutState(rootElement)
+ private val cut = new ActionConverter(Some(sutState))
+
+ "ActionConverter" should {
+
+ "convert ChangeValueOfAction" in {
+ val list = util.Arrays.asList("foo", "bar", "waa")
+ val cs = list.toArray(new Array[CharSequence](list.size))
+ val action = new ChangeValueOfAction(rootElement, cs)
+
+ val result = cut.toGraphProperty(action)
+ result shouldEqual
+ """
+ |ChangeValueOfActionretestIdfoobarwaa
+ |""".stripMargin
+
+ val loadedAction = cut.toEntityAttribute(result)
+ loadedAction shouldEqual action
+ }
+
+ "convert ClickOnAction" in {
+ val action = new ClickOnAction(rootElement)
+
+ val result = cut.toGraphProperty(action)
+ result shouldEqual
+ """
+ |ClickOnActionretestId
+ |""".stripMargin
+
+ val loadedAction = cut.toEntityAttribute(result)
+ loadedAction shouldEqual action
+ }
+
+ "convert NavigateToAction" in {
+ val action = new NavigateToAction("http://google.com")
+
+ val result = cut.toGraphProperty(action)
+ result shouldEqual
+ """
+ |NavigateToActionhttp://google.com
+ |""".stripMargin
+
+ val loadedAction = cut.toEntityAttribute(result)
+ loadedAction shouldEqual action
+ }
+
+ "convert NavigateBackAction" in {
+ val action = new NavigateBackAction()
+
+ val result = cut.toGraphProperty(action)
+ result shouldEqual
+ """
+ |NavigateBackAction
+ |""".stripMargin
+
+ val loadedAction = cut.toEntityAttribute(result)
+ loadedAction shouldEqual action
+ }
+
+ "convert NavigateForwardAction" in {
+ val action = new NavigateForwardAction()
+
+ val result = cut.toGraphProperty(action)
+ result shouldEqual
+ """
+ |NavigateForwardAction
+ |""".stripMargin
+
+ val loadedAction = cut.toEntityAttribute(result)
+ loadedAction shouldEqual action
+ }
+
+ "convert NavigateRefreshAction" in {
+ val action = new NavigateRefreshAction()
+
+ val result = cut.toGraphProperty(action)
+ result shouldEqual
+ """
+ |NavigateRefreshAction
+ |""".stripMargin
+
+ val loadedAction = cut.toEntityAttribute(result)
+ loadedAction shouldEqual action
+ }
+
+ "convert SwitchToWindowAction" in {
+ val action = new SwitchToWindowAction("test")
+
+ val result = cut.toGraphProperty(action)
+ result shouldEqual
+ """
+ |SwitchToWindowActiontest
+ |""".stripMargin
+
+ val loadedAction = cut.toEntityAttribute(result)
+ loadedAction shouldEqual action
+ }
+ }
+}