Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bring back graphml exporter (adding more tests and a test domain etc.) #181

Merged
merged 8 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/main/scala/flatgraph/Accessors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ object Accessors {
new ISeq(vals, qty(seq), qty(seq + 1))
}

def getNodeProperties(node: GNode): IterableOnce[(String, AnyRef)] = {
def getNodeProperties(node: GNode): IterableOnce[(String, IndexedSeq[Any])] = {
val schema = node.graph.schema
for {
propertyKind <- schema.propertyKinds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@ class DomainClassesGenerator(schema: Schema) {
val basePackage = schema.basePackage

val outputDir0 = {
val outputDirRoot = os.Path(outputDir.toAbsolutePath)

// start with a clean slate
os.remove.all(outputDirRoot)

val outputDirRoot = os.Path(outputDir.toAbsolutePath)
val outputDirForBasePackage = outputDirRoot / os.RelPath(basePackage.replace('.', '/'))

// start clean
os.remove.all(outputDirForBasePackage)
os.makeDir.all(outputDirForBasePackage)
outputDirForBasePackage
}
Expand All @@ -58,7 +57,9 @@ class DomainClassesGenerator(schema: Schema) {
schema.allNodeTypes.map { nodeType =>
nodeType -> nodeType.properties.toSet.diff(nodeType.extendzRecursively.flatMap(_.properties).toSet)
}.toMap
val newPropsAtNodeList = newPropertiesByNodeType.mapValues(_.toList.sortBy(_.name))
val newPropsAtNodeList = newPropertiesByNodeType.view.map { case (key, values) =>
key -> values.toList.sortBy(_.name)
}.toMap
val newExtendzMap = schema.allNodeTypes.map { nodeType =>
nodeType -> nodeType.extendz.toSet.diff(nodeType.extendzRecursively.flatMap(_.extendz).toSet).toList.sortBy(_.name)
}.toMap
Expand Down Expand Up @@ -213,7 +214,7 @@ class DomainClassesGenerator(schema: Schema) {
if (edgeType.properties.length > 1) throw new RuntimeException("we only support zero or one edge properties")

// format: off
val accessor = if (edgeType.properties.length == 1) {
val propertyAccessor = if (edgeType.properties.length == 1) {
val p = edgeType.properties.head
p.cardinality match {
case _: Cardinality.One[?] =>
Expand All @@ -227,12 +228,15 @@ class DomainClassesGenerator(schema: Schema) {
case Cardinality.List => throw new RuntimeException("edge properties are only supported with cardinality one or optional")
}
} else ""
// format: on

s"""class ${edgeType.className}(src_4762: flatgraph.GNode, dst_4762: flatgraph.GNode, subSeq_4862: Int, property_4862: Any)
| extends flatgraph.Edge(src_4762, dst_4762, ${edgeKindByEdgeType(
edgeType
)}.toShort, subSeq_4862, property_4862) $accessor""".stripMargin
s"""object ${edgeType.className} {
| val Label = "${edgeType.name}"
|}
|
|class ${edgeType.className}(src_4762: flatgraph.GNode, dst_4762: flatgraph.GNode, subSeq_4862: Int, property_4862: Any)
| extends flatgraph.Edge(src_4762, dst_4762, ${edgeKindByEdgeType(edgeType)}.toShort, subSeq_4862, property_4862) $propertyAccessor
|""".stripMargin
// format: on
}
.mkString(
s"""package $basePackage.edges
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
package flatgraph.formats.graphml

import better.files.File
import flatgraph.testdomains.gratefuldead.GratefulDead
import flatgraph.testdomains.gratefuldead.Language.*
import flatgraph.DiffGraphApplier
import flatgraph.util.DiffTool
import org.scalatest.matchers.should.Matchers.*
import org.scalatest.wordspec.AnyWordSpec
import flatgraph.util.DiffTool

import java.lang.System.lineSeparator
import java.nio.file.Paths
import scala.jdk.CollectionConverters.{CollectionHasAsScala, IterableHasAsJava}
import scala.jdk.CollectionConverters.CollectionHasAsScala

class GraphMLTests extends AnyWordSpec {

"import minified gratefuldead graph" in {
import flatgraph.testdomains.gratefuldead.GratefulDead
import flatgraph.testdomains.gratefuldead.Language.*
val gratefulDead = GratefulDead.empty
val graph = gratefulDead.graph
graph.nodeCount() shouldBe 0

GraphMLImporter.runImport(graph, Paths.get(getClass.getResource("/graphml-small.xml").toURI))
GraphMLImporter.runImport(graph, Paths.get(this.getClass.getResource("/graphml-small.xml").toURI))
graph.nodeCount() shouldBe 3
graph.edgeCount() shouldBe 2

Expand All @@ -38,83 +39,86 @@ class GraphMLTests extends AnyWordSpec {
graph.close()
}

// "Exporter should export valid xml" when {
// "not using (unsupported) list properties" in {
// val graph = SimpleDomain.newGraph()
//
// val node2 = graph.addNode(2, TestNode.LABEL, TestNode.STRING_PROPERTY, "stringProp2")
// val node3 = graph.addNode(3, TestNode.LABEL, TestNode.INT_PROPERTY, 13)
//
// // only allows values defined in FunkyList.funkyWords
// val funkyList = new FunkyList()
// funkyList.add("apoplectic")
// funkyList.add("bucolic")
// val node1 = graph.addNode(1, TestNode.LABEL, TestNode.INT_PROPERTY, 11, TestNode.STRING_PROPERTY, "<stringProp1>")
//
// node1.addEdge(TestEdge.LABEL, node2, TestEdge.LONG_PROPERTY, Long.MaxValue)
// node2.addEdge(TestEdge.LABEL, node3)
//
// File.usingTemporaryDirectory(getClass.getName) { exportRootDirectory =>
// val exportResult = GraphMLExporter.runExport(graph, exportRootDirectory.pathAsString)
// exportResult.nodeCount shouldBe 3
// exportResult.edgeCount shouldBe 2
// val Seq(graphMLFile) = exportResult.files
//
// // import graphml into new graph, use difftool for round trip of conversion
// val reimported = SimpleDomain.newGraph()
// GraphMLImporter.runImport(reimported, graphMLFile)
// val diff = DiffTool.compare(graph, reimported)
// withClue(
// s"original graph and reimport from graphml should be completely equal, but there are differences:\n" +
// diff.asScala.mkString("\n") +
// "\n"
// ) {
// diff.size shouldBe 0
// }
// }
// }
//
// "using list properties" in {
// val graph = SimpleDomain.newGraph()
//
// // will discard the list properties
// val node1 = graph.addNode(
// 1,
// TestNode.LABEL,
// TestNode.INT_PROPERTY,
// 11,
// TestNode.STRING_PROPERTY,
// "<stringProp1>",
// TestNode.STRING_LIST_PROPERTY,
// List("stringListProp1a", "stringListProp1b").asJava,
// TestNode.INT_LIST_PROPERTY,
// List(21, 31, 41).asJava
// )
//
// File.usingTemporaryDirectory(getClass.getName) { exportRootDirectory =>
// val exportResult = GraphMLExporter.runExport(graph, exportRootDirectory.pathAsString)
// exportResult.nodeCount shouldBe 1
// exportResult.edgeCount shouldBe 0
// exportResult.additionalInfo.get should include("discarded 2 list properties")
// val Seq(graphMLFile) = exportResult.files
//
// // import graphml into new graph, use difftool for round trip of conversion
// val reimported = SimpleDomain.newGraph()
// GraphMLImporter.runImport(reimported, graphMLFile)
// val diff = DiffTool.compare(graph, reimported)
// val diffString = diff.asScala.mkString(lineSeparator)
// withClue(
// s"because the original graph contained two list properties, and those are not supported by graphml, " +
// s"the exporter drops them. therefor they'll not be part of the reimported graph" +
// diffString +
// lineSeparator
// ) {
// diff.size shouldBe 2
// diffString should include("IntListProperty")
// diffString should include("StringListProperty")
// }
// }
// }
// }
"Exporter should export valid xml" when {
import flatgraph.testdomains.generic.GenericDomain
import flatgraph.testdomains.generic.Language.*
import flatgraph.testdomains.generic.edges.ConnectedTo
import flatgraph.testdomains.generic.nodes.NewNodeA

"not using (unsupported) list properties" in {
val genericDomain = GenericDomain.empty
val graph = genericDomain.graph

val node1 = NewNodeA().stringOptional("node 1 opt")
val node2 = NewNodeA().stringMandatory("node 2 mandatory").stringOptional("node 2 opt")
val node3 = NewNodeA().intMandatory(1).intOptional(2)

DiffGraphApplier.applyDiff(
graph,
GenericDomain.newDiffGraphBuilder
.addEdge(node1, node2, ConnectedTo.Label)
.addEdge(node2, node3, ConnectedTo.Label)
)

File.usingTemporaryDirectory(this.getClass.getName) { exportRootDirectory =>
val exportResult = GraphMLExporter.runExport(graph, exportRootDirectory.pathAsString)
exportResult.nodeCount shouldBe 3
exportResult.edgeCount shouldBe 2
val Seq(graphMLFile) = exportResult.files

// import graphml into new graph, use difftool for round trip of conversion
val reimported = GenericDomain.empty.graph
GraphMLImporter.runImport(reimported, graphMLFile)
val diff = DiffTool.compare(graph, reimported)
withClue(
s"original graph and reimport from graphml should be completely equal, but there are differences:\n" +
diff.asScala.mkString("\n") +
"\n"
) {
diff.size shouldBe 0
}
}
}

"using list properties" in {
val genericDomain = GenericDomain.empty
val graph = genericDomain.graph

// exporter will discard the list properties, but inform the user about it
val node1 = NewNodeA().stringMandatory("node 2 a").stringOptional("node 2 b").stringList(Seq("node 3 c1", "node 3 c2"))
val node2 = NewNodeA().intMandatory(1).intOptional(2).intList(Seq(10, 11, 12))

DiffGraphApplier.applyDiff(
graph,
GenericDomain.newDiffGraphBuilder
.addEdge(node1, node2, ConnectedTo.Label)
)

File.usingTemporaryDirectory(this.getClass.getName) { exportRootDirectory =>
val exportResult = GraphMLExporter.runExport(graph, exportRootDirectory.pathAsString)
exportResult.nodeCount shouldBe 2
exportResult.edgeCount shouldBe 1
exportResult.additionalInfo.get should include("discarded 2 list properties")

val Seq(graphMLFile) = exportResult.files

// import graphml into new graph, use difftool for round trip of conversion
val reimported = GenericDomain.empty.graph
GraphMLImporter.runImport(reimported, graphMLFile)
val diff = DiffTool.compare(graph, reimported)
val diffString = diff.asScala.mkString(lineSeparator)
withClue(
s"because the original graph contained two list properties, and those are not supported by graphml, " +
s"the exporter drops them. therefor they'll not be part of the reimported graph" +
diffString +
lineSeparator
) {
diff.size shouldBe 2
diffString should include("Seq(10, 11, 12)")
diffString should include("Seq(node 3 c1, node 3 c2)")
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package flatgraph.formats.graphml

import flatgraph.formats.{ExportResult, Exporter, isList, resolveOutputFileSingle, writeFile}
import flatgraph.{Accessors, Edge, GNode, Graph, Schema}
import flatgraph.{Accessors, Edge, FormalQtyType, GNode, Schema}

import java.lang.System.lineSeparator
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicInteger
import scala.collection.mutable
import scala.jdk.CollectionConverters.MapHasAsScala
import scala.xml.{PrettyPrinter, XML}

/** Exports OverflowDB Graph to GraphML
*
* Warning: list properties are not natively supported by graphml... We initially built some support for those which deviated from the
* spec, but given that other tools don't support it, some refusing to import the remainder, we've dropped it. Now, lists are serialised to
* `;`-separated strings.
* spec, but given that other tools don't support it and the complications re re-importing, we also dropped support for lists. Now, lists
* are dropped and we print a warning.
*
* https://en.wikipedia.org/wiki/GraphML http://graphml.graphdrawing.org/primer/graphml-primer.html
*/
Expand All @@ -29,9 +28,29 @@ object GraphMLExporter extends Exporter {
val discardedListPropertyCount = new AtomicInteger(0)

val nodeEntries = nodes.iterator.map { node =>
val properties = schema.propertyKinds.flatMap { propertyKind =>
val graph = node.graph
val nodeKind = node.nodeKind
val nodeSeq = node.seq()
val valueMaybe = schema.getNodePropertyFormalQuantity(nodeKind, propertyKind) match {
case FormalQtyType.QtyNone =>
None
case FormalQtyType.QtyOne | FormalQtyType.QtyOption =>
Accessors.getNodePropertyOption[Object](graph, nodeKind, propertyKind, nodeSeq)
case FormalQtyType.QtyMulti =>
Option(Accessors.getNodePropertyMulti[Object](graph, nodeKind, propertyKind, nodeSeq)).filter(_.nonEmpty).flatMap { p =>
// as per class scaladoc: we want to skip list properties, but keep track so we can later inform the user about it...
discardedListPropertyCount.incrementAndGet()
None
}
}
valueMaybe.map { value =>
schema.getPropertyLabel(nodeKind, propertyKind) -> value
}
}
s"""<node id="${node.id}">
| <data key="$KeyForNodeLabel">${node.label}</data>
| ${dataEntries("node", node.label(), Accessors.getNodeProperties(node), nodePropertyContextById, discardedListPropertyCount)}
| ${dataEntries("node", node.label(), properties, nodePropertyContextById, discardedListPropertyCount)}
|</node>
|""".stripMargin
}.toSeq
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,12 @@ object GraphMLImporter extends Importer {
entry \@ "key" match {
case KeyForNodeLabel => // ignore, already extracted in `addNodesRaw`
case key =>
val propertyType = nodePropertyContextById
val propertyContext = nodePropertyContextById
.get(key)
.getOrElse(throw new AssertionError(s"key $key not found in propertyContext..."))
.tpe
val value = entry.text
val convertedValue = convertValue(value, propertyType, context = graphmlNode)
diffGraph.setNodeProperty(graphmlNodeIdToGNode(nodeId), key, value)
val convertedValue = convertValue(value, propertyContext.tpe, context = graphmlNode)
diffGraph.setNodeProperty(graphmlNodeIdToGNode(nodeId), propertyContext.name, convertedValue)
}
}

Expand Down
Loading
Loading