When talking about single sourceing in terms of e4 we talk about the following:
Write for e4 and run in e4-Applications, Eclipse 4.0 SDK and Eclipse 3.x SDK
In the first part of the series we are going to get familiar with how we can use the e4-Programming model which is build upon DI in a plain 3.x world. The application we’ll develop a small translator application/view.
The project sources are available from the EclipseLabs-Site I just created.
I’ll assume through out the tutorial that you’ve in depth knowledge about things like RCP, 3.x ServiceLocators, SWT/JFace and probably some Eclipse-Databinding background.
Step 1: Generate an 3.x ViewPart-Bundle
To start with the first thing we’ll do is to use the PDE-Project-Wizard “Plug-in with a view” to get an ViewPart which integrates into 3.x (e.g. named at.bestsolution.translate.view).
You should have an application looking like this:
Let’s now modify the content in the application to show a small translator UI which looks like this:
If you familiar with SWT (which i will assume through out this tutorial) here’s the code:
package at.bestsolution.translate.app; import org.eclipse.jface.viewers.ComboViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.part.ViewPart; public class View extends ViewPart { public static final String ID = "at.bestsolution.translate.app.view"; private Text term; /** * This is a callback that will allow us to create the viewer and initialize * it. */ public void createPartControl(Composite parent) { GridLayout layout = new GridLayout(2, false); parent.setLayout(layout); Label l = new Label(parent, SWT.NONE); l.setLayoutData( new GridData(GridData.FILL, GridData.BEGINNING, true,false, 2, 1) ); l.setText("Translator"); l = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); l.setLayoutData( new GridData(GridData.FILL, GridData.BEGINNING, true,false, 2, 1) ); l = new Label(parent, SWT.NONE); l.setText("Term"); term = new Text(parent, SWT.BORDER); term.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); l = new Label(parent, SWT.NONE); l.setText("Translator"); ComboViewer translator = new ComboViewer(parent); translator.getControl().setLayoutData( new GridData(GridData.FILL_HORIZONTAL) ); l = new Label(parent, SWT.NONE); l.setText("Language"); ComboViewer language = new ComboViewer(parent); language.getControl().setLayoutData( new GridData(GridData.FILL_HORIZONTAL) ); Button b = new Button(parent, SWT.PUSH); b.setText("Translate"); b.setLayoutData( new GridData(GridData.END, GridData.CENTER, false,false, 2, 1) ); l = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); l.setLayoutData( new GridData(GridData.FILL, GridData.CENTER, true,false, 2, 1) ); l = new Label(parent, SWT.NONE); l.setText("Translation"); } /** * Passing the focus request to the viewer's control. */ public void setFocus() { term.setFocus(); } }
Step 2: Create the translation services
Create the Service-Interface-Bundle
Create a new plain OSGi-Bundle (without any dependencies in MANIFEST.MF) e.g. named “at.bestsolution.translate.services” and define an ITranslator-Interface like this:
package at.bestsolution.demo.translate.services; import java.lang.reflect.InvocationTargetException; public interface ITranslator { public class FromTo { public final String from; public final String to; public FromTo(String from, String to) { this.from = from; this.to = to; } } public String getName(); public FromTo[] getFromTo(); public String translate(FromTo fromTo, String term) throws InvocationTargetException; }
Create Translator implementations
Lets’s as an example create a translator which uses the Google-Services for Translations by using this library from code.google.
The implementation class looks like this:
package at.bestsolution.translate.services.google; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import at.bestsolution.translate.services.ITranslator; import com.google.api.translate.Language; import com.google.api.translate.Translate; public class GoogleTranslator implements ITranslator { private static FromTo[] FROM_TOS; static { ArrayList<FromTo> l = new ArrayList<FromTo>(); for(Language fromLang : Language.values()) { for( Language toLanguage : Language.values() ) { if( fromLang != toLanguage ) { l.add(new FromTo(fromLang.name(), toLanguage.name())); } } } FROM_TOS = l.toArray(new FromTo[0]); } public String getName() { return "Google Translate"; } public FromTo[] getFromTo() { return FROM_TOS; } public String translate(FromTo fromTo, String term) throws InvocationTargetException { try { return Translate.execute(term, Language.valueOf(fromTo.from), Language.valueOf(fromTo.to)); } catch (Exception e) { throw new InvocationTargetException(e); } } }
And the registration through DS looks like this:
<?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="at.bestsolution.translate.services.google.googletranslator"> <implementation class="at.bestsolution.translate.services.google.GoogleTranslator"/> <service> <provide interface="at.bestsolution.translate.services.ITranslator"/> </service> </scr:component>
Next we add a ITranslatorProviderService which collects the various ITranslator-Service implementation and makes them available as IObservableList an.
The service definition:
package at.bestsolution.translate.services; import org.eclipse.core.databinding.observable.list.IObservableList; public interface ITranslatorProvider { public IObservableList getTranslators(); }
It’s implementation:
package at.bestsolution.translate.services.internal; import org.eclipse.core.databinding.observable.list.IObservableList; import org.eclipse.core.databinding.observable.list.WritableList; import at.bestsolution.translate.services.ITranslator; import at.bestsolution.translate.services.ITranslatorProvider; public class TranslatorProviderComponent implements ITranslatorProvider { private IObservableList list = new WritableList(); public IObservableList getTranslators() { return list; } public void addTranslator(final ITranslator translator) { list.getRealm().exec(new Runnable() { public void run() { list.add(translator); } }); } public void removeTranslator(final ITranslator translator ) { list.getRealm().exec(new Runnable() { public void run() { list.remove(translator); } }); } }
It’s registration:
<?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="at.bestsolution.translate.services.translatorprovider"> <implementation class="at.bestsolution.translate.services.internal.TranslatorProviderComponent"/> <service> <provide interface="at.bestsolution.translate.services.ITranslatorProvider"/> </service> <reference bind="addTranslator" cardinality="0..n" interface="at.bestsolution.translate.services.ITranslator" name="ITranslator" policy="dynamic" unbind="removeTranslator"/> </scr:component>
Consume the Service in the UI
Let’s for the start now consume the service directly throught the OSGi-Service registry. As a first step add a new method in our TranslatorView:
public class TranslatorView extends ViewPart { private IObservableList getTranslators() { Bundle bundle = FrameworkUtil.getBundle(getClass()); BundleContext context = bundle.getBundleContext(); ServiceReference reference = context.getServiceReference(ITranslatorProvider.class.getName()); ITranslatorProvider pv = (ITranslatorProvider) context.getService(reference); return pv.getTranslators(); } }
We don’t have to deal with the case that the ITranslatorProvider is going away because our bundle depends on the TranslatorProvider-Services-Bundle hence if the service goes away we get shutdown as well.
Let’s use this IObservableList in our ui:
public class TranslatorView extends ViewPart { public void createPartControl(Composite parent) { // .... IObservableList translatorsObs = getTranslators(); ComboViewer translator = new ComboViewer(parent); translator.getControl().setLayoutData( new GridData(GridData.FILL_HORIZONTAL)); translator.setContentProvider(new ObservableListContentProvider()); translator.setLabelProvider(new LabelProvider() { @Override public String getText(Object element) { return ((ITranslator)element).getName(); } }); translator.setInput(translatorsObs); l = new Label(parent, SWT.NONE); l.setText("Language"); ComboViewer language = new ComboViewer(parent); language.getControl().setLayoutData( new GridData(GridData.FILL_HORIZONTAL) ); language.setContentProvider(new ObservableListContentProvider()); language.setLabelProvider(new LabelProvider() { @Override public String getText(Object element) { return ((FromTo)element).from + " - " + ((FromTo)element).to; } }); IListProperty fromToProp = PojoProperties.list("fromTo"); IValueProperty selectionProp = ViewerProperties.singleSelection(); IObservableList input = fromToProp.observeDetail(selectionProp.observe(translator)); language.setInput(input); // .... } }
As you see this is already working very smooth but though the service stuff is working great we can use our View-Component only inside an RCP-Application (e.g. we can’t use it in a plain SWT one). At this point we can learn something from e4s way of doing things.
Learning from e4
Refactor code to use POJOs
In e4 everything you write are simply POJOs who annotated with DI-Annotation like:
- @Inject
- @PostConstruct
- …
So what we do next is to factor out our UI-Component-Code is a simple POJO class which makes our ViewPart look like this:
package at.bestsolution.translate.view; import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.part.*; public class TranslatorView extends ViewPart { public static final String ID = "at.bestsolution.translate.view.views.TranslatorView"; private TranslatorComponent component = new TranslatorComponent(); public void createPartControl(Composite parent) { component.createUI(parent); } public void setFocus() { component.setFocus(); } }
We could stop at this point because our View-Component can now be used in any SWT-application but we can still improve the situation a bit by using e4 DI-Support which helps us to get access to OSGi-Services.
Next we have to set up Eclipse-DI ideally at the Workbench-Level so that e.g. in an RAP-Application every workbench instance has its own EclipseContext. So we are using the ui.services-Extension Point to set up such a context provider stuff:
The service-factory:
package at.bestsolution.translate.view; import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.internal.services.IWorkbenchLocationService; import org.eclipse.ui.services.AbstractServiceFactory; import org.eclipse.ui.services.IServiceLocator; @SuppressWarnings("restriction") public class DIContextServiceFactory extends AbstractServiceFactory { @Override public Object create(@SuppressWarnings("rawtypes") Class serviceInterface, IServiceLocator parentLocator, IServiceLocator locator) { if( ! IEclipseContextProvider.class.equals(serviceInterface) ) { return null; } Object o = parentLocator.getService(serviceInterface); IWorkbenchLocationService wls = (IWorkbenchLocationService) locator.getService(IWorkbenchLocationService.class); final IWorkbenchWindow window = wls.getWorkbenchWindow(); final IWorkbenchPartSite site = wls.getPartSite(); if( window == null && site == null ) { return new IEclipseContextProviderImpl(); } else if( o != null && site == null ) { return new IEclipseContextProviderImpl((IEclipseContextProvider) o); } return o; } }
The workbench-service
package at.bestsolution.translate.view; import org.eclipse.e4.core.contexts.IEclipseContext; public interface IEclipseContextProvider { public IEclipseContext getContext(); }
The service implementation:
package at.bestsolution.translate.view; import org.eclipse.e4.core.contexts.EclipseContextFactory; import org.eclipse.e4.core.contexts.IEclipseContext; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; public class IEclipseContextProviderImpl implements IEclipseContextProvider { private IEclipseContext context; public IEclipseContextProviderImpl(IEclipseContextProvider parent) { this.context = parent.getContext().createChild(); } public IEclipseContextProviderImpl() { Bundle bundle = FrameworkUtil.getBundle(IEclipseContextProviderImpl.class); BundleContext bundleContext = bundle.getBundleContext(); IEclipseContext serviceContext = EclipseContextFactory.getServiceContext(bundleContext); context = serviceContext.createChild("WorkbenchContext"); //$NON-NLS-1$ } public IEclipseContext getContext() { return context; } }
What’s left now is to refactor our POJO code a bit to use DI and make use of it in our ViewPart. Let’s at first adjust our POJO.
public class TranslatorComponent { private Text term; private ComboViewer translator; @Inject public TranslatorComponent(Composite parent) { createUI(parent); } // ... }
We annotate our constructor with the @Inject annotation so that call the createUI-method there. Next we remove the OSGi-Service lookup with a method like this:
@Inject void setTranslationProvider(@Optional ITranslatorProvider provider) { if( provider == null ) { if( ! translator.getControl().isDisposed() ) { translator.setInput(new WritableList()); } } else { translator.setInput(provider.getTranslators()); } }
This method gets called by the Injection Framework and passes in the currently available ITranslatorProvider-Instance in the OSGi-Service registry. Finally we adjust our ViewPart like this:
public class TranslatorView extends ViewPart { public static final String ID = "at.bestsolution.translate.view.views.TranslatorView"; private IEclipseContext context; private TranslatorComponent component; public TranslatorView() { } public void createPartControl(Composite parent) { IEclipseContextProvider p = (IEclipseContextProvider) getSite().getService(IEclipseContextProvider.class); context = p.getContext().createChild(); context.set(Composite.class, parent); component = ContextInjectionFactory.make(TranslatorComponent.class, context); } /** * Passing the focus request to the viewer's control. */ public void setFocus() { component.setFocus(); } @Override public void dispose() { context.dispose(); super.dispose(); } }
Pingback: e4 – A forward compat layer « Tomsondev Blog
Way cool! Thanks!
Are IEclipseContextProvider and friends something that will get into org.erlide.e4.tools.compat?
And, just to make sure, the e4 compatibility layer requires only
org.eclipse.e4.core.contexts
org.eclipse.e4.tools.compat
org.eclipse.e4.tools.services
javax.inject
org.eclipse.e4.core.services
org.eclipse.e4.core.di
to be installed in a plain Eclipse 3.6? (I am going to test that, but there might be dependencies that aren't easily visible for a casual observer)
Looking forward to the next part(s)!
best regards,
Vlad
I’ll try to come up with a blog and list of required bundles to make it work in 3.x, The compat-bundle itself has only dependencies on the above but those themselves need other like e.g. javax.annotations.
If using this e4 compatibility layer and running my plugins on e4 proper, will the components that are e4-aware use it directly, or will the flow first get adapted to e3 and then back to e4 via the respective compatibility layers?
Thanks a lot for the help!
Let me put it this way. They get half adapted :-). When running e4 bundles through the forward-compat-layer in the Eclipse 4.0 SDK the 4.0 backwards compat layer wraps them into native e4-components. The forward-compat-layer though detects that it is running in an e4 env and directly communicates with the Eclipse 4.0 Application platform (or better the DI-Container) so they get wrapped halfway 🙂
I’d like to mention that the foward compat layer is in a very early stage and most workbench services are not yet available (it was driven only by the needs of my Model-Editor) but I’m sure we’ll be able to close those holes with feedback from people like you.
Makes sense.
Yes, I noticed that not everything is in place 🙂 but I’m sure there will be a lot of feedback because IMHO this is the most important tool for converting the existing codebase.
I suppose that until the layer is complete it will be impossible to split the UI plugin into a “e4 only” one and a “legacy e3” one, like you suggest in the other post about the compatibility layer.
BTW, is this a good place to discuss, you think?
No. We should discuss this stuff on e4-dev@eclipse.org and afterwards create bugzillas afterwards. I just like to mention that many of of as going to be on holiday so I guess really development will start mid of september. We should try to bring this on the table for the planning of 4.1 so that we can allocate resources
Just one more short note. One of the problems is that we didn’t had time to work on the “20 things” story but we will do this in 4.1 and then we can map them in the forward-compat-layer. We are currently discussing how to can deal with selection propagation in 4.1 in a more user friendly way. If someone reading this the bug for it is 321201
Hi Tom,
I am trying to go the described way of this post but the dependencies seem not to work (I am using E4.2M3 and tried with E3.7.1 and E3.5.2). Did you finally come up with a complete list of dependencies (of using just Eclipse DI in E3.x)? We have a larger RCP app and my intention is to migrate our views step by step to e4 and keeping the backward compatibility to E3.5 (ideally). Is this possible? Or do I have to make my own DI (e.g. with Google Guice) to have a single source?
Many thanks for any answer
Did you take a look at https://tomsondev.bestsolution.at/2011/06/10/how-to-apply-the-e4-programming-model-to-3-x/ and the sources I uploaded to my gitub repo
Yes, but I am working against targets and I was not able to include this feature in my target – the plugins did not get recognized properly and the code won’t compile. But if you can assure me, that it is intended to work with Eclipse 3.5 and up, I will try again – I just want to make sure, I am on the right track of things.
Well I think it is better to check out the bundles from CVS (or if you want to use the latest and greatest then you might use the git repo) then because though they might work wit 3.5 as well the minimal versions specified might cause problems. I have never tested all those things with 3.5 (has DS been available by then?) so I can’t definately say if it works. I’ll try to assemble a list of bundles until next week (I’m currently traveling).
Thanks Tom, I will do that and have already started. It looks like several minimal versions are set very restrictive in the bundles and yes, I think DS started with 3.5
Just to finish up: I can confirm that it works with Eclipse 3.5.2, at least for the scenario I have tested, which is a view and a declared service. I changed the required versions of the following plugins (which is basically the list of plugins needed for the forward bridge):
org.eclipse.e4.core.contexts
org.eclipse.e4.core.di
org.eclipse.e4.core.services
org.eclipse.e4.ui.css.core
org.eclipse.e4.ui.css.swt
org.eclipse.e4.ui.css.swt.theme
org.eclipse.e4.ui.widgets
org.eclipse.e4.ui.di
org.eclipse.e4.ui.services
org.eclipse.e4.core.di.extensions
org.eclipse.e4.tools.compat
org.eclipse.e4.tools.services
(the list is based on the latest GIT-Head of e4 from the end of November, which I took as the base of my check-out)
In order to make things work the following bundles need to be existent as well, whereby javax.injet and javax.annotation can be borrowed from an Eclipse 4 installation quite easy:
org.eclipse.equinox.ds
org.eclipse.equinox.event
org.eclipse.equinox.util
javax.inject
javax.annotation
org.eclipse.osgi
org.eclipse.osgi.services
The only two problems in terms of the source code:
1) Some service relateded classes, e.g. ServiceTracker, did become generic types parameters (I guess since 3.6). I changed the code back to non-generic types.
2) the “reskin” method in SWT (for CSS stuff, I guess), was not yet available in 3.5, therefore I commented it out. This might cause some problems with CSS related code, which is than not usable in the 3.5 world, which I can live with.
If someone is interested, I might put together an update site with the modified e4 binary bundles as an e4 bridge tool for Eclipse 3.5.2.