Skip to main content


Eclipse Community Forums
Forum Search:

Search      Help    Register    Login    Home
Home » Modeling » TMF (Xtext) » Can I make Xtext throw, when a containment reference is reassigned?(I want to forbid assigning a containment reference to another container)
Can I make Xtext throw, when a containment reference is reassigned? [message #1832810] Fri, 25 September 2020 12:55 Go to next message
Jan Hermes is currently offline Jan HermesFriend
Messages: 27
Registered: September 2020
Junior Member
Hello fellow Xtexters,

while designing my language and especially its type-system, I a specific requirement reoccurs sometimes:

I want to forbid reassigning containment-references to other containers.

I want to do it so I avoid bugs that occur when I reassign a parsed containment-reference to an JIT-model-element.

So there are two situations that should be valid for me:



  1. copy the containment-reference
  2. wrap the containment-reference in a model element as a non-containment reference



In all other situations where a containment-reference gets "stolen" from its container I want java to throw an exception.

Is this easily possible to achieve?

Greetings, Jan

[Updated on: Fri, 25 September 2020 13:19]

Report message to a moderator

Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832821 is a reply to message #1832810] Fri, 25 September 2020 15:23 Go to previous messageGo to next message
Ed Willink is currently offline Ed WillinkFriend
Messages: 7655
Registered: July 2009
Senior Member
Hi

This is the child stealing phenomenon that I discussed in Section 3.4 of http://www.eclipse.org/mmt/qvt/docs/ICMT2014/QVTtraceability.pdf

The solution may be found in https://git.eclipse.org/r/plugins/gitiles/ocl/org.eclipse.ocl/+/refs/heads/master/plugins/org.eclipse.ocl.pivot/src/org/eclipse/ocl/pivot/internal/utilities/PivotObjectImpl.java

The override of eBasicSetContainer asserts that it is not a steal; change to a throw if you like. Ensure all your classes inherit the override.

The PivotUtilInternal helper

/**
* Detach object from its container so that a child-stealing detection is avoided when attaching to a new container.
*/
public static void resetContainer(@NonNull EObject eObject) {
EStructuralFeature eContainingFeature = eObject.eContainingFeature();
if (eContainingFeature != null) {
EObject eContainer = eObject.eContainer();
if (eContainer != null) {
if (!eContainingFeature.isMany()) {
eContainer.eSet(eContainingFeature, null);
}
else {
Object objects = eContainer.eGet(eContainingFeature);
if (objects instanceof List<?>) {
((List<?>)objects).remove(eObject);
}
}
}
}
}

allows code that is relocating rather than stealing to disable the detector by explicitly setting the container to null before something better is assigned.

Regards

Ed Willink


Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832822 is a reply to message #1832821] Fri, 25 September 2020 15:52 Go to previous messageGo to next message
Christian Dietrich is currently offline Christian DietrichFriend
Messages: 14665
Registered: July 2009
Senior Member
I don't think we talk about emf containment here
But rather a concept in your dsl right ?


Twitter : @chrdietrich
Blog : https://www.dietrich-it.de
Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832825 is a reply to message #1832822] Fri, 25 September 2020 18:01 Go to previous messageGo to next message
Ed Willink is currently offline Ed WillinkFriend
Messages: 7655
Registered: July 2009
Senior Member
HI

The single container problem is the same in Ecore or UML. There can only be one container, so re-assignment is potentially a stealing hazard.

Regards

Ed Willink
Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832860 is a reply to message #1832825] Mon, 28 September 2020 05:33 Go to previous messageGo to next message
Jan Hermes is currently offline Jan HermesFriend
Messages: 27
Registered: September 2020
Junior Member
Hi,

thanks a lot for your answer @Ed Willlink, and the useful links!

I will try to incorporate the solution into my approach soon.

@Christian Dietrich:
I'm actually talking about emf containment, maybe I should give a better example and I maybe did missunderstand some detail of emf.

Simple Language:
Model:
  Foo | Symbol | Container;

Foo: "foo" name=ID;

// this container has "Foo" containment references
Container: 
"container" "{"
    elements+=(Foo|Symbol)*
"}";

// this container has "Foo" non-containment references
NonContainer:
"non-container" "{"
  elements+=[Foo]
"};

// this construct has exactly one "Foo" non-containment reference
Symbol: "@" ref=[Foo];


Now to demonstrate the "stealing" here is some trivial code:

static val factory = MyDslFactory.eINSTANCE

def someFunc(Container victim) {

  // assert that victim only holds Foo containment references (no "Symbol") -- just for demonstration purposes

  val thief = factory.createContainer => [
    elements+=victim.elements
  ]
  // now "thief" owns all containment-refs of "victim" => victim.elements.empty == true

  val copycat = factory.createContainer => [
    elements+= victim.elements.map[e | EcoreUtil2.copy(e)]
  ]
  // now "copycat" has (deep?)-copys of all containment references of "victim", so victim stays unchanged

  val delegator = factory.createContainer => [
    elements+= victim.elements.map[e | factory.createSymbol => [ref=e] ]
  ]
  // now "delegator" has symbols to all of "victims" elements => "victim" also stays unchanged
}


Do I understand this correctly?
Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832861 is a reply to message #1832860] Mon, 28 September 2020 05:56 Go to previous messageGo to next message
Christian Dietrich is currently offline Christian DietrichFriend
Messages: 14665
Registered: July 2009
Senior Member
So you want to prevent the dsl developer from messing the emf tree and not the dsl user ?
Or is your dsl user expected to write the Xtend code ?


Twitter : @chrdietrich
Blog : https://www.dietrich-it.de
Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832862 is a reply to message #1832861] Mon, 28 September 2020 06:16 Go to previous messageGo to next message
Jan Hermes is currently offline Jan HermesFriend
Messages: 27
Registered: September 2020
Junior Member
Exactly, I want to prevent myself as a developer from messing with the emf tree. The solution could also be some temporal assertion which I could disable when the developing process is over.

It's just that I ran into situations where I accidentally "stole" an object from somewhere in the emf tree which produced uneasy errors because I did not expect some container to be empty.

Also because the error itself is completely unrelated to the error-producing code... E.g. it could be that I "steal" an object early in the chain of processes. And at some much later point (e.g. when the generator is started) some object suddenly is missing a containment-reference and I have to search in my code-base where I accidentally stole it.
Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832867 is a reply to message #1832862] Mon, 28 September 2020 07:28 Go to previous messageGo to next message
Ed Willink is currently offline Ed WillinkFriend
Messages: 7655
Registered: July 2009
Senior Member
Hi

What you describe is precisely the use case that prompted my eBasicSetContainer / resetContainer instrumentation/redefinition of containment change semantics.

They have been invaluable in debugging tree-rewrite algorithms that weren't quite right. Every so often they slap me on the wrists before my new code provokes yet another hard to debug nightmare. A few explicit calls to resetContainer are a very small and arguably desirable price to pay.

Regards

Ed Willink
Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832868 is a reply to message #1832867] Mon, 28 September 2020 07:42 Go to previous messageGo to next message
Ed Merks is currently offline Ed MerksFriend
Messages: 33137
Registered: July 2009
Senior Member
What I do is set a conditional breakpoint at this line:

https://git.eclipse.org/c/emf/org.eclipse.emf.git/tree/plugins/org.eclipse.emf.ecore/src/org/eclipse/emf/ecore/impl/BasicEObjectImpl.java#n1398

I.e., here in BasicEObjectImpl:
  public NotificationChain eBasicSetContainer(InternalEObject newContainer, int newContainerFeatureID, NotificationChain msgs)
  {
    InternalEObject oldContainer = eInternalContainer();
    Resource.Internal oldResource = this.eDirectResource();
After the assignment of oldContainer, the condition tests "oldContainer != null && newContainer != null". In other words, stop the process when the container is changing from non-null to non-null. So simply removing an object, or just adding an object that is not already contained elsewhere will not stop.


Ed Merks
Professional Support: https://www.macromodeling.com/

[Updated on: Mon, 28 September 2020 07:43]

Report message to a moderator

Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832874 is a reply to message #1832868] Mon, 28 September 2020 11:53 Go to previous messageGo to next message
Jan Hermes is currently offline Jan HermesFriend
Messages: 27
Registered: September 2020
Junior Member
Hi,

yes I also really wanted this to help me to avoid bugs too.

For now I will be using this approach until it's not working anymore:


Use an adapter that will be added to every EObject created while I develop the language-code

The adapter essentially has an internal map of potentially stolen objects and throws if it receives the
notification of an object that was added and remove before from somewhere else.

@Singleton
class StealingAdapter extends AdapterImpl {

	Map<Object, Object> stolenObjects = newHashMap
	
	override notifyChanged(Notification notification) {
		switch (notification.eventType) {
			case Notification.REMOVE: containerRemove(notification.notifier, notification.oldValue)
			case Notification.ADD: containerAdd(notification.notifier, notification.newValue)
		}
	}
	
	def containerAdd(Object potentialThief, Object wallet) {
		val maybeVictim = Optional.ofNullable(stolenObjects.get(wallet))
		
		if (maybeVictim.present) {
			throw new IllegalStateException('''«wallet» was stolen from «maybeVictim.get»''')
		}
	}
	
	def containerRemove(Object potentialVictim, Object wallet) {
		this.stolenObjects.put(wallet, potentialVictim)	
	}
}


Add the adapter upon creation of EObjects for the semantic model

I just wrote a custom "create()" method for the ECoreElementFactory and bound this custom factory in the runtime module.

class MyDslEcoreElementFactory extends DefaultEcoreElementFactory {
	
	@Inject MyDslStealingAdapter adapter
	
	override EObject create(EClassifier classifier) {
		val eobj = super.create(classifier)

		eobj.eAdapters().add(adapter)	
		eobj
	}
}


Add the adapter upon creation of objects from the generic MyDslFactory

This is just a delegating Factory that uses use the delegate to create objects but adds adapters before creating them.
I need to implement this process for each create... method of the MyDslFactory interface, which is a bit ugly, but I couldn't get
a better generic solution for now.

public class MyDslAdapterAddingFactory extends MyDslSwitch<EObject> implements MyDslFactory {

	@Inject MyDslStealingAdapter adapter;
	private MyDslFactory delegate;

	
	public MyDslAdapterAddingFactory(MyDslFactory delegate) {
		this.delegate = delegate;
	}

	@Override
	public EObject defaultCase(EObject obj) {
		// final MyDslStealingAdapter adapter = adapterProvider.get();
		obj.eAdapters().add(adapter);
		return obj;
	}
	
	@SuppressWarnings("unchecked")
	public <T extends EObject> T adapt(T obj) {
		return (T) doSwitch(obj);
	}


	public Model createModel() {
		return adapt(delegate.createModel());
	}

	public createFoo createFoo() {
		return adapt(delegate.createFoo());
	}

    // ... rest of the overriden methods 

}


Bind everything in the MyDslRuntimeModule

The usual process of binding the newly implementing classes:

class MyDslRuntimeModule extends AbstractMyDslRuntimeModule {

	def MyDslFactory bindMyDslFactory() {
		val factory = MyDslFactory.eINSTANCE
		return new MyDslAdapterAddingFactory(factory)
	}	

	override Class<? extends IAstFactory> bindIAstFactory() {
		return MyDslEcoreElementFactory
	}

    // ...
}


Now my code raises an exception when I add an object to another container that was stolen from somewhere else before.

Thanks for your help,
Jan

[Updated on: Mon, 28 September 2020 11:59]

Report message to a moderator

Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832878 is a reply to message #1832874] Mon, 28 September 2020 14:01 Go to previous messageGo to next message
Ed Willink is currently offline Ed WillinkFriend
Messages: 7655
Registered: July 2009
Senior Member
Hi

If you ensure that every element in your DSL inherits from a common element e.g. Element in UML, EModelElement in Ecore, you ony need to insert the eBasicSetContainer override in one place,. A common inheritance root has many advantages so just do it. Adapters are certainly possible, but as soon as eAdapters is non-empty you take quite a hit in any set operation since every adapter gets to participate in the eNotify.. Avoid until you really need them.

Regards

Ed Willink
Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832879 is a reply to message #1832878] Mon, 28 September 2020 15:51 Go to previous messageGo to next message
Jan Hermes is currently offline Jan HermesFriend
Messages: 27
Registered: September 2020
Junior Member
Hi Ed,

this adapter solution would only be enabled during developing. For releases I would like to change the bindings again to the default factories.

But anyway I would like to choose your solution and just making sure every element inherits from a common custom element, but I don't know which button to tweak for this to work.

I have a basic mwe2-workflow which generates a MyDsl.genmodel with the standard property "Model Class Defaults" -> "Root Extends Class" = org.eclipse.emf.ecore.impl.MinimalEObjectImpl$Container

Is it possible to change this automatically generated property to always use my custom base class upon model-generation via the mwe2-workflow?

Regards
Jan Hermes

UPDATE
I found the configuration of the emfGenerator fragment

emfGenerator {
  rootExtendsClass = "org.example.mydsl.MyDslEObjectImpl"
}


So now I will try to inherit MinimalEObjectImpl and override your suggested function with the specific test.

Thanks a lot

[Updated on: Mon, 28 September 2020 16:18]

Report message to a moderator

Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832881 is a reply to message #1832879] Mon, 28 September 2020 16:26 Go to previous messageGo to next message
Jan Hermes is currently offline Jan HermesFriend
Messages: 27
Registered: September 2020
Junior Member
Yes with the above configuration for the mwe2 workflow and the custom EObjectImpl below everything works now as desired. Thank you very much for this additional insight. :)

class MyDslEObjectImpl extends MinimalEObjectImpl.Container {

	override NotificationChain eBasicSetContainer(InternalEObject newContainer, int newContainerFeatureID,
		NotificationChain msgs) {

		val oldContainer = eInternalContainer();
		
		if (oldContainer !== null && newContainer !== null) {
			throw new IllegalStateException('''«newContainer» attempts to steal «this» from «oldContainer»''')
		}
		
		super.eBasicSetContainer(newContainer, newContainerFeatureID, msgs)
	}
}


Regards
Jan Hermes
Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832892 is a reply to message #1832881] Mon, 28 September 2020 19:53 Go to previous messageGo to next message
Ed Willink is currently offline Ed WillinkFriend
Messages: 7655
Registered: July 2009
Senior Member
Hi

If you are manually maintaining your *.ecore, which I recommend, adding a root element is easy, just add an eSuperType to all roots.

If you still use Xtext to auto-generate your *.ecore, you may be able to just add a

MyDsLEObject: Root1 | Root2 | Root3

However the Xtext synthesis may be 'clever' enough to optimize the dead MyDslObject rule away, so you may need to add a bogus use of the type in some dark corner of your language.

Regards

Ed Willink
Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832903 is a reply to message #1832892] Tue, 29 September 2020 04:58 Go to previous messageGo to next message
Ed Merks is currently offline Ed MerksFriend
Messages: 33137
Registered: July 2009
Senior Member
If you maintain a *.ecore then in your *.genmodel you can change the Root Extends Class property from org.eclipse.emf.ecore.impl.MinimalEObjectImpl$Container to your own subclass thereof. This way your specialized class is not visible in the reflective Ecore model as an EClass (nor in your API interfaces) but rather is a generated implementation detail of the implementation classes.

Of course setting a conditional breakpoint is the least disruptive approach for tracking down bugs...


Ed Merks
Professional Support: https://www.macromodeling.com/
Re: Can I make Xtext throw, when a containment reference is reassigned? [message #1832907 is a reply to message #1832879] Tue, 29 September 2020 05:52 Go to previous message
Jan Hermes is currently offline Jan HermesFriend
Messages: 27
Registered: September 2020
Junior Member
Jan Hermes wrote on Mon, 28 September 2020 15:51

...

UPDATE
I found the configuration of the emfGenerator fragment

emfGenerator {
  rootExtendsClass = "org.example.mydsl.MyDslEObjectImpl"
}

...


Jan Hermes wrote on Mon, 28 September 2020 16:26


... with the above configuration for the mwe2 workflow and the custom EObjectImpl below everything works now as desired. Thank you very much for this additional insight. :)

class MyDslEObjectImpl extends MinimalEObjectImpl.Container {

	override NotificationChain eBasicSetContainer(InternalEObject newContainer, int newContainerFeatureID,
		NotificationChain msgs) {

		val oldContainer = eInternalContainer();
		
		if (oldContainer !== null && newContainer !== null) {
			throw new IllegalStateException('''«newContainer» attempts to steal «this» from «oldContainer»''')
		}
		
		super.eBasicSetContainer(newContainer, newContainerFeatureID, msgs)
	}
}

...


Hello Ed Merks and Ed Willink,

thanks for your advice. As I you can see in the quoted posts above, I found the place to exchange the "rootExtendsClass" (@see "UPDATE") already and managed to use the "MyDslEObjectImpl" successfully. Everything is working as expected and I learned a lot from it again, also very helpful for future customizations, I'm really grateful for that.

As for the conditional breakpoint, I might have totally overread it, because I didn't grasp its concept. I wasn't aware that this is possible in eclipse and java. I normally develop in completely different environments. So I will add this conditional breakpoint now as you suggested, which then would of course be the least invasive way to assert the stealing-condition.

Thanks for educating me. :D
Regards, Jan Hermes
Previous Topic:Problem with Maven-Tycho build of Xtext project in GitLab CI
Next Topic:15 min tutorial - falling at first hurdle
Goto Forum:
  


Current Time: Fri Apr 19 11:00:56 GMT 2024

Powered by FUDForum. Page generated in 0.02561 seconds
.:: Contact :: Home ::.

Powered by: FUDforum 3.0.2.
Copyright ©2001-2010 FUDforum Bulletin Board Software

Back to the top