You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@s2graph.apache.org by st...@apache.org on 2017/07/01 15:28:22 UTC
[31/46] incubator-s2graph git commit: bug fix on S2Edge.vertices.
bug fix on S2Edge.vertices.
Project: http://git-wip-us.apache.org/repos/asf/incubator-s2graph/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-s2graph/commit/38ec51dc
Tree: http://git-wip-us.apache.org/repos/asf/incubator-s2graph/tree/38ec51dc
Diff: http://git-wip-us.apache.org/repos/asf/incubator-s2graph/diff/38ec51dc
Branch: refs/heads/master
Commit: 38ec51dca0bbc21893142878807345c8f7286742
Parents: ed035a3
Author: DO YUNG YOON <st...@apache.org>
Authored: Wed May 3 08:23:39 2017 +0900
Committer: DO YUNG YOON <st...@apache.org>
Committed: Wed May 3 08:23:39 2017 +0900
----------------------------------------------------------------------
.../scala/org/apache/s2graph/core/S2Edge.scala | 23 +++--
.../scala/org/apache/s2graph/core/S2Graph.scala | 49 +++++++--
.../core/tinkerpop/S2GraphProvider.scala | 102 ++++++++++---------
.../core/tinkerpop/structure/S2GraphTest.scala | 75 +++++++++++++-
4 files changed, 180 insertions(+), 69 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/38ec51dc/s2core/src/main/scala/org/apache/s2graph/core/S2Edge.scala
----------------------------------------------------------------------
diff --git a/s2core/src/main/scala/org/apache/s2graph/core/S2Edge.scala b/s2core/src/main/scala/org/apache/s2graph/core/S2Edge.scala
index 5712754..28a0b06 100644
--- a/s2core/src/main/scala/org/apache/s2graph/core/S2Edge.scala
+++ b/s2core/src/main/scala/org/apache/s2graph/core/S2Edge.scala
@@ -561,6 +561,7 @@ case class S2Edge(innerGraph: S2Graph,
override def toString: String = {
// E + L_BRACKET + edge.id() + R_BRACKET + L_BRACKET + edge.outVertex().id() + DASH + edge.label() + ARROW + edge.inVertex().id() + R_BRACKET;
s"e[${id}][${srcForVertex.id}-${innerLabel.label}->${tgtForVertex.id}]"
+ // s"e[${srcForVertex.id}-${innerLabel.label}->${tgtForVertex.id}]"
}
def checkProperty(key: String): Boolean = propsWithTs.containsKey(key)
@@ -604,18 +605,20 @@ case class S2Edge(innerGraph: S2Graph,
direction match {
case Direction.OUT =>
- val newVertexId = this.direction match {
- case "out" => VertexId(innerLabel.srcColumn, srcVertex.innerId)
- case "in" => VertexId(innerLabel.tgtColumn, tgtVertex.innerId)
- case _ => throw new IllegalArgumentException("direction can only be out/in.")
- }
+// val newVertexId = this.direction match {
+// case "out" => VertexId(innerLabel.srcColumn, srcVertex.innerId)
+// case "in" => VertexId(innerLabel.tgtColumn, tgtVertex.innerId)
+// case _ => throw new IllegalArgumentException("direction can only be out/in.")
+// }
+ val newVertexId = edgeId.srcVertexId
innerGraph.getVertex(newVertexId).foreach(arr.add)
case Direction.IN =>
- val newVertexId = this.direction match {
- case "in" => VertexId(innerLabel.srcColumn, srcVertex.innerId)
- case "out" => VertexId(innerLabel.tgtColumn, tgtVertex.innerId)
- case _ => throw new IllegalArgumentException("direction can only be out/in.")
- }
+// val newVertexId = this.direction match {
+// case "in" => VertexId(innerLabel.srcColumn, srcVertex.innerId)
+// case "out" => VertexId(innerLabel.tgtColumn, tgtVertex.innerId)
+// case _ => throw new IllegalArgumentException("direction can only be out/in.")
+// }
+ val newVertexId = edgeId.tgtVertexId
innerGraph.getVertex(newVertexId).foreach(arr.add)
case _ =>
import scala.collection.JavaConversions._
http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/38ec51dc/s2core/src/main/scala/org/apache/s2graph/core/S2Graph.scala
----------------------------------------------------------------------
diff --git a/s2core/src/main/scala/org/apache/s2graph/core/S2Graph.scala b/s2core/src/main/scala/org/apache/s2graph/core/S2Graph.scala
index f05f2cb..a0d8f37 100644
--- a/s2core/src/main/scala/org/apache/s2graph/core/S2Graph.scala
+++ b/s2core/src/main/scala/org/apache/s2graph/core/S2Graph.scala
@@ -540,10 +540,42 @@ object S2Graph {
))
@Graph.OptOuts(value = Array(
/** Process */
-// new Graph.OptOut(
-// test = "org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchTest$Traversals",
-// method = "g_V_valueMap_matchXa_selectXnameX_bX",
-// reason = "Hadoop-Gremlin is OLAP-oriented and for OLTP operations, linear-scan joins are required. This particular tests takes many minutes to execute."),
+// new Graph.OptOut(test = "org.apache.tinkerpop.gremlin.process.traversal.step.branch.BranchTest$Traversals", method = "*", reason = "no"),
+ // passed: all
+
+// new Graph.OptOut(test = "org.apache.tinkerpop.gremlin.process.traversal.step.branch.ChooseTest$Traversals", method = "*", reason = "no"),
+ // passed: , failed: g_V_chooseXlabel_eqXpersonX__outXknowsX__inXcreatedXX_name
+
+// new Graph.OptOut(test = "org.apache.tinkerpop.gremlin.process.traversal.step.branch.OptionalTest$Traversals", method = "*", reason = "no"),
+// passed: all
+
+// new Graph.OptOut(test = "org.apache.tinkerpop.gremlin.process.traversal.step.branch.LocalTest$Traversals", method = "*", reason = "no"),
+// failed: g_V_localXmatchXproject__created_person__person_name_nameX_selectXname_projectX_by_byXnameX,
+// g_V_localXbothEXcreatedX_limitX1XX_otherV_name
+// g_VX4X_localXbothEX1_createdX_limitX1XX
+//g_VX4X_localXbothEXknows_createdX_limitX1XX
+
+// new Graph.OptOut(test = "org.apache.tinkerpop.gremlin.process.traversal.step.branch.RepeatTest$Traversals", method = "*", reason = "no"),
+// failed: g_V_repeatXoutX_timesX2X_repeatXinX_timesX2X_name
+
+// new Graph.OptOut(test = "org.apache.tinkerpop.gremlin.process.traversal.step.branch.UnionTest$Traversals", method = "*", reason = "no"),
+//failed: g_V_chooseXlabel_eq_person__unionX__out_lang__out_nameX__in_labelX_groupCount,
+//g_V_unionXout__inX_name
+//g_V_chooseXlabel_eq_person__unionX__out_lang__out_nameX__in_labelX
+//g_V_unionXrepeatXunionXoutXcreatedX__inXcreatedXX_timesX2X__repeatXunionXinXcreatedX__outXcreatedXX_timesX2XX_label_groupCount
+//g_VX1X_unionXrepeatXoutX_timesX2X__outX_name
+
+// new Graph.OptOut(test = "org.apache.tinkerpop.gremlin.process.traversal.step.filter.AndTest$Traversals", method = "*", reason = "no"),
+// failed: g_V_asXaX_outXknowsX_and_outXcreatedX_inXcreatedX_asXaX_name
+
+// new Graph.OptOut(test = "org.apache.tinkerpop.gremlin.process.traversal.step.filter.CoinTest$Traversals", method = "*", reason = "no"),
+// passed: all
+
+// new Graph.OptOut(test = "org.apache.tinkerpop.gremlin.process.traversal.step.filter.CyclicPathTest$Traversals", method = "*", reason = "no"),
+// failed: all
+
+ new Graph.OptOut(test = "org.apache.tinkerpop.gremlin.process.traversal.step.filter.CyclicPathTest$Traversals", method = "*", reason = "no"),
+
/** Structure */
new Graph.OptOut(test="org.apache.tinkerpop.gremlin.structure.EdgeTest$BasicEdgeTest", method="shouldValidateIdEquality", reason="reference equals on EdgeId is not supported."),
@@ -1459,11 +1491,16 @@ class S2Graph(_config: Config)(implicit val ec: ExecutionContext) extends Graph
def fetchEdgesAsync(vertex: S2Vertex, labelNameWithDirs: Seq[(String, String)]): Future[util.Iterator[Edge]] = {
val queryParams = labelNameWithDirs.map { case (l, direction) =>
- QueryParam(labelName = l, direction = direction)
+ QueryParam(labelName = l, direction = direction.toLowerCase)
}
val query = Query.toQuery(Seq(vertex), queryParams)
-
+// val queryRequests = queryParams.map { param => QueryRequest(query, 0, vertex, param) }
+// val ls = new util.ArrayList[Edge]()
+// fetches(queryRequests, Map.empty).map { stepResultLs =>
+// stepResultLs.foreach(_.edgeWithScores.foreach(es => ls.add(es.edge)))
+// ls.iterator()
+// }
getEdges(query).map { stepResult =>
val ls = new util.ArrayList[Edge]()
stepResult.edgeWithScores.foreach(es => ls.add(es.edge))
http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/38ec51dc/s2core/src/test/scala/org/apache/s2graph/core/tinkerpop/S2GraphProvider.scala
----------------------------------------------------------------------
diff --git a/s2core/src/test/scala/org/apache/s2graph/core/tinkerpop/S2GraphProvider.scala b/s2core/src/test/scala/org/apache/s2graph/core/tinkerpop/S2GraphProvider.scala
index 74edd6d..6ac12c8 100644
--- a/s2core/src/test/scala/org/apache/s2graph/core/tinkerpop/S2GraphProvider.scala
+++ b/s2core/src/test/scala/org/apache/s2graph/core/tinkerpop/S2GraphProvider.scala
@@ -25,51 +25,13 @@ object S2GraphProvider {
classOf[S2VertexProperty[_]],
classOf[S2Graph]
)
-}
-
-class S2GraphProvider extends AbstractGraphProvider {
-
- override def getBaseConfiguration(s: String, aClass: Class[_], s1: String, graphData: GraphData): util.Map[String, AnyRef] = {
- val config = ConfigFactory.load()
- val m = new java.util.HashMap[String, AnyRef]()
- m.put(Graph.GRAPH, classOf[S2Graph].getName)
-// m.put("db.default.url", "jdbc:h2:mem:db1;MODE=MYSQL")
- m
- }
-
- override def clear(graph: Graph, configuration: Configuration): Unit =
- if (graph != null) {
- val s2Graph = graph.asInstanceOf[S2Graph]
- if (s2Graph.isRunning) {
-// val labels = Label.findAll()
-// labels.groupBy(_.hbaseTableName).values.foreach { labelsWithSameTable =>
-// labelsWithSameTable.headOption.foreach { label =>
-// s2Graph.management.truncateStorage(label.label)
-// }
-// }
-// s2Graph.shutdown(modelDataDelete = true)
- cleanupSchema
- s2Graph.shutdown(modelDataDelete = true)
- logger.info("S2Graph Shutdown")
- }
- }
-
- override def getImplementations: util.Set[Class[_]] = S2GraphProvider.Implementation.asJava
-
- def initTestSchema(testClass: Class[_], testName: String) = {
- val testClassName = testClass.getSimpleName
- testClass.getSimpleName match {
- case _ =>
- }
- }
-
def initDefaultSchema(graph: S2Graph): Unit = {
val management = graph.management
-// Management.deleteService(DefaultServiceName)
+ // Management.deleteService(DefaultServiceName)
val DefaultService = management.createService(DefaultServiceName, "localhost", "s2graph", 0, None).get
-// Management.deleteColumn(DefaultServiceName, DefaultColumnName)
+ // Management.deleteColumn(DefaultServiceName, DefaultColumnName)
val DefaultColumn = ServiceColumn.findOrInsert(DefaultService.id.get, DefaultColumnName, Some("string"), HBaseType.DEFAULT_VERSION, useCache = false)
val DefaultColumnMetas = {
@@ -95,14 +57,14 @@ class S2GraphProvider extends AbstractGraphProvider {
ColumnMeta.findOrInsert(DefaultColumn.id.get, "acl", "string", useCache = false)
}
-// Management.deleteLabel("_s2graph")
+ // Management.deleteLabel("_s2graph")
val DefaultLabel = management.createLabel("_s2graph", DefaultService.serviceName, DefaultColumn.columnName, DefaultColumn.columnType,
DefaultService.serviceName, DefaultColumn.columnName, DefaultColumn.columnType, true, DefaultService.serviceName, Nil, Nil, "weak", None, None,
options = Option("""{"skipReverse": false}""")
).get
}
- private def cleanupSchema: Unit = {
+ def cleanupSchema: Unit = {
val columnNames = Set(S2Graph.DefaultColumnName, "person", "software", "product", "dog")
val labelNames = Set(S2Graph.DefaultLabelName, "knows", "created", "bought", "test", "self", "friends", "friend", "hate", "collaborator", "test1", "test2", "test3", "pets", "walks", "hates", "link")
@@ -113,6 +75,43 @@ class S2GraphProvider extends AbstractGraphProvider {
Management.deleteLabel(labelName)
}
}
+}
+
+class S2GraphProvider extends AbstractGraphProvider {
+
+ override def getBaseConfiguration(s: String, aClass: Class[_], s1: String, graphData: GraphData): util.Map[String, AnyRef] = {
+ val config = ConfigFactory.load()
+ val m = new java.util.HashMap[String, AnyRef]()
+ m.put(Graph.GRAPH, classOf[S2Graph].getName)
+// m.put("db.default.url", "jdbc:h2:mem:db1;MODE=MYSQL")
+ m
+ }
+
+ override def clear(graph: Graph, configuration: Configuration): Unit =
+ if (graph != null) {
+ val s2Graph = graph.asInstanceOf[S2Graph]
+ if (s2Graph.isRunning) {
+// val labels = Label.findAll()
+// labels.groupBy(_.hbaseTableName).values.foreach { labelsWithSameTable =>
+// labelsWithSameTable.headOption.foreach { label =>
+// s2Graph.management.truncateStorage(label.label)
+// }
+// }
+// s2Graph.shutdown(modelDataDelete = true)
+ S2GraphProvider.cleanupSchema
+ s2Graph.shutdown(modelDataDelete = true)
+ logger.info("S2Graph Shutdown")
+ }
+ }
+
+ override def getImplementations: util.Set[Class[_]] = S2GraphProvider.Implementation.asJava
+
+ def initTestSchema(testClass: Class[_], testName: String) = {
+ val testClassName = testClass.getSimpleName
+ testClass.getSimpleName match {
+ case _ =>
+ }
+ }
// override def openTestGraph(config: Configuration): Graph = new S2Graph(config)(ExecutionContext.global)
@@ -120,9 +119,9 @@ class S2GraphProvider extends AbstractGraphProvider {
val s2Graph = graph.asInstanceOf[S2Graph]
val mnt = s2Graph.getManagement()
- cleanupSchema
+ S2GraphProvider.cleanupSchema
initTestSchema(testClass, testName)
- initDefaultSchema(s2Graph)
+ S2GraphProvider.initDefaultSchema(s2Graph)
val defaultService = Service.findByName(S2Graph.DefaultServiceName).getOrElse(throw new IllegalStateException("default service is not initialized."))
val defaultServiceColumn = ServiceColumn.find(defaultService.id.get, S2Graph.DefaultColumnName).getOrElse(throw new IllegalStateException("default column is not initialized."))
@@ -141,7 +140,8 @@ class S2GraphProvider extends AbstractGraphProvider {
Prop("stars", "0", "integer"),
Prop("since", "0", "integer"),
Prop("myEdgeId", "0", "integer"),
- Prop("data", "-", "string")
+ Prop("data", "-", "string"),
+ Prop("name", "-", "string")
)
// Change dataType for ColumnMeta('aKey') for PropertyFeatureSupportTest
@@ -183,23 +183,27 @@ class S2GraphProvider extends AbstractGraphProvider {
val personColumn = Management.createServiceColumn(defaultService.serviceName, "person", "integer",
- Seq(Prop(T.id.toString, "-1", "integer"), Prop("name", "-", "string"), Prop("age", "0", "integer"), Prop("location", "-", "string")))
- val softwareColumn = Management.createServiceColumn(defaultService.serviceName, "software", "integer", Seq(Prop(T.id.toString, "-1", "integer"), Prop("name", "-", "string"), Prop("lang", "-", "string")))
+ Seq(Prop("name", "-", "string"), Prop("age", "0", "integer"), Prop("location", "-", "string")))
+ val softwareColumn = Management.createServiceColumn(defaultService.serviceName, "software", "integer",
+ Seq(Prop("name", "-", "string"), Prop("lang", "-", "string")))
+
val productColumn = Management.createServiceColumn(defaultService.serviceName, "product", "integer", Nil)
val dogColumn = Management.createServiceColumn(defaultService.serviceName, "dog", "integer", Nil)
// val vertexColumn = Management.createServiceColumn(service.serviceName, "vertex", "integer", Seq(Prop(T.id.toString, "-1", "integer"), Prop("name", "-", "string"), Prop("age", "-1", "integer"), Prop("lang", "scala", "string")))
+ val createdProps = Seq(Prop("weight", "0.0", "double"), Prop("name", "-", "string"))
+
val created =
if (loadGraphWith != null && loadGraphWith.value() == GraphData.MODERN) {
mnt.createLabel("created",
defaultService.serviceName, "person", "integer",
defaultService.serviceName, "software", "integer",
- true, defaultService.serviceName, Nil, Seq(Prop("weight", "0.0", "double")), "strong", None, None)
+ true, defaultService.serviceName, Nil, createdProps, "strong", None, None)
} else {
mnt.createLabel("created",
defaultService.serviceName, defaultServiceColumn.columnName, defaultServiceColumn.columnType,
defaultService.serviceName, defaultServiceColumn.columnName, defaultServiceColumn.columnType,
- true, defaultService.serviceName, Nil, Seq(Prop("weight", "0.0", "double")), "strong", None, None)
+ true, defaultService.serviceName, Nil, createdProps, "strong", None, None)
}
http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/38ec51dc/s2core/src/test/scala/org/apache/s2graph/core/tinkerpop/structure/S2GraphTest.scala
----------------------------------------------------------------------
diff --git a/s2core/src/test/scala/org/apache/s2graph/core/tinkerpop/structure/S2GraphTest.scala b/s2core/src/test/scala/org/apache/s2graph/core/tinkerpop/structure/S2GraphTest.scala
index e6083e9..2a04df3 100644
--- a/s2core/src/test/scala/org/apache/s2graph/core/tinkerpop/structure/S2GraphTest.scala
+++ b/s2core/src/test/scala/org/apache/s2graph/core/tinkerpop/structure/S2GraphTest.scala
@@ -19,14 +19,18 @@
package org.apache.s2graph.core.tinkerpop.structure
+import java.util.function.Predicate
+
+import org.apache.s2graph.core.GraphExceptions.LabelNotExistException
import org.apache.s2graph.core.Management.JsonModel.Prop
import org.apache.s2graph.core._
+import org.apache.s2graph.core.mysqls.{Service, ServiceColumn}
+import org.apache.s2graph.core.tinkerpop.S2GraphProvider
import org.apache.s2graph.core.utils.logger
+import org.apache.tinkerpop.gremlin.process.traversal.Scope
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource
-import org.apache.tinkerpop.gremlin.structure.Graph.Features.EdgePropertyFeatures
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.{in, out}
import org.apache.tinkerpop.gremlin.structure._
-import org.apache.tinkerpop.gremlin.structure.util.Attachable
-import org.apache.tinkerpop.gremlin.structure.util.detached.{DetachedEdge, DetachedFactory}
import org.scalatest.{FunSuite, Matchers}
@@ -409,7 +413,7 @@ class S2GraphTest extends FunSuite with Matchers with TestCommonWithModels {
//// graph.addVertex(T.id, id);
////
//// // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
-//// // to throw the exception
+//// // to throw the excepStion
//// if (!graph.features().vertex().willAllowId(id))
//// fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), FEATURE_ANY_IDS));
//// } catch (Exception e) {
@@ -417,4 +421,67 @@ class S2GraphTest extends FunSuite with Matchers with TestCommonWithModels {
//// }
//// }
// }
+ test("Modern") {
+ val mnt = graph.management
+ S2GraphProvider.cleanupSchema
+ S2GraphProvider.initDefaultSchema(graph)
+
+ val softwareColumn = Management.createServiceColumn(S2Graph.DefaultServiceName, "software", "integer", Seq(Prop(T.id.toString, "-1", "integer"), Prop("name", "-", "string"), Prop("lang", "-", "string")))
+ val personColumn = Management.createServiceColumn(S2Graph.DefaultServiceName, "person", "integer",
+ Seq(Prop(T.id.toString, "-1", "integer"), Prop("name", "-", "string"), Prop("age", "0", "integer"), Prop("location", "-", "string")))
+
+ val knows = mnt.createLabel("knows",
+ S2Graph.DefaultServiceName, "person", "integer",
+ S2Graph.DefaultServiceName, "person", "integer",
+ true, S2Graph.DefaultServiceName, Nil, Seq(Prop("since", "0", "integer"), Prop("year", "0", "integer")), consistencyLevel = "strong", None, None)
+
+ val created = mnt.createLabel("created",
+ S2Graph.DefaultServiceName, "person", "integer",
+ S2Graph.DefaultServiceName, "person", "integer",
+ true, S2Graph.DefaultServiceName, Nil, Seq(Prop("weight", "0.0", "double")), "strong", None, None)
+
+ val g = graph.traversal()
+ val v1 = graph.addVertex(T.label, "person", T.id, Int.box(1), "name", "marko", "age", Int.box(29))
+ val v2 = graph.addVertex(T.label, "person", T.id, Int.box(2), "name", "vadas", "age", Int.box(27))
+ val v3 = graph.addVertex(T.label, "software", T.id, Int.box(3), "name", "lop", "lang", "java")
+ val v4 = graph.addVertex(T.label, "person", T.id, Int.box(4), "name", "josh", "josh", Int.box(32))
+ val v5 = graph.addVertex(T.label, "software", T.id, Int.box(5), "name", "ripple", "lang", "java")
+ val v6 = graph.addVertex(T.label, "person", T.id, Int.box(6), "name", "peter", "age", Int.box(35))
+
+ val e1 = v1.addEdge("created", v3, "weight", Double.box(0.4))
+
+ val e2 = v1.addEdge("knows", v2, "weight", Double.box(0.5))
+ val e3 = v1.addEdge("knows", v4, "weight", Double.box(1.0))
+
+
+ val e4 = v2.addEdge("knows", v1, "weight", Double.box(0.5))
+
+ val e5 = v3.addEdge("created", v1, "weight", Double.box(0.4))
+ val e6 = v3.addEdge("created", v4, "weight", Double.box(0.4))
+ val e7 = v3.addEdge("created", v6, "weight", Double.box(0.2))
+
+ val e8 = v4.addEdge("knows", v1, "weight", Double.box(1.0))
+ val e9 = v4.addEdge("created", v5, "weight", Double.box(1.0))
+ val e10 = v4.addEdge("created", v3, "weight", Double.box(0.4))
+
+ val e11 = v5.addEdge("created", v4, "weight", Double.box(1.0))
+
+ val e12 = v6.addEdge("created", v3, "weight", Double.box(0.2))
+
+ val ls = graph.traversal().V().choose(new Predicate[Vertex] {
+ override def test(t: Vertex): Boolean =
+ t.label().equals("person")
+ }, out("knows"), in("created")).values("name").asAdmin()
+
+ val l = ls.toList
+ logger.error(s"[Size]: ${l.size}")
+ logger.error(l.toArray.toSeq.mkString("\n"))
+ println(ls.toList)
+ ls
+// val traversal = g.V().out().as("x").in().as("y").select("x", "y").by("name").fold()
+// .dedup(Scope.local, "x", "y").unfold();
+
+// val ls = traversal.toList
+// ls
+ }
}
\ No newline at end of file