@tripsnek/tmf
Version:
TypeScript Modeling Framework - A TypeScript port of the Eclipse Modeling Framework (EMF)
440 lines • 17.7 kB
JavaScript
import { TUtils } from '../tutils';
import { SerializedReference } from './serialized-reference';
import { EClassImpl } from '../metamodel/eclass-impl';
import { EReferenceImpl } from '../metamodel/ereference-impl';
/**
* Utilities for converting between EObjects and JSON. Usage:
*
* (1) TJson.makeJson(EObject) - converts an EObject to JSON.
* (2) TJson.makeEObject(json) - deserializes EObject encoded with (1).
* (3) TJson.makeJsonArray(EObject[]) - converts an array of EObjects to a JSON Array.
* (4) TJson.makeEObjectArray(json) - converts a JSON Array to an array of EObjects.
*
* You may also configure which EClasses are eligible for conversion with
* 'addPackages(EPackage[])' and 'setPackages(EPackage[])'.
*
*/
export class TJson {
//the name of the special JSON field that indicates each object's type
static JSON_FIELD_TYPESCRIPT_TYPE = '@type';
//These will be automatically added by the PackageInitializer when a Package is "touched"
static packages = [];
static setPackages(packages) {
this.packages = packages;
}
//automatically used by package initializers to add JSON serialization support when they are touched
static addPackages(packages) {
for (const p of packages) {
if (!this.packages.includes(p))
this.packages.push(p);
}
}
//the currently registered EPackages
static getPackages() {
return TJson.packages;
}
// Simple check - warn if no packages registered
static warnIfNotInitialized() {
if (this.packages.length === 0) {
console.warn('TJson: No packages registered. Call TJson.setPackages([...]) or ' +
'import and touch your package (e.g., MyPackage.eINSTANCE) before using TJson.');
}
}
/**
* Converts a TMF EObject to JSON.
*
* @param obj
*/
static makeJson(obj) {
this.warnIfNotInitialized();
return this.eObjectToJsonAux(obj, new Map(), false);
}
/**
* Converts an object in JSON Object into a TMF EObject.
* Creates proxy objects for unresolved non-containment references.
*
* @param json
* @return
*/
static makeEObject(jsonObj) {
this.warnIfNotInitialized();
const serializedReferences = new Array();
//build containment heirarchy
const eobj = this.jsonToEObject(jsonObj, serializedReferences);
if (!eobj)
return undefined;
//build index of all contained objects
const idsToObjs = new Map();
eobj.eAllContents().forEach((elem) => {
idsToObjs.set(elem.fullId(), elem);
});
//deserialize references, collecting unresolved ones
const unresolvedRefs = new Array();
serializedReferences.forEach((ref) => {
if (!ref.deserialize(idsToObjs)) {
unresolvedRefs.push(ref);
}
});
// Create proxies for unresolved references
unresolvedRefs.forEach((ref) => {
this.createAndSetProxy(ref, idsToObjs);
});
return eobj;
}
/**
* Creates a proxy object for an unresolved reference and sets it on the source object.
*
* @param ref The unresolved serialized reference
* @param idsToObjs Map of all resolved objects
*/
static createAndSetProxy(ref, idsToObjs) {
const fromObj = idsToObjs.get(ref.fromId);
if (!fromObj)
return;
const feature = fromObj.eClass().getEStructuralFeature(ref.refName);
if (!feature || !(feature instanceof EReferenceImpl))
return;
// Check if proxy already exists for this target
let proxy = idsToObjs.get(ref.toId);
if (!proxy) {
proxy = this.createProxy(ref.toId, feature);
if (proxy) {
// Add proxy to the index so future references to it can be resolved
idsToObjs.set(ref.toId, proxy);
}
}
if (proxy) {
// Set the reference
if (feature.isMany()) {
fromObj.eGet(feature).add(proxy);
}
else {
fromObj.eSet(feature, proxy);
}
}
}
/**
* Creates a proxy EObject for an unresolved reference.
*
* @param fullId The full ID of the target object (format: "ClassName_actualId")
* @param reference The reference feature to determine the target EClass
* @returns A proxy EObject or undefined if creation failed
*/
static createProxy(fullId, reference) {
// Parse the fullId to extract class name and actual ID
const underscoreIndex = fullId.indexOf('_');
if (underscoreIndex === -1) {
console.warn(`Invalid fullId format for proxy creation: ${fullId}`);
return undefined;
}
const className = fullId.substring(0, underscoreIndex);
const actualId = fullId.substring(underscoreIndex + 1);
// Get the target EClass from the reference type
const targetEClass = reference.getEType();
if (!targetEClass) {
console.warn(`No target EClass found for reference: ${reference.getName()}`);
return undefined;
}
// Verify the class name matches (safety check)
if (targetEClass.getName() !== className) {
console.warn(`Class name mismatch for proxy: expected ${className}, got ${targetEClass.getName()}`);
// Continue anyway - the reference type is authoritative
}
// Find the appropriate EPackage and factory
for (const pkg of this.packages) {
const classifiers = pkg.getEClassifiers();
for (const classifier of classifiers) {
if (classifier === targetEClass) {
try {
// Create the proxy object
const proxy = pkg.getEFactoryInstance().create(targetEClass);
// Set the ID attribute if it exists
const idAttribute = targetEClass.getEIDAttribute();
if (idAttribute) {
try {
// Parse the ID value to the correct type using TUtils
const parsedId = TUtils.parseAttrValFromString(idAttribute, actualId);
proxy.eSet(idAttribute, parsedId);
}
catch (error) {
console.warn(`Failed to parse ID value for proxy: ${actualId}`, error);
}
}
// Mark as proxy
proxy.eSetProxy(true);
// console.debug(`Created proxy object for ${fullId}`);
return proxy;
}
catch (error) {
console.error(`Failed to create proxy for ${fullId}:`, error);
return undefined;
}
}
}
}
console.warn(`No EPackage found containing EClass ${targetEClass.getName()} for proxy creation`);
return undefined;
}
/**
* Converts a TMF EObject array to JSON array.
*
* @param obj
*/
static makeJsonArray(objs) {
this.warnIfNotInitialized();
const jsonArray = [];
if (objs) {
objs.forEach((element) => {
jsonArray.push(this.makeJson(element));
});
}
return jsonArray;
}
/**
* Converts an object in JSON Array into am array of TMF EObjects.
*
* @param json
* @return
*/
static makeEObjectArray(jsonArray) {
this.warnIfNotInitialized();
const eobjArray = [];
jsonArray.forEach((element) => {
const eobj = this.makeEObject(element);
if (eobj)
eobjArray.push(eobj);
});
return eobjArray;
}
static jsonToEObject(jsonObj, serializedRefs) {
if (jsonObj == null) {
throw new Error('ERROR: null value for JSON Object. Cannot convert to EObject.');
}
//get the type indicator and instantiate the TMF Object
const objectType = jsonObj[this.JSON_FIELD_TYPESCRIPT_TYPE];
if (objectType == null) {
console.error('ERROR: No value for ' +
this.JSON_FIELD_TYPESCRIPT_TYPE +
' was specified for the JSON object: ' +
jsonObj);
return undefined;
}
let eClass;
let eObj;
for (const pkg of this.packages) {
eClass = this.eClassByNameCaseInsensitive(objectType, pkg);
if (eClass) {
eObj = pkg.getEFactoryInstance().create(eClass);
break;
}
}
if (eClass && eObj) {
//handle all primitive attributes
this.setPrimitiveValuesOnJson(jsonObj, eObj);
//handle all references
for (const ref of eObj.eClass().getEAllReferences()) {
this.deserializeReferencedObjects(jsonObj, eObj, ref, serializedRefs);
}
}
return eObj;
}
static deserializeReferencedObjects(jsonObj, dObj, ref, serializedRefs) {
const jsonFieldName = this.getJsonFieldName(ref);
//multi-valued references
if (ref.isMany()) {
this.deserializeManyValuedReference(jsonObj, jsonFieldName, ref, dObj, serializedRefs);
} //single-valued references
else {
this.deserializeSingleValuedReference(jsonObj, jsonFieldName, ref, dObj, serializedRefs);
}
}
static deserializeSingleValuedReference(jsonObj, jsonFieldName, ref, tObj, serializedRefs) {
const referencedObj = jsonObj[jsonFieldName];
if (referencedObj) {
if (ref.isContainment()) {
const containeTJsonObj = referencedObj;
if (!containeTJsonObj[this.JSON_FIELD_TYPESCRIPT_TYPE]) {
containeTJsonObj.put(this.JSON_FIELD_TYPESCRIPT_TYPE, ref.getEType().getName());
}
const referencedEmfObj = this.jsonToEObject(containeTJsonObj, serializedRefs);
tObj.eSet(ref, referencedEmfObj);
}
else {
serializedRefs.push(SerializedReference.create(tObj.fullId(), referencedObj, ref.getName()));
}
}
}
static deserializeManyValuedReference(jsonObj, jsonFieldName, ref, tObj, serializedRefs) {
const jsonArray = jsonObj[jsonFieldName];
if (jsonArray) {
jsonArray.forEach((containedTJsonObj, index) => {
if (ref.isContainment()) {
if (!containedTJsonObj[this.JSON_FIELD_TYPESCRIPT_TYPE]) {
containedTJsonObj[this.JSON_FIELD_TYPESCRIPT_TYPE] = ref
.getEType()
.getName();
}
const containedDObj = this.jsonToEObject(containedTJsonObj, serializedRefs);
tObj.eGet(ref).add(containedDObj);
}
else {
serializedRefs.push(SerializedReference.create(tObj.fullId(), containedTJsonObj, ref.getName()));
}
});
}
}
static eObjectToJsonAux(obj, serializedSoFar, attributesOnly) {
//make sure there is really an object to convert
if (obj == null) {
return null;
}
const jsonObj = {};
//Generate IDs for all Entities in the heirarchy which do not have them
if (!TUtils.getOrCreateIdForObject(obj))
TUtils.genIdIfNotExists(obj);
//add a type indicator for everything (the class name)
jsonObj[this.JSON_FIELD_TYPESCRIPT_TYPE] = obj.eClass().getName();
//handle all primitive attributes
this.attributesToJson(obj, jsonObj);
//handle all references
if (!attributesOnly) {
this.referencesToJson(obj, serializedSoFar, jsonObj);
}
return jsonObj;
}
static referencesToJson(obj, serializedSoFar, jsonObj) {
for (const ref of obj.eClass().getEAllReferences()) {
if (!ref.isVolatile() && !ref.isTransient()) {
//multi-valued references
if (ref.isMany()) {
this.manyValuedReferenceToJson(obj, ref, serializedSoFar, jsonObj);
}
//single-valued references
else {
this.singleValuedRefToJson(obj, ref, serializedSoFar, jsonObj);
}
}
}
}
static manyValuedReferenceToJson(obj, ref, serializedSoFar, jsonObj) {
const jsonFieldName = this.getJsonFieldName(ref);
const referencedColl = obj.eGet(ref);
const jsonArray = [];
for (const referencedObj of referencedColl) {
if (referencedObj != null) {
if (ref.isContainment()) {
const referenceTJsonObj = this.eObjectToJsonAux(referencedObj, serializedSoFar, !ref.isContainment());
if (referenceTJsonObj != null)
jsonArray.push(referenceTJsonObj);
}
else {
const serializedRef = new SerializedReference(TUtils.getOrCreateIdForObject(obj).toString(), TUtils.getOrCreateIdForObject(referencedObj).toString(), jsonFieldName).serialize();
jsonArray.push(serializedRef);
}
}
}
jsonObj[jsonFieldName] = jsonArray;
}
static singleValuedRefToJson(obj, ref, serializedSoFar, jsonObj) {
const jsonFieldName = this.getJsonFieldName(ref);
const referencedObj = obj.eGet(ref);
if (referencedObj != null) {
let referenceTJsonObj = null;
//if the object is 'contained', we generate it's full representation
if (ref.isContainment()) {
referenceTJsonObj = this.eObjectToJsonAux(referencedObj, serializedSoFar, !ref.isContainment());
}
//otherwise, we generate a serialized pointer to the referenced object
else {
if (referencedObj.eClass().getEIDAttribute()) {
referenceTJsonObj = new SerializedReference(TUtils.getOrCreateIdForObject(obj).toString(), TUtils.getOrCreateIdForObject(referencedObj).toString(), jsonFieldName).serialize();
}
}
if (referenceTJsonObj != null)
jsonObj[jsonFieldName] = referenceTJsonObj;
}
}
static attributesToJson(obj, jsonObj) {
for (const attr of obj.eClass().getEAllAttributes()) {
if (!attr.isVolatile() && !attr.isTransient()) {
const jsonFieldName = this.getJsonFieldName(attr);
const origVal = obj.eGet(attr);
const convertedVal = this.primitiveValueToJson(attr, origVal);
jsonObj[jsonFieldName] = convertedVal;
}
}
}
/**
* Converts a primitive value (or a Date) for use inside JSON.
*
* @param val
* @return
*/
static primitiveValueToJson(attr, val) {
//serialize many-valued eattributes
if (attr.isMany()) {
const toRet = [];
for (const v of val) {
toRet.push(v instanceof Date ? v.toJSON() : v);
}
return toRet;
}
if (val instanceof Date) {
return val.toJSON();
}
return val;
}
static getJsonFieldName(feature) {
return feature.getName();
}
static eClassByNameCaseInsensitive(objectType, pkg) {
let dClass = pkg.getEClassifier(objectType);
if (dClass) {
//case insensitive checking of names
for (const eclass of pkg.getEClassifiers()) {
if (eclass instanceof EClassImpl &&
eclass.getName().toLowerCase() === objectType.toLowerCase()) {
dClass = eclass;
}
}
}
return dClass;
}
/**
* Sets all primitive (EAttribute) values on the eobject, given a
* JSONObject with the same fields.
*
* @param propsObj
* @param eObj
*/
static setPrimitiveValuesOnJson(propsObj, eObj) {
for (const attr of eObj.eClass().getEAllAttributes()) {
const jsonFieldName = this.getJsonFieldName(attr);
const jsonVal = propsObj[jsonFieldName];
if (jsonVal || jsonVal === 0) {
if (attr.isMany()) {
const array = jsonVal;
const coll = eObj.eGet(attr);
for (const o of array) {
coll.add(TUtils.parseAttrValFromString(attr, o));
}
}
else {
const tVal = TUtils.parseAttrValFromString(attr, jsonVal);
if (tVal || tVal === 0) {
eObj.eSet(attr, tVal);
}
else {
console.warn('JSON parse failed for ' +
eObj.eClass().getName() +
'.' +
attr.getName() +
' with value ' +
jsonVal.toString());
}
}
}
}
}
}
//# sourceMappingURL=tjson.js.map