Fixed TunnelToServer bean registration for replaced services

Change-Id: I9f1044b3f2d01c0bd68544ab1b84da9d7f58e21d
Reviewed-on: https://git.eclipse.org/r/58438
Tested-by: Hudson CI
Reviewed-by: Andi Bur <andi.bur@gmail.com>
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/RegisterTunnelToServerPlatformListenerTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/RegisterTunnelToServerPlatformListenerTest.java
new file mode 100644
index 0000000..ae0a033
--- /dev/null
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/RegisterTunnelToServerPlatformListenerTest.java
@@ -0,0 +1,177 @@
+package org.eclipse.scout.rt.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.eclipse.scout.commons.annotations.Replace;
+import org.eclipse.scout.commons.exception.ProcessingException;
+import org.eclipse.scout.rt.platform.IBean;
+import org.eclipse.scout.rt.platform.interceptor.IBeanInterceptor;
+import org.eclipse.scout.rt.platform.interceptor.IBeanInvocationContext;
+import org.eclipse.scout.rt.platform.internal.BeanManagerImplementor;
+import org.eclipse.scout.rt.platform.inventory.IClassInfo;
+import org.eclipse.scout.rt.platform.inventory.IClassInventory;
+import org.eclipse.scout.rt.platform.inventory.internal.JandexClassInventory;
+import org.eclipse.scout.rt.platform.service.IService;
+import org.eclipse.scout.rt.shared.TunnelToServer;
+import org.jboss.jandex.Indexer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Test {@link TunnelToServer} with and without {@link Replace} annotations
+ */
+public class RegisterTunnelToServerPlatformListenerTest {
+
+  private static JandexClassInventory s_classInventory;
+
+  private BeanManagerImplementor m_beanManager;
+  private RegisterTunnelToServerPlatformListener m_registrator;
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    Indexer indexer = new Indexer();
+    indexClass(indexer, IFixtureTunnelToServer.class);
+    indexClass(indexer, IFixtureTunnelToServerEx1.class);
+    indexClass(indexer, IFixtureTunnelToServerEx2.class);
+    s_classInventory = new JandexClassInventory(indexer.complete());
+  }
+
+  protected static void indexClass(Indexer indexer, Class<?> clazz) throws IOException {
+    indexer.index(clazz.getResourceAsStream(RegisterTunnelToServerPlatformListenerTest.class.getSimpleName() + "$" + clazz.getSimpleName() + ".class"));
+  }
+
+  @Before
+  public void before() {
+    m_beanManager = new BeanManagerImplementor(new FixtureClientBeanDecorationFactory());
+    m_registrator = new RegisterTunnelToServerPlatformListener();
+  }
+
+  @After
+  public void after() {
+    m_beanManager = null;
+  }
+
+  @Test
+  public void testBase() {
+    registerTunnelToServerBeans(IFixtureTunnelToServer.class);
+    assertPings("return IFixtureTunnelToServer#ping", IFixtureTunnelToServer.class);
+  }
+
+  @Test
+  public void testReplaceEx1() {
+    registerTunnelToServerBeans(IFixtureTunnelToServer.class, IFixtureTunnelToServerEx1.class);
+    assertPings("return IFixtureTunnelToServerEx1#ping", IFixtureTunnelToServer.class, IFixtureTunnelToServerEx1.class);
+  }
+
+  @Test
+  public void testReplaceEx1ReverseOrderRegistration() {
+    registerTunnelToServerBeans(IFixtureTunnelToServerEx1.class, IFixtureTunnelToServer.class);
+    assertPings("return IFixtureTunnelToServerEx1#ping", IFixtureTunnelToServer.class, IFixtureTunnelToServerEx1.class);
+  }
+
+  @Test
+  public void testReplaceEx2_1() {
+    registerTunnelToServerBeans(IFixtureTunnelToServer.class, IFixtureTunnelToServerEx1.class, IFixtureTunnelToServerEx2.class);
+    assertReplaceEx2();
+  }
+
+  @Test
+  public void testReplaceEx2_2() {
+    registerTunnelToServerBeans(IFixtureTunnelToServer.class, IFixtureTunnelToServerEx2.class, IFixtureTunnelToServerEx1.class);
+    assertReplaceEx2();
+  }
+
+  @Test
+  public void testReplaceEx2_3() {
+    registerTunnelToServerBeans(IFixtureTunnelToServerEx1.class, IFixtureTunnelToServer.class, IFixtureTunnelToServerEx2.class);
+    assertReplaceEx2();
+  }
+
+  @Test
+  public void testReplaceEx2_4() {
+    registerTunnelToServerBeans(IFixtureTunnelToServerEx1.class, IFixtureTunnelToServerEx2.class, IFixtureTunnelToServer.class);
+    assertReplaceEx2();
+  }
+
+  @Test
+  public void testReplaceEx2_5() {
+    registerTunnelToServerBeans(IFixtureTunnelToServerEx2.class, IFixtureTunnelToServer.class, IFixtureTunnelToServerEx1.class);
+    assertReplaceEx2();
+  }
+
+  @Test
+  public void testReplaceEx2_6() {
+    registerTunnelToServerBeans(IFixtureTunnelToServerEx2.class, IFixtureTunnelToServerEx1.class, IFixtureTunnelToServer.class);
+    assertReplaceEx2();
+  }
+
+  private void assertReplaceEx2() {
+    assertPings("return IFixtureTunnelToServerEx2#ping", IFixtureTunnelToServer.class, IFixtureTunnelToServerEx1.class, IFixtureTunnelToServerEx2.class);
+  }
+
+  private void registerTunnelToServerBeans(Class<?>... classes) {
+    Set<IClassInfo> classInfos = new LinkedHashSet<>();
+    for (Class<?> c : classes) {
+      classInfos.add(s_classInventory.getClassInfo(c));
+    }
+    IClassInventory classInventory = mock(IClassInventory.class);
+    when(classInventory.getKnownAnnotatedTypes(TunnelToServer.class)).thenReturn(classInfos);
+    m_registrator.registerTunnelToServerProxies(m_beanManager, classInventory);
+  }
+
+  private void assertPings(String expectedPingResult, Class<?>... queryClasses) {
+    for (Class<?> queryClass : queryClasses) {
+      @SuppressWarnings("unchecked")
+      Class<? extends IFixtureTunnelToServer> clazz = (Class<? extends IFixtureTunnelToServer>) queryClass;
+      assertOnePing(expectedPingResult, clazz);
+    }
+  }
+
+  private <T extends IFixtureTunnelToServer> void assertOnePing(String expectedPingResult, Class<T> queryClass) {
+    assertTrue(IFixtureTunnelToServer.class.isAssignableFrom(queryClass));
+    IBean<T> bean = m_beanManager.getBean(queryClass);
+    assertNotNull(bean);
+    T obj = bean.getInstance(queryClass);
+    assertNotNull(obj);
+    assertEquals(expectedPingResult, obj.ping());
+  }
+
+  private static final class FixtureClientBeanDecorationFactory extends ClientBeanDecorationFactory {
+    @Override
+    protected <T> IBeanInterceptor<T> decorateWithTunnelToServer(final IBean<T> bean, final Class<T> queryType) {
+      return new IBeanInterceptor<T>() {
+        @Override
+        public Object invoke(IBeanInvocationContext<T> context) throws ProcessingException {
+          Method method = context.getTargetMethod();
+          return "return " + bean.getBeanClazz().getSimpleName() + "#" + method.getName();
+        }
+      };
+    }
+  }
+
+  @TunnelToServer
+  public static interface IFixtureTunnelToServer extends IService {
+    String ping();
+  }
+
+  @Replace
+  @TunnelToServer
+  public static interface IFixtureTunnelToServerEx1 extends IFixtureTunnelToServer {
+  }
+
+  @Replace
+  @TunnelToServer
+  public static interface IFixtureTunnelToServerEx2 extends IFixtureTunnelToServerEx1 {
+  }
+}
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/RegisterTunnelToServerPlatformListener.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/RegisterTunnelToServerPlatformListener.java
index 9c708fa..28495f9 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/RegisterTunnelToServerPlatformListener.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/RegisterTunnelToServerPlatformListener.java
@@ -1,9 +1,13 @@
 package org.eclipse.scout.rt.client;
 
