diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a5b301de2e3f121b8dd7a09ea9fe0b667da4c369 --- /dev/null +++ b/.gitignore @@ -0,0 +1,178 @@ +#### joe made this: http://goel.io/joe + +#### scala #### +*.class +*.log + + +#### sbt #### +# Simple Build Tool +# http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control + +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +.history +.cache +.lib/ + + +#### jetbrains #### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + + +#### java #### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +#### eclipse #### + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +#### Custom (user defined) insertions #### +.idea/ +.classpath +.project +output/ +doc/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..5ed635731bd2541c2765d6ab654a00c411e18bbc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "lib/ModelSyncProvider"] + path = lib/ModelSyncProvider + url = https://git-st.inf.tu-dresden.de/cwerner/role_model_synchronization_provider.git +[submodule "lib/SCROLL"] + path = lib/SCROLL + url = https://github.com/portux/SCROLL.git +[submodule "src/main/asciidoc"] + path = src/main/doc + url = https://git-st.inf.tu-dresden.de/cwerner/code_generator.wiki.git diff --git a/assets/family.png b/assets/family.png new file mode 100644 index 0000000000000000000000000000000000000000..e2d93ecd8b345770d27b6847e4af6ca1aff673d0 Binary files /dev/null and b/assets/family.png differ diff --git a/assets/ttc17.ecore b/assets/ttc17.ecore new file mode 100644 index 0000000000000000000000000000000000000000..4768ec7123e7e93faa9a3a93a1d283b0fd7f84e5 --- /dev/null +++ b/assets/ttc17.ecore @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="ttc17" nsURI="http://www.example.org/ttc17" nsPrefix="ttc17"> + <eClassifiers xsi:type="ecore:EClass" name="Person"> + <eStructuralFeatures xsi:type="ecore:EAttribute" name="fullName" unique="false" + eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/> + <eStructuralFeatures xsi:type="ecore:EAttribute" name="birthday" unique="false" + eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EDate"/> + </eClassifiers> + <eClassifiers xsi:type="ecore:EClass" name="Male" eSuperTypes="#//Person"/> + <eClassifiers xsi:type="ecore:EClass" name="Female" eSuperTypes="#//Person"/> + <eClassifiers xsi:type="ecore:EClass" name="Family"> + <eStructuralFeatures xsi:type="ecore:EAttribute" name="lastName" unique="false" + eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/> + <eStructuralFeatures xsi:type="ecore:EReference" name="mother" eType="#//Member" + containment="true" eOpposite="#//Member/familyMother"/> + <eStructuralFeatures xsi:type="ecore:EReference" name="father" eType="#//Member" + containment="true" eOpposite="#//Member/familyFather"/> + <eStructuralFeatures xsi:type="ecore:EReference" name="familySons" upperBound="-1" + eType="#//Member" containment="true" eOpposite="#//Member/familySon"/> + <eStructuralFeatures xsi:type="ecore:EReference" name="familyDaughters" upperBound="-1" + eType="#//Member" containment="true" eOpposite="#//Member/familyDaughter"/> + </eClassifiers> + <eClassifiers xsi:type="ecore:EClass" name="Member"> + <eStructuralFeatures xsi:type="ecore:EAttribute" name="firstName" unique="false" + eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/> + <eStructuralFeatures xsi:type="ecore:EReference" name="familyMother" eType="#//Family" + eOpposite="#//Family/mother"/> + <eStructuralFeatures xsi:type="ecore:EReference" name="familyFather" eType="#//Family" + eOpposite="#//Family/father"/> + <eStructuralFeatures xsi:type="ecore:EReference" name="familySon" eType="#//Family" + eOpposite="#//Family/familySons"/> + <eStructuralFeatures xsi:type="ecore:EReference" name="familyDaughter" eType="#//Family" + eOpposite="#//Family/familyDaughters"/> + </eClassifiers> + <eClassifiers xsi:type="ecore:EClass" name="SimplePerson"> + <eStructuralFeatures xsi:type="ecore:EAttribute" name="completeName" unique="false" + eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/> + <eStructuralFeatures xsi:type="ecore:EAttribute" name="address" unique="false" + eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/> + <eStructuralFeatures xsi:type="ecore:EAttribute" name="male" unique="false" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EBoolean"/> + </eClassifiers> +</ecore:EPackage> diff --git a/assets/ttc17.sync.json b/assets/ttc17.sync.json new file mode 100644 index 0000000000000000000000000000000000000000..2315bd5c9df40c6f2b5277acd599417901897f40 --- /dev/null +++ b/assets/ttc17.sync.json @@ -0,0 +1,31 @@ +{ + "name": "TTC 17", + "init": { + "name": "ModelB", + "primaryClass": "ttc17.Family", + "image": "family.png", + "nested": [ + { + "name": "Model B - members", + "primaryClass": "ttc17.Member" + } + ] + }, + "integration": [ + { + "name": "ModelA", + "primaryClass": "ttc17.Person", + "additionalClasses": [ + "ttc17.Female", + "ttc17.Male" + ], + "image": null, + "rule": "person.atl" + }, + { + "name": "ModelC", + "primaryClass": "ttc17.SimplePerson", + "image": null + } + ] +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..24e4b959b4ea8a41f46d545de3fd7d7da6e30203 --- /dev/null +++ b/build.sbt @@ -0,0 +1,40 @@ +import sbt.Keys.{libraryDependencies, scalacOptions, version} + +val emfcommonVersion = "2.12.0" +val emfecoreVersion = "2.12.0" +val scrollVersion = "1.6" +val scoptVersion = "3.7.0" +val liftVersion = "3.3.0" + +val syncProvider = RootProject(file("lib/ModelSyncProvider")) + +lazy val generator = (project in file(".")) + .settings( + name := "CodeGenerator", + version := "0.1", + scalaVersion := "2.12.6", + libraryDependencies ++= Seq( + "org.scala-lang" % "scala-reflect" % scalaVersion.value, + "org.scala-lang" % "scala-compiler" % scalaVersion.value, + "org.eclipse.emf" % "org.eclipse.emf.common" % emfcommonVersion, + "org.eclipse.emf" % "org.eclipse.emf.ecore" % emfecoreVersion, + "com.github.scopt" %% "scopt" % scoptVersion, + "net.liftweb" %% "lift-json" % liftVersion + ), + scalacOptions ++= Seq( + "-language:implicitConversions" + ), + mainClass in assembly := Some("org.rosi_project.model_sync.generator.Generator"), + assemblyMergeStrategy in assembly := { + case "MANIFEST.MF" => MergeStrategy.first + case "plugin.xml" => MergeStrategy.discard + case "plugin.properties" => MergeStrategy.discard + case "generated_package.exsd" => MergeStrategy.discard + case "dynamic_package.exsd" => MergeStrategy.discard + case PathList("schema", ps @ _ *) if ps.lastOption.exists(_.endsWith("generated_package.exsd")) => MergeStrategy.discard + case PathList("schema", ps @ _ *) if ps.lastOption.exists(_.endsWith("dynamic_package.exsd")) => MergeStrategy.discard + case x => + val oldStrategy = (assemblyMergeStrategy in assembly).value + oldStrategy(x) + } + ).dependsOn(syncProvider) diff --git a/lib/ModelSyncProvider b/lib/ModelSyncProvider new file mode 160000 index 0000000000000000000000000000000000000000..ace8bdf5799dd89bd66d01782237123172bb0ec0 --- /dev/null +++ b/lib/ModelSyncProvider @@ -0,0 +1 @@ +Subproject commit ace8bdf5799dd89bd66d01782237123172bb0ec0 diff --git a/lib/SCROLL b/lib/SCROLL new file mode 160000 index 0000000000000000000000000000000000000000..623765816af6d27387efb82dfb890bc4318c58a2 --- /dev/null +++ b/lib/SCROLL @@ -0,0 +1 @@ +Subproject commit 623765816af6d27387efb82dfb890bc4318c58a2 diff --git a/project/assembly.sbt b/project/assembly.sbt new file mode 100644 index 0000000000000000000000000000000000000000..d95475f16ffa0d9a0670795c8296b31fa24c76ec --- /dev/null +++ b/project/assembly.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.7") diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..8db5ca22266ea113c2b39c92dddb73c83e61ffee --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.1 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..45a6f02d1880ea4c9bff889a194ab87adf467611 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4") +addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") diff --git a/src/main/doc b/src/main/doc new file mode 160000 index 0000000000000000000000000000000000000000..89e2af4e060ae7c39c261fde55ba9140caf85853 --- /dev/null +++ b/src/main/doc @@ -0,0 +1 @@ +Subproject commit 89e2af4e060ae7c39c261fde55ba9140caf85853 diff --git a/src/main/scala/org/rosi_project/model_sync/generator/CLIGenerator.scala b/src/main/scala/org/rosi_project/model_sync/generator/CLIGenerator.scala new file mode 100644 index 0000000000000000000000000000000000000000..9f6af4d784e94a44459d35eb17e5c33159c54a02 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/CLIGenerator.scala @@ -0,0 +1,50 @@ +package org.rosi_project.model_sync.generator + +import java.{io => jio} + +import scopt.OptionParser + +/** Main entry into the application. It is intended to run as a console application and should be + * called like the following: `generator [-o DIR] [-c] ECORE` with the following options: + * + * - `-o | --outdir DIR` will write the generated `model.jar` to the directory `DIR` + * - `-c | --cleanup` will remove the generated `.scala` files and only keep the `.class` in the + * generated `.jar` file + * - `ECORE` should specify the ecore (XML) files which contains the model to create the scala + * files for + * + * @author Rico Bergmann + */ +object CLIGenerator extends App { + + val parser = new OptionParser[GeneratorConfig](programName="modelgen") { + head("modelgen", "0.1") + + arg[jio.File]("ecore").required().action( (ef, conf) => + conf.copy(source = ef.getAbsolutePath) + ).text("The ecore (XML) file of the model") + + opt[jio.File]('o', "outdir").optional().action( (dir, conf) => + conf.copy(outDir = dir) + ).text("The directory to place the generated model in (current dir by default)") + + opt[jio.File]('w', "workdir").optional().action( (dir, conf) => + conf.copy(workDir = dir) + ).text("The directory to place the generated .scala files in (temp dir by default)") + + opt[jio.File]('m', "model").optional().action( (mj, conf) => + conf.copy(modelFile = mj.getAbsolutePath) + ).text("The description of the model's components (mapped to the name of the ecore with .sync.json extension by default)") + + opt[Unit]('c', "cleanup").optional().action( (_, conf) => + conf.copy(cleanUp = true) + ).text("Remove the generated .scala files and only keep the compiled .class files") + } + + parser.parse(args, GeneratorConfig()) match { + case Some(config) => + new Generator(config).run() + case None => + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/EcoreLoader.scala b/src/main/scala/org/rosi_project/model_sync/generator/EcoreLoader.scala new file mode 100644 index 0000000000000000000000000000000000000000..6bd33b84fa74494b769718936aa11b759e4c5bea --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/EcoreLoader.scala @@ -0,0 +1,29 @@ +package org.rosi_project.model_sync.generator + +import org.eclipse.emf.ecore.{EObject, EPackage} +import scroll.internal.ecore.ECoreImporter + +/** Simple service to load an ECORE model from a file. + * + * @author Rico Bergmann + */ +class EcoreLoader extends ECoreImporter { + + /** Fetches an ecore model from XML. + * + * @param path where to find the model + * @return the model described by the XML + */ + def loadEcore(path: String = "assets/ttc17.ecore"): EPackage = { + this.path = path + val res = loadModel() + res.getContents.toArray(new Array[EObject](0)).toList.find(_.isInstanceOf[EPackage]).map((p: EObject) => p.asInstanceOf[EPackage]).orNull + } + +} + +/** Exception to indicate that the model images may not be copied into the JAR. + * + * @param cause the causing exception + */ +class EcoreLoadException(cause: Exception) extends RuntimeException(cause) diff --git a/src/main/scala/org/rosi_project/model_sync/generator/Generator.scala b/src/main/scala/org/rosi_project/model_sync/generator/Generator.scala new file mode 100644 index 0000000000000000000000000000000000000000..69c91044992b22cb5d4733834f2136c4360e7409 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/Generator.scala @@ -0,0 +1,74 @@ +package org.rosi_project.model_sync.generator + +import net.liftweb.json._ +import org.eclipse.emf.ecore._ +import org.rosi_project.model_sync.generator.conversion.SModelGenerator +import org.rosi_project.model_sync.generator.env.{CompilationException, JarPackaginException} +import org.rosi_project.model_sync.generator.io.{ClassWritingException, ModelImagePreparationException, SModelFSWriter} + +import scala.io.Source +import scala.reflect.io.File + +/** The `Generator` runs the whole workflow of generating a JAR of compiled Scala source files + * based on an ECORE file of some sync model. + * + * @param config the configuration that tells the generator where to locate the necessary files. + * + * @author Rico Bergmann + */ +class Generator(config: GeneratorConfig) { + + /** + * Starts the generation. + */ + def run(): Unit = { + try { + val modelJson = Source.fromFile(config.getModelFile).getLines.mkString + implicit val jsonFormats: Formats = DefaultFormats + val modelCfg = parse(modelJson).extract[ModelConfig] + + var ecoreModel: EPackage = null + try { + ecoreModel = new EcoreLoader loadEcore config.source + } catch { + case e: Exception => throw new EcoreLoadException(e) + } + + val sModel = new SModelGenerator convert ecoreModel + new SModelSyncPreparationService prepareModel(sModel, modelCfg) + // write the model and create the JAR + println("... Writing model") + + if (config.hasWorkDir) { + sModel.accept( + new SModelFSWriter( + outputDir = File(config.outDir).toDirectory, + workingDir = File(config.workDir), + keepClassFiles = !config.cleanUp, + modelCfg = modelCfg, + currentDir = config.getModelPath)) + } else { + sModel.accept( + new SModelFSWriter( + outputDir = File(config.outDir).toDirectory, + keepClassFiles = !config.cleanUp, + modelCfg = modelCfg, + currentDir = config.getModelPath)) + } + + } + catch { + case ele: EcoreLoadException => + println(s"** ERROR ** could not load ecore model: $ele") + case mipe: ModelImagePreparationException => + println(s"** ERROR ** could not prepare model images: $mipe") + case cwe: ClassWritingException => + println(s"** ERROR ** could not write classes: $cwe") + case ce: CompilationException => + println(s"** ERROR ** could not compile classes: $ce") + case jpe: JarPackaginException => + println(s"** ERROR ** could not package JAR: $jpe") + } + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/GeneratorConfig.scala b/src/main/scala/org/rosi_project/model_sync/generator/GeneratorConfig.scala new file mode 100644 index 0000000000000000000000000000000000000000..2d1a971fc94cf9febb32cd153100518c26525d98 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/GeneratorConfig.scala @@ -0,0 +1,17 @@ +package org.rosi_project.model_sync.generator + +import java.io.File + +/** Wrapper for the different command line options for the [[Generator]] + * + * @author Rico Bergmann + */ +case class GeneratorConfig(source: String = "", cleanUp: Boolean = false, outDir: File = new File(System.getProperty("user.dir")), workDir: File = null, modelFile: String = "") { + + def hasWorkDir: Boolean = workDir != null + + def getModelFile: File = if (modelFile.isEmpty) new File(source.replace(".ecore", ".sync.json")) else new File(modelFile) + + def getModelPath: File = new File(source).getParentFile + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/ModelConfig.scala b/src/main/scala/org/rosi_project/model_sync/generator/ModelConfig.scala new file mode 100644 index 0000000000000000000000000000000000000000..abcd564426912f437e3bdb022225dcd0a16e372f --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/ModelConfig.scala @@ -0,0 +1,53 @@ +package org.rosi_project.model_sync.generator + +import org.rosi_project.model_sync.provider.DisplayableModel +import org.rosi_project.model_sync.provider.ModelSyncProvider + +/** Representation of a model configuration (i.e. a number of corresponding models). It tells the + * generator how the model looks like, which types are needed, etc. + * + * This class as well as the derived types form a a reification of ''JSON'' files. + * + * @param name the name of the model + * @param init the initial sub-model to display + * @param integration sub-models which should be integrable + * + * @see [[ModelSyncProvider]] + * + * @author Rico Bergmann + */ +case class ModelConfig(name: String, init: Model, integration: Option[List[Model]]) + +/** Representation of a single model. + * + * Classes are expected to look like `package.ClassName` + * + * @param name the model's name + * @param primaryClass the main class of the model (see [[DisplayableModel.getInstanceClass]] + * @param additionalClasses derived classes of the model + * @param image graphical (e.g. UML) representation + * @param nested nested models + * + * @see [[DisplayableModel]] + */ +case class Model(name: String, primaryClass: String, additionalClasses: Option[List[String]], image: Option[String], nested: Option[List[Model]]) + +/** The companion provides some utility methods. + */ +object Model { + + /** Decomposes a model name into its package and class name components. + */ + def parseClass(qualifiedName: String): (String, String) = { + val lastSeparator = qualifiedName.lastIndexOf(".") + val pckg = qualifiedName.substring(0, lastSeparator) + val className = qualifiedName.substring(lastSeparator + 1, qualifiedName.length) + (pckg, className) + } + +} + +/** Exception to indicate that a (''JSON'') model does not contain the expected data for the + * Generator to work correctly. + */ +class ModelDataException extends RuntimeException diff --git a/src/main/scala/org/rosi_project/model_sync/generator/SModelSyncPreparationService.scala b/src/main/scala/org/rosi_project/model_sync/generator/SModelSyncPreparationService.scala new file mode 100644 index 0000000000000000000000000000000000000000..130f2527eae224b7fed30dcf4295e0b1ed7fa876 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/SModelSyncPreparationService.scala @@ -0,0 +1,26 @@ +package org.rosi_project.model_sync.generator + +import org.rosi_project.model_sync.generator.acr_model.SModel +import org.rosi_project.model_sync.generator.sync.{GetterSetterGeneratingVisitor, SyncEnhancingVisitor} + +/** Simple service to perform all the necessary adaptions of an [[SModel]] to make applicable for + * synchronization. + * + * @author Rico Bergmann + */ +class SModelSyncPreparationService { + + /** Augments a [[SModel]] with the necessary methods and statements to make it usable in a + * synchronization context. + * + * @param sModel the model to augment + */ + def prepareModel(sModel: SModel, modelCfg: ModelConfig): Unit = { + val getterSetterVisitor = new GetterSetterGeneratingVisitor + val syncNotificationVisitor = new SyncEnhancingVisitor(modelCfg) + + sModel.accept(getterSetterVisitor) + sModel.accept(syncNotificationVisitor) + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/GenericSType.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/GenericSType.scala new file mode 100644 index 0000000000000000000000000000000000000000..0a1cb48ef2b5a2fbbf3b46639e6ffc2a0e6f81ab --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/GenericSType.scala @@ -0,0 +1,25 @@ +package org.rosi_project.model_sync.generator.acr_model + +/** Adds support for generic types to [[SType]] instances. + * + * @param name `this` type's name + * @param sPackage the package in which `this` type resides + * @param typeParam the type parameter (a.k.a. the ''Generic'') + * + * @author Rico Bergmann + */ +class GenericSType(name: String, sPackage: String = "", val typeParam: STypedElement) extends SType(name, sPackage) { + + override def getName: String = s"$name[${typeParam.getName}]" + + override def getNecessaryImports: Set[SImport] = if (typeParam.getPackage.isEmpty || typeParam.getPackage == sPackage) + typeParam.getNecessaryImports + SImport(typeParam.getPackage, typeParam.getName) else typeParam.getNecessaryImports +} + +/** The companion provides an `unapply` implementation. + */ +object GenericSType { + + def unapply(arg: GenericSType): Option[(String, String, STypedElement)] = Some((arg.name, arg.sPackage, arg.typeParam)) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SAttribute.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SAttribute.scala new file mode 100644 index 0000000000000000000000000000000000000000..72c7beaace32d66527a258b06268b1361bdb40dc --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SAttribute.scala @@ -0,0 +1,21 @@ +package org.rosi_project.model_sync.generator.acr_model + +/** Representation of class attributes. This abstraction is a simplification of an actual scala + * attribute: it is impossible to distinguish between ''final'' and ''non-final'' fields. Instead, + * all attributes will be treated mutable. + * + * @author Rico Bergmann + */ +case class SAttribute(name: String, attrType: STypedElement) extends SModelElement { + + /** Provides the type of `this` attribute as a `String`. + * + * @return the type. Will never be `null` nor empty. + */ + def getType: String = attrType.getName + + override def accept(visitor: SModelVisitor): Unit = visitor.visit(this) + + override def toString: String = s"$name: $getType" + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SClass.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SClass.scala new file mode 100644 index 0000000000000000000000000000000000000000..fe3882754e85ae49eef024d526df59bf15d71a73 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SClass.scala @@ -0,0 +1,219 @@ +package org.rosi_project.model_sync.generator.acr_model + +import java.util.Objects + +/** Representation of Scala classes. Attributes, methods, etc. have their own wrappers which should + * be used to modify these individual parts of the class. + * + * @see [[SType]] + * @author Rico Bergmann + */ +class SClass(val name: String, + val sPackage: String = "", + var attributes: Seq[SAttribute] = Seq.empty, + var parent: STypedElement = null, + protected var methods: Seq[SMethod] = Seq.empty) + extends STypedElement { + + protected var constructorStatements: Seq[SMethodStatement] = Seq.empty + + /** Provides code that should be executed when invoking the constructor. + */ + def getAdditionalConstructorStatements: Seq[SMethodStatement] = constructorStatements + + /** Checks, whether `this` is part of the default package + */ + def isDefaultPackage: Boolean = sPackage == "" + + /** Checks, whether `this` extends any other classes. If this is the case, it is '''not''' + * considered a ''root class'', otherwise it is. + */ + def isRootClass: Boolean = parent == null + + /** Provides the set of all symbols that have to be imported in order to use `this` complete + * class. + */ + def collectImports: Set[String] = getNecessaryImports.map(imp => s"${imp.pckg}.${imp.name}") + + /** Augments `this` class with another method. + * + * @param m the additional method. May not be `null` + */ + def addMethod(m: SMethod): Unit = { + Objects.requireNonNull(m, "Method to add may not be null") + if (methods.contains(m)) { + return + } + methods = methods :+ m + } + + def getMethods: Seq[SMethod] = { + if (methods.exists(_.name == "toString")) { + methods + } else { + methods :+ new SMethod( + name = "toString", + result = SType.String, + params = Seq.empty, + implementation = Seq(SMethodStatement((List(s""" "$name: " """) ++ attributes.map(attr => s""" "${attr.name}=" + ${attr.name} + " " """)).mkString(" + "))), + overrides = true + ) + } + } + + /** Augments the constructor by another statement. The statement will be executed lastly (though + * this may change due to further modifications of the constructor). + * + * @param statement the new statement. May not be `null` + */ + def augmentConstructor(statement: SMethodStatement): Unit = { + Objects.requireNonNull(statement, "Statement to add may not be null") + constructorStatements = constructorStatements :+ statement + } + + /** Makes `this` class a subclass of `parent`. + * + * If `parent == this` nothing will happen. + * + * @param parent the superclass. May be `null` to indicate that there is no superclass (other + * than `AnyRef`) + */ + def setParent(parent: STypedElement): Unit = { + if (parent == this) { + return + } + this.parent = parent + } + + /** Sets the attributes of `this` class. + * + * @param attrs the attributes. May be `null` to remove all current attributes. + */ + def setAttributes(attrs: Seq[SAttribute]): Unit = { + if (attrs == null) { + this.attributes = Seq.empty + } else { + this.attributes = attrs + } + } + + /** Provides all types `this` classes accesses in some way. + */ + def getUsedTypes: Set[STypedElement] = { + val parentImport: Seq[STypedElement] = if (isRootClass) List() else List(parent) + + val parentConstructorParamImports: Seq[STypedElement] = if (isRootClass) List() else parent match { + case parent: SClass => parent.attributes.map(_.attrType) + case _ => List() + } + + val attrTypeImports: Seq[STypedElement] = attributes.map(_.attrType) + + val methodResultImports: Seq[STypedElement] = methods.map(_.result) + + val methodParamImports: Seq[STypedElement] = methods.flatMap(_.params).map(_.paramType) + + val methodImplImports: Seq[STypedElement] = methods.flatMap(_.getUsedTypes.toList) + + // create a set to eliminate duplicates + (parentImport + ++ parentConstructorParamImports + ++ attrTypeImports + ++ methodResultImports + ++ methodParamImports + ++ methodImplImports).toSet + } + + override def getName: String = name + + override def getPackage: String = sPackage + + override def getConstructorParameters: Seq[SMethodParameter] = { + val ownParams = attributes.map(attr => SMethodParameter(attr.name, attr.attrType)) + val parentParams = if (isRootClass) List() else parent.getConstructorParameters + ownParams ++ parentParams + } + + override def getInheritanceHierarchy: Seq[STypedElement] = if (isRootClass) List(this) else this +: parent.getInheritanceHierarchy + + override def getNecessaryImports: Set[SImport] = { + val parentImport: List[SImport] = if (isRootClass) List() else includeImportIfNecessary(parent.getPackage, parent.getName) + + val parentConstructorParamImports: List[SImport] = if (isRootClass) List() else parent match { + case parent: SClass => + parent.attributes + .map(attr => includeImportIfNecessary(attr.attrType.getPackage, attr.attrType.getName)) + .fold(List())((l1, l2) => l1 ++ l2) + case _ => List() + } + + val attrTypeImports: List[SImport] = + attributes + .map(attr => includeImportIfNecessary(attr.attrType.getPackage, attr.getType)) + .fold(List())((l1, l2) => l1 ++ l2) + + val methodResultImports: List[SImport] = + methods + .map(_.result) + .map(res => includeImportIfNecessary(res.getPackage, res.getName)) + .fold(List())((l1, l2) => l1 ++ l2) + + val methodParamImports: List[SImport] = + methods + .map(_.params) + .fold(List())((l1, l2) => l1 ++ l2) + .map(param => includeImportIfNecessary(param.paramType.getPackage, param.paramType.getName)) + .fold(List())((l1, l2) => l1 ++ l2) + + val methodImplImports: List[SImport] = + methods + .map(_.getUsedTypes.toList) + .fold(List())((l1, l2) => l1 ++ l2) + .map(typ => includeImportIfNecessary(typ.getPackage, typ.getName)) + .fold(List())((l1, l2) => l1 ++ l2) + + // create a set to eliminate duplicates + (parentImport + ++ parentConstructorParamImports + ++ attrTypeImports + ++ methodResultImports + ++ methodParamImports + ++ methodImplImports).toSet + } + + override def accept(visitor: SModelVisitor): Unit = { + attributes.foreach(_.accept(visitor)) + methods.foreach(_.accept(visitor)) + visitor.visit(this) + } + + protected def canEqual(other: Any): Boolean = other.isInstanceOf[SClass] + + /** Checks if some class has to be imported in order to be usable from `this` class. If this + * is the case it will wrap the necessary import in a `List` for further usage. + * + * @param sPackage the package of the class + * @param sClass the class + * @return an empty list if the class does not need to be imported, or the necessary import + * otherwise + */ + private def includeImportIfNecessary(sPackage: String, sClass: String): List[SImport] = { + if (sPackage != this.sPackage && sPackage != "") List(SImport(sPackage, sClass)) else List() + } + + override def equals(other: Any): Boolean = other match { + case that: SClass => + (that canEqual this) && + name == that.name && + sPackage == that.sPackage + case _ => false + } + + override def hashCode(): Int = { + val state = Seq(name, sPackage) + state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + } + + override def toString: String = s"$name(${attributes.map(_.name).mkString(", ")})" + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SGetter.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SGetter.scala new file mode 100644 index 0000000000000000000000000000000000000000..3cb1dce28b2cca0c736031693bc0c10a392f6988 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SGetter.scala @@ -0,0 +1,16 @@ +package org.rosi_project.model_sync.generator.acr_model + +import org.rosi_project.model_sync.generator.support.ExtendedString.stringToExtended + + +/** Simple representation of a getter method. + * + * @author Rico Bergmann + */ +class SGetter(attr: SAttribute) extends SMethod( + name = s"get${attr.name.firstLetterToUpperCase}", + result = attr.attrType, + params = Seq.empty, + implementation = Seq(SMethodStatement(content = attr.name, usedTypes = Set(attr.attrType)))) { + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SImport.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SImport.scala new file mode 100644 index 0000000000000000000000000000000000000000..b3511ce453d0d8b02a42e8625e8fda954c4faf22 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SImport.scala @@ -0,0 +1,34 @@ +package org.rosi_project.model_sync.generator.acr_model + +/** Wraps a type that needs to be imported in order for some class to compile. + * + * @param pckg the package that contains the type to import + * @param name the name of the type to import + * @author Rico Bergmann + */ +case class SImport(pckg: String, name: String) + +/** The companion provides convenience methods to deal with sets of imports. + */ +object SImport { + + /** Creates the imports necessary to access the specified types. + */ + def generateImports(types: STypedElement*): Set[SImport] = removeUnnecessaryImports(types.map(t => SImport(t.getPackage, t.getName)).toSet) + + /** Creates the imports necessary to access the specified types. Necessity to imports will be + * assessed from a base package. + */ + def generateImportsForBasePackage(basePckg: String, types: STypedElement*) = removeUnnecessaryImports(generateImports(types: _*), basePckg) + + /** Deletes all the imports which refer to types in the default package and are therefore not + * necessary. + */ + def removeUnnecessaryImports(imports: Set[SImport]): Set[SImport] = imports.filter(_.pckg != "") + + /** Deletes all the imports which refer to types in the default package or the same package as + * specified and are therefore not necessary. + */ + def removeUnnecessaryImports(imports: Set[SImport], basePckg: String): Set[SImport] = imports.filter(imp => imp.pckg != "" && imp.pckg != basePckg) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SMethod.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SMethod.scala new file mode 100644 index 0000000000000000000000000000000000000000..e17dd79cbe0f5b8a3c2f1e88c1e14bc994cd9526 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SMethod.scala @@ -0,0 +1,79 @@ +package org.rosi_project.model_sync.generator.acr_model + +import java.util.Objects + +// TODO use a simple DSL to represent the method body? + +/** Abstraction of a method. + * + * @author Rico Bergmann + */ +class SMethod(val name: String, + val result: STypedElement, + val params: Seq[SMethodParameter], + var implementation: Seq[SMethodStatement], + var overrides: Boolean = false) + extends SModelElement { + + /** Provides the return-type as a `String`. + * + * @return the type. Will never be `null` nor empty. + */ + def getResultType: String = result.getName + + /** Replaces the current body of `this` method. + * + * @param impl the new body. May be `null` to remove all current statements, leaving the body + * empty. + */ + def updateImplementation(impl: Seq[SMethodStatement]): Unit = { + if (impl == null) { + this.implementation = Seq.empty + } else { + this.implementation = impl + } + } + + /** Adds another statement to the current method body. + * + * @param statement the statement. May not be `null` + */ + def augmentImplementation(statement: SMethodStatement): Unit = { + Objects.requireNonNull(statement, "Statement to add may not be null") + this.implementation = this.implementation :+ statement + } + + /** Collects all types which need to be imported for `this` method to use. + */ + def getUsedTypes: Set[STypedElement] = { + val resultType = result match { + case GenericSType(_, _, typeParam) => Seq(result, typeParam) + case SType.Unit => Seq.empty + case _ => Seq(result) + } + val paramTypes = params.map(_.paramType) + val implementationTypes = implementation.map(_.usedTypes).fold(Set.empty)((s1, s2) => s1 ++ s2) + (resultType ++ paramTypes).toSet ++ implementationTypes + } + + override def accept(visitor: SModelVisitor): Unit = visitor.visit(this) + + protected def canEqual(other: Any): Boolean = other.isInstanceOf[SMethod] + + override def equals(other: Any): Boolean = other match { + case that: SMethod => + (that canEqual this) && + name == that.name && + result == that.result && + params == that.params + case _ => false + } + + override def hashCode(): Int = { + val state = Seq(name, result, params) + state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + } + + override def toString: String = s"$name(${params.map(_.name).mkString(", ")}): $getResultType" + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SMethodParameter.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SMethodParameter.scala new file mode 100644 index 0000000000000000000000000000000000000000..6ac68dd5e24b1733742e43a3fbbf5f1dc7f2b3bb --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SMethodParameter.scala @@ -0,0 +1,17 @@ +package org.rosi_project.model_sync.generator.acr_model + +/** Wrapper for parameters that need to be passed to a method. + * + * @author Rico Bergmann + */ +case class SMethodParameter(name: String, paramType: STypedElement) { + + /** Provides the type of `this` parameter as a `String`. + * + * @return the type. Will never be `null` nor empty. + */ + def getType: String = paramType.getName + + override def toString: String = s"$name: $getType" + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SMethodStatement.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SMethodStatement.scala new file mode 100644 index 0000000000000000000000000000000000000000..c3b7c213d9fec9f83b1947f9d3b9c4dbf54b0677 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SMethodStatement.scala @@ -0,0 +1,30 @@ +package org.rosi_project.model_sync.generator.acr_model + +/** Wrapper for the statements that form a method. + * + * As for now we represent the actual statement as a simple `String` the types used in the + * statement need to be given explicitly and cannot be derived as e.g. in the case of [[SClass]] + * or [[SMethod]]. + * + * To get the actual statement [[getContent]] should be used instead of accessing `content` + * directly. This will make usage more robust if the content will be replaced by a more + * sophisticated structure later on (which is likely to happen). + * + * @author Rico Bergmann + */ +case class SMethodStatement(content: String, usedTypes: Set[STypedElement] = Set.empty) { + + /** The actual statement as a `String`. + * + * We provide this method as a stable API to the statement. Accessing the content directly + * through the corresponding property will couple code too tightly to this current + * implementation of `content` - which is likely to be replaced by a more sophisticated + * structure to represent method statements. + * + * @return the statement. Will never be `null` nor empty. + */ + def getContent: String = content + + override def toString: String = getContent + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SModel.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SModel.scala new file mode 100644 index 0000000000000000000000000000000000000000..280d69f0f4631ce8f75a2845ed3ec768d9fb7e5f --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SModel.scala @@ -0,0 +1,22 @@ +package org.rosi_project.model_sync.generator.acr_model + +/** A model is a collection of classes that represents a specific data-driven view on the + * application. + * + * @author Rico Bergmann + */ +trait SModel extends SModelElement { + + /** Provides all the classes in `this` model. + */ + def getAllClasses: Set[SClass] + + /** Extends the model by a new class. + * + * @param mClass the class to add. May never `null`. + */ + def addModelClass(mClass: SClass): Unit + + override def toString: String = s"SModel: classes=$getAllClasses" + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SModelElement.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SModelElement.scala new file mode 100644 index 0000000000000000000000000000000000000000..cf4aa07916483f80a0d3cef3d2c60070fe9ed3f3 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SModelElement.scala @@ -0,0 +1,19 @@ +package org.rosi_project.model_sync.generator.acr_model + +/** Base class for all parts of the ''ACR''. It merely exists to enable the ''Visitor pattern''. + * + * @see [[https://en.wikipedia.org/wiki/Visitor_pattern Visitor pattern @ Wikipedia]] + * @author Rico Bergmann + */ +trait SModelElement { + + /** Executes the `visitor` on the structure of `this` element. + * + * Implementations will differ naturally but the most common approach is to recursively call + * `accept` on all composed elements and run `visitor.visit(this)` at the end. + * + * @param visitor the visitor. Might not be `null`. + */ + def accept(visitor: SModelVisitor) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SModelVisitor.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SModelVisitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..f4c43e6c3dbe89fdeadb23a54a2cedbad6d434ab --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SModelVisitor.scala @@ -0,0 +1,46 @@ +package org.rosi_project.model_sync.generator.acr_model + +// TODO fix cyclic imports with classes from acr package + +/** Runs an algorithm on model instances. + * + * @see [[https://en.wikipedia.org/wiki/Visitor_pattern Visitor pattern @ Wikipedia]] + * @author Rico Bergmann + */ +trait SModelVisitor { + + /** Runs the algorithm as appropriate for the whole model. + * + * @param sModel the model to run the algorithm on. Whether it may be `null` is implementation + * specific. + */ + def visit(sModel: SModel): Unit + + /** Runs the algorithm as appropriate for a single class of the model. + * + * @param sClass the class to run the algorithm on + */ + def visit(sClass: SClass): Unit + + /** Runs the algorithm as appropriate for an attribute of a model class. + * + * @param sAttr the attribute to run the algorithm on. Whether it may be `null` is implementation + * specific. + */ + def visit(sAttr: SAttribute): Unit + + /** Runs the algorithm as appropriate for a method of a model class. + * + * @param sMethod the method to run the algorithm on. Whether it may be `null` is implementation + * specific. + */ + def visit(sMethod: SMethod): Unit + + /** Runs the algorithm as appropriate for a type of the model. + * + * @param sType the type to run the algorithm on. Whether it may be `null` is implementation + * specific. + */ + def visit(sType: SType): Unit + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SSetter.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SSetter.scala new file mode 100644 index 0000000000000000000000000000000000000000..563c9a8d114b1d7752a059ae79dba181286885e4 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SSetter.scala @@ -0,0 +1,15 @@ +package org.rosi_project.model_sync.generator.acr_model + +import org.rosi_project.model_sync.generator.support.ExtendedString.stringToExtended + +/** Simple representation of a setter method. + * + * @author Rico Bergmann + */ +class SSetter(attr: SAttribute) extends SMethod( + name = s"set${attr.name.firstLetterToUpperCase}", + result = SType.Unit, + params = Seq(SMethodParameter(attr.name.head.toString, attr.attrType)), + implementation = Seq(SMethodStatement(content = s"${attr.name} = ${attr.name.head}", usedTypes = Set(attr.attrType)))) { + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SType.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SType.scala new file mode 100644 index 0000000000000000000000000000000000000000..a3a15f4f511ef046fa9fad55d026a46c5566828f --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SType.scala @@ -0,0 +1,40 @@ +package org.rosi_project.model_sync.generator.acr_model + +/** Represents types provided by the Java runtime (either as part of the Java/Scala standard + * libraries or other third-party projects). + * + * In general as a rule of thumb when loading a model the classes defined by the model should be + * mapped to instances of [[SClass]] whereas all those types that are used (but not defined) within + * the model and therefore function as some kind of "support-type" should become instances of this + * class. + * + * @see [[SClass]] + * @author Rico Bergmann + */ +case class SType(name: String, sPackage: String = "") extends STypedElement { + + override def getName: String = name + + override def getPackage: String = sPackage + + override def accept(visitor: SModelVisitor): Unit = visitor.visit(this) + +} + +/** The companion defines frequently used types. + */ +object SType { + + /** The empty type. + */ + val Unit = SType("Unit") + + /** Wrapper for `AnyRef`. + */ + val AnyRef = SType("AnyRef") + + /** Wrapper for `String`. + */ + val String = SType("String") + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/STypeRegistry.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/STypeRegistry.scala new file mode 100644 index 0000000000000000000000000000000000000000..345d9b8b7b13d8a2c53fdde1423736865ddad4f6 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/STypeRegistry.scala @@ -0,0 +1,68 @@ +package org.rosi_project.model_sync.generator.acr_model + +/** Repository to keep track of all types and classes of model. It ensures that all attributes, + * methods, etc. reference the same type instances and thus prevent duplication and conflicting + * states. The registry should be treated as "''single point of truth''". + * + * @author Rico Bergmann + */ +object STypeRegistry { + + private var registeredTypes: Set[STypedElement] = Set() + + registerDefaultTypes() + + /** Registers a type if it is not already known. + * + * @param theType the new type. May never be `null`. + * @return `theType` if it was indeed unknown before. Otherwise the currently registered type. + */ + def addType(theType: STypedElement): STypedElement = { + val existing: Option[STypedElement] = registeredTypes.find(existing => existing.getName == theType.getName && existing.getPackage == theType.getPackage) + if (existing.isEmpty) { + registeredTypes += theType + theType + } else { + existing.get + } + } + + /** Searches for a type. + * + * @param name the type's name + * @param sPackage the package that contains the type + * @return the type if it was found + */ + def query(name: String, sPackage: String): Option[STypedElement] = { + registeredTypes.find(t => t.getName == name && t.getPackage == sPackage) + } + + /** Searches for a type based on its name. It may reside in any package. + * + * @param name the type's name + * @return the type if it was found + */ + def queryForName(name: String): Option[STypedElement] = { + registeredTypes.find(_.getName == name) + } + + /** Provides all types that are currently in the repository. + */ + def allTypes: Set[STypedElement] = registeredTypes + + private def registerDefaultTypes(): Unit = { + registeredTypes ++= Seq( + SType("Boolean"), + SType("Byte"), + SType("Short"), + SType("Integer"), + SType("Long"), + SType("Float"), + SType("Double"), + SType("String"), + ) + } + + override def toString: String = s"Registry: $registeredTypes" + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/STypedElement.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/STypedElement.scala new file mode 100644 index 0000000000000000000000000000000000000000..d89878c254515cac3bf962435253300052e1135f --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/STypedElement.scala @@ -0,0 +1,41 @@ +package org.rosi_project.model_sync.generator.acr_model + +/** Abstraction of "elements" that may be used in source code where a type is expected. + * + * Currently there are two different kinds of such elements: + * + * - instances of [[SClass]] are supplied by the user + * - instances of [[SType]] are provided by the Java runtime (they are classes of the standard + * Java libraries or others) + * + * @author Rico Bergmann + */ +trait STypedElement extends SModelElement { + + /** The name of `this` type. + * + * @return the name. Will never be `null` nor empty. + */ + def getName: String + + /** The package containing `this` type. + * + * @return + */ + def getPackage: String + + /** The parameters necessary to create an instance of `this` type. + */ + def getConstructorParameters: Seq[SMethodParameter] = Seq.empty + + /** The inheritance hierarchy provides information about the types `this` extends. It will start + * with `this` (thus the hierarchy will 'never' be empty) and end with the root type. `AnyRef` + * will be ignored however. + */ + def getInheritanceHierarchy: Seq[STypedElement] = Seq.empty + + /** Provides all classes that need to be imported for `this` type. + */ + def getNecessaryImports: Set[SImport] = Set.empty + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SimpleSModel.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SimpleSModel.scala new file mode 100644 index 0000000000000000000000000000000000000000..4722a9ad758e180a69e4fc59cf9c92ffa927471c --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/SimpleSModel.scala @@ -0,0 +1,39 @@ +package org.rosi_project.model_sync.generator.acr_model + +import org.rosi_project.model_sync.generator.support.Assert + +/** Default implementation of the [[SModel]]. + * + * @author Rico Bergmann + */ +class SimpleSModel extends SModel { + + private var sClasses: Set[SClass] = Set.empty + + override def addModelClass(mClass: SClass): Unit = { + Assert.notNull(mClass, "Class may not be null") + sClasses += mClass + } + + override def getAllClasses: Set[SClass] = sClasses + + override def accept(visitor: SModelVisitor): Unit = { + sClasses.foreach(_.accept(visitor)) + visitor.visit(this) + } + + def canEqual(other: Any): Boolean = other.isInstanceOf[SimpleSModel] + + override def equals(other: Any): Boolean = other match { + case that: SimpleSModel => + (that canEqual this) && + sClasses == that.sClasses + case _ => false + } + + override def hashCode(): Int = { + val state = Seq(sClasses) + state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/package.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/package.scala new file mode 100644 index 0000000000000000000000000000000000000000..1be79e6a19ce1c5a1a18f968e2ab516cd2bcb175 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/package.scala @@ -0,0 +1,16 @@ +package org.rosi_project.model_sync.generator + +import org.rosi_project.model_sync.generator.acr_model.{SClass, SModel} + +/** Abstract class representation of the Scala code used within a model. + * + * This package specifies a simplified meta-model of scala classes. Most parts of (our take on) a + * Scala class have a corresponding wrapper class that may be modified for further adaptations. + * + * @see [[SClass]] + * @see [[SModel]] + * @author Rico Bergmann + */ +package object acr_model { + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/types/PredefTypes.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/types/PredefTypes.scala new file mode 100644 index 0000000000000000000000000000000000000000..d181569e09da6186cb0ba1aa877b4d1e4f586a3a --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/types/PredefTypes.scala @@ -0,0 +1,38 @@ +package org.rosi_project.model_sync.generator.acr_model.types + +import org.rosi_project.model_sync.generator.acr_model.{GenericSType, SType, STypedElement} + +/** Contains a number of types that are part of the JSE as well as some more advanced Scala types. + * + * This should prevent creating them over and over again every time a `SType` should be set to any + * of these types. + * + * @author Rico Bergmann + */ +object PredefTypes { + + /** `java.lang.Object` + */ + val Object = SType("Object") + + /** `java.io.File` + */ + val File = SType("File", "java.io") + + /** `java.lang.reflect.Parameter` + */ + val Parameter = SType("Parameter", "java.lang.reflect") + + /** `java.lang.Class[typ]` + * + * @param typ the type parameter for the class type + */ + def classOf(typ: STypedElement): GenericSType = new GenericSType("Class", typeParam = typ) + + /** `Option[typ]` + * + * @param typ the type parameter for the Option type + */ + def option(typ: STypedElement): GenericSType = new GenericSType("Option", typeParam = typ) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/types/SList.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/types/SList.scala new file mode 100644 index 0000000000000000000000000000000000000000..a7d11b35fa3045539727f7d5ddc5df9ea01603f8 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/types/SList.scala @@ -0,0 +1,21 @@ +package org.rosi_project.model_sync.generator.acr_model.types + +import org.rosi_project.model_sync.generator.acr_model._ + +/** Wraps a Scala `List` for some type. + */ +class SList(elemType: STypedElement) extends SSeq(elemType) { + + override def getConstructorParameters: Seq[SMethodParameter] = Seq(SMethodParameter("elems", elemType)) + +} + +/** The companion provides `apply` and `unapply` methods. + */ +object SList { + + def apply(elemType: STypedElement): SList = new SList(elemType) + + def unapply(arg: SList): Option[STypedElement] = Some(arg.elemType) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/acr_model/types/SSeq.scala b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/types/SSeq.scala new file mode 100644 index 0000000000000000000000000000000000000000..e6a9f5ded7b0d289ec384126b9a20c07481f26aa --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/acr_model/types/SSeq.scala @@ -0,0 +1,21 @@ +package org.rosi_project.model_sync.generator.acr_model.types + +import org.rosi_project.model_sync.generator.acr_model.{GenericSType, SMethodParameter, STypedElement} + +/** Wraps a Scala `Seq` for some type. + */ +class SSeq(val elemType: STypedElement) extends GenericSType(name = "Seq", sPackage = "", typeParam = elemType) { + + override def getConstructorParameters: Seq[SMethodParameter] = Seq(SMethodParameter("elems", elemType)) + +} + +/** The companion provides `apply` and `unapply` methods. + */ +object SSeq { + + def apply(elemType: STypedElement): SSeq = new SSeq(elemType) + + def unapply(arg: SSeq): Option[STypedElement] = Some(arg.elemType) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/conversion/Converter.scala b/src/main/scala/org/rosi_project/model_sync/generator/conversion/Converter.scala new file mode 100644 index 0000000000000000000000000000000000000000..502465439eda93fe71934b67d95cc9eca6fd1d0e --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/conversion/Converter.scala @@ -0,0 +1,18 @@ +package org.rosi_project.model_sync.generator.conversion + +/** A converter converts instance of a source type `S` to a target type `T`. + * + * @tparam S the source type + * @tparam T the target type + * @author Rico Bergmann + */ +trait Converter[S, T] { + + /** Converts the source object. + * + * @param source the object to convert + * @return the converted object + */ + def convert(source: S): T + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/conversion/EmfTypeTranslator.scala b/src/main/scala/org/rosi_project/model_sync/generator/conversion/EmfTypeTranslator.scala new file mode 100644 index 0000000000000000000000000000000000000000..716e851ce938be96a22c2dc072cf257a0a29ce75 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/conversion/EmfTypeTranslator.scala @@ -0,0 +1,49 @@ +package org.rosi_project.model_sync.generator.conversion + +import java.util.Objects + +import org.eclipse.emf.ecore.EDataType +import org.rosi_project.model_sync.generator.acr_model.SType + +/** Service to map an instance of [[EDataType]] to its corresponding [[SType]] (which wrap Scala's + * native types). + * + * @author Rico Bergmann + */ +object EmfTypeTranslator { + + private val typeMap: Map[String, SType] = Map( + "EBoolean" -> SType("Boolean"), + "EByte" -> SType("Byte"), + "EChar" -> SType("Char"), + "EDate" -> SType(name = "Date", sPackage = "java.util"), + "EDouble" -> SType("Double"), + "EFloat" -> SType("Float"), + "EInt" -> SType("Int"), + "EJavaObject" -> SType("Object"), + "ELong" -> SType("Long"), + "EShort" -> SType("Short"), + "EString" -> SType("String"), + ) + + /** Maps an EMF data type to its corresponding Scala type. + * + * @param dataType the source type. May not be `null`. + * @return the matching type. If there is none, the `Option` will be empty. + */ + def getSClassFromEmf(dataType: EDataType): Option[SType] = { + Objects.requireNonNull(dataType, "Data type may not be null") + typeMap.get(dataType.getName) + } + + /** Maps an EMF data type to its corresponding Scala type. + * + * @param dataType the source type. May not be `null`. + * @return the matching type. If there is none, the `Option` will be empty. + */ + def getSClassFromEmf(dataType: String): Option[SType] = { + Objects.requireNonNull(dataType, "Data type may not be null") + typeMap.get(dataType) + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/conversion/SClassConverter.scala b/src/main/scala/org/rosi_project/model_sync/generator/conversion/SClassConverter.scala new file mode 100644 index 0000000000000000000000000000000000000000..76b10c5c96d06b597cdbe8ec78dbe3896b66e75a --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/conversion/SClassConverter.scala @@ -0,0 +1,135 @@ +package org.rosi_project.model_sync.generator.conversion + +import org.eclipse.emf.ecore.{EAttribute, EClass, EReference} +import org.rosi_project.model_sync.generator.acr_model._ +import org.rosi_project.model_sync.generator.acr_model.types.SList + +/** Converter to generate instances of [[SClass]] based on [[EClass EClasses]]. + * + * @author Rico Bergmann + */ +class SClassConverter extends Converter[EClass, SClass] { + + override def convert(source: EClass): SClass = { + var attrs: List[SAttribute] = List.empty + + // fetch the attributes of the SClass + (source.getEAttributes: List[EAttribute]).foreach(eAttr => { + val attrType: STypedElement = STypeRegistry + + // check if the attribute type is already known and registered + .query(eAttr.getEAttributeType.getName, eAttr.getEAttributeType.getEPackage.getName) + + // otherwise create a new type + .getOrElse { + val newAttr = EmfTypeTranslator + + // if the type is wrapped by EMF (such as EString), use the corresponding scala type + .getSClassFromEmf(eAttr.getEAttributeType) + + // otherwise create a new class (as the attribute should instance of a class rather than a type in this case) + .getOrElse(new SClass(eAttr.getEAttributeType.getName, sPackage = eAttr.getEAttributeType.getEPackage.getName)) + + // finally save the type + STypeRegistry.addType(newAttr) + } + + attrs ::= SAttribute(eAttr.getName, attrType) + }) + + // TODO add support for multiple inheritance + + (source.getEReferences: List[EReference]) + .filter(_.isContainment) + .foreach(eRef => { + val attrName = eRef.getName + val attrType = eRef.getEReferenceType.getName + val attrPckg = eRef.getEReferenceType.getEPackage.getName + + var sAttrType: STypedElement = STypeRegistry + + // check if the attribute type is already known and registered + .query(attrType, attrPckg) + + // otherwise create a new type + .getOrElse { + val newAttr = EmfTypeTranslator + + // if the type is wrapped by EMF (such as EString), use the corresponding scala type + .getSClassFromEmf(attrType) + + // otherwise create a new class (as the attribute should instance of a class rather than a type in this case) + .getOrElse(new SClass(attrType, sPackage = attrPckg)) + + // finally save the type + STypeRegistry.addType(newAttr) + } + + if (eRef.getUpperBound > 1 || eRef.getUpperBound == -1) { + sAttrType = SList(sAttrType) + } + + attrs ::= SAttribute(attrName, sAttrType) + + }) + + val parents: List[EClass] = source.getESuperTypes.toArray(new Array[EClass](0)).toList + + // we may not convert class hierarchies that utilize multiple inheritance yet + if (violatesSingleInheritance(parents)) { + throw new UnconvertibleEmfException(source.getEPackage, s"For class: $source") + } + + val parent: STypedElement = parents.headOption.map((p: EClass) => { + STypeRegistry + + // check if we already know the parent + .queryForName(p.getName) + + // otherwise we need to create and register it + .getOrElse { + val parentSClass: SClass = new SClass(p.getName, sPackage = p.getEPackage.getName) + + // register the parent (it will be visited and completely inflated later on) + STypeRegistry.addType(parentSClass) + parentSClass + } + }).orNull + + + // TODO add methods to SClass + + /* `convert` may be called on two different occasions: either for a completely new type or + * for a type that was already created before when another type was being inflated by `convert`. + * In the first case we need to set up all the necessary attributes of the SClass and therefore + * create it from scratch. + * Otherwise the most basic attributes (i.e. name and package) have already been set up and + * have to be left unchanged (they are values, actually). However parent and attributes of the + * type have not been inflated during the first visit (as the only important part then was to + * have the type known). Therefore we will set these now. + */ + + val currentClass: Option[STypedElement] = STypeRegistry.query(source.getName, source.getEPackage.getName) + + currentClass.map { + case clazz: SClass => + clazz.attributes = attrs + clazz.parent = parent + clazz + case sType => + sys.error(s"sType should have been a class: $sType") + }.getOrElse { + val createdClass: SClass = new SClass(source.getName, source.getEPackage.getName, attrs, parent) + STypeRegistry.addType(createdClass) + createdClass + } + + } + + /** Checks whether there is more than one parent class. + * + * This method is just to express the constraint more explicitly. + */ + private def violatesSingleInheritance(parents: List[EClass]): Boolean = parents.length > 1 + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/conversion/SModelGenerator.scala b/src/main/scala/org/rosi_project/model_sync/generator/conversion/SModelGenerator.scala new file mode 100644 index 0000000000000000000000000000000000000000..51057830ce435267e5f7f8b15f34cb14b4ef870e --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/conversion/SModelGenerator.scala @@ -0,0 +1,35 @@ +package org.rosi_project.model_sync.generator.conversion + +import org.eclipse.emf.ecore.{EClass, EGenericType, EPackage} +import org.rosi_project.model_sync.generator.acr_model.{SModel, SType, STypeRegistry, SimpleSModel} + +import scala.collection.JavaConverters._ + +/** Converter to generate an [[SModel]] from ecore. + * + * @author Rico Bergmann + */ +class SModelGenerator extends Converter[EPackage, SModel] { + + override def convert(source: EPackage): SModel = { + val packageName = if (source.getName != null) source.getName else "" + val contents = source.eAllContents().asScala + + val model = new SimpleSModel + + println("... Converting ecore model") + contents.foreach { + case ec: EClass => + model.addModelClass(new SClassConverter convert ec) + STypeRegistry.addType(SType(ec.getName, packageName)) + case _ => + // we only care about classes. Types will be registered as soon as they are needed as + // attributes + } + println("... Conversion finished") + println(s"Generated model: $model") + + model + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/conversion/SModelGeneratorTest.scala b/src/main/scala/org/rosi_project/model_sync/generator/conversion/SModelGeneratorTest.scala new file mode 100644 index 0000000000000000000000000000000000000000..16cee3fbd4f8dee34a5278703d94aa3dc352bc64 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/conversion/SModelGeneratorTest.scala @@ -0,0 +1,21 @@ +package org.rosi_project.model_sync.generator.conversion + +import org.eclipse.emf.ecore.{EObject, EPackage} +import org.eclipse.emf.ecore.resource.Resource +import scroll.internal.ecore.ECoreImporter + +/** + * @author Rico Bergmann + */ +object SModelGeneratorTest/* extends App */{ + + val importer = new ECoreImporter { + def lm(): Resource = loadModel() + } + importer.path = "assets/ttc17.ecore" + val res = importer.lm() + println(s"We done: $res") + + res.getContents.toArray(new Array[EObject](0)).toList.find(_.isInstanceOf[EPackage]).foreach(e => (new SModelGenerator).convert(e.asInstanceOf[EPackage])) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/conversion/UnconvertibleEmfException.scala b/src/main/scala/org/rosi_project/model_sync/generator/conversion/UnconvertibleEmfException.scala new file mode 100644 index 0000000000000000000000000000000000000000..12093b2899c94002cee12b9775937659cf511caf --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/conversion/UnconvertibleEmfException.scala @@ -0,0 +1,15 @@ +package org.rosi_project.model_sync.generator.conversion + +import org.eclipse.emf.ecore.EPackage +import org.rosi_project.model_sync.generator.acr_model.SModel + +/** Exception to indicate that a EMF model may not be converted to an [[SModel]] because some + * constraints are violated. + * + * @author Rico Bergmann + */ +class UnconvertibleEmfException(val model: EPackage, val message: String = "") extends RuntimeException(message) { + + override def toString: String = s"UnconvertibleException: $model ${if (message.isEmpty) "" else s"($message)"}" + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/conversion/package.scala b/src/main/scala/org/rosi_project/model_sync/generator/conversion/package.scala new file mode 100644 index 0000000000000000000000000000000000000000..46f952645700b7610e8b23d1389d831e862e6845 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/conversion/package.scala @@ -0,0 +1,32 @@ +package org.rosi_project.model_sync.generator + +import org.eclipse.emf.common.util.EList +import org.rosi_project.model_sync.generator.acr_model.SModel + +/** Provides services to convert EMF ecore-models to corresponding instances of [[SModel]]. + * + * @author Rico Bergmann + */ +package object conversion { + + /** Converts an EMF list instance to a corresponding Scala `List`. + * + * This converter is necessary as `EList` is an instance of `java.util.List` and may thus not be + * used like a Scala list conveniently. + * + * @param eList the EMF list. May be `null`. + * @tparam T the elements in the EMF list + * @return the Scala list containing all the elements of the original list. Will be `null` if + * `elist` is. + */ + implicit def elist2slist[T](eList: EList[T]): List[T] = { + if (eList == null) { + null + } else { + var res: List[T] = List() + eList.forEach(elem => res = res :+ elem) + res + } + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/env/FilesCompiler.scala b/src/main/scala/org/rosi_project/model_sync/generator/env/FilesCompiler.scala new file mode 100644 index 0000000000000000000000000000000000000000..516f2859e01e910e66f57eaeef2d17b0a28f814c --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/env/FilesCompiler.scala @@ -0,0 +1,42 @@ +package org.rosi_project.model_sync.generator.env + +import java.io.PrintWriter + +import scala.reflect.io.File +import scala.tools.nsc.reporters.ConsoleReporter +import scala.tools.nsc.{GenericRunnerSettings, Global, Settings} + +/** Utility to compile all set of Scala source files to Java byte code. + * + * @param outDir the directory where the compiled files should placed + * @author Rico Bergmann + */ +class FilesCompiler(outDir: File) { + + /** Compiles all given files. + */ + def run(filesToCompile: List[File]): Unit = { + // see: https://stackoverflow.com/a/20323371/5161760 + try { + val out = new PrintWriter(System.out) + val compilationSettings: Settings = new GenericRunnerSettings(out.println) + // just re-use the whole classpath + compilationSettings.classpath.value = JClassPath.adaptClassPathToOSEnv(JClassPath.fetchCurrentClassPath).distinct.mkString(File.pathSeparator) + println(s"Using classpath ${compilationSettings.classpath.value}") + compilationSettings.outdir.value = outDir.toAbsolute.toString + val reporter = new ConsoleReporter(compilationSettings) + val compiler = new Global(compilationSettings, reporter) + val runner = new compiler.Run + runner.compile(filesToCompile.map(_.toAbsolute.toString)) + } catch { + case e: Exception => throw new CompilationException(e) + } + } + +} + +/** Exception to indicate that the compilation process failed. + * + * @param cause the causing exception + */ +class CompilationException(cause: Exception) extends RuntimeException(cause) diff --git a/src/main/scala/org/rosi_project/model_sync/generator/env/JClassPath.scala b/src/main/scala/org/rosi_project/model_sync/generator/env/JClassPath.scala new file mode 100644 index 0000000000000000000000000000000000000000..fa936742e12179c6c168dab65432b6f220969982 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/env/JClassPath.scala @@ -0,0 +1,56 @@ +package org.rosi_project.model_sync.generator.env + +import java.net.URLClassLoader + +/** Utility to provide high level access to the application's ''classpath'' as well as some basic + * transformations. + * + * @author Rico Bergmann + */ +object JClassPath { + + /** Provides a platform-specific version of the ''classpath''. All entries may than be resolved as + * files on the current OS. + * + * @param cp the ''classpath'' to adapt + */ + def adaptClassPathToOSEnv(cp: List[String]): List[String] = { + sys.props("os.name").toLowerCase match { + case win if win contains "windows" => + cp.map(entry => { + val WinUrl = """file:/(.*)""".r + entry match { + case WinUrl(path) => path + case p => p + } + }).map(_.replaceAll("/", "\\\\").replaceAll("%20", " ")) + case unix => + val UnixUrl = """file:(.*)""".r + cp.map { + case UnixUrl(path) => path + case p => p + } + } + } + + /** Gets the ''classpath'' as it is currently used by this application. + */ + def fetchCurrentClassPath: List[String] = { + val ctxLoader = Thread.currentThread().getContextClassLoader + ctxLoader match { + case urlCL: URLClassLoader => + urlCL.getURLs.toList.map { + _.toString + } + case wrappedCL => + wrappedCL.getParent match { + case urlCL: URLClassLoader => + urlCL.getURLs.toList.map { + _.toString + } + } + case something => sys.error(s"Could not unwrap class loader: $something") + } + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/env/JarPackager.scala b/src/main/scala/org/rosi_project/model_sync/generator/env/JarPackager.scala new file mode 100644 index 0000000000000000000000000000000000000000..3d1478f542696ad245c92c73ba770637a9f0ad6b --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/env/JarPackager.scala @@ -0,0 +1,79 @@ +package org.rosi_project.model_sync.generator.env + +import java.io.{BufferedInputStream, FileInputStream, FileOutputStream} +import java.util.jar.{Attributes, JarEntry, JarOutputStream, Manifest} + +import scala.reflect.io.{Directory, File} +import scala.util.control.Breaks.{break, breakable} + +/** Utility to put all files from some directory into a JAR. + * + * This will simply copy all files from the source directory into the JAR at the target directory. + * + * @param inputDir the directory where all source files reside. + * @param outputDir the target directory where the JAR will be created + * @param jarName the name of the JAR file to create + * @author Rico Bergmann + */ +class JarPackager(inputDir: File, outputDir: File, jarName: String = "model.jar") { + + /** Starts the packaging process. + */ + def run(): Unit = { + // see: https://stackoverflow.com/a/1281295/5161760 + try { + val mf = new Manifest + mf.getMainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0") + val targetJar = new JarOutputStream(new FileOutputStream(outputDir + File.separator + jarName), mf) + var filesToAdd: List[java.io.File] = inputDir.jfile.listFiles.toList + + while (filesToAdd.nonEmpty) { + val file = filesToAdd.head + var fname = file.getAbsolutePath.replace(inputDir.jfile.getAbsolutePath + File.separator, "").replaceAll("\\\\", "/") + println(s"Adding to JAR: $fname") + if (file.isDirectory) { + if (!fname.endsWith("/")) { + fname += "/" + } + filesToAdd ++= file.listFiles.toList + + val entry = new JarEntry(fname) + entry.setTime(file.lastModified) + targetJar.putNextEntry(entry) + targetJar.closeEntry() + } else { + val entry = new JarEntry(fname) + entry.setTime(file.lastModified) + targetJar.putNextEntry(entry) + + val in = new BufferedInputStream(new FileInputStream(file)) + var buffer = new Array[Byte](1024) + breakable { + while (true) { + val bytesRead = in.read(buffer) + if (bytesRead == -1) { + break + } + targetJar.write(buffer, 0, bytesRead) + } + } + targetJar.closeEntry() + in.close() + } + + filesToAdd = filesToAdd.tail + } + + targetJar.close() + } catch { + case e: Exception => throw new JarPackaginException(e) + } + } + +} + +/** Exception to indicate that the packaging process failed. + * + * @param cause the causing exception + */ +class JarPackaginException(cause: Exception) extends RuntimeException(cause) diff --git a/src/main/scala/org/rosi_project/model_sync/generator/io/SClassWriter.scala b/src/main/scala/org/rosi_project/model_sync/generator/io/SClassWriter.scala new file mode 100644 index 0000000000000000000000000000000000000000..ca1d49cefc4bcbb85b8656b7c42ac925366eb984 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/io/SClassWriter.scala @@ -0,0 +1,71 @@ +package org.rosi_project.model_sync.generator.io + +import org.rosi_project.model_sync.generator.acr_model.{SAttribute, SMethod, SClass} +import org.rosi_project.model_sync.generator.support.ExtendedString.stringToExtended + +/** The `Writer` generates the source code of a [[SClass]] and provides it as a `String`. + * + * @author Rico Bergmann + */ +class SClassWriter(val modelClass: SClass) { + + private val pckg: String = if (modelClass.isDefaultPackage) "" else s"package ${modelClass.sPackage}" + private val imports: Seq[String] = modelClass.collectImports.toSeq + + private val clazzFixture: String = generateClassFixture + + /** Provides a source code representation of the `modelClass` as a `String`. + * + * When writing it to a file it will be able to be compiled with the `scalac` (assuming the + * ''classpath'' is set-up correctly). + */ + def stringify: String = { + s"""$pckg + | + |${imports.map(i => s"import $i").mkString("\n")} + | + |class $clazzFixture { + | + | ${modelClass.getAdditionalConstructorStatements.map(_.getContent).mkString("\n")} + | + | ${modelClass.getMethods.map(stringifyMethod).mkString("\n")} + | + |} + """.stripMargin + } + + /** Writes a method as source code. + * + * @param m the method to write + * @return the `String` representation of `m` + */ + protected def stringifyMethod(m: SMethod): String = { + s"""${if (m.overrides) "override" else ""} def ${m.name}(${m.params.map(param => s"${param.name}: ${param.getType}").mkString(", ")}): ${m.getResultType} = { + | ${m.implementation.map(_.getContent).mkString("\n")} + |} + """.stripMargin + } + + /** Writes the "''class fixture''", i.e. the `class` identifier followed by the constructor and + * optionally a parent class. The parent constructor will be called correctly. + */ + protected def generateClassFixture: String = { + var params: List[String] = modelClass.attributes.map(attr => s"var ${attr.name}: ${attr.getType}").toList + val parentConstructorParams: String = + if (modelClass.isRootClass) + "" + else + modelClass.parent.getConstructorParameters.map(param => s"${param.name}: ${param.getType}").mkString(", ") + + val parentConstructor: String = if (modelClass.isRootClass) "" else s"(${modelClass.parent.getConstructorParameters.map(_.name).toList.mkString(", ")})" + + if (parentConstructorParams != "") + params ::= parentConstructorParams + + val constructor = s"(${params.mkString(", ")})" + val baseFixture = s"${modelClass.getName}$constructor" + + if (modelClass.isRootClass) baseFixture else s"$baseFixture extends ${modelClass.parent.getName}$parentConstructor" + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/io/SClassWriterTest.scala b/src/main/scala/org/rosi_project/model_sync/generator/io/SClassWriterTest.scala new file mode 100644 index 0000000000000000000000000000000000000000..528232225f239873446d478ce402be9a6a76a604 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/io/SClassWriterTest.scala @@ -0,0 +1,26 @@ +package org.rosi_project.model_sync.generator.io + +import org.rosi_project.model_sync.generator.acr_model._ +import org.rosi_project.model_sync.generator.sync.{GetterSetterGeneratingVisitor, SyncEnhancingVisitor} + +/** + * @author Rico Bergmann + */ +object SClassWriterTest/* extends App */{ + + val stringType = SType("String") + val attrs = Seq(SAttribute("name", stringType)) + val sayHelloMethod = new SMethod("sayHello", stringType, Seq.empty, Seq(SMethodStatement("s\"Hello $name\""))) + val modelClass = new SClass("Person", attributes = attrs, methods = Seq(sayHelloMethod)) + + val getterSetterVisitor = new GetterSetterGeneratingVisitor + modelClass.accept(getterSetterVisitor) + + val syncNotificationVisitor = new SyncEnhancingVisitor(null) + modelClass.accept(syncNotificationVisitor) + + val writer = new SClassWriter(modelClass) + + println(writer.stringify) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/io/SModelFSWriter.scala b/src/main/scala/org/rosi_project/model_sync/generator/io/SModelFSWriter.scala new file mode 100644 index 0000000000000000000000000000000000000000..5a42da3e582afb901a5f5bcca1955b888d58f829 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/io/SModelFSWriter.scala @@ -0,0 +1,143 @@ +package org.rosi_project.model_sync.generator.io + +import java.nio.file.{Files, StandardCopyOption} +import java.{io => jio} + +import org.rosi_project.model_sync.generator.ModelConfig +import org.rosi_project.model_sync.generator.acr_model._ +import org.rosi_project.model_sync.generator.env.{FilesCompiler, JarPackager} + +import scala.reflect.io.{Directory, File, Path} + +/** The `FSWriter` writes a [[SModel]] as a single compiled ''JAR'' file to the File System. + * + * @param workingDir directory to write the `.scala` files to. Will default to a temporary dir. + * @param outputDir directory to write the generated `model.jar` to + * @param keepClassFiles whether the `.scala` files should be added to the ''JAR'' as well + * + * @author Rico Bergmann + */ +class SModelFSWriter( + workingDir: File = File(Files.createTempDirectory("model").toFile), + outputDir: Directory = File("output").toDirectory, + keepClassFiles: Boolean = false, + modelCfg: ModelConfig, + currentDir: jio.File = new jio.File("").getAbsoluteFile) extends SModelVisitor { + + workingDir.jfile.mkdirs() + outputDir.jfile.mkdirs() + + private var sFilesToCompile: List[File] = List.empty + + println(s"Temp dir (sources) is $workingDir") + println(s"Output dir is $outputDir") + + println("... Copying model images") + private var images: List[jio.File] = _ + private var imagesTargetDir: jio.File = _ + try { + images = collectAllModelImages(modelCfg, currentDir) + imagesTargetDir = new jio.File(workingDir.toAbsolute.toString() + File.separator + "res") + imagesTargetDir.mkdirs() + images.foreach(img => { + Files.copy(img.toPath, imagesTargetDir.toPath.resolve(img.getName), StandardCopyOption.REPLACE_EXISTING) + }) + } catch { + case e: Exception => throw new ModelImagePreparationException(e) + } + + + override def visit(sModel: SModel): Unit = { + println(s"... Wrote files (sources) $sFilesToCompile") + + println("... Starting compilation") + new FilesCompiler(workingDir).run(sFilesToCompile) + println("... Compilation done") + + if (!keepClassFiles) { + println("... Cleaning up") + sFilesToCompile.foreach { + _.delete + } + } else { + println("... No clean-up requested") + } + + println("... Generating JAR") + new JarPackager(workingDir, outputDir.toFile).run() + println("... Done") + } + + override def visit(sClass: SClass): Unit = { + try { + println(s"Writing class $sClass") + val classNameWithPath = workingDir.toAbsolute.toString() + File.separator + pckg2Path(sClass.getPackage) + File.separator + s"${sClass.name}.scala" + val writer = new SClassWriter(sClass) + + val classFile = File(classNameWithPath) + + classFile.jfile.getParentFile.mkdirs() + classFile.writeAll(writer.stringify) + sFilesToCompile ::= classFile + } catch { + case e: Exception => throw new ClassWritingException(e) + } + } + + override def visit(sAttr: SAttribute): Unit = { + // pass + } + + override def visit(sMethod: SMethod): Unit = { + // pass + } + + override def visit(sType: SType): Unit = { + // pass + } + + /** Maps a Scala package to an relative path on the File System. + * + * @return the OS-specific path + */ + private def pckg2Path(pckg: String): Path = Path(pckg.replace(".", File.separator)) + + private def collectAllModelImages(modelConfig: ModelConfig, workingPath: jio.File): List[jio.File] = { + var images: List[jio.File] = List.empty + + modelConfig.init.image.foreach(img => { + images = images :+ new jio.File(workingPath.getPath + File.separator + img) + }) + + modelConfig.init.nested.foreach(nested => { + nested.foreach(model => { + model.image.foreach(img => { + images = images :+ new jio.File(workingPath.getPath + File.separator + img) + }) + }) + }) + + modelConfig.integration.foreach(integration => { + integration.foreach(model => { + model.image.foreach(img => { + images = images :+ new jio.File(workingPath.getPath + File.separator + img) + }) + }) + }) + + images + } + +} + +/** Exception to indicate that the model images may not be copied into the JAR. + * + * @param cause the causing exception + */ +class ModelImagePreparationException(cause: Exception) extends RuntimeException(cause) + +/** Exception to indicate that a class may not be written. + * + * @param cause the causing exception + */ +class ClassWritingException(cause: Exception) extends RuntimeException(cause) diff --git a/src/main/scala/org/rosi_project/model_sync/generator/io/SModelFSWriterTest.scala b/src/main/scala/org/rosi_project/model_sync/generator/io/SModelFSWriterTest.scala new file mode 100644 index 0000000000000000000000000000000000000000..266077240831ebcfb08efbc09ea975e656a2c3b4 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/io/SModelFSWriterTest.scala @@ -0,0 +1,28 @@ +package org.rosi_project.model_sync.generator.io + +import org.rosi_project.model_sync.generator.acr_model.{SimpleSModel, _} +import org.rosi_project.model_sync.generator.sync.{GetterSetterGeneratingVisitor, SyncEnhancingVisitor} + +import scala.reflect.io.File + +/** @author Rico Bergmann + */ +object SModelFSWriterTest/* extends App */{ + + val model = new SimpleSModel + + val stringType = SType("String") + + val personAttrs = Seq(SAttribute("name", stringType)) + val personSayHelloMethod = new SMethod("sayHello", stringType, Seq.empty, Seq(SMethodStatement("s\"Hello $name\""))) + val personClass = new SClass("Person", sPackage = "foo", attributes = personAttrs, methods = Seq(personSayHelloMethod)) + + val getterSetterVisitor = new GetterSetterGeneratingVisitor + val syncNotificationVisitor = new SyncEnhancingVisitor(null) + + model.addModelClass(personClass) + + model.accept(getterSetterVisitor) + model.accept(syncNotificationVisitor) + //model.accept(new SModelFSWriter(workingDir = File("output/raw"), keepClassFiles = true)) +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/io/package.scala b/src/main/scala/org/rosi_project/model_sync/generator/io/package.scala new file mode 100644 index 0000000000000000000000000000000000000000..cbc3d55c39e06f54594441cf2281c5b997283795 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/io/package.scala @@ -0,0 +1,11 @@ +package org.rosi_project.model_sync.generator + +import org.rosi_project.model_sync.generator.acr_model.SModel + +/** Contains services to write [[SModel]] instances to the File system. + * + * @author Rico Bergmann + */ +package object io { + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/support/Assert.scala b/src/main/scala/org/rosi_project/model_sync/generator/support/Assert.scala new file mode 100644 index 0000000000000000000000000000000000000000..ffc599f8bbcb39a78eb42b1e353d01f19029010a --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/support/Assert.scala @@ -0,0 +1,35 @@ +package org.rosi_project.model_sync.generator.support + +/** Utilities to enforce some domain constraints. If the constraint fails, most methods will throw + * an `AssertionError` (unless stated otherwise). + * + * @author Rico Bergmann + */ +object Assert { + + /** Ensures that an object is not `null`. Will throw an `AssertionError` if it is. + * + * @param obj the object to check + * @param msg the message to display in case the constraint fails + */ + def notNull(obj: AnyRef, msg: String): Unit = isTrue(obj != null, msg) + + /** Ensures that some condition holds (i.e. evaluates to `true`). Will throw an `AssertionError` + * if it `false`. + * + * @param cond the condition to evaluate + * @param msg the message to display in case the constraint fails + */ + def isTrue(cond: Boolean, msg: String): Unit = if (!cond) { + throw new AssertionError(msg) + } + + /** Ensures that some condition does '''not''' hold (i.e. evaluates to `false`). Will throw an + * `AssertionError` if it is `true`. + * + * @param cond the condition to evaluate + * @param msg the message to display in case the constraint fails + */ + def isFalse(cond: Boolean, msg: String): Unit = isTrue(!cond, msg) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/support/ExtendedString.scala b/src/main/scala/org/rosi_project/model_sync/generator/support/ExtendedString.scala new file mode 100644 index 0000000000000000000000000000000000000000..3a277849ff038f05615b79da9100ce785a08c900 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/support/ExtendedString.scala @@ -0,0 +1,25 @@ +package org.rosi_project.model_sync.generator.support + +/** Adds more methods to a `String`. + * + * @author Rico Bergmann + */ +class ExtendedString(wrappedString: String) { + + /** Turns the first character of a `String` into its upper-case equivalent. + * + * If no such equivalent exists, nothing will happen. If the `String` is empty neither. + */ + def firstLetterToUpperCase: String = + wrappedString.headOption.map(h => h.toUpper + wrappedString.tail).getOrElse("") + +} + +/** The companion provides the implicit conversion. + */ +object ExtendedString { + + implicit def stringToExtended(str: String): ExtendedString = new ExtendedString(str) + +} + diff --git a/src/main/scala/org/rosi_project/model_sync/generator/support/package.scala b/src/main/scala/org/rosi_project/model_sync/generator/support/package.scala new file mode 100644 index 0000000000000000000000000000000000000000..3be377dd9a52ca67ded41a8506b3d6d2b549e622 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/support/package.scala @@ -0,0 +1,9 @@ +package org.rosi_project.model_sync.generator + +/** Contains various helper classes that are independent of the actual domain business logic. + * + * @author Rico Bergmann + */ +package object support { + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/sync/ConstructorTemplate.scala b/src/main/scala/org/rosi_project/model_sync/generator/sync/ConstructorTemplate.scala new file mode 100644 index 0000000000000000000000000000000000000000..c1dfbbec7dee61f50deceb0b3ad7a26c2c215ce8 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/sync/ConstructorTemplate.scala @@ -0,0 +1,51 @@ +package org.rosi_project.model_sync.generator.sync + +import org.rosi_project.model_sync.instances.ModelInstanceConstructor +import org.rosi_project.model_sync.generator.acr_model.{SClass, SImport, SMethod, SMethodParameter, SMethodStatement, SType, types => acr} + +/** Creates implementations of [[ModelInstanceConstructor]] for model classes. + * + * @param modelClass the class for which the constructor should be wrapped + * @author Rico Bergmann + */ +class ConstructorTemplate(modelClass: SClass) extends SClass(name = s"${modelClass.name}Constructor") { + + parent = PredefTypes.ModelInstanceConstructor + + private val constructorParams = modelClass.attributes.map(attr => s"classOf[${attr.attrType.getName}]").mkString(", ") + + constructorStatements = constructorStatements :+ SMethodStatement(s"private val ${ConstructorTemplate.ConstructorAttr} = classOf[${modelClass.getName}].getConstructor($constructorParams)") + + methods = Seq( + new SMethod( + name = "getParameters", + result = acr.SSeq(acr.PredefTypes.Parameter), + params = Seq.empty, + implementation = s"${ConstructorTemplate.ConstructorAttr}.getParameters", + overrides = true + ), + new SMethod( + name = "invoke", + result = SType.AnyRef, + params = Seq(SMethodParameter("args", acr.SSeq(acr.PredefTypes.Object))), + implementation = s"${ConstructorTemplate.ConstructorAttr}.newInstance(args: _*)", + overrides = true + ), + new SMethod( + name = "toString", + result = SType.String, + params = Seq.empty, + implementation = s""" "${modelClass.getName} Constructor" """, + overrides = true + ) + ) + + override def getNecessaryImports: Set[SImport] = modelClass.getUsedTypes.filter(_.getPackage != "").map(elem => SImport(elem.getPackage, elem.getName)) ++ super.getNecessaryImports + SImport(modelClass.getPackage, modelClass.getName) + +} + +object ConstructorTemplate { + + val ConstructorAttr = "constructor" + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/sync/GetterSetterGeneratingVisitor.scala b/src/main/scala/org/rosi_project/model_sync/generator/sync/GetterSetterGeneratingVisitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..dd6fd38c5c7cc1da67789de45328b9a876637bfb --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/sync/GetterSetterGeneratingVisitor.scala @@ -0,0 +1,36 @@ +package org.rosi_project.model_sync.generator.sync +import org.rosi_project.model_sync.generator.acr_model._ + +/** Service to extend [[SClass SClasses]] with getter and setter methods for all attributes. + * + * @author Rico Bergmann + */ +class GetterSetterGeneratingVisitor extends SModelVisitor { + + override def visit(sModel: SModel): Unit = { + // pass + } + + override def visit(sClass: SClass): Unit = { + sClass.attributes.foreach(attr => { + val getter = new SGetter(attr) + val setter = new SSetter(attr) + sClass.addMethod(getter) + sClass.addMethod(setter) + }) + + } + + override def visit(sAttr: SAttribute): Unit = { + // pass + } + + override def visit(sMethod: SMethod): Unit = { + // pass + } + + override def visit(sType: SType): Unit = { + // pass + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/sync/Imports.scala b/src/main/scala/org/rosi_project/model_sync/generator/sync/Imports.scala new file mode 100644 index 0000000000000000000000000000000000000000..bf4ba3cf8cda50fdef4ec5f8b52bc201d1ae2d78 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/sync/Imports.scala @@ -0,0 +1,15 @@ +package org.rosi_project.model_sync.generator.sync + +import org.rosi_project.model_sync.generator.acr_model.SImport + +/** Contains a number of imports which are used throughout the sync infrastructure. + * + * @author Rico Bergmann + */ +object Imports { + + /** Import for the `ModelRegistry` type. + */ + val ModelRegistry = SImport(PredefTypes.ModelRegistry.sPackage, PredefTypes.ModelRegistry.name) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/sync/InitialModelTemplate.scala b/src/main/scala/org/rosi_project/model_sync/generator/sync/InitialModelTemplate.scala new file mode 100644 index 0000000000000000000000000000000000000000..018824aa198f879de82decbd01bf37b5fbf06ac6 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/sync/InitialModelTemplate.scala @@ -0,0 +1,97 @@ +package org.rosi_project.model_sync.generator.sync + +import java.io.File + +import org.rosi_project.model_sync.generator.{Model, ModelDataException} +import org.rosi_project.model_sync.generator.acr_model._ +import org.rosi_project.model_sync.generator.acr_model.{types => acr} +import org.rosi_project.model_sync.generator.support.ExtendedString.stringToExtended +import org.rosi_project.model_sync.provider.DisplayableModelForInitialization + + +/** Creates implementations of [[DisplayableModelForInitialization]] for models. + * + * @param model the model which should be initialized + * @author Rico Bergmann + */ +class InitialModelTemplate(model: Model) extends SClass(name = s"Initialized${Model.parseClass(model.primaryClass)._2.firstLetterToUpperCase}") { + + parent = SType("DisplayableModelForInitialization", sPackage = "org.rosi_project.model_sync.provider") + + private val primaryClassComps = Model.parseClass(model.primaryClass) + private val primaryClass: SClass = STypeRegistry.query(primaryClassComps._2, primaryClassComps._1).getOrElse(throw new ModelDataException).asInstanceOf[SClass] + private val additionalClasses: Seq[SClass] = model.additionalClasses.getOrElse(List()).map(Model.parseClass).map(cls => STypeRegistry.query(cls._2, cls._1).getOrElse(throw new ModelDataException).asInstanceOf[SClass]) + private val allModelClasses = primaryClass +: additionalClasses + + private var modelImagePath = "None" + model.image.foreach(image => { + val imageFile = new File(image) + modelImagePath = s"""Some(new File("${RES_PATH + imageFile.getName}"))""" + }) + + + methods = Seq( + new SMethod ( + name = "getName", + result = SType.String, + params = Seq.empty, + implementation = s""" "${model.name}" """ + ), + new SMethod ( + name = "getInstanceClass", + result = SType("Class[_ <: AnyRef]"), + params = Seq.empty, + implementation = s"classOf[${primaryClass.getName}]" + ), + new SMethod ( + name = "getModelDiagram", + result = acr.PredefTypes.option(acr.PredefTypes.File), + params = Seq.empty, + implementation = modelImagePath + ), + + new SMethod ( + name = "getConstructors", + result = acr.SSeq(PredefTypes.ModelInstanceConstructor), + params = Seq.empty, + implementation = s"Seq(${allModelClasses.map(cls => s"new ${cls.getName}Constructor").mkString(", ")})" + ), + new SMethod ( + name = "getUpdateMethods", + result = acr.SSeq(PredefTypes.ModelInstanceModifier), + params = Seq.empty, + implementation = s"Seq(${allModelClasses.map(cls => cls.attributes.map(attr => s"new ${cls.name}${attr.name.firstLetterToUpperCase}Modifier").mkString(", ")).mkString(", ")})" + ), + new SMethod ( + name = "getInstances", + result = acr.SSeq(SType.AnyRef), + params = Seq.empty, + implementation = "Seq.empty" // TODO + ) + ) + + if (model.nested.nonEmpty) { + + methods = methods :+ new SMethod( + name = "getNestedModels", + result = acr.SSeq(PredefTypes.DisplayableModel), + params = Seq.empty, + implementation = s"Seq( ${model.nested.getOrElse(List()).map(nested => "new Initialized" + Model.parseClass(nested.primaryClass)._2.firstLetterToUpperCase).mkString(", ")} )", + overrides = true + ) + + } + + constructorStatements = Statements.ModelRegistration +: constructorStatements + + override def getNecessaryImports: Set[SImport] = { + val base = super.getNecessaryImports + Imports.ModelRegistry + + if (primaryClass.isDefaultPackage) { + base + } else { + base + SImport(primaryClass.getPackage, primaryClass.getName) + } + } + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/sync/ModelProviderTemplate.scala b/src/main/scala/org/rosi_project/model_sync/generator/sync/ModelProviderTemplate.scala new file mode 100644 index 0000000000000000000000000000000000000000..9b912f66a65b930ceeb400206f4c89d5f195f283 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/sync/ModelProviderTemplate.scala @@ -0,0 +1,32 @@ +package org.rosi_project.model_sync.generator.sync + +import org.rosi_project.model_sync.generator.Model +import org.rosi_project.model_sync.generator.acr_model.{SClass, SMethod, types => acr} +import org.rosi_project.model_sync.generator.support.ExtendedString.stringToExtended +import org.rosi_project.model_sync.provider.ModelSyncProvider + +/** Creates implementations of [[ModelSyncProvider]] for models. + * + * @param model the model which should be initialized + * @author Rico Bergmann + */ +class ModelProviderTemplate(model: Model) extends SClass(name = "ModelProvider") { + + parent = PredefTypes.ModelProvider + + methods = Seq( + new SMethod ( + name = "getInitialDisplayableModel", + result = PredefTypes.DisplayableModelForInitialization, + params = Seq.empty, + implementation = s"new Initialized${Model.parseClass(model.primaryClass)._2.firstLetterToUpperCase}" + ), + new SMethod ( + name = "getDisplayableModelsForIntegration", + result = acr.SSeq(PredefTypes.DisplayableModelForIntegration), + params = Seq.empty, + implementation = "Seq.empty" + ) + ) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/sync/ModifierTemplate.scala b/src/main/scala/org/rosi_project/model_sync/generator/sync/ModifierTemplate.scala new file mode 100644 index 0000000000000000000000000000000000000000..065a04ca7a11ca0813534c284d4384eb951db4d0 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/sync/ModifierTemplate.scala @@ -0,0 +1,58 @@ +package org.rosi_project.model_sync.generator.sync + +import org.rosi_project.model_sync.generator.acr_model.{GenericSType, SAttribute, SClass, SImport, SMethod, SMethodParameter, SMethodStatement, SType, STypedElement, types => acr} +import org.rosi_project.model_sync.generator.support.ExtendedString.stringToExtended +import org.rosi_project.model_sync.instances.ModelInstanceModifier + +/** Creates implementations of [[ModelInstanceModifier]] for attributes of model classes. + * + * @param modelClass the class which contains the attribute + * @param attribute the attribute for which the modifier should be created + * @author Rico Bergmann + */ +class ModifierTemplate(modelClass: SClass, attribute: SAttribute) extends SClass(name = s"${modelClass.name}${attribute.name.firstLetterToUpperCase}Modifier") { + + parent = PredefTypes.ModelInstanceModifier + + constructorStatements = constructorStatements :+ + SMethodStatement(s"""private val ${ModifierTemplate.MethodAttr} = classOf[${modelClass.getName}].getMethod("set${attribute.name.firstLetterToUpperCase}", classOf[${attribute.getType}])""") + + methods = Seq( + new SMethod ( + name = "getParameters", + result = acr.SSeq(acr.PredefTypes.Parameter), + params = Seq.empty, + implementation = s"${ModifierTemplate.MethodAttr}.getParameters", + overrides = true + ), + new SMethod ( + name = "invoke", + result = SType.Unit, + params = Seq(SMethodParameter("instance", SType.AnyRef), SMethodParameter("args", acr.SSeq(SType.AnyRef))), + implementation = s"${ModifierTemplate.MethodAttr}.invoke(instance, args: _*)", + overrides = true + ), + new SMethod( + name = "toString", + result = SType.String, + params = Seq.empty, + implementation = s""" "Change ${attribute.name}" """, + overrides = true + ) + ) + + override def getNecessaryImports: Set[SImport] = { + val attrImports = attribute.attrType match { + case gt : GenericSType => SImport.generateImports(gt, gt.typeParam) + case t : STypedElement => SImport.generateImports(t) + } + modelClass.getNecessaryImports ++ super.getNecessaryImports + SImport(modelClass.getPackage, modelClass.getName) ++ attrImports + } + +} + +object ModifierTemplate { + + val MethodAttr = "method" + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/sync/PredefTypes.scala b/src/main/scala/org/rosi_project/model_sync/generator/sync/PredefTypes.scala new file mode 100644 index 0000000000000000000000000000000000000000..0491c444f2512bada9edabbedf5f803c9ab01ca2 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/sync/PredefTypes.scala @@ -0,0 +1,42 @@ +package org.rosi_project.model_sync.generator.sync + +import org.rosi_project.model_sync.generator.acr_model.SType + +/** Contains a number of types which are used throughout the synchronization. + * + * Equivalent to [[org.rosi_project.model_sync.generator.acr_model.types.PredefTypes]], just for + * the sync types. + * + * @author Rico Bergmann + */ +object PredefTypes { + + /** `org.rosi_project.model_sync.provider.DisplayableModel` + */ + val DisplayableModel = SType("DisplayableModel", "org.rosi_project.model_sync.provider") + + /** `org.rosi_project.model_sync.provider.DisplayableModelForInitialization` + */ + val DisplayableModelForInitialization = SType("DisplayableModelForInitialization", "org.rosi_project.model_sync.provider") + + /** `org.rosi_project.model_sync.provider.DisplayableModelForIntegration` + */ + val DisplayableModelForIntegration = SType("DisplayableModelForIntegration", "org.rosi_project.model_sync.provider") + + /** `org.rosi_project.model_sync.provider.ModelSyncProvider` + */ + val ModelProvider = SType("ModelSyncProvider", "org.rosi_project.model_sync.provider") + + /** `org.rosi_project.model_sync.provider.ModelRegistry` + */ + val ModelRegistry = SType("ModelRegistry", "org.rosi_project.model_sync.provider") + + /** `org.rosi_project.model_sync.provider.instances.ModelInstanceConstructor` + */ + val ModelInstanceConstructor = SType("ModelInstanceConstructor", "org.rosi_project.model_sync.instances") + + /** `org.rosi_project.model_sync.provider.instances.ModelInstanceModifier` + */ + val ModelInstanceModifier = SType("ModelInstanceModifier", "org.rosi_project.model_sync.instances") + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/sync/Statements.scala b/src/main/scala/org/rosi_project/model_sync/generator/sync/Statements.scala new file mode 100644 index 0000000000000000000000000000000000000000..59fa88d7d81fe94ed4dcccfbc1ae5a9b1eedf680 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/sync/Statements.scala @@ -0,0 +1,15 @@ +package org.rosi_project.model_sync.generator.sync + +import org.rosi_project.model_sync.generator.acr_model.SMethodStatement + +/** Contains method statements which are used for throughout the synchronization. + * + * @see [[SMethodStatement]] + * + * @author Rico Bergmann + */ +object Statements { + + val ModelRegistration = SMethodStatement("ModelRegistry.registerNewModel(this)", Set(PredefTypes.ModelRegistry)) + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/sync/SyncEnhancingVisitor.scala b/src/main/scala/org/rosi_project/model_sync/generator/sync/SyncEnhancingVisitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..23e433740969af76c2c8dff3b0ff702283d61443 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/sync/SyncEnhancingVisitor.scala @@ -0,0 +1,94 @@ +package org.rosi_project.model_sync.generator.sync + +import org.rosi_project.model_sync.generator.ModelConfig +import org.rosi_project.model_sync.generator.acr_model._ +import org.rosi_project.model_sync.sync.PlayerSync +import org.rosi_project.model_sync.sync.ISynchronizationCompartment + +/** Augments [[SClass SClasses]] with the necessary method calls to make it usable in a + * synchronization context. + * + * The following modifications are performed: + * - the class (or its furthest parent) becomes a subclass of [[PlayerSync]] + * - [[PlayerSync.buildClass()]] will be called in the constructor + * - each setter will notify the synchronization context about the change + * + * @see [[ISynchronizationCompartment]] + * @author Rico Bergmann + */ +class SyncEnhancingVisitor(val modelCfg: ModelConfig) extends SModelVisitor { + + private var additionalSyncClasses: Seq[SClass] = Seq.empty + + override def visit(sModel: SModel): Unit = { + additionalSyncClasses.foreach(sModel.addModelClass) + + modelCfg.init.nested.foreach(_.foreach(model => sModel.addModelClass(new InitialModelTemplate(model)))) + sModel.addModelClass(new InitialModelTemplate(modelCfg.init)) + sModel.addModelClass(new ModelProviderTemplate(modelCfg.init)) + + } + + override def visit(sClass: SClass): Unit = { + sClass.getInheritanceHierarchy.reverse.headOption.foreach{ + case root: SClass => root.setParent(SyncEnhancingVisitor.PLAYER_SYNC_STYPE) + case tp => sys.error(s"May not enhance $tp as it is a type and not a class") + } + + sClass.augmentConstructor(SyncEnhancingVisitor.PLAYER_SYNC_INIT) + + additionalSyncClasses = additionalSyncClasses :+ new ConstructorTemplate(sClass) + sClass.attributes.foreach(attr => { + additionalSyncClasses = additionalSyncClasses :+ new ModifierTemplate(sClass, attr) + }) + } + + override def visit(sAttr: SAttribute): Unit = { + // pass + } + + override def visit(sMethod: SMethod): Unit = { + extractSetterAttr(sMethod).foreach(attr => sMethod.augmentImplementation(SMethodStatement(s"+this change$attr ()"))) + } + + override def visit(sType: SType): Unit = { + // pass + } + + /** Tries to get the attribute's name from a setter method. + * + * A ''valid'' setter will have the following signature: `setXyz(x: T): Unit` (the parameter's + * name does not matter). + * + * Mind that the first letter will be left uppercase (i.e. `Xyz` will be returned although the + * actual attribute may be `xyz`) + * + * @param sMethod the method to analyze. May be any method (not necessarily a setter) but never + * `null`. + * @return the attribute's name if `sMethod` was a valid setter. '''The first letter will be left + * uppercase.''' + */ + private def extractSetterAttr(sMethod: SMethod): Option[String] = { + sMethod.name match { + case SyncEnhancingVisitor.Setter(attrName) => + Option(attrName) + case _ => + None + } + + } + +} + +/** The companion contains some static values. + */ +object SyncEnhancingVisitor { + + private val Setter = """set([A-Z][a-zA-z0-9]*)""".r + + private val PLAYER_SYNC_CLASS = classOf[PlayerSync] + private val PLAYER_SYNC_STYPE = SType(PLAYER_SYNC_CLASS.getSimpleName, PLAYER_SYNC_CLASS.getPackage.getName) + + private val PLAYER_SYNC_INIT = SMethodStatement("buildClass()") + +} diff --git a/src/main/scala/org/rosi_project/model_sync/generator/sync/package.scala b/src/main/scala/org/rosi_project/model_sync/generator/sync/package.scala new file mode 100644 index 0000000000000000000000000000000000000000..4e0a694ebd741b85202f40dac9ad9ecf4b9298e4 --- /dev/null +++ b/src/main/scala/org/rosi_project/model_sync/generator/sync/package.scala @@ -0,0 +1,17 @@ +package org.rosi_project.model_sync.generator + +import org.rosi_project.model_sync.generator.acr_model.{SMethodStatement, SModel} + +/** Contains services to adapt a [[SModel]] for usage in a ''Synchronization context''. + * + * @author Rico Bergmann + */ +package object sync { + + val RES_PATH = "res/" + + implicit def t2SeqT[T](t: T): Seq[T] = Seq(t) + + implicit def string2SeqMethodImpl(impl: String): Seq[SMethodStatement] = Seq(SMethodStatement(impl)) + +}