ASSIGNED - bug 441440: Add support for nested @Replace annotated classes 
https://bugs.eclipse.org/bugs/show_bug.cgi?id=441440

Change-Id: I10681c14a7f8c7f97450ccc70850438171703e2c
Reviewed-on: https://git.eclipse.org/r/31307
Tested-by: Hudson CI
Reviewed-by: Andi Bur <andi.bur@gmail.com>
diff --git a/org.eclipse.scout.rt.client.test/src/org/eclipse/scout/rt/client/ui/form/fields/ReplaceFieldTest.java b/org.eclipse.scout.rt.client.test/src/org/eclipse/scout/rt/client/ui/form/fields/ReplaceFieldTest.java
index f8274ac..8992776 100644
--- a/org.eclipse.scout.rt.client.test/src/org/eclipse/scout/rt/client/ui/form/fields/ReplaceFieldTest.java
+++ b/org.eclipse.scout.rt.client.test/src/org/eclipse/scout/rt/client/ui/form/fields/ReplaceFieldTest.java
@@ -127,6 +127,13 @@
     assertSame(AbstractTemplateBox.SecondTemplateField.class, form.getTemplate2Box().getSecondTemplateField().getClass());
   }
 
+  @Test
+  public void testNestedReplace() throws Exception {
+    ExampleExForm form = new ExampleExForm();
+    assertNotNull(form.getTextField());
+    assertSame(ExampleExForm.DetailExBox.TextExField.class, form.getTextField().getClass());
+  }
+
   public static class BaseField extends AbstractStringField {
   }
 
@@ -309,4 +316,42 @@
       }
     }
   }
+
+  public static class ExampleForm extends AbstractForm {
+
+    public ExampleForm() throws ProcessingException {
+      super();
+    }
+
+    public MainBox.DetailBox.TextField getTextField() {
+      return getFieldByClass(MainBox.DetailBox.TextField.class);
+    }
+
+    @Order(10)
+    public class MainBox extends AbstractGroupBox {
+      @Order(10)
+      public class DetailBox extends AbstractGroupBox {
+        @Order(10)
+        public class TextField extends AbstractStringField {
+        }
+      }
+    }
+  }
+
+  public static class ExampleExForm extends ExampleForm {
+    public ExampleExForm() throws ProcessingException {
+      super();
+    }
+
+    @Replace
+    public class DetailExBox extends MainBox.DetailBox {
+      public DetailExBox(ExampleForm.MainBox container) {
+        container.super();
+      }
+
+      @Replace
+      public class TextExField extends TextField {
+      }
+    }
+  }
 }
diff --git a/org.eclipse.scout.rt.client/src/org/eclipse/scout/rt/client/ui/form/fields/AbstractCompositeField.java b/org.eclipse.scout.rt.client/src/org/eclipse/scout/rt/client/ui/form/fields/AbstractCompositeField.java
index 1cc5823..65572a3 100644
--- a/org.eclipse.scout.rt.client/src/org/eclipse/scout/rt/client/ui/form/fields/AbstractCompositeField.java
+++ b/org.eclipse.scout.rt.client/src/org/eclipse/scout/rt/client/ui/form/fields/AbstractCompositeField.java
@@ -12,12 +12,15 @@
 
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
+import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 import org.eclipse.scout.commons.CollectionUtility;
+import org.eclipse.scout.commons.CompareUtility;
 import org.eclipse.scout.commons.ConfigurationUtility;
 import org.eclipse.scout.commons.annotations.ClassId;
 import org.eclipse.scout.commons.annotations.InjectFieldTo;
@@ -30,6 +33,10 @@
 import org.eclipse.scout.rt.client.ui.form.IForm;
 import org.eclipse.scout.rt.client.ui.form.IFormFieldVisitor;
 import org.eclipse.scout.rt.client.ui.form.fields.groupbox.AbstractGroupBox;
+import org.eclipse.scout.rt.client.ui.form.fields.sequencebox.AbstractSequenceBox;
+import org.eclipse.scout.rt.client.ui.form.fields.snapbox.AbstractSnapBox;
+import org.eclipse.scout.rt.client.ui.form.fields.splitbox.AbstractSplitBox;
+import org.eclipse.scout.rt.client.ui.form.fields.tabbox.AbstractTabBox;
 import org.eclipse.scout.rt.client.ui.form.fields.wrappedform.IWrappedFormField;
 import org.eclipse.scout.rt.shared.services.common.exceptionhandler.IExceptionHandlerService;
 import org.eclipse.scout.service.SERVICES;