+import java.util.List;
+
 import org.eclipse.scout.commons.logger.IScoutLogger;
 import org.eclipse.scout.commons.logger.ScoutLogManager;
 import org.eclipse.scout.rt.platform.BeanMetaData;
+import org.eclipse.scout.rt.platform.IBean;
 import org.eclipse.scout.rt.platform.IBeanDecorationFactory;
+import org.eclipse.scout.rt.platform.IBeanManager;
 import org.eclipse.scout.rt.platform.IPlatform;
 import org.eclipse.scout.rt.platform.IPlatformListener;
 import org.eclipse.scout.rt.platform.PlatformEvent;
@@ -11,6 +15,7 @@
 import org.eclipse.scout.rt.platform.exception.PlatformException;
 import org.eclipse.scout.rt.platform.inventory.ClassInventory;
 import org.eclipse.scout.rt.platform.inventory.IClassInfo;
+import org.eclipse.scout.rt.platform.inventory.IClassInventory;
 import org.eclipse.scout.rt.shared.SharedConfigProperties.CreateTunnelToServerBeansProperty;
 import org.eclipse.scout.rt.shared.TunnelToServer;
 
@@ -27,24 +32,53 @@
         return;
       }
       //register all tunnels to server
-      for (IClassInfo ci : ClassInventory.get().getKnownAnnotatedTypes(TunnelToServer.class)) {
-        if (!ci.isInterface() || !ci.isPublic()) {
-          LOG.error("The annotation @" + TunnelToServer.class.getSimpleName() + " can only be used on public interfaces, not on " + ci.name());
-          continue;
-        }
-        Class<?> c;
-        try {
-          c = ci.resolveClass();
-        }
-        catch (Exception e) {
-          LOG.warn("loading class", e);
-          continue;
-        }
-        if (!event.getSource().getBeanManager().getBeans(c).isEmpty()) {
-          continue;
-        }
-        event.getSource().getBeanManager().registerBean(new BeanMetaData(c).withApplicationScoped(false));
+      final IBeanManager beanManager = event.getSource().getBeanManager();
+      final IClassInventory classInventory = ClassInventory.get();
+      registerTunnelToServerProxies(beanManager, classInventory);
+    }
+  }
+
+  protected void registerTunnelToServerProxies(final IBeanManager beanManager, final IClassInventory classInventory) {
+    for (IClassInfo ci : classInventory.getKnownAnnotatedTypes(TunnelToServer.class)) {
+      if (!ci.isInterface() || !ci.isPublic()) {
+        LOG.error("The annotation @" + TunnelToServer.class.getSimpleName() + " can only be used on public interfaces, not on " + ci.name());
+        continue;
+      }
+      Class<?> c;
+      try {
+        c = ci.resolveClass();
+      }
+      catch (Exception e) {
+        LOG.warn("loading class", e);
+        continue;
+      }
+
+      if (!acceptClass(beanManager, c)) {
+        LOG.debug("ignoring class [{}]", c);
+        continue;
+      }
+
+      beanManager.registerBean(createBeanMetaData(c));
+    }
+  }
+
+  /**
+   * Returns <code>true</code> if the given class (a public interface) should be registered as bean.
+   */
+  protected boolean acceptClass(IBeanManager beanManager, Class<?> beanClass) {
+    List<? extends IBean<?>> beans = beanManager.getBeans(beanClass);
+    for (IBean<?> bean : beans) {
+      if (!bean.getBeanClazz().isInterface()) {
+        return false;
       }
     }
+    return true;
+  }
+
+  /**
+   * Creates a new {@link BeanMetaData} for the given class.
+   */
+  protected BeanMetaData createBeanMetaData(Class<?> c) {
+    return new BeanMetaData(c).withApplicationScoped(false);
   }
 }