diff --git a/CIMTool/src/main/scala/ch/ninecode/cim/tool/CodeGenerator.scala b/CIMTool/src/main/scala/ch/ninecode/cim/tool/CodeGenerator.scala index d62f26a53..b064aec5e 100644 --- a/CIMTool/src/main/scala/ch/ninecode/cim/tool/CodeGenerator.scala +++ b/CIMTool/src/main/scala/ch/ninecode/cim/tool/CodeGenerator.scala @@ -1,10 +1,26 @@ package ch.ninecode.cim.tool +import java.io.File + /** * Interface for code generation. */ trait CodeGenerator { + /** + * Make directories. + * + * @param directory the directory path to create + */ + def mkdir (directory: String): Unit = + { + val dir = new File (directory) + if (!dir.exists) + { + val _ = dir.mkdir + } + } + /** * Generate code. */ diff --git a/CIMTool/src/main/scala/ch/ninecode/cim/tool/Scala.scala b/CIMTool/src/main/scala/ch/ninecode/cim/tool/Scala.scala index f1886dee5..a19bedc2a 100644 --- a/CIMTool/src/main/scala/ch/ninecode/cim/tool/Scala.scala +++ b/CIMTool/src/main/scala/ch/ninecode/cim/tool/Scala.scala @@ -15,6 +15,60 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene { val log: Logger = LoggerFactory.getLogger (getClass) + val package_doc_header: String = + """ * ==Overview== + | * Provides Common Information Model (CIM) classes for electrical, topological, asset, spatial + | * and other categories of objects that are germane to electric network operations. + | * + | * Some examples are shown in the following image: + | * + | * + | * + | * These classes are the types of, and objects contained in, the RDD that are created by the CIMReader, + | * e.g. RDD[Switch]. + | * + | * Classes are nested according to the hierarchical package structure found in CIM. + | * + | * Each class has the reference to its parent class, available as the generic sup field, + | * and also as a typed reference of the same name as the parent class. + | * + | * This is illustrated in the following image, where the object with id TE1932 (a Switch) is found in + | * RDD[Switch] and all RDD for which the relation 'a Switch "Is A" X' holds, + | * e.g. RDD[ConductingEquipment]: + | * + | * + | * + | * The packages and their descriptions are itemized below. + | * + | * A short summary of all classes is found below that. + | * The classes can be ordered by package (Grouped) or alphabetically. + | * The classes are also listed in the panel on the left for easy reference.""".stripMargin + + implicit val ordering: Ordering[Member] = new Ordering[Member] + { + def unquote (variable: String): String = if ('`' == variable.charAt (0)) variable.substring (1, variable.length - 1) else variable + def compare (a: Member, b: Member): Int = + if (a.name == "sup") + -1 + else if (b.name == "sup") + 1 + else + { + val a_ = unquote (a.variable) + val b_ = unquote (b.variable) + if (a_.charAt (0).isLower) + if (b_.charAt (0).isLower) + a_.compareTo (b_) + else + -1 + else + if (b_.charAt (0).isLower) + 1 + else + a_.compareTo (b_) + } + } + def register (pkg: Package): String = { s"_${pkg.valid_class_name}" @@ -47,7 +101,7 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene case "MonthDay" => Member (name, variable, false, comment, false, "0..1", "0..*", "String", "null", "", null) case "URI" => Member (name, variable, false, comment, false, "0..1", "0..*", "String", "null", "", null) case _ => - throw new Exception ("""unknown primitive type "%s"""".format (dom.name)) + throw new Exception (s"""unknown primitive type "${dom.name}"""") } case "enumeration" => Member (name, variable, false, comment, true, "0..1", "0..*", "String", "null", "", null) @@ -76,7 +130,7 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene Member (name, variable, false, comment, false, "0..1", "0..*", "String", "null", "", null) } case _ => - throw new Exception ("""unknown Domain stereotype "%s"""".format (dom.stereotype)) + throw new Exception (s"""unknown Domain stereotype "${dom.stereotype}"""") } case None => classes.find (_.name == attribute.typ) match @@ -101,7 +155,7 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene Member (name, variable, false, comment, true, role.card, role.mate.card, "List[String]", "null", "", referenced_class) } - def declaration (name: String, members: SortedSet[Member]): String = + def declareClass (name: String, members: SortedSet[Member]): String = { def initializers: List[String] = { @@ -160,7 +214,7 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene |""".stripMargin } - def export_fields (name: String, fields: SortedSet[Member]): String = + def exportFields (name: String, fields: SortedSet[Member]): String = { val ref = if (fields.exists (!_.reference)) s" def emitelem (position: Int, value: Any): Unit = if (mask (position)) emit_element ($name.fields (position), value)\n|" @@ -215,6 +269,109 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene | }""".stripMargin } + def declareObject (name: String): String = + { + s""" + |object $name + |extends + | Parseable[$name]""".stripMargin + } + + def parseRelationships (fields: SortedSet[Member]): String = + { + if (fields.nonEmpty) + { + // output the fields map + val fieldmap = fields.iterator.map (x => s""""${x.name}"""") + .mkString (" override val fields: Array[String] = Array[String] (\n ", ",\n ", "\n )\n") + + // output the relations list + val references = fields.filter (member => (null != member.referenced_class)) + val relationships = if (references.nonEmpty) + (for (r <- references.iterator) // need to use iterator here because SortedSet is brain dead + yield + s""" Relationship ("${r.variable}", "${r.referenced_class}", "${r.this_cardinality}", "${r.mate_cardinality}")""" + ).mkString (" override val relations: List[Relationship] = List (\n", ",\n", "\n )\n") + else + "" + + // output the field parsers + def pa (m: Member): String = + { + if (m.reference) + if (m.multiple) + "parse_attributes (attribute" + else + "parse_attribute (attribute" + else + "parse_element (element" + } + val parsers = (for (x <- fields.iterator.zipWithIndex) + yield + { + val (member, index) = x + val fielder = if (member.multiple) "FielderMultiple" else "Fielder" + s"val ${member.variable}: $fielder = ${pa (member)} (cls, fields(${index})))" + } + ).mkString (" ", "\n ", "\n") + s"$fieldmap$relationships$parsers" + } + else + "" + } + + def parse (name: String, members: SortedSet[Member]): String = + { + val identified_object = name == "IdentifiedObject" // special handling for IdentifiedObject.mRID + val fields: SortedSet[Member] = members.filter ("sup" != _.name) + val boilerplate = if (fields.nonEmpty) + { + val initializer = (for (_ <- 0 until 1 + (fields.size / 32)) yield "0").mkString (",") + s""" + | implicit val ctx: Context = context + | implicit val bitfields: Array[Int] = Array($initializer)""".stripMargin + } + else + "" + val base = if (identified_object) + s""" + | val base = BasicElement.parse (context)""".stripMargin + else + "" + + // add field parser calls + def wrap (members: Iterator[(Member, String)]): String = + members.map (x => if (x._1.function != "") s"${x._1.function} (${x._2})" else x._2).mkString (" ", ",\n ", "") + def masker (x: (Member, Int)): String = + { + val (member, index) = x + val mask = if (member.multiple) "masks" else "mask" + s"$mask (${member.variable} (), ${index - 1})" + } + val parsers = if (identified_object) + wrap (members.iterator.zipWithIndex.map (x => (x._1, if (x._1.name == "sup") "base" else if (x._1.name == "mRID") s"{val _ = ${masker (x)}; base.id}" else masker (x) ))) + else + wrap (members.iterator.zipWithIndex.map (x => (x._1, if (x._1.name == "sup") s"${x._1.datatype}.parse (context)" else masker (x)))) + + val update = if (fields.nonEmpty) + """ + | ret.bitfields = bitfields""" + else + "" + + // output the parse method + s""" + | def parse (context: Context): $name = + | {$boilerplate$base + | val ret = $name ( + |$parsers + | )$update + | ret + | } + |} + |""".stripMargin + } + def asText (pkg: Package): String = { val case_classes = parser.classesFor (pkg) @@ -222,31 +379,6 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene for (cls <- case_classes) { val name = cls.valid_class_name - val identified_object = name == "IdentifiedObject" // special handling for IdentifiedObject.mRID - implicit val ordering: Ordering[Member] = new Ordering[Member] - { - def unquote (variable: String): String = if ('`' == variable.charAt (0)) variable.substring (1, variable.length - 1) else variable - def compare (a: Member, b: Member): Int = - if (a.name == "sup") - -1 - else if (b.name == "sup") - 1 - else - { - val a_ = unquote (a.variable) - val b_ = unquote (b.variable) - if (a_.charAt (0).isLower) - if (b_.charAt (0).isLower) - a_.compareTo (b_) - else - -1 - else - if (b_.charAt (0).isLower) - 1 - else - a_.compareTo (b_) - } - } val sup = Member ("sup", "sup", true, "Reference to the superclass object.", false, "1", "", if (null != cls.sup) cls.sup.name else "BasicElement", "null", "", if (null == cls.sup) null else cls.sup.valid_class_name) val members: mutable.SortedSet[Member] = mutable.SortedSet[Member](sup) ++ @@ -254,82 +386,19 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene .union (parser.rolesFor (cls).map (details).toSet) val fields: mutable.SortedSet[Member] = members.filter ("sup" != _.name) val s = new StringBuilder () - .append (JavaDoc (cls.note, 0, members, pkg.name, "Package %s".format (pkg.name), pkg.notes).asText) - .append (declaration (name, members)) + .append (JavaDoc (cls.note, 0, members, pkg.name, s"Package ${pkg.name}", pkg.notes).asText) + .append (declareClass (name, members)) .append ("\n{") .append (superclass (cls)) .append (row_overrides) - .append (export_fields (name, fields)) + .append (exportFields (name, fields)) .append (export (cls)) .append ("\n}\n") - - s.append (""" - |object %s - |extends - | Parseable[%s] - |{ - |""".stripMargin.format (name, name)) - - val any = members.exists (_.name != "sup") - if (any) - { - // output the fields map - s.append (fields.iterator.map (x => "\"%s\"".format (x.name)).mkString (" override val fields: Array[String] = Array[String] (\n ", ",\n ", "\n )\n")) - - // output the relations list - val relationships = members.filter (member => (member.name != "sup") && (null != member.referenced_class)) - if (relationships.nonEmpty) - s.append (relationships.iterator.map ( - member => """ Relationship ("%s", "%s", "%s", "%s")""".format (member.variable, member.referenced_class, member.this_cardinality, member.mate_cardinality)).mkString (" override val relations: List[Relationship] = List (\n", ",\n", "\n )\n")) - - // output the field parsers - def pa (m: Member): String = - { - if (m.reference) - if (m.multiple) - "parse_attributes (attribute" - else - "parse_attribute (attribute" - else - "parse_element (element" - } - s.append (fields.iterator.zipWithIndex.map (x => { val fielder = if (x._1.multiple) "FielderMultiple" else "Fielder"; s"val ${x._1.variable}: $fielder = ${pa (x._1)} (cls, fields(${x._2})))" }).mkString (" ", "\n ", "\n")) - } - // output the parse method - s.append (""" - | def parse (context: Context): %s = - | { - |""".stripMargin.format (name)) - if (any) - { - s.append (" implicit val ctx: Context = context\n") - val initializer = (for (_ <- 0 until 1 + (fields.size / 32)) yield "0").mkString (",") - s.append (" implicit val bitfields: Array[Int] = Array(%s)\n".format (initializer)) - } - if (identified_object) - s.append (" val base = BasicElement.parse (context)\n") - s.append (" val ret = %s (\n".format (name)) - // add field parser calls - def wrap (members: Iterator[(Member, String)]): String = - members.map (x => if (x._1.function != "") s"${x._1.function} (${x._2})" else x._2).mkString (" ", ",\n ", "\n") - def masker (x: (Member, Int)): String = - { - val mask = if (x._1.multiple) "masks" else "mask" - s"$mask (${x._1.variable} (), ${x._2 - 1})" - } - s.append ( - if (identified_object) - wrap (members.iterator.zipWithIndex.map (x => (x._1, if (x._1.name == "sup") "base" else if (x._1.name == "mRID") s"{val _ = ${masker (x)}; base.id}" else masker (x) ))) - else - wrap (members.iterator.zipWithIndex.map (x => (x._1, if (x._1.name == "sup") s"${x._1.datatype}.parse (context)" else masker (x)))) - ) - s.append (" )\n") - if (any) - s.append (" ret.bitfields = bitfields\n") - s.append (" ret\n }\n") - s.append ("""} - | - |""".stripMargin) + .append (declareObject (name)) + .append ("\n{\n") + .append (parseRelationships (fields)) + .append (parse (name, members)) + .append ("\n") p.append (s) } @@ -338,29 +407,28 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene { val v = new StringBuilder () - v.append ("""package ch.ninecode.model - | - |import org.apache.spark.sql.Row - | - |import ch.ninecode.cim.ClassInfo - |import ch.ninecode.cim.Context - |import ch.ninecode.cim.Parseable - |import ch.ninecode.cim.Relationship - | - |""".stripMargin) - v.append (p.toString) - - v.append ("""private[ninecode] object """) - v.append (register (pkg)) - v.append (""" - |{ - | def register: List[ClassInfo] = - | { - |""".stripMargin) - v.append (case_classes.map (cls => s"${cls.valid_class_name}.register").mkString (" List (\n ", ",\n ", "\n )")) - v.append (""" - | } - |}""".stripMargin) + .append ("""package ch.ninecode.model + | + |import org.apache.spark.sql.Row + | + |import ch.ninecode.cim.ClassInfo + |import ch.ninecode.cim.Context + |import ch.ninecode.cim.Parseable + |import ch.ninecode.cim.Relationship + | + |""".stripMargin) + .append (p.toString) + .append ("""private[ninecode] object """) + .append (register (pkg)) + .append (""" + |{ + | def register: List[ClassInfo] = + | { + |""".stripMargin) + .append (case_classes.map (cls => s"${cls.valid_class_name}.register").mkString (" List (\n ", ",\n ", "\n )")) + .append (""" + | } + |}""".stripMargin) v.toString } @@ -368,9 +436,42 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene "" } + def save (filename: String, text: String): Unit = + { + val _ = Files.write (Paths.get (filename), text.getBytes (StandardCharsets.UTF_8)) + } + + def writeRegistration (registers: List[String]): Unit = + { + val register = + s""" lazy val classes: List[ClassInfo] = + | List ( + |${registers.mkString (",\n")} + | ).flatten + |""".stripMargin + save (s"${options.directory}/chim_register.scala", register) + } + + def writePackage (package_docs: List[String]): Unit = + { + val pkg_doc = + s"""package ch.ninecode + | + |/** + |$package_doc_header + |${package_docs.mkString ("\n")} + | */ + |package object model + |{ + |} + |""".stripMargin + save (s"${options.directory}/model/package.scala", pkg_doc) + } + def generate (): Unit = { - new File ("%s/model".format (options.directory)).mkdirs + val dir = s"${options.directory}/model" + mkdir (dir) val sc = Scala (parser, options) val packages = scala.collection.mutable.SortedSet[(String, Int)]() @@ -378,44 +479,7 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene { packages.add ((sc.register (pkg._2), pkg._1)) } - val register = new StringBuilder () - register.append (""" lazy val classes: List[ClassInfo] = - | List ( - |""".stripMargin) var registers: List[String] = List[String]() - val pkg_doc = new StringBuilder () - pkg_doc.append ( - """package ch.ninecode - | - |/** - | * ==Overview== - | * Provides Common Information Model (CIM) classes for electrical, topological, asset, spatial - | * and other categories of objects that are germane to electric network operations. - | * - | * Some examples are shown in the following image: - | * - | * - | * - | * These classes are the types of, and objects contained in, the RDD that are created by the CIMReader, - | * e.g. RDD[Switch]. - | * - | * Classes are nested according to the hierarchical package structure found in CIM. - | * - | * Each class has the reference to its parent class, available as the generic sup field, - | * and also as a typed reference of the same name as the parent class. - | * - | * This is illustrated in the following image, where the object with id TE1932 (a Switch) is found in - | * RDD[Switch] and all RDD for which the relation 'a Switch "Is A" X' holds, - | * e.g. RDD[ConductingEquipment]: - | * - | * - | * - | * The packages and their descriptions are itemized below. - | * - | * A short summary of all classes is found below that. - | * The classes can be ordered by package (Grouped) or alphabetically. - | * The classes are also listed in the panel on the left for easy reference. - |""".stripMargin) var package_docs: List[String] = List[String]() for (q <- packages) { @@ -423,30 +487,23 @@ case class Scala (parser: ModelParser, options: CIMToolOptions) extends CodeGene val s = sc.asText (pkg) if (s.trim != "") { - val file = "%s/model/%s.scala".format (options.directory, pkg.name) + val file = s"$dir/${pkg.name}.scala" log.info (file) - Files.write (Paths.get (file), s.getBytes (StandardCharsets.UTF_8)) - registers = registers :+ """ %s.register""".format (sc.register (pkg)) + save (file, s) + registers = registers :+ s""" ${sc.register (pkg)}.register""" package_docs = package_docs :+ " *" package_docs = package_docs :+ s" * ===${pkg.name}===" if (pkg.notes != null) package_docs = package_docs :+ JavaDoc (pkg.notes, 0).contents } else - log.debug ("no text generated for package %s (%s)".format (pkg.xuid, pkg.name)) + log.debug (s"no text generated for package ${pkg.xuid} (${pkg.name})") } - register.append (registers.mkString (",\n")) - register.append (""" - | ).flatten - |""".stripMargin) - Files.write (Paths.get ("%s/chim_register.scala".format (options.directory)), register.toString.getBytes (StandardCharsets.UTF_8)) - pkg_doc.append (package_docs.mkString ("\n")) - pkg_doc.append (""" - | */ - |package object model - |{ - |} - |""".stripMargin) - Files.write (Paths.get ("%s/model/package.scala".format (options.directory)), pkg_doc.toString.getBytes (StandardCharsets.UTF_8)) + + // write the registration code + writeRegistration (registers) + + // write the package file + writePackage (package_docs) } }