Jump to: navigation, search

Difference between revisions of "MoDisco/MDT Migration"

Line 141: Line 141:
  
 
=== Difficulties ===
 
=== Difficulties ===
 +
* For methods that return a collection (list, map, queue, set, etc.), the user can then proceed to call methods on this collection to modify it. And we would expect the underlying collection to be modified accordingly. So, we can't just get away with copying the collection when making a proxy for it. We have to provide a collection proxy that delegates every operation to its delegate collection. And this, for each and every collection that is used in MoDisco (lists, sets, maps, etc.)
 +
* Similarly, for collections that are passed as parameters to methods that modify it, we must also create a proxy collection, in the other direction this time.
 +
* Public fields: we can't have proxies for public fields, so they wouldn't be handled at all
 +
* Protected fields: we can't have proxies for them either, and they can be accessed by sub-classes made by users. So, this wouldn't work properly.
 +
* Implicit knowledge:
 +
** Generics: if the return type is <code>List<EObject></code> for example, they we will create a proxy for <code>List</code>. But the user can cast this to <code>BasicEList<EObject></code>, knowing that the collection will always be this type. This is bad practice, but it happens anyway, and we wouldn't be able to handle this kind of uses.
 +
** if a method returns something like <code>Object</code>, we don't know (statically) whether we should create a proxy for it. For example, when using the <code>IAdaptable</code> interface, the method <code>Object getAdapter(Class adapter)</code> always returns an Object, but since by contract this Object is supposed to be of the type that is passed as a parameter, the user will cast it right away. And that would fail because we haven't made a proxy for this. (In fact, this is even worse, since <code>getAdapter</code> would be passed a <code>Class</code> in the GMT namespace, which it wouldn't know about).
 +
* arrays: we can't create a proxy for an array, so we have to return a copy of it with the other type. So, if a method returns an array and the user modifies this array, these modifications won't be propagated to the right array.
 +
* non-MoDisco code that returns MoDisco objects: for example, a user who wants to work with the ModelBrowser can use an Eclipse API that will return an instance of <code>EcoreBrowser</code> from the <code>org.eclipse.modisco</code> namespace, and there is nothing we can do to substitute it with a proxy:
 +
<source lang="java">
 +
IEditorPart editor = IDE.openEditor(page, file, EcoreBrowser.EDITOR_ID);
 +
EcoreBrowser browser = (EcoreBrowser) editor; // this will fail!!!
 +
</source>
 +
* A sub-class can call protected methods on its super-class. But since these methods are protected, they can't normally be called from the proxy (this can be solved by the use of reflection)
 +
* If the super-class of a MoDisco class is not itself a MoDisco class, then we won't have a proxy for it. This means that method calls won't execute on the right object. So, we have to create a proxy method for every method, including those inherited from super-classes. This is bad for memory use, and we risk PermGen space exceptions.
 +
* As seen above, MoDisco objects with the <code>org.eclipse.modisco</code> namespace can leak through (under the cover of an <code>Object</code> for example). If the user uses the <code>==</code> comparison operator, s/he will get the wrong result when comparing a proxy with an original MoDisco element.

Revision as of 04:22, 18 June 2010

Overview

MoDisco moved from Modeling/GMT to Modeling/MDT (see this bug). As part of this migration, we should have renamed all Java packages from org.eclipse.gmt.modisco.* to org.eclipse.modisco.*.

Unfortunately, this means breaking every existing API, which is in contradiction with the API policy that was established for MoDisco. Among other things, this policy states that API cannot be broken without notifying adopters at least one year before that.

This means for MoDisco that we cannot just rename the Java packages, because we still have to support the API that existed in the org.eclipse.gmt.modisco.* namespace.

To reconcile these two contradicting requirements, we explored several solutions, which are presented below.

Possible solutions

duplicate

We could, before releasing a new version of MoDisco with the renamed packages, duplicate all the previously renamed plug-ins, and rename all packages (through an automated refactoring) from org.eclipse.modisco.* back to org.eclipse.gmt.modisco.*. And then distribute this as a "backward compatibility SDK".

The problem with this solution is for adopters that rely on plug-ins with the old namespace, but that want to start developing plug-ins for the latest version of MoDisco. This would be an all-or-nothing solution: either migrate everything to the latest version of MoDisco, or develop new projects with the old version.

delegate

Another solution would be to create a "backward compatibility SDK" (with GMT packages) that delegates to the latest version of MoDisco. The idea looks straightforward: every call to an API from the old version delegates to the corresponding API in the latest version.

This could be automated through a refactoring that would create proxies (in the GMT namespace) for each MoDisco API, that delegate to the corresponding methods with the same name, but in the org.eclipse.modisco namespace.

This would look like this:

New API (org.eclipse.modisco namespace) Proxy implementation (org.eclipse.gmt.modisco namespace)
public class A {
  public A() {
    // do some initialization
  }
 
  public void doStuff() {
    // do some stuff
  }
}
public class A {
  private org.eclipse.modisco.A proxy;
 
  public org.eclipse.modisco.A getProxy() {
    return proxy;
  }
 
  public A() {
    // guard to avoid creating 2 proxies 
    // (Java adds super call automatically)
    if (!(this.getClass() == A.class))
      return;
    proxy = new org.eclipse.modisco.A();
  }
 
  public void doStuff() {
    getProxy().doStuff();
  }
}


A proxy knows its delegate, but a delegate doesn't know its proxy. But we need this information to be able to find the proxy that was associated with an element that is returned from a method. So, we have to keep this information somewhere. For example in a HashMap:

New API (org.eclipse.modisco namespace) Proxy implementation (org.eclipse.gmt.modisco namespace)
public class C {
  private int value;
 
  public C() {
    this(123);
  }
 
  public C(int value) {
    this.value = value;
  }
 
  public int getValue() {
    return value;
  }
 
  public void setValue(int value) {
    this.value = value;
  }
}
public class C {
  private org.eclipse.modisco.C proxy;
 
  public org.eclipse.modisco.C getProxy() {
    return proxy;
  }
 
  public C() {
    proxy = new org.eclipse.modisco.C();
    proxies.put(proxy, this);
  }
 
  public C(int value) {
    proxy = new org.eclipse.modisco.C(value);
    proxies.put(proxy, this);
  }
 
  public C(org.eclipse.modisco.C element) {
    this.proxy = element;
  }
 
  public int getValue() {
    return getProxy().getValue();
  }
 
  public void setValue(int value) {
    getProxy().setValue(value);
  }
 
  private static Map<Object, Object> proxies = 
      new HashMap<Object, Object>();
 
  public static C resolveProxy(
        org.eclipse.modisco.C element) {
    C proxy = (C) proxies.get(element);
    if (proxy == null) {
      proxy = new C(element);
      proxies.put(proxy, element);
    }
    return proxy;
  }
}


So far, so good. But in practice, we discovered that this is not always so simple, and there are many roadblocks that prevent this solution from working 100%. Moreover, even getting it to work only for common cases that can work is far from trivial, and requires a fair amount of work.

Difficulties

  • For methods that return a collection (list, map, queue, set, etc.), the user can then proceed to call methods on this collection to modify it. And we would expect the underlying collection to be modified accordingly. So, we can't just get away with copying the collection when making a proxy for it. We have to provide a collection proxy that delegates every operation to its delegate collection. And this, for each and every collection that is used in MoDisco (lists, sets, maps, etc.)
  • Similarly, for collections that are passed as parameters to methods that modify it, we must also create a proxy collection, in the other direction this time.
  • Public fields: we can't have proxies for public fields, so they wouldn't be handled at all
  • Protected fields: we can't have proxies for them either, and they can be accessed by sub-classes made by users. So, this wouldn't work properly.
  • Implicit knowledge:
    • Generics: if the return type is List<EObject> for example, they we will create a proxy for List. But the user can cast this to BasicEList<EObject>, knowing that the collection will always be this type. This is bad practice, but it happens anyway, and we wouldn't be able to handle this kind of uses.
    • if a method returns something like Object, we don't know (statically) whether we should create a proxy for it. For example, when using the IAdaptable interface, the method Object getAdapter(Class adapter) always returns an Object, but since by contract this Object is supposed to be of the type that is passed as a parameter, the user will cast it right away. And that would fail because we haven't made a proxy for this. (In fact, this is even worse, since getAdapter would be passed a Class in the GMT namespace, which it wouldn't know about).
  • arrays: we can't create a proxy for an array, so we have to return a copy of it with the other type. So, if a method returns an array and the user modifies this array, these modifications won't be propagated to the right array.
  • non-MoDisco code that returns MoDisco objects: for example, a user who wants to work with the ModelBrowser can use an Eclipse API that will return an instance of EcoreBrowser from the org.eclipse.modisco namespace, and there is nothing we can do to substitute it with a proxy:
IEditorPart editor = IDE.openEditor(page, file, EcoreBrowser.EDITOR_ID);
EcoreBrowser browser = (EcoreBrowser) editor; // this will fail!!!
  • A sub-class can call protected methods on its super-class. But since these methods are protected, they can't normally be called from the proxy (this can be solved by the use of reflection)
  • If the super-class of a MoDisco class is not itself a MoDisco class, then we won't have a proxy for it. This means that method calls won't execute on the right object. So, we have to create a proxy method for every method, including those inherited from super-classes. This is bad for memory use, and we risk PermGen space exceptions.
  • As seen above, MoDisco objects with the org.eclipse.modisco namespace can leak through (under the cover of an Object for example). If the user uses the == comparison operator, s/he will get the wrong result when comparing a proxy with an original MoDisco element.