/*
 * The SimpleUML to RDB Sample demonstrates how to use QVT transformations for 
 * transforming platform independent model to platform specific model. 
 * 
 * It also demonstrates the following basic features of QVT language: 
 * helper queries, mapping guards, and resolution operations.
 * 
 * Sample model pim.simpleuml is included to be used as an input for the transformation.
 */

/*
 * Two modeltypes are declared. The http NS URIs correspond to those used to register the 
 * Ecore models in the environment. Alternatively, a workspace metamodel may be used 
 * in conjunction with mappings defined in the project properties.
 */
modeltype UML uses 'http://www.eclipse.org/qvt/1.0.0/Operational/examples/simpleuml';
modeltype RDB uses 'http://www.eclipse.org/qvt/1.0.0/Operational/examples/rdb';

/*
 * The transformation signature declares that a UML modeltype is required as input, while an RDB
 * modeltype is produced as an output.  The UML modeltype is referenced as 'uml' throughout the 
 * transformation definition, while no name is needed for the output RDB modeltype.  Note that OCL 
 * type and namespace notation are used in operational QVT (: and :: respectively).
 */
transformation Simpleuml_To_Rdb(in uml : UML, out RDB);

/*
 * The main entry point of the transformation.  The 'uml' reference to the input UML modeltype 
 * instance is used to collect all rootObjects() of type Model.  The rootObjects() operation 
 * is available on all QVT Model objects (extents) and returns those objects found at the 
 * root of the input model.  The [UML::Model] statement following the call to rootObjects() 
 * is shorthand notation for the imperative select (xselect) construct where the condition is 
 * a type expression, which effectively performs a oclIsKindOf(UML::Model) with type recasting 
 * as a sequence.
 *
 * The invocation of the model2RDBModel() mapping is done using an -> operator, which is a 
 * shorthand notation for the imperative collect (xcollect) construct.  Alternatively, it could
 * be written as uml.rootObjects()[UML::Model]->xcollect(a | a.map model2RDBModel());
 */
main() {
	uml.rootObjects()[UML::Model]->map model2RDBModel();
}

/*
 * This mapping returns an RDB::Model instance from the UML::Model passed from main().  The name 
 * attributes map directly using the OCL assignment operator.  The RDB Model has a collection 
 * of schemas populated by the package2schemas() mapping.  The Sequence of RDB::Schema objects 
 * that is returned by the mapping is converted into the required OrderedSet using the OCL operation 
 * asOrderedSet().
 *
 * This mapping has no init or end section, leaving the body as an implicit population section.
 */
mapping UML::Model::model2RDBModel() : RDB::Model {
	name := self.name;
	schemas := self.map package2schemas()->asOrderedSet();
}

/*
 * This mapping recursively invokes the package2schema() mapping to produce a Sequence of 
 * RDB Schema objects from a UML Package and its subpackages.  Note the use of OCL union() 
 * and flatten() operations to produce a single flattened Sequence of Schema objects.
 *
 * There is no population section in this mapping, but an init section that assigns the 
 * Sequence of returned objects to the result.  Alternatively, the statement below could 
 * have been used in the schemas assignment of the mapping above.
 */
mapping UML::Package::package2schemas() : Sequence(RDB::Schema) {
	init {
		result := self.map package2schema()->asSequence()->
			union(self.getSubpackages()->map package2schemas()->flatten());
	}
}

/*
 * This mapping creates an RDB Schema object from a UML Package.  It includes a when clause to 
 * verify the passed Package contains persistent classes.  Look below to see the logic employed
 * within the hasPersistentClasses() query.
 *
 * The name attributes map directly, while the elements reference is populated with RDB Table 
 * objects mapped from persistent UML Class objects using a map invocation and similar 
 * shorthand notation used above.
 */
mapping UML::Package::package2schema() : RDB::Schema 
	when { self.hasPersistentClasses() }
{
	name := self.name;
	elements := self.ownedElements[UML::Class]->map persistentClass2table()->asOrderedSet()
}

