You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2016/02/03 22:23:40 UTC

[1/3] isis git commit: ISIS-993: normalizing the BS3 grid, factoring out commonalities into superclass.

Repository: isis
Updated Branches:
  refs/heads/ISIS-993 63d03d29b -> 8fcac9648


http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/fixedcols/GridNormalizerServiceFC.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/fixedcols/GridNormalizerServiceFC.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/fixedcols/GridNormalizerServiceFC.java
index 820f8c0..cf18f80 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/fixedcols/GridNormalizerServiceFC.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/fixedcols/GridNormalizerServiceFC.java
@@ -16,7 +16,6 @@
  */
 package org.apache.isis.core.metamodel.services.grid.fixedcols;
 
-import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -30,7 +29,6 @@ import com.google.common.collect.Maps;
 
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.NatureOfService;
-import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.layout.common.ActionLayoutData;
 import org.apache.isis.applib.layout.common.ActionLayoutDataOwner;
 import org.apache.isis.applib.layout.common.CollectionLayoutData;
@@ -69,63 +67,25 @@ import org.apache.isis.core.metamodel.facets.properties.propertylayout.MultiLine
 import org.apache.isis.core.metamodel.facets.properties.propertylayout.NamedFacetForPropertyXml;
 import org.apache.isis.core.metamodel.facets.properties.propertylayout.RenderedAdjustedFacetForPropertyXml;
 import org.apache.isis.core.metamodel.facets.properties.propertylayout.TypicalLengthFacetForPropertyXml;
-import org.apache.isis.core.metamodel.services.grid.GridNormalizerService;
-import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.services.grid.GridNormalizerServiceAbstract;
 import org.apache.isis.core.metamodel.spec.SpecificationLoader;
-import org.apache.isis.core.metamodel.spec.feature.Contributed;
 import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
-import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
-import org.apache.isis.core.metamodel.spec.feature.ObjectMember;
 import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
 
 @DomainService(
         nature = NatureOfService.DOMAIN
 )
