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:41 UTC

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

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;
+    }
 
 }