/*
 * This mapping produces an RDB Table object from a UML persistent Class object. The when clause
 * uses the isPersistent() query below to see if the Class has a 'persistent' string as one of 
 * its stereotype strings.
 *
 * Again, the name attributes map directly.  The Class is mapped to a set of TableColumn objects
 * using the class2columns() mapping, the results of which are sorted by name using the OCL 
 * sortedBy() operation.  The primaryKey is set using the class2primaryKey() mapping, while 
 * foreignKeys are set using a resolveIn function.  This will allow us to resolve RDB ForeignKey 
 * objects created using the relationshipAttribute2foreignKey() mapping for each of the Class 
 * attributes. 
 */
mapping UML::Class::persistentClass2table() : RDB::Table 
	when { self.isPersistent() }
{
	name := self.name;
	columns := self.map class2columns(self)->sortedBy(name);
	primaryKey := self.map class2primaryKey();
	foreignKeys := self.attributes.resolveIn(
					UML::Property::relationshipAttribute2foreignKey, 
					RDB::constraints::ForeignKey)->asOrderedSet();
}

/*
 * A PrimaryKey object is created from a Class by prefixing the name with 'PK' and resolving 
 * one (the first) Table created from the Class in order to obtain its primary key columns 
 * using a query.
 */
mapping UML::Class::class2primaryKey() : RDB::constraints::PrimaryKey {
	name := 'PK' + self.name;
	includedColumns := self.resolveoneIn(UML::Class::persistentClass2table, RDB::Table).getPrimaryKeyColumns()
}

/*
 * This mapping will create an OrderedSet of RDB TableColumn objects from a UML Class object.
 * Similar to package2schemas(), this mapping has no population section, but just an init 
 * that assigns the result based on the union of type mappings from the Class and its
 * generalizations (Class extends DataType).
 *
 * Note that this mapping is defined for type UML::Class and also takes a UML::Class named 
 * targetClass as a parameter.  This pattern is used in several places within this
 * transformation definition to account for how generalization in the UML model is 
 * mapped to columns in the RDB model.  As properties and inherited properties are 
 * flattened into columns, the combined use of 'self' and 'target' parameter represent a
 * way to allow multiple copies of columns for a given property source, as subsequent
 * mapping invocations retrieve the same resulting columns from the trace model. 
 */
mapping UML::Class::class2columns(targetClass: UML::Class) : OrderedSet(RDB::TableColumn) {
	init {
		result := self.map dataType2columns(targetClass)->
			union(self.map generalizations2columns(targetClass))->asOrderedSet()
	}	
}

/*
 * For the passed DataType, an OrderedSet of TableColumn objects is created.  Again, the result
 * is assigned within the init block to the union of several attribute to column mappings. 
 */
mapping UML::DataType::dataType2columns(in targetType : UML::DataType) : OrderedSet(RDB::TableColumn) {
	init {
		result := self.map primitiveAttributes2columns(targetType)->
			union(self.map enumerationAttributes2columns(targetType))->
			union(self.map relationshipAttributes2columns(targetType))->
			union(self.map associationAttributes2columns(targetType))->asOrderedSet()
	}
}

/*
 * This mapping creates an OrderedSet of TableColumn objects from a DataType object.
 * The mapping declares three input parameters, including a prefix string and primary key
 * boolean.  
 *
 * The init section uses the result keyword with mapping invocation, as we've seen before.
 * What's new in this mapping is the use of an object definition within a collect operation.
 * Here, the OrderedSet of TableColumn objects returned from the dataType2columns() 
 * mapping is filtered to select only those marked as primary keys, which in turn are
 * used within the context of the collect where those matching the TableColumn created
 * using object are returned.
 */
