diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5e142013c2bdbc64d4fe3558748328a282bbfc7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,183 @@ +#### 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/ +[lL]ocal[tT]est* + +# ANTLR + +gen/ diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..b76c173210ab971ec9e4f94c18cdffca08361536 --- /dev/null +++ b/build.sbt @@ -0,0 +1,38 @@ +import com.simplytyped.Antlr4Plugin.autoImport.antlr4PackageName +import sbt.Keys.{libraryDependencies, scalacOptions, version} + +val emfcommonVersion = "2.12.0" +val emfecoreVersion = "2.12.0" +val scoptVersion = "3.7.0" +val liftVersion = "3.3.0" + +javacOptions ++= Seq("-encoding", "UTF-8") + +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, + + "org.junit.platform" % "junit-platform-runner" % "1.0.0" % "test", + "org.junit.jupiter" % "junit-jupiter-engine" % "5.0.0" % "test", + "org.junit.vintage" % "junit-vintage-engine" % "4.12.0" % "test", + "org.assertj" % "assertj-core" % "3.12.2" % "test", + + "net.aichler" % "jupiter-interface" % JupiterKeys.jupiterVersion.value % Test + ), + scalacOptions ++= Seq( + "-language:implicitConversions" + ), + ).enablePlugins(Antlr4Plugin) + +antlr4PackageName in Antlr4 := Some("org.rosi_project.model_sync.model_join.representation.parser.antlr.generated") +antlr4GenVisitor in Antlr4 := true +antlr4GenListener in Antlr4 := false diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..cabf73b45107a5feb964e2c564c9687faafffdbe --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.7 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..4a65abadaf7e792155072db22cce49a4abbf4d54 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,6 @@ + +resolvers += Resolver.jcenterRepo + +addSbtPlugin("net.aichler" % "sbt-jupiter-interface" % "0.8.2") +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4") +addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") diff --git a/project/sbt-antlr4.sbt b/project/sbt-antlr4.sbt new file mode 100644 index 0000000000000000000000000000000000000000..7b26d1fc7b9ee12df151e57cb9e2528c1a4f8d9f --- /dev/null +++ b/project/sbt-antlr4.sbt @@ -0,0 +1,2 @@ + +addSbtPlugin("com.simplytyped" % "sbt-antlr4" % "0.8.2") diff --git a/src/main/antlr4/ModelJoin.g4 b/src/main/antlr4/ModelJoin.g4 new file mode 100644 index 0000000000000000000000000000000000000000..70817b5b7a0eed652b51f26aaf916e280a5bbc90 --- /dev/null +++ b/src/main/antlr4/ModelJoin.g4 @@ -0,0 +1,152 @@ +/* + * ModelJoin.g4 + * + * Defines the structure of our ModelJoin grammar + * + * Author: Rico Bergmann + */ +grammar ModelJoin; + +/* + * Parser + */ +modeljoin : join+ EOF ; +join : (naturaljoin | thetajoin | outerjoin) AS classres + ( OPENCURLYBRAKET + keepattributesexpr* + keepaggregatesexpr* + keepcalculatedexpr* + keepexpr* + CLOSEDCURLYBRAKET )? ; +naturaljoin : NATURAL JOIN classres WITH classres ; +thetajoin : THETA JOIN classres WITH classres WHERE oclcond; +outerjoin : (leftouterjoin | rightouterjoin) OUTER JOIN classres WITH classres; +leftouterjoin : LEFT ; +rightouterjoin : RIGHT ; + +keepattributesexpr : KEEP ATTRIBUTES attrres (COMMA attrres)* ; +keepaggregatesexpr : KEEP AGGREGATE aggrtype + OPENBRAKET relattr CLOSEDBRAKET + OVER classres AS attrres ; +keepcalculatedexpr : KEEP CALCULATED ATTRIBUTE oclcond AS (typedattrres | attrres) ; +keepexpr : (keeptypeexpr | keepoutgoingexpr | keepincomingexpr) + ( OPENCURLYBRAKET + keepattributesexpr* + keepaggregatesexpr* + keepcalculatedexpr* + keepexpr* + CLOSEDCURLYBRAKET)? ; +keeptypeexpr : keepsupertypeexpr | keepsubtypeexpr ; +keepsupertypeexpr : KEEP SUPERTYPE classres ( AS TYPE classres )? ; +keepsubtypeexpr : KEEP SUBTYPE classres ( AS TYPE classres )? ; +keepoutgoingexpr : KEEP OUTGOING attrres ( AS TYPE classres )? ; +keepincomingexpr : KEEP INCOMING attrres (AS TYPE classres )? ; + +classres : WORD (DOT WORD)* ; +attrres : WORD (DOT WORD)+ ; +typedattrres : attrres COLON WORD ; +relattr : WORD ; +oclcond : (WORD | NUMBER | specialchar | anytoken | WHITESPACE | NEWLINE)+; +aggrtype : SUM | AVG | MIN | MAX | SIZE ; + +specialchar : DOT + | OPENBRAKET + | CLOSEDBRAKET + | OPENCURLYBRAKET + | CLOSEDCURLYBRAKET + | COMMA + | UNDERSCORE + | SPECIALCHAR ; + +// anytoken matches all possible tokens in case their content may appear as part of some regular +// text as well +anytoken : OPENBRAKET + | CLOSEDBRAKET + | OPENCURLYBRAKET + | CLOSEDCURLYBRAKET + | DOT + | COLON + | COMMA + | UNDERSCORE + | SPECIALCHAR + + | JOIN + | NATURAL + | THETA + | WHERE + | OUTER + | RIGHT + | LEFT + | WITH + | AS + + | KEEP + | ATTRIBUTES + | ATTRIBUTE + | AGGREGATE + | CALCULATED + | SUPERTYPE + | SUBTYPE + | OUTGOING + | INCOMING + | TYPE + | OVER + + | SUM + | AVG + | MIN + | MAX + | SIZE ; + +/* + * Lexer + */ + +fragment LOWERCASE : [a-z] ; +fragment UPPERCASE : [A-Z] ; +fragment ANYCASE : LOWERCASE | UPPERCASE ; +fragment DIGIT : [0-9] ; + +OPENBRAKET : '(' ; +CLOSEDBRAKET : ')' ; +OPENCURLYBRAKET : '{' ; +CLOSEDCURLYBRAKET : '}' ; +DOT : '.' ; +COLON : ':' ; +COMMA : ',' ; +UNDERSCORE : '_' ; +SPECIALCHAR : [-><!="'|] ; + +JOIN : 'join' ; +NATURAL : 'natural' ; +THETA : 'theta' ; +WHERE : 'where' ; +OUTER : 'outer' ; +RIGHT : 'right' ; +LEFT : 'left' ; +WITH : 'with' ; +AS : 'as' ; + +KEEP : 'keep' ; +ATTRIBUTES : 'attributes' ; +ATTRIBUTE : 'attribute' ; +AGGREGATE : 'aggregate' ; +CALCULATED : 'calculated' ; +SUPERTYPE : 'supertype' ; +SUBTYPE : 'subtype' ; +OUTGOING : 'outgoing' ; +INCOMING : 'incoming' ; +TYPE : 'type' ; +OVER : 'over' ; + +SUM : 'sum' ; +AVG : 'avg' ; +MIN : 'min' ; +MAX : 'max' ; +SIZE : 'size' ; + +WORD : ANYCASE (ANYCASE | DIGIT | UNDERSCORE)* ; +NUMBER : [+-]? DIGIT+ DOT? DIGIT* ; + +WHITESPACE : ' ' -> skip ; +NEWLINE : ('\r'? '\n' | '\r')+ -> skip ; diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/AttributePath.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/AttributePath.java new file mode 100644 index 0000000000000000000000000000000000000000..2f28ca24b1a9e644ec9799541d88ef1d65c8e972 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/AttributePath.java @@ -0,0 +1,147 @@ +package org.rosi_project.model_sync.model_join.representation.core; + +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * An {@code AttributePath} represents a single attribute of some model class. + * <p> + * As <em>ModelJoin</em> does not care about types at a conceptual level, no type information is + * stored. + * + * @author Rico Bergmann + */ +public class AttributePath { + + private static final String CLASS_ATTRIBUTE_DELIMITER = "."; + + /** + * Generates a new {@code path} given the owning class as well as the attribute's name. + */ + @Nonnull + public static AttributePath from( + @Nonnull ClassResource containingClass, + @Nonnull String attributeName) { + return new AttributePath(containingClass, attributeName); + } + + /** + * Generates a new {@code path} given the owning class as well as the attribute. + */ + @Nonnull + public static AttributePath from( + @Nonnull ClassResource containingClass, + @Nonnull RelativeAttribute attribute) { + return new AttributePath(containingClass, attribute); + } + + public static AttributePath from(String containingClass, String attributeName) { + return new AttributePath(containingClass, attributeName); + } + + /** + * Generates a new {@code AttributePath} by splitting up the {@code qualifiedPath} into the + * {@link ClassResource} and attribute portion. Therefore the {@code qualifiedPath} is expected + * to adhere to the following scheme: {@code [package].class.attribute}. + */ + @Nonnull + public static AttributePath fromQualifiedPath(@Nonnull String qualifiedPath) { + final int splitPos = qualifiedPath.lastIndexOf(CLASS_ATTRIBUTE_DELIMITER); + + if (splitPos < 0) { + throw new IllegalArgumentException("Missing class portion on '" + qualifiedPath + "'"); + } + + String classResourcePortion = qualifiedPath.substring(0, splitPos); + String attributePortion = qualifiedPath.substring(splitPos + 1); + return new AttributePath(ClassResource.fromQualifiedName(classResourcePortion), attributePortion); + } + + @Nonnull + private final ClassResource containingClass; + + @Nonnull + private final RelativeAttribute attribute; + + public AttributePath(String containingClass, String attributeName) { + this.containingClass = ClassResource.fromQualifiedName(containingClass); + this.attribute = RelativeAttribute.of(attributeName); + } + + /** + * Full constructor, parsing the attribute's name into a valid {@link RelativeAttribute}. + * + * @param containingClass the class which owns the attribute + * @param attributeName the attribute's name + */ + public AttributePath(@Nonnull ClassResource containingClass, @Nonnull String attributeName) { + this.containingClass = containingClass; + this.attribute = RelativeAttribute.of(attributeName); + } + + /** + * Full constructor. + * + * @param containingClass the class which owns the attribute + * @param attribute the attribute's name + */ + public AttributePath(@Nonnull ClassResource containingClass, @Nonnull RelativeAttribute attribute) { + this.containingClass = containingClass; + this.attribute = attribute; + } + + /** + * Creates a new {@code AttributePath} by copying another path. + */ + protected AttributePath(@Nonnull AttributePath other) { + this.containingClass = other.getContainingClass(); + this.attribute = other.getAttribute(); + } + + /** + * Provides the class which owns {@code this} attribute. + */ + @Nonnull + public ClassResource getContainingClass() { + return containingClass; + } + + /** + * Provides {@code this} attribute as a {@link RelativeAttribute}. + */ + @Nonnull + public RelativeAttribute getAttribute() { + return attribute; + } + + /** + * Provides the name of {@code this} attribute. + */ + @Nonnull + public String getAttributeName() { + return attribute.getAttributeName(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AttributePath)) { + return false; + } + AttributePath that = (AttributePath) o; + return containingClass.equals(that.containingClass) && + attribute.equals(that.attribute); + } + + @Override + public int hashCode() { + return Objects.hash(containingClass, attribute); + } + + @Override + public String toString() { + return containingClass.toString() + "." + attribute; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ClassResource.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ClassResource.java new file mode 100644 index 0000000000000000000000000000000000000000..7143ad71f59935a361fbb255c576ad208ae7a1d3 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ClassResource.java @@ -0,0 +1,86 @@ +package org.rosi_project.model_sync.model_join.representation.core; + +import javax.annotation.Nonnull; + +/** + * A {@code ClassResource} represents a fully-qualified reference to some class to be used in a + * join. This class merely exist to emphasize that some resource actually represents a class and + * thus provides a nicer (i.e. better named) interface to query for the resource's fields. + * <p> + * In difference to a {@link ResourcePath}, instances of this class will drop the {@code .java} + * ending of the file name if referenced from the file system. + * + * @author Rico Bergmann + */ +public class ClassResource extends ResourcePath { + + /** + * Generates a new {@code path} given the path to its containing <em>package</em> and the name of + * the class which should be referenced. + */ + @Nonnull + public static ClassResource from(@Nonnull String packagePath, @Nonnull String className) { + return new ClassResource(packagePath, className); + } + + /** + * @see ResourcePath#parse(String) + */ + @Nonnull + public static ClassResource fromQualifiedName(@Nonnull String qualifiedClass) { + /* + * A class resource is just a better representation of a class (in difference to just _some_ + * kind of resource) but works just the same - thus we will re-use all the parsing logic from + * the ResourcePath + */ + ResourcePath parsedResource = ResourcePath.parse(qualifiedClass); + return new ClassResource(parsedResource.resourcePath, parsedResource.resourceName); + } + + /** + * @see ResourcePath#parseAsFileSystemPath(String) + */ + @Nonnull + public static ClassResource fromFileSystem(@Nonnull String fullPath) { + /* + * A class resource is just a better representation of a class (in difference to just _some_ + * kind of resource) but works just the same - thus we will re-use all the parsing logic from + * the ResourcePath + */ + ResourcePath parsedResource = ResourcePath.parseAsFileSystemPath(dropFileTypeEnding(fullPath)); + return new ClassResource(parsedResource.resourcePath, parsedResource.resourceName); + } + + /** + * Deletes the {@code .java} ending from a resource name. + */ + private static String dropFileTypeEnding(String fullPath) { + return fullPath.replaceAll("\\.java$", ""); + } + + /** + * Full constructor. + * + * @param containingPackage the path to the package to which the class belongs + * @param className the name of the class + */ + public ClassResource(@Nonnull String containingPackage, @Nonnull String className) { + super(containingPackage, className); + } + + /** + * Provides the name of the class represented by {@code this} resource. + */ + @Nonnull + public String getClassName() { + return resourceName; + } + + /** + * Provides the package that contains the class represented by {@code this} resource. + */ + @Nonnull + public String getPackage() { + return resourcePath; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLConstraint.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLConstraint.java new file mode 100644 index 0000000000000000000000000000000000000000..6f8afef2b4ba2623225ac2f1aed8ed72a6d838f5 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLConstraint.java @@ -0,0 +1,32 @@ +package org.rosi_project.model_sync.model_join.representation.core; + +import javax.annotation.Nonnull; + +/** + * An {@code OCLConstraint} specifies some predicate on (possibly multiple) models that have to hold + * under certain conditions. + * <p> + * This class is a specialization of an arbitrary {@link OCLStatement} for statements, that + * evaluate to a boolean values. + * + * @author Rico Bergmann + */ +public class OCLConstraint extends OCLStatement { + + /** + * Generates a new constraint with the given content. + */ + @Nonnull + public static OCLConstraint of(@Nonnull String content) { + return new OCLConstraint(content); + } + + /** + * Full constructor. + * + * @param content the actual constraint + */ + public OCLConstraint(@Nonnull String content) { + super(content); + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLStatement.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLStatement.java new file mode 100644 index 0000000000000000000000000000000000000000..5acfc57b4c2c5ef660e5e9ee2ff6cc51a5d819dd --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLStatement.java @@ -0,0 +1,64 @@ +package org.rosi_project.model_sync.model_join.representation.core; + +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * An {@code OCLStatement} is a simple expression written in the {@code Object Constraint Language}. + * <p> + * This class simply wraps an arbitrary OCL expression. No further checks or the like are performed. + * + * @author Rico Bergmann + * @see <a href="https://www.omg.org/spec/OCL/">OMG OCL specification</a> + */ +public class OCLStatement { + + /** + * Creates a new statement with the given {@code content}. + */ + @Nonnull + public static OCLStatement of(@Nonnull String content) { + return new OCLStatement(content); + } + + @Nonnull + protected final String content; + + /** + * Full constructor. + * + * @param content the actual expression + */ + public OCLStatement(@Nonnull String content) { + this.content = content; + } + + /** + * Provides the actual statement as a {@code String}. + */ + public String get() { + return content; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OCLStatement that = (OCLStatement) o; + return content.equals(that.content); + } + + @Override + public int hashCode() { + return Objects.hash(content); + } + + @Override + public String toString() { + return content; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/RelativeAttribute.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/RelativeAttribute.java new file mode 100644 index 0000000000000000000000000000000000000000..d3e002c63df945d8c681a987390ffa2b1b3150f3 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/RelativeAttribute.java @@ -0,0 +1,78 @@ +package org.rosi_project.model_sync.model_join.representation.core; + +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * A {@code RelativeAttribute} represents a member of some class. + * <p> + * In difference to the {@link AttributePath} this class does not retain any information about the + * class to which it belongs. + * + * @author Rico Bergmann + */ +public class RelativeAttribute { + + /** + * Extracts the attribute from a qualified {@link AttributePath}. + */ + @Nonnull + public static RelativeAttribute from(@Nonnull AttributePath attributePath) { + return new RelativeAttribute(attributePath.getAttributeName()); + } + + /** + * Generates a new {@code RelativeAttribute} with the given name. + */ + @Nonnull + public static RelativeAttribute of(@Nonnull String attributeName) { + if (!attributeName.matches(ATTRIBUTE_NAME_REGEX)) { + throw new IllegalArgumentException("Not a valid attribute name: " + attributeName); + } + return new RelativeAttribute(attributeName); + } + + private static final String ATTRIBUTE_NAME_REGEX = "^[a-zA-Z_]\\w*$"; + + @Nonnull + private final String attributeName; + + /** + * Full constructor. + * + * @param attributeName the name of the attribute to generate + */ + public RelativeAttribute(@Nonnull String attributeName) { + this.attributeName = attributeName; + } + + /** + * Provides the name of the represented attribute. + */ + @Nonnull + public String getAttributeName() { + return attributeName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RelativeAttribute that = (RelativeAttribute) o; + return attributeName.equals(that.attributeName); + } + + @Override + public int hashCode() { + return Objects.hash(attributeName); + } + + @Override + public String toString() { + return attributeName; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ResourcePath.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ResourcePath.java new file mode 100644 index 0000000000000000000000000000000000000000..d4f99857bc7c86cb284681e6853b86ae2f52a677 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ResourcePath.java @@ -0,0 +1,154 @@ +package org.rosi_project.model_sync.model_join.representation.core; + +import java.io.File; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * A {@code ResourcePath} represents a fully-qualified name of some model element to be used in a + * join. + * + * @author Rico Bergmann + */ +public class ResourcePath { + + /** + * Generates a new {@code path} given the path to its containing <em>package</em> and the name of + * the resource which should be referenced. + */ + @Nonnull + public static ResourcePath from(@Nonnull String packagePath, @Nonnull String resourceName) { + return new ResourcePath(packagePath, resourceName); + } + + /** + * Generates a new {@code path} given its fully-qualified name. + * + * @param qualifiedResourcePath the path. It is expected to conform to the format {@code + * pathToResource.resourceName} + */ + @Nonnull + public static ResourcePath parse(@Nonnull String qualifiedResourcePath) { + if (qualifiedResourcePath.equals(".")) { + throw new IllegalArgumentException( + "Not a valid resource path: '" + qualifiedResourcePath + "'"); + } + + int splitPos = qualifiedResourcePath.lastIndexOf("."); + + String packagePath; + if (splitPos == -1) { + packagePath = ""; + } else { + packagePath = qualifiedResourcePath.substring(0, splitPos); + } + + String resourceName = qualifiedResourcePath.substring(splitPos + 1); + return new ResourcePath(packagePath, resourceName); + } + + /** + * Generates a new {@code path} given its fully-qualified name. + * <p> + * The name will be interpreted as some location on the file system (that is as {@code + * /path/to/resource}). However, both {@code \} as well as {@code /} will be accepted as path + * separators. + * + * @param qualifiedResourcePath the path. It is expected to conform to the format {@code + * pathToResource/resourceName} + */ + public static ResourcePath parseAsFileSystemPath(@Nonnull String qualifiedResourcePath) { + return parse(toPackageStylePath(qualifiedResourcePath)); + } + + /** + * Converts a path as used by the file system to the corresponding path as used in a Java package. + */ + protected static String toPackageStylePath(@Nonnull String fileSystemResourcePath) { + return fileSystemResourcePath.replaceAll("[\\\\/]", "."); + } + + @Nonnull + protected final String resourcePath; + + @Nonnull + protected final String resourceName; + + /** + * Full constructor. + * + * @param resourcePath the path to the directory/package in which the resource resides + * @param resourceName the name of the class (or other resource) + */ + public ResourcePath(@Nonnull String resourcePath, @Nonnull String resourceName) { + if (resourceName.isEmpty()) { + throw new IllegalArgumentException("Resource name may not be empty"); + } + this.resourcePath = resourcePath; + this.resourceName = resourceName; + } + + /** + * Provides the path to the directory/package in which {@code this} resource resides. The path + * will be in "Java-package style", i.e. its components will be separated by {@code .}. + */ + @Nonnull + public String getResourcePath() { + return resourcePath; + } + + /** + * Provides the name of the resource which is represented by {@code this} path. + * <p> + * The name is relative to the containing directory/package. + */ + @Nonnull + public String getResourceName() { + return resourceName; + } + + /** + * Converts {@code this} path to {@code String}. It will match the format {@code package.name}. + */ + @Nonnull + public String get() { + return this.toString(); + } + + /** + * Converts {@code this} path to a {@code String}, treating it as some path on the file system. + * <p> + * The components of the path will be separated by the platform-dependent separator for the + * current platform. That is, its result may vary depending on the OS on which it is run. + */ + @Nonnull + public String getAsFileSystemPath() { + return this.toString().replaceAll("\\.", File.pathSeparator); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ResourcePath that = (ResourcePath) o; + return this.resourcePath.equals(that.resourcePath) && + this.resourceName.equals(that.resourceName); + } + + @Override + public int hashCode() { + return Objects.hash(resourcePath, resourceName); + } + + @Override + public String toString() { + if (resourcePath.isEmpty()) { + return resourceName; + } + return resourcePath + "." + resourceName; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/TypedAttributePath.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/TypedAttributePath.java new file mode 100644 index 0000000000000000000000000000000000000000..0384a2acf0bdb6fdacbbeb050a89c035d9a9fad1 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/TypedAttributePath.java @@ -0,0 +1,95 @@ +package org.rosi_project.model_sync.model_join.representation.core; + +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * A {@code TypedAttributePath} extends a default {@link AttributePath} by adding type information, + * i.e. adding information about the type of the object the path references. + * <p> + * As we do not have any knowledge about the actual classes that are represented by paths we may + * not validate, whether the given type indeed matches the attribute's type. + * + * @author Rico Bergmann + */ +public class TypedAttributePath extends AttributePath { + + private static final String PATH_TYPE_SEPARATOR = ":"; + + /** + * Creates a new {@code TypedAttributePath} by combining the provided path and type information. + */ + @Nonnull + public static TypedAttributePath of(@Nonnull AttributePath path, @Nonnull ClassResource type) { + return new TypedAttributePath(path, type); + } + + /** + * Generates a new {@code TypedAttributePath} by splitting up the given path into its path and + * type portion. The {@code qualifiedTypedPath} is expected to match the format {@code path:type}, + * where {@code path} may be parsed using {@link AttributePath#fromQualifiedPath(String)} and type + * may be parsed using {@link ClassResource#fromQualifiedName(String)}. + */ + @Nonnull + public static TypedAttributePath fromQualifiedTypedPath(@Nonnull String qualifiedTypedPath) { + final int pathTypeSplitPos = qualifiedTypedPath.lastIndexOf(PATH_TYPE_SEPARATOR); + + if (pathTypeSplitPos < 0) { + throw new IllegalArgumentException("Missing type portion on '" + qualifiedTypedPath + "'"); + } + + final String qualifiedPath = qualifiedTypedPath.substring(0, pathTypeSplitPos); + final String type = qualifiedTypedPath.substring(pathTypeSplitPos + 1); + return new TypedAttributePath(AttributePath.fromQualifiedPath(qualifiedPath), ClassResource.fromQualifiedName(type)); + } + + @Nonnull + private final ClassResource type; + + /** + * Full constructor. + * + * @param path the attribute + * @param type the attribute's type + */ + public TypedAttributePath( + @Nonnull AttributePath path, + @Nonnull ClassResource type) { + super(path); + this.type = type; + } + + /** + * Provides the type of {@code this} attribute. + */ + @Nonnull + public ClassResource getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TypedAttributePath)) { + return false; + } + if (!super.equals(o)) { + return false; + } + TypedAttributePath that = (TypedAttributePath) o; + return type.equals(that.type); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), type); + } + + @Override + public String toString() { + return super.toString() + PATH_TYPE_SEPARATOR + type.toString(); + } + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/CompoundKeepExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/CompoundKeepExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..1e253dafd1feee21a329b2a2f65b4cb47488627f --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/CompoundKeepExpression.java @@ -0,0 +1,20 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.List; +import javax.annotation.Nonnull; + +/** + * A {@code CompoundKeepExpression} is a special kind of {@code KeepExpression} which in turn may + * contain a number of other {@code KeepExpression}s. + * + * @author Rico Bergmann + */ +public abstract class CompoundKeepExpression extends KeepExpression { + + /** + * Provides all the keep expressions that this expression is build of. + */ + @Nonnull + public abstract List<KeepExpression> getKeeps(); + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/JoinExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/JoinExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..ecdccf901a9aa31d861a66f11afdad9c78ba96e3 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/JoinExpression.java @@ -0,0 +1,204 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; + +/** + * A {@code JoinExpression} combines two classes of the source models and + * generates a new view derived from them. + * <p> + * At a conceptual level, these classes are referred to as the {@code left} and + * {@code right} class. + * <p> + * Other than the classes to combine, each join also consists of a number of + * {@link KeepExpression}s which contain the fields which should form the + * resulting class as well as information on how to set their values. + * <p> + * As there are a number of different joins available, this class is left + * {@code abstract} with subclasses to represent the specific joins. + * + * @author Rico Bergmann + * @see NaturalJoinExpression + * @see OuterJoinExpression + * @see ThetaJoinExpression + */ +public abstract class JoinExpression implements Iterable<KeepExpression> { + + /** + * The {@code JoinType} defines which rules should be applied when + * performing the join. + */ + public enum JoinType { + + /** + * The {@code natural join} combines two classes based on attributes + * with equal name and type. + * + * @see NaturalJoinExpression + */ + NATURAL, + + /** + * The {@code outer join} works like the {@link #NATURAL} one, but + * leaves instances from one class with no corresponding instance in the + * other class according to the {@code outer join + * type}. See the subclass for details. + * + * @see OuterJoinExpression + */ + OUTER, + + /** + * The {@code theta join} is more general than the {@link #NATURAL} and + * {@link #OUTER} one as it enables an arbitrary criteria to define + * whether two instances are "joinable" or not. + * + * @see ThetaJoinExpression + */ + THETA + } + + @Nonnull + protected final ClassResource left; + + @Nonnull + protected final ClassResource right; + + @Nonnull + protected final ClassResource target; + + @Nonnull + protected final List<KeepExpression> keeps; + + /** + * Full constructor. + * + * @param left + * the left class to use in the join + * @param right + * the right class to use in the join + * @param target + * the name of the resulting view + * @param keeps + * the keep statements in the join + */ + protected JoinExpression(@Nonnull ClassResource left, @Nonnull ClassResource right, @Nonnull ClassResource target, + @Nonnull List<KeepExpression> keeps) { + + this.left = left; + this.right = right; + this.target = target; + this.keeps = keeps; + } + + /** + * Proof if the right and the left class path are equals. + * @return result value + */ + public boolean isSameElement() { + if (left.getResourceName().equals(right.getResourceName()) + && left.getResourcePath().equals(right.getResourcePath())) { + return true; + } + return false; + } + + /** + * Provides the based class used in {@code this} join. Only important for + * outer join in other cases always Left. + */ + @Nonnull + public ClassResource getBaseModel() { + return this.getLeft(); + } + + /** + * Provides the other class used in {@code this} join. Only important for + * outer join in other cases always Right. + */ + @Nonnull + public ClassResource getOtherModel() { + return this.getRight(); + } + + /** + * Provides the left class used in {@code this} join. + */ + @Nonnull + public ClassResource getLeft() { + return left; + } + + /** + * Provides the right class used in {@code this} join. + */ + @Nonnull + public ClassResource getRight() { + return right; + } + + /** + * Provides the name of the class resulting from {@code this} join. + */ + @Nonnull + public ClassResource getTarget() { + return target; + } + + /** + * Provides the {@code keep} statements that should be used to build the + * resulting class. + */ + @Nonnull + public Iterable<KeepExpression> getKeeps() { + return keeps; + } + + public List<KeepExpression> getKeepsList() { + return Collections.unmodifiableList(keeps); + } + + /** + * Provides the type of {@code this} join. + */ + @Nonnull + public abstract JoinType getType(); + + /** + * Provides the type of {@code this} join as a non-technical {@code String}. + */ + @Nonnull + abstract String getJoinTypeAsString(); + + @Nonnull + @Override + public Iterator<KeepExpression> iterator() { + return keeps.iterator(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JoinExpression that = (JoinExpression) o; + return this.target.equals(that.target); + } + + @Override + public int hashCode() { + return Objects.hash(target); + } + + @Override + public String toString() { + return getJoinTypeAsString() + " " + left.toString() + " with " + right.toString() + " as " + target.toString(); + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAggregateExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAggregateExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..d0da5aa0b03a4d87b7cacf86d1d7c8442e577eb1 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAggregateExpression.java @@ -0,0 +1,261 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.Objects; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.AttributePath; +import org.rosi_project.model_sync.model_join.representation.core.RelativeAttribute; + +/** + * A {@code KeepAggregateExpression} adds an attribute to some join by calculating an aggregation on + * the referenced collection of numerical values. The possible aggregation operations are equal to + * those provided by the SQL programming language. + * <p> + * The aggregate's result will in turn be some numerical value. + * + * @author Rico Bergmann + */ +public class KeepAggregateExpression extends KeepExpression { + + /** + * The {@code AggregationType} defines what aggregation should be performed. + */ + public enum AggregationType { + + /** + * The attribute's value should be the sum of all the referenced values. + */ + SUM, + + /** + * The attribute's value should be the average value of all the referenced values. + */ + AVG, + + /** + * The attribute's value should be the minimum value of all the referenced values. + */ + MIN, + + /** + * The attribute's value should be the maximum value of all the referenced values. + */ + MAX, + + /** + * The attribute's value should be the number of referenced elements. + */ + SIZE + } + + /** + * The {@code AggregateBuilder} enables the construction of new {@link KeepAggregateExpression}s + * through a nice and fluent interface. + * + * @author Rico Bergmann + */ + public static class AggregateBuilder { + + @Nonnull + private final AggregationType aggregationType; + + @Nonnull + private final RelativeAttribute aggregatedAttribute; + + @Nonnull + private AttributePath source; + + /** + * Full constructor. + * + * @param aggregationType the aggregation operation that should be performed + * @param aggregatedAttribute the attribute on which the aggregation should be performed + */ + AggregateBuilder(@Nonnull AggregationType aggregationType, + @Nonnull RelativeAttribute aggregatedAttribute) { + this.aggregationType = aggregationType; + this.aggregatedAttribute = aggregatedAttribute; + } + + /** + * Specifies the reference destination that provides the attribute to join. + * <p> + * Suppose the following example: + * <pre> + * {@code + * class City { + * List<Street> streets; + * } + * + * class Street { + * int length; + * } + * } + * </pre> + * If one was to calculate the average length of all streets per city, the following aggregation + * could be used: {@code keep aggregate avg(length) over City.streets as avgStreetLength}. In + * this case, {@code City.streets} is the wanted reference destination as it provides the {@code + * length} attribute which should be used for the aggregation. + */ + @Nonnull + public AggregateBuilder over(@Nonnull AttributePath source) { + this.source = source; + return this; + } + + /** + * Specifies the name of the aggregation attribute in the target join. + */ + @Nonnull + public KeepAggregateExpression as(@Nonnull AttributePath target) { + return new KeepAggregateExpression(aggregationType, aggregatedAttribute, source, target); + } + } + + /** + * Generates a new {@code keep aggregate} expression that sums up the referenced values. + */ + @Nonnull + public static AggregateBuilder sum(@Nonnull RelativeAttribute attribute) { + return new AggregateBuilder(AggregationType.SUM, attribute); + } + + /** + * Generates a new {@code keep aggregate} expression that calculates the average value of the + * referenced values. + */ + @Nonnull + public static AggregateBuilder avg(@Nonnull RelativeAttribute attribute) { + return new AggregateBuilder(AggregationType.AVG, attribute); + } + + /** + * Generates a new {@code keep aggregate} expression that provides the minimum value of the + * referenced values. + */ + @Nonnull + public static AggregateBuilder min(@Nonnull RelativeAttribute attribute) { + return new AggregateBuilder(AggregationType.MIN, attribute); + } + + /** + * Generates a new {@code keep aggregate} expression that provides the maximum value of the + * referenced values. + */ + @Nonnull + public static AggregateBuilder max(@Nonnull RelativeAttribute attribute) { + return new AggregateBuilder(AggregationType.MAX, attribute); + } + + /** + * Generates a new {@code keep aggregate} expression that counts the number of values that are + * referenced. + */ + @Nonnull + public static AggregateBuilder size(@Nonnull RelativeAttribute attribute) { + return new AggregateBuilder(AggregationType.SIZE, attribute); + } + + @Nonnull + private final AggregationType aggregation; + + @Nonnull + private final RelativeAttribute aggregatedAttribute; + + @Nonnull + private final AttributePath source; + + @Nonnull + private final AttributePath target; + + /** + * Full constructor. + * + * @param aggregation the aggregation operation that should be performed + * @param aggregatedAttribute the attribute on which the aggregation should be performed + * @param source the field which provides the {@code aggregatedAttribute}. Therefore this + * attribute has to be multi-valued. + * @param target the name of the attribute under which the aggregation result should be + * available in the join + */ + public KeepAggregateExpression( + @Nonnull AggregationType aggregation, + @Nonnull RelativeAttribute aggregatedAttribute, + @Nonnull AttributePath source, + @Nonnull AttributePath target) { + this.aggregation = aggregation; + this.aggregatedAttribute = aggregatedAttribute; + this.source = source; + this.target = target; + } + + /** + * Provides the aggregation operation {@code this} expression will perform. + */ + @Nonnull + public AggregationType getAggregation() { + return aggregation; + } + + /** + * Provides the attribute that will be aggregated. As all {@link AggregationType aggregation + * operations} are defined on numerical values, this attribute in turn has to be of numerical + * type. + */ + @Nonnull + public RelativeAttribute getAggregatedAttribute() { + return aggregatedAttribute; + } + + /** + * Provides the field which contains the {@link #getAggregatedAttribute() aggregated attribute}. + * As an aggregation will - by definition - be performed upon an arbitrary number of values, this + * attribute has to be some kind of collection. + */ + @Nonnull + public AttributePath getSource() { + return source; + } + + /** + * Provides the attribute under which the result of {@code this} aggregation should be made + * available. + */ + @Nonnull + public AttributePath getTarget() { + return target; + } + + @Override + public void accept(@Nonnull KeepExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + KeepAggregateExpression that = (KeepAggregateExpression) o; + return aggregation == that.aggregation && + aggregatedAttribute.equals(that.aggregatedAttribute) && + source.equals(that.source) && + target.equals(that.target); + } + + @Override + public int hashCode() { + return Objects.hash(aggregation, aggregatedAttribute, source, target); + } + + @Override + public String toString() { + String aggregationOperation = aggregation.toString().toLowerCase(); + return "keep aggregate " // + + aggregationOperation + "(" + aggregatedAttribute + ") " // + + "over" + source // + + " as " + target; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAttributesExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAttributesExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..99cdb8109e16e584442425b0eac0aceefbe49337 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAttributesExpression.java @@ -0,0 +1,82 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import com.google.common.collect.Lists; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.AttributePath; + +/** + * A {@code KeepAttributesExpression} simply retains a number of attributes from arbitrary source + * classes and includes them in the target join. The attributes' name and type will be left + * unchanged. + * + * @author Rico Bergmann + */ +public class KeepAttributesExpression extends KeepExpression { + + /** + * Generates a new keep expression for a number of attributes. + */ + @Nonnull + public static KeepAttributesExpression keepAttributes(@Nonnull AttributePath firstAttribute, + @Nonnull AttributePath... moreAttributes) { + List<AttributePath> attributes = Lists.newArrayList(moreAttributes); + attributes.add(0, firstAttribute); + return new KeepAttributesExpression(attributes); + } + + @Nonnull + private final List<AttributePath> attributes; + + /** + * Full constructor. + * + * @param attributes the attributes for which the expression should be created + */ + public KeepAttributesExpression(@Nonnull List<AttributePath> attributes) { + this.attributes = attributes; + } + + /** + * Provides the attributes that should be retained in the join. + * <p> + * The returned {@code List} is a shallow copy of the attributes in {@code this} expression. + */ + @Nonnull + public List<AttributePath> getAttributes() { + return new ArrayList<>(attributes); + } + + @Override + public void accept(@Nonnull KeepExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + KeepAttributesExpression that = (KeepAttributesExpression) o; + return this.attributes.equals(that.attributes); + } + + @Override + public int hashCode() { + return Objects.hash(attributes); + } + + @Override + public String toString() { + return "keep attributes " + attributes.stream() // + .map(AttributePath::toString) // + .reduce((firstAttribute, secondAttribute) -> firstAttribute + ", " + secondAttribute) // + .orElseThrow(() -> new IllegalStateException( + "A keep attribute expression has to keep at least one attribute")); + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepCalculatedExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepCalculatedExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..19a1a22247fbbb23dcbcb582746b528897fc5423 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepCalculatedExpression.java @@ -0,0 +1,125 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.Objects; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.AttributePath; +import org.rosi_project.model_sync.model_join.representation.core.OCLStatement; +import org.rosi_project.model_sync.model_join.representation.core.TypedAttributePath; + +/** + * A {@code KeepCalculatedExpression} adds an attribute to some join by evaluating an arbitrary + * {@link OCLStatement OCL statement}. + * <p> + * This is the most general kind of {@link KeepExpression} as all other expressions may be expressed + * in OCL as well. + * + * @author Rico Bergmann + * @see OCLStatement + */ +public class KeepCalculatedExpression extends KeepExpression { + + /** + * The {@code KeepCalculatedBuilder} enables the construction of new {@link + * KeepCalculatedExpression}s through a nice and fluent interface. + * + * @author Rico Bergmann + */ + public static class KeepCalculatedBuilder { + + @Nonnull + private final OCLStatement calculationRule; + + /** + * Full constructor. + * + * @param calculationRule the OCL statement that should be used to calculate the attribute's + * value. + */ + KeepCalculatedBuilder(@Nonnull OCLStatement calculationRule) { + this.calculationRule = calculationRule; + } + + /** + * Specifies the name of the attribute in the target join. + */ + @Nonnull + public KeepCalculatedExpression as(@Nonnull TypedAttributePath target) { + return new KeepCalculatedExpression(calculationRule, target); + } + } + + /** + * Generates a new {@code keep calculated} expression. + */ + @Nonnull + public static KeepCalculatedBuilder keepCalculatedAttribute( + @Nonnull OCLStatement calculationRule) { + return new KeepCalculatedBuilder(calculationRule); + } + + @Nonnull + private final OCLStatement calculationRule; + + @Nonnull + private final TypedAttributePath target; + + /** + * Full constructor. + * + * @param calculationRule the OCL statement that should be used to calculate the attribute's + * value + * @param target the name of the attribute under which the aggregation result should be + * available in the join + */ + public KeepCalculatedExpression( + @Nonnull OCLStatement calculationRule, + @Nonnull TypedAttributePath target) { + this.calculationRule = calculationRule; + this.target = target; + } + + /** + * Provides the OCL expression that is used to determine the value of {@code this} attribute. + */ + @Nonnull + public OCLStatement getCalculationRule() { + return calculationRule; + } + + /** + * Provides the attribute under which the result of {@code this} calculation should be made + * available. + */ + @Nonnull + public TypedAttributePath getTarget() { + return target; + } + + @Override + public void accept(@Nonnull KeepExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + KeepCalculatedExpression that = (KeepCalculatedExpression) o; + return calculationRule.equals(that.calculationRule) && + target.equals(that.target); + } + + @Override + public int hashCode() { + return Objects.hash(calculationRule, target); + } + + @Override + public String toString() { + return "keep calculated attribute " + calculationRule + " as " + target; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..bd879001f642b3dc1a5beef04e196f86534e97ab --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepExpression.java @@ -0,0 +1,60 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import javax.annotation.Nonnull; + +/** + * A {@code KeepExpression} configures the resulting model of a join. + * <p> + * This mostly means retaining some attributes or calculating their value but may also include + * maintaining references to other classes. + * + * @author Rico Bergmann + */ +public abstract class KeepExpression { + + /** + * The {@code visitor} enables the execution of arbitrary algorithms on {@code KeepExpression}s + * without taking care of the actual traversal of the expression tree. + */ + public interface KeepExpressionVisitor { + + /** + * Runs the algorithm as appropriate for {@link KeepAggregateExpression}s. + */ + void visit(@Nonnull KeepAggregateExpression expression); + + /** + * Runs the algorithm as appropriate for {@link KeepAttributesExpression}s. + */ + void visit(@Nonnull KeepAttributesExpression expression); + + /** + * Runs the algorithm as appropriate for {@link KeepCalculatedExpression}s. + */ + void visit(@Nonnull KeepCalculatedExpression expression); + + /** + * Runs the algorithm as appropriate for {@link KeepReferenceExpression}s. + */ + void visit(@Nonnull KeepReferenceExpression expression); + + /** + * Runs the algorithm as appropriate for {@link KeepSubTypeExpression}s. + */ + void visit(@Nonnull KeepSubTypeExpression expression); + + /** + * Runs the algorithm as appropriate for {@link KeepSuperTypeExpression}s. + */ + void visit(@Nonnull KeepSuperTypeExpression expression); + + } + + // TODO `as` statements may be omitted !?!? ... (╯°□°)╯︵ ┻━┻ + + /** + * Applies a {@code visitor} to {@code this} expression structure. + */ + public abstract void accept(@Nonnull KeepExpressionVisitor visitor); + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepReferenceExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepReferenceExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..5d6f0f32924f2f495b21b9ab3107dcbf15c044b2 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepReferenceExpression.java @@ -0,0 +1,223 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.AttributePath; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.util.Assert; + +// TODO the Xtext grammar also specifies an optional `as reference` part - what does that part do? +// most likely `as reference` will refer to another type that was already defined in its dedicated +// join statement. + +/** + * A {@code KeepReferenceExpression} retains links to other instances in the original models. All + * objects that are referenced in the base models will also be available in the resulting ModelJoin + * views. + * <p> + * As references are directional, each expression also distinguishes between {@code outgoing} and + * {@code incoming} references - depending on whether the model class of {@code this} expression + * owns the reference ({@code outgoing}) or instances of this model class are being referenced + * ({@code incoming}). + * <p> + * The referenced instances will be made available according to a number of keep statements which + * specify the attributes to include. This boils down to the creation of a nested ModelJoin view for + * these instances. + * + * @author Rico Bergmann + */ +public class KeepReferenceExpression extends CompoundKeepExpression { + + /** + * The {@code ReferenceDirection} defines whether a reference is owned by the containing model + * class or whether it is the class being referenced. + */ + public enum ReferenceDirection { + + /** + * Indicates that the containing model class owns the reference. + */ + OUTGOING, + + /** + * Indicates that the containing model class is referenced by some other class. + */ + INCOMING + } + + /** + * The {@code KeepReferenceBuilder} enables the construction of new {@code + * KeepReferenceExpression} instances through a nice and fluent interface. + * + * @author Rico Bergmann + */ + public static class KeepReferenceBuilder { + + private ReferenceDirection referenceDirection; + private AttributePath attribute; + private ClassResource target; + + @Nonnull + private List<KeepExpression> keeps; + + /** + * Default constructor. + */ + private KeepReferenceBuilder() { + this.keeps = new ArrayList<>(); + } + + /** + * Creates an outgoing reference for the given attribute. + */ + @Nonnull + public KeepReferenceBuilder outgoing(@Nonnull AttributePath attribute) { + this.referenceDirection = ReferenceDirection.OUTGOING; + this.attribute = attribute; + return this; + } + + /** + * Creates an incoming reference for the given attribute. + */ + @Nonnull + public KeepReferenceBuilder incoming(@Nonnull AttributePath attribute) { + this.referenceDirection = ReferenceDirection.INCOMING; + this.attribute = attribute; + return this; + } + + /** + * Specifies the name of the attribute in the target join. + */ + @Nonnull + public KeepReferenceBuilder as(@Nonnull ClassResource target) { + this.target = target; + return this; + } + + /** + * Adds an attribute to the view for the referenced instances. + */ + @Nonnull + public KeepReferenceBuilder keep(@Nonnull KeepExpression keepExpression) { + this.keeps.add(keepExpression); + return this; + } + + /** + * Finishes the construction process. + */ + @Nonnull + public KeepReferenceExpression buildExpression() { + Assert.noNullArguments("All components must be specified", attribute, referenceDirection, target, keeps); + return new KeepReferenceExpression(attribute, referenceDirection, target, keeps); + } + } + + /** + * Starts the creation process for a new {@code KeepReferenceExpression}. + */ + @Nonnull + public static KeepReferenceBuilder keep() { + return new KeepReferenceBuilder(); + } + + @Nonnull + private final AttributePath attribute; + + @Nonnull + private final ReferenceDirection referenceDirection; + + @Nonnull + private final ClassResource target; + + @Nonnull + private final List<KeepExpression> keeps; + + /** + * Full constructor. + * + * @param attribute the attribute of the source model which contains the references + * @param referenceDirection whether the source model owns the reference or is being + * referenced + * @param target the name of the class which enca + * @param keeps the statements which should be build the "nested" view for the references + */ + public KeepReferenceExpression( + @Nonnull AttributePath attribute, + @Nonnull ReferenceDirection referenceDirection, + @Nonnull ClassResource target, + @Nonnull List<KeepExpression> keeps) { + this.attribute = attribute; + this.referenceDirection = referenceDirection; + this.target = target; + this.keeps = keeps; + } + + /** + * Provides the attribute of the source model which contains the references. + */ + @Nonnull + public AttributePath getAttribute() { + return attribute; + } + + /** + * Provides the kind of reference, that is whether the containing Join owns the reference or is + * being referenced by another model class or Join. + */ + @Nonnull + public ReferenceDirection getReferenceDirection() { + return referenceDirection; + } + + /** + * Provides the name of the attribute under which the referenced instances should be made + * available. + */ + @Nonnull + public ClassResource getTarget() { + return target; + } + + /** + * Provides all {@code KeepExpression keep expressions} that should be used to build the Join for + * the referenced instances. + */ + @Nonnull + @Override + public List<KeepExpression> getKeeps() { + return new ArrayList<>(keeps); + } + + @Override + public void accept(@Nonnull KeepExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + KeepReferenceExpression that = (KeepReferenceExpression) o; + return attribute.equals(that.attribute) && target.equals(that.target); + } + + @Override + public int hashCode() { + return Objects.hash(attribute, target); + } + + @Override + public String toString() { + String directionString = referenceDirection.toString().toLowerCase(); + return "keep " + directionString + " " + attribute + " as type " + target; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSubTypeExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSubTypeExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..76e8d6027462a00d341906020ca57c638b1ed074 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSubTypeExpression.java @@ -0,0 +1,167 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.util.Assert; + +/** + * A {@code KeepSubTypeExpression} instructs the {@code ModelJoin} runtime to instantiate the most + * specific type that inherits from this type as well. The additional instance will be built + * according to the {@code KeepExpression keep statements} that it is constructed of. + * <p> + * In order to prevent ambiguities a {@code KeepSubTypeExpression} may only be specified on joins + * that do not participate in another {@link JoinExpression join statement}. + * + * @author Rico Bergmann + */ +public class KeepSubTypeExpression extends CompoundKeepExpression { + + /** + * The {@code KeepSubTypeBuilder} enables the construction of new {@code KeepSubTypeExpression} + * instances through a nice and fluent interface. + * + * @author Rico Bergmann + */ + public static class KeepSubTypeBuilder { + + private ClassResource typeToKeep; + private ClassResource target; + private List<KeepExpression> keeps; + + /** + * Default constructor. + */ + private KeepSubTypeBuilder() { + this.keeps = new ArrayList<>(); + } + + /** + * Specifies the subtype that should be instantiated. + */ + @Nonnull + public KeepSubTypeBuilder subtype(@Nonnull ClassResource type) { + this.typeToKeep = type; + return this; + } + + /** + * Specifies the name of the class that should be generated for the subtype instances. + */ + @Nonnull + public KeepSubTypeBuilder as(@Nonnull ClassResource target) { + this.target = target; + return this; + } + + /** + * Adds a {@link KeepExpression keep statement} for the subtype-class. The described attribute + * will be initialized for each instantiated subtype element. + */ + @Nonnull + public KeepSubTypeBuilder keep(@Nonnull KeepExpression keep) { + this.keeps.add(keep); + return this; + } + + /** + * Finishes the construction process. + */ + @Nonnull + public KeepSubTypeExpression buildExpression() { + Assert.noNullArguments("All components must be specified", typeToKeep, target, keeps); + return new KeepSubTypeExpression(typeToKeep, target, keeps); + } + + } + + /** + * Starts the creation process for a new {@code KeepSubTypeExpression}. + */ + @Nonnull + public static KeepSubTypeBuilder keep() { + return new KeepSubTypeBuilder(); + } + + @Nonnull + private final ClassResource typeToKeep; + + @Nonnull + private final ClassResource target; + + @Nonnull + private final List<KeepExpression> keeps; + + /** + * Full constructor. + * + * @param typeToKeep the subtype that should be instantiated. + * @param target the name of the view that should be generated for all matching instances + * @param keeps the keep statements that should form the attributes of the generated view + * instances + */ + public KeepSubTypeExpression( + @Nonnull ClassResource typeToKeep, + @Nonnull ClassResource target, + @Nonnull List<KeepExpression> keeps) { + this.typeToKeep = typeToKeep; + this.target = target; + this.keeps = keeps; + } + + /** + * Provides the subtype that should be instantiated. + */ + @Nonnull + public ClassResource getType() { + return typeToKeep; + } + + /** + * Provides the name of the view that should be generated for all matching instances. + */ + @Nonnull + public ClassResource getTarget() { + return target; + } + + /** + * Provides all {@code KeepExpression keep expressions} that should be used to build the Join for + * the instances of the subclass instances. + */ + @Nonnull + @Override + public List<KeepExpression> getKeeps() { + return new ArrayList<>(keeps); + } + + @Override + public void accept(@Nonnull KeepExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + KeepSubTypeExpression that = (KeepSubTypeExpression) o; + return typeToKeep.equals(that.typeToKeep) && + target.equals(that.target); + } + + @Override + public int hashCode() { + return Objects.hash(typeToKeep, target); + } + + @Override + public String toString() { + return "keep subtype " + typeToKeep + " as " + target; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSuperTypeExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSuperTypeExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..6c72a3d9a42fdef32eafe621ceba232af271b803 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSuperTypeExpression.java @@ -0,0 +1,166 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.util.Assert; + +// TODO add doc + +/** + * A {@code KeepSuperTypeExpression} instructs the {@code ModelJoin} runtime to instantiate the a + * supertype of some class in a join along an instance of that class. The additional instance will + * be built according to the {@code KeepExpression keep statements} that it is constructed of. + * + * @author Rico Bergmann + */ +public class KeepSuperTypeExpression extends CompoundKeepExpression { + + /** + * The {@code KeepSuperTypeBuilder} enables the construction of new {@code KeepSuperTypeExpression} + * instances through a nice and fluent interface. + * + * @author Rico Bergmann + */ + public static class KeepSuperTypeBuilder { + + private ClassResource typeToKeep; + private ClassResource target; + private List<KeepExpression> keeps; + + /** + * Default constructor. + */ + private KeepSuperTypeBuilder() { + this.keeps = new ArrayList<>(); + } + + /** + * Specifies the supertype that should be instantiated. + */ + @Nonnull + public KeepSuperTypeBuilder supertype(@Nonnull ClassResource type) { + this.typeToKeep = type; + return this; + } + + /** + * Specifies the name of the class that should be generated for the subtype instances. + */ + @Nonnull + public KeepSuperTypeBuilder as(@Nonnull ClassResource target) { + this.target = target; + return this; + } + + /** + * Adds a {@link KeepExpression keep statement} for the subtype-class. The described attribute + * will be initialized for each instantiated subtype element. + */ + @Nonnull + public KeepSuperTypeBuilder keep(@Nonnull KeepExpression keep) { + this.keeps.add(keep); + return this; + } + + /** + * Finishes the construction process. + */ + @Nonnull + public KeepSuperTypeExpression buildExpression() { + Assert.noNullArguments("All components must be specified", typeToKeep, target, keeps); + return new KeepSuperTypeExpression(typeToKeep, target, keeps); + } + + } + + /** + * Starts the creation process for a new {@code KeepSuperTypeExpression}. + */ + @Nonnull + public static KeepSuperTypeBuilder keep() { + return new KeepSuperTypeBuilder(); + } + + @Nonnull + private final ClassResource typeToKeep; + + @Nonnull + private final ClassResource target; + + @Nonnull + private final List<KeepExpression> keeps; + + /** + * Full constructor. + * + * @param typeToKeep the supertype that should be instantiated. + * @param target the name of the view that should be generated for all matching instances + * @param keeps the keep statements that should form the attributes of the generated view + * instances + */ + public KeepSuperTypeExpression( + @Nonnull ClassResource typeToKeep, + @Nonnull ClassResource target, + @Nonnull List<KeepExpression> keeps) { + this.typeToKeep = typeToKeep; + this.target = target; + this.keeps = keeps; + } + + /** + * Provides the supertype that should be instantiated. + */ + @Nonnull + public ClassResource getType() { + return typeToKeep; + } + + /** + * Provides the name of the view that should be generated for all matching instances. + */ + @Nonnull + public ClassResource getTarget() { + return target; + } + + /** + * Provides all {@code KeepExpression keep expressions} that should be used to build the Join for + * the instances of the superclass instances. + */ + @Nonnull + @Override + public List<KeepExpression> getKeeps() { + return new ArrayList<>(keeps); + } + + @Override + public void accept(@Nonnull KeepExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + KeepSuperTypeExpression that = (KeepSuperTypeExpression) o; + return typeToKeep.equals(that.typeToKeep) && + target.equals(that.target); + } + + @Override + public int hashCode() { + return Objects.hash(typeToKeep, target); + } + + @Override + public String toString() { + return "keep supertype " + typeToKeep + " as " + target; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ModelJoinExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ModelJoinExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..30382b532ada48b163756519650e01c25516fc3a --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ModelJoinExpression.java @@ -0,0 +1,102 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * The {@code ModelJoinExpression} represents an actual ModelJoin, i.e. a specification of which + * joins to use and how they should be built. + * + * @author Rico Bergmann + */ +public class ModelJoinExpression implements Iterable<JoinExpression> { + + private String name = ""; + + public String getName() { + return name; + } + + public void setName(String n) { + name = n; + } + + /** + * Creates a new {@code ModelJoin}. + * + * @param joins the joins that should be included. + */ + @Nonnull + public static ModelJoinExpression with(@Nonnull List<JoinExpression> joins) { + return new ModelJoinExpression(joins); + } + + /** + * Ensures that all joins specify distinct join targets and throws an {@code + * IllegalArgumentException} otherwise. + */ + private static void assertDifferentTargetsForAllJoins(Collection<JoinExpression> joinsToCheck) { + final long totalNumberOfJoins = joinsToCheck.size(); + final long numberOfDistinctTargetsSpecified = joinsToCheck.stream()// + .map(JoinExpression::getTarget) // + .distinct() // + .count(); + + if (numberOfDistinctTargetsSpecified < totalNumberOfJoins) { + throw new IllegalArgumentException("Some joins share the same target"); + } + } + + @Nonnull + private final List<JoinExpression> joins; + + /** + * Full constructor. + * + * @param joins the joins that this ModelJoin consists of + */ + public ModelJoinExpression(List<JoinExpression> joins) { + assertDifferentTargetsForAllJoins(joins); + this.joins = joins; + } + + /** + * Provides all joins {@code this} model specifies. + */ + @Nonnull + public Collection<JoinExpression> getJoins() { + return new ArrayList<>(joins); + } + + @Nonnull + @Override + public Iterator<JoinExpression> iterator() { + return joins.iterator(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ModelJoinExpression that = (ModelJoinExpression) o; + return joins.equals(that.joins); + } + + @Override + public int hashCode() { + return Objects.hash(joins); + } + + @Override + public String toString() { + return joins.toString(); + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/NaturalJoinExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/NaturalJoinExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..7c3d7b4d9aa8a374d367bf5d3ba6948531538b9c --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/NaturalJoinExpression.java @@ -0,0 +1,96 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.List; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; + +/** + * The {@code natural join} combines model classes based on attributes of the + * same name and type. + * <p> + * As an example, consider the following two classes: + * + * <pre> + * { + * @code + * class Person { + * String firstname; + * String lastname; + * Date dateOfBirth; + * Char gender; + * // ... other code omitted + * } + * + * class Employee { + * int id; + * String firstname; + * String lastname; + * Date dateOfBirth; + * int salary; + * // ... other code omitted + * } + * } + * </pre> + * <p> + * Performing a natural join on {@code Person} and {@code Employee} will combine + * instances only if they share the same first name, last name and date of + * birth. All other fields will not be considered during the comparison. + * + * <pre> + * ┌───────────────────┐ ┌───────────────────┐ + * │ Person │ │ Employee │ + * ├───────────────────┤ ├───────────────────┤ + * │firstname: String │ ┄┄┄┄┄┄┄╮ │id: int │ + * │lastname: String │ ┄┄┄┄┄╮ ╰┄┄┄ │firstname: String │ + * │dateOfBirth: Date │ ┄┄┄╮ ╰┄┄┄┄┄ │lastname: String │ + * │gender: Char │ ╰┄┄┄┄┄┄┄ │dateOfBirth: Date │ + * ├───────────────────┤ │salary: int │ + * │ ... │ ├───────────────────┤ + * └───────────────────┘ │ ... │ + * └───────────────────┘ + * </pre> + * <p> + * The resulting class will by default only contain the attributes that where + * used in comparison (first name, last name and date of birth in the example). + * + * @author Rico Bergmann + */ +public class NaturalJoinExpression extends JoinExpression { + + /* + * TODO: discuss should the natural join extend theta join instead (from a + * theoretical view more appropriate. However this would also mean that the + * natural join has to derive the join condition as well. This in turn would + * also meant that class resources would have to know which attributes are + * defined on them. + */ + + /** + * Full constructor. + * + * @param left + * the left class to use in the join + * @param right + * the right class to use in the join + * @param target + * the name of the resulting view + * @param keeps + * the keep statements in the join + */ + public NaturalJoinExpression(@Nonnull ClassResource left, @Nonnull ClassResource right, + @Nonnull ClassResource target, @Nonnull List<KeepExpression> keeps) { + super(left, right, target, keeps); + } + + @Nonnull + @Override + public JoinType getType() { + return JoinType.NATURAL; + } + + @Nonnull + @Override + String getJoinTypeAsString() { + return "natural join"; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/OuterJoinExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/OuterJoinExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..376dad0da9717dea91041d9951f1febe2e0de01c --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/OuterJoinExpression.java @@ -0,0 +1,150 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.List; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; + +/** + * The {@code outer join} is a special kind of {@code natural join}: depending + * on its specific type, all elements of one source will definitely be included + * - potentially being joined with matching instances of the other source. + * <p> + * Matches will be determined the same way as with a normal + * {@code natural join}. However, depending on the kind of join, the result will + * include all instances of the {@code left} or {@code right} source model, no + * matter whether they have a matching instance in the other model or not. In + * case that some instance has no corresponding instance in the other model, all + * fields that belong to the other model will be set to {@code null}. + * <p> + * Consider the following example (derived from the model shown in the + * {@link NaturalJoinExpression} documentation) + * + * <pre> + * Person: + * ┌───────────┬──────────┬─────────────┬────────┐ + * │ firstname │ lastname │ dateOfBirth │ gender │ + * ├───────────┼──────────┼─────────────┼────────┤ + * │ Jane │ Doe │ 1999-01-01 │ f │ + * │ John │ Doe │ 1998-02-03 │ m │ + * │ Maria │ Miller │ 1990-12-31 │ f │ + * └───────────┴──────────┴─────────────┴────────┘ + * + * Employee: + * ┌────┬───────────┬──────────┬─────────────┬────────┐ + * │ id │ firstname │ lastname │ dateOfBirth │ salary │ + * ├────┼───────────┼──────────┼─────────────┼────────┤ + * │ 1 │ Jane │ Doe │ 1999-01-01 │ 20000 │ + * │ 2 │ John │ Doe │ 1997-03-04 │ 15000 │ + * │ 3 │ Maria │ Miller │ 1990-12-31 │ 17500 │ + * └────┴───────────┴──────────┴─────────────┴────────┘ + * </pre> + * + * Performing an left outer join as described in the + * {@link NaturalJoinExpression natural join} documentation will lead to the + * following result (again written in relational style): + * + * <pre> + * ┌───────────┬──────────┬─────────────┬────────┬──────┬────────┐ + * │ firstname │ lastname │ dateOfBirth │ gender │ id │ salary │ + * ├───────────┼──────────┼─────────────┼────────┼──────┼────────┤ + * │ Jane │ Doe │ 1999-01-01 │ f │ 1 │ 20000 │ + * │ John │ Doe │ 1998-02-03 │ m │ NULL │ NULL │ + * │ Maria │ Miller │ 1990-12-31 │ f │ 3 │ 17500 │ + * └───────────┴──────────┴─────────────┴────────┴──────┴────────┘ + * </pre> + * + * @author Rico Bergmann + */ +public class OuterJoinExpression extends NaturalJoinExpression { + + /** + * The {@code JoinDirection} denotes which side of the source models should + * keep all of its instances - no matter whether they have a matching + * instances in the other model or not. + */ + public enum JoinDirection { + LEFT, RIGHT + } + + @Nonnull + private final JoinDirection direction; + + /** + * Full constructor. + * + * @param left + * the left class to use in the join + * @param right + * the right class to use in the join + * @param target + * the name of the resulting view + * @param direction + * the source model which should definitely be kept + * @param keeps + * the keep statements in the join + */ + public OuterJoinExpression(@Nonnull ClassResource left, @Nonnull ClassResource right, @Nonnull ClassResource target, + @Nonnull JoinDirection direction, @Nonnull List<KeepExpression> keeps) { + super(left, right, target, keeps); + this.direction = direction; + } + + /** + * Provides the position of the source model on which the join should be + * based. + */ + @Nonnull + public JoinDirection getDirection() { + return direction; + } + + /** + * Provides the source model on which the join should be based. + */ + @Override + public ClassResource getBaseModel() { + switch (direction) { + case LEFT: + return left; + case RIGHT: + return right; + default: + throw new AssertionError(direction); + } + } + + @Override + public ClassResource getOtherModel() { + switch (direction) { + case LEFT: + return right; + case RIGHT: + return left; + default: + throw new AssertionError(direction); + } + } + + @Nonnull + @Override + public JoinType getType() { + return JoinType.OUTER; + } + + @Nonnull + @Override + String getJoinTypeAsString() { + String directionString; + switch (this.direction) { + case LEFT: + directionString = "left"; + break; + case RIGHT: + directionString = "right"; + break; + default: + throw new AssertionError(this.direction); + } + return directionString + " outer join"; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ThetaJoinExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ThetaJoinExpression.java new file mode 100644 index 0000000000000000000000000000000000000000..39f777b05b511d0248c0c84a47535c05470dc892 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ThetaJoinExpression.java @@ -0,0 +1,77 @@ +package org.rosi_project.model_sync.model_join.representation.grammar; + +import java.util.List; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.core.OCLConstraint; + +/** + * A {@code theta join} is the most general type of join. It will determine join-compatibility + * based on an arbitrary predicate which may evaluate to {@code true} or {@code false}, denoting + * whether two instances are to be joined or not. + * <p> + * To specify this condition, {@link OCLConstraint OCL} is used. + * <p> + * As a simple example, consider the following two classes: + * <pre> + * ┌───────────────────┐ ┌───────────────────┐ + * │ Person │ │ Employee │ + * ├───────────────────┤ ├───────────────────┤ + * │firstname: String │ │id: int │ + * │lastname: String │ │fullname: String │ + * │dateOfBirth: Date │ │dateOfBirth: Date │ + * │gender: Char │ │salary: int │ + * ├───────────────────┤ ├───────────────────┤ + * │ ... │ │ ... │ + * └───────────────────┘ └───────────────────┘ + * </pre> + * <p> + * The expression {@code person.firstname + ' ' + person.lastname = employee.fullname} would than + * match all persons with employee instances if they have the same name. + * + * @author Rico Bergmann + */ +public class ThetaJoinExpression extends JoinExpression { + + @Nonnull + private final OCLConstraint condition; + + /** + * Full constructor. + * + * @param left the left class to use in the join + * @param right the right class to use in the join + * @param target the name of the resulting view + * @param condition the condition which should be used to determine join-compatibility + */ + public ThetaJoinExpression( + @Nonnull ClassResource left, + @Nonnull ClassResource right, + @Nonnull ClassResource target, + @Nonnull OCLConstraint condition, + @Nonnull List<KeepExpression> keeps) { + super(left, right, target, keeps); + this.condition = condition; + } + + /** + * Provides the condition which should be used to determine whether two instances are join + * compatible or not. + */ + @Nonnull + public OCLConstraint getCondition() { + return condition; + } + + @Nonnull + @Override + public JoinType getType() { + return JoinType.THETA; + } + + @Nonnull + @Override + String getJoinTypeAsString() { + return "theta join"; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParser.java new file mode 100644 index 0000000000000000000000000000000000000000..87c6f0cc6383feb4e067c767e08bd12c303d308e --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParser.java @@ -0,0 +1,32 @@ +package org.rosi_project.model_sync.model_join.representation.parser; + +import java.io.File; +import java.util.Optional; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; + +/** + * The {@code ModelJoinParser} reads a {@code ModelJoin} file from disk and creates a matching + * logical representation for it. + * + * @author Rico Bergmann + */ +public interface ModelJoinParser { + + /** + * Converts the {@code ModelJoin} file into a {@link ModelJoinExpression}, wrapped into an {@code + * Optional}. If parsing fails, an empty {@code Optional} will be returned. + */ + @Nonnull + Optional<ModelJoinExpression> read(@Nonnull File modelFile); + + /** + * Converts the {@code ModelJoin} file into a {@link ModelJoinExpression}. If parsing fails, a + * {@link ModelJoinParsingException} will be thrown. + * + * @throws ModelJoinParsingException if the model may not be parsed for some reason. + */ + @Nonnull + ModelJoinExpression readOrThrow(@Nonnull File modelFile); + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParsingException.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParsingException.java new file mode 100644 index 0000000000000000000000000000000000000000..6b72c01ee5d4926b87db1cfa80bc9839e385d104 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParsingException.java @@ -0,0 +1,31 @@ +package org.rosi_project.model_sync.model_join.representation.parser; + +/** + * The {@code ModelJoinParsingException} indicates that a {@code ModelJoin} file could not be read + * correctly, e.g. because of missing values or a malformed structure. + * + * @author Rico Bergmann + */ +public class ModelJoinParsingException extends RuntimeException { + + /** + * @see RuntimeException#RuntimeException(String) + */ + public ModelJoinParsingException(String message) { + super(message); + } + + /** + * @see RuntimeException#RuntimeException(String, Throwable) + */ + public ModelJoinParsingException(String message, Throwable cause) { + super(message, cause); + } + + /** + * @see RuntimeException#RuntimeException(Throwable) + */ + public ModelJoinParsingException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/AntlrBackedModelJoinParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/AntlrBackedModelJoinParser.java new file mode 100644 index 0000000000000000000000000000000000000000..86773781a112c27a910fca63acc710f9d50f331f --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/AntlrBackedModelJoinParser.java @@ -0,0 +1,104 @@ +package org.rosi_project.model_sync.model_join.representation.parser.antlr; + +import java.io.File; +import java.io.IOException; +import java.util.Optional; +import javax.annotation.Nonnull; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.TokenStream; +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; +import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParser; +import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinLexer; + +/** + * The {@code AntlrBackedModelJoinParser} reads a {@code ModelJoin} file according to a compiled + * {@code ANTLR} grammar and generates the matching {@link ModelJoinExpression} for the file. + * + * @author Rico Bergmann + * @see <a href="https://www.antlr.org/">ANTLR project</a> + */ +public class AntlrBackedModelJoinParser implements ModelJoinParser { + + @Nonnull + @Override + public Optional<ModelJoinExpression> read(@Nonnull File modelFile) { + try { + return Optional.of(invokeAntlr(modelFile)); + } catch (ModelJoinParsingException e) { + return Optional.empty(); + } + } + + @Nonnull + @Override + public ModelJoinExpression readOrThrow(@Nonnull File modelFile) { + return invokeAntlr(modelFile); + } + + /** + * Executes the whole ANTLR workflow. + * + * @throws ModelJoinParsingException if the workflow fails. Details on the error will be added + * to the exception. + */ + protected ModelJoinExpression invokeAntlr(@Nonnull File modelFile) { + CharStream modelStream = consumeCompleteModelFileContent(modelFile); + ModelJoinLexer lexer = initializeLexerBasedOn(modelStream); + TokenStream tokenStream = tokenize(lexer); + + org.rosi_project.model_sync.model_join.representation.parser.antlr.generated. // + ModelJoinParser modelJoinParser = initializeParserBasedOn(tokenStream); + + org.rosi_project.model_sync.model_join.representation.parser.antlr.generated. // + ModelJoinParser.ModeljoinContext modelJoinContext = modelJoinParser.modeljoin(); + + ModelJoinVisitor modelJoinVisitor = new ModelJoinVisitor(); + + return modelJoinVisitor.visitModeljoin(modelJoinContext); + } + + /** + * Converts the content of a {@code ModelJoin} file into an ANTLR input stream. + * + * @throws ModelJoinParsingException if the file may not be read + */ + @Nonnull + protected CharStream consumeCompleteModelFileContent(@Nonnull File modelFile) { + try { + return CharStreams.fromPath(modelFile.toPath()); + } catch (IOException e) { + throw new ModelJoinParsingException(e); + } + } + + /** + * Sets up a lexer for the given {@code ModelJoin} file. + */ + @Nonnull + protected ModelJoinLexer initializeLexerBasedOn(@Nonnull CharStream modelFile) { + return new ModelJoinLexer(modelFile); + } + + /** + * Executes the lexing step. + */ + @Nonnull + protected TokenStream tokenize(@Nonnull ModelJoinLexer lexer) { + return new CommonTokenStream(lexer); + } + + /** + * Sets up the parser. + */ + @Nonnull + protected org.rosi_project.model_sync.model_join.representation.parser.antlr.generated. // + ModelJoinParser initializeParserBasedOn(@Nonnull TokenStream tokens) { + return new org.rosi_project.model_sync.model_join.representation.parser.antlr.generated. // + ModelJoinParser(tokens); + } + +} + diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/JoinStatementVisitor.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/JoinStatementVisitor.java new file mode 100644 index 0000000000000000000000000000000000000000..3fe1c7bd180e24161204ccab4a59d2a557985536 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/JoinStatementVisitor.java @@ -0,0 +1,183 @@ +package org.rosi_project.model_sync.model_join.representation.parser.antlr; + +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.core.OCLConstraint; +import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression.JoinType; +import org.rosi_project.model_sync.model_join.representation.grammar.NaturalJoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.OuterJoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.ThetaJoinExpression; +import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinBaseVisitor; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.JoinContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.NaturaljoinContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.OuterjoinContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.ThetajoinContext; +import org.rosi_project.model_sync.model_join.representation.util.JoinFactory; +import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.NaturalJoinBuilder; +import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.OuterJoinBuilder; +import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.ThetaJoinBuilder; + +/** + * The {@code JoinStatementVisitor} creates {@link JoinExpression} instances for a parsed {@code + * ModelJoin} file. + * + * @author Rico Bergmann + */ +public class JoinStatementVisitor extends ModelJoinBaseVisitor<JoinExpression> { + + private final KeepStatementVisitor keepStatementVisitor; + + /** + * Default constructor. Nothing special about it. + */ + JoinStatementVisitor() { + this.keepStatementVisitor = new KeepStatementVisitor(); + } + + @Override + public JoinExpression visitJoin(JoinContext ctx) { + JoinType joinType = determineJoinType(ctx); + return invokeJoinBuilderAccordingTo(joinType, ctx); + } + + /** + * Checks, which kind of join is specified in a specific {@code JoinContext}. + */ + private JoinType determineJoinType(@Nonnull JoinContext ctx) { + if (ctx.thetajoin() != null) { + return JoinType.THETA; + } else if (ctx.naturaljoin() != null) { + return JoinType.NATURAL; + } else if (ctx.outerjoin() != null) { + return JoinType.OUTER; + } else { + throw new ModelJoinParsingException("Unkown join type in join " + ctx); + } + } + + /** + * Generates a {@link JoinExpression} according to the {@link JoinType} of the given {@code + * JoinContext}. + */ + private JoinExpression invokeJoinBuilderAccordingTo(@Nonnull JoinType type, + @Nonnull JoinContext ctx) { + switch (type) { + case THETA: + return generateThetaJoin(ctx); + case NATURAL: + return generateNaturalJoin(ctx); + case OUTER: + return generateOuterJoin(ctx); + default: + throw new AssertionError(type); + } + } + + /** + * Creates the {@link ThetaJoinExpression} instance contained in a {@code JoinContext}. This + * method assumes that the provided {@code ctx} indeed contains a theta join. + */ + private ThetaJoinExpression generateThetaJoin(@Nonnull JoinContext ctx) { + ThetajoinContext thetajoinContext = ctx.thetajoin(); + + ClassResource left = ClassResource.fromQualifiedName(thetajoinContext.classres(0).getText()); + ClassResource right = ClassResource.fromQualifiedName(thetajoinContext.classres(1).getText()); + + ThetaJoinBuilder joinBuilder = JoinFactory.createNew() + .theta() + .join(left) + .with(right) + .as(ClassResource.fromQualifiedName(ctx.classres().getText())) + .where(OCLConstraint.of(thetajoinContext.oclcond().getText())); + + ctx.keepaggregatesexpr().stream() + .map(keepStatementVisitor::visitKeepaggregatesexpr) + .forEach(joinBuilder::keep); + ctx.keepattributesexpr().stream() + .map(keepStatementVisitor::visitKeepattributesexpr) + .forEach(joinBuilder::keep); + ctx.keepcalculatedexpr().stream() + .map(keepStatementVisitor::visitKeepcalculatedexpr) + .forEach(joinBuilder::keep); + ctx.keepexpr().stream() + .map(keepStatementVisitor::visitKeepexpr) + .forEach(joinBuilder::keep); + + return joinBuilder.done(); + } + + /** + * Creates the {@link NaturalJoinExpression} instance contained in a {@code JoinContext}. This + * method assumes that the provided {@code ctx} indeed contains a natural join. + */ + private NaturalJoinExpression generateNaturalJoin(@Nonnull JoinContext ctx) { + NaturaljoinContext naturalJoinContext = ctx.naturaljoin(); + + ClassResource left = ClassResource.fromQualifiedName(naturalJoinContext.classres(0).getText()); + ClassResource right = ClassResource.fromQualifiedName(naturalJoinContext.classres(1).getText()); + + NaturalJoinBuilder joinBuilder = JoinFactory.createNew() + .natural() + .join(left) + .with(right) + .as(ClassResource.fromQualifiedName(ctx.classres().getText())); + + ctx.keepaggregatesexpr().stream() + .map(keepStatementVisitor::visitKeepaggregatesexpr) + .forEach(joinBuilder::keep); + ctx.keepattributesexpr().stream() + .map(keepStatementVisitor::visitKeepattributesexpr) + .forEach(joinBuilder::keep); + ctx.keepcalculatedexpr().stream() + .map(keepStatementVisitor::visitKeepcalculatedexpr) + .forEach(joinBuilder::keep); + ctx.keepexpr().stream() + .map(keepStatementVisitor::visitKeepexpr) + .forEach(joinBuilder::keep); + + return joinBuilder.done(); + } + + /** + * Creates the {@link OuterJoinExpression} instance contained in a {@code JoinContext}. This + * method assumes that the provided {@code ctx} indeed contains an outer join. + */ + private OuterJoinExpression generateOuterJoin(@Nonnull JoinContext ctx) { + OuterjoinContext outerJoinContext = ctx.outerjoin(); + + ClassResource left = ClassResource.fromQualifiedName(outerJoinContext.classres(0).getText()); + ClassResource right = ClassResource.fromQualifiedName(outerJoinContext.classres(1).getText()); + + OuterJoinBuilder joinBuilder = JoinFactory.createNew() + .outer() + .join(left) + .with(right) + .as(ClassResource.fromQualifiedName(ctx.classres().getText())); + + if (outerJoinContext.leftouterjoin() != null) { + joinBuilder.leftOuter(); + } else if (outerJoinContext.rightouterjoin() != null) { + joinBuilder.rightOuter(); + } else { + throw new ModelJoinParsingException("Expected either 'left' or 'right' 'outer join'"); + } + + ctx.keepaggregatesexpr().stream() + .map(keepStatementVisitor::visitKeepaggregatesexpr) + .forEach(joinBuilder::keep); + ctx.keepattributesexpr().stream() + .map(keepStatementVisitor::visitKeepattributesexpr) + .forEach(joinBuilder::keep); + ctx.keepcalculatedexpr().stream() + .map(keepStatementVisitor::visitKeepcalculatedexpr) + .forEach(joinBuilder::keep); + ctx.keepexpr().stream() + .map(keepStatementVisitor::visitKeepexpr) + .forEach(joinBuilder::keep); + + return joinBuilder.done(); + } + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/KeepStatementVisitor.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/KeepStatementVisitor.java new file mode 100644 index 0000000000000000000000000000000000000000..2e9789d220ded97f3b053e9c1a461ecd29f9d022 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/KeepStatementVisitor.java @@ -0,0 +1,307 @@ +package org.rosi_project.model_sync.model_join.representation.parser.antlr; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.rosi_project.model_sync.model_join.representation.core.AttributePath; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.core.OCLStatement; +import org.rosi_project.model_sync.model_join.representation.core.RelativeAttribute; +import org.rosi_project.model_sync.model_join.representation.core.TypedAttributePath; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepAggregateExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepAggregateExpression.AggregationType; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepAttributesExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepCalculatedExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepReferenceExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepSubTypeExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepSuperTypeExpression; +import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinBaseVisitor; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.AttrresContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepaggregatesexprContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepattributesexprContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepcalculatedexprContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepexprContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepincomingexprContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepoutgoingexprContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepsubtypeexprContext; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepsupertypeexprContext; + +/** + * The {@code KeepStatementVisitor} creates {@link KeepExpression} instances from a parsed {@code + * ModelJoin} file. + * + * @author Rico Bergmann + */ +public class KeepStatementVisitor extends ModelJoinBaseVisitor<KeepExpression> { + + /** + * A simple enumeration of the possible keep statements for compound statements as defined in the + * {@code ModelJoin} ANTLR grammar. + */ + private enum KeepExprType { + + /** + * The {@code KEEP_TYPE_EXPR} subsumes both {@code keep subtype} as well as {@code keep + * supertype} expressions. + */ + KEEP_TYPE_EXPR, + + /** + * The {@code KEEP_INCOMING_EXPR} corresponds to a {@code keep incoming reference} statement. + */ + KEEP_INCOMING_EXPR, + + /** + * The {@code KEEP_OUTGOING_EXPR} corresponds to a {@code keep outgoing reference} statement. + */ + KEEP_OUTGOING_EXPR + + } + + @Override + public KeepExpression visitKeepattributesexpr(KeepattributesexprContext ctx) { + return new KeepAttributesExpression( + ctx.attrres().stream() + .map(AttrresContext::getText) + .map(AttributePath::fromQualifiedPath) + .collect(ArrayList::new, ArrayList::add, ArrayList::addAll) + ); + + } + + @Override + public KeepExpression visitKeepaggregatesexpr(KeepaggregatesexprContext ctx) { + String rawAggregationType = ctx.aggrtype().getText(); + AggregationType aggregationType = AggregationType.valueOf(rawAggregationType.toUpperCase()); + + return new KeepAggregateExpression( + aggregationType /* SUM / MAX / MIN etc */, + RelativeAttribute.of(ctx.relattr().getText()) /* the attribute in the target relation */, + AttributePath.fromQualifiedPath(ctx.classres().getText()) /* the source attribute */, + AttributePath.fromQualifiedPath(ctx.attrres().getText()) /* name in the new ModelJoin */ + ); + } + + @Override + public KeepExpression visitKeepcalculatedexpr(KeepcalculatedexprContext ctx) { + OCLStatement oclExpr = OCLStatement.of(ctx.oclcond().getText()); + + return KeepCalculatedExpression // + .keepCalculatedAttribute(oclExpr) // + .as(TypedAttributePath.fromQualifiedTypedPath(ctx.typedattrres().getText()) + ); + } + + @Override + public KeepExpression visitKeepexpr(KeepexprContext ctx) { + KeepExprType keepExprType = determineExpressionTypeFor(ctx); + + switch (keepExprType) { + case KEEP_TYPE_EXPR: + return buildKeepTypeExpressionFor(ctx); + case KEEP_INCOMING_EXPR: + return buildKeepIncomingReferenceExpressionFor(ctx); + case KEEP_OUTGOING_EXPR: + return buildKeepOutgoingReferenceExpressionFor(ctx); + default: + throw new AssertionError(keepExprType); + } + } + + /** + * Checks, which kind of expression is specified in a specific {@code KeepexprContext}. + */ + private KeepExprType determineExpressionTypeFor(KeepexprContext ctx) { + + /* + * From the ANTLR grammar we know that exactly one of the IF cases has to hold + */ + if (ctx.keeptypeexpr() != null) { + return KeepExprType.KEEP_TYPE_EXPR; + } else if (ctx.keepincomingexpr() != null) { + return KeepExprType.KEEP_INCOMING_EXPR; + } else if (ctx.keepoutgoingexpr() != null) { + return KeepExprType.KEEP_OUTGOING_EXPR; + } else { + throw new ModelJoinParsingException( + "Expected either 'keep reference' or 'keep type' expression: " + ctx); + } + } + + /** + * Creates the appropriate {@link KeepExpression} for a {@code KeepexprContext}. + */ + private KeepExpression buildKeepTypeExpressionFor(KeepexprContext ctx) { + if (ctx.keeptypeexpr().keepsubtypeexpr() != null) { + return buildKeepSubtypeExpressionFor(ctx); + } else if (ctx.keeptypeexpr().keepsupertypeexpr() != null) { + return buildKeepSupertypeExpressionFor(ctx); + } else { + throw new ModelJoinParsingException( + "Expected either 'keep supertype' or 'keep subtype' expression: " + ctx); + } + } + + /** + * Creates the {@link KeepReferenceExpression} instance contained in a {@code KeepexprContext}. + * This method assumes that the provided {@code ctx} indeed contains a keep incoming reference + * statement. + */ + private KeepReferenceExpression buildKeepIncomingReferenceExpressionFor(KeepexprContext ctx) { + KeepincomingexprContext incomingCtx = ctx.keepincomingexpr(); + + AttributePath attributeToKeep = AttributePath + .fromQualifiedPath(incomingCtx.attrres().getText()); + + List<KeepExpression> nestedExpressions = ctx.keepattributesexpr().stream() + .map(this::visitKeepattributesexpr) + .collect(Collectors.toList()); + + nestedExpressions.addAll(ctx.keepaggregatesexpr().stream() + .map(this::visitKeepaggregatesexpr) + .collect(Collectors.toList()) + ); + + nestedExpressions.addAll(ctx.keepexpr().stream() + .map(this::visitKeepexpr) + .collect(Collectors.toList()) + ); + + KeepReferenceExpression.KeepReferenceBuilder expressionBuilder = KeepReferenceExpression.keep() + .incoming(attributeToKeep); + + ClassResource targetClass; + if (incomingCtx.classres() != null) { + targetClass = ClassResource.fromQualifiedName(incomingCtx.classres().getText()); + } else { + targetClass = attributeToKeep.getContainingClass(); + } + + expressionBuilder.as(targetClass); + nestedExpressions.forEach(expressionBuilder::keep); + + return expressionBuilder.buildExpression(); + } + + /** + * Creates the {@link KeepReferenceExpression} instance contained in a {@code KeepexprContext}. + * This method assumes that the provided {@code ctx} indeed contains a keep outgoing reference + * statement. + */ + private KeepReferenceExpression buildKeepOutgoingReferenceExpressionFor(KeepexprContext ctx) { + KeepoutgoingexprContext outgoingCtx = ctx.keepoutgoingexpr(); + + AttributePath attributeToKeep = AttributePath + .fromQualifiedPath(outgoingCtx.attrres().getText()); + + List<KeepExpression> nestedExpressions = ctx.keepattributesexpr().stream() + .map(this::visitKeepattributesexpr) + .collect(Collectors.toList()); + + nestedExpressions.addAll(ctx.keepaggregatesexpr().stream() + .map(this::visitKeepaggregatesexpr) + .collect(Collectors.toList()) + ); + + nestedExpressions.addAll(ctx.keepexpr().stream() + .map(this::visitKeepexpr) + .collect(Collectors.toList()) + ); + + KeepReferenceExpression.KeepReferenceBuilder expressionBuilder = KeepReferenceExpression.keep() + .outgoing(attributeToKeep); + + ClassResource targetClass; + if (outgoingCtx.classres() != null) { + targetClass = ClassResource.fromQualifiedName(outgoingCtx.classres().getText()); + } else { + targetClass = attributeToKeep.getContainingClass(); + } + + expressionBuilder.as(targetClass); + nestedExpressions.forEach(expressionBuilder::keep); + + return expressionBuilder.buildExpression(); + } + + /** + * Creates the {@link KeepSubTypeExpression} instance contained in a {@code KeepexprContext}. This + * method assumes that the provided {@code ctx} indeed contains a keep subtype statement. + */ + private KeepSubTypeExpression buildKeepSubtypeExpressionFor(KeepexprContext ctx) { + KeepsubtypeexprContext subtypeCtx = ctx.keeptypeexpr().keepsubtypeexpr(); + + ClassResource typeToKeep = ClassResource.fromQualifiedName(subtypeCtx.classres(0).getText()); + + List<KeepExpression> nestedExpressions = ctx.keepattributesexpr().stream() + .map(this::visitKeepattributesexpr) + .collect(Collectors.toList()); + + nestedExpressions.addAll(ctx.keepaggregatesexpr().stream() + .map(this::visitKeepaggregatesexpr) + .collect(Collectors.toList()) + ); + + nestedExpressions.addAll(ctx.keepexpr().stream() + .map(this::visitKeepexpr) + .collect(Collectors.toList()) + ); + + KeepSubTypeExpression.KeepSubTypeBuilder expressionBuilder = KeepSubTypeExpression.keep() + .subtype(typeToKeep); + + ClassResource targetClass; + if (subtypeCtx.classres() != null) { + targetClass = ClassResource.fromQualifiedName(subtypeCtx.classres(1).getText()); + } else { + targetClass = typeToKeep; + } + + expressionBuilder.as(targetClass); + nestedExpressions.forEach(expressionBuilder::keep); + + return expressionBuilder.buildExpression(); + } + + /** + * Creates the {@link KeepSuperTypeExpression} instance contained in a {@code KeepexprContext}. + * This method assumes that the provided {@code ctx} indeed contains a keep supertype statement. + */ + private KeepSuperTypeExpression buildKeepSupertypeExpressionFor(KeepexprContext ctx) { + KeepsupertypeexprContext supertypeCtx = ctx.keeptypeexpr().keepsupertypeexpr(); + + ClassResource typeToKeep = ClassResource.fromQualifiedName(supertypeCtx.classres(0).getText()); + + List<KeepExpression> nestedExpressions = ctx.keepattributesexpr().stream() + .map(this::visitKeepattributesexpr) + .collect(Collectors.toList()); + + nestedExpressions.addAll(ctx.keepaggregatesexpr().stream() + .map(this::visitKeepaggregatesexpr) + .collect(Collectors.toList()) + ); + + nestedExpressions.addAll(ctx.keepexpr().stream() + .map(this::visitKeepexpr) + .collect(Collectors.toList()) + ); + + KeepSuperTypeExpression.KeepSuperTypeBuilder expressionBuilder = KeepSuperTypeExpression.keep() + .supertype(typeToKeep); + + ClassResource targetClass; + if (supertypeCtx.classres() != null) { + targetClass = ClassResource.fromQualifiedName(supertypeCtx.classres(1).getText()); + } else { + targetClass = typeToKeep; + } + + expressionBuilder.as(targetClass); + nestedExpressions.forEach(expressionBuilder::keep); + + return expressionBuilder.buildExpression(); + } + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/ModelJoinVisitor.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/ModelJoinVisitor.java new file mode 100644 index 0000000000000000000000000000000000000000..f1883fa77954a74ed677d8a5b6cc358ca2721946 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/ModelJoinVisitor.java @@ -0,0 +1,28 @@ +package org.rosi_project.model_sync.model_join.representation.parser.antlr; + +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinBaseVisitor; +import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.ModeljoinContext; +import org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder; + +/** + * The {@code ModelJoinVisitor} creates a {@link ModelJoinExpression} instances from a parsed {@code + * ModelJoin} file. + * + * @author Rico Bergmann + */ +public class ModelJoinVisitor extends ModelJoinBaseVisitor<ModelJoinExpression> { + + @Override + public ModelJoinExpression visitModeljoin(ModeljoinContext ctx) { + JoinStatementVisitor joinVisitor = new JoinStatementVisitor(); + ModelJoinBuilder modelJoinBuilder = ModelJoinBuilder.createNewModelJoin(); + + ctx.join().stream() // + .map(joinVisitor::visitJoin) // + .forEach(modelJoinBuilder::add); + + return modelJoinBuilder.build(); + } + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/generated/.gitignore b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/generated/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..6942af6691a6f3699d09ce0f2974327d155b37a7 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/generated/.gitignore @@ -0,0 +1,8 @@ +/ModelJoin.tokens +/ModelJoinBaseListener.java +/ModelJoinBaseVisitor.java +/ModelJoinLexer.java +/ModelJoinLexer.tokens +/ModelJoinListener.java +/ModelJoinParser.java +/ModelJoinVisitor.java diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/DefaultModelJoinParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/DefaultModelJoinParser.java new file mode 100644 index 0000000000000000000000000000000000000000..25f4b9ce3871862e570befcb4f28ad761f467345 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/DefaultModelJoinParser.java @@ -0,0 +1,187 @@ +package org.rosi_project.model_sync.model_join.representation.parser.legacy; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Optional; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; +import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParser; +import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException; +import org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder; + +/** + * The {@code DefaultModelJoinParser} reads a {@code ModelJoin} file from disk and creates matching + * representation for it. + * + * @author Rico Bergmann + */ +@Deprecated +public class DefaultModelJoinParser { + + /** + * Converts the {@code ModelJoin} file into a {@link ModelJoinExpression}, wrapped into an {@code + * Optional}. If parsing fails, an empty {@code Optional} will be returned. + */ + @Nonnull + public static Optional<ModelJoinExpression> read(@Nonnull File modelFile) { + DefaultModelJoinParser parser = new DefaultModelJoinParser(modelFile, ErrorReportingStrategy.OPTIONAL); + return parser.run(); + } + + /** + * Converts the {@code ModelJoin} file into a {@link ModelJoinExpression}. If parsing fails, a + * {@link ModelJoinParsingException} will be thrown. + * + * @throws ModelJoinParsingException if the model may not be parsed for some reason. + */ + @Nonnull + public static ModelJoinExpression readOrThrow(@Nonnull File modelFile) { + DefaultModelJoinParser parser = new DefaultModelJoinParser(modelFile, ErrorReportingStrategy.EXCEPTION); + Optional<ModelJoinExpression> result = parser.run(); + + if (result.isPresent()) { + return result.get(); + } else { + /* + * Theoretically, the parser should throw the exception by itself, so this code should never + * actually run. It is merely here for safety purposes (unchecked access of Optional.get()) + */ + throw new ModelJoinParsingException("Result not present but no exception was thrown either"); + } + } + + /** + * Indicates what should happen if parsing fails. + */ + enum ErrorReportingStrategy { + + /** + * On failure an empty {@code Optional} should be returned. + */ + OPTIONAL, + + /** + * On failure an {@link ModelJoinParsingException} should be thrown. + */ + EXCEPTION + } + + private static final Pattern EMPTY_LINE = Pattern.compile("^\\s*$"); + private static final Pattern IMPORT_STATEMENT = Pattern.compile("^import .*"); + private static final Pattern TARGET_STATEMENT = Pattern.compile("^target .*"); + private static final Pattern JOIN_STATEMENT = Pattern + .compile("^(((left|right) outer)|theta|natural) join .*"); + + @Nonnull + private final File modelFile; + + @Nonnull + private final ErrorReportingStrategy errorReportingStrategy; + + /** + * Full constructor. + * + * @param modelFile the file to parse + * @param errorReportingStrategy what to do in case an error occurs. + */ + private DefaultModelJoinParser(@Nonnull File modelFile, + @Nonnull ErrorReportingStrategy errorReportingStrategy) { + this.modelFile = modelFile; + this.errorReportingStrategy = errorReportingStrategy; + } + + /** + * Creates the {@code ModelJoin} representation from the {@code modelFile}. + * <p> + * Depending on the selected {@code errorReportingStrategy} either an Exception will be thrown, or + * an empty {@code Optional} will be returned if something goes wrong. + */ + private Optional<ModelJoinExpression> run() { + ModelJoinExpression resultingModel; + try (FileInputStream modelInputStream = new FileInputStream(modelFile); + InputStreamReader modelInputReader = new InputStreamReader(modelInputStream); + BufferedReader bufferedModelReader = new BufferedReader(modelInputReader)) { + + ModelJoinBuilder modelJoinBuilder = ModelJoinBuilder.createNewModelJoin(); + readModelFileAndPopulateBuilder(bufferedModelReader, modelJoinBuilder); + resultingModel = modelJoinBuilder.build(); + + } catch (ModelJoinParsingException e) { + switch (errorReportingStrategy) { + case OPTIONAL: + return Optional.empty(); + case EXCEPTION: + throw e; + default: + throw new AssertionError(errorReportingStrategy); + } + } catch (IOException e) { + switch (errorReportingStrategy) { + case OPTIONAL: + return Optional.empty(); + case EXCEPTION: + throw new ModelJoinParsingException(e); + default: + throw new AssertionError(errorReportingStrategy); + } + } + return Optional.of(resultingModel); + } + + /** + * Reads the {@code modelFile} and constructs the corresponding {@code ModelJoin} on the fly. + * <p> + * On error, an {@code IOException} or an {@link ModelJoinParsingException} will be thrown. + * + * @throws ModelJoinParsingException if the model file's content are malformed or another + * component failed + * @throws IOException if the model file might not be read any further + */ + private void readModelFileAndPopulateBuilder(BufferedReader modelReader, + ModelJoinBuilder modelBuilder) throws IOException { + String currentLine; + while ((currentLine = modelReader.readLine()) != null) { + if (lineShouldBeSkipped(currentLine)) { + // the line should be skipped. So we do nothing. + continue; + } else if (lineStartsJoinDeclaration(currentLine)) { + // a new join is being declared. Delegate to the join parser to handle the rest. + JoinParser joinParser = new JoinParser(currentLine, modelReader); + modelBuilder.add(joinParser.run()); + } else { + // we do not know what to do with the current line. Should abort. + throw new ModelJoinParsingException("Unexpected line: '" + currentLine + "'"); + } + } + } + + /** + * Checks, whether the parser should consider the given {@code line} any further, or just ignore + * it. + * <p> + * A line should be skipped, iff. + * <ul> + * <li>It only consists of whitespace characters</li> + * <li>It is an {@code import statement} (as those are not yet considered in the current {@code + * ModelJoin} abstraction)</li> + * <li>It is a {@code target statement} (as those are not yet considered in the current {@code + * ModelJoin} abstraction)</li> + * </ul> + */ + private boolean lineShouldBeSkipped(String line) { + return EMPTY_LINE.matcher(line).matches() // + || IMPORT_STATEMENT.matcher(line).matches() // + || TARGET_STATEMENT.matcher(line).matches(); + } + + /** + * Checks, whether the given {@code line} is the beginning of a {@code join statement}. + */ + private boolean lineStartsJoinDeclaration(String line) { + return JOIN_STATEMENT.matcher(line).matches(); + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/JoinParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/JoinParser.java new file mode 100644 index 0000000000000000000000000000000000000000..ee32b6167aa20c1785f1671af0ac52f1a2c46b75 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/JoinParser.java @@ -0,0 +1,254 @@ +package org.rosi_project.model_sync.model_join.representation.parser.legacy; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.core.OCLConstraint; +import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepExpression; +import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException; +import org.rosi_project.model_sync.model_join.representation.util.JoinFactory; +import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.NaturalJoinBuilder; +import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.OuterJoinBuilder; +import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.ThetaJoinBuilder; + +/** + * The {@code JoinParser} takes care of reading an individual {@code join statement} and creates the + * corresponding {@link JoinExpression}. + * <p> + * This class is primarily intended to be created and invoked by the {@link DefaultModelJoinParser} only as + * will modify the state of that as well. + * + * @author Rico Bergmann + */ +@Deprecated +class JoinParser { + + private static final String START_OF_JOIN_BLOCK_DELIMITER = "{"; + private static final String JOIN_TYPE_THETA = "theta"; + private static final Pattern JOIN_TYPE_DECLARATION = Pattern + .compile(".*(?<type>((?<outertype>(right |left )outer)|theta|natural)) join .*"); + private static final Pattern LEFT_SOURCE_DECLARATION = Pattern + .compile(".*join (?<leftsource>[\\w\\\\.]*) .*"); + private static final Pattern RIGHT_SOURCE_DECLARATION = Pattern + .compile(".*with (?<rightsource>[\\w\\\\.]*) .*"); + private static final Pattern TARGET_DECLARATION = Pattern + .compile(".*as (?<target>[\\w\\\\.]*)( .*|\\{)"); + private static final Pattern THETA_PREDICATE_DECLARATION = Pattern + .compile(".*where (?<predicate>[\\w\\\\.]*)\\{"); + + @Nonnull + private final String startingLine; + + @Nonnull + private final BufferedReader modelReader; + + private boolean hasReadStartOfJoinBlockCharacter = false; + + /** + * Full constructor. + * + * @param startingLine the line that was last read by the {@link DefaultModelJoinParser} and should + * therefore become the starting point for {@code this} parser. + * @param modelReader the reader that provides access to the following lines in the {@code + * ModelJoin} file currently being read. + */ + JoinParser(@Nonnull String startingLine, @Nonnull BufferedReader modelReader) { + this.startingLine = startingLine; + this.modelReader = modelReader; + } + + /** + * Creates the {@link JoinExpression} from the current {@code join statement}. + * <p> + * If something goes wrong, either an {@code IOException} will be thrown directly or a {@link + * ModelJoinParsingException} is used. + */ + @Nonnull + public JoinExpression run() throws IOException { + + // first, we will determine the Join type + + Matcher joinTypeDeclarationMatcher = JOIN_TYPE_DECLARATION.matcher(startingLine); + + if (!joinTypeDeclarationMatcher.matches()) { + throw new ModelJoinParsingException( + "Expected beginning of a Join declaration on '" + startingLine + "'"); + } + + String joinType = joinTypeDeclarationMatcher.group("type"); + String outerJoinType = ""; + + if (joinType.equals("outer")) { + outerJoinType = joinTypeDeclarationMatcher.group("outertype"); + } + + // second, we will try to extract all necessary info from the first line + + String leftSource = null; + String rightSource = null; + String target = null; + String thetaPredicate = null; + + Matcher leftSourceMatcher = LEFT_SOURCE_DECLARATION.matcher(startingLine); + Matcher rightSourceMatcher = RIGHT_SOURCE_DECLARATION.matcher(startingLine); + Matcher targetMatcher = TARGET_DECLARATION.matcher(startingLine); + Matcher thetaPredicateMatcher = THETA_PREDICATE_DECLARATION.matcher(startingLine); + + final int necessaryComponents; + int foundComponents = 0; + + if (leftSourceMatcher.matches()) { + leftSource = leftSourceMatcher.group("leftsource"); + foundComponents++; + } + + if (rightSourceMatcher.matches()) { + rightSource = rightSourceMatcher.group("rightsource"); + foundComponents++; + } + + if (targetMatcher.matches()) { + target = targetMatcher.group("target"); + foundComponents++; + } + + if (joinType.equals(JOIN_TYPE_THETA)) { + necessaryComponents = 4; + if (thetaPredicateMatcher.matches()) { + thetaPredicate = thetaPredicateMatcher.group("predicate"); + foundComponents++; + } + } else { + necessaryComponents = 3; + } + + // in case all information is present on the first line, we are done already + + if (startingLine.contains(START_OF_JOIN_BLOCK_DELIMITER)) { + // TODO could the start character be part of an OCL statement? + hasReadStartOfJoinBlockCharacter = true; + } + + // otherwise, we need to read the following lines as well + + String currentLine = ""; + while (!hasReadStartOfJoinBlockCharacter // + && foundComponents < necessaryComponents // + && (currentLine = modelReader.readLine()) != null) { + + // again, we try to extract as much as possible of the info we need + // however, we now also have to check, whether we have already specified a component, as well + + leftSourceMatcher = LEFT_SOURCE_DECLARATION.matcher(currentLine); + if (leftSource == null && leftSourceMatcher.matches()) { + leftSource = leftSourceMatcher.group("leftsource"); + foundComponents++; + } + + rightSourceMatcher = RIGHT_SOURCE_DECLARATION.matcher(currentLine); + if (rightSource == null && rightSourceMatcher.matches()) { + rightSource = rightSourceMatcher.group("rightsource"); + foundComponents++; + } + + targetMatcher = TARGET_DECLARATION.matcher(currentLine); + if (target == null && targetMatcher.matches()) { + target = targetMatcher.group("target"); + } + + if (joinType.equals(JOIN_TYPE_THETA)) { + thetaPredicateMatcher = THETA_PREDICATE_DECLARATION.matcher(currentLine); + if (thetaPredicate == null && thetaPredicateMatcher.matches()) { + thetaPredicate = thetaPredicateMatcher.group("predicate"); + foundComponents++; + } + } + + if (currentLine.contains(START_OF_JOIN_BLOCK_DELIMITER)) { + hasReadStartOfJoinBlockCharacter = true; + } + } + + // if we found the beginning of the Join body, but did not read all our information, the + // ModelJoin file is flawed + + if (foundComponents < necessaryComponents) { + throw new ModelJoinParsingException( + "ModelJoin grammar violation: found components " + foundComponents + " but expected " + + necessaryComponents); + } + + // otherwise we are ready to go. So we skip forward until the Join body is reached. + + while (!hasReadStartOfJoinBlockCharacter && (currentLine = modelReader.readLine()) != null) { + if (currentLine.contains(START_OF_JOIN_BLOCK_DELIMITER)) { + hasReadStartOfJoinBlockCharacter = true; + } + } + + // if we read everything, but did not find the Join body, the ModelJoin file is flawed again + + if (!hasReadStartOfJoinBlockCharacter) { + throw new ModelJoinParsingException( + "Expected start of join block but could not read any further"); + } + + // if not all components are initialized by now, we are working on a flawed ModelJoin file + + if (currentLine == null || leftSource == null || rightSource == null || target == null) { + throw new ModelJoinParsingException( + "Not all required information was found but could not read any further"); + } + + // at this point the declaration of the join statement is valid + // therefore we want the keep expressions it contains. This is the KeepParser's job. + + KeepParser keepParser = new KeepParser(currentLine, modelReader); + Collection<KeepExpression> keeps = keepParser.run(); + + // finally we have to create the correct JoinExpression + + JoinFactory joinFactory = JoinFactory.createNew(); + switch (joinType) { + case "outer": + OuterJoinBuilder outerJoinBuilder = joinFactory.outer(); + if (outerJoinType.equals("left")) { + outerJoinBuilder.leftOuter(); + } else if (outerJoinType.equals("right")) { + outerJoinBuilder.rightOuter(); + } else { + throw new ModelJoinParsingException("Unknown direction for outer join: " + outerJoinType); + } + outerJoinBuilder // + .join(ClassResource.fromQualifiedName(leftSource)) // + .with(ClassResource.fromQualifiedName(rightSource)) // + .as(ClassResource.fromQualifiedName(target)); + keeps.forEach(outerJoinBuilder::keep); + return outerJoinBuilder.done(); + case "theta": + ThetaJoinBuilder thetaJoinBuilder = joinFactory // + .theta() // + .join(ClassResource.fromQualifiedName(leftSource)) // + .with(ClassResource.fromQualifiedName(rightSource)) // + .as(ClassResource.fromQualifiedName(target)) // + .where(OCLConstraint.of(thetaPredicate)); + keeps.forEach(thetaJoinBuilder::keep); + return thetaJoinBuilder.done(); + case "natural": + NaturalJoinBuilder naturalJoinBuilder = joinFactory // + .natural() // + .join(ClassResource.fromQualifiedName(leftSource)) // + .with(ClassResource.fromQualifiedName(rightSource)) // + .as(ClassResource.fromQualifiedName(target)); + keeps.forEach(naturalJoinBuilder::keep); + return naturalJoinBuilder.done(); + default: + throw new ModelJoinParsingException("Unknown join type: " + joinType); + } + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/KeepParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/KeepParser.java new file mode 100644 index 0000000000000000000000000000000000000000..ac82f383b79ed8a38b3a7590daf4bbe5843d4d99 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/KeepParser.java @@ -0,0 +1,98 @@ +package org.rosi_project.model_sync.model_join.representation.parser.legacy; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.AttributePath; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepAttributesExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepExpression; +import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException; + +/** + * The {@code KeepParser} takes care of reading the {@code keep statements} and creating the + * corresponding {@link KeepExpression}s for the body of an {@code join statement}. + * <p> + * This class is primarily intended to be created and invoked by the {@link JoinParser} only as will + * modify the state of that as well. + * + * @author Rico Bergmann + */ +@Deprecated +class KeepParser { + + private static final String END_OF_JOIN_BLOCK_DELIMITER = "}"; + private static final Pattern KEEP_ATTRIBUTE_DECLARATION = Pattern + .compile(".*keep attributes\\W*(?<attribute>[\\w\\\\.]*)$"); + + @Nonnull + private final String startingLine; + + @Nonnull + private final BufferedReader modelReader; + + private boolean hasReadEndOfJoinBlockDelimiter = false; + + /** + * Full constructor. + * @param startingLine the line that was last read by the {@link JoinParser} and should + * therefore become the starting point for {@code this} parser. + * @param modelReader the reader that provides access to the following lines in the {@code + * ModelJoin} file currently being read. + */ + KeepParser(@Nonnull String startingLine, @Nonnull BufferedReader modelReader) { + this.startingLine = startingLine; + this.modelReader = modelReader; + } + + /** + * Creates the {@link KeepExpression}s for the current {@code join statement}. + * <p> + * If something goes wrong, either an {@code IOException} will be thrown directly or a {@link + * ModelJoinParsingException} is used. + */ + @Nonnull + public Collection<KeepExpression> run() throws IOException { + List<KeepExpression> detectedKeeps = new LinkedList<>(); + Matcher keepMatcher = KEEP_ATTRIBUTE_DECLARATION.matcher(startingLine); + + parseLine(startingLine, keepMatcher).ifPresent(detectedKeeps::add); + + String currentLine; + while (!hasReadEndOfJoinBlockDelimiter && (currentLine = modelReader.readLine()) != null) { + keepMatcher = KEEP_ATTRIBUTE_DECLARATION.matcher(currentLine); + parseLine(currentLine, keepMatcher).ifPresent(detectedKeeps::add); + } + + return detectedKeeps; + } + + /** + * Tries to create a {@link KeepExpression} for a single {@code keep statement} written on the + * {@code current line}. + * <p> + * If there is no such statement, an empty {@code Optional} will be returned. + * <p> + * If the line indicates the end of the keep-block, the state of {@code this} parser will be + * updated accordingly. + */ + @Nonnull + private Optional<KeepExpression> parseLine(String currentLine, Matcher keepMatcher) { + Optional<KeepExpression> result = Optional.empty(); + if (keepMatcher.matches()) { + AttributePath attribute = AttributePath.fromQualifiedPath(keepMatcher.group("attribute")); + result = Optional.of(KeepAttributesExpression.keepAttributes(attribute)); + } + + if (currentLine.contains(END_OF_JOIN_BLOCK_DELIMITER)) { + hasReadEndOfJoinBlockDelimiter = true; + } + return result; + } + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Assert.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Assert.java new file mode 100644 index 0000000000000000000000000000000000000000..d72ecca9b380b86828c5134808cef2e48bc46cd8 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Assert.java @@ -0,0 +1,36 @@ +package org.rosi_project.model_sync.model_join.representation.util; + +import javax.annotation.Nonnull; + +/** + * Utility class to provide a number of useful assertion mechanisms. + * <p> + * On failure, an {@code AssertionError} with a dedicated message will be thrown. + * + * @author Rico Bergmann + */ +public class Assert { + + /** + * Ensures that an object is not {@code null}. + */ + public static void notNull(Object obj, @Nonnull String failureMessage) { + if (obj == null) { + throw new AssertionError(failureMessage); + } + } + + /** + * Ensures that none of the arguments is {@code null}. + */ + public static void noNullArguments(@Nonnull String failureMessage, Object... arguments) { + if (arguments == null) { + throw new AssertionError("Vararg arguments may not be null"); + } + for (Object arg : arguments) { + if (arg == null) { + throw new AssertionError(failureMessage); + } + } + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Functional.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Functional.java new file mode 100644 index 0000000000000000000000000000000000000000..853ee96122a43dfe41d4269dcaf6feb592b284d9 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Functional.java @@ -0,0 +1,235 @@ +package org.rosi_project.model_sync.model_join.representation.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.swing.text.html.Option; + +/** + * Contains a number of useful classes to mimic behaviour from functional programming languages in + * Java. + * + * @author Rico Bergmann + */ +public class Functional { + + /** + * Emulates a dispatch on the concrete type of an object. Depending on the type, different control + * paths will be executed. + * + * @param something the object to match on + */ + @Nonnull + public static <R> FunctionalMatch<R> match(@Nonnull Object something) { + return new FunctionalMatch<>(something); + } + + /** + * + * @author Rico Bergmann + * + * @param <R> + */ + public static class FunctionalMatch<R> extends Functional { + + @Nonnull + private final Object matchInstance; + + @Nonnull + private final List<Pair<Class<?>, CaseResult<R>>> functions; + + @Nullable + private CaseResult<R> defaultAction; + + /** + * + * @param matchInstance + */ + private FunctionalMatch(@Nonnull Object matchInstance) { + Assert.notNull(matchInstance, "Object to match on may not be null"); + this.matchInstance = matchInstance; + this.functions = new ArrayList<>(); + } + + /** + * @param clazz + * @param action + * @return + */ + @SuppressWarnings("unchecked") + public <T> FunctionalMatch<R> caseOf(@Nonnull Class<T> clazz, @Nonnull Function<T, R> action) { + Assert.notNull(clazz, "Class to match on may not be null"); + Assert.notNull(action, "Action to perform may not be null"); + functions.add( + Pair.of(clazz, new RunnableCaseResult<>((Function<Object, R>) action, matchInstance))); + return this; + } + + /** + * @param clazz + * @param result + * @return + */ + public <T> FunctionalMatch<R> caseOf(@Nonnull Class<T> clazz, @Nonnull R result) { + Assert.notNull(clazz, "Class to match on may not be null"); + Assert.notNull(result, "Match result may not be null"); + functions.add(Pair.of(clazz, new StaticCaseResult<>(result))); + return this; + } + + /** + * @param action + * @return + */ + public FunctionalMatch<R> defaultCase(@Nonnull Function<? super Object, R> action) { + Assert.notNull(action, "Default action may not be null"); + this.defaultAction = new RunnableCaseResult<>(action, matchInstance); + return this; + } + + /** + * @param result + * @return + */ + public FunctionalMatch<R> defaultCase(R result) { + Assert.notNull(result, "Match result may not be null"); + this.defaultAction = new StaticCaseResult<>(result); + return this; + } + + /** + * Performs the match. + * <p> + * This will check for the first case to which the match object is assignable and execute the + * associated action. + * + * @return the result of the action or {@code null} if no matching case was specified. + */ + public R run() { + try { + R res = runOrThrow(); + return res; + } catch (MatchException e) { + return null; + } + } + + public Optional<R> tryRun() { + try { + R res = runOrThrow(); + return Optional.of(res); + } catch (MatchException e) { + return Optional.empty(); + } + } + + /** + * Attempts to perform the match. + * <p> + * This will check for the first case to which the match object is assignable and execute the + * associated action. If no matching action was specified, an exception will be raised. + * + * @return the result of the action or {@code null} if no matching case was specified. + * @throws MatchException if no matching action was given. + */ + public R runOrThrow() { + for (Pair<Class<?>, CaseResult<R>> caseEntry : functions) { + Class<?> entryClass = caseEntry.getFirst(); + if (entryClass.isAssignableFrom(matchInstance.getClass())) { + return caseEntry.getSecond().calculate(); + } + } + if (defaultAction != null) { + return defaultAction.calculate(); + } + throw new MatchException(matchInstance.getClass(), "No case specified"); + } + + private static abstract class CaseResult<R> { + + abstract R calculate(); + + } + + private static class RunnableCaseResult<R> extends CaseResult<R> { + + private final Function<Object, R> action; + private final Object param; + + /** + * @param action + * @param param + */ + RunnableCaseResult(@Nonnull Function<Object, R> action, @Nonnull Object param) { + Assert.notNull(action, "Action may not be null"); + Assert.notNull(param, "Param may not be null"); + this.action = action; + this.param = param; + } + + /* + * (non-Javadoc) + * + * @see de.naju.adebar.util.Functional.FunctionalMatch.CaseResult#calculate() + */ + @Override + R calculate() { + return action.apply(param); + } + + } + + private static class StaticCaseResult<R> extends CaseResult<R> { + + private final R result; + + /** + * @param result + */ + public StaticCaseResult(R result) { + this.result = result; + } + + /* + * (non-Javadoc) + * + * @see de.naju.adebar.util.Functional.FunctionalMatch.CaseResult#calculate() + */ + @Override + R calculate() { + return result; + } + + } + + } + + public static class MatchException extends RuntimeException { + + private static final long serialVersionUID = -3903882205980399140L; + + private final Class<?> actualClass; + + MatchException(Class<?> actualClass) { + this.actualClass = actualClass; + } + + MatchException(Class<?> actualClass, String message) { + super(message); + this.actualClass = actualClass; + } + + /** + * @return the actualClass + */ + public Class<?> getActualClass() { + return actualClass; + } + + } + + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/JoinFactory.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/JoinFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..bd873738be32d3169868662f1f048e01480db08e --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/JoinFactory.java @@ -0,0 +1,302 @@ +package org.rosi_project.model_sync.model_join.representation.util; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.OCLConstraint; +import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.NaturalJoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.OuterJoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.OuterJoinExpression.JoinDirection; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.grammar.ThetaJoinExpression; + +/** + * The {@code JoinFactory} should be used as the main facility to create new {@link JoinExpression} + * instances. It supports instantiation for all subclasses and provides a fluid and natural + * interface to set them up. + * + * @author Rico Bergmann + */ +public class JoinFactory { + + /** + * Starts the construction process for a new join. + */ + @Nonnull + public static JoinFactory createNew() { + return new JoinFactory(); + } + + private JoinFactory() { + // pass + } + + /** + * Constructs a new {@link OuterJoinExpression outer join} + */ + @Nonnull + public OuterJoinBuilder outer() { + return new OuterJoinBuilder(); + } + + /** + * Constructs a new {@link NaturalJoinExpression natural join}. + */ + @Nonnull + public NaturalJoinBuilder natural() { + return new NaturalJoinBuilder(); + } + + /** + * Constructs a new {@link ThetaJoinExpression theta join}. + */ + @Nonnull + public ThetaJoinBuilder theta() { + return new ThetaJoinBuilder(); + } + + /** + * This is the base for all actual join builders and contains the fields that have to be present + * in each join. Consult the documentation of the {@link JoinExpression} class for details on + * those fields. + * <p> + * Despite all builders working very similarly, they may not be refactored to have more logic + * provided through this class due to limitations of the Java language. + * + * @see JoinExpression + */ + public static abstract class AbstractJoinBuilder { + protected ClassResource left; + protected ClassResource right; + protected ClassResource target; + protected List<KeepExpression> keeps = new ArrayList<>(); + } + + /** + * The {@code OuterJoinBuilder} takes care of setting up new {@link OuterJoinExpression} + * instances. + * + * @see OuterJoinExpression + */ + public static class OuterJoinBuilder extends AbstractJoinBuilder { + + private OuterJoinExpression.JoinDirection direction; + + /** + * Default constructor. + * <p> + * {@code private} to prevent direct instantiation. + */ + private OuterJoinBuilder() { + // pass + } + + /** + * Configures the join to use the right source model as the primary one. + */ + @Nonnull + public OuterJoinBuilder rightOuter() { + direction = JoinDirection.RIGHT; + return this; + } + + /** + * Configures the join to use the left source model as the primary one. + */ + @Nonnull + public OuterJoinBuilder leftOuter() { + direction = JoinDirection.LEFT; + return this; + } + + /** + * Specifies the left source. + */ + @Nonnull + public OuterJoinBuilder join(@Nonnull ClassResource left) { + this.left = left; + return this; + } + + /** + * Specifies the right source. + */ + @Nonnull + public OuterJoinBuilder with(@Nonnull ClassResource right) { + this.right = right; + return this; + } + + /** + * Specifies the name of the resulting class. + */ + @Nonnull + public OuterJoinBuilder as(@Nonnull ClassResource target) { + this.target = target; + return this; + } + + /** + * Specifies a field that should be contained in the resulting view. + */ + @Nonnull + public OuterJoinBuilder keep(@Nonnull KeepExpression keep) { + this.keeps.add(keep); + return this; + } + + /** + * Finishes the construction process and provides the resulting {@code join}. + */ + @Nonnull + public OuterJoinExpression done() { + return new OuterJoinExpression(left, right, target, direction, keeps); + } + } + + /** + * The {@code NaturalJoinBuilder} takes care of setting up new {@link NaturalJoinExpression} + * instances. + * + * @see NaturalJoinExpression + */ + public static class NaturalJoinBuilder extends AbstractJoinBuilder { + + /** + * Default constructor. + * <p> + * {@code private} to prevent direct instantiation. + */ + private NaturalJoinBuilder() { + // pass + } + + /** + * Specifies the left source. + */ + @Nonnull + public NaturalJoinBuilder join(@Nonnull ClassResource left) { + this.left = left; + return this; + } + + /** + * Specifies the right source. + */ + @Nonnull + public NaturalJoinBuilder with(@Nonnull ClassResource right) { + this.right = right; + return this; + } + + /** + * Specifies the name of the resulting class. + */ + @Nonnull + public NaturalJoinBuilder as(@Nonnull ClassResource target) { + this.target = target; + return this; + } + + /** + * Specifies a field that should be contained in the resulting view. + */ + @Nonnull + public NaturalJoinBuilder keep(@Nonnull KeepExpression keep) { + this.keeps.add(keep); + return this; + } + + /** + * Finishes the construction process and provides the resulting {@code join}. + */ + @Nonnull + public NaturalJoinExpression done() { + return new NaturalJoinExpression(left, right, target, keeps); + } + } + + /** + * The {@code ThetaJoinBuilder} takes care of setting up new {@link ThetaJoinExpression} + * instances. + * + * @see ThetaJoinExpression + */ + public static class ThetaJoinBuilder extends AbstractJoinBuilder { + + private OCLConstraint condition; + + /** + * Default constructor. + * <p> + * {@code private} to prevent direct instantiation. + */ + private ThetaJoinBuilder() { + // pass + } + + /** + * Specifies the left source. + */ + @Nonnull + public ThetaJoinBuilder join(@Nonnull ClassResource left) { + this.left = left; + return this; + } + + /** + * Specifies the right source. + */ + @Nonnull + public ThetaJoinBuilder with(@Nonnull ClassResource right) { + this.right = right; + return this; + } + + /** + * Specifies the name of the resulting class. + */ + @Nonnull + public ThetaJoinBuilder as(@Nonnull ClassResource target) { + this.target = target; + return this; + } + + /** + * Specifies the join condition. + */ + @Nonnull + public ThetaJoinBuilder where(@Nonnull OCLConstraint condition) { + this.condition = condition; + return this; + } + + /** + * Specifies the join condition. + */ + @Nonnull + public ThetaJoinBuilder where(@Nonnull String oclCondition) { + return where(OCLConstraint.of(oclCondition)); + } + + /** + * Specifies a field that should be contained in the resulting view. + */ + @Nonnull + public ThetaJoinBuilder keep(@Nonnull KeepExpression keep) { + this.keeps.add(keep); + return this; + } + + /** + * Finishes the construction process and provides the resulting {@code join}. + */ + @Nonnull + public ThetaJoinExpression done() { + return new ThetaJoinExpression(left, right, target, condition, keeps); + } + } + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/ModelJoinBuilder.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/ModelJoinBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..c92b525cc3cab941a9524c8b396b0de39958d25c --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/ModelJoinBuilder.java @@ -0,0 +1,79 @@ +package org.rosi_project.model_sync.model_join.representation.util; + +import java.util.LinkedList; +import java.util.List; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.core.AttributePath; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepAttributesExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepReferenceExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepReferenceExpression.KeepReferenceBuilder; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepSuperTypeExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepSuperTypeExpression.KeepSuperTypeBuilder; +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; +import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.NaturalJoinBuilder; +import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.ThetaJoinBuilder; + +/** + * The {@code ModelJoinBuilder} provides a nice and fluent way of creating new {@code ModelJoin} + * instances. + * + * @author Rico Bergmann + */ +public class ModelJoinBuilder { + + public static ThetaJoinBuilder thetaJoin() { + return JoinFactory.createNew().theta(); + } + + public static NaturalJoinBuilder naturalJoin() { + return JoinFactory.createNew().natural(); + } + + public static KeepAttributesExpression attributes(AttributePath firstAttribute, AttributePath... moreAttributes) { + return KeepAttributesExpression.keepAttributes(firstAttribute, moreAttributes); + } + + public static KeepReferenceBuilder outgoing(AttributePath attr) { + return KeepReferenceExpression.keep().outgoing(attr); + } + + public static KeepSuperTypeBuilder supertype(ClassResource supertype) { + return KeepSuperTypeExpression.keep().supertype(supertype); + } + + /** + * Starts the construction process for a new {@link ModelJoinExpression}. + */ + @Nonnull + public static ModelJoinBuilder createNewModelJoin() { + return new ModelJoinBuilder(); + } + + private List<JoinExpression> joinBuffer = new LinkedList<>(); + + /** + * Full/default constructor. + */ + private ModelJoinBuilder() { + // pass + } + + /** + * Extends the current {@code ModelJoin} by another join. + */ + @Nonnull + public ModelJoinBuilder add(@Nonnull JoinExpression join) { + joinBuffer.add(join); + return this; + } + + /** + * Finishes the construction process and provides the resulting {@code ModelJoin}. + */ + @Nonnull + public ModelJoinExpression build() { + return new ModelJoinExpression(joinBuffer); + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Nothing.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Nothing.java new file mode 100644 index 0000000000000000000000000000000000000000..6f4ddf4e0686a58f691c788bf2c71a5bcabde1b4 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Nothing.java @@ -0,0 +1,11 @@ +package org.rosi_project.model_sync.model_join.representation.util; + +public final class Nothing { + + public static Nothing __() { + return new Nothing(); + } + + private Nothing() {}; + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Pair.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Pair.java new file mode 100644 index 0000000000000000000000000000000000000000..8cfb469171b91948577ec7a0016ecba646cd7029 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Pair.java @@ -0,0 +1,48 @@ +package org.rosi_project.model_sync.model_join.representation.util; + +import java.util.Objects; + +public class Pair<F, S> { + + public static <F, S> Pair<F, S> of(F first, S second) { + return new Pair<F, S>(first, second); + } + + private final F first; + private final S second; + + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + public F getFirst() { + return first; + } + + public S getSecond() { + return second; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Pair)) { + return false; + } + Pair<?, ?> pair = (Pair<?, ?>) o; + return Objects.equals(first, pair.first) && + Objects.equals(second, pair.second); + } + + @Override + public int hashCode() { + return Objects.hash(first, second); + } + + public String toString() { + return "(" + first + ", " + second + ")"; + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriter.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriter.java new file mode 100644 index 0000000000000000000000000000000000000000..e9cc804f59a7bbf116cfc0916c61381b322e9401 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriter.java @@ -0,0 +1,51 @@ +package org.rosi_project.model_sync.model_join.representation.writer; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; + +/** + * The {@code FileBasedModelJoinWriter} outputs a {@link ModelJoinExpression model join description} + * onto the File System. + * + * @author Rico Bergmann + */ +public class FileBasedModelJoinWriter implements ModelJoinWritingService<Boolean> { + + private final File outputFile; + private final StringBasedModelJoinWriter toStringWriter; + + /** + * Full constructor. + * + * @param outputFile the file to write to + */ + public FileBasedModelJoinWriter(@Nonnull File outputFile) { + this.outputFile = outputFile; + this.toStringWriter = StringBasedModelJoinWriter.withNewlines(); + } + + /** + * Writes the given description to the {@code outputFile}. + * + * @return whether the file was successfully written + */ + @Override + public Boolean write(@Nonnull ModelJoinExpression modelJoin) { + try (BufferedWriter writer = Files.newBufferedWriter( + outputFile.toPath(), + StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { + + String modelJoinTextRepresentation = toStringWriter.write(modelJoin); + writer.write(modelJoinTextRepresentation); + + return true; + } catch (IOException ioe) { + return false; + } + } +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWriter.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWriter.java new file mode 100644 index 0000000000000000000000000000000000000000..96d3434153904743c2f634cb890be8ba823e32a8 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWriter.java @@ -0,0 +1,42 @@ +package org.rosi_project.model_sync.model_join.representation.writer; + +import java.io.File; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; + +/** + * The {@code ModelJoinWriter} acts as an entry point to write and or transform {@link + * ModelJoinExpression model join descriptions} to various representations. + * + * @author Rico Bergmann + */ +public class ModelJoinWriter { + + private static final StringBasedModelJoinWriter stringWriter = StringBasedModelJoinWriter + .createDefault(); + + /** + * Converts a whole model join description to a single {@code String}. + * <p> + * This {@code String} is primarily intended to be human-readable and does not focus in being easy + * to reuse for further purposes. + */ + public static String writeToString(@Nonnull ModelJoinExpression modelJoin) { + return stringWriter.write(modelJoin); + } + + /** + * Outputs a whole model join description to a single file. + * <p> + * The resulting file is intended to be both human-readable as well as to be parsed by further + * applications. + * + * @return whether the expression was written successfully. + */ + public static boolean writeToFile( + @Nonnull File outputFile, + @Nonnull ModelJoinExpression modelJoin) { + return new FileBasedModelJoinWriter(outputFile).write(modelJoin); + } + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWritingService.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWritingService.java new file mode 100644 index 0000000000000000000000000000000000000000..4f89772ac9e62bef6ad2952892b60862cfcd6510 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWritingService.java @@ -0,0 +1,20 @@ +package org.rosi_project.model_sync.model_join.representation.writer; + +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; + +/** + * A {@code ModelJoinWritingService} transforms a {@link ModelJoinExpression model join description} + * into some different representation. + * + * @author Rico Bergmann + */ +public interface ModelJoinWritingService<T> { + + /** + * Performs the transformation. The result of this method depends on the actual details of the + * writing service and should be documented there. + */ + T write(@Nonnull ModelJoinExpression modelJoin); + +} diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriter.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriter.java new file mode 100644 index 0000000000000000000000000000000000000000..8281979371398a408c082622e8d6d10fa37de98d --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriter.java @@ -0,0 +1,160 @@ +package org.rosi_project.model_sync.model_join.representation.writer; + +import java.util.StringJoiner; +import javax.annotation.Nonnull; +import org.rosi_project.model_sync.model_join.representation.grammar.CompoundKeepExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepAggregateExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepAttributesExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepCalculatedExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepReferenceExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepSubTypeExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.KeepSuperTypeExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.NaturalJoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.OuterJoinExpression; +import org.rosi_project.model_sync.model_join.representation.grammar.ThetaJoinExpression; +import org.rosi_project.model_sync.model_join.representation.util.Functional; +import org.rosi_project.model_sync.model_join.representation.util.Nothing; + +/** + * The {@code StringBasedModelJoinWriter} transforms a {@link ModelJoinExpression model join description} + * to a single {@code String}. + * + * @author Rico Bergmann + */ +class StringBasedModelJoinWriter implements ModelJoinWritingService<String> { + + private static final String NEWLINE_DELIM = "\n"; + private static final String EMPTY_DELIM = " "; + private static final String DEFAULT_INDENTATION = " "; + + static StringBasedModelJoinWriter createDefault() { + return new StringBasedModelJoinWriter(false); + } + + static StringBasedModelJoinWriter withNewlines() { + return new StringBasedModelJoinWriter(true); + } + + private final String delimiter; + private final boolean useNewlineDelimiters; + + private String currentIndentation = ""; + + private StringBasedModelJoinWriter(boolean useNewlinesDelimiters) { + if (useNewlinesDelimiters) { + this.delimiter = NEWLINE_DELIM; + } else { + this.delimiter = EMPTY_DELIM; + } + this.useNewlineDelimiters = useNewlinesDelimiters; + } + + @Override + public String write(@Nonnull ModelJoinExpression modelJoin) { + StringJoiner joiner = new StringJoiner(delimiter + delimiter); + + modelJoin.getJoins().forEach( + joinStatement -> joiner.merge(writeJoin(joinStatement)) + ); + + return joiner.toString(); + } + + private StringJoiner writeJoin(@Nonnull JoinExpression join) { + StringJoiner joiner = new StringJoiner(" "); + + String joinHeader = Functional.<String>match(join) + .caseOf(ThetaJoinExpression.class, "theta join") + .caseOf(NaturalJoinExpression.class, "natural join") + .caseOf(OuterJoinExpression.class, outerJoin -> { + switch (outerJoin.getDirection()) { + case LEFT: + return "left outer join"; + case RIGHT: + return "right outer join"; + default: + throw new AssertionError(outerJoin.getDirection()); + } + }) + .runOrThrow(); + joiner.add(String.format(currentIndentation + "%s %s with %s as %s", joinHeader, join.getLeft(), join.getRight(), join.getTarget())); + Functional.<Nothing>match(join) + .caseOf(ThetaJoinExpression.class, thetaJoin -> { + joiner.add("where").add(thetaJoin.getCondition().toString()); + return Nothing.__(); + }) + .defaultCase(Nothing.__()) + .runOrThrow(); + + joiner.add("{\n"); + increaseIndentationIfNecessary(); + join.getKeeps().forEach(keep -> joiner.add(currentIndentation + writeKeep(keep) + delimiter)); + decreaseIndentationIfNecessary(); + joiner.add(currentIndentation + "}"); + + return joiner; + } + + private String writeKeep(KeepExpression keep) { + + return Functional.<String>match(keep) + .caseOf(KeepAttributesExpression.class, keepAttributes -> { + StringJoiner attributesJoiner = new StringJoiner(", "); + keepAttributes.getAttributes().forEach(attr -> attributesJoiner.add(attr.toString())); + return "keep attributes " + attributesJoiner; + }) + .caseOf(KeepCalculatedExpression.class, keepCalculated -> + "keep calculated attribute " + + keepCalculated.getCalculationRule() + + " as " + keepCalculated.getTarget()) + .caseOf(KeepAggregateExpression.class, keepAggregate -> + String.format("keep aggregate %s(%s) over %s as %s", + keepAggregate.getAggregation().name().toLowerCase(), + keepAggregate.getAggregatedAttribute(), + keepAggregate.getSource(), keepAggregate.getTarget()) + ) + .caseOf(KeepReferenceExpression.class, keepRef -> { + String header = String.format("keep %s %s as type %s {" + delimiter, keepRef.getReferenceDirection().name().toLowerCase(), keepRef.getAttribute(), keepRef.getTarget()); + return writeCompound(keepRef, header); + }) + .caseOf(KeepSubTypeExpression.class, keepSubtype -> { + String header = String.format("keep subtype %s as type %s {" + delimiter, keepSubtype.getType(), keepSubtype.getTarget()); + return writeCompound(keepSubtype, header); + }) + .caseOf(KeepSuperTypeExpression.class, keepSupertype -> { + String header = String.format("keep supertype %s as type %s {" + delimiter, keepSupertype.getType(), keepSupertype.getTarget()); + return writeCompound(keepSupertype, header); + }) + .runOrThrow(); + + } + + private String writeCompound(CompoundKeepExpression compoundKeep, String header) { + StringJoiner refJoiner = new StringJoiner(" "); + + refJoiner.add(header); + increaseIndentationIfNecessary(); + compoundKeep.getKeeps().forEach(keep -> refJoiner.add(currentIndentation + writeKeep(keep) + delimiter)); + decreaseIndentationIfNecessary(); + refJoiner.add(currentIndentation + "}"); + + return refJoiner.toString(); + } + + private void increaseIndentationIfNecessary() { + if (useNewlineDelimiters) { + currentIndentation += DEFAULT_INDENTATION; + } + } + + private void decreaseIndentationIfNecessary() { + if (useNewlineDelimiters) { + int deletionIdx = currentIndentation.lastIndexOf(DEFAULT_INDENTATION); + currentIndentation = currentIndentation.substring(0, deletionIdx); + } + } + +} diff --git a/src/main/java/org/rosi_project/model_sync/modelrepresentation/ModelJoinCreation.java b/src/main/java/org/rosi_project/model_sync/modelrepresentation/ModelJoinCreation.java new file mode 100644 index 0000000000000000000000000000000000000000..44fa819a32695600f414d3adb075e0d8c4411cb6 --- /dev/null +++ b/src/main/java/org/rosi_project/model_sync/modelrepresentation/ModelJoinCreation.java @@ -0,0 +1,131 @@ +package org.rosi_project.model_sync.modelrepresentation; + +import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.attributes; +import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.outgoing; +import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.supertype; +import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.thetaJoin; +import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.naturalJoin; + +import java.io.File; + +import org.rosi_project.model_sync.model_join.representation.core.AttributePath; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; +import org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder; +import org.rosi_project.model_sync.model_join.representation.writer.*; + +public class ModelJoinCreation { + + public static void main(String[] args) { + /*ClassResource person = ClassResource.fromQualifiedName("contact.Person"); + ClassResource employee = ClassResource.fromQualifiedName("company.Employee"); + AttributePath dateOfBirth = AttributePath.from(person, "dateOfBirth"); + + ModelJoinExpression mj = ModelJoinBuilder.createNewModelJoin() + .add(JoinFactory.createNew() + .natural() + .join(person) + .with(employee) + .as(ClassResource.fromQualifiedName("unified.Person")) + .keep(KeepAttributesExpression + .keepAttributes(dateOfBirth)) + .done() + ).build(); + System.out.println(mj); + for (JoinExpression j: mj.getJoins()) { + System.out.println(j); + for (KeepExpression k: j.getKeeps()) { + System.out.println(k); + } + }*/ + + ClassResource library = ClassResource.from("lib", "Library"); + ClassResource employee = ClassResource.from("lib", "Employee"); + ClassResource person = ClassResource.from("lib", "Person"); + + //ClassResource jointargetMovie = ClassResource.from("jointarget", "Movie"); + //ClassResource jointargetVote = ClassResource.from("jointarget", "Vote"); + //ClassResource jointargetMediaItem = ClassResource.from("jointarget", "MediaItem"); + + AttributePath libraryName = AttributePath.from(library, "name"); + AttributePath libraryEmployees = AttributePath.from(library, "employees"); + AttributePath employeeSalary = AttributePath.from(employee, "salary"); + AttributePath employeeManager = AttributePath.from(employee, "manager"); + AttributePath personName = AttributePath.from(person, "name"); + + ModelJoinExpression mjManager = ModelJoinBuilder.createNewModelJoin() + .add(naturalJoin() + .join(employee) + .with(employee) + .as(employee) + .keep(attributes(employeeSalary)) + .keep(outgoing(employeeManager) + .as(employee) + .buildExpression() + ) + .keep(supertype(person) + .as(person) + .keep(attributes(personName)) + .buildExpression() + ) + .done()) + .build(); + + ModelJoinExpression mjComplete = ModelJoinBuilder.createNewModelJoin() + .add(naturalJoin() + .join(library) + .with(library) + .as(library) + .keep(attributes(libraryName)) + .keep(outgoing(libraryEmployees) + .as(employee) + .keep(attributes(employeeSalary)) + .keep(outgoing(employeeManager) + .as(employee) + .buildExpression() + ) + .keep(supertype(person) + .as(person) + .keep(attributes(personName)) + .buildExpression() + ) + .buildExpression() + ) + .done()) + .build(); + + ModelJoinExpression mjSimple = ModelJoinBuilder.createNewModelJoin() + .add(naturalJoin() + .join(library) + .with(library) + .as(library) + .keep(attributes(libraryName)) + .keep(outgoing(libraryEmployees) + .as(employee) + .keep(attributes(employeeSalary)) + .keep(supertype(person) + .as(person) + .keep(attributes(personName)) + .buildExpression() + ) + .buildExpression() + ) + .done()) + .build(); + + File fileComplete = new File("libraryComplete.modeljoin"); + File fileSimple = new File("librarySimple.modeljoin"); + File fileManager = new File("manager.modeljoin"); + //registerCreatedFile(outputFile); + + FileBasedModelJoinWriter writerComplete = new FileBasedModelJoinWriter(fileComplete); + writerComplete.write(mjComplete); + + FileBasedModelJoinWriter writerSimple = new FileBasedModelJoinWriter(fileSimple); + writerSimple.write(mjSimple); + + FileBasedModelJoinWriter writerManager = new FileBasedModelJoinWriter(fileManager); + writerManager.write(mjManager); + } + +} diff --git a/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriterAcceptanceTests.java b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriterAcceptanceTests.java new file mode 100644 index 0000000000000000000000000000000000000000..115556369bdefdd7850579d6c81c04ceb348390f --- /dev/null +++ b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriterAcceptanceTests.java @@ -0,0 +1,64 @@ +package org.rosi_project.model_sync.model_join.representation.writer; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.rosi_project.model_sync.model_join.representation.writer.TestedModels.Listing2; + +class FileBasedModelJoinWriterAcceptanceTests { + + private static final Logger log = Logger.getLogger(FileBasedModelJoinWriterAcceptanceTests.class.getSimpleName()); + + private List<File> createdFiles = new ArrayList<>(); + + @AfterEach + void tearDown() { + createdFiles.forEach(file -> { + boolean result = file.delete(); + log.info(String.format("Deleting file %s, successfull=%s", file, result)); + }); + createdFiles.clear(); + } + + /** + * Depends on {@link StringBasedModelJoinWriterAcceptanceTests#writeProducesListing2FromTechnicalReport()} + */ + @Test + void writeProducesListing2FromTechnicalReport() { + File outputFile = new File("listing2.modeljoin"); + registerCreatedFile(outputFile); + + FileBasedModelJoinWriter writer = new FileBasedModelJoinWriter(outputFile); + writer.write(Listing2.asModelJoin); + + logFileContent(outputFile); + + String expectedContent = StringBasedModelJoinWriter.withNewlines().write(Listing2.asModelJoin); + assertThat(outputFile).exists(); + assertThat(outputFile).hasContent(expectedContent); + } + + private void registerCreatedFile(File newFile) { + createdFiles.add(newFile); + log.info(String.format("Created new file %s", newFile)); + } + + private void logFileContent(File file) { + try { + String contents = Files.lines(file.toPath()).collect(Collectors.joining()); + log.info(String.format("Content of file %s:", file)); + log.info(contents); + } catch (IOException e) { + log.severe(String.format("Could not read file %s", file)); + } + } + +} diff --git a/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriterAcceptanceTests.java b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriterAcceptanceTests.java new file mode 100644 index 0000000000000000000000000000000000000000..716d8d54e9644a7c9eaa2e0f0c6e9e09d8c9e474 --- /dev/null +++ b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriterAcceptanceTests.java @@ -0,0 +1,39 @@ +package org.rosi_project.model_sync.model_join.representation.writer; + +import static org.assertj.core.api.Assertions.*; + +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.rosi_project.model_sync.model_join.representation.writer.TestedModels.Listing2; + +class StringBasedModelJoinWriterAcceptanceTests { + + private static final Logger log = Logger.getLogger(StringBasedModelJoinWriterAcceptanceTests.class.getSimpleName()); + + /** + * @see TestedModels + */ + @Test + void writeProducesListing2FromTechnicalReport() { + StringBasedModelJoinWriter writer = StringBasedModelJoinWriter.withNewlines(); + String writerOutput = writer.write(Listing2.asModelJoin); + + log.info("Writer output:"); + log.info(writerOutput); + + String sanitizedWriterOutput = sanitize(writerOutput); + + String expectedOutput = sanitize(Listing2.asString); + + // Although AssertJ provides an isEqualToIgnoringWhitespace as well as an + // isEqualToIgnoringNewline method, there's none that ignores both. Thus we reside to manual + // sanitization + assertThat(sanitizedWriterOutput).isEqualTo(expectedOutput); + } + + private String sanitize(String rawModelJoin) { + return rawModelJoin.replaceAll("\\s", ""); + } +} diff --git a/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/TestedModels.java b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/TestedModels.java new file mode 100644 index 0000000000000000000000000000000000000000..12ab7c144319e4a5a33174852cf402f2e22e644f --- /dev/null +++ b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/TestedModels.java @@ -0,0 +1,69 @@ +package org.rosi_project.model_sync.model_join.representation.writer; + +import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.attributes; +import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.outgoing; +import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.supertype; +import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.thetaJoin; + +import org.rosi_project.model_sync.model_join.representation.core.AttributePath; +import org.rosi_project.model_sync.model_join.representation.core.ClassResource; +import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression; +import org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder; + +class TestedModels { + + /* + * The technical report which forms the basis of the tests here is + * + * Burger, E. et Al.: "ModelJoin - A Textual Domain-Specific Language for the Combination of + * Heterogeneous Models"; Karslruhe Institute of Technololgy 2014 + */ + + static class Listing2 { + static final ClassResource imdbFilm = ClassResource.from("imdb", "Film"); + static final ClassResource libraryVideoCassette = ClassResource.from("library", "VideoCassette"); + static final ClassResource libraryAudioVisualItem = ClassResource.from("library", "AudioVisualItem"); + static final ClassResource jointargetMovie = ClassResource.from("jointarget", "Movie"); + static final ClassResource jointargetVote = ClassResource.from("jointarget", "Vote"); + static final ClassResource jointargetMediaItem = ClassResource.from("jointarget", "MediaItem"); + + static final AttributePath imdbFilmYear = AttributePath.from(imdbFilm, "year"); + static final AttributePath imdbFilmVotes = AttributePath.from(imdbFilm, "votes"); + static final AttributePath imdbVoteScore = AttributePath.from("imdb.Vote", "score"); + static final AttributePath libraryAudioVisualItemMinutesLength = AttributePath.from(libraryAudioVisualItem, "minutesLength"); + + static final ModelJoinExpression asModelJoin = ModelJoinBuilder.createNewModelJoin() + .add(thetaJoin() + .join(imdbFilm) + .with(libraryVideoCassette) + .as(jointargetMovie) + .where( + "library.VideoCassette.cast->forAll (p | imdb.Film.figures->playedBy->exists (a | p.firstname.concat(\" \") .concat(p.lastName) == a.name))") + .keep(attributes(imdbFilmYear)) + .keep(outgoing(imdbFilmVotes) + .as(jointargetVote) + .keep(attributes(imdbVoteScore)) + .buildExpression() + ) + .keep(supertype(libraryAudioVisualItem) + .as(jointargetMediaItem) + .keep(attributes(libraryAudioVisualItemMinutesLength)) + .buildExpression() + ) + .done()) + .build(); + + static final String asString = "theta join imdb.Film with library.VideoCassette as jointarget.Movie\n" + + "where library.VideoCassette.cast->forAll (p | imdb.Film.figures->playedBy->exists (a | p.\n" + + "firstname.concat(\" \") .concat(p.lastName) == a.name)) {\n" + + " keep attributes imdb.Film.year\n" + + " keep outgoing imdb.Film.votes as type jointarget.Vote {\n" + + " keep attributes imdb.Vote.score\n" + + "}\n" + + "keep supertype library.AudioVisualItem as type jointarget.MediaItem {\n" + + " keep attributes library.AudioVisualItem.minutesLength\n" + + "}\n" + + "}"; + } + +}