Skip to content

Commit

Permalink
Add property view editor for attribute mapping
Browse files Browse the repository at this point in the history
- Remove string and number literals, use expressions instead
- Mark target attributes with an expression in diagram
- Add form for attribute mapping in properties view
- Generalize AttributeGrid to GridComponent
  • Loading branch information
martin-fleck-at committed Jun 6, 2024
1 parent a7614c4 commit a9e0ff4
Show file tree
Hide file tree
Showing 41 changed files with 784 additions and 673 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ mapping:
sources:
- CustomerSourceObject.Country
- attribute: FixedNumber
sources:
- 1
expression: "1"
- attribute: FixedString
sources:
- "Hoppa"
expression: "Hoppa"
- attribute: Name
sources:
- CustomerSourceObject.FirstName
Expand Down
6 changes: 2 additions & 4 deletions examples/mapping-example/ExampleDWH/DWH.mapping.cm
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ mapping:
sources:
- CalcAgeSourceObject.Age
- attribute: FixedNumber
sources:
- 1337
expression: "1337"
- attribute: FixedString
sources:
- "Fixed String"
expression: "Fixed String"
101 changes: 63 additions & 38 deletions extensions/crossmodel-lang/src/glsp-server/common/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
********************************************************************************/

import { ATTRIBUTE_COMPARTMENT_TYPE, createLeftPortId, createRightPortId } from '@crossbreeze/protocol';
import { GCompartment, GLabel, GPort } from '@eclipse-glsp/server';
import { GCompartment, GCompartmentBuilder, GLabel, GPort } from '@eclipse-glsp/server';
import { Attribute } from '../../language-server/generated/ast.js';
import { CrossModelIndex } from './cross-model-index.js';

Expand All @@ -19,50 +19,75 @@ export function createHeader(text: string, containerId: string): GCompartment {
.build();
}