mapping UML::DataType::dataType2primaryKeyColumns(in prefix : String, in leaveIsPrimaryKey : Boolean, in targetType : UML::DataType) : OrderedSet(RDB::TableColumn) {
	init {           
		result := self.map dataType2columns(self)->select(isPrimaryKey)->
			collect(c | object RDB::TableColumn {
				name := prefix + '_' + c.name;
				domain := c.domain;
				type := object RDB::datatypes::PrimitiveDataType {
					name := c.type.name;
				};
				isPrimaryKey := leaveIsPrimaryKey
			})->asOrderedSet();
	}
}

/*
 * This mapping returns an OrderedSet of TableColumn objects by invoking the 
 * primitiveAttribute2column() mapping for each attribute of the DataType.
 */
mapping UML::DataType::primitiveAttributes2columns(in targetType: UML::DataType) : OrderedSet(RDB::TableColumn) {
	init {
		result := self.attributes->map primitiveAttribute2column(targetType)->asOrderedSet()
	}
}

/*
 * This mapping creates a TableColumn from a Property when the isPrimitive() query
 * returns true.  The isPrimaryKey and name mappings are straightforward, while
 * the type reference is created using the object keyword to create a new
 * PrimitiveDataType with name initialized to the result of the query
 * umlPrimitive2rdbPrimitive() with the type name passed as a parameter.
 */
mapping UML::Property::primitiveAttribute2column(in targetType: UML::DataType) : RDB::TableColumn 
	when { self.isPrimitive() }
{
	isPrimaryKey := self.isPrimaryKey();
	name := self.name;
	type := object RDB::datatypes::PrimitiveDataType { name := umlPrimitive2rdbPrimitive(self.type.name); };
}

/*
 * This mapping returns an OrderedSet of TableColumn objects by invoking the 
 * enumerationAttribute2column() mapping for each attribute of the DataType.
 */
mapping UML::DataType::enumerationAttributes2columns(in targetType: UML::DataType) : OrderedSet(RDB::TableColumn) {
	init {
		result := self.attributes->map enumerationAttribute2column(targetType)->asOrderedSet()
	}
}

/*
 * This mapping creates a TableColumn from a Property when the isEnumeration() query
 * returns true.  The isPrimaryKey and name mappings are straightforward, while
 * the type reference is created using the object keyword to create a new
 * PrimitiveDataType with name initialized to 'int'.
 */
mapping UML::Property::enumerationAttribute2column(in targetType: UML::DataType) : RDB::TableColumn 
	when { self.isEnumeration() }
{
	isPrimaryKey := self.isPrimaryKey();	
	name := self.name;
	type := object RDB::datatypes::PrimitiveDataType { name := 'int'; };
}

/*
 * This mapping creates an OrderedSet of TableColumn objects from relationship
 * attributes.  The check for if the DataType is a relationship is performed
 * in the when clause of the invoked relationshipAttribute2foreignKey mapping.
 */
mapping UML::DataType::relationshipAttributes2columns(in targetType: UML::DataType) : OrderedSet(RDB::TableColumn) {
	init {
		result := self.attributes->map relationshipAttribute2foreignKey(targetType)->
			collect(includedColumns)->asOrderedSet();
	}
}

/*
 * This mapping creates a ForeignKey object from a DataType that returns true from 
 * the isRelationship() query in the when clause.  The name is prefixed with 'FK'.
 * The includedColumns collection is populated using the dataType2primaryKeyColumns()
 * mapping on the Property type reference cast to DataType.
 *
 * The referredUC reference uses resolveoneIn, but with the late modifier.  This 
 * causes the resolution to happen at the end of the transformation, thereby avoiding
 * a second pass to resolve objects that may not have been created during execution
 * at this point.
 */
mapping UML::Property::relationshipAttribute2foreignKey(in targetType: UML::DataType) : RDB::constraints::ForeignKey 
	when { self.isRelationship() }
{
	name := 'FK' + self.name;
	includedColumns := self.type.asDataType().map dataType2primaryKeyColumns(self.name, self.isIdentifying(), targetType);
	referredUC := self.type.late resolveoneIn(UML::Class::class2primaryKey, RDB::constraints::PrimaryKey);
}