-public class GridNormalizerServiceFC implements GridNormalizerService {
+public class GridNormalizerServiceFC extends GridNormalizerServiceAbstract {
 
     public static final String TNS = "http://isis.apache.org/schema/applib/layout/fixedcols";
     public static final String SCHEMA_LOCATION = "http://isis.apache.org/schema/applib/layout/fixedcols/fixedcols.xsd";
 
-
-    @Programmatic
-    @Override
-    public Class<? extends Grid> gridImplementation() {
-        return FCGrid.class;
-    }
-
-    @Programmatic
-    @Override
-    public String tns() {
-        return TNS;
-    }
-
-    @Programmatic
-    @Override
-    public String schemaLocation() {
-        return SCHEMA_LOCATION;
+    public GridNormalizerServiceFC() {
+        super(FCGrid.class, TNS, SCHEMA_LOCATION);
     }
 
 
-    @Programmatic
-    @Override
-    public void normalize(final Grid grid, final Class<?> domainClass) {
-
-        final FCGrid fcGrid = (FCGrid) grid;
-
-        final ObjectSpecification objectSpec = specificationLookup.loadSpecification(domainClass);
-
-        final Map<String, OneToOneAssociation> oneToOneAssociationById =
-                ObjectMember.Util.mapById(getOneToOneAssociations(objectSpec));
-        final Map<String, OneToManyAssociation> oneToManyAssociationById =
-                ObjectMember.Util.mapById(getOneToManyAssociations(objectSpec));
-        final Map<String, ObjectAction> objectActionById =
-                ObjectMember.Util.mapById(objectSpec.getObjectActions(Contributed.INCLUDED));
-
-        derive(fcGrid, oneToOneAssociationById, oneToManyAssociationById, objectActionById);
-        overwrite(fcGrid, oneToOneAssociationById, oneToManyAssociationById, objectActionById);
-    }
-
     /**
      * Ensures that all object members (properties, collections and actions) are in the metadata.
      *
@@ -133,23 +93,26 @@ public class GridNormalizerServiceFC implements GridNormalizerService {
      *     If they are missing then they will be added to default tabs (created on the fly if need be).
      * </p>
      */
-    private static void derive(
-            final FCGrid metadata,
-            final Map<String, OneToOneAssociation> oneToOneAssociationById,
-            final Map<String, OneToManyAssociation> oneToManyAssociationById,
-            final Map<String, ObjectAction> objectActionById) {
+    @Override
+    protected boolean validateAndDerive(
+            final Grid grid,
+            final Map oneToOneAssociationById,
+            final Map oneToManyAssociationById,
+            final Map objectActionById) {
+
+        final FCGrid fcGrid = (FCGrid) grid;
 
-        final LinkedHashMap<String, PropertyLayoutData> propertyIds = metadata.getAllPropertiesById();
-        final LinkedHashMap<String, CollectionLayoutData> collectionIds = metadata.getAllCollectionsById();
-        final LinkedHashMap<String, ActionLayoutData> actionIds = metadata.getAllActionsById();
+        final LinkedHashMap<String, PropertyLayoutData> propertyIds = fcGrid.getAllPropertiesById();
+        final LinkedHashMap<String, CollectionLayoutData> collectionIds = fcGrid.getAllCollectionsById();
+        final LinkedHashMap<String, ActionLayoutData> actionIds = fcGrid.getAllActionsById();
 
-        final AtomicReference<FieldSet> defaultPropertyGroupRef = new AtomicReference<>();
+        final AtomicReference<FieldSet> defaultFieldSetRef = new AtomicReference<>();
         final AtomicReference<FCColumn> firstColumnRef = new AtomicReference<>();
         final AtomicReference<FCTabGroup> lastTabGroupRef = new AtomicReference<>();
 
         // capture the first column, and also
         // capture the first property group (if any) with the default name ('General')
-        metadata.visit(new FCGrid.VisitorAdapter() {
+        fcGrid.visit(new FCGrid.VisitorAdapter() {
             @Override
             public void visit(final FCColumn fcColumn) {
                 firstColumnRef.compareAndSet(null, fcColumn);
@@ -157,7 +120,7 @@ public class GridNormalizerServiceFC implements GridNormalizerService {
             @Override
             public void visit(final FieldSet fieldSet) {
                 if(MemberGroupLayoutFacet.DEFAULT_GROUP.equals(fieldSet.getName())) {
-                    defaultPropertyGroupRef.compareAndSet(null, fieldSet);
+                    defaultFieldSetRef.compareAndSet(null, fieldSet);
                 }
             }
             @Override
@@ -179,8 +142,8 @@ public class GridNormalizerServiceFC implements GridNormalizerService {
 
         if(!missingPropertyIds.isEmpty()) {
             // ensure that there is a property group to use
-            boolean wasSet = defaultPropertyGroupRef.compareAndSet(null, new FieldSet(MemberGroupLayoutFacet.DEFAULT_GROUP));
-            final FieldSet defaultFieldSet = defaultPropertyGroupRef.get();
+            boolean wasSet = defaultFieldSetRef.compareAndSet(null, new FieldSet(MemberGroupLayoutFacet.DEFAULT_GROUP));
+            final FieldSet defaultFieldSet = defaultFieldSetRef.get();
             if(wasSet) {
                 firstColumnRef.get().getFieldSets().add(defaultFieldSet);
             }
@@ -201,9 +164,9 @@ public class GridNormalizerServiceFC implements GridNormalizerService {
         }
 
         if(!missingCollectionIds.isEmpty()) {
-            while(metadata.getTabGroups().size() < 2) {
+            while(fcGrid.getTabGroups().size() < 2) {
                 final FCTabGroup tabGroup = new FCTabGroup();
-                metadata.getTabGroups().add(tabGroup);
+                fcGrid.getTabGroups().add(tabGroup);
                 lastTabGroupRef.set(tabGroup);
             }
             final FCTabGroup lastTabGroup = lastTabGroupRef.get();
@@ -229,203 +192,16 @@ public class GridNormalizerServiceFC implements GridNormalizerService {
 
         if(!missingActionIds.isEmpty()) {
             for (String actionId : missingActionIds) {
-                List<ActionLayoutData> actions = metadata.getActions();
+                List<ActionLayoutData> actions = fcGrid.getActions();
                 if(actions == null) {
                     actions = Lists.newArrayList();
-                    metadata.setActions(actions);
+                    fcGrid.setActions(actions);
                 }
                 actions.add(new ActionLayoutData(actionId));
             }
         }
+        return true;
     }
 
-    static class Tuple<T> {
-        final T first;
-        final T second;
-        private Tuple(final T first, final T second) {
-            this.first = first;
-            this.second = second;
-        }
-        public static <T> Tuple<T> of(final T first, final T second) {
-            return new Tuple<>(first, second);
-        }
-    }
-    /**
-     * Returns a 2-element tuple of [first-second, second-first]
-     */
-    static <T> Tuple<List<T>> surplusAndMissing(final Collection<T> first, final Collection<T> second){
-        final List<T> firstNotSecond = Lists.newArrayList(first);
-        firstNotSecond.removeAll(second);
-        final List<T> secondNotFirst = Lists.newArrayList(second);
-        secondNotFirst.removeAll(first);
-        return Tuple.of(firstNotSecond, secondNotFirst);
-    }
-
-    private void overwrite(
-            final FCGrid page,
-            final Map<String, OneToOneAssociation> oneToOneAssociationById,
-            final Map<String, OneToManyAssociation> oneToManyAssociationById,
-            final Map<String, ObjectAction> objectActionById) {
-
-        final Map<String, int[]> propertySequenceByGroup = Maps.newHashMap();
-
-        page.visit(new FCGrid.VisitorAdapter() {
-            private int collectionSequence = 1;
-            private int actionDomainObjectSequence = 1;
-            private int actionPropertyGroupSequence = 1;
-            private int actionPropertySequence = 1;
-            private int actionCollectionSequence = 1;
-
-            @Override
-            public void visit(final ActionLayoutData actionLayoutData) {
-                final ActionLayoutDataOwner actionLayoutDataOwner = actionLayoutData.getOwner();
-                final ObjectAction objectAction = objectActionById.get(actionLayoutData.getId());
-                if(objectAction == null) {
-                    return;
-                }
-
-                final String memberOrderName;
-                final int memberOrderSequence;
-                if(actionLayoutDataOwner instanceof FieldSet) {
-                    final FieldSet fieldSet = (FieldSet) actionLayoutDataOwner;
-                    final List<PropertyLayoutData> properties = fieldSet.getProperties();
-                    final PropertyLayoutData propertyLayoutData = properties.get(0); // any will do
-                    memberOrderName = propertyLayoutData.getId();
-                    memberOrderSequence = actionPropertyGroupSequence++;
-                } else if(actionLayoutDataOwner instanceof PropertyLayoutData) {
-                    final PropertyLayoutData propertyLayoutData = (PropertyLayoutData) actionLayoutDataOwner;
-                    memberOrderName = propertyLayoutData.getId();
-                    memberOrderSequence = actionPropertySequence++;
-                } else if(actionLayoutDataOwner instanceof CollectionLayoutData) {
-                    final CollectionLayoutData collectionLayoutData = (CollectionLayoutData) actionLayoutDataOwner;
-                    memberOrderName = collectionLayoutData.getId();
-                    memberOrderSequence = actionCollectionSequence++;
-                } else {
-                    // DomainObject
-                    memberOrderName = null;
-                    memberOrderSequence = actionDomainObjectSequence++;
-                }
-                FacetUtil.addFacet(
-                        new MemberOrderFacetXml(memberOrderName, ""+memberOrderSequence, translationService, objectAction));
-
-
-                if(actionLayoutDataOwner instanceof FieldSet) {
-                    if(actionLayoutData.getPosition() == null ||
-                            actionLayoutData.getPosition() == org.apache.isis.applib.annotation.ActionLayout.Position.BELOW ||
-                            actionLayoutData.getPosition() == org.apache.isis.applib.annotation.ActionLayout.Position.RIGHT) {
-                        actionLayoutData.setPosition(org.apache.isis.applib.annotation.ActionLayout.Position.PANEL);
-                    }
-                } else if(actionLayoutDataOwner instanceof PropertyLayoutData) {
-                    if(actionLayoutData.getPosition() == null ||
-                            actionLayoutData.getPosition() == org.apache.isis.applib.annotation.ActionLayout.Position.PANEL_DROPDOWN ||
-                            actionLayoutData.getPosition() == org.apache.isis.applib.annotation.ActionLayout.Position.PANEL) {
-                        actionLayoutData.setPosition(org.apache.isis.applib.annotation.ActionLayout.Position.BELOW);
-                    }
-                } else {
-                    // doesn't do anything for DomainObject or Collection
-                    actionLayoutData.setPosition(null);
-                }
-
-                FacetUtil.addFacet(ActionPositionFacetForActionXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(BookmarkPolicyFacetForActionXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(CssClassFacetForActionXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(CssClassFaFacetForActionXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(DescribedAsFacetForActionXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(HiddenFacetForActionLayoutXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(NamedFacetForActionXml.create(actionLayoutData, objectAction));
-            }
-
-            @Override
-            public void visit(final PropertyLayoutData propertyLayoutData) {
-                final OneToOneAssociation oneToOneAssociation = oneToOneAssociationById.get(propertyLayoutData.getId());
-                if(oneToOneAssociation == null) {
-                    return;
-                }
-
-                FacetUtil.addFacet(CssClassFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(DescribedAsFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(HiddenFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(LabelAtFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(MultiLineFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(NamedFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(
-                        RenderedAdjustedFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(TypicalLengthFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-
-                // @MemberOrder#name based on owning property group, @MemberOrder#sequence monotonically increasing
-                final FieldSet fieldSet = propertyLayoutData.getOwner();
-                final String groupName = fieldSet.getName();
-                final String sequence = nextInSequenceFor(groupName, propertySequenceByGroup);
-                FacetUtil.addFacet(
-                        new MemberOrderFacetXml(groupName, sequence, translationService, oneToOneAssociation));
-            }
-
-            @Override
-            public void visit(final CollectionLayoutData collectionLayoutData) {
-                final OneToManyAssociation oneToManyAssociation = oneToManyAssociationById.get(collectionLayoutData.getId());
-                if(oneToManyAssociation == null) {
-                    return;
-                }
-
-                FacetUtil.addFacet(CssClassFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(
-                        DefaultViewFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(
-                        DescribedAsFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(HiddenFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(NamedFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(PagedFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(SortedByFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-
-                // @MemberOrder#name based on the collection's id (so that each has a single "member group")
-                final String groupName = collectionLayoutData.getId();
-                final String sequence = "" + collectionSequence++;
-                FacetUtil.addFacet(
-                        new MemberOrderFacetXml(groupName, sequence, translationService, oneToManyAssociation));
-
-                // if there is only a single column and no other contents, then copy the collection Id onto the tab'
-                final MemberRegionOwner memberRegionOwner = collectionLayoutData.getOwner();
-                if(memberRegionOwner instanceof FCColumn) {
-                    final FCColumn FCColumn = (FCColumn) memberRegionOwner;
-                    final FCColumnOwner holder = FCColumn.getOwner();
-                    if(holder instanceof FCTab) {
-                        final FCTab FCTab = (FCTab) holder;
-                        if(FCTab.getContents().size() == 1 && Strings.isNullOrEmpty(FCTab.getName()) ) {
-                            final String collectionName = oneToManyAssociation.getName();
-                            FCTab.setName(collectionName);
-                        }
-                    }
-                }
-            }
-        });
-    }
-
-    private String nextInSequenceFor(
-            final String key, final Map<String, int[]> seqByKey) {
-        synchronized (seqByKey) {
-            int[] holder = seqByKey.get(key);
-            if(holder == null) {
-                holder = new int[]{0};
-                seqByKey.put(key, holder);
-            }
-            holder[0]++;
-            return ""+holder[0];
-        }
-    }
-
-    private static List<OneToOneAssociation> getOneToOneAssociations(final ObjectSpecification objectSpec) {
-        List associations = objectSpec.getAssociations(Contributed.INCLUDED, ObjectAssociation.Filters.PROPERTIES);
-        return associations;
-    }
-    private static List<OneToManyAssociation> getOneToManyAssociations(final ObjectSpecification objectSpec) {
-        List associations = objectSpec.getAssociations(Contributed.INCLUDED, ObjectAssociation.Filters.COLLECTIONS);
-        return associations;
-    }
-
-    @Inject
-    SpecificationLoader specificationLookup;
-    @Inject
-    TranslationService translationService;
-
 
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/viewer-wicket-ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/layout/bs3/BS3GridPanel.java
----------------------------------------------------------------------
diff --git a/core/viewer-wicket-ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/layout/bs3/BS3GridPanel.java b/core/viewer-wicket-ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/layout/bs3/BS3GridPanel.java
index 1be3c4b..1fe5b48 100644
--- a/core/viewer-wicket-ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/layout/bs3/BS3GridPanel.java
+++ b/core/viewer-wicket-ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/layout/bs3/BS3GridPanel.java
@@ -22,7 +22,7 @@ package org.apache.isis.viewer.wicket.ui.components.layout.bs3;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.repeater.RepeatingView;
 
-import org.apache.isis.applib.layout.bootstrap3.BS3ElementAbstract;
+import org.apache.isis.applib.layout.bootstrap3.BS3Element;
 import org.apache.isis.applib.layout.bootstrap3.BS3Grid;
 import org.apache.isis.applib.layout.bootstrap3.BS3Row;
 import org.apache.isis.viewer.wicket.model.models.EntityModel;
@@ -49,8 +49,7 @@ public class BS3GridPanel extends PanelAbstract<EntityModel> {
 
         final RepeatingView rv = new RepeatingView(ID_ROWS);
 
-        final BS3ElementAbstract element = this.bs3Page;
-
+        final BS3Element element = this.bs3Page;
         for(final BS3Row bs3Row: this.bs3Page.getRows()) {
 
             final String id = rv.newChildId();

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/viewer-wicket-ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/layout/bs3/Util.java
----------------------------------------------------------------------
diff --git a/core/viewer-wicket-ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/layout/bs3/Util.java b/core/viewer-wicket-ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/layout/bs3/Util.java
index 0b189e3..06d8877 100644
--- a/core/viewer-wicket-ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/layout/bs3/Util.java
+++ b/core/viewer-wicket-ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/layout/bs3/Util.java
@@ -21,6 +21,7 @@ package org.apache.isis.viewer.wicket.ui.components.layout.bs3;
 
 import org.apache.wicket.MarkupContainer;
 
+import org.apache.isis.applib.layout.bootstrap3.BS3Element;
 import org.apache.isis.applib.layout.bootstrap3.BS3ElementAbstract;
 import org.apache.isis.viewer.wicket.ui.util.CssClassAppender;
 
@@ -28,7 +29,7 @@ public class Util {
 
     private Util(){}
 
-    public static void appendCssClassIfRequired(final MarkupContainer markupContainer, final BS3ElementAbstract element) {
+    public static void appendCssClassIfRequired(final MarkupContainer markupContainer, final BS3Element element) {
         final String cssClass = element.getCssClass();
         if(cssClass != null) {
             CssClassAppender.appendCssClassTo(markupContainer, cssClass);


[3/3] isis git commit: ISIS-993: unreferenced properties/actions/collectoins handling

Posted by da...@apache.org.
ISIS-993: unreferenced properties/actions/collectoins handling


Project: http://git-wip-us.apache.org/repos/asf/isis/repo
Commit: http://git-wip-us.apache.org/repos/asf/isis/commit/8fcac964
Tree: http://git-wip-us.apache.org/repos/asf/isis/tree/8fcac964
Diff: http://git-wip-us.apache.org/repos/asf/isis/diff/8fcac964

Branch: refs/heads/ISIS-993
Commit: 8fcac96481cb21a47d8db0a64a76fa4693d5582d
Parents: fe46a78
Author: Dan Haywood <da...@haywood-associates.co.uk>
Authored: Wed Feb 3 21:22:30 2016 +0000
Committer: Dan Haywood <da...@haywood-associates.co.uk>
Committed: Wed Feb 3 21:22:30 2016 +0000

----------------------------------------------------------------------
 .../applib/layout/bootstrap3/bootstrap3.xsd     |  29 ++-
 .../schema/applib/layout/common/common.xsd      |   6 +-
 .../isis/applib/layout/bootstrap3/BS3Col.java   |  41 +---
 .../isis/applib/layout/bootstrap3/BS3Grid.java  |  31 ++-
 .../isis/applib/layout/bootstrap3/BS3Row.java   |  15 +-
 .../isis/applib/layout/bootstrap3/BS3Tab.java   |   7 +-
 .../applib/layout/bootstrap3/BS3TabGroup.java   |  54 ++++-
 .../isis/applib/layout/common/FieldSet.java     |  81 +++++++-
 .../isis/applib/layout/common/GridAbstract.java |   2 +-
 .../applib/layout/fixedcols/FCTabGroup.java     |   9 +-
 .../isis/core/metamodel/facetapi/FacetUtil.java |  21 +-
 .../facets/object/grid/GridFacetDefault.java    |   3 +
 .../grid/GridNormalizerServiceAbstract.java     | 110 ++++++----
 .../bootstrap3/GridNormalizerServiceBS3.java    | 201 +++++++++++++------
 .../grid/fixedcols/GridNormalizerServiceFC.java |  39 +---
 .../metamodel/services/grid/BS3GridTest.java    |  24 ++-
 .../java/domainapp/dom/simple/SimpleObject.java |   2 +
 .../dom/simple/SimpleObject.layout.xml          |  70 +++----
 18 files changed, 473 insertions(+), 272 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/adocs/documentation/src/main/asciidoc/schema/applib/layout/bootstrap3/bootstrap3.xsd
----------------------------------------------------------------------
diff --git a/adocs/documentation/src/main/asciidoc/schema/applib/layout/bootstrap3/bootstrap3.xsd b/adocs/documentation/src/main/asciidoc/schema/applib/layout/bootstrap3/bootstrap3.xsd
index 7259fcc..12c9de7 100644
--- a/adocs/documentation/src/main/asciidoc/schema/applib/layout/bootstrap3/bootstrap3.xsd
+++ b/adocs/documentation/src/main/asciidoc/schema/applib/layout/bootstrap3/bootstrap3.xsd
@@ -12,18 +12,10 @@
     <xs:element name="grid" type="tns:grid"/>
 
     <xs:complexType name="grid">
-        <xs:complexContent>
-            <xs:extension base="tns:bs3ElementAbstract">
-                <xs:sequence>
-                    <xs:element name="row" type="tns:row" maxOccurs="unbounded"/>
-                    <xs:element name="metadataErrors" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
-                </xs:sequence>
-            </xs:extension>
-        </xs:complexContent>
-    </xs:complexType>
-
-    <xs:complexType name="bs3ElementAbstract" abstract="true">
-        <xs:sequence/>
+        <xs:sequence>
+            <xs:element name="row" type="tns:row" maxOccurs="unbounded"/>
+            <xs:element name="metadataError" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
+        </xs:sequence>
         <xs:attribute name="cssClass" type="xs:string"/>
     </xs:complexType>
 
@@ -36,12 +28,18 @@
                         <xs:element ref="tns:clearFixVisible"/>
                         <xs:element ref="tns:clearFixHidden"/>
                     </xs:choice>
+                    <xs:element name="metadataError" type="xs:string" minOccurs="0"/>
                 </xs:sequence>
                 <xs:attribute name="id" type="xs:string"/>
             </xs:extension>
         </xs:complexContent>
     </xs:complexType>
 
+    <xs:complexType name="bs3ElementAbstract" abstract="true">
+        <xs:sequence/>
+        <xs:attribute name="cssClass" type="xs:string"/>
+    </xs:complexType>
+
     <xs:complexType name="col">
         <xs:complexContent>
             <xs:extension base="tns:bs3RowContent">
@@ -52,12 +50,11 @@
                     <xs:element name="tabGroup" type="tns:tabGroup" minOccurs="0" maxOccurs="unbounded"/>
                     <xs:element ref="ns1:fieldSet" minOccurs="0" maxOccurs="unbounded"/>
                     <xs:element ref="ns1:collection" minOccurs="0" maxOccurs="unbounded"/>
+                    <xs:element name="metadataError" type="xs:string" minOccurs="0"/>
                 </xs:sequence>
                 <xs:attribute name="id" type="xs:string"/>
                 <xs:attribute name="span" type="xs:int" use="required"/>
                 <xs:attribute name="unreferencedActions" type="xs:boolean"/>
-                <xs:attribute name="unreferencedCollections" type="xs:boolean"/>
-                <xs:attribute name="unreferencedProperties" type="xs:boolean"/>
             </xs:extension>
         </xs:complexContent>
     </xs:complexType>
@@ -75,8 +72,10 @@
         <xs:complexContent>
             <xs:extension base="tns:bs3ElementAbstract">
                 <xs:sequence>
-                    <xs:element name="tab" type="tns:tab" maxOccurs="unbounded"/>
+                    <xs:element name="tab" type="tns:tab" minOccurs="0" maxOccurs="unbounded"/>
+                    <xs:element name="metadataError" type="xs:string" minOccurs="0"/>
                 </xs:sequence>
+                <xs:attribute name="unreferencedCollections" type="xs:boolean"/>
             </xs:extension>
         </xs:complexContent>
     </xs:complexType>

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/adocs/documentation/src/main/asciidoc/schema/applib/layout/common/common.xsd
----------------------------------------------------------------------
diff --git a/adocs/documentation/src/main/asciidoc/schema/applib/layout/common/common.xsd b/adocs/documentation/src/main/asciidoc/schema/applib/layout/common/common.xsd
index 992a378..5b614a9 100644
--- a/adocs/documentation/src/main/asciidoc/schema/applib/layout/common/common.xsd
+++ b/adocs/documentation/src/main/asciidoc/schema/applib/layout/common/common.xsd
@@ -43,9 +43,13 @@
   <xs:complexType name="fieldSet">
     <xs:sequence>
       <xs:element ref="tns:action" minOccurs="0" maxOccurs="unbounded"/>
-      <xs:element ref="tns:property" maxOccurs="unbounded"/>
+      <xs:element ref="tns:property" minOccurs="0" maxOccurs="unbounded"/>
+      <xs:element name="metadataError" type="xs:string" minOccurs="0"/>
     </xs:sequence>
     <xs:attribute name="name" type="xs:string" use="required"/>
+    <xs:attribute name="id" type="xs:string"/>
+    <xs:attribute name="unreferencedActions" type="xs:boolean"/>
+    <xs:attribute name="unreferencedProperties" type="xs:boolean"/>
   </xs:complexType>
 
   <xs:complexType name="property">

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Col.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Col.java b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Col.java
index 752c7e4..f51fcba 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Col.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Col.java
@@ -118,43 +118,6 @@ public class BS3Col extends BS3RowContent
     }
 
 
-    private Boolean unreferencedProperties;
-    /**
-     * Whether the first fieldset in this column should be used to hold any unreferenced properties (contributed or &quot;native&quot;).
-     *
-     * <p>
-     *     Any layout must have precisely one column that has this attribute set, and that column must have at least one {@link FieldSet}.
-     * </p>
-     */
-    @XmlAttribute(required = false)
-    public Boolean isUnreferencedProperties() {
-        return unreferencedProperties;
-    }
-
-    public void setUnreferencedProperties(final Boolean unreferencedProperties) {
-        this.unreferencedProperties = unreferencedProperties;
-    }
-
-
-    private Boolean unreferencedCollections;
-    /**
-     * Whether this column should be used to hold any unreferenced collections (contributed or &quot;native&quot;).
-     *
-     * <p>
-     *     Any layout must have precisely one column that has this attribute set.
-     * </p>
-     */
-    @XmlAttribute(required = false)
-    public Boolean isUnreferencedCollections() {
-        return unreferencedCollections;
-    }
-
-    public void setUnreferencedCollections(final Boolean unreferencedCollections) {
-        this.unreferencedCollections = unreferencedCollections;
-    }
-
-
-
 
     private DomainObjectLayoutData domainObject;
 
@@ -241,6 +204,10 @@ public class BS3Col extends BS3RowContent
     }
 
 
+    @Override
+    public String getPath() {
+        return getId() != null? getId(): super.getPath();
+    }
 
 
 

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Grid.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Grid.java b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Grid.java
index 6360480..f5e9d5d 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Grid.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Grid.java
@@ -18,7 +18,6 @@
  */
 package org.apache.isis.applib.layout.bootstrap3;
 
-import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 
@@ -28,6 +27,7 @@ import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
 import org.apache.isis.applib.annotation.Programmatic;
@@ -54,8 +54,8 @@ import org.apache.isis.applib.services.dto.Dto;
 @XmlType(
         name = "grid"
         , propOrder = {
-            "rows"
-            , "metadataErrors"
+            "rows",
+            "metadataErrors"
         }
 )
 public class BS3Grid extends GridAbstract implements BS3Element, Dto, BS3RowOwner {
@@ -91,9 +91,7 @@ public class BS3Grid extends GridAbstract implements BS3Element, Dto, BS3RowOwne
 
 
 
-    private List<BS3Row> rows = new ArrayList<BS3Row>(){{
-        add(new BS3Row());
-    }};
+    private List<BS3Row> rows = Lists.newArrayList();
 
     // no wrapper
     @XmlElement(name = "row", required = true)
@@ -107,6 +105,27 @@ public class BS3Grid extends GridAbstract implements BS3Element, Dto, BS3RowOwne
 
 
 
+    private List<String> metadataErrors = Lists.newArrayList();
+
+    /**
+     * For diagnostics; populated by the framework if and only if a metadata error.
+     */
+    @XmlElement(name = "metadataError", required = false)
+    public List<String> getMetadataErrors() {
+        return metadataErrors;
+    }
+
+    public void setMetadataErrors(final List<String> metadataErrors) {
+        this.metadataErrors = metadataErrors;
+    }
+
+
+
+
+    private BS3RowOwner owner;
+
+
+
     interface Visitor extends Grid.Visitor {
         void visit(final BS3Grid bs3Page);
         void visit(final BS3Row bs3Row);

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Row.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Row.java b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Row.java
index d05dbf8..097c883 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Row.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Row.java
@@ -18,7 +18,6 @@
  */
 package org.apache.isis.applib.layout.bootstrap3;
 
-import java.util.ArrayList;
 import java.util.List;
 
 import javax.xml.bind.annotation.XmlAttribute;
@@ -28,6 +27,8 @@ import javax.xml.bind.annotation.XmlElementRefs;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
+import com.google.common.collect.Lists;
+
 /**
  * Contains a row of content, either on the top-level {@link BS3Grid page} or at any other lower-level element that can
  * contain rows, eg {@link BS3Tab tabs}.
@@ -64,9 +65,7 @@ public class BS3Row extends BS3ElementAbstract implements HasCssId, BS3RowConten
 
 
 
-    private List<BS3RowContent> cols = new ArrayList<BS3RowContent>(){{
-        add(new BS3Col());
-    }};
+    private List<BS3RowContent> cols = Lists.newArrayList();
 
     // no wrapper
     @XmlElementRefs({
@@ -84,6 +83,13 @@ public class BS3Row extends BS3ElementAbstract implements HasCssId, BS3RowConten
 
 
 
+    @Override
+    public String getPath() {
+        return getId() != null? getId(): super.getPath();
+    }
+
+
+
     private String metadataError;
 
     /**
@@ -101,6 +107,7 @@ public class BS3Row extends BS3ElementAbstract implements HasCssId, BS3RowConten
 
     private BS3RowOwner owner;
 
+
     /**
      * Owner.
      *

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Tab.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Tab.java b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Tab.java
index 57e7ca5..f1d4256 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Tab.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Tab.java
@@ -18,7 +18,6 @@
  */
 package org.apache.isis.applib.layout.bootstrap3;
 
-import java.util.ArrayList;
 import java.util.List;
 
 import javax.xml.bind.annotation.XmlAttribute;
@@ -27,6 +26,7 @@ import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
 import com.google.common.base.Predicate;
+import com.google.common.collect.Lists;
 
 /**
  * Represents a tab within a {@link BS3TabGroup tab group}.
@@ -57,10 +57,7 @@ public class BS3Tab extends BS3ElementAbstract implements BS3RowOwner {
     }
 
 
-    // at least one row
-    private List<BS3Row> rows = new ArrayList<BS3Row>(){{
-        add(new BS3Row());
-    }};
+    private List<BS3Row> rows = Lists.newArrayList();
 
     // no wrapper
     @XmlElement(name = "row", required = true)

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3TabGroup.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3TabGroup.java b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3TabGroup.java
index c96634a..6f65bf7 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3TabGroup.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3TabGroup.java
@@ -18,20 +18,23 @@
  */
 package org.apache.isis.applib.layout.bootstrap3;
 
-import java.util.ArrayList;
 import java.util.List;
 
+import javax.xml.bind.annotation.XmlAttribute;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
+import com.google.common.collect.Lists;
+
 /**
  * Represents a tab group containing one or more {@link BS3Tab tab}s.
  */
 @XmlType(
         name = "tabGroup"
         , propOrder = {
-            "tabs"
+            "tabs",
+            "metadataError"
         }
 )
 public class BS3TabGroup extends BS3ElementAbstract implements BS3TabOwner {
@@ -39,12 +42,31 @@ public class BS3TabGroup extends BS3ElementAbstract implements BS3TabOwner {
     private static final long serialVersionUID = 1L;
 
 
-    private List<BS3Tab> tabs = new ArrayList<BS3Tab>(){{
-        add(new BS3Tab());
-    }};
 
-    // no wrapper
-    @XmlElement(name = "tab", required = true)
+    private Boolean unreferencedCollections;
+    /**
+     * Whether this column should be used to hold any unreferenced collections (contributed or &quot;native&quot;).
+     *
+     * <p>
+     *     Any layout must have precisely one column that has this attribute set.
+     * </p>
+     */
+    @XmlAttribute(required = false)
+    public Boolean isUnreferencedCollections() {
+        return unreferencedCollections;
+    }
+
+    public void setUnreferencedCollections(final Boolean unreferencedCollections) {
+        this.unreferencedCollections = unreferencedCollections;
+    }
+
+
+
+
+    private List<BS3Tab> tabs = Lists.newArrayList();
+
+    // no wrapper; required=false because may be auto-generated
+    @XmlElement(name = "tab", required = false)
     public List<BS3Tab> getTabs() {
         return tabs;
     }
@@ -72,5 +94,23 @@ public class BS3TabGroup extends BS3ElementAbstract implements BS3TabOwner {
         this.owner = owner;
     }
 
+
+
+    private String metadataError;
+
+    /**
+     * For diagnostics; populated by the framework if and only if a metadata error.
+     */
+    @XmlElement(required = false)
+    public String getMetadataError() {
+        return metadataError;
+    }
+
+    public void setMetadataError(final String metadataError) {
+        this.metadataError = metadataError;
+    }
+
+
+
 }
 

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/applib/src/main/java/org/apache/isis/applib/layout/common/FieldSet.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/common/FieldSet.java b/core/applib/src/main/java/org/apache/isis/applib/layout/common/FieldSet.java
index fe0a3fb..40769eb 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/common/FieldSet.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/common/FieldSet.java
@@ -19,7 +19,6 @@
 package org.apache.isis.applib.layout.common;
 
 import java.io.Serializable;
-import java.util.ArrayList;
 import java.util.List;
 
 import javax.annotation.Nullable;
@@ -49,6 +48,7 @@ import org.apache.isis.applib.annotation.Programmatic;
                 "name"
                 , "actions"
                 , "properties"
+                , "metadataError"
         }
 )
 public class FieldSet implements MemberRegion, ActionLayoutDataOwner, Serializable {
@@ -62,6 +62,63 @@ public class FieldSet implements MemberRegion, ActionLayoutDataOwner, Serializab
         setName(name);
     }
 
+
+
+    private String id;
+
+    /**
+     * As per &lt;div id=&quot;...&quot;&gt;...&lt;/div&gt; : must be unique across entire page.
+     */
+    @XmlAttribute(required = false)
+    public String getId() {
+        return id;
+    }
+
+    public void setId(final String id) {
+        this.id = id;
+    }
+
+
+
+    private Boolean unreferencedActions;
+
+    /**
+     * Whether this fieldset should be used to hold any unreferenced actions (contributed or &quot;native&quot;).
+     *
+     * <p>
+     *     Any layout must have precisely one fieldset that has this attribute set.
+     * </p>
+     */
+    @XmlAttribute(required = false)
+    public Boolean isUnreferencedActions() {
+        return unreferencedActions;
+    }
+
+    public void setUnreferencedActions(final Boolean unreferencedActions) {
+        this.unreferencedActions = unreferencedActions;
+    }
+
+
+    private Boolean unreferencedProperties;
+    /**
+     * Whether this fieldset should be used to hold any unreferenced properties (contributed or &quot;native&quot;).
+     *
+     * <p>
+     *     Any grid layout must have precisely one fieldset that has this attribute set.
+     * </p>
+     */
+    @XmlAttribute(required = false)
+    public Boolean isUnreferencedProperties() {
+        return unreferencedProperties;
+    }
+
+    public void setUnreferencedProperties(final Boolean unreferencedProperties) {
+        this.unreferencedProperties = unreferencedProperties;
+    }
+
+
+
+
     private String name;
 
     /**
@@ -92,11 +149,10 @@ public class FieldSet implements MemberRegion, ActionLayoutDataOwner, Serializab
 
 
 
-    private List<PropertyLayoutData> properties = new ArrayList<PropertyLayoutData>() {{
-        add(new PropertyLayoutData());
-    }};
+    private List<PropertyLayoutData> properties = Lists.newArrayList();
 
-    @XmlElement(name = "property", required = true)
+    // no wrapper; required=false because may be auto-generated
+    @XmlElement(name = "property", required = false)
     public List<PropertyLayoutData> getProperties() {
         return properties;
     }
@@ -141,6 +197,21 @@ public class FieldSet implements MemberRegion, ActionLayoutDataOwner, Serializab
 
 
 
+    private String metadataError;
+
+    /**
+     * For diagnostics; populated by the framework if and only if a metadata error.
+     */
+    @XmlElement(required = false)
+    public String getMetadataError() {
+        return metadataError;
+    }
+
+    public void setMetadataError(final String metadataError) {
+        this.metadataError = metadataError;
+    }
+
+
 
     public static class Util {
         private Util(){}

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/applib/src/main/java/org/apache/isis/applib/layout/common/GridAbstract.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/common/GridAbstract.java b/core/applib/src/main/java/org/apache/isis/applib/layout/common/GridAbstract.java
index 70cee11..2efb85c 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/common/GridAbstract.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/common/GridAbstract.java
@@ -27,7 +27,6 @@ import com.google.common.collect.Maps;
 
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.layout.bootstrap3.BS3Grid;
-import org.apache.isis.applib.layout.fixedcols.FCColumn;
 import org.apache.isis.applib.services.layout.GridService;
 
 /**
@@ -37,6 +36,7 @@ import org.apache.isis.applib.services.layout.GridService;
  *     It is used by the {@link GridService} as a common based type for any layouts read in from XML.
  * </p>
  */
+@XmlTransient // ignore this class
 public abstract class GridAbstract implements Grid {
 
     private boolean normalized;

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCTabGroup.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCTabGroup.java b/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCTabGroup.java
index 567f278..d4ad9dc 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCTabGroup.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCTabGroup.java
@@ -19,7 +19,6 @@
 package org.apache.isis.applib.layout.fixedcols;
 
 import java.io.Serializable;
-import java.util.ArrayList;
 import java.util.List;
 
 import javax.xml.bind.annotation.XmlElement;
@@ -28,6 +27,7 @@ import javax.xml.bind.annotation.XmlType;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Lists;
 
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.layout.common.Owned;
@@ -42,13 +42,10 @@ public class FCTabGroup implements FCColumnOwner, Serializable, Owned<FCTabGroup
 
     private static final long serialVersionUID = 1L;
 
-    // must be at least one tab.
-    private List<FCTab> tabs = new ArrayList<FCTab>(){{
-        add(new FCTab());
-    }};
+    private List<FCTab> tabs = Lists.newArrayList();
 
     // no wrapper
-    @XmlElement(name = "tab", required = true)
+    @XmlElement(name = "tab", required = false)
     public List<FCTab> getTabs() {
         return tabs;
     }

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facetapi/FacetUtil.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facetapi/FacetUtil.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facetapi/FacetUtil.java
index b5e67f7..8d9eb73 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facetapi/FacetUtil.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facetapi/FacetUtil.java
@@ -33,6 +33,26 @@ public final class FacetUtil {
     private FacetUtil() {
     }
 
+    public static void addOrReplaceFacet(final Facet facet) {
+        if (facet == null) {
+            return;
+        }
+        final FacetHolder facetHolder = facet.getFacetHolder();
+        final List<Facet> facets = facetHolder.getFacets(new Filter<Facet>() {
+            @Override
+            public boolean accept(final Facet each) {
+                return facet.facetType() == each.facetType() && facet.getClass() == each.getClass();
+            }
+        });
+        if(facets.size() == 1) {
+            final Facet existingFacet = facets.get(0);
+            final Facet underlyingFacet = existingFacet.getUnderlyingFacet();
+            facetHolder.removeFacet(existingFacet);
+            facet.setUnderlyingFacet(underlyingFacet);
+        }
+        facetHolder.addFacet(facet);
+    }
+
     /**
      * Attaches the {@link Facet} to its {@link Facet#getFacetHolder() facet
      * holder}.
@@ -151,5 +171,4 @@ public final class FacetUtil {
         }
     }
 
-
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/grid/GridFacetDefault.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/grid/GridFacetDefault.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/grid/GridFacetDefault.java
index f2de0fc..596829d 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/grid/GridFacetDefault.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/grid/GridFacetDefault.java
@@ -104,6 +104,9 @@ public class GridFacetDefault
         final Class<?> domainClass = getSpecification().getCorrespondingClass();
 
         gridNormalizerService.normalize(grid, domainClass);
+
+        grid.setNormalized(true);
+
         return grid;
     }
 

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerServiceAbstract.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerServiceAbstract.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerServiceAbstract.java
index 1c0ab06..91d8aba 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerServiceAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerServiceAbstract.java
@@ -26,6 +26,10 @@ import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.isis.applib.DomainObjectContainer;
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.layout.common.ActionLayoutData;
 import org.apache.isis.applib.layout.common.ActionLayoutDataOwner;
@@ -39,6 +43,8 @@ import org.apache.isis.applib.layout.fixedcols.FCColumnOwner;
 import org.apache.isis.applib.layout.fixedcols.FCGrid;
 import org.apache.isis.applib.layout.fixedcols.FCTab;
 import org.apache.isis.applib.services.i18n.TranslationService;
+import org.apache.isis.applib.services.jaxb.JaxbService;
+import org.apache.isis.core.metamodel.facetapi.Facet;
 import org.apache.isis.core.metamodel.facetapi.FacetUtil;
 import org.apache.isis.core.metamodel.facets.actions.layout.ActionPositionFacetForActionXml;
 import org.apache.isis.core.metamodel.facets.actions.layout.BookmarkPolicyFacetForActionXml;
@@ -65,6 +71,7 @@ import org.apache.isis.core.metamodel.facets.properties.propertylayout.RenderedA
 import org.apache.isis.core.metamodel.facets.properties.propertylayout.TypicalLengthFacetForPropertyXml;
 import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 import org.apache.isis.core.metamodel.spec.SpecificationLoader;
+import org.apache.isis.core.metamodel.spec.SpecificationLoaderAware;
 import org.apache.isis.core.metamodel.spec.feature.Contributed;
 import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
 import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
@@ -72,7 +79,10 @@ import org.apache.isis.core.metamodel.spec.feature.ObjectMember;
 import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
 
-public abstract class GridNormalizerServiceAbstract<G extends Grid> implements GridNormalizerService<G> {
+public abstract class GridNormalizerServiceAbstract<G extends Grid>
+        implements GridNormalizerService<G>, SpecificationLoaderAware {
+
+    private static final Logger LOG = LoggerFactory.getLogger(GridNormalizerServiceAbstract.class);
 
     private final Class<G> gridImplementation;
     private final String tns;
@@ -118,9 +128,17 @@ public abstract class GridNormalizerServiceAbstract<G extends Grid> implements G
         final Map<String, ObjectAction> objectActionById =
                 ObjectMember.Util.mapById(objectSpec.getObjectActions(Contributed.INCLUDED));
 
-        final boolean validation = validateAndDerive(grid, oneToOneAssociationById, oneToManyAssociationById, objectActionById);
-        if(validation) {
+        final boolean valid =
+                validateAndDerive(
+                    grid, oneToOneAssociationById, oneToManyAssociationById, objectActionById, objectSpec);
+        if (valid) {
             overwrite(grid, oneToOneAssociationById, oneToManyAssociationById, objectActionById);
+            if(LOG.isDebugEnabled()) {
+                LOG.debug("Grid:\n\n" + jaxbService.toXml(grid) + "\n\n");
+            }
+        } else {
+            container.warnUser("Grid metadata errors; check the error log");
+            LOG.error("Grid metadata errors:\n\n" + jaxbService.toXml(grid) + "\n\n");
         }
     }
 
@@ -131,11 +149,16 @@ public abstract class GridNormalizerServiceAbstract<G extends Grid> implements G
             final Grid grid,
             final Map<String, OneToOneAssociation> oneToOneAssociationById,
             final Map<String, OneToManyAssociation> oneToManyAssociationById,
-            final Map<String, ObjectAction> objectActionById);
-
-
-
+            final Map<String, ObjectAction> objectActionById,
+            final ObjectSpecification objectSpec);
 
+    /**
+     * Overwrites (replaces) any existing facets in the metamodel with info taken from the grid.
+     *
+     * <p>
+     *     This code uses {@link FacetUtil#addOrReplaceFacet(Facet)} because the layout might be changed multiple times.
+     * </p>
+     */
     protected void overwrite(
             final G fcGrid,
             final Map<String, OneToOneAssociation> oneToOneAssociationById,
@@ -143,9 +166,9 @@ public abstract class GridNormalizerServiceAbstract<G extends Grid> implements G
             final Map<String, ObjectAction> objectActionById) {
 
         final Map<String, int[]> propertySequenceByGroup = Maps.newHashMap();
-
         fcGrid.visit(new FCGrid.VisitorAdapter() {
             private int collectionSequence = 1;
+
             private int actionDomainObjectSequence = 1;
             private int actionPropertyGroupSequence = 1;
             private int actionPropertySequence = 1;
@@ -176,14 +199,17 @@ public abstract class GridNormalizerServiceAbstract<G extends Grid> implements G
                     memberOrderName = collectionLayoutData.getId();
                     memberOrderSequence = actionCollectionSequence++;
                 } else {
-                    // DomainObject
+                    // means' don't add: any existing metadata should be preserved
                     memberOrderName = null;
                     memberOrderSequence = actionDomainObjectSequence++;
                 }
-                FacetUtil.addFacet(
-                        new MemberOrderFacetXml(memberOrderName, ""+memberOrderSequence, translationService, objectAction));
-
+                if(memberOrderName != null) {
+                    FacetUtil.addOrReplaceFacet(
+                            new MemberOrderFacetXml(memberOrderName, "" + memberOrderSequence, translationService,
+                                    objectAction));
+                }
 
+                // fix up the action position if required
                 if(actionLayoutDataOwner instanceof FieldSet) {
                     if(actionLayoutData.getPosition() == null ||
                             actionLayoutData.getPosition() == org.apache.isis.applib.annotation.ActionLayout.Position.BELOW ||
@@ -201,13 +227,13 @@ public abstract class GridNormalizerServiceAbstract<G extends Grid> implements G
                     actionLayoutData.setPosition(null);
                 }
 
-                FacetUtil.addFacet(ActionPositionFacetForActionXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(BookmarkPolicyFacetForActionXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(CssClassFacetForActionXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(CssClassFaFacetForActionXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(DescribedAsFacetForActionXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(HiddenFacetForActionLayoutXml.create(actionLayoutData, objectAction));
-                FacetUtil.addFacet(NamedFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addOrReplaceFacet(ActionPositionFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addOrReplaceFacet(BookmarkPolicyFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addOrReplaceFacet(CssClassFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addOrReplaceFacet(CssClassFaFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addOrReplaceFacet(DescribedAsFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addOrReplaceFacet(HiddenFacetForActionLayoutXml.create(actionLayoutData, objectAction));
+                FacetUtil.addOrReplaceFacet(NamedFacetForActionXml.create(actionLayoutData, objectAction));
             }
 
             @Override
@@ -217,21 +243,21 @@ public abstract class GridNormalizerServiceAbstract<G extends Grid> implements G
                     return;
                 }
 
-                FacetUtil.addFacet(CssClassFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(DescribedAsFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(HiddenFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(LabelAtFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(MultiLineFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(NamedFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(
+                FacetUtil.addOrReplaceFacet(CssClassFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addOrReplaceFacet(DescribedAsFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addOrReplaceFacet(HiddenFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addOrReplaceFacet(LabelAtFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addOrReplaceFacet(MultiLineFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addOrReplaceFacet(NamedFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addOrReplaceFacet(
                         RenderedAdjustedFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
-                FacetUtil.addFacet(TypicalLengthFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addOrReplaceFacet(TypicalLengthFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
 
                 // @MemberOrder#name based on owning property group, @MemberOrder#sequence monotonically increasing
                 final FieldSet fieldSet = propertyLayoutData.getOwner();
                 final String groupName = fieldSet.getName();
                 final String sequence = nextInSequenceFor(groupName, propertySequenceByGroup);
-                FacetUtil.addFacet(
+                FacetUtil.addOrReplaceFacet(
                         new MemberOrderFacetXml(groupName, sequence, translationService, oneToOneAssociation));
             }
 
@@ -242,20 +268,20 @@ public abstract class GridNormalizerServiceAbstract<G extends Grid> implements G
                     return;
                 }
 
-                FacetUtil.addFacet(CssClassFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(
+                FacetUtil.addOrReplaceFacet(CssClassFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addOrReplaceFacet(
                         DefaultViewFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(
+                FacetUtil.addOrReplaceFacet(
                         DescribedAsFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(HiddenFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(NamedFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(PagedFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
-                FacetUtil.addFacet(SortedByFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addOrReplaceFacet(HiddenFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addOrReplaceFacet(NamedFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addOrReplaceFacet(PagedFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addOrReplaceFacet(SortedByFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
 
                 // @MemberOrder#name based on the collection's id (so that each has a single "member group")
                 final String groupName = collectionLayoutData.getId();
                 final String sequence = "" + collectionSequence++;
-                FacetUtil.addFacet(
+                FacetUtil.addOrReplaceFacet(
                         new MemberOrderFacetXml(groupName, sequence, translationService, oneToManyAssociation));
 
                 // if there is only a single column and no other contents, then copy the collection Id onto the tab'
@@ -326,10 +352,20 @@ public abstract class GridNormalizerServiceAbstract<G extends Grid> implements G
     }
 
 
-    @Inject
     SpecificationLoader specificationLookup;
+
+    public void setSpecificationLoader(final SpecificationLoader specificationLookup) {
+        this.specificationLookup = specificationLookup;
+    }
+
     @Inject
     TranslationService translationService;
 
+    @Inject
+    JaxbService jaxbService;
+
+    @Inject
+    DomainObjectContainer container;
+
 
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/bootstrap3/GridNormalizerServiceBS3.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/bootstrap3/GridNormalizerServiceBS3.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/bootstrap3/GridNormalizerServiceBS3.java
index a4f845a..5761965 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/bootstrap3/GridNormalizerServiceBS3.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/bootstrap3/GridNormalizerServiceBS3.java
@@ -21,6 +21,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
 
+import javax.annotation.Nullable;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
@@ -29,6 +33,7 @@ import org.apache.isis.applib.annotation.NatureOfService;
 import org.apache.isis.applib.layout.bootstrap3.BS3Col;
 import org.apache.isis.applib.layout.bootstrap3.BS3Grid;
 import org.apache.isis.applib.layout.bootstrap3.BS3Row;
+import org.apache.isis.applib.layout.bootstrap3.BS3RowContent;
 import org.apache.isis.applib.layout.bootstrap3.BS3Tab;
 import org.apache.isis.applib.layout.bootstrap3.BS3TabGroup;
 import org.apache.isis.applib.layout.common.ActionLayoutData;
@@ -36,9 +41,9 @@ import org.apache.isis.applib.layout.common.CollectionLayoutData;
 import org.apache.isis.applib.layout.common.FieldSet;
 import org.apache.isis.applib.layout.common.Grid;
 import org.apache.isis.applib.layout.common.PropertyLayoutData;
-import org.apache.isis.applib.layout.fixedcols.FCColumn;
-import org.apache.isis.core.metamodel.facets.object.membergroups.MemberGroupLayoutFacet;
+import org.apache.isis.core.metamodel.facets.members.order.MemberOrderFacet;
 import org.apache.isis.core.metamodel.services.grid.GridNormalizerServiceAbstract;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
 import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
@@ -61,7 +66,7 @@ public class GridNormalizerServiceBS3 extends GridNormalizerServiceAbstract<BS3G
             final Grid grid,
             final Map<String, OneToOneAssociation> oneToOneAssociationById,
             final Map<String, OneToManyAssociation> oneToManyAssociationById,
-            final Map<String, ObjectAction> objectActionById) {
+            final Map<String, ObjectAction> objectActionById, final ObjectSpecification objectSpec) {
 
         final BS3Grid bs3Grid = (BS3Grid) grid;
 
@@ -117,53 +122,75 @@ public class GridNormalizerServiceBS3 extends GridNormalizerServiceAbstract<BS3G
         // unreferencedActions, unreferencedProperties and unreferencedCollections attribute set.
 
         final AtomicReference<BS3Col> colForUnreferencedActionsRef = new AtomicReference<>();
-        final AtomicReference<BS3Col> colForUnreferencedPropertiesRef = new AtomicReference<>();
-        final AtomicReference<FieldSet> fieldSetForUnreferencedPropsRef = new AtomicReference<>();
-        final AtomicReference<BS3Col> colForUnreferencedCollectionsRef = new AtomicReference<>();
+        final AtomicReference<FieldSet> fieldSetForUnreferencedActionsRef = new AtomicReference<>();
+        final AtomicReference<FieldSet> fieldSetForUnreferencedPropertiesRef = new AtomicReference<>();
+        final AtomicReference<BS3TabGroup> tabGroupForUnreferencedCollectionsRef = new AtomicReference<>();
 
         bs3Grid.visit(new BS3Grid.VisitorAdapter(){
             @Override
             public void visit(final BS3Col bs3Col) {
-                if(bs3Col.isUnreferencedActions()) {
+                if(isSet(bs3Col.isUnreferencedActions())) {
                     if(colForUnreferencedActionsRef.get() != null) {
                         bs3Col.setMetadataError("More than one col with 'unreferencedActions' attribute set");
+                    } else if(fieldSetForUnreferencedActionsRef.get() != null) {
+                        bs3Col.setMetadataError("Already found a fieldset with 'unreferencedActions' attribute set");
                     } else {
                         colForUnreferencedActionsRef.set(bs3Col);
                     }
                 }
-                if(bs3Col.isUnreferencedProperties()) {
-                    if(colForUnreferencedPropertiesRef.get() != null) {
-                        bs3Col.setMetadataError("More than one col with 'unreferencedProperties' attribute set");
+            }
+
+            @Override
+            public void visit(final FieldSet fieldSet) {
+                if(isSet(fieldSet.isUnreferencedActions())) {
+                    if(fieldSetForUnreferencedActionsRef.get() != null) {
+                        fieldSet.setMetadataError("More than one fieldset with 'unreferencedActions' attribute set");
+                    } else if(colForUnreferencedActionsRef.get() != null) {
+                        fieldSet.setMetadataError("Already found a column with 'unreferencedActions' attribute set");
                     } else {
-                        final List<FieldSet> fieldSets = bs3Col.getFieldSets();
-                        for (FieldSet fieldSet : fieldSets) {
-                            if(fieldSet.getName().equals(MemberGroupLayoutFacet.DEFAULT_GROUP)) {
-                                fieldSetForUnreferencedPropsRef.set(fieldSet);
-                            }
-                        }
-                        colForUnreferencedPropertiesRef.set(bs3Col);
+                        fieldSetForUnreferencedActionsRef.set(fieldSet);
                     }
                 }
-                if(bs3Col.isUnreferencedCollections()) {
-                    if(colForUnreferencedCollectionsRef.get() != null) {
-                        bs3Col.setMetadataError("More than one col with 'unreferencedCollections' attribute set");
+                if(isSet(fieldSet.isUnreferencedProperties())) {
+                    if(fieldSetForUnreferencedPropertiesRef.get() != null) {
+                        fieldSet.setMetadataError("More than one col with 'unreferencedProperties' attribute set");
+                    } else {
+                        fieldSetForUnreferencedPropertiesRef.set(fieldSet);
+                    }
+                }
+            }
+
+            @Override
+            public void visit(final BS3TabGroup bs3TabGroup) {
+                if(isSet(bs3TabGroup.isUnreferencedCollections())) {
+                    if(tabGroupForUnreferencedCollectionsRef.get() != null) {
+                        bs3TabGroup.setMetadataError("More than one tabgroup with 'unreferencedCollections' attribute set");
                     } else {
-                        colForUnreferencedCollectionsRef.set(bs3Col);
+                        tabGroupForUnreferencedCollectionsRef.set(bs3TabGroup);
                     }
                 }
             }
         });
 
-        if(     colForUnreferencedActionsRef.get() == null ||
-                colForUnreferencedPropertiesRef.get() == null ||
-                colForUnreferencedCollectionsRef.get() == null) {
-            return false;
+        if(colForUnreferencedActionsRef.get() == null && fieldSetForUnreferencedActionsRef.get() == null) {
+            bs3Grid.getMetadataErrors().add("No column and no fieldset has the 'unreferencedActions' attribute set");
+        }
+        if(fieldSetForUnreferencedPropertiesRef.get() == null) {
+            bs3Grid.getMetadataErrors().add("No fieldset has the 'unreferencedProperties' attribute set");
+        }
+        if(tabGroupForUnreferencedCollectionsRef.get() == null) {
+            bs3Grid.getMetadataErrors().add("No tabgroup has the 'unreferencedCollections' attribute set");
         }
 
+        if(     colForUnreferencedActionsRef.get() == null && fieldSetForUnreferencedActionsRef.get() == null ||
+                fieldSetForUnreferencedPropertiesRef.get() == null ||
+                tabGroupForUnreferencedCollectionsRef.get() == null) {
+            return false;
+        }
 
         // add missing properties will be added to the first fieldset of the specified column
         final Tuple<List<String>> propertyIdTuple =
-                surplusAndMissing(propertyIds.keySet(), oneToOneAssociationById.keySet());
+                surplusAndMissing(propertyIds.keySet(),  oneToOneAssociationById.keySet());
         final List<String> surplusPropertyIds = propertyIdTuple.first;
         final List<String> missingPropertyIds = propertyIdTuple.second;
 
@@ -172,18 +199,9 @@ public class GridNormalizerServiceBS3 extends GridNormalizerServiceAbstract<BS3G
         }
 
         if(!missingPropertyIds.isEmpty()) {
-            final BS3Col bs3Col = colForUnreferencedPropertiesRef.get();
-            if(bs3Col != null) {
-                // ensure that there is a field set to use, else create
-                boolean wasSet = fieldSetForUnreferencedPropsRef.compareAndSet(null, new FieldSet(MemberGroupLayoutFacet.DEFAULT_GROUP));
-                final FieldSet fieldSetForUnref = fieldSetForUnreferencedPropsRef.get();
-                if(wasSet) {
-                    fieldSetForUnref.setOwner(bs3Col);
-                    bs3Col.getFieldSets().add(fieldSetForUnref);
-                }
-                for (final String propertyId : missingPropertyIds) {
-                    bs3Col.getFieldSets().get(0).getProperties().add(new PropertyLayoutData(propertyId));
-                }
+            final FieldSet fieldSet = fieldSetForUnreferencedPropertiesRef.get();
+            if(fieldSet != null) {
+                addMissingPropertiesTo(fieldSet, missingPropertyIds);
             }
         }
 
@@ -198,20 +216,9 @@ public class GridNormalizerServiceBS3 extends GridNormalizerServiceAbstract<BS3G
         }
 
         if(!missingCollectionIds.isEmpty()) {
-            final BS3Col bs3Col = colForUnreferencedCollectionsRef.get();
-            if(bs3Col != null) {
-                final BS3TabGroup tabGroup = new BS3TabGroup();
-                tabGroup.setOwner(bs3Col);
-                bs3Col.getTabGroups().add(tabGroup);
-                for (final String collectionId : missingCollectionIds) {
-                    final BS3Tab bs3Tab = new BS3Tab();
-                    tabGroup.getTabs().add(bs3Tab);
-                    FCColumn left = new FCColumn(12);
-                    bs3Tab.setOwner(tabGroup);
-                    final CollectionLayoutData layoutMetadata = new CollectionLayoutData(collectionId);
-                    layoutMetadata.setDefaultView("table");
-                    left.getCollections().add(layoutMetadata);
-                }
+            final BS3TabGroup bs3TabGroup = tabGroupForUnreferencedCollectionsRef.get();
+            if(bs3TabGroup != null) {
+                addMissingCollectionsTo(bs3TabGroup, missingCollectionIds, objectSpec);
             }
         }
 
@@ -219,7 +226,27 @@ public class GridNormalizerServiceBS3 extends GridNormalizerServiceAbstract<BS3G
         final Tuple<List<String>> actionIdTuple =
                 surplusAndMissing(actionIds.keySet(), objectActionById.keySet());
         final List<String> surplusActionIds = actionIdTuple.first;
-        final List<String> missingActionIds = actionIdTuple.second;
+
+        // ... the missing actions are those in the second tuple, excluding those bound via @MemberOrder#name
+        // to a property or collection.
+        final List<String> missingActionIds =
+                FluentIterable.from(actionIdTuple.second)
+                        .filter(new Predicate<String>() {
+                            @Override public boolean apply(@Nullable final String actionId) {
+                                final ObjectAction oa = objectActionById.get(actionId);
+                                final MemberOrderFacet memberOrderFacet = oa.getFacet(MemberOrderFacet.class);
+                                if(memberOrderFacet == null) {
+                                    return true;
+                                }
+                                final String memberOrderName = memberOrderFacet.name();
+                                if (memberOrderName == null) {
+                                    return true;
+                                }
+                                return  !oneToOneAssociationById.containsKey(memberOrderName) &&
+                                        !oneToManyAssociationById.containsKey(memberOrderName);
+                            }
+                        })
+                        .toList();
 
         for (String surplusActionId : surplusActionIds) {
             actionIds.get(surplusActionId).setMetadataError("No such action");
@@ -228,13 +255,11 @@ public class GridNormalizerServiceBS3 extends GridNormalizerServiceAbstract<BS3G
         if(!missingActionIds.isEmpty()) {
             final BS3Col bs3Col = colForUnreferencedActionsRef.get();
             if(bs3Col != null) {
-                for (String actionId : missingActionIds) {
-                    List<ActionLayoutData> actions = bs3Col.getActions();
-                    if(actions == null) {
-                        actions = Lists.newArrayList();
-                        bs3Col.setActions(actions);
-                    }
-                    actions.add(new ActionLayoutData(actionId));
+                addMissingActionsTo(bs3Col, missingActionIds);
+            } else {
+                final FieldSet fieldSet = fieldSetForUnreferencedActionsRef.get();
+                if(fieldSet != null) {
+                    addMissingActionsTo(fieldSet, missingActionIds);
                 }
             }
         }
@@ -242,4 +267,60 @@ public class GridNormalizerServiceBS3 extends GridNormalizerServiceAbstract<BS3G
         return true;
     }
 
+    protected void addMissingPropertiesTo(
+            final FieldSet fieldSet,
+            final List<String> missingPropertyIds) {
+        for (final String propertyId : missingPropertyIds) {
+            fieldSet.getProperties().add(new PropertyLayoutData(propertyId));
+        }
+    }
+
+    protected void addMissingActionsTo(final BS3Col bs3Col, final List<String> missingActionIds) {
+        for (String actionId : missingActionIds) {
+            List<ActionLayoutData> actions = bs3Col.getActions();
+            if(actions == null) {
+                actions = Lists.newArrayList();
+                bs3Col.setActions(actions);
+            }
+            actions.add(new ActionLayoutData(actionId));
+        }
+    }
+
+    protected void addMissingActionsTo(final FieldSet fieldSet, final List<String> missingActionIds) {
+        List<ActionLayoutData> actions = fieldSet.getActions();
+        for (String actionId : missingActionIds) {
+            actions.add(new ActionLayoutData(actionId));
+        }
+    }
+
+    protected void addMissingCollectionsTo(
+            final BS3TabGroup tabGroup,
+            final List<String> missingCollectionIds,
+            final ObjectSpecification objectSpec) {
+        for (final String collectionId : missingCollectionIds) {
+            final BS3Tab bs3Tab = new BS3Tab();
+            bs3Tab.setName(objectSpec.getAssociation(collectionId).getName());
+            tabGroup.getTabs().add(bs3Tab);
+            bs3Tab.setOwner(tabGroup);
+
+            final BS3Row tabRow = new BS3Row();
+            tabRow.setOwner(bs3Tab);
+            bs3Tab.getRows().add(tabRow);
+
+            final BS3Col tabRowCol = new BS3Col();
+            tabRowCol.setSpan(12);
+            tabRowCol.setSize(BS3RowContent.Size.MD);
+            tabRowCol.setOwner(tabRow);
+            tabRow.getCols().add(tabRowCol);
+
+            final CollectionLayoutData layoutMetadata = new CollectionLayoutData(collectionId);
+            layoutMetadata.setDefaultView("table");
+            tabRowCol.getCollections().add(layoutMetadata);
+        }
+    }
+
+    private static Boolean isSet(final Boolean flag) {
+        return flag != null && flag;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/fixedcols/GridNormalizerServiceFC.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/fixedcols/GridNormalizerServiceFC.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/fixedcols/GridNormalizerServiceFC.java
index cf18f80..c617c73 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/fixedcols/GridNormalizerServiceFC.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/fixedcols/GridNormalizerServiceFC.java
@@ -21,57 +21,22 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
 
-import javax.inject.Inject;
-
-import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.NatureOfService;
 import org.apache.isis.applib.layout.common.ActionLayoutData;
-import org.apache.isis.applib.layout.common.ActionLayoutDataOwner;
 import org.apache.isis.applib.layout.common.CollectionLayoutData;
 import org.apache.isis.applib.layout.common.FieldSet;
 import org.apache.isis.applib.layout.common.Grid;
-import org.apache.isis.applib.layout.common.MemberRegionOwner;
 import org.apache.isis.applib.layout.common.PropertyLayoutData;
 import org.apache.isis.applib.layout.fixedcols.FCColumn;
-import org.apache.isis.applib.layout.fixedcols.FCColumnOwner;
 import org.apache.isis.applib.layout.fixedcols.FCGrid;
 import org.apache.isis.applib.layout.fixedcols.FCTab;
 import org.apache.isis.applib.layout.fixedcols.FCTabGroup;
-import org.apache.isis.applib.services.i18n.TranslationService;
-import org.apache.isis.core.metamodel.facetapi.FacetUtil;
-import org.apache.isis.core.metamodel.facets.actions.layout.ActionPositionFacetForActionXml;
-import org.apache.isis.core.metamodel.facets.actions.layout.BookmarkPolicyFacetForActionXml;
-import org.apache.isis.core.metamodel.facets.actions.layout.CssClassFaFacetForActionXml;
-import org.apache.isis.core.metamodel.facets.actions.layout.CssClassFacetForActionXml;
-import org.apache.isis.core.metamodel.facets.actions.layout.DescribedAsFacetForActionXml;
-import org.apache.isis.core.metamodel.facets.actions.layout.HiddenFacetForActionLayoutXml;
-import org.apache.isis.core.metamodel.facets.actions.layout.NamedFacetForActionXml;
-import org.apache.isis.core.metamodel.facets.collections.layout.CssClassFacetForCollectionXml;
-import org.apache.isis.core.metamodel.facets.collections.layout.DefaultViewFacetForCollectionXml;
-import org.apache.isis.core.metamodel.facets.collections.layout.DescribedAsFacetForCollectionXml;
-import org.apache.isis.core.metamodel.facets.collections.layout.HiddenFacetForCollectionXml;
-import org.apache.isis.core.metamodel.facets.collections.layout.NamedFacetForCollectionXml;
-import org.apache.isis.core.metamodel.facets.collections.layout.PagedFacetForCollectionXml;
-import org.apache.isis.core.metamodel.facets.collections.layout.SortedByFacetForCollectionXml;
-import org.apache.isis.core.metamodel.facets.members.order.annotprop.MemberOrderFacetXml;
 import org.apache.isis.core.metamodel.facets.object.membergroups.MemberGroupLayoutFacet;
-import org.apache.isis.core.metamodel.facets.properties.propertylayout.CssClassFacetForPropertyXml;
-import org.apache.isis.core.metamodel.facets.properties.propertylayout.DescribedAsFacetForPropertyXml;
-import org.apache.isis.core.metamodel.facets.properties.propertylayout.HiddenFacetForPropertyXml;
-import org.apache.isis.core.metamodel.facets.properties.propertylayout.LabelAtFacetForPropertyXml;
-import org.apache.isis.core.metamodel.facets.properties.propertylayout.MultiLineFacetForPropertyXml;
-import org.apache.isis.core.metamodel.facets.properties.propertylayout.NamedFacetForPropertyXml;
-import org.apache.isis.core.metamodel.facets.properties.propertylayout.RenderedAdjustedFacetForPropertyXml;
-import org.apache.isis.core.metamodel.facets.properties.propertylayout.TypicalLengthFacetForPropertyXml;
 import org.apache.isis.core.metamodel.services.grid.GridNormalizerServiceAbstract;
-import org.apache.isis.core.metamodel.spec.SpecificationLoader;
-import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
-import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
-import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 
 @DomainService(
         nature = NatureOfService.DOMAIN
@@ -98,7 +63,7 @@ public class GridNormalizerServiceFC extends GridNormalizerServiceAbstract {
             final Grid grid,
             final Map oneToOneAssociationById,
             final Map oneToManyAssociationById,
-            final Map objectActionById) {
+            final Map objectActionById, final ObjectSpecification objectSpec) {
 
         final FCGrid fcGrid = (FCGrid) grid;
 

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/grid/BS3GridTest.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/grid/BS3GridTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/grid/BS3GridTest.java
index 3d9d6b2..706db56 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/grid/BS3GridTest.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/grid/BS3GridTest.java
@@ -72,8 +72,10 @@ public class BS3GridTest {
         final BS3Grid bs3Grid = new BS3Grid();
 
         // header
-        final BS3Row headerRow = bs3Grid.getRows().get(0);
-        final BS3Col headerCol = (BS3Col) headerRow.getCols().get(0);
+        final BS3Row headerRow = new BS3Row();
+        bs3Grid.getRows().add(headerRow);
+        final BS3Col headerCol = new BS3Col();
+        headerRow.getCols().add(headerCol);
         headerCol.setSpan(12);
 
         final DomainObjectLayoutData objectLayoutData = new DomainObjectLayoutData();
@@ -88,18 +90,22 @@ public class BS3GridTest {
         final BS3Row contentRow = new BS3Row();
         bs3Grid.getRows().add(contentRow);
 
-        final BS3Col contentCol = (BS3Col) contentRow.getCols().get(0);
+        final BS3Col contentCol = new BS3Col();
+        contentRow.getCols().add(contentCol);
         contentCol.setSpan(12);
 
         // a tabgroup containing a 'Common' tab
         final BS3TabGroup tabGroup = new BS3TabGroup();
         contentCol.getTabGroups().add(tabGroup);
-        BS3Tab bs3Tab = tabGroup.getTabs().get(0);
+        BS3Tab bs3Tab = new BS3Tab();
+        tabGroup.getTabs().add(bs3Tab);
         bs3Tab.setName("Common");
 
         // with a left col...
-        final BS3Row tabRow = bs3Tab.getRows().get(0);
-        final BS3Col tabLeftCol = (BS3Col) tabRow.getCols().get(0);
+        final BS3Row tabRow = new BS3Row();
+        bs3Tab.getRows().add(tabRow);
+        final BS3Col tabLeftCol = new BS3Col();
+        tabRow.getCols().add(tabLeftCol);
         tabLeftCol.setSpan(6);
 
         // containing a fieldset
@@ -109,7 +115,8 @@ public class BS3GridTest {
         leftPropGroup.setName("General");
 
         // with a single property
-        final PropertyLayoutData namePropertyLayoutData = leftPropGroup.getProperties().get(0);
+        final PropertyLayoutData namePropertyLayoutData = new PropertyLayoutData();
+        leftPropGroup.getProperties().add(namePropertyLayoutData);
         namePropertyLayoutData.setNamed("name");
 
         // and its associated action
@@ -119,7 +126,8 @@ public class BS3GridTest {
         namePropertyLayoutData.getActions().add(updateNameActionLayoutData);
 
         // and the tab also has a right col...
-        final BS3Col tabRightCol = (BS3Col) tabRow.getCols().get(0);
+        final BS3Col tabRightCol = new BS3Col();
+        tabRow.getCols().add(tabRightCol);
         tabRightCol.setSpan(6);
 
         // containing a collection

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/example/application/simpleapp/dom/src/main/java/domainapp/dom/simple/SimpleObject.java
----------------------------------------------------------------------
diff --git a/example/application/simpleapp/dom/src/main/java/domainapp/dom/simple/SimpleObject.java b/example/application/simpleapp/dom/src/main/java/domainapp/dom/simple/SimpleObject.java
index dad55cf..7f2702e 100644
--- a/example/application/simpleapp/dom/src/main/java/domainapp/dom/simple/SimpleObject.java
+++ b/example/application/simpleapp/dom/src/main/java/domainapp/dom/simple/SimpleObject.java
@@ -31,6 +31,7 @@ import org.apache.isis.applib.annotation.BookmarkPolicy;
 import org.apache.isis.applib.annotation.DomainObject;
 import org.apache.isis.applib.annotation.DomainObjectLayout;
 import org.apache.isis.applib.annotation.Editing;
+import org.apache.isis.applib.annotation.MemberOrder;
 import org.apache.isis.applib.annotation.Parameter;
 import org.apache.isis.applib.annotation.ParameterLayout;
 import org.apache.isis.applib.annotation.Property;
@@ -104,6 +105,7 @@ public class SimpleObject implements Comparable<SimpleObject> {
             domainEvent = UpdateNameDomainEvent.class,
             semantics = SemanticsOf.IDEMPOTENT
     )
+    @MemberOrder(name = "name", sequence = "1")
     public SimpleObject updateName(
             @Parameter(maxLength = NAME_LENGTH)
             @ParameterLayout(named = "New name")

http://git-wip-us.apache.org/repos/asf/isis/blob/8fcac964/example/application/simpleapp/dom/src/main/java/domainapp/dom/simple/SimpleObject.layout.xml
----------------------------------------------------------------------
diff --git a/example/application/simpleapp/dom/src/main/java/domainapp/dom/simple/SimpleObject.layout.xml b/example/application/simpleapp/dom/src/main/java/domainapp/dom/simple/SimpleObject.layout.xml
index b1b3ed9..34bbb54 100644
--- a/example/application/simpleapp/dom/src/main/java/domainapp/dom/simple/SimpleObject.layout.xml
+++ b/example/application/simpleapp/dom/src/main/java/domainapp/dom/simple/SimpleObject.layout.xml
@@ -1,64 +1,50 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <bs3:grid xsi:schemaLocation="http://isis.apache.org/schema/applib/layout/common http://isis.apache.org/schema/applib/layout/common/common.xsd http://isis.apache.org/schema/applib/layout/bootstrap3 http://isis.apache.org/schema/applib/layout/bootstrap3/bootstrap3.xsd" xmlns:c="http://isis.apache.org/schema/applib/layout/common" xmlns:bs3="http://isis.apache.org/schema/applib/layout/bootstrap3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 
-    <bs3:row cssClass="bishbosh">
-        <bs3:col span="8">
+<!--
+    <bs3:row>
+        <bs3:col span="8" unreferencedActions="false">
             <c:domainObject/>
         </bs3:col>
-        <bs3:col span="4" cssClass="foobar" unreferencedActions="true">
-            <c:action id="delete" cssClassFa="fa-trash"/>
-        </bs3:col>
     </bs3:row>
 
     <bs3:row>
         <bs3:col span="12">
             <bs3:tabGroup>
-                <bs3:tab name="Metadata">
+                <bs3:tab name="Properties">
                     <bs3:row>
-                        <bs3:col span="12">
-                            <c:fieldSet name="Metadata">
-                                <c:action id="downloadJdMetadata"/>
-                                <c:action id="downloadLayoutXml" position="PANEL_DROPDOWN"/>
-                                <c:property id="versionSequence"/>
-                            </c:fieldSet>
-                        </bs3:col>
                         <bs3:col span="6">
-                            <c:fieldSet name="Xxx">
-                                <c:property id="name" labelPosition="TOP">
-                                    <c:action id="updateName">
-                                        <c:describedAs>This allows the name to be updated</c:describedAs>
-                                    </c:action>
-                                </c:property>
-                            </c:fieldSet>
+                            <c:fieldSet name="General" unreferencedProperties="true" unreferencedActions="true"/>
                         </bs3:col>
                         <bs3:col span="6">
-                            <c:fieldSet name="Xxx">
-                                <c:property id="name" labelPosition="TOP">
-                                    <c:action id="updateName">
-                                        <c:describedAs>This allows the name to be updated</c:describedAs>
-                                    </c:action>
-                                </c:property>
+                            <c:fieldSet name="Metadata">
+                                <c:action id="downloadJdoMetadata"/>
+                                <c:action id="downloadLayoutXml" position="PANEL_DROPDOWN"/>
+                                <c:property id="versionSequence"/>
                             </c:fieldSet>
                         </bs3:col>
                     </bs3:row>
                 </bs3:tab>
             </bs3:tabGroup>
-            <bs3:tabGroup>
-                <bs3:tab name="SimilarTo">
-                    <bs3:row>
-                        <bs3:col span="12">
-                            <c:collection id="similarTo" defaultView="table"/>
-                        </bs3:col>
-                    </bs3:row>
-                </bs3:tab>
-                <bs3:tab name="Others">
-                    <bs3:row>
-                        <bs3:col span="12">
-                            <c:collection id="others" defaultView="hide"/>
-                        </bs3:col>
-                    </bs3:row>
-                </bs3:tab>
-            </bs3:tabGroup>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="12">
+            <bs3:tabGroup unreferencedCollections="true"/>
+        </bs3:col>
+    </bs3:row>
+-->
+    <bs3:row>
+        <bs3:col span="12" unreferencedActions="true">
+            <c:domainObject/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="6">
+            <c:fieldSet name="Properties" unreferencedProperties="true"/>
+        </bs3:col>
+        <bs3:col span="6">
+            <bs3:tabGroup unreferencedCollections="true"/>
         </bs3:col>
     </bs3:row>
 </bs3:grid>


[2/3] isis git commit: ISIS-993: normalizing the BS3 grid, factoring out commonalities into superclass.

Posted by da...@apache.org.
ISIS-993: normalizing the BS3 grid, factoring out commonalities into superclass.


Project: http://git-wip-us.apache.org/repos/asf/isis/repo
Commit: http://git-wip-us.apache.org/repos/asf/isis/commit/fe46a781
Tree: http://git-wip-us.apache.org/repos/asf/isis/tree/fe46a781
Diff: http://git-wip-us.apache.org/repos/asf/isis/diff/fe46a781

Branch: refs/heads/ISIS-993
Commit: fe46a781eb7472694e705cfd396e630964c3add3
Parents: 63d03d2
Author: Dan Haywood <da...@haywood-associates.co.uk>
Authored: Tue Feb 2 21:29:22 2016 +0000
Committer: Dan Haywood <da...@haywood-associates.co.uk>
Committed: Tue Feb 2 21:29:22 2016 +0000

----------------------------------------------------------------------
 .../isis/applib/layout/bootstrap3/BS3Col.java   |  26 +-
 .../applib/layout/bootstrap3/BS3Element.java    |  38 +++
 .../layout/bootstrap3/BS3ElementAbstract.java   |   5 +-
 .../isis/applib/layout/bootstrap3/BS3Grid.java  | 248 +++++---------
 .../isis/applib/layout/bootstrap3/BS3Row.java   |  17 +
 .../common/CollectionLayoutDataOwner.java       |  25 ++
 .../applib/layout/common/FieldSetOwner.java     |  25 ++
 .../apache/isis/applib/layout/common/Grid.java  |  39 ++-
 .../isis/applib/layout/common/GridAbstract.java | 164 +++++++++
 .../isis/applib/layout/fixedcols/FCColumn.java  |   7 +-
 .../isis/applib/layout/fixedcols/FCGrid.java    | 159 ++-------
 .../services/grid/GridNormalizerService.java    |   6 +-
 .../grid/GridNormalizerServiceAbstract.java     | 335 +++++++++++++++++++
 .../services/grid/GridNormalizerUtil.java       |  38 +++
 .../bootstrap3/GridNormalizerServiceBS3.java    | 232 +++++++++++--
 .../grid/fixedcols/GridNormalizerServiceFC.java | 274 ++-------------
 .../ui/components/layout/bs3/BS3GridPanel.java  |   5 +-
 .../wicket/ui/components/layout/bs3/Util.java   |   3 +-
 18 files changed, 1049 insertions(+), 597 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Col.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Col.java b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Col.java
index cef0f80..752c7e4 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Col.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Col.java
@@ -31,9 +31,10 @@ import com.google.common.collect.Lists;
 import org.apache.isis.applib.layout.common.ActionLayoutData;
 import org.apache.isis.applib.layout.common.ActionLayoutDataOwner;
 import org.apache.isis.applib.layout.common.CollectionLayoutData;
+import org.apache.isis.applib.layout.common.CollectionLayoutDataOwner;
 import org.apache.isis.applib.layout.common.DomainObjectLayoutData;
 import org.apache.isis.applib.layout.common.FieldSet;
-import org.apache.isis.applib.layout.common.MemberRegionOwner;
+import org.apache.isis.applib.layout.common.FieldSetOwner;
 
 /**
  * A column within a row which, depending on its {@link #getSpan()}, could be as narrow as 1/12th of the page's width, all the way up to spanning the entire page.
@@ -60,9 +61,12 @@ import org.apache.isis.applib.layout.common.MemberRegionOwner;
             "tabGroups",
             "fieldSets",
             "collections",
+            "metadataError"
         }
 )
-public class BS3Col extends BS3RowContent implements ActionLayoutDataOwner, BS3TabGroupOwner, BS3RowOwner, MemberRegionOwner, HasCssId {
+public class BS3Col extends BS3RowContent
+        implements ActionLayoutDataOwner, BS3TabGroupOwner, BS3RowOwner, FieldSetOwner, HasCssId,
+        CollectionLayoutDataOwner {
 
     private static final long serialVersionUID = 1L;
 
@@ -239,8 +243,26 @@ public class BS3Col extends BS3RowContent implements ActionLayoutDataOwner, BS3T
 
 
 
+
+    private String metadataError;
+
+    /**
+     * For diagnostics; populated by the framework if and only if a metadata error.
+     */
+    @XmlElement(required = false)
+    public String getMetadataError() {
+        return metadataError;
+    }
+
+    public void setMetadataError(final String metadataError) {
+        this.metadataError = metadataError;
+    }
+
+
+
     public String toCssClass() {
         final Size size = getSize() != null? getSize(): Size.MD;
         return "col-" + size.toCssClassFragment() + "-" + getSpan();
     }
+
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Element.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Element.java b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Element.java
new file mode 100644
index 0000000..669fcbb
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Element.java
@@ -0,0 +1,38 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.applib.layout.bootstrap3;
+
+import java.io.Serializable;
+
+public interface BS3Element extends Serializable {
+
+    /**
+     * Any additional CSS classes to render on the page element corresponding to this object,
+     * eg as per the <a href="http://getbootstrap.com/css/#grid-less">Bootstrap mixins</a> or just for
+     * custom styling.
+     */
+    String getCssClass();
+
+    void setCssClass(final String cssClass);
+
+
+    String getPath();
+    void setPath(final String path);
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3ElementAbstract.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3ElementAbstract.java b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3ElementAbstract.java
index 6bcd45a..95aa611 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3ElementAbstract.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3ElementAbstract.java
@@ -18,8 +18,6 @@
  */
 package org.apache.isis.applib.layout.bootstrap3;
 
-import java.io.Serializable;
-
 import javax.xml.bind.annotation.XmlAttribute;
 import javax.xml.bind.annotation.XmlTransient;
 
@@ -28,7 +26,7 @@ import org.apache.isis.applib.annotation.Programmatic;
 /**
  * Superclass for all layout classes, factoring out the common {@link #getCssClass()} attribute.
  */
-public abstract class BS3ElementAbstract implements Serializable {
+public abstract class BS3ElementAbstract implements BS3Element {
 
     private String cssClass;
 
@@ -63,5 +61,4 @@ public abstract class BS3ElementAbstract implements Serializable {
     }
 
 
-
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Grid.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Grid.java b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Grid.java
index e3904c4..6360480 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Grid.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Grid.java
@@ -22,21 +22,21 @@ import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 
+import javax.xml.bind.annotation.XmlAttribute;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
-import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.layout.common.ActionLayoutData;
-import org.apache.isis.applib.layout.common.ActionLayoutDataOwner;
 import org.apache.isis.applib.layout.common.CollectionLayoutData;
 import org.apache.isis.applib.layout.common.DomainObjectLayoutData;
 import org.apache.isis.applib.layout.common.FieldSet;
 import org.apache.isis.applib.layout.common.Grid;
+import org.apache.isis.applib.layout.common.GridAbstract;
 import org.apache.isis.applib.layout.common.PropertyLayoutData;
 import org.apache.isis.applib.services.dto.Dto;
 
@@ -58,77 +58,67 @@ import org.apache.isis.applib.services.dto.Dto;
             , "metadataErrors"
         }
 )
-public class BS3Grid extends BS3ElementAbstract implements Grid, Dto, BS3RowOwner {
+public class BS3Grid extends GridAbstract implements BS3Element, Dto, BS3RowOwner {
 
     private static final long serialVersionUID = 1L;
 
-    private List<BS3Row> rows = new ArrayList<BS3Row>(){{
-        add(new BS3Row());
-    }};
 
-    // no wrapper
-    @XmlElement(name = "row", required = true)
-    public List<BS3Row> getRows() {
-        return rows;
+    private String cssClass;
+
+    @XmlAttribute(required = false)
+    public String getCssClass() {
+        return cssClass;
     }
 
-    public void setRows(final List<BS3Row> rows) {
-        this.rows = rows;
+    public void setCssClass(final String cssClass) {
+        this.cssClass = cssClass;
     }
 
 
 
-    private boolean normalized;
+    private String path;
 
     @Programmatic
     @XmlTransient
-    public boolean isNormalized() {
-        return normalized;
+    public String getPath() {
+        return path;
     }
 
     @Programmatic
-    public void setNormalized(final boolean normalized) {
-        this.normalized = normalized;
+    public void setPath(final String path) {
+        this.path = path;
     }
 
-    private List<String> metadataErrors = Lists.newArrayList();
-
-    /**
-     * For diagnostics; populated by the framework if and only if a metadata error.
-     * 
-     * <p>
-     *     For example, if there is not exactly one {@link BS3Col} with the
-     *     {@link BS3Col#isUnreferencedActions()} attribute set, then this is an error.  Ditto for
-     *     {@link BS3Col#isUnreferencedCollections() collections}
-     *     and {@link BS3Col#isUnreferencedProperties() properties}.
-     * </p>
-     */
-    @XmlElement(required = false)
-    public List<String> getMetadataErrors() {
-        return metadataErrors;
+
+
+    private List<BS3Row> rows = new ArrayList<BS3Row>(){{
+        add(new BS3Row());
+    }};
+
+    // no wrapper
+    @XmlElement(name = "row", required = true)
+    public List<BS3Row> getRows() {
+        return rows;
     }
 
-    public void setMetadataErrors(final List<String> metadataErrors) {
-        this.metadataErrors = metadataErrors;
+    public void setRows(final List<BS3Row> rows) {
+        this.rows = rows;
     }
 
-    interface Visitor {
+
+
+    interface Visitor extends Grid.Visitor {
         void visit(final BS3Grid bs3Page);
         void visit(final BS3Row bs3Row);
         void visit(final BS3Col bs3Col);
         void visit(final BS3ClearFix bs3ClearFix);
         void visit(final BS3TabGroup bs3TabGroup);
         void visit(final BS3Tab bs3Tab);
-        void visit(final FieldSet fieldSet);
-        void visit(final DomainObjectLayoutData domainObjectLayout);
-        void visit(final PropertyLayoutData propertyLayoutData);
-        void visit(final CollectionLayoutData collectionLayoutData);
-        void visit(final ActionLayoutData actionLayoutData);
     }
 
-    public static class VisitorAdapter implements Visitor {
+    public static class VisitorAdapter extends Grid.VisitorAdapter implements Visitor {
         @Override
-        public void visit(final BS3Grid bs3Page) { }
+        public void visit(final BS3Grid bs3Grid) { }
         @Override
         public void visit(final BS3Row bs3Row) { }
         @Override
@@ -139,51 +129,48 @@ public class BS3Grid extends BS3ElementAbstract implements Grid, Dto, BS3RowOwne
         public void visit(final BS3TabGroup bs3TabGroup) { }
         @Override
         public void visit(final BS3Tab bs3Tab) { }
-        @Override
-        public void visit(final DomainObjectLayoutData domainObjectLayout) { }
-        @Override
-        public void visit(final FieldSet fieldSet) {}
-        @Override
-        public void visit(final PropertyLayoutData propertyLayoutData) {}
-        @Override
-        public void visit(final CollectionLayoutData collectionLayoutData) {}
-        @Override
-        public void visit(final ActionLayoutData actionLayoutData) { }
     }
 
-    public void visit(final BS3Grid.Visitor visitor) {
-        visitor.visit(this);
+    public void visit(final Grid.Visitor visitor) {
+        final BS3Grid.Visitor bs3Visitor = asBs3Visitor(visitor);
+        bs3Visitor.visit(this);
         traverseRows(this, visitor);
     }
 
-    protected void traverseRows(final BS3RowOwner rowOwner, final Visitor visitor) {
+    protected void traverseRows(final BS3RowOwner rowOwner, final Grid.Visitor visitor) {
+        final BS3Grid.Visitor bs3Visitor = asBs3Visitor(visitor);
         for (BS3Row bs3Row : rowOwner.getRows()) {
             bs3Row.setOwner(this);
-            visitor.visit(bs3Row);
-            final List<BS3RowContent> cols = bs3Row.getCols();
-            for (BS3RowContent rowContent : cols) {
-                rowContent.setOwner(bs3Row);
-                if(rowContent instanceof BS3Col) {
-                    final BS3Col bs3Col = (BS3Col) rowContent;
-                    visitor.visit(bs3Col);
-                    traverseDomainObject(bs3Col, visitor);
-                    traverseTabGroups(bs3Col, visitor);
-                    traverseActions(bs3Col, visitor);
-                    traverseFieldSets(bs3Col, visitor);
-                    traverseCollections(bs3Col, visitor);
-                    traverseRows(bs3Col, visitor);
-                } else if (rowContent instanceof BS3ClearFix) {
-                    final BS3ClearFix bs3ClearFix = (BS3ClearFix) rowContent;
-                    visitor.visit(bs3ClearFix);
-                } else {
-                    throw new IllegalStateException(
-                            "Unrecognized implementation of BS3RowContent, " + rowContent);
-                }
+            bs3Visitor.visit(bs3Row);
+            traverseCols(visitor, bs3Row);
+        }
+    }
+
+    private void traverseCols(final Grid.Visitor visitor, final BS3Row bs3Row) {
+        final BS3Grid.Visitor bs3Visitor = asBs3Visitor(visitor);
+        final List<BS3RowContent> cols = bs3Row.getCols();
+        for (BS3RowContent rowContent : cols) {
+            rowContent.setOwner(bs3Row);
+            if(rowContent instanceof BS3Col) {
+                final BS3Col bs3Col = (BS3Col) rowContent;
+                bs3Visitor.visit(bs3Col);
+                traverseDomainObject(bs3Col, visitor);
+                traverseTabGroups(bs3Col, visitor);
+                traverseActions(bs3Col, visitor);
+                traverseFieldSets(bs3Col, visitor);
+                traverseCollections(bs3Col, visitor);
+                traverseRows(bs3Col, visitor);
+            } else if (rowContent instanceof BS3ClearFix) {
+                final BS3ClearFix bs3ClearFix = (BS3ClearFix) rowContent;
+                bs3Visitor.visit(bs3ClearFix);
+            } else {
+                throw new IllegalStateException(
+                        "Unrecognized implementation of BS3RowContent, " + rowContent);
             }
         }
     }
 
-    private void traverseDomainObject(final BS3Col bs3Col, final Visitor visitor) {
+    private void traverseDomainObject(final BS3Col bs3Col, final Grid.Visitor visitor) {
         final DomainObjectLayoutData domainObject = bs3Col.getDomainObject();
         if(domainObject == null) {
             return;
@@ -194,120 +181,50 @@ public class BS3Grid extends BS3ElementAbstract implements Grid, Dto, BS3RowOwne
 
     private void traverseTabGroups(
             final BS3TabGroupOwner bs3TabGroupOwner,
-            final Visitor visitor) {
+            final Grid.Visitor visitor) {
+        final BS3Grid.Visitor bs3Visitor = asBs3Visitor(visitor);
         final List<BS3TabGroup> tabGroups = bs3TabGroupOwner.getTabGroups();
         for (BS3TabGroup bs3TabGroup : tabGroups) {
             bs3TabGroup.setOwner(bs3TabGroupOwner);
-            visitor.visit(bs3TabGroup);
+            bs3Visitor.visit(bs3TabGroup);
             traverseTabs(bs3TabGroup, visitor);
         }
     }
 
     private void traverseTabs(
             final BS3TabOwner bs3TabOwner,
-            final Visitor visitor) {
+            final Grid.Visitor visitor) {
+        final BS3Grid.Visitor bs3Visitor = asBs3Visitor(visitor);
         final List<BS3Tab> tabs = bs3TabOwner.getTabs();
         for (BS3Tab tab : tabs) {
             tab.setOwner(bs3TabOwner);
-            visitor.visit(tab);
+            bs3Visitor.visit(tab);
             traverseRows(tab, visitor);
         }
     }
 
-    private void traverseActions(
-            final ActionLayoutDataOwner actionLayoutDataOwner,
-            final Visitor visitor) {
-        final List<ActionLayoutData> actionLayoutDatas = actionLayoutDataOwner.getActions();
-        if(actionLayoutDatas == null) {
-            return;
-        }
-        for (final ActionLayoutData actionLayoutData : actionLayoutDatas) {
-            actionLayoutData.setOwner(actionLayoutDataOwner);
-            visitor.visit(actionLayoutData);
-        }
-    }
-
-    private void traverseFieldSets(final BS3Col bs3Col, final Visitor visitor) {
-        final List<FieldSet> fieldSets = bs3Col.getFieldSets();
-        for (FieldSet fieldSet : fieldSets) {
-            fieldSet.setOwner(bs3Col);
-            visitor.visit(fieldSet);
-            traverseActions(fieldSet, visitor);
-            final List<PropertyLayoutData> properties = fieldSet.getProperties();
-            for (final PropertyLayoutData property : properties) {
-                property.setOwner(fieldSet);
-                visitor.visit(property);
-                traverseActions(property, visitor);
+    private static Visitor asBs3Visitor(final Grid.Visitor visitor) {
+        return visitor instanceof Visitor? (Visitor) visitor : new BS3Grid.VisitorAdapter() {
+            @Override public void visit(final DomainObjectLayoutData domainObjectLayoutData) {
+                visitor.visit(domainObjectLayoutData);
             }
-        }
-    }
 
-    private void traverseCollections(final BS3Col bs3Col, final Visitor visitor) {
-        final List<CollectionLayoutData> collections = bs3Col.getCollections();
-        for (CollectionLayoutData collection : collections) {
-            collection.setOwner(bs3Col);
-            visitor.visit(collection);
-            traverseActions(collection, visitor);
-        }
-    }
-
-
-
-    @Programmatic
-    @XmlTransient
-    public LinkedHashMap<String, PropertyLayoutData> getAllPropertiesById() {
-        final LinkedHashMap<String, PropertyLayoutData> propertiesById = Maps.newLinkedHashMap();
-        visit(new BS3Grid.VisitorAdapter() {
-            public void visit(final PropertyLayoutData propertyLayoutData) {
-                propertiesById.put(propertyLayoutData.getId(), propertyLayoutData);
+            @Override public void visit(final ActionLayoutData actionLayoutData) {
+                visitor.visit(actionLayoutData);
             }
-        });
-        return propertiesById;
-    }
-
 
-    @Programmatic
-    @XmlTransient
-    public LinkedHashMap<String, CollectionLayoutData> getAllCollectionsById() {
-        final LinkedHashMap<String, CollectionLayoutData> collectionsById = Maps.newLinkedHashMap();
-
-        visit(new BS3Grid.VisitorAdapter() {
-            @Override
-            public void visit(final CollectionLayoutData collectionLayoutData) {
-                collectionsById.put(collectionLayoutData.getId(), collectionLayoutData);
+            @Override public void visit(final PropertyLayoutData propertyLayoutData) {
+                visitor.visit(propertyLayoutData);
             }
-        });
-        return collectionsById;
-    }
 
-
-    @Programmatic
-    @XmlTransient
-    public LinkedHashMap<String, ActionLayoutData> getAllActionsById() {
-        final LinkedHashMap<String, ActionLayoutData> actionsById = Maps.newLinkedHashMap();
-
-        visit(new BS3Grid.VisitorAdapter() {
-            @Override
-            public void visit(final ActionLayoutData actionLayoutData) {
-                actionsById.put(actionLayoutData.getId(), actionLayoutData);
+            @Override public void visit(final CollectionLayoutData collectionLayoutData) {
+                visitor.visit(collectionLayoutData);
             }
-        });
-        return actionsById;
-    }
-
 
-    @Programmatic
-    @XmlTransient
-    public LinkedHashMap<String, FieldSet> getAllFieldSetsByName() {
-        final LinkedHashMap<String, FieldSet> fieldSetsByName = Maps.newLinkedHashMap();
-
-        visit(new BS3Grid.VisitorAdapter() {
-            @Override
-            public void visit(final FieldSet fieldSet) {
-                fieldSetsByName.put(fieldSet.getName(), fieldSet);
+            @Override public void visit(final FieldSet fieldSet) {
+                visitor.visit(fieldSet);
             }
-        });
-        return fieldSetsByName;
+        };
     }
 
 
@@ -326,7 +243,6 @@ public class BS3Grid extends BS3ElementAbstract implements Grid, Dto, BS3RowOwne
     }
 
 
-    // TODO: need to figure out where the checking that can't have multiple divs with the same CSS id should go...
     @Programmatic
     @XmlTransient
     public LinkedHashMap<String, HasCssId> getAllCssId() {

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Row.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Row.java b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Row.java
index ff75f46..d05dbf8 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Row.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/bootstrap3/BS3Row.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlElementRef;
 import javax.xml.bind.annotation.XmlElementRefs;
 import javax.xml.bind.annotation.XmlTransient;
@@ -39,6 +40,7 @@ import javax.xml.bind.annotation.XmlType;
         name = "row"
         , propOrder = {
             "cols"
+            , "metadataError"
         }
 )
 public class BS3Row extends BS3ElementAbstract implements HasCssId, BS3RowContentOwner {
@@ -82,6 +84,21 @@ public class BS3Row extends BS3ElementAbstract implements HasCssId, BS3RowConten
 
 
 
+    private String metadataError;
+
+    /**
+     * For diagnostics; populated by the framework if and only if a metadata error.
+     */
+    @XmlElement(required = false)
+    public String getMetadataError() {
+        return metadataError;
+    }
+
+    public void setMetadataError(final String metadataError) {
+        this.metadataError = metadataError;
+    }
+
+
     private BS3RowOwner owner;
 
     /**

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/common/CollectionLayoutDataOwner.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/common/CollectionLayoutDataOwner.java b/core/applib/src/main/java/org/apache/isis/applib/layout/common/CollectionLayoutDataOwner.java
new file mode 100644
index 0000000..438ae56
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/common/CollectionLayoutDataOwner.java
@@ -0,0 +1,25 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.applib.layout.common;
+
+import java.util.List;
+
+public interface CollectionLayoutDataOwner extends MemberRegionOwner {
+    List<CollectionLayoutData> getCollections();
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/common/FieldSetOwner.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/common/FieldSetOwner.java b/core/applib/src/main/java/org/apache/isis/applib/layout/common/FieldSetOwner.java
new file mode 100644
index 0000000..f3e5fd0
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/common/FieldSetOwner.java
@@ -0,0 +1,25 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.applib.layout.common;
+
+import java.util.List;
+
+public interface FieldSetOwner extends MemberRegionOwner {
+    List<FieldSet> getFieldSets();
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/common/Grid.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/common/Grid.java b/core/applib/src/main/java/org/apache/isis/applib/layout/common/Grid.java
index 92e5a1d..7380a3a 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/common/Grid.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/common/Grid.java
@@ -19,8 +19,10 @@
 package org.apache.isis.applib.layout.common;
 
 import java.util.LinkedHashMap;
+import java.util.List;
 
 import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.layout.bootstrap3.BS3Col;
 import org.apache.isis.applib.services.layout.GridService;
 
 /**
@@ -36,16 +38,37 @@ public interface Grid {
 
     void setNormalized(final boolean normalized);
 
-    @Programmatic
-    LinkedHashMap<String, PropertyLayoutData> getAllPropertiesById();
+    @Programmatic LinkedHashMap<String, PropertyLayoutData> getAllPropertiesById();
 
-    @Programmatic
-    LinkedHashMap<String, CollectionLayoutData> getAllCollectionsById();
+    @Programmatic LinkedHashMap<String, CollectionLayoutData> getAllCollectionsById();
 
-    @Programmatic
-    LinkedHashMap<String, ActionLayoutData> getAllActionsById();
+    @Programmatic LinkedHashMap<String, ActionLayoutData> getAllActionsById();
 
-    @Programmatic
-    LinkedHashMap<String, FieldSet> getAllFieldSetsByName();
+    interface Visitor {
+        void visit(final DomainObjectLayoutData domainObjectLayoutData);
+
+        void visit(final ActionLayoutData actionLayoutData);
+
+        void visit(final PropertyLayoutData propertyLayoutData);
+
+        void visit(final CollectionLayoutData collectionLayoutData);
+
+        void visit(final FieldSet fieldSet);
+    }
+
+    class VisitorAdapter implements Visitor {
+        @Override public void visit(final DomainObjectLayoutData domainObjectLayoutData) {
+        }
+        @Override public void visit(final ActionLayoutData actionLayoutData) {
+        }
+        @Override public void visit(final PropertyLayoutData propertyLayoutData) {
+        }
+        @Override public void visit(final CollectionLayoutData collectionLayoutData) {
+        }
+        @Override public void visit(final FieldSet fieldSet) {
+        }
+    }
+
+    void visit(final Grid.Visitor visitor);
 
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/common/GridAbstract.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/common/GridAbstract.java b/core/applib/src/main/java/org/apache/isis/applib/layout/common/GridAbstract.java
new file mode 100644
index 0000000..70cee11
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/common/GridAbstract.java
@@ -0,0 +1,164 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.applib.layout.common;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlTransient;
+
+import com.google.common.collect.Maps;
+
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.layout.bootstrap3.BS3Grid;
+import org.apache.isis.applib.layout.fixedcols.FCColumn;
+import org.apache.isis.applib.services.layout.GridService;
+
+/**
+ * All top-level page layout classes should implement this interface.
+ *
+ * <p>
+ *     It is used by the {@link GridService} as a common based type for any layouts read in from XML.
+ * </p>
+ */
+public abstract class GridAbstract implements Grid {
+
+    private boolean normalized;
+
+    @Programmatic
+    @XmlTransient
+    public boolean isNormalized() {
+        return normalized;
+    }
+
+    @Programmatic
+    public void setNormalized(final boolean normalized) {
+        this.normalized = normalized;
+    }
+
+
+    /**
+     * Convenience for subclasses.
+     */
+    protected void traverseActions(
+            final ActionLayoutDataOwner actionLayoutDataOwner,
+            final GridAbstract.Visitor visitor) {
+        final List<ActionLayoutData> actionLayoutDatas = actionLayoutDataOwner.getActions();
+        if(actionLayoutDatas == null) {
+            return;
+        }
+        for (final ActionLayoutData actionLayoutData : actionLayoutDatas) {
+            actionLayoutData.setOwner(actionLayoutDataOwner);
+            visitor.visit(actionLayoutData);
+        }
+    }
+
+
+    /**
+     * Convenience for subclasses.
+     */
+    protected void traverseFieldSets(final FieldSetOwner fieldSetOwner, final GridAbstract.Visitor visitor) {
+        final List<FieldSet> fieldSets = fieldSetOwner.getFieldSets();
+        for (FieldSet fieldSet : fieldSets) {
+            fieldSet.setOwner(fieldSetOwner);
+            visitor.visit(fieldSet);
+            traverseActions(fieldSet, visitor);
+            final List<PropertyLayoutData> properties = fieldSet.getProperties();
+            for (final PropertyLayoutData property : properties) {
+                property.setOwner(fieldSet);
+                visitor.visit(property);
+                traverseActions(property, visitor);
+            }
+        }
+    }
+
+
+    /**
+     * Convenience for subclasses.
+     */
+    protected void traverseCollections(
+            final CollectionLayoutDataOwner owner, final GridAbstract.Visitor visitor) {
+        final List<CollectionLayoutData> collections = owner.getCollections();
+        for (CollectionLayoutData collection : collections) {
+            collection.setOwner(owner);
+            visitor.visit(collection);
+            traverseActions(collection, visitor);
+        }
+    }
+
+
+    @Programmatic
+    @XmlTransient
+    public LinkedHashMap<String, PropertyLayoutData> getAllPropertiesById() {
+        final LinkedHashMap<String, PropertyLayoutData> propertiesById = Maps.newLinkedHashMap();
+        visit(new BS3Grid.VisitorAdapter() {
+            public void visit(final PropertyLayoutData propertyLayoutData) {
+                propertiesById.put(propertyLayoutData.getId(), propertyLayoutData);
+            }
+        });
+        return propertiesById;
+    }
+
+
+    @Programmatic
+    @XmlTransient
+    public LinkedHashMap<String, CollectionLayoutData> getAllCollectionsById() {
+        final LinkedHashMap<String, CollectionLayoutData> collectionsById = Maps.newLinkedHashMap();
+
+        visit(new BS3Grid.VisitorAdapter() {
+            @Override
+            public void visit(final CollectionLayoutData collectionLayoutData) {
+                collectionsById.put(collectionLayoutData.getId(), collectionLayoutData);
+            }
+        });
+        return collectionsById;
+    }
+
+
+    @Programmatic
+    @XmlTransient
+    public LinkedHashMap<String, ActionLayoutData> getAllActionsById() {
+        final LinkedHashMap<String, ActionLayoutData> actionsById = Maps.newLinkedHashMap();
+
+        visit(new BS3Grid.VisitorAdapter() {
+            @Override
+            public void visit(final ActionLayoutData actionLayoutData) {
+                actionsById.put(actionLayoutData.getId(), actionLayoutData);
+            }
+        });
+        return actionsById;
+    }
+
+
+    @Programmatic
+    @XmlTransient
+    public LinkedHashMap<String, FieldSet> getAllFieldSetsByName() {
+        final LinkedHashMap<String, FieldSet> fieldSetsByName = Maps.newLinkedHashMap();
+
+        visit(new BS3Grid.VisitorAdapter() {
+            @Override
+            public void visit(final FieldSet fieldSet) {
+                fieldSetsByName.put(fieldSet.getName(), fieldSet);
+            }
+        });
+        return fieldSetsByName;
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCColumn.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCColumn.java b/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCColumn.java
index 25436ea..4c29f26 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCColumn.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCColumn.java
@@ -31,10 +31,11 @@ import com.google.common.collect.Lists;
 import org.apache.isis.applib.annotation.MemberGroupLayout;
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.layout.common.CollectionLayoutData;
+import org.apache.isis.applib.layout.common.CollectionLayoutDataOwner;
 import org.apache.isis.applib.layout.common.FieldSet;
-import org.apache.isis.applib.layout.common.PropertyLayoutData;
-import org.apache.isis.applib.layout.common.MemberRegionOwner;
+import org.apache.isis.applib.layout.common.FieldSetOwner;
 import org.apache.isis.applib.layout.common.Owned;
+import org.apache.isis.applib.layout.common.PropertyLayoutData;
 
 /**
  * The column contains a mixture of {@link FieldSet}s (of {@link PropertyLayoutData properties}) and also
@@ -52,7 +53,7 @@ import org.apache.isis.applib.layout.common.Owned;
                 , "collections"
         }
 )
-public class FCColumn implements Serializable, MemberRegionOwner, Owned<FCColumnOwner> {
+public class FCColumn implements Serializable, FieldSetOwner, CollectionLayoutDataOwner, Owned<FCColumnOwner> {
 
     private static final long serialVersionUID = 1L;
 

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCGrid.java
----------------------------------------------------------------------
diff --git a/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCGrid.java b/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCGrid.java
index ed32e64..01626dd 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCGrid.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/layout/fixedcols/FCGrid.java
@@ -19,23 +19,20 @@
 package org.apache.isis.applib.layout.fixedcols;
 
 import java.io.Serializable;
-import java.util.LinkedHashMap;
 import java.util.List;
 
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlElementRef;
 import javax.xml.bind.annotation.XmlRootElement;
-import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
-import com.google.common.collect.Maps;
-
-import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.layout.common.ActionLayoutData;
 import org.apache.isis.applib.layout.common.ActionLayoutDataOwner;
 import org.apache.isis.applib.layout.common.CollectionLayoutData;
+import org.apache.isis.applib.layout.common.DomainObjectLayoutData;
 import org.apache.isis.applib.layout.common.FieldSet;
 import org.apache.isis.applib.layout.common.Grid;
+import org.apache.isis.applib.layout.common.GridAbstract;
 import org.apache.isis.applib.layout.common.PropertyLayoutData;
 import org.apache.isis.applib.services.dto.Dto;
 
@@ -55,7 +52,7 @@ import org.apache.isis.applib.services.dto.Dto;
                 , "right"
         }
 )
-public class FCGrid implements Grid, Dto, ActionLayoutDataOwner, Serializable, FCColumnOwner, FCTabGroupOwner {
+public class FCGrid extends GridAbstract implements Dto, ActionLayoutDataOwner, Serializable, FCColumnOwner, FCTabGroupOwner {
 
     private static final long serialVersionUID = 1L;
 
@@ -114,18 +111,15 @@ public class FCGrid implements Grid, Dto, ActionLayoutDataOwner, Serializable, F
     }
 
 
-    interface Visitor {
+
+    interface Visitor extends Grid.Visitor {
         void visit(final FCGrid fcPage);
         void visit(final FCTabGroup fcTabGroup);
         void visit(final FCTab fcTab);
         void visit(final FCColumn fcColumn);
-        void visit(final FieldSet fieldSet);
-        void visit(final PropertyLayoutData propertyLayoutData);
-        void visit(final CollectionLayoutData collectionLayoutData);
-        void visit(final ActionLayoutData actionLayoutData);
     }
 
-    public static class VisitorAdapter implements Visitor {
+    public static class VisitorAdapter extends Grid.VisitorAdapter implements Visitor {
         @Override
         public void visit(final FCGrid fcPage) { }
         @Override
@@ -135,8 +129,6 @@ public class FCGrid implements Grid, Dto, ActionLayoutDataOwner, Serializable, F
         @Override
         public void visit(final FCColumn fcColumn) { }
         @Override
-        public void visit(final FieldSet fieldSet) {}
-        @Override
         public void visit(final PropertyLayoutData propertyLayoutData) {}
         @Override
         public void visit(final CollectionLayoutData collectionLayoutData) {}
@@ -149,18 +141,19 @@ public class FCGrid implements Grid, Dto, ActionLayoutDataOwner, Serializable, F
      * Visits all elements of the graph.  The {@link Visitor} implementation
      * can assume that all "owner" references are populated.
      */
-    public void visit(final FCGrid.Visitor visitor) {
-        visitor.visit(this);
+    public void visit(final Grid.Visitor visitor) {
+        FCGrid.Visitor fcVisitor = asFcVisitor(visitor);
+        fcVisitor.visit(this);
         traverseActions(this, visitor);
         traverseColumn(getLeft(), this, visitor);
         final List<FCTabGroup> tabGroups = getTabGroups();
         for (final FCTabGroup fcTabGroup : tabGroups) {
             fcTabGroup.setOwner(this);
-            visitor.visit(fcTabGroup);
+            fcVisitor.visit(fcTabGroup);
             final List<FCTab> tabs = fcTabGroup.getTabs();
             for (final FCTab fcTab : tabs) {
                 fcTab.setOwner(fcTabGroup);
-                visitor.visit(fcTab);
+                fcVisitor.visit(fcTab);
                 traverseColumn(fcTab.getLeft(), fcTab, visitor);
                 traverseColumn(fcTab.getMiddle(), fcTab, visitor);
                 traverseColumn(fcTab.getRight(), fcTab, visitor);
@@ -170,135 +163,41 @@ public class FCGrid implements Grid, Dto, ActionLayoutDataOwner, Serializable, F
     }
 
     private void traverseColumn(
-            final FCColumn fcColumn, final FCColumnOwner fcColumnOwner, final Visitor visitor) {
+            final FCColumn fcColumn,
+            final FCColumnOwner fcColumnOwner,
+            final Grid.Visitor visitor) {
         if(fcColumn == null) {
             return;
         }
+        FCGrid.Visitor fcVisitor = asFcVisitor(visitor);
         fcColumn.setOwner(fcColumnOwner);
-        visitor.visit(fcColumn);
+        fcVisitor.visit(fcColumn);
         traverseFieldSets(fcColumn, visitor);
         traverseCollections(fcColumn, visitor);
     }
 
-    private void traverseFieldSets(final FCColumn fcColumn, final Visitor visitor) {
-        for (final FieldSet fieldSet : fcColumn.getFieldSets()) {
-            fieldSet.setOwner(fcColumn);
-            visitor.visit(fieldSet);
-            traverseActions(fieldSet, visitor);
-            final List<PropertyLayoutData> properties = fieldSet.getProperties();
-            for (final PropertyLayoutData propertyLayoutData : properties) {
-                propertyLayoutData.setOwner(fieldSet);
-                visitor.visit(propertyLayoutData);
-                traverseActions(propertyLayoutData, visitor);
+    private static Visitor asFcVisitor(final Grid.Visitor visitor) {
+        return visitor instanceof Visitor? (Visitor) visitor : new VisitorAdapter() {
+            @Override public void visit(final DomainObjectLayoutData domainObjectLayoutData) {
+                visitor.visit(domainObjectLayoutData);
             }
-        }
-    }
 
-    private void traverseCollections(final FCColumn fcColumn, final Visitor visitor) {
-        for (final CollectionLayoutData collectionLayoutData : fcColumn.getCollections()) {
-            collectionLayoutData.setOwner(fcColumn);
-            visitor.visit(collectionLayoutData);
-            traverseActions(collectionLayoutData, visitor);
-        }
-    }
-
-    private void traverseActions(final ActionLayoutDataOwner actionLayoutDataOwner, final Visitor visitor) {
-        final List<ActionLayoutData> actionLayoutDatas = actionLayoutDataOwner.getActions();
-        if(actionLayoutDatas == null) {
-            return;
-        }
-        for (final ActionLayoutData actionLayoutData : actionLayoutDatas) {
-            actionLayoutData.setOwner(actionLayoutDataOwner);
-            visitor.visit(actionLayoutData);
-        }
-    }
-
-
-    @Programmatic
-    @XmlTransient
-    public LinkedHashMap<String, PropertyLayoutData> getAllPropertiesById() {
-        final LinkedHashMap<String, PropertyLayoutData> propertiesById = Maps.newLinkedHashMap();
-        visit(new FCGrid.VisitorAdapter() {
-            public void visit(final PropertyLayoutData propertyLayoutData) {
-                propertiesById.put(propertyLayoutData.getId(), propertyLayoutData);
+            @Override public void visit(final ActionLayoutData actionLayoutData) {
+                visitor.visit(actionLayoutData);
             }
-        });
-        return propertiesById;
-    }
-
 
-    @Programmatic
-    @XmlTransient
-    public LinkedHashMap<String, CollectionLayoutData> getAllCollectionsById() {
-        final LinkedHashMap<String, CollectionLayoutData> collectionsById = Maps.newLinkedHashMap();
-
-        visit(new FCGrid.VisitorAdapter() {
-            @Override
-            public void visit(final CollectionLayoutData collectionLayoutData) {
-                collectionsById.put(collectionLayoutData.getId(), collectionLayoutData);
-            }
-        });
-        return collectionsById;
-    }
-
-
-    @Programmatic
-    @XmlTransient
-    public LinkedHashMap<String, ActionLayoutData> getAllActionsById() {
-        final LinkedHashMap<String, ActionLayoutData> actionsById = Maps.newLinkedHashMap();
-
-        visit(new FCGrid.VisitorAdapter() {
-            @Override
-            public void visit(final ActionLayoutData actionLayoutData) {
-                actionsById.put(actionLayoutData.getId(), actionLayoutData);
+            @Override public void visit(final PropertyLayoutData propertyLayoutData) {
+                visitor.visit(propertyLayoutData);
             }
-        });
-        return actionsById;
-    }
-
 
-    @Programmatic
-    @XmlTransient
-    public LinkedHashMap<String, FieldSet> getAllFieldSetsByName() {
-        final LinkedHashMap<String, FieldSet> fieldSetsByName = Maps.newLinkedHashMap();
-
-        visit(new FCGrid.VisitorAdapter() {
-            @Override
-            public void visit(final FieldSet fieldSet) {
-                fieldSetsByName.put(fieldSet.getName(), fieldSet);
+            @Override public void visit(final CollectionLayoutData collectionLayoutData) {
+                visitor.visit(collectionLayoutData);
             }
-        });
-        return fieldSetsByName;
-    }
 
-
-    @Programmatic
-    @XmlTransient
-    public LinkedHashMap<String, FCTab> getAllTabsByName() {
-        final LinkedHashMap<String, FCTab> tabsByName = Maps.newLinkedHashMap();
-
-        visit(new FCGrid.VisitorAdapter() {
-            @Override
-            public void visit(final FCTab fcTab) {
-                tabsByName.put(fcTab.getName(), fcTab);
+            @Override public void visit(final FieldSet fieldSet) {
+                visitor.visit(fieldSet);
             }
-        });
-        return tabsByName;
-    }
-
-
-
-    private boolean normalized;
-
-    @Programmatic
-    @XmlTransient
-    public boolean isNormalized() {
-        return normalized;
-    }
-
-    @Programmatic
-    public void setNormalized(final boolean normalized) {
-        this.normalized = normalized;
+        };
     }
 
 

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerService.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerService.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerService.java
index d2436db..844168b 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerService.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerService.java
@@ -19,7 +19,7 @@ package org.apache.isis.core.metamodel.services.grid;
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.layout.common.Grid;
 
-public interface GridNormalizerService {
+public interface GridNormalizerService<G extends Grid> {
 
     @Programmatic
     Class<? extends Grid> gridImplementation();
@@ -31,6 +31,8 @@ public interface GridNormalizerService {
     String schemaLocation();
 
     @Programmatic
-    void normalize(Grid grid, Class<?> domainClass);
+    void normalize(G grid, Class<?> domainClass);
+
+
 
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerServiceAbstract.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerServiceAbstract.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerServiceAbstract.java
new file mode 100644
index 0000000..1c0ab06
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerServiceAbstract.java
@@ -0,0 +1,335 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.isis.core.metamodel.services.grid;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.layout.common.ActionLayoutData;
+import org.apache.isis.applib.layout.common.ActionLayoutDataOwner;
+import org.apache.isis.applib.layout.common.CollectionLayoutData;
+import org.apache.isis.applib.layout.common.FieldSet;
+import org.apache.isis.applib.layout.common.Grid;
+import org.apache.isis.applib.layout.common.MemberRegionOwner;
+import org.apache.isis.applib.layout.common.PropertyLayoutData;
+import org.apache.isis.applib.layout.fixedcols.FCColumn;
+import org.apache.isis.applib.layout.fixedcols.FCColumnOwner;
+import org.apache.isis.applib.layout.fixedcols.FCGrid;
+import org.apache.isis.applib.layout.fixedcols.FCTab;
+import org.apache.isis.applib.services.i18n.TranslationService;
+import org.apache.isis.core.metamodel.facetapi.FacetUtil;
+import org.apache.isis.core.metamodel.facets.actions.layout.ActionPositionFacetForActionXml;
+import org.apache.isis.core.metamodel.facets.actions.layout.BookmarkPolicyFacetForActionXml;
+import org.apache.isis.core.metamodel.facets.actions.layout.CssClassFaFacetForActionXml;
+import org.apache.isis.core.metamodel.facets.actions.layout.CssClassFacetForActionXml;
+import org.apache.isis.core.metamodel.facets.actions.layout.DescribedAsFacetForActionXml;
+import org.apache.isis.core.metamodel.facets.actions.layout.HiddenFacetForActionLayoutXml;
+import org.apache.isis.core.metamodel.facets.actions.layout.NamedFacetForActionXml;
+import org.apache.isis.core.metamodel.facets.collections.layout.CssClassFacetForCollectionXml;
+import org.apache.isis.core.metamodel.facets.collections.layout.DefaultViewFacetForCollectionXml;
+import org.apache.isis.core.metamodel.facets.collections.layout.DescribedAsFacetForCollectionXml;
+import org.apache.isis.core.metamodel.facets.collections.layout.HiddenFacetForCollectionXml;
+import org.apache.isis.core.metamodel.facets.collections.layout.NamedFacetForCollectionXml;
+import org.apache.isis.core.metamodel.facets.collections.layout.PagedFacetForCollectionXml;
+import org.apache.isis.core.metamodel.facets.collections.layout.SortedByFacetForCollectionXml;
+import org.apache.isis.core.metamodel.facets.members.order.annotprop.MemberOrderFacetXml;
+import org.apache.isis.core.metamodel.facets.properties.propertylayout.CssClassFacetForPropertyXml;
+import org.apache.isis.core.metamodel.facets.properties.propertylayout.DescribedAsFacetForPropertyXml;
+import org.apache.isis.core.metamodel.facets.properties.propertylayout.HiddenFacetForPropertyXml;
+import org.apache.isis.core.metamodel.facets.properties.propertylayout.LabelAtFacetForPropertyXml;
+import org.apache.isis.core.metamodel.facets.properties.propertylayout.MultiLineFacetForPropertyXml;
+import org.apache.isis.core.metamodel.facets.properties.propertylayout.NamedFacetForPropertyXml;
+import org.apache.isis.core.metamodel.facets.properties.propertylayout.RenderedAdjustedFacetForPropertyXml;
+import org.apache.isis.core.metamodel.facets.properties.propertylayout.TypicalLengthFacetForPropertyXml;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.spec.SpecificationLoader;
+import org.apache.isis.core.metamodel.spec.feature.Contributed;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.isis.core.metamodel.spec.feature.ObjectMember;
+import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+
+public abstract class GridNormalizerServiceAbstract<G extends Grid> implements GridNormalizerService<G> {
+
+    private final Class<G> gridImplementation;
+    private final String tns;
+    private final String schemaLocation;
+
+    public GridNormalizerServiceAbstract(
+            final Class<G> gridImplementation,
+            final String tns,
+            final String schemaLocation) {
+        this.gridImplementation = gridImplementation;
+        this.tns = tns;
+        this.schemaLocation = schemaLocation;
+    }
+
+    @Programmatic
+    @Override
+    public Class<G> gridImplementation() {
+        return gridImplementation;
+    }
+
+    @Programmatic
+    @Override
+    public String tns() {
+        return tns;
+    }
+
+    @Programmatic
+    @Override
+    public String schemaLocation() {
+        return schemaLocation;
+    }
+
+    @Programmatic
+    @Override
+    public void normalize(final G grid, final Class<?> domainClass) {
+
+        final ObjectSpecification objectSpec = specificationLookup.loadSpecification(domainClass);
+
+        final Map<String, OneToOneAssociation> oneToOneAssociationById =
+                ObjectMember.Util.mapById(getOneToOneAssociations(objectSpec));
+        final Map<String, OneToManyAssociation> oneToManyAssociationById =
+                ObjectMember.Util.mapById(getOneToManyAssociations(objectSpec));
+        final Map<String, ObjectAction> objectActionById =
+                ObjectMember.Util.mapById(objectSpec.getObjectActions(Contributed.INCLUDED));
+
+        final boolean validation = validateAndDerive(grid, oneToOneAssociationById, oneToManyAssociationById, objectActionById);
+        if(validation) {
+            overwrite(grid, oneToOneAssociationById, oneToManyAssociationById, objectActionById);
+        }
+    }
+
+    /**
+     * Ensures that all object members (properties, collections and actions) are in the metadata.
+     */
+    protected abstract boolean validateAndDerive(
+            final Grid grid,
+            final Map<String, OneToOneAssociation> oneToOneAssociationById,
+            final Map<String, OneToManyAssociation> oneToManyAssociationById,
+            final Map<String, ObjectAction> objectActionById);
+
+
+
+
+    protected void overwrite(
+            final G fcGrid,
+            final Map<String, OneToOneAssociation> oneToOneAssociationById,
+            final Map<String, OneToManyAssociation> oneToManyAssociationById,
+            final Map<String, ObjectAction> objectActionById) {
+
+        final Map<String, int[]> propertySequenceByGroup = Maps.newHashMap();
+
+        fcGrid.visit(new FCGrid.VisitorAdapter() {
+            private int collectionSequence = 1;
+            private int actionDomainObjectSequence = 1;
+            private int actionPropertyGroupSequence = 1;
+            private int actionPropertySequence = 1;
+            private int actionCollectionSequence = 1;
+
+            @Override
+            public void visit(final ActionLayoutData actionLayoutData) {
+                final ActionLayoutDataOwner actionLayoutDataOwner = actionLayoutData.getOwner();
+                final ObjectAction objectAction = objectActionById.get(actionLayoutData.getId());
+                if(objectAction == null) {
+                    return;
+                }
+
+                final String memberOrderName;
+                final int memberOrderSequence;
+                if(actionLayoutDataOwner instanceof FieldSet) {
+                    final FieldSet fieldSet = (FieldSet) actionLayoutDataOwner;
+                    final List<PropertyLayoutData> properties = fieldSet.getProperties();
+                    final PropertyLayoutData propertyLayoutData = properties.get(0); // any will do
+                    memberOrderName = propertyLayoutData.getId();
+                    memberOrderSequence = actionPropertyGroupSequence++;
+                } else if(actionLayoutDataOwner instanceof PropertyLayoutData) {
+                    final PropertyLayoutData propertyLayoutData = (PropertyLayoutData) actionLayoutDataOwner;
+                    memberOrderName = propertyLayoutData.getId();
+                    memberOrderSequence = actionPropertySequence++;
+                } else if(actionLayoutDataOwner instanceof CollectionLayoutData) {
+                    final CollectionLayoutData collectionLayoutData = (CollectionLayoutData) actionLayoutDataOwner;
+                    memberOrderName = collectionLayoutData.getId();
+                    memberOrderSequence = actionCollectionSequence++;
+                } else {
+                    // DomainObject
+                    memberOrderName = null;
+                    memberOrderSequence = actionDomainObjectSequence++;
+                }
+                FacetUtil.addFacet(
+                        new MemberOrderFacetXml(memberOrderName, ""+memberOrderSequence, translationService, objectAction));
+
+
+                if(actionLayoutDataOwner instanceof FieldSet) {
+                    if(actionLayoutData.getPosition() == null ||
+                            actionLayoutData.getPosition() == org.apache.isis.applib.annotation.ActionLayout.Position.BELOW ||
+                            actionLayoutData.getPosition() == org.apache.isis.applib.annotation.ActionLayout.Position.RIGHT) {
+                        actionLayoutData.setPosition(org.apache.isis.applib.annotation.ActionLayout.Position.PANEL);
+                    }
+                } else if(actionLayoutDataOwner instanceof PropertyLayoutData) {
+                    if(actionLayoutData.getPosition() == null ||
+                            actionLayoutData.getPosition() == org.apache.isis.applib.annotation.ActionLayout.Position.PANEL_DROPDOWN ||
+                            actionLayoutData.getPosition() == org.apache.isis.applib.annotation.ActionLayout.Position.PANEL) {
+                        actionLayoutData.setPosition(org.apache.isis.applib.annotation.ActionLayout.Position.BELOW);
+                    }
+                } else {
+                    // doesn't do anything for DomainObject or Collection
+                    actionLayoutData.setPosition(null);
+                }
+
+                FacetUtil.addFacet(ActionPositionFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addFacet(BookmarkPolicyFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addFacet(CssClassFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addFacet(CssClassFaFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addFacet(DescribedAsFacetForActionXml.create(actionLayoutData, objectAction));
+                FacetUtil.addFacet(HiddenFacetForActionLayoutXml.create(actionLayoutData, objectAction));
+                FacetUtil.addFacet(NamedFacetForActionXml.create(actionLayoutData, objectAction));
+            }
+
+            @Override
+            public void visit(final PropertyLayoutData propertyLayoutData) {
+                final OneToOneAssociation oneToOneAssociation = oneToOneAssociationById.get(propertyLayoutData.getId());
+                if(oneToOneAssociation == null) {
+                    return;
+                }
+
+                FacetUtil.addFacet(CssClassFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addFacet(DescribedAsFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addFacet(HiddenFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addFacet(LabelAtFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addFacet(MultiLineFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addFacet(NamedFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addFacet(
+                        RenderedAdjustedFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+                FacetUtil.addFacet(TypicalLengthFacetForPropertyXml.create(propertyLayoutData, oneToOneAssociation));
+
+                // @MemberOrder#name based on owning property group, @MemberOrder#sequence monotonically increasing
+                final FieldSet fieldSet = propertyLayoutData.getOwner();
+                final String groupName = fieldSet.getName();
+                final String sequence = nextInSequenceFor(groupName, propertySequenceByGroup);
+                FacetUtil.addFacet(
+                        new MemberOrderFacetXml(groupName, sequence, translationService, oneToOneAssociation));
+            }
+
+            @Override
+            public void visit(final CollectionLayoutData collectionLayoutData) {
+                final OneToManyAssociation oneToManyAssociation = oneToManyAssociationById.get(collectionLayoutData.getId());
+                if(oneToManyAssociation == null) {
+                    return;
+                }
+
+                FacetUtil.addFacet(CssClassFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addFacet(
+                        DefaultViewFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addFacet(
+                        DescribedAsFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addFacet(HiddenFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addFacet(NamedFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addFacet(PagedFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+                FacetUtil.addFacet(SortedByFacetForCollectionXml.create(collectionLayoutData, oneToManyAssociation));
+
+                // @MemberOrder#name based on the collection's id (so that each has a single "member group")
+                final String groupName = collectionLayoutData.getId();
+                final String sequence = "" + collectionSequence++;
+                FacetUtil.addFacet(
+                        new MemberOrderFacetXml(groupName, sequence, translationService, oneToManyAssociation));
+
+                // if there is only a single column and no other contents, then copy the collection Id onto the tab'
+                final MemberRegionOwner memberRegionOwner = collectionLayoutData.getOwner();
+                if(memberRegionOwner instanceof FCColumn) {
+                    final FCColumn FCColumn = (FCColumn) memberRegionOwner;
+                    final FCColumnOwner holder = FCColumn.getOwner();
+                    if(holder instanceof FCTab) {
+                        final FCTab FCTab = (FCTab) holder;
+                        if(FCTab.getContents().size() == 1 && Strings.isNullOrEmpty(FCTab.getName()) ) {
+                            final String collectionName = oneToManyAssociation.getName();
+                            FCTab.setName(collectionName);
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+
+    protected static List<OneToOneAssociation> getOneToOneAssociations(final ObjectSpecification objectSpec) {
+        List associations = objectSpec.getAssociations(Contributed.INCLUDED, ObjectAssociation.Filters.PROPERTIES);
+        return associations;
+    }
+    protected  static List<OneToManyAssociation> getOneToManyAssociations(final ObjectSpecification objectSpec) {
+        List associations = objectSpec.getAssociations(Contributed.INCLUDED, ObjectAssociation.Filters.COLLECTIONS);
+        return associations;
+    }
+
+
+
+    protected static class Tuple<T> {
+        public final T first;
+        public final T second;
+        private Tuple(final T first, final T second) {
+            this.first = first;
+            this.second = second;
+        }
+        public static <T> Tuple<T> of(final T first, final T second) {
+            return new Tuple<>(first, second);
+        }
+    }
+
+
+    /**
+     * Returns a 2-element tuple of [first-second, second-first]
+     */
+    protected static <T> Tuple<List<T>> surplusAndMissing(final Collection<T> first, final Collection<T> second){
+        final List<T> firstNotSecond = Lists.newArrayList(first);
+        firstNotSecond.removeAll(second);
+        final List<T> secondNotFirst = Lists.newArrayList(second);
+        secondNotFirst.removeAll(first);
+        return Tuple.of(firstNotSecond, secondNotFirst);
+    }
+
+
+    protected static String nextInSequenceFor(
+            final String key, final Map<String, int[]> seqByKey) {
+        synchronized (seqByKey) {
+            int[] holder = seqByKey.get(key);
+            if(holder == null) {
+                holder = new int[]{0};
+                seqByKey.put(key, holder);
+            }
+            holder[0]++;
+            return ""+holder[0];
+        }
+    }
+
+
+    @Inject
+    SpecificationLoader specificationLookup;
+    @Inject
+    TranslationService translationService;
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerUtil.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerUtil.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerUtil.java
new file mode 100644
index 0000000..7f09ee9
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/GridNormalizerUtil.java
@@ -0,0 +1,38 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.isis.core.metamodel.services.grid;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.Lists;
+
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.spec.feature.Contributed;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+
+public class GridNormalizerUtil {
+
+    private GridNormalizerUtil(){}
+
+
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/fe46a781/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/bootstrap3/GridNormalizerServiceBS3.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/bootstrap3/GridNormalizerServiceBS3.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/bootstrap3/GridNormalizerServiceBS3.java
index 0b69210..a4f845a 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/bootstrap3/GridNormalizerServiceBS3.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/grid/bootstrap3/GridNormalizerServiceBS3.java
@@ -16,56 +16,230 @@
  */
 package org.apache.isis.core.metamodel.services.grid.bootstrap3;
 
-import javax.inject.Inject;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.NatureOfService;
-import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.layout.bootstrap3.BS3Col;
 import org.apache.isis.applib.layout.bootstrap3.BS3Grid;
+import org.apache.isis.applib.layout.bootstrap3.BS3Row;
+import org.apache.isis.applib.layout.bootstrap3.BS3Tab;
+import org.apache.isis.applib.layout.bootstrap3.BS3TabGroup;
+import org.apache.isis.applib.layout.common.ActionLayoutData;
+import org.apache.isis.applib.layout.common.CollectionLayoutData;
+import org.apache.isis.applib.layout.common.FieldSet;
 import org.apache.isis.applib.layout.common.Grid;
-import org.apache.isis.applib.services.i18n.TranslationService;
-import org.apache.isis.core.metamodel.services.grid.GridNormalizerService;
-import org.apache.isis.core.metamodel.spec.SpecificationLoader;
+import org.apache.isis.applib.layout.common.PropertyLayoutData;
+import org.apache.isis.applib.layout.fixedcols.FCColumn;
+import org.apache.isis.core.metamodel.facets.object.membergroups.MemberGroupLayoutFacet;
+import org.apache.isis.core.metamodel.services.grid.GridNormalizerServiceAbstract;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
+import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
 
 @DomainService(
         nature = NatureOfService.DOMAIN
 )
-public class GridNormalizerServiceBS3 implements GridNormalizerService {
+public class GridNormalizerServiceBS3 extends GridNormalizerServiceAbstract<BS3Grid> {
 
     public static final String TNS = "http://isis.apache.org/schema/applib/layout/bootstrap3";
     public static final String SCHEMA_LOCATION = "http://isis.apache.org/schema/applib/layout/bootstrap3/bootstrap3.xsd";
 
+    public GridNormalizerServiceBS3() {
+        super(BS3Grid.class, TNS, SCHEMA_LOCATION);
+    }
 
 
-    @Programmatic
     @Override
-    public Class<? extends Grid> gridImplementation() {
-        return BS3Grid.class;
-    }
+    protected boolean validateAndDerive(
+            final Grid grid,
+            final Map<String, OneToOneAssociation> oneToOneAssociationById,
+            final Map<String, OneToManyAssociation> oneToManyAssociationById,
+            final Map<String, ObjectAction> objectActionById) {
 
-    @Programmatic
-    @Override
-    public String tns() {
-        return TNS;
-    }
+        final BS3Grid bs3Grid = (BS3Grid) grid;
 
-    @Programmatic
-    @Override
-    public String schemaLocation() {
-        return SCHEMA_LOCATION;
-    }
+        final LinkedHashMap<String, PropertyLayoutData> propertyIds = bs3Grid.getAllPropertiesById();
+        final LinkedHashMap<String, CollectionLayoutData> collectionIds = bs3Grid.getAllCollectionsById();
+        final LinkedHashMap<String, ActionLayoutData> actionIds = bs3Grid.getAllActionsById();
 
-    @Override
-    public void normalize(final Grid grid, final Class<?> domainClass) {
-        BS3Grid bs3Grid = (BS3Grid) grid;
 
-        // TODO
-    }
+        // find all row and col ids
+        // ensure that all Ids are different
+
+        final LinkedHashMap<String, BS3Row> rowIds = Maps.newLinkedHashMap();
+        final LinkedHashMap<String, BS3Col> colIds = Maps.newLinkedHashMap();
+
+        final AtomicReference<Boolean> duplicateIdDetected = new AtomicReference<>(false);
+
+        bs3Grid.visit(new BS3Grid.VisitorAdapter(){
+            @Override
+            public void visit(final BS3Row bs3Row) {
+                final String id = bs3Row.getId();
+                if(id == null) {
+                    return;
+                }
+                if(rowIds.containsKey(id) || colIds.containsKey(id)) {
+                    bs3Row.setMetadataError("There is another col with this id");
+                    duplicateIdDetected.set(true);
+                    return;
+                }
+                rowIds.put(id, bs3Row);
+            }
+
+            @Override
+            public void visit(final BS3Col bs3Col) {
+                final String id = bs3Col.getId();
+                if(id == null) {
+                    return;
+                }
+                if(rowIds.containsKey(id) || colIds.containsKey(id)) {
+                    bs3Col.setMetadataError("There is another col with this id");
+                    duplicateIdDetected.set(true);
+                    return;
+                }
+                colIds.put(id, bs3Col);
+            }
+        });
+
+        if(duplicateIdDetected.get()) {
+            return false;
+        }
+
 
+        // ensure that there is exactly one col with the
+        // unreferencedActions, unreferencedProperties and unreferencedCollections attribute set.
 
-    @Inject
-    SpecificationLoader specificationLookup;
-    @Inject
-    TranslationService translationService;
+        final AtomicReference<BS3Col> colForUnreferencedActionsRef = new AtomicReference<>();
+        final AtomicReference<BS3Col> colForUnreferencedPropertiesRef = new AtomicReference<>();
+        final AtomicReference<FieldSet> fieldSetForUnreferencedPropsRef = new AtomicReference<>();
+        final AtomicReference<BS3Col> colForUnreferencedCollectionsRef = new AtomicReference<>();
+
+        bs3Grid.visit(new BS3Grid.VisitorAdapter(){
+            @Override
+            public void visit(final BS3Col bs3Col) {
+                if(bs3Col.isUnreferencedActions()) {
+                    if(colForUnreferencedActionsRef.get() != null) {
+                        bs3Col.setMetadataError("More than one col with 'unreferencedActions' attribute set");
+                    } else {
+                        colForUnreferencedActionsRef.set(bs3Col);
+                    }
+                }
+                if(bs3Col.isUnreferencedProperties()) {
+                    if(colForUnreferencedPropertiesRef.get() != null) {
+                        bs3Col.setMetadataError("More than one col with 'unreferencedProperties' attribute set");
+                    } else {
+                        final List<FieldSet> fieldSets = bs3Col.getFieldSets();
+                        for (FieldSet fieldSet : fieldSets) {
+                            if(fieldSet.getName().equals(MemberGroupLayoutFacet.DEFAULT_GROUP)) {
+                                fieldSetForUnreferencedPropsRef.set(fieldSet);
+                            }
+                        }
+                        colForUnreferencedPropertiesRef.set(bs3Col);
+                    }
+                }
+                if(bs3Col.isUnreferencedCollections()) {
+                    if(colForUnreferencedCollectionsRef.get() != null) {
+                        bs3Col.setMetadataError("More than one col with 'unreferencedCollections' attribute set");
+                    } else {
+                        colForUnreferencedCollectionsRef.set(bs3Col);
+                    }
+                }
+            }
+        });
+
+        if(     colForUnreferencedActionsRef.get() == null ||
+                colForUnreferencedPropertiesRef.get() == null ||
+                colForUnreferencedCollectionsRef.get() == null) {
+            return false;
+        }
+
+
+        // add missing properties will be added to the first fieldset of the specified column
+        final Tuple<List<String>> propertyIdTuple =
+                surplusAndMissing(propertyIds.keySet(), oneToOneAssociationById.keySet());
+        final List<String> surplusPropertyIds = propertyIdTuple.first;
+        final List<String> missingPropertyIds = propertyIdTuple.second;
+
+        for (String surplusPropertyId : surplusPropertyIds) {
+            propertyIds.get(surplusPropertyId).setMetadataError("No such property");
+        }
+
+        if(!missingPropertyIds.isEmpty()) {
+            final BS3Col bs3Col = colForUnreferencedPropertiesRef.get();
+            if(bs3Col != null) {
+                // ensure that there is a field set to use, else create
+                boolean wasSet = fieldSetForUnreferencedPropsRef.compareAndSet(null, new FieldSet(MemberGroupLayoutFacet.DEFAULT_GROUP));
+                final FieldSet fieldSetForUnref = fieldSetForUnreferencedPropsRef.get();
+                if(wasSet) {
+                    fieldSetForUnref.setOwner(bs3Col);
+                    bs3Col.getFieldSets().add(fieldSetForUnref);
+                }
+                for (final String propertyId : missingPropertyIds) {
+                    bs3Col.getFieldSets().get(0).getProperties().add(new PropertyLayoutData(propertyId));
+                }
+            }
+        }
+
+        // any missing collections will be added as tabs to a new TabGroup in the specified column
+        final Tuple<List<String>> collectionIdTuple =
+                surplusAndMissing(collectionIds.keySet(), oneToManyAssociationById.keySet());
+        final List<String> surplusCollectionIds = collectionIdTuple.first;
+        final List<String> missingCollectionIds = collectionIdTuple.second;
+
+        for (String surplusCollectionId : surplusCollectionIds) {
+            collectionIds.get(surplusCollectionId).setMetadataError("No such collection");
+        }
+
+        if(!missingCollectionIds.isEmpty()) {
+            final BS3Col bs3Col = colForUnreferencedCollectionsRef.get();
+            if(bs3Col != null) {
+                final BS3TabGroup tabGroup = new BS3TabGroup();
+                tabGroup.setOwner(bs3Col);
+                bs3Col.getTabGroups().add(tabGroup);
+                for (final String collectionId : missingCollectionIds) {
+                    final BS3Tab bs3Tab = new BS3Tab();
+                    tabGroup.getTabs().add(bs3Tab);
+                    FCColumn left = new FCColumn(12);
+                    bs3Tab.setOwner(tabGroup);
+                    final CollectionLayoutData layoutMetadata = new CollectionLayoutData(collectionId);
+                    layoutMetadata.setDefaultView("table");
+                    left.getCollections().add(layoutMetadata);
+                }
+            }
+        }
+
+        // any missing actions will be added as actions in the specified column
+        final Tuple<List<String>> actionIdTuple =
+                surplusAndMissing(actionIds.keySet(), objectActionById.keySet());
+        final List<String> surplusActionIds = actionIdTuple.first;
+        final List<String> missingActionIds = actionIdTuple.second;
+
+        for (String surplusActionId : surplusActionIds) {
+            actionIds.get(surplusActionId).setMetadataError("No such action");
+        }
+
+        if(!missingActionIds.isEmpty()) {
+            final BS3Col bs3Col = colForUnreferencedActionsRef.get();
+            if(bs3Col != null) {
+                for (String actionId : missingActionIds) {
+                    List<ActionLayoutData> actions = bs3Col.getActions();
+                    if(actions == null) {
+                        actions = Lists.newArrayList();
+                        bs3Col.setActions(actions);
+                    }
+                    actions.add(new ActionLayoutData(actionId));
+                }
+            }
+        }
+
+        return true;
+    }
 
 }