Bug 413287 - [e4] EclipseContextOSGi does not deal with OSGi-Services
appropriately

Change-Id: Id5ae889ffc78f65d2c943e3941a3c488227838bf
Signed-off-by: Dirk Fauth <dirk.fauth@googlemail.com>
diff --git a/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/osgi/EclipseContextOSGi.java b/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/osgi/EclipseContextOSGi.java
index 1167d7c..1492271 100644
--- a/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/osgi/EclipseContextOSGi.java
+++ b/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/osgi/EclipseContextOSGi.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2011, 2015 IBM Corporation and others.

+ * Copyright (c) 2011, 2017 IBM Corporation and others.

  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License v1.0

  * which accompanies this distribution, and is available at

@@ -87,13 +87,6 @@
 		return service;

 	}

 

-	/**

-	 * Returns the service name for a service reference

-	 */

-	private String serviceName(ServiceReference<?> reference) {

-		return ((String[]) reference.getProperty(Constants.OBJECTCLASS))[0];

-	}

-

 	@Override

 	public void dispose() {

 		for (ServiceReference<?> ref : refs.values()) {

@@ -109,21 +102,29 @@
 	@Override

 	public void serviceChanged(ServiceEvent event) {

 		ServiceReference<?> ref = event.getServiceReference();

-		String name = serviceName(ref);

-		if (IContextFunction.SERVICE_NAME.equals(name)) // those keep associated names

-			name = (String) ref.getProperty(IContextFunction.SERVICE_CONTEXT_KEY);

+		String[] names = ((String[]) ref.getProperty(Constants.OBJECTCLASS));

+		for (String name : names) {

+			if (IContextFunction.SERVICE_NAME.equals(name)) {

+				name = (String) ref.getProperty(IContextFunction.SERVICE_CONTEXT_KEY);

+			}

 

-		if (refs.containsKey(name)) {

-			ServiceReference<?> oldRef = refs.get(name);

-			if (oldRef != null)

-				bundleContext.ungetService(oldRef);

-			if (event.getType() == ServiceEvent.UNREGISTERING) {

-				refs.put(name, null);

-				remove(name);

-			} else {

-				Object service = bundleContext.getService(ref);

-				refs.put(name, ref);

-				set(name, service);

+			if (refs.containsKey(name)) {

+				// retrieve the highest ranked service of the same type

+				ref = bundleContext.getServiceReference(name);

+

+				ServiceReference<?> oldRef = refs.get(name);

+				if (oldRef != null && oldRef != ref) {

+					bundleContext.ungetService(oldRef);

+				}

+

+				if (ref != null) {

+					Object service = bundleContext.getService(ref);

+					refs.put(name, ref);

+					set(name, service);

+				} else {

+					refs.put(name, null);

+					remove(name);

+				}

 			}

 		}

 	}

@@ -131,8 +132,9 @@
 	@Override

 	public void bundleChanged(BundleEvent event) {

 		// In case OSGi context has not being properly disposed by the application,

-		// OSGi framework shutdown will trigged uninjection of all consumed OSGi

-		// service. To avoid this, we detect framework shutdown and release services.

+		// OSGi framework shutdown will triggered uninjection of all consumed

+		// OSGi service. To avoid this, we detect framework shutdown and release

+		// services.

 		if (event.getType() != BundleEvent.STOPPING)

 			return;

 		if (event.getBundle().getBundleId() == 0)

diff --git a/tests/org.eclipse.e4.core.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.e4.core.tests/META-INF/MANIFEST.MF
index 40f7fc4..3dafa7e 100644
--- a/tests/org.eclipse.e4.core.tests/META-INF/MANIFEST.MF
+++ b/tests/org.eclipse.e4.core.tests/META-INF/MANIFEST.MF
@@ -38,7 +38,10 @@
  OSGI-INF/org.eclipse.e4.core.internal.tests.di.extensions.SampleServiceB.xml,
  OSGI-INF/org.eclipse.e4.core.internal.tests.di.extensions.ComponentEnabler.xml,
  OSGI-INF/DisabledServiceA.xml,
- OSGI-INF/DisabledServiceB.xml
+ OSGI-INF/DisabledServiceB.xml,
+ OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceController.xml,
+ OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceA.xml,
+ OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceB.xml
 Eclipse-BundleShape: dir
 Require-Capability: osgi.extender;
   filter:="(&(osgi.extender=osgi.component)(version>=1.3)(!(version>=2.0)))",
diff --git a/tests/org.eclipse.e4.core.tests/OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceA.xml b/tests/org.eclipse.e4.core.tests/OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceA.xml
new file mode 100644
index 0000000..7cf23f5
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceA.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" enabled="false" name="org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceA">
+   <service>
+      <provide interface="org.eclipse.e4.core.internal.tests.contexts.inject.TestService"/>
+      <provide interface="org.eclipse.e4.core.internal.tests.contexts.inject.TestOtherService"/>
+   </service>
+   <implementation class="org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceA"/>
+</scr:component>
\ No newline at end of file
diff --git a/tests/org.eclipse.e4.core.tests/OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceB.xml b/tests/org.eclipse.e4.core.tests/OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceB.xml
new file mode 100644
index 0000000..bed5984
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceB.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" enabled="false" name="org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceB">
+   <property name="service.ranking" type="Integer" value="5"/>
+   <service>
+      <provide interface="org.eclipse.e4.core.internal.tests.contexts.inject.TestService"/>
+      <provide interface="org.eclipse.e4.core.internal.tests.contexts.inject.TestOtherService"/>
+   </service>
+   <implementation class="org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceB"/>
+</scr:component>
\ No newline at end of file
diff --git a/tests/org.eclipse.e4.core.tests/OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceController.xml b/tests/org.eclipse.e4.core.tests/OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceController.xml
new file mode 100644
index 0000000..f5a8db8
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/OSGI-INF/org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceController.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="activate" name="org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceController">
+   <service>
+      <provide interface="org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceController"/>
+   </service>
+   <implementation class="org.eclipse.e4.core.internal.tests.contexts.inject.TestServiceController"/>
+</scr:component>
\ No newline at end of file
diff --git a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/ServiceContextTest.java b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/ServiceContextTest.java
index 98f655e..06e3740 100644
--- a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/ServiceContextTest.java
+++ b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/ServiceContextTest.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2009, 2015 IBM Corporation and others.
+ * Copyright (c) 2009, 2017 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -15,6 +15,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
 import java.util.ArrayList;
@@ -34,6 +35,7 @@
 import org.junit.Test;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
 
@@ -88,6 +90,16 @@
 		}
 	}
 
+	static class TestBean {
+		@Inject
+		@Optional
+		TestService testService;
+
+		@Inject
+		@Optional
+		TestOtherService testOtherService;
+	}
+
 	private IEclipseContext context;
 	private final List<ServiceRegistration<?>> registrations = new ArrayList<>();
 
@@ -277,4 +289,103 @@
 		crayon.draw();
 	}
 
+	@Test
+	public void testOptionalReferences() throws InterruptedException {
+		BundleContext context = FrameworkUtil.getBundle(getClass()).getBundleContext();
+		IEclipseContext serviceContext = EclipseContextFactory.getServiceContext(context);
+		TestBean bean = ContextInjectionFactory.make(TestBean.class, serviceContext);
+
+		assertNull(bean.testService);
+
+		ServiceReference<TestServiceController> ref = context.getServiceReference(TestServiceController.class);
+		TestServiceController controller = context.getService(ref);
+		try {
+			controller.enableTestServiceA();
+			// give the service registry and the injection some time
+			Thread.sleep(100);
+			assertNotNull(bean.testService);
+			assertSame(TestServiceA.class, bean.testService.getClass());
+			assertNotNull(bean.testOtherService);
+			assertSame(TestServiceA.class, bean.testOtherService.getClass());
+
+			controller.enableTestServiceB();
+			// give the service registry and the injection some time
+			Thread.sleep(100);
+			assertNotNull(bean.testService);
+			assertSame(TestServiceB.class, bean.testService.getClass());
+			assertNotNull(bean.testOtherService);
+			assertSame(TestServiceB.class, bean.testOtherService.getClass());
+
+			controller.disableTestServiceB();
+			// give the service registry and the injection some time
+			Thread.sleep(100);
+			assertNotNull(bean.testService);
+			assertSame(TestServiceA.class, bean.testService.getClass());
+			assertNotNull(bean.testOtherService);
+			assertSame(TestServiceA.class, bean.testOtherService.getClass());
+
+			controller.disableTestServiceA();
+			// give the service registry and the injection some time
+			Thread.sleep(100);
+			assertNull(bean.testService);
+			assertNull(bean.testOtherService);
+		} finally {
+			controller.disableTestServiceA();
+			controller.disableTestServiceB();
+			// give the service registry and the injection some time to ensure
+			// clear state after this test
+			Thread.sleep(100);
+		}
+	}
+
+	@Test
+	public void testServiceRanking() throws InterruptedException {
+		BundleContext context = FrameworkUtil.getBundle(getClass()).getBundleContext();
+		IEclipseContext serviceContext = EclipseContextFactory.getServiceContext(context);
+		TestBean bean = ContextInjectionFactory.make(TestBean.class, serviceContext);
+
+		assertNull(bean.testService);
+
+		ServiceReference<TestServiceController> ref = context.getServiceReference(TestServiceController.class);
+		TestServiceController controller = context.getService(ref);
+		try {
+			controller.enableTestServiceB();
+			// give the service registry and the injection some time
+			Thread.sleep(100);
+			assertNotNull(bean.testService);
+			assertSame(TestServiceB.class, bean.testService.getClass());
+			assertNotNull(bean.testOtherService);
+			assertSame(TestServiceB.class, bean.testOtherService.getClass());
+
+			// enable a service with a lower ranking
+			controller.enableTestServiceA();
+			// give the service registry and the injection some time
+			Thread.sleep(100);
+			assertNotNull(bean.testService);
+			// we expect that still the highest ranked service is injected
+			assertSame(TestServiceB.class, bean.testService.getClass());
+			assertNotNull(bean.testOtherService);
+			assertSame(TestServiceB.class, bean.testOtherService.getClass());
+
+			controller.disableTestServiceB();
+			// give the service registry and the injection some time
+			Thread.sleep(100);
+			assertNotNull(bean.testService);
+			assertSame(TestServiceA.class, bean.testService.getClass());
+			assertNotNull(bean.testOtherService);
+			assertSame(TestServiceA.class, bean.testOtherService.getClass());
+
+			controller.disableTestServiceA();
+			// give the service registry and the injection some time
+			Thread.sleep(100);
+			assertNull(bean.testService);
+			assertNull(bean.testOtherService);
+		} finally {
+			controller.disableTestServiceA();
+			controller.disableTestServiceB();
+			// give the service registry and the injection some time to ensure
+			// clear state after this test
+			Thread.sleep(100);
+		}
+	}
 }
diff --git a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestOtherService.java b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestOtherService.java
new file mode 100644
index 0000000..b983f30
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestOtherService.java
@@ -0,0 +1,5 @@
+package org.eclipse.e4.core.internal.tests.contexts.inject;
+
+public interface TestOtherService {
+
+}
diff --git a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestService.java b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestService.java
new file mode 100644
index 0000000..11f52fa
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestService.java
@@ -0,0 +1,5 @@
+package org.eclipse.e4.core.internal.tests.contexts.inject;
+
+public interface TestService {
+
+}
diff --git a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestServiceA.java b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestServiceA.java
new file mode 100644
index 0000000..aa1eac4
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestServiceA.java
@@ -0,0 +1,8 @@
+package org.eclipse.e4.core.internal.tests.contexts.inject;
+
+import org.osgi.service.component.annotations.Component;
+
+@Component(enabled = false)
+public class TestServiceA implements TestService, TestOtherService {
+
+}
diff --git a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestServiceB.java b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestServiceB.java
new file mode 100644
index 0000000..32df303
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestServiceB.java
@@ -0,0 +1,8 @@
+package org.eclipse.e4.core.internal.tests.contexts.inject;
+
+import org.osgi.service.component.annotations.Component;
+
+@Component(enabled = false, property = "service.ranking:Integer=5")
+public class TestServiceB implements TestService, TestOtherService {
+
+}
diff --git a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestServiceController.java b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestServiceController.java
new file mode 100644
index 0000000..6873ed6
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/inject/TestServiceController.java
@@ -0,0 +1,32 @@
+package org.eclipse.e4.core.internal.tests.contexts.inject;
+
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = TestServiceController.class)
+public class TestServiceController {
+
+	ComponentContext context;
+
+	@Activate
+	void activate(ComponentContext context) {
+		this.context = context;
+	}
+
+	public void enableTestServiceA() {
+		this.context.enableComponent(TestServiceA.class.getName());
+	}
+
+	public void disableTestServiceA() {
+		this.context.disableComponent(TestServiceA.class.getName());
+	}
+
+	public void enableTestServiceB() {
+		this.context.enableComponent(TestServiceB.class.getName());
+	}
+
+	public void disableTestServiceB() {
+		this.context.disableComponent(TestServiceB.class.getName());
+	}
+}