/*
 * This mapping produces an OrderedSet of TableColumn objects from DataType attributes 
 * that return true from the isAssociation() query. The TableColumn objects are created 
 * using a call to the dataType2columns() mapping.
 */
mapping UML::DataType::associationAttributes2columns(targetType : UML::DataType) : OrderedSet(RDB::TableColumn) {
	init {
		result := self.attributes[isAssociation()]->
			collect(type.asDataType()->map dataType2columns(targetType))->asOrderedSet()
	}
}

/*
 * This mapping returns an OrderedSet of TableColumn objects from a Class using the 
 * generalizations of the class and the class2columns() mapping.
 */
mapping UML::Class::generalizations2columns(targetClass : UML::Class) : OrderedSet(RDB::TableColumn) {
	init {
		result := self.generalizations.general->map class2columns(targetClass)->flatten()->asOrderedSet();
	}
}

/*
 * This query returns an OrderedSet of Package objects from a Package's ownedElements
 * collection that are of type UML::Package using shorthand xselect notation.
 */
query UML::Package::getSubpackages() : OrderedSet(UML::Package) {
	return self.ownedElements[UML::Package]->asOrderedSet()
}

/*
 * This query performs a type cast from a UML Type to a UML DataType.
 */
query UML::Type::asDataType() : UML::DataType {
	return self.oclAsType(UML::DataType)
}

/*
 * This query returns true if the list of string stereotypes includes one
 * equal to 'primaryKey'.
 */
query UML::Property::isPrimaryKey() : Boolean {
	return self.stereotype->includes('primaryKey')
}

/*
 * This query returns true if the list of string stereotypes includes one
 * equal to 'identifying'.
 */
query UML::Property::isIdentifying() : Boolean {
	return self.stereotype->includes('identifying')
}

/*
 * This query returns true if the type attribute of the Property conforms
 * to the UML PrimitiveType.
 */
query UML::Property::isPrimitive() : Boolean {
	return self.type.oclIsKindOf(UML::PrimitiveType)
}

/*
 * This query returns true if the type attribute of the Property conforms
 * to the UML Enumeration.
 */
query UML::Property::isEnumeration() : Boolean {
	return self.type.oclIsKindOf(UML::Enumeration)
}

/*
 * This query returns true if the type attribute of the Property conforms
 * to the UML DataType and returns true from the isPersistent() query.
 */
query UML::Property::isRelationship() : Boolean {
	return self.type.oclIsKindOf(UML::DataType) and self.type.isPersistent()
}

/*
 * This query returns true if the type attribute of the Property conforms
 * to the UML DataType and returns false from the isPersistent() query.
 */
query UML::Property::isAssociation() : Boolean {
	return self.type.oclIsKindOf(UML::DataType) and not self.type.isPersistent()
}

/*
 * This query returns an OrderedSet of TableColumn objects from those columns
 * where isPrimaryKey returns true.
 */
query RDB::Table::getPrimaryKeyColumns() : OrderedSet(RDB::TableColumn) {
	return self.columns->select(isPrimaryKey)
}

/*
 * This query returns true if the list of string stereotypes includes one
 * equal to 'persistent'.
 */
query UML::ModelElement::isPersistent() : Boolean {
	return self.stereotype->includes('persistent')
}

/*
 * This query examines the contents of a Package to determine if there exists
 * at least one Class that returns true for the isPersistent() query.
 */
query UML::Package::hasPersistentClasses() : Boolean {
	return self.ownedElements->exists(
		let c : UML::Class = oclAsType(UML::Class) in 
			c.oclIsUndefined() implies c.isPersistent())
}

/*
 * This helper returns the RDB primitive string corresponding to the passed 
 * UML primitive string.  This helper produces no side effects and could be 
 * written as a query, alternatively.
 */
helper umlPrimitive2rdbPrimitive(in name : String) : String {
	return if name = 'String' then 'varchar' else
		if name = 'Boolean' then 'int' else
			if name = 'Integer' then 'int' else
				name
			endif
		endif
	endif
}
