Noticing changes on serialized model [message #1808431] |
Mon, 24 June 2019 09:14  |
Eclipse User |
|
|
|
Hello everyone,
I have an application build a dynamic model at runtime based on a user-specified meta model and then persist this concrete model in an XMI-format.
The user then may modify this created model like reassigning certain values to existing attributes of classes in the model on the XMI model file.
The model elements in the Ecore model have counter parts in the application itself which need to get modified accordingly when the user changes something in the XMI model file.
I currently have the contents of the old model (as an XMIResource) as well as the contents of the new model after it got modified (also as an XMIResource). I now need a way to find out what changed from the new version to the old one to modify the programmatic counterparts of the model elements, delete them if the model element got deleted or create new ones if a model element got added (basic CRUD methods).
But I do not seem to find a way to identify corresponding elements after a modification of the element in the XMI. How do I know that an element got modified and I need to modify the existing programmatic counterpart, and it isn't a new one of the same type with different parameters? Let me give an example:
Lets say I have an Ecore meta model "designmodel" consisting of a single class "State" that has one attribute "name" of type String. So at runtime a concrete model instance gets built with the name-attribute being set to "Waiting" and saved into an XMI file like so:
<?xml version="1.0" encoding="ASCII"?>
<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:designmodel="http://www.example.org/designmodel">
<designmodel:State name="Waiting"/>
</xmi:XMI>
Now the model gets modified through a renaming of the name-attribute to the new value "Ready":
<?xml version="1.0" encoding="ASCII"?>
<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:designmodel="http://www.example.org/designmodel">
<designmodel:State name="Ready"/>
</xmi:XMI>
I now would want to get the corresponding element in the application that represents this State-object and rename its name-value from "Waiting" to "Ready" as well, without changing anything else in it (so I explicitly cannot just generate a new State-Element from scratch and throw away the old one since there is additional data in it that does not get persisted into the XMI).
Is there a way to get something like a unique identifier for the model element after having persisted it as an XMI? I know the EContentAdapter can act as some kind of listener to model changes, but I guess that is only applicable when having the model loaded in the app while it gets changed. But I dont know how my user would be able to change the model if not by modifying the persisted XMI-file. I would need something like a unique ID for each model element, then compare the values of the model element with the same ID from the previous model to the modified one and then act upon the differences between the 2 versions.
Is there any way this could be done? The key requirement for my application is that a model gets created at runtime and the internal data and the model representation of it shall stay synchronized. Meaning when the internal program data gets changed I can just regenerate the model, but when the model changes I need to propagate these changes back to the internal program data somehow. My only thought to implement this and to give the user a way to modify the created model was to persist it as an XMI and then let the user directly modify the XMI. If it is not possible to solve this problem this way I would be open for other solutions as well.
Thanks a lot!
|
|
|
|
|
|
|
|
|
Re: Noticing changes on serialized model [message #1808605 is a reply to message #1808568] |
Thu, 27 June 2019 13:00   |
Eclipse User |
|
|
|
So I went to the GenPackage and set the "Resource Type" property to "XMI" ("File Extensions" property for me is "designmodel" btw) and regenerated the model code. I then went to the DesignmodelResourceImpl.java and overwrote the useUUIDs function to return true. The plugin.xml of the respective Ecore-project then contained - as you pointed out - this:
<extension point="org.eclipse.emf.ecore.extension_parser">
<!-- @generated designmodel -->
<parser
type="designmodel"
class="mic.model_code_synchronization.designmodel.util.DesignmodelResourceFactoryImpl"/>
</extension>
Where it stops working for me atm I guess is that this Factory does not get used when I create my instances, since when inspecting the XMI it contains nothing like UUIDs and resSet.getID(myDeserializedEObject) returns "null" as well.
Maybe there is a problem with how I create the resource? You wrote this factory would be used automatically when loading resources of the specified file extension (so ".designmodel" in my case), but I never do that. As written earlier I get the underlying Ecore model and its respective factory at runtime via the .ecore-file like this:
ResourceSet rs = new ResourceSetImpl();
rs.getResourceFactoryRegistry().getExtensionToFactoryMap().put("ecore", new XMIResourceFactoryImpl());
Resource res = rs.createResource(URI.createFileURI("C:/.../.../designmodel.ecore"));
res.load(null);
EPackage metapackage = (EPackage) res.getContents().get(0);
EFactory metaFactory = metapackage.getEFactoryInstance();
and then I create the initial XMI resource that gets saved via this "EFactory metaFactory".
So I never load an instance via the specified file extension ".designmodel", which is why I guess the factory that uses UUIDs does not get used and I do not receive the UUIDs in my instances. I only load the .ecore file to receive the ecore-model at runtime, save the initally created instance as an XMI as shown in the next code snippet and reload this XMI later on to notice differences on it:
ResourceSet savingResSet = new ResourceSetImpl();
savingResSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put("xmi", new XMIResourceFactoryImpl());
XMIResource savingRes = (XMIResource) savingResSet.createResource(URI.createFileURI(outputPath), null);
savingRes.getDefaultSaveOptions().put(XMIResource.OPTION_KEEP_DEFAULT_CONTENT, Boolean.TRUE);
//adding the created EObjects to this savingRes...
savingRes.save(Collections.EMPTY_MAP);
//this returns null
saving.getID(someEObject_I_created);
So how exactly do I get the factory with the overriden useUUIDs-method to run when I only know at runtime which exact factory this should be? Otherwise I guess I could just rewrite the 2nd line of code above to this
rs.getResourceFactoryRegistry().getExtensionToFactoryMap().put("ecore", new DesignmodelResourceFactoryImpl());
but I do not know this exact factory at design time as already stated. Or is the problem with the way I initially serialize my instance into the XMI (maybe through the casting to XMIResource for the savingRes-variable?)?
I am very sorry for currently not seeing the forest for the trees, but I do not seem to get this to work with my beginner EMF knowledge at the moment.
Thanks a lot for all your time :)
EDIT:
A workaround I thought would be to just call savingRes.setID(myEObject, UUID.randomUUID().toString()); before saving the instance as an XMI. Maybe this will work as well to identify the changed instances, but I would ofc be interested in the "right way" still :)
[Updated on: Thu, 27 June 2019 13:41] by Moderator
|
|
|
|
|
|
|
Re: Noticing changes on serialized model [message #1808862 is a reply to message #1808652] |
Wed, 03 July 2019 08:37   |
Eclipse User |
|
|
|
Hey again,
I have a follow-up question:
As stated I want to be able to synchronize the model instance with some internal data it represents. I can now identify changes on the persisted instance (via their UUID) and update the internal data according to user changes at the persisted instance.
I need full CRUD operations though, meaning I want to create, update or delete this internal data according to respective changes to the model instance. Currently I can only update. When trying to synchronize delete or create operations on the model I get errors:
So whenever a user changes the serialized model instance through simple modifications like renaming existing attributes there is no problem. I can then identify the respective old model instance - EObject through the ID and do the transformations on the internal data object according to the user changes to the model instance. But whenever the user deletes a whole element (or adds a new one, its the exact same it seems) in the serialized instance, I always get NULL-returns when trying to retrieve any EObject via their UUID. It seems like something gets broken as soon as the user modifications on the serialized instance go further than simply renaming attributes and it crashes when whole elements get removed or added.
Here the relevant code with some comments:
//method getting called with the user-modified instance
public void updateDataRepresentation(Resource updatedModel) {
updatedModel.getContents().forEach(updatedModelElement -> {
//getting respective model element in existing designmodel (existentDesignmodel is simply a Resource of the previously saved version of the instance)
EObject existentModelElement = existentDesignmodel.getEObject(updatedModel.getID(updatedModelElement));
if(existentModelElement == null) {
//no existend model element there yet, meaning it got added by the user through modifications at the design model
System.out.println(updatedModelElement + " was newly created");
//TODO create data object
return;
}
//MappingEntry is a data object consisting of the EObject and its respective data object it gets mapped to (+ some other stuff)
//see below for these helper-methods
MappingEntry entry = getMappingEntryByModelelement(existentModelElement);
if(entry == null) {
entry = getMappingEntryByID(updatedModel.getID(updatedModelElement));
}
//this is where I change the internal date object according to the (maybe updated) new model element
entry = updateMappingEntry(entry, updatedModelElement);
});
[...] //here I also go through the existentDesignmodel-contents to check if it contains an element that got removed in the updatedModel to then delete it in the respective data objects I maintain
//setting the existent model to the newly updated one after modifying the data objects in my code
this.existentDesignmodel = updatedModel;
}
//helper methods from above
private MappingEntry getMappingEntryByModelelement(EObject modelelement) {
for(MappingEntry e: this.mappings) {
if(e.getDesignmodelElement().equals(modelelement))
return e;
}
return null;
}
private MappingEntry getMappingEntryByID(String id) {
for(MappingEntry e: this.mappings) {
EObject o = e.getDesignmodelElement();
String existentID = this.existentDesignmodel.getID(o);
if(existentID.contentEquals(id))
return e;
}
return null;
}
Now when having added a new element into the model instance or removed a whole element, I always get a NULL-return from from both my helper methods (both trying to find it via the EObject.equals or the UUID). Lets say the serialized instance looks like the following, and then I remove the 2nd State-element with name "Waiting" or add another one etc.:
<designmodel:State xmi:id="cb33c7a3-8769-479e-bbc3-f4b36363b658" name="Ready"/>
<designmodel:State xmi:id="f9ad4afa-1d4f-46cf-982d-d49cdbaf8a90" name="Waiting"/>
I honestly have no idea why they do not work, since they work as intended when just changing attribute-values in the instance (like renaming "Waiting" to "Finished" works totally fine and synchronizes these changes as intended). But as soon as I add or remove whole elements of the serialized instance, they cannot find the existing elements (even for the ones that did not get changed, added, or deleted).
Does anyone have an idea on why that is? Is it maybe something with EMF when adding or removing instance-elements? Or is it something that should work and the error has to lie somewhere else in my own code?
[Updated on: Wed, 03 July 2019 08:40] by Moderator
|
|
|
|
|
Re: Noticing changes on serialized model [message #1809613 is a reply to message #1808943] |
Thu, 18 July 2019 16:31   |
Eclipse User |
|
|
|
Hello, it's me again with a related follow-up question in the same program I am still working on :)
In the meanwhile I have added some model-features I want to synchronize with internal data that is modeled as containment-references. I have a class "State" in my ecore-model with a containment-reference "transition" that points to instances of a second type "Transition" with the cardinality 0..* (so implemented as EList<Transition>).
When initially creating the model instance everything works fine and let's say I receive the following serialized version:
<?xml version="1.0" encoding="ASCII"?>
<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:designmodel="http://www.example.org/designmodel">
<designmodel:State xmi:id="0f359d4a-5154-462c-90aa-e125197cdb6d" name="Ready">
<transition href="#7880aa8f-1e86-42e0-a212-e91326292d31"/>
</designmodel:State>
<designmodel:Transition xmi:id="7880aa8f-1e86-42e0-a212-e91326292d31" name="switchToWaiting"/>
</xmi:XMI>
Deleting or updating the existent Transition-instance works completely fine (via the respective algorithms being called after comparing the 2 lists of xmi:id's as described in the earlier post). But when adding a new Transition-instance to the model I seem to get weird behavior:
Let's say I add another Transition instance, give it some random new xmi:id and reference it in the State-instance, so that it should be contained by it:
<?xml version="1.0" encoding="ASCII"?>
<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:designmodel="http://www.example.org/designmodel">
<designmodel:State xmi:id="0f359d4a-5154-462c-90aa-e125197cdb6d" name="Ready">
<transition href="#7880aa8f-1e86-42e0-a212-e91326292d31"/>
<transition href="#11111111-2222-3333-4444-ffffffffffff"/>
</designmodel:State>
<designmodel:Transition xmi:id="7880aa8f-1e86-42e0-a212-e91326292d31" name="switchToWaiting"/>
<designmodel:Transition xmi:id="11111111-2222-3333-4444-ffffffffffff" name="userAddedTransition"/>
</xmi:XMI>
Now, when I read in this xmi-version of the instance, iterate over all objects and check for their container via EObject.eContainer(), I receive the containing EObject for all instance-elements but for the ones that the user created. So for the newly added Transition-instance with the attribute name=userAddedTransition the eContainer()-call returns null for some reason. When checking eContents() on the containing State-EObject, I also respectively only get the Transition instances of the transition-reference that were there prior, the user-added one is missing here as well.
It would seem to me that the behavior should be the exact same, since I deserialize all objects from the same xmi and there should be no difference for whether it was there in the first place or inserted by some user action on the model-instance. The added Transition-instance gets recognized correctly and I can also read in their name-attributes etc., but the eContainer for some reason is null and not the containing object I referenced it in. But I cannot think of anything else that would have to be done for it to be recognized correctly as a contained-reference other than adding the <transition href=.../> line referencing the xmi.id of the newly created Transition-instance. Since this is the only thing in the serialized xmi indicating an existing containment-reference from existing objects as well.
Does anyone have an idea on why this could be?
Any help would be greatly appreciated again.
[Updated on: Fri, 19 July 2019 04:31] by Moderator
|
|
|
|
|
|
Re: Noticing changes on serialized model [message #1809676 is a reply to message #1809669] |
Fri, 19 July 2019 15:45   |
Eclipse User |
|
|
|
classReference.isContainment() is TRUE, this is the reference from State to Transition (so with the name "transition" in the following model). Transition-objects also are only contained in exactly one State-object, so this shouldnt be a problem. The relevant .ecore model looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="designmodel" nsURI="http://www.example.org/designmodel" nsPrefix="designmodel">
<eClassifiers xsi:type="ecore:EClass" name="State">
<eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2003/XMLType#//String"
iD="true"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="transition" upperBound="-1"
eType="#//Transition" containment="true"/>
</eClassifiers>
<eClassifiers xsi:type="ecore:EClass" name="Transition">
<eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2003/XMLType#//String"
iD="true"/>
</eClassifiers>
</ecore:EPackage>
So this definitely is a containment reference. When parsing in the serialized .xmi-model-instance (after a user has added a new Transition-object and added a containment href just like the existing ones to the State-object), I go about it like this (behaviour in comments):
//loading in the updated model via a helper method as an XMIResource
XMIResource updatedModel = Utility.loadExistingModel(existingModelPath);
//iterating over the objects directly contained in the resource (so all State- and Transition-objects get reached here, since they are root objects themselves)
updatedModel.getContents().forEach(updatedModelElement -> {
System.out.println(updatedModelelement + updatedModelElement.eContainer());
});
I also just realized that I was incorrect formerly, because this eContainer call always returns NULL - also for existing model elements that were not added by the user. I just did not get the error because I used the internal version of the same object if it did not get added by the user (so if updatedModelElement also exists in the prior model version which I still have in memory and compare with to identify newly added elements).
So yeah, you were right, and I guess I never serialize the Transition-objects into the XMI as actual containments. When stepping through the debugger and looking at the XMIResources I can see this too: For the existing one I have in memory, the eContainer's of the Transition-objects are set correctly to the respective State-object, but in the deserialized XMIResource all eContainer-values are NULL.
So my question would then change to: How do I serialize it in a way, that they are actually contained by the State-object i added them to? I posted in my previous post how I go about adding them to the containment-reference via getting the eContents-list, adding the Transition-object to it, and then setting it as the EStructuralFeature "transition" of the State-object. I guess this is where my mistake is, since this does not seem to work as intended.
[Updated on: Sat, 20 July 2019 02:31] by Moderator
|
|
|
|
Re: Noticing changes on serialized model [message #1809705 is a reply to message #1809701] |
Sun, 21 July 2019 09:03  |
Eclipse User |
|
|
|
Hey,
you are right, I mistakenly added the contained EObjects to the Resource on their own as well. So they were then contained twice in the serialized model-instance: once without a container and once within the container.
Simply checking for each created EObject whether it is contained by another object to then not add it again (due to it already being implicitly serialized from within its container object) solved everything.
Thanks a lot for your help, I truely appreciate it!
|
|
|