From e650379d5cdae875e4329d7d0ba9b801b529a189 Mon Sep 17 00:00:00 2001 From: Tamino Dauth Date: Thu, 21 Mar 2019 11:49:12 +0100 Subject: [PATCH] Initial Neo4J support #19 Use an embedded database and the Java API. --- README.md | 17 ++++ build.sbt | 5 + .../api/GuiStateMachineApi.scala | 7 +- .../guistatemachine/api/neo4j/Example.scala | 55 +++++++++++ .../api/neo4j/GuiStateMachineApiNeo4J.scala | 41 +++++++++ .../api/neo4j/GuiStateMachineNeo4J.scala | 91 +++++++++++++++++++ .../api/neo4j/RelationshipTypeAction.scala | 8 ++ .../api/neo4j/StateNeo4J.scala | 79 ++++++++++++++++ .../api/neo4j/SutStateLabel.scala | 7 ++ 9 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/neo4j/RelationshipTypeAction.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateLabel.scala diff --git a/README.md b/README.md index f4d1e94..f1f41f7 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,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 diff --git a/build.sbt b/build.sbt index 2f81d1b..ecfa4c4 100644 --- a/build.sbt +++ b/build.sbt @@ -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" diff --git a/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala b/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala index 65f57d0..dcb7872 100644 --- a/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala +++ b/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala @@ -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 @@ -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 } diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala new file mode 100644 index 0000000..1e36502 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/Example.scala @@ -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 + } +} diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala new file mode 100644 index 0000000..47abb13 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala @@ -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() } }) + } +} diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala new file mode 100644 index 0000000..81e7e9c --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala @@ -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 + } + } +} diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/RelationshipTypeAction.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/RelationshipTypeAction.scala new file mode 100644 index 0000000..42f2503 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/RelationshipTypeAction.scala @@ -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 +} diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala new file mode 100644 index 0000000..da640ea --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala @@ -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() } + } + } +} diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateLabel.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateLabel.scala new file mode 100644 index 0000000..d11265d --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateLabel.scala @@ -0,0 +1,7 @@ +package de.retest.guistatemachine.api.neo4j + +import org.neo4j.graphdb.Label + +object SutStateLabel extends Label { + override def name() = "SutState" +}