@@ -150,12 +157,78 @@
   @Override
   public void setFormInternal(IForm form) {
     super.setFormInternal(form);
+    if (this == form.getRootGroupBox() && form instanceof AbstractForm) {
+      // this is the root group box. Publish replacement map to form and keep local map for better performance (see getReplacingFieldClass)
+      ((AbstractForm) form).registerFormFieldReplacementsInternal(m_formFieldReplacements);
+    }
     for (IFormField field : m_fields) {
       field.setFormInternal(form);
     }
   }
 
   @Override
+  public void setParentFieldInternal(ICompositeField parentField) {
+    super.setParentFieldInternal(parentField);
+    if (!(parentField instanceof AbstractCompositeField)) {
+      return;
+    }
+
+    // check if this is a template field box
+    if (isTemplateField()) {
+      // do not publish replacement map for template field boxes
+      return;
+    }
+
+    // publish replacement map to parent AbstractCompositeField and keep local map for better performance (see getReplacingFieldClass)
+    ((AbstractCompositeField) parentField).registerFormFieldReplacements(m_formFieldReplacements);
+  }
+
+  /**
+   * Returns <code>true</code> if this field is a template group box, i.e. an abstract box class containing
+   * other {@link IFormField}s.
+   * <p/>
+   * This default implementation checks the path of super classes, starting by the most specific one and stopping by
+   * this class or one of its well known direct sub classes (i.e {@link AbstractGroupBox}, {@link AbstractSequenceBox},
+   * {@link AbstractSnapBox}, {@link AbstractSplitBox} and {@link AbstractTabBox}). If there exists an abstract class
+   * containing {@link IFormField}, this method returns <code>true</code>. Subclasses may override this default
+   * behavior.
+   * 
+   * @since 4.0.1
+   */
+  protected boolean isTemplateField() {
+    Class<?> c = getClass();
+    while (c.getSuperclass() != null) {
+      c = c.getSuperclass();
+
+      // non-abstract classes are not considered as template
+      if (!Modifier.isAbstract(c.getModifiers())) {
+        continue;
+      }
+
+      // quick check for well known Scout classes
+      if (CompareUtility.isOneOf(c,
+          AbstractCompositeField.class,
+          AbstractGroupBox.class,
+          AbstractSequenceBox.class,
+          AbstractSnapBox.class,
+          AbstractSplitBox.class,
+          AbstractTabBox.class)) {
+        return false;
+      }
+
+      // class is only a template if it contains fields
+      for (Class<?> innerClass : c.getDeclaredClasses()) {
+        int m = innerClass.getModifiers();
+        if (Modifier.isPublic(m) && !Modifier.isAbstract(m) && IFormField.class.isAssignableFrom(innerClass)) {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+  @Override
   public int getFieldIndex(IFormField f) {
     return m_fields.indexOf(f);
   }
@@ -217,6 +290,23 @@
   }
 
   /**
+   * Registers the given form field replacements on this composite field.
+   * 
+   * @param replacements
+   *          Map having old field classes as key and replacing field classes as values.
+   * @since 4.0.1
+   */
+  private void registerFormFieldReplacements(Map<Class<?>, Class<? extends IFormField>> replacements) {
+    if (replacements == null || replacements.isEmpty()) {
+      return;
+    }
+    if (m_formFieldReplacements == null) {
+      m_formFieldReplacements = new HashMap<Class<?>, Class<? extends IFormField>>();
+    }
+    m_formFieldReplacements.putAll(replacements);
+  }
+
+  /**
    * Checks whether the form field with the given class has been replaced by another form field. If so, the replacing
    * form field's class is returned. Otherwise the given class itself.
    * 
@@ -246,7 +336,22 @@
         }
       }
     }
-    // 3. field is not replaced
+    // 3. check parent field replacements (used for templates only. It is less common and therefore checked after global replacements)
+    ICompositeField parentField = getParentField();
+    while (parentField != null) {
+      if (parentField instanceof AbstractCompositeField) {
+        Map<Class<?>, Class<? extends IFormField>> parentReplacements = ((AbstractCompositeField) parentField).m_formFieldReplacements;
+        if (parentReplacements != null) {
+          @SuppressWarnings("unchecked")
+          Class<? extends T> replacementFieldClass = (Class<? extends T>) parentReplacements.get(c);
+          if (replacementFieldClass != null) {
+            return replacementFieldClass;
+          }
+        }
+      }
+      parentField = parentField.getParentField();
+    }
+    // 4. field is not replaced
     return c;
   }