export function createAttributesCompartment(attributes: Attribute[], containerId: string, index: CrossModelIndex): GCompartment {
const attributesContainer = GCompartment.builder()
.id(`${containerId}_attributes`)
.addCssClass('attributes-compartment')
.layout('vbox')
.addLayoutOption('hAlign', 'left')
.addLayoutOption('paddingBottom', 0);
export type MarkerFunction<T extends Attribute> = (attribute: T, id: string) => GLabel | undefined;

// Add the attributes of the entity.
export function createAttributesCompartment<T extends Attribute>(
attributes: T[],
containerId: string,
index: CrossModelIndex,
markerFn?: MarkerFunction<T>
): GCompartment {
const attributesContainer = new AttributesCompartmentBuilder().set(containerId);
for (const attribute of attributes) {
attributesContainer.add(createAttributeCompartment(attribute, index));
attributesContainer.add(AttributeCompartment.builder().set(attribute, index, markerFn).build());
}
return attributesContainer.build();
}

export function createAttributeCompartment(attribute: Attribute, index: CrossModelIndex): GCompartment {
const attributeId = index.createId(attribute);
const attributeCompartment = GCompartment.builder()
.id(attributeId)
.type(ATTRIBUTE_COMPARTMENT_TYPE)
.addCssClass('attribute-compartment')
.layout('hbox')
.addLayoutOption('paddingBottom', 3)
.addLayoutOption('paddingTop', 3)
.addLayoutOption('hGap', 3);
export class AttributesCompartmentBuilder extends GCompartmentBuilder {
constructor() {
super(GCompartment);
}

const leftPortId = createLeftPortId(attributeId);
index.indexSemanticElement(leftPortId, attribute);
attributeCompartment.add(GPort.builder().id(leftPortId).build());
set(containerId: string): this {
this.id(`${containerId}_attributes`)
.addCssClass('attributes-compartment')
.layout('vbox')
.addLayoutOption('hAlign', 'left')
.addLayoutOption('paddingBottom', 0);
return this;
}
}

attributeCompartment.add(
GLabel.builder()
.id(`${attributeId}_attribute_name`)
.text(attribute.name || '')
.addCssClass('attribute')
.build()
);
attributeCompartment.add(GLabel.builder().text(':').id(`${attributeId}_attribute_del`).build());
attributeCompartment.add(
GLabel.builder().id(`${attributeId}_attribute_datatype`).text(attribute.datatype).addCssClass('datatype').build()
);
const rightPortId = createRightPortId(attributeId);
index.indexSemanticElement(rightPortId, attribute);
attributeCompartment.add(GPort.builder().id(rightPortId).build());
export class AttributeCompartment extends GCompartment {
override type = ATTRIBUTE_COMPARTMENT_TYPE;

return attributeCompartment.build();
static override builder(): AttributeCompartmentBuilder {
return new AttributeCompartmentBuilder(AttributeCompartment).type(ATTRIBUTE_COMPARTMENT_TYPE);
}
}

export class AttributeCompartmentBuilder extends GCompartmentBuilder<AttributeCompartment> {
set<T extends Attribute>(attribute: T, index: CrossModelIndex, markerFn?: MarkerFunction<T>): this {
const attributeId = index.createId(attribute);
this.id(attributeId)
.type(ATTRIBUTE_COMPARTMENT_TYPE)
.addCssClass('attribute-compartment')
.layout('hbox')
.addLayoutOption('paddingBottom', 3)
.addLayoutOption('paddingTop', 3)
.addLayoutOption('hGap', 3);

const leftPortId = createLeftPortId(attributeId);
index.indexSemanticElement(leftPortId, attribute);
this.add(GPort.builder().id(leftPortId).build());

this.add(
GLabel.builder()
.id(`${attributeId}_attribute_name`)
.text(attribute.name || '')
.addCssClass('attribute')
.build()
);
this.add(GLabel.builder().text(':').id(`${attributeId}_attribute_del`).build());
this.add(GLabel.builder().id(`${attributeId}_attribute_datatype`).text(attribute.datatype).addCssClass('datatype').build());
const marker = markerFn?.(attribute, attributeId);
if (marker) {
this.add(marker);
}
const rightPortId = createRightPortId(attributeId);
index.indexSemanticElement(rightPortId, attribute);
this.add(GPort.builder().id(rightPortId).build());
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { inject, injectable } from 'inversify';
import { combineIds } from '../../../language-server/cross-model-naming.js';
import { isSourceObjectAttribute, isTargetObjectAttribute } from '../../../language-server/generated/ast.js';
import { createAttributeMapping, getOwner } from '../../../language-server/util/ast-util.js';
import { createAttributeMapping, createAttributeMappingSource, getOwner } from '../../../language-server/util/ast-util.js';
import { CrossModelCommand } from '../../common/cross-model-command.js';
import { MappingModelState } from '../model/mapping-model-state.js';

Expand All @@ -36,22 +36,19 @@ export class MappingEdgeCreationOperationHandler extends JsonCreateEdgeOperation
protected createEdge(operation: CreateEdgeOperation): void {
const sourceElementId = isLeftPortId(operation.targetElementId) ? operation.sourceElementId : operation.targetElementId;
const targetElementId = isLeftPortId(operation.targetElementId) ? operation.targetElementId : operation.sourceElementId;
if (operation.args?.isLiteral === true) {
if (isPortId(sourceElementId)) {
const container = this.modelState.mapping.target;
const sourceElement = this.modelState.index.findSemanticElement(sourceElementId, isSourceObjectAttribute);
const targetElement = this.modelState.index.findSemanticElement(targetElementId, isTargetObjectAttribute);
if (!targetElement) {
if (!targetElement || !sourceElement) {
return;
}
// interpret sourceElementId as literal
const source = Number.parseFloat(sourceElementId);
const mapping = createAttributeMapping(container, isNaN(source) ? sourceElementId : source, targetElement.id, true);
container.mappings.push(mapping);
} else if (isPortId(sourceElementId)) {
const container = this.modelState.mapping.target;
const sourceElement = this.modelState.index.findSemanticElement(sourceElementId, isSourceObjectAttribute);
const targetElement = this.modelState.index.findSemanticElement(targetElementId, isTargetObjectAttribute);
if (sourceElement && targetElement) {
const mapping = createAttributeMapping(container, combineIds(getOwner(sourceElement).id, sourceElement.id), targetElement.id);
const sourceAttributeReference = combineIds(getOwner(sourceElement).id, sourceElement.id);
const existingMapping = container.mappings.find(mapping => mapping.attribute.value.ref === targetElement);
if (existingMapping) {
existingMapping.sources.push(createAttributeMappingSource(existingMapping, sourceAttributeReference));
} else {
const mapping = createAttributeMapping(container, sourceAttributeReference, targetElement.id);
container.mappings.push(mapping);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,10 @@ import { Command, DeleteElementOperation, GEdge, GNode, JsonOperationHandler, Mo
import { inject, injectable } from 'inversify';
import {
AttributeMappingSource,
NumberLiteral,
SourceObject,
SourceObjectRelations,
StringLiteral,
isAttributeMappingSource,
isNumberLiteral,
isReferenceSource,
isSourceObject,
isStringLiteral
isSourceObject
} from '../../../language-server/generated/ast.js';
import { getOwner } from '../../../language-server/util/ast-util.js';
import { CrossModelCommand } from '../../common/cross-model-command.js';
Expand Down Expand Up @@ -64,8 +59,6 @@ export class MappingDiagramDeleteElementOperationHandler extends JsonOperationHa
const astNode = this.modelState.index.findSemanticElement(node.id);
if (isSourceObject(astNode)) {
this.deleteSourceObject(node, astNode, deleteInfo);
} else if (isStringLiteral(astNode) || isNumberLiteral(astNode)) {
this.deleteLiteralObject(node, astNode, deleteInfo);
}
}

Expand All @@ -76,19 +69,11 @@ export class MappingDiagramDeleteElementOperationHandler extends JsonOperationHa
deleteInfo.relations.push(...mapping.sources.flatMap(src => src.relations).filter(relation => relation.source.ref === source));
deleteInfo.attributeSources.push(
...mapping.target.mappings.flatMap(attrMapping =>
attrMapping.sources.filter(
attrSource => isReferenceSource(attrSource) && attrSource.value.ref && getOwner(attrSource.value?.ref) === source
)
attrMapping.sources.filter(attrSource => attrSource.value.ref && getOwner(attrSource.value?.ref) === source)
)
);
}

protected deleteLiteralObject(node: GNode, literal: StringLiteral | NumberLiteral, deleteInfo: DeleteInfo): void {
// Literal nodes are contained by the corresponding targetAttributeMapping => we only have to delete
// the mapping that correlates to the outgoing edge of the literal node
this.modelState.index.getOutgoingEdges(node).forEach(edge => this.deleteEdge(edge, deleteInfo));
}

protected deleteEdge(edge: GEdge, deleteInfo: DeleteInfo): void {
// the edge has the source as semantic element with an additional UUID cause the user may use the same source multiple times
// see the generation of the edge id in edges.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { SOURCE_NUMBER_NODE_TYPE, SOURCE_STRING_NODE_TYPE, TARGET_OBJECT_NODE_TYPE, isLeftPortId } from '@crossbreeze/protocol';
import { GCompartment, GModelRoot, GNode, GPort, LayoutEngine, MaybePromise, findParentByClass } from '@eclipse-glsp/server';
import { inject, injectable } from 'inversify';
import { isReferenceSource } from '../../language-server/generated/ast.js';
import { getOwner } from '../../language-server/util/ast-util.js';
import { MappingModelState } from './model/mapping-model-state.js';
import { GTargetObjectNode } from './model/nodes.js';
Expand All @@ -24,7 +23,7 @@ export class MappingDiagramLayoutEngine implements LayoutEngine {

const index = this.modelState.index;

// position source nodes (references and literals) in correct order
// position source nodes in correct order
let offset = 0;
let maxSourceWidth = 0;
const marginBetweenSourceNodes = 20;
Expand Down Expand Up @@ -62,7 +61,7 @@ export class MappingDiagramLayoutEngine implements LayoutEngine {
const idx = this.modelState.index;
const sourceNodeOrder = [...target.mappings]
.sort((left, right) => (left.attribute.value.ref?.$containerIndex ?? 0) - (right.attribute.value.ref?.$containerIndex ?? 0))
.flatMap(mapping => mapping.sources.map(source => idx.createId(isReferenceSource(source) ? getOwner(source.value.ref) : source)));
.flatMap(mapping => mapping.sources.map(source => idx.createId(getOwner(source.value.ref))));
return (left: GNode, right: GNode): number => {
if (!sourceNodeOrder.includes(left.id)) {
return 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/

import { TARGET_ATTRIBUTE_MAPPING_EDGE_TYPE, createLeftPortId, createRightPortId } from '@crossbreeze/protocol';
import { RenderProps, TARGET_ATTRIBUTE_MAPPING_EDGE_TYPE, createLeftPortId, createRightPortId } from '@crossbreeze/protocol';
import { GEdge, GEdgeBuilder } from '@eclipse-glsp/server';
import { AttributeMappingSource, isReferenceSource } from '../../../language-server/generated/ast.js';
import { AttributeMappingSource } from '../../../language-server/generated/ast.js';
import { MappingModelIndex } from './mapping-model-index.js';

export class GTargetObjectEdge extends GEdge {
Expand All @@ -18,7 +18,7 @@ export class GTargetObjectEdge extends GEdge {
export class GTargetMappingSourceEdgeBuilder extends GEdgeBuilder<GTargetObjectEdge> {
set(source: AttributeMappingSource, index: MappingModelIndex): this {
const mapping = source.$container;
const sourceId = isReferenceSource(source) ? createRightPortId(index.createId(source.value.ref)) : index.createId(source);
const sourceId = createRightPortId(index.createId(source.value.ref));
const targetId = createLeftPortId(index.createId(mapping.attribute));

const id = 'edge_' + index.createId(source);
Expand All @@ -27,6 +27,10 @@ export class GTargetMappingSourceEdgeBuilder extends GEdgeBuilder<GTargetObjectE
this.addCssClasses('diagram-edge', 'mapping-edge', 'attribute-mapping');
this.addArg('edgePadding', 5);

if (mapping.$containerIndex !== undefined) {
this.addArg(RenderProps.TARGET_ATTRIBUTE_MAPPING_IDX, mapping.$containerIndex);
}

this.sourceId(sourceId);
this.targetId(targetId);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,10 @@
********************************************************************************/
import { GEdge, GGraph, GModelFactory, GNode } from '@eclipse-glsp/server';
import { inject, injectable } from 'inversify';
import {
AttributeMapping,
AttributeMappingSource,
SourceObject,
TargetObject,
isNumberLiteral,
isStringLiteral
} from '../../../language-server/generated/ast.js';
import { AttributeMapping, SourceObject, TargetObject } from '../../../language-server/generated/ast.js';
import { GTargetObjectEdge } from './edges.js';
import { MappingModelState } from './mapping-model-state.js';
import { GNumberLiteralNode, GSourceObjectNode, GStringLiteralNode, GTargetObjectNode } from './nodes.js';
import { GSourceObjectNode, GTargetObjectNode } from './nodes.js';

@injectable()
export class MappingDiagramGModelFactory implements GModelFactory {
Expand All @@ -37,13 +30,6 @@ export class MappingDiagramGModelFactory implements GModelFactory {
// source nodes
mappingRoot.sources.map(sourceObject => this.createSourceObjectNode(sourceObject)).forEach(node => graphBuilder.add(node));

// literals that serve as source
mappingRoot.target.mappings
.flatMap(mapping => mapping.sources)
.map(source => this.createSourceLiteralNode(source))
.filter(node => node !== undefined)
.forEach(node => graphBuilder.add(node!));

// target node
graphBuilder.add(this.createTargetNode(mappingRoot.target));

Expand All @@ -64,14 +50,4 @@ export class MappingDiagramGModelFactory implements GModelFactory {
protected createTargetNode(target: TargetObject): GNode {
return GTargetObjectNode.builder().set(target, this.modelState.index).build();
}

protected createSourceLiteralNode(node: AttributeMappingSource): GNode | undefined {
if (isStringLiteral(node)) {
return GStringLiteralNode.builder().set(node, this.modelState.index).build();
}
if (isNumberLiteral(node)) {
return GNumberLiteralNode.builder().set(node, this.modelState.index).build();
}
return undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,10 @@ import {
AttributeMapping,
AttributeMappingSource,
AttributeMappingTarget,
NumberLiteral,
ReferenceSource,
SourceObject,
StringLiteral,
isAttributeMapping,
isAttributeMappingSource,
isAttributeMappingTarget,
isReferenceSource,
isSourceObject,
isTargetObject
} from '../../../language-server/generated/ast.js';
Expand Down Expand Up @@ -67,14 +63,6 @@ export class MappingModelIndex extends CrossModelIndex {
}

protected createAttributeMappingSourceId(source: AttributeMappingSource): string | undefined {
return isReferenceSource(source) ? this.createReferenceSourceId(source) : this.createLiteralId(source);
}

protected createLiteralId(literal: NumberLiteral | StringLiteral): string {
return `mapping_${literal.$container?.$containerIndex ?? 0}_source_${literal.$containerIndex ?? 0}_${literal.value}`;
}

protected createReferenceSourceId(source: ReferenceSource): string | undefined {
return `mapping_${source.$container?.$containerIndex ?? 0}_source_${source.$containerIndex ?? 0}_${this.createId(source.value.ref)}`;
}

Expand Down
Loading

0 comments on commit a9e0ff4

Please sign in to comment.