http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/ObjectAdapterMemento.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/ObjectAdapterMemento.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/ObjectAdapterMemento.java
new file mode 100644
index 0000000..107bde0
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/ObjectAdapterMemento.java
@@ -0,0 +1,458 @@
+/*
+ *  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.viewer.wicket.model.mementos;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.google.common.base.Function;
+
+import org.apache.isis.core.commons.ensure.Ensure;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
+import 
org.apache.isis.core.metamodel.adapter.mgr.AdapterManager.ConcurrencyChecking;
+import org.apache.isis.core.metamodel.adapter.oid.Oid;
+import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller;
+import org.apache.isis.core.metamodel.adapter.oid.RootOid;
+import org.apache.isis.core.metamodel.adapter.oid.TypedOid;
+import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet;
+import org.apache.isis.core.metamodel.spec.ObjectSpecId;
+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.ObjectActionParameter;
+import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.isis.core.runtime.memento.Memento;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+import org.apache.isis.core.runtime.system.persistence.PersistenceSession;
+
+public class ObjectAdapterMemento implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Factory method
+     */
+    public static ObjectAdapterMemento createOrNull(final ObjectAdapter 
adapter) {
+        if (adapter == null) {
+            return null;
+        }
+        return new ObjectAdapterMemento(adapter);
+    }
+
+    /**
+     * Factory method
+     */
+    public static ObjectAdapterMemento createPersistent(final RootOid rootOid) 
{
+        return new ObjectAdapterMemento(rootOid);
+    }
+
+
+
+    enum Type {
+        /**
+         * The {@link ObjectAdapter} that this is the memento for directly has
+         * an {@link EncodableFacet} (it is almost certainly a value), and so 
is
+         * stored directly.
+         */
+        ENCODEABLE {
+            @Override
+            ObjectAdapter recreateAdapter(final ObjectAdapterMemento oam, 
ConcurrencyChecking concurrencyChecking) {
+                ObjectSpecId objectSpecId = oam.objectSpecId;
+                ObjectSpecification objectSpec = 
SpecUtils.getSpecificationFor(objectSpecId);
+                final EncodableFacet encodableFacet = 
objectSpec.getFacet(EncodableFacet.class);
+                return encodableFacet.fromEncodedString(oam.encodableValue);
+            }
+
+            @Override
+            public boolean equals(ObjectAdapterMemento oam, 
ObjectAdapterMemento other) {
+                return other.type == ENCODEABLE && 
oam.encodableValue.equals(other.encodableValue);
+            }
+
+            @Override
+            public int hashCode(ObjectAdapterMemento oam) {
+                return oam.encodableValue.hashCode();
+            }
+
+            @Override
+            public String toString(final ObjectAdapterMemento oam) {
+                return oam.encodableValue;
+            }
+
+            @Override
+            public void resetVersion(ObjectAdapterMemento 
objectAdapterMemento) {
+            }
+        },
+        /**
+         * The {@link ObjectAdapter} that this is for is already known by its
+         * (persistent) {@link Oid}.
+         */
+        PERSISTENT {
+            @Override
+            ObjectAdapter recreateAdapter(final ObjectAdapterMemento oam, 
ConcurrencyChecking concurrencyChecking) {
+                TypedOid oid = 
getOidMarshaller().unmarshal(oam.persistentOidStr, TypedOid.class);
+                try {
+                    final ObjectAdapter adapter = 
getAdapterManager().adapterFor(oid, concurrencyChecking);
+                    return adapter;
+                    
+                } finally {
+                    // a side-effect of AdapterManager#adapterFor(...) is that 
it will update the oid
+                    // with the correct version, even when there is a 
concurrency exception
+                    // we copy this updated oid string into our memento so 
that, if we retry, 
+                    // we will succeed second time around
+                    
+                    oam.persistentOidStr = oid.enString(getOidMarshaller());
+                }
+            }
+
+            @Override
+            public void resetVersion(ObjectAdapterMemento oam) {
+                // REVIEW: this may be redundant because recreateAdapter also 
guarantees the version will be reset.
+                final ObjectAdapter adapter = recreateAdapter(oam, 
ConcurrencyChecking.NO_CHECK);
+                Oid oid = adapter.getOid();
+                oam.persistentOidStr = oid.enString(getOidMarshaller());
+            }
+
+            @Override
+            public boolean equals(ObjectAdapterMemento oam, 
ObjectAdapterMemento other) {
+                return other.type == PERSISTENT && 
oam.persistentOidStr.equals(other.persistentOidStr);
+            }
+
+            @Override
+            public int hashCode(ObjectAdapterMemento oam) {
+                return oam.persistentOidStr.hashCode();
+            }
+
+            @Override
+            public String toString(final ObjectAdapterMemento oam) {
+                return oam.persistentOidStr;
+            }
+
+        },
+        /**
+         * Uses Isis' own {@link Memento}, to capture the state of a transient
+         * object.
+         */
+        TRANSIENT {
+            /**
+             * {@link ConcurrencyChecking} is ignored for transients.
+             */
+            @Override
+            ObjectAdapter recreateAdapter(final ObjectAdapterMemento oam, 
ConcurrencyChecking concurrencyChecking) {
+                return oam.transientMemento.recreateObject();
+            }
+
+            @Override
+            public boolean equals(ObjectAdapterMemento oam, 
ObjectAdapterMemento other) {
+                return other.type == TRANSIENT && 
oam.transientMemento.equals(other.transientMemento);
+            }
+
+            @Override
+            public int hashCode(ObjectAdapterMemento oam) {
+                return oam.transientMemento.hashCode();
+            }
+            
+            @Override
+            public String toString(final ObjectAdapterMemento oam) {
+                return oam.transientMemento.toString();
+            }
+
+            @Override
+            public void resetVersion(ObjectAdapterMemento 
objectAdapterMemento) {
+            }
+        };
+
+        public synchronized ObjectAdapter getAdapter(final 
ObjectAdapterMemento nom, ConcurrencyChecking concurrencyChecking) {
+            return recreateAdapter(nom, concurrencyChecking);
+        }
+
+        abstract ObjectAdapter recreateAdapter(ObjectAdapterMemento nom, 
ConcurrencyChecking concurrencyChecking);
+
+        public abstract boolean equals(ObjectAdapterMemento oam, 
ObjectAdapterMemento other);
+        public abstract int hashCode(ObjectAdapterMemento 
objectAdapterMemento);
+        
+        public abstract String toString(ObjectAdapterMemento adapterMemento);
+
+        public abstract void resetVersion(ObjectAdapterMemento 
objectAdapterMemento);
+    }
+
+    private Type type;
+
+    private final ObjectSpecId objectSpecId;
+    private String titleHint;
+
+    /**
+     * The current value, if {@link Type#ENCODEABLE}.
+     * 
+     * <p>
+     * Will be <tt>null</tt> otherwise.
+     */
+    private String encodableValue;
+    
+    /**
+     * The current value, if {@link Type#PERSISTENT}.
+     * 
+     * <p>
+     * Will be <tt>null</tt> otherwise.
+     */
+    private String persistentOidStr;
+
+    /**
+     * The current value, if {@link Type#TRANSIENT}.
+     * 
+     * <p>
+     * Will be <tt>null</tt> otherwise.
+     */
+    private Memento transientMemento;
+
+    private ObjectAdapterMemento(final RootOid rootOid) {
+        Ensure.ensureThatArg(rootOid, Oid.Matchers.isPersistent());
+        this.persistentOidStr = rootOid.enString(getOidMarshaller());
+        this.objectSpecId = rootOid.getObjectSpecId();
+        this.type = Type.PERSISTENT;
+    }
+
+    private ObjectAdapterMemento(final ObjectAdapter adapter) {
+        if (adapter == null) {
+            throw new IllegalArgumentException("adapter cannot be null");
+        }
+        final ObjectSpecification specification = adapter.getSpecification();
+        objectSpecId = specification.getSpecId();
+        init(adapter);
+    }
+
+    private void init(final ObjectAdapter adapter) {
+        
+        final ObjectSpecification specification = adapter.getSpecification();
+
+        final EncodableFacet encodableFacet = 
specification.getFacet(EncodableFacet.class);
+        final boolean isEncodable = encodableFacet != null;
+        if (isEncodable) {
+            encodableValue = encodableFacet.toEncodedString(adapter);
+            type = Type.ENCODEABLE;
+            return;
+        }
+        
+        final RootOid oid = (RootOid) adapter.getOid();
+        if (oid.isTransient()) {
+            transientMemento = new Memento(adapter);
+            type = Type.TRANSIENT;
+            return;
+        } 
+        
+        persistentOidStr = oid.enString(getOidMarshaller());
+        type = Type.PERSISTENT;
+    }
+
+    public void resetVersion() {
+        type.resetVersion(this);
+    }
+    
+
+    /**
+     * Lazily looks up {@link ObjectAdapter} if required.
+     * 
+     * <p>
+     * For transient objects, be aware that calling this method more than once
+     * will cause the underlying {@link ObjectAdapter} to be recreated,
+     * overwriting any changes that may have been made. In general then it's
+     * best to call once and then hold onto the value thereafter. 
Alternatively,
+     * can call {@link #setAdapter(ObjectAdapter)} to keep this memento in 
sync.
+     */
+    public ObjectAdapter getObjectAdapter(ConcurrencyChecking 
concurrencyChecking) {
+        return type.getAdapter(this, concurrencyChecking);
+    }
+
+    /**
+     * Updates the memento if the adapter's state has changed.
+     * 
+     * <p>
+     * This is a no-op for
+     * 
+     * @param adapter
+     */
+    public void setAdapter(final ObjectAdapter adapter) {
+        init(adapter);
+    }
+
+    public ObjectSpecId getObjectSpecId() {
+        return objectSpecId;
+    }
+
+    /**
+     * Analogous to {@link List#contains(Object)}, but does not perform
+     * {@link ConcurrencyChecking concurrency checking} of the OID.
+     */
+    public boolean containedIn(List<ObjectAdapterMemento> list) {
+        // REVIEW: heavy handed, ought to be possible to just compare the OIDs
+        // ignoring the concurrency checking
+        final ObjectAdapter currAdapter = 
getObjectAdapter(ConcurrencyChecking.NO_CHECK);
+        for (ObjectAdapterMemento each : list) {
+            if(each == null) {
+                continue;
+            }
+            final ObjectAdapter otherAdapter = 
each.getObjectAdapter(ConcurrencyChecking.NO_CHECK);
+            if(currAdapter == otherAdapter) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return type.hashCode(this);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return (obj instanceof ObjectAdapterMemento) && type.equals(this, 
(ObjectAdapterMemento)obj);
+    }
+
+
+    @Override
+    public String toString() {
+        return asString();
+    }
+
+    public String asString() {
+        return type.toString(this);
+    }
+
+
+    //////////////////////////////////////////////////
+    // Functions
+    //////////////////////////////////////////////////
+    
+    
+    public final static class Functions {
+
+        private Functions() {
+        }
+
+        public static Function<ObjectSpecification, ObjectSpecId> fromSpec() {
+            return new Function<ObjectSpecification, ObjectSpecId>() {
+
+                @Override
+                public ObjectSpecId apply(final ObjectSpecification from) {
+                    return from.getSpecId();
+                }
+            };
+        }
+
+        public static Function<OneToOneAssociation, PropertyMemento> 
fromProperty() {
+            return new Function<OneToOneAssociation, PropertyMemento>() {
+                @Override
+                public PropertyMemento apply(final OneToOneAssociation from) {
+                    return new PropertyMemento(from);
+                }
+            };
+        }
+
+        public static Function<OneToManyAssociation, CollectionMemento> 
fromCollection() {
+            return new Function<OneToManyAssociation, CollectionMemento>() {
+                @Override
+                public CollectionMemento apply(final OneToManyAssociation 
from) {
+                    return new CollectionMemento(from);
+                }
+            };
+        }
+
+        public static Function<ObjectAction, ActionMemento> fromAction() {
+            return new Function<ObjectAction, ActionMemento>() {
+                @Override
+                public ActionMemento apply(final ObjectAction from) {
+                    return new ActionMemento(from);
+                }
+            };
+        }
+
+        public static Function<ObjectActionParameter, ActionParameterMemento> 
fromActionParameter() {
+            return new Function<ObjectActionParameter, 
ActionParameterMemento>() {
+                @Override
+                public ActionParameterMemento apply(final 
ObjectActionParameter from) {
+                    return new ActionParameterMemento(from);
+                }
+            };
+        }
+
+        public static Function<Object, ObjectAdapterMemento> fromPojo(final 
AdapterManager adapterManager) {
+            return new Function<Object, ObjectAdapterMemento>() {
+                @Override
+                public ObjectAdapterMemento apply(final Object pojo) {
+                    final ObjectAdapter adapter = 
adapterManager.adapterFor(pojo);
+                    return ObjectAdapterMemento.createOrNull(adapter);
+                }
+            };
+        }
+
+        public static Function<ObjectAdapter, ObjectAdapterMemento> 
fromAdapter() {
+            return new Function<ObjectAdapter, ObjectAdapterMemento>() {
+                @Override
+                public ObjectAdapterMemento apply(final ObjectAdapter adapter) 
{
+                    return ObjectAdapterMemento.createOrNull(adapter);
+                }
+            };
+        }
+
+
+        public static Function<ObjectAdapterMemento, ObjectAdapter> 
fromMemento(final ConcurrencyChecking concurrencyChecking) {
+            return new Function<ObjectAdapterMemento, ObjectAdapter>() {
+                @Override
+                public ObjectAdapter apply(final ObjectAdapterMemento from) {
+                    return from.getObjectAdapter(concurrencyChecking);
+                }
+            };
+        }
+
+        public static Function<ObjectAdapter, ObjectAdapterMemento> 
toMemento() {
+            return new Function<ObjectAdapter, ObjectAdapterMemento>() {
+
+                @Override
+                public ObjectAdapterMemento apply(ObjectAdapter from) {
+                    return ObjectAdapterMemento.createOrNull(from);
+                }
+                
+            };
+        }
+
+    }
+
+    
+    //////////////////////////////////////////////////
+    // Dependencies (from context)
+    //////////////////////////////////////////////////
+    
+    private static AdapterManager getAdapterManager() {
+        return getPersistenceSession().getAdapterManager();
+    }
+
+    private static PersistenceSession getPersistenceSession() {
+        return IsisContext.getPersistenceSession();
+    }
+
+       public static OidMarshaller getOidMarshaller() {
+               return IsisContext.getOidMarshaller();
+       }
+
+
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PageParameterNames.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PageParameterNames.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PageParameterNames.java
new file mode 100644
index 0000000..75f1a64
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PageParameterNames.java
@@ -0,0 +1,142 @@
+/*
+ *  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.viewer.wicket.model.mementos;
+
+import java.util.List;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.util.string.StringValue;
+
+import org.apache.isis.core.commons.lang.StringExtensions;
+import org.apache.isis.core.metamodel.adapter.oid.Oid;
+
+public enum PageParameterNames {
+
+    /**
+     * The object's {@link Oid}.
+     * 
+     * <p>
+     * Also encodes the object's spec, and whether the object is persistent or 
not.
+     */
+    OBJECT_OID,
+    
+    /**
+     * Hints for the rendering of an entity.
+     */
+    ANCHOR,
+    
+    /**
+     * Owning type of an action.
+     * 
+     * <p>
+     * Whereas {@link #OBJECT_SPEC} is the concrete runtime type of the 
adapter,
+     * the owning type could be some superclass if the action has been
+     * inherited.
+     */
+    ACTION_OWNING_SPEC, 
+    
+    /**
+     * Whether user, exploration, prototype etc.
+     */
+    ACTION_TYPE, 
+    
+    /**
+     * The name of the action, along with its parameters.
+     */
+    ACTION_ID, 
+    
+    /**
+     * When a single object is returned, whether to redirect to it or simply 
inline it.
+     */
+    ACTION_SINGLE_RESULTS_MODE,
+    
+    /**
+     * The argument acting as a context for a contributed action, if any.
+     * 
+     * <p>
+     * In the format N=OBJECT_OID, where N is the 0-based action parameter
+     * index.
+     */
+    ACTION_PARAM_CONTEXT,
+
+    /**
+     * Action argument(s), if known.
+     */
+    ACTION_ARGS;
+
+    /**
+     * Returns the {@link #name()} formatted as
+     * {@link Strings#camelCase(String) camel case}.
+     * 
+     * <p>
+     * For example, <tt>ACTION_TYPE</tt> becomes <tt>actionType</tt>.
+     */
+    @Override
+    public String toString() {
+        return StringExtensions.toCamelCase(name());
+    }
+
+    public String getStringFrom(final PageParameters pageParameters) {
+        return getStringFrom(pageParameters, null);
+    }
+
+    public String getStringFrom(PageParameters pageParameters, String 
defaultValue) {
+        if(pageParameters == null) {
+            return defaultValue;
+        }
+        return pageParameters.get(this.toString()).toString(defaultValue);
+    }
+
+    public <T extends Enum<T>> T getEnumFrom(PageParameters pageParameters, 
Class<T> enumClass) {
+        String value = getStringFrom(pageParameters);
+        return value != null? Enum.valueOf(enumClass, value): null;
+    }
+
+    public List<String> getListFrom(PageParameters pageParameters) {
+        return Lists.transform(pageParameters.getValues(this.toString()), new 
Function<StringValue, String>() {
+            @Override
+            public String apply(StringValue input) {
+                return input.toString();
+            }
+        });
+    }
+
+
+    public void addStringTo(final PageParameters pageParameters, final String 
value) {
+        pageParameters.add(this.toString(), value);
+    }
+
+    public void addEnumTo(final PageParameters pageParameters, final Enum<?> 
someEnum) {
+        addStringTo(pageParameters, someEnum.name());
+    }
+
+    /**
+     * @param pageParameters
+     */
+    public void removeFrom(PageParameters pageParameters) {
+        pageParameters.remove(this.toString());
+    }
+
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PropertyMemento.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PropertyMemento.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PropertyMemento.java
new file mode 100644
index 0000000..6504be6
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PropertyMemento.java
@@ -0,0 +1,123 @@
+/*
+ *  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.viewer.wicket.model.mementos;
+
+import java.io.Serializable;
+
+import org.apache.isis.core.metamodel.spec.ObjectSpecId;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+import org.apache.isis.viewer.wicket.model.models.EntityModel;
+
+public class PropertyMemento implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private static ObjectSpecification owningSpecFor(final OneToOneAssociation 
association) {
+        return 
IsisContext.getSpecificationLoader().loadSpecification(association.getIdentifier().toClassIdentityString());
+    }
+
+    private final ObjectSpecId owningSpecId;
+    private final String identifier;
+    private final ObjectSpecId specId;
+
+//    private transient OneToOneAssociation property;
+
+    public PropertyMemento(final ObjectSpecId owningType, final String 
identifier) {
+        this(owningType, identifier, null);
+    }
+
+    public PropertyMemento(final ObjectSpecId owningType, final String 
identifier, final ObjectSpecId type) {
+        this(owningType, identifier, type, propertyFor(owningType, 
identifier));
+    }
+
+    public PropertyMemento(final OneToOneAssociation property) {
+        this(owningSpecFor(property).getSpecId(), 
property.getIdentifier().toNameIdentityString(), 
property.getSpecification().getSpecId(), property);
+    }
+    
+    private PropertyMemento(final ObjectSpecId owningSpecId, final String 
name, final ObjectSpecId specId, final OneToOneAssociation property) {
+        this.owningSpecId = owningSpecId;
+        this.identifier = name;
+        this.specId = specId;
+//        this.property = property;
+    }
+
+    public ObjectSpecId getOwningType() {
+        return owningSpecId;
+    }
+
+    public ObjectSpecId getType() {
+        return specId;
+    }
+
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public OneToOneAssociation getProperty() {
+//        if (property == null) {
+//            property = propertyFor(owningSpecId, identifier);
+//        }
+//        return property;
+        return propertyFor(owningSpecId, identifier);
+    }
+
+    private static OneToOneAssociation propertyFor(ObjectSpecId owningType, 
String identifier) {
+        return (OneToOneAssociation) 
SpecUtils.getSpecificationFor(owningType).getAssociation(identifier);
+    }
+
+    /**
+     * Value semantics so can use as a key in {@link EntityModel} hash.
+     */
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((identifier == null) ? 0 : 
identifier.hashCode());
+        return result;
+    }
+
+    /**
+     * Value semantics so can use as a key in {@link EntityModel} hash.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final PropertyMemento other = (PropertyMemento) obj;
+        if (identifier == null) {
+            if (other.identifier != null) {
+                return false;
+            }
+        } else if (!identifier.equals(other.identifier)) {
+            return false;
+        }
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/SpecUtils.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/SpecUtils.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/SpecUtils.java
new file mode 100644
index 0000000..8e34386
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/SpecUtils.java
@@ -0,0 +1,44 @@
+/*
+ *  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.viewer.wicket.model.mementos;
+
+import org.apache.isis.core.metamodel.spec.ObjectSpecId;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.spec.SpecificationLoaderSpi;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+
+public final class SpecUtils {
+    
+    private SpecUtils(){}
+
+    public static ObjectSpecification getSpecificationFor(ObjectSpecId 
objectSpecId) {
+        ObjectSpecification objectSpec = 
getSpecificationLoader().lookupBySpecId(objectSpecId);
+        if(objectSpec != null) {
+            return objectSpec;
+        } 
+        
+        // attempt to load directly.
+        return 
getSpecificationLoader().loadSpecification(objectSpecId.asString());
+    }
+
+    protected static SpecificationLoaderSpi getSpecificationLoader() {
+        return IsisContext.getSpecificationLoader();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/AboutModel.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/AboutModel.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/AboutModel.java
new file mode 100644
index 0000000..7ca84ab
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/AboutModel.java
@@ -0,0 +1,47 @@
+/*
+ *  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.viewer.wicket.model.models;
+
+/**
+ * Model providing welcome text.
+ */
+public class AboutModel extends ModelAbstract<String> {
+
+    private static final long serialVersionUID = 1L;
+    
+    public AboutModel(String message) {
+        super();
+        setObject(message);
+    }
+
+    @Override
+    protected String load() {
+        return getObject();
+    }
+
+    @Override
+    public void setObject(final String message) {
+        if(message == null) {
+            return;
+        }
+        super.setObject(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionExecutor.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionExecutor.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionExecutor.java
new file mode 100644
index 0000000..d634262
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionExecutor.java
@@ -0,0 +1,35 @@
+/*
+ *  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.viewer.wicket.model.models;
+
+import java.io.Serializable;
+
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.markup.html.form.Form;
+
+/**
+ * Decouples the {@link ActionModel}, which needs to delegate the actual
+ * execution of an action, from its implementor.
+ */
+public interface ActionExecutor extends Serializable {
+
+    boolean executeActionAndProcessResults(AjaxRequestTarget target, Form<?> 
feedbackForm);
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionModel.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionModel.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionModel.java
new file mode 100644
index 0000000..ee5e341
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionModel.java
@@ -0,0 +1,663 @@
+/*
+ *  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.viewer.wicket.model.models;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import com.google.common.base.Throwables;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
+import org.apache.wicket.request.http.handler.RedirectRequestHandler;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.resource.ContentDisposition;
+import org.apache.wicket.util.resource.AbstractResourceStream;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.isis.applib.Identifier;
+import org.apache.isis.applib.RecoverableException;
+import org.apache.isis.applib.annotation.ActionSemantics;
+import org.apache.isis.applib.annotation.BookmarkPolicy;
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.applib.value.Blob;
+import org.apache.isis.applib.value.Clob;
+import org.apache.isis.applib.value.NamedWithMimeType;
+import org.apache.isis.core.commons.authentication.AuthenticationSession;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import 
org.apache.isis.core.metamodel.adapter.mgr.AdapterManager.ConcurrencyChecking;
+import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller;
+import org.apache.isis.core.metamodel.adapter.oid.RootOid;
+import org.apache.isis.core.metamodel.adapter.oid.RootOidDefault;
+import org.apache.isis.core.metamodel.consent.Consent;
+import 
org.apache.isis.core.metamodel.facets.object.bookmarkpolicy.BookmarkPolicyFacet;
+import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet;
+import org.apache.isis.core.metamodel.spec.ActionType;
+import org.apache.isis.core.metamodel.spec.ObjectSpecId;
+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.ObjectActionParameter;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+import org.apache.isis.viewer.wicket.model.common.PageParametersUtils;
+import org.apache.isis.viewer.wicket.model.mementos.ActionMemento;
+import org.apache.isis.viewer.wicket.model.mementos.ActionParameterMemento;
+import org.apache.isis.viewer.wicket.model.mementos.ObjectAdapterMemento;
+import org.apache.isis.viewer.wicket.model.mementos.PageParameterNames;
+
+/**
+ * Models an action invocation, either the gathering of arguments for the
+ * action's {@link Mode#PARAMETERS parameters}, or the handling of the
+ * {@link Mode#RESULTS results} once invoked.
+ */
+public class ActionModel extends BookmarkableModel<ObjectAdapter> {
+    
+    private static final long serialVersionUID = 1L;
+    
+    private static final String NULL_ARG = "$nullArg$";
+    private static final Pattern KEY_VALUE_PATTERN = 
Pattern.compile("([^=]+)=(.+)");
+
+    /**
+     * Whether we are obtaining arguments (eg in a dialog), or displaying the
+     * results
+     */
+    private enum Mode {
+        PARAMETERS, 
+        RESULTS
+    }
+
+
+    
+    //////////////////////////////////////////////////
+    // Factory methods
+    //////////////////////////////////////////////////
+
+    /**
+     * @param objectAdapter
+     * @param action
+     * @return
+     */
+    public static ActionModel create(final ObjectAdapter objectAdapter, final 
ObjectAction action) {
+        final ObjectAdapterMemento serviceMemento = 
ObjectAdapterMemento.Functions.fromAdapter().apply(objectAdapter);
+        final ActionMemento homePageActionMemento = 
ObjectAdapterMemento.Functions.fromAction().apply(action);
+        final Mode mode = action.getParameterCount() > 0? Mode.PARAMETERS : 
Mode.RESULTS;
+        return new ActionModel(serviceMemento, homePageActionMemento, mode);
+    }
+
+    public static ActionModel createForPersistent(final PageParameters 
pageParameters) {
+        return new ActionModel(pageParameters);
+    }
+
+    /**
+     * Factory method for creating {@link PageParameters}.
+     * 
+     * see {@link #ActionModel(PageParameters)}
+     */
+    public static PageParameters createPageParameters(
+            final ObjectAdapter adapter, final ObjectAction objectAction, 
final ConcurrencyChecking concurrencyChecking) {
+        
+        final PageParameters pageParameters = 
PageParametersUtils.newPageParameters();
+        
+        final String oidStr = concurrencyChecking == ConcurrencyChecking.CHECK?
+                adapter.getOid().enString(getOidMarshaller()):
+                adapter.getOid().enStringNoVersion(getOidMarshaller());
+        PageParameterNames.OBJECT_OID.addStringTo(pageParameters, oidStr);
+        
+        final ActionType actionType = objectAction.getType();
+        PageParameterNames.ACTION_TYPE.addEnumTo(pageParameters, actionType);
+        
+        final ObjectSpecification actionOnTypeSpec = objectAction.getOnType();
+        if (actionOnTypeSpec != null) {
+            PageParameterNames.ACTION_OWNING_SPEC.addStringTo(pageParameters, 
actionOnTypeSpec.getFullIdentifier());
+        }
+        
+        final String actionId = determineActionId(objectAction);
+        PageParameterNames.ACTION_ID.addStringTo(pageParameters, actionId);
+        
+        return pageParameters;
+    }
+
+
+    public static Entry<Integer, String> parse(final String paramContext) {
+        final Matcher matcher = KEY_VALUE_PATTERN.matcher(paramContext);
+        if (!matcher.matches()) {
+            return null;
+        }
+
+        final int paramNum;
+        try {
+            paramNum = Integer.parseInt(matcher.group(1));
+        } catch (final Exception e) {
+            // ignore
+            return null;
+        }
+
+        final String oidStr;
+        try {
+            oidStr = matcher.group(2);
+        } catch (final Exception e) {
+            return null;
+        }
+
+        return new Map.Entry<Integer, String>() {
+
+            @Override
+            public Integer getKey() {
+                return paramNum;
+            }
+
+            @Override
+            public String getValue() {
+                return oidStr;
+            }
+
+            @Override
+            public String setValue(final String value) {
+                return null;
+            }
+        };
+    }
+
+    //////////////////////////////////////////////////
+    // BookmarkableModel
+    //////////////////////////////////////////////////
+
+    public PageParameters getPageParameters() {
+        final ObjectAdapter adapter = getTargetAdapter();
+        final ObjectAction objectAction = getActionMemento().getAction();
+        final PageParameters pageParameters = createPageParameters(
+                adapter, objectAction, ConcurrencyChecking.NO_CHECK);
+
+        // capture argument values
+        final ObjectAdapter[] argumentsAsArray = getArgumentsAsArray();
+        for(final ObjectAdapter argumentAdapter: argumentsAsArray) {
+            final String encodedArg = encodeArg(argumentAdapter);
+            PageParameterNames.ACTION_ARGS.addStringTo(pageParameters, 
encodedArg);
+        }
+
+        return pageParameters;
+    }
+
+    @Override
+    public String getTitle() {
+        final ObjectAdapter adapter = getTargetAdapter();
+        final ObjectAction objectAction = getActionMemento().getAction();
+        
+        final StringBuilder buf = new StringBuilder();
+        final ObjectAdapter[] argumentsAsArray = getArgumentsAsArray();
+        for(final ObjectAdapter argumentAdapter: argumentsAsArray) {
+            if(buf.length() > 0) {
+                buf.append(",");
+            }
+            buf.append(abbreviated(titleOf(argumentAdapter), 8));
+        }
+
+        return adapter.titleString(null) + "." + objectAction.getName() + 
(buf.length()>0?"(" + buf.toString() + ")":"");
+    }
+
+    @Override
+    public boolean hasAsRootPolicy() {
+        return true;
+    }
+
+    //////////////////////////////////////////////////
+    // helpers
+    //////////////////////////////////////////////////
+
+    
+    private static String titleOf(final ObjectAdapter argumentAdapter) {
+        return argumentAdapter!=null?argumentAdapter.titleString(null):"";
+    }
+    
+    private static String abbreviated(final String str, final int maxLength) {
+        return str.length() < maxLength ? str : str.substring(0, maxLength - 
3) + "...";
+    }
+
+
+    private static String determineActionId(final ObjectAction objectAction) {
+        final Identifier identifier = objectAction.getIdentifier();
+        if (identifier != null) {
+            return identifier.toNameParmsIdentityString();
+        }
+        // fallback (used for action sets)
+        return objectAction.getId();
+    }
+
+    public static Mode determineMode(final ObjectAction action) {
+        return action.getParameterCount() > 0 ? Mode.PARAMETERS : Mode.RESULTS;
+    }
+
+    private final ObjectAdapterMemento targetAdapterMemento;
+    private final ActionMemento actionMemento;
+    private Mode actionMode;
+
+
+    /**
+     * Lazily populated in {@link #getArgumentModel(ActionParameterMemento)}
+     */
+    private final Map<Integer, ScalarModel> arguments = Maps.newHashMap();
+    private ActionExecutor executor;
+
+
+    private ActionModel(final PageParameters pageParameters) {
+        this(newObjectAdapterMementoFrom(pageParameters), 
newActionMementoFrom(pageParameters), actionModeFrom(pageParameters));
+
+        setArgumentsIfPossible(pageParameters);
+        setContextArgumentIfPossible(pageParameters);
+    }
+
+    private static ActionMemento newActionMementoFrom(final PageParameters 
pageParameters) {
+        final ObjectSpecId owningSpec = 
ObjectSpecId.of(PageParameterNames.ACTION_OWNING_SPEC.getStringFrom(pageParameters));
+        final ActionType actionType = 
PageParameterNames.ACTION_TYPE.getEnumFrom(pageParameters, ActionType.class);
+        final String actionNameParms = 
PageParameterNames.ACTION_ID.getStringFrom(pageParameters);
+        return new ActionMemento(owningSpec, actionType, actionNameParms);
+    }
+
+    private static Mode actionModeFrom(final PageParameters pageParameters) {
+        final ActionMemento actionMemento = 
newActionMementoFrom(pageParameters);
+        if(actionMemento.getAction().getParameterCount() == 0) {
+            return Mode.RESULTS;
+        }
+        final List<String> listFrom = 
PageParameterNames.ACTION_ARGS.getListFrom(pageParameters);
+        return !listFrom.isEmpty()? Mode.RESULTS: Mode.PARAMETERS;
+    }
+
+
+    private static ObjectAdapterMemento newObjectAdapterMementoFrom(final 
PageParameters pageParameters) {
+        final RootOid oid = oidFor(pageParameters);
+        if(oid.isTransient()) {
+            return null;
+        } else {
+            return ObjectAdapterMemento.createPersistent(oid);
+        }
+    }
+
+    private static RootOid oidFor(final PageParameters pageParameters) {
+        final String oidStr = 
PageParameterNames.OBJECT_OID.getStringFrom(pageParameters);
+        return getOidMarshaller().unmarshal(oidStr, RootOid.class);
+    }
+
+
+    private ActionModel(final ObjectAdapterMemento adapterMemento, final 
ActionMemento actionMemento, final Mode actionMode) {
+        this.targetAdapterMemento = adapterMemento;
+        this.actionMemento = actionMemento;
+        this.actionMode = actionMode;
+    }
+
+    /**
+     * Copy constructor, as called by {@link #copy()}.
+     */
+    private ActionModel(final ActionModel actionModel) {
+        this.targetAdapterMemento = actionModel.targetAdapterMemento;
+        this.actionMemento = actionModel.actionMemento;
+        this.actionMode = actionModel.actionMode;
+        //this.actionPrompt = actionModel.actionPrompt;
+        
+        primeArgumentModels();
+        final Map<Integer, ScalarModel> argumentModelByIdx = 
actionModel.arguments;
+        for (final Map.Entry<Integer,ScalarModel> argumentModel : 
argumentModelByIdx.entrySet()) {
+            setArgument(argumentModel.getKey(), 
argumentModel.getValue().getObject());
+        }
+
+        this.executor = actionModel.executor;
+    }
+
+    private void setArgumentsIfPossible(final PageParameters pageParameters) {
+        final List<String> args = 
PageParameterNames.ACTION_ARGS.getListFrom(pageParameters);
+
+        final ObjectAction action = actionMemento.getAction();
+        final List<ObjectSpecification> parameterTypes = 
action.getParameterTypes();
+
+        for (int paramNum = 0; paramNum < args.size(); paramNum++) {
+            final String encoded = args.get(paramNum);
+            setArgument(paramNum, parameterTypes.get(paramNum), encoded);
+        }
+    }
+
+    public boolean hasParameters() {
+        return actionMode == ActionModel.Mode.PARAMETERS;
+    }
+
+    private boolean setContextArgumentIfPossible(final PageParameters 
pageParameters) {
+        final String paramContext = 
PageParameterNames.ACTION_PARAM_CONTEXT.getStringFrom(pageParameters);
+        if (paramContext == null) {
+            return false;
+        }
+
+        final ObjectAction action = actionMemento.getAction();
+        final List<ObjectSpecification> parameterTypes = 
action.getParameterTypes();
+        final int parameterCount = parameterTypes.size();
+
+        final Map.Entry<Integer, String> mapEntry = parse(paramContext);
+
+        final int paramNum = mapEntry.getKey();
+        if (paramNum >= parameterCount) {
+            return false;
+        }
+
+        final String encoded = mapEntry.getValue();
+        setArgument(paramNum, parameterTypes.get(paramNum), encoded);
+
+        return true;
+    }
+
+    private void setArgument(final int paramNum, final ObjectSpecification 
argSpec, final String encoded) {
+        final ObjectAdapter argumentAdapter = decodeArg(argSpec, encoded);
+        setArgument(paramNum, argumentAdapter);
+    }
+
+    private String encodeArg(final ObjectAdapter adapter) {
+        if(adapter == null) {
+            return NULL_ARG;
+        }
+        
+        final ObjectSpecification objSpec = adapter.getSpecification();
+        if(objSpec.isEncodeable()) {
+            final EncodableFacet encodeable = 
objSpec.getFacet(EncodableFacet.class);
+            return encodeable.toEncodedString(adapter);
+        }
+        
+        return adapter.getOid().enStringNoVersion(getOidMarshaller());
+    }
+
+    private ObjectAdapter decodeArg(final ObjectSpecification objSpec, final 
String encoded) {
+        if(NULL_ARG.equals(encoded)) {
+            return null;
+        }
+        
+        if(objSpec.isEncodeable()) {
+            final EncodableFacet encodeable = 
objSpec.getFacet(EncodableFacet.class);
+            return encodeable.fromEncodedString(encoded);
+        }
+        
+        try {
+            final RootOid oid = RootOidDefault.deStringEncoded(encoded, 
getOidMarshaller());
+            return getAdapterManager().adapterFor(oid);
+        } catch (final Exception e) {
+            return null;
+        }
+    }
+
+    private void setArgument(final int paramNum, final ObjectAdapter 
argumentAdapter) {
+        final ObjectAction action = actionMemento.getAction();
+        final ObjectActionParameter actionParam = 
action.getParameters().get(paramNum);
+        final ActionParameterMemento apm = new 
ActionParameterMemento(actionParam);
+        final ScalarModel argumentModel = getArgumentModel(apm);
+        argumentModel.setObject(argumentAdapter);
+    }
+
+
+    public ScalarModel getArgumentModel(final ActionParameterMemento apm) {
+        final int i = apm.getNumber();
+               ScalarModel scalarModel = arguments.get(i);
+        if (scalarModel == null) {
+            scalarModel = new ScalarModel(targetAdapterMemento, apm);
+            final int number = scalarModel.getParameterMemento().getNumber();
+            arguments.put(number, scalarModel);
+        }
+        return scalarModel;
+    }
+
+    public ObjectAdapter getTargetAdapter() {
+        return targetAdapterMemento.getObjectAdapter(getConcurrencyChecking());
+    }
+
+    protected ConcurrencyChecking getConcurrencyChecking() {
+        return actionMemento.getConcurrencyChecking();
+    }
+
+    public ActionMemento getActionMemento() {
+        return actionMemento;
+    }
+
+    @Override
+    protected ObjectAdapter load() {
+        
+        // from getObject()/reExecute
+        detach(); // force re-execute
+        
+        // TODO: think we need another field to determine if args have been 
populated.
+        final ObjectAdapter results = executeAction();
+        this.actionMode = Mode.RESULTS;
+        
+        return results;
+    }
+
+    // REVIEW: should provide this rendering context, rather than hardcoding.
+    // the net effect currently is that class members annotated with
+    // @Hidden(where=Where.ANYWHERE) or @Disabled(where=Where.ANYWHERE) will 
indeed
+    // be hidden/disabled, but will be visible/enabled (perhaps incorrectly)
+    // for any other value for Where
+    public static final Where WHERE_FOR_ACTION_INVOCATION = Where.ANYWHERE;
+
+    private ObjectAdapter executeAction() {
+
+        final ObjectAdapter targetAdapter = getTargetAdapter();
+        final ObjectAdapter[] arguments = getArgumentsAsArray();
+        final ObjectAction action = getActionMemento().getAction();
+
+        final AuthenticationSession session = getAuthenticationSession();
+        return action.executeWithRuleChecking(targetAdapter, arguments, 
session, WHERE_FOR_ACTION_INVOCATION);
+    }
+
+    public String getReasonInvalidIfAny() {
+        final ObjectAdapter targetAdapter = getTargetAdapter();
+        final ObjectAdapter[] proposedArguments = getArgumentsAsArray();
+        final ObjectAction objectAction = getActionMemento().getAction();
+        final Consent validity = 
objectAction.isProposedArgumentSetValid(targetAdapter, proposedArguments);
+        return validity.isAllowed() ? null : validity.getReason();
+    }
+
+    @Override
+    public void setObject(final ObjectAdapter object) {
+        throw new UnsupportedOperationException("target adapter for 
ActionModel cannot be changed");
+    }
+
+    public ObjectAdapter[] getArgumentsAsArray() {
+       if(this.arguments.size() < 
this.getActionMemento().getAction().getParameterCount()) {
+               primeArgumentModels();
+       }
+       
+        final ObjectAction objectAction = getActionMemento().getAction();
+        final ObjectAdapter[] arguments = new 
ObjectAdapter[objectAction.getParameterCount()];
+        for (int i = 0; i < arguments.length; i++) {
+            final ScalarModel scalarModel = this.arguments.get(i);
+            arguments[i] = scalarModel.getObject();
+        }
+        return arguments;
+    }
+    
+    public ActionExecutor getExecutor() {
+        return executor;
+    }
+
+    public void setExecutor(final ActionExecutor executor) {
+        this.executor = executor;
+    }
+
+    public void reset() {
+        this.actionMode = determineMode(actionMemento.getAction());
+    }
+
+    public void clearArguments() {
+        for (final ScalarModel argumentModel : arguments.values()) {
+            argumentModel.reset();
+        }
+        this.actionMode = determineMode(actionMemento.getAction());
+    }
+
+    /**
+     * Bookmarkable if the {@link ObjectAction action} has a {@link 
BookmarkPolicyFacet bookmark} policy
+     * of {@link BookmarkPolicy#AS_ROOT root}, and has safe {@link 
ObjectAction#getSemantics() semantics}.
+     */
+    public boolean isBookmarkable() {
+        final ObjectAction action = getActionMemento().getAction();
+        final BookmarkPolicyFacet bookmarkPolicy = 
action.getFacet(BookmarkPolicyFacet.class);
+        final boolean safeSemantics = action.getSemantics() == 
ActionSemantics.Of.SAFE;
+        return bookmarkPolicy.value() == BookmarkPolicy.AS_ROOT && 
safeSemantics;
+    }
+
+    // //////////////////////////////////////
+
+    /**
+     * Executes the action, handling any {@link RecoverableException}s that
+     * might be encountered.
+     *
+     * <p>
+     * If an {@link RecoverableException} is encountered, then the application 
error will be
+     * {@link 
org.apache.isis.core.commons.authentication.MessageBroker#setApplicationError(String)
 set} so that a suitable message can be
+     * rendered higher up the call stack.
+     *
+     * <p>
+     * Any other types of exception will be ignored (to be picked up higher up 
in the callstack)
+     */
+    public ObjectAdapter executeHandlingApplicationExceptions() {
+        try {
+            final ObjectAdapter resultAdapter = this.getObject();
+            return resultAdapter;
+
+        } catch (final RuntimeException ex) {
+
+            // see if is an application-defined exception
+            // if so, is converted to an application error,
+            // equivalent to calling DomainObjectContainer#raiseError(...)
+            final RecoverableException appEx = 
getApplicationExceptionIfAny(ex);
+            if (appEx != null) {
+                
IsisContext.getMessageBroker().setApplicationError(appEx.getMessage());
+
+                // there's no need to set the abort cause on the transaction, 
it will have already been done
+                // (in IsisTransactionManager#executeWithinTransaction(...)).
+
+                return null;
+            }
+
+            // not handled, so propagate
+            throw ex;
+        }
+    }
+
+
+    // //////////////////////////////////////
+    
+    public static RecoverableException getApplicationExceptionIfAny(final 
Exception ex) {
+        final Iterable<RecoverableException> appEx = 
Iterables.filter(Throwables.getCausalChain(ex), RecoverableException.class);
+        final Iterator<RecoverableException> iterator = appEx.iterator();
+        return iterator.hasNext() ? iterator.next() : null;
+    }
+
+    public static IRequestHandler redirectHandler(final Object value) {
+        if(value instanceof java.net.URL) {
+            final java.net.URL url = (java.net.URL) value;
+            return new RedirectRequestHandler(url.toString());
+        }
+        return null;
+    }
+
+    public static IRequestHandler downloadHandler(final Object value) {
+        if(value instanceof Clob) {
+            final Clob clob = (Clob)value;
+            return handlerFor(resourceStreamFor(clob), clob);
+        }
+        if(value instanceof Blob) {
+            final Blob blob = (Blob)value;
+            return handlerFor(resourceStreamFor(blob), blob);
+        }
+        return null;
+    }
+    
+    private static IResourceStream resourceStreamFor(final Blob blob) {
+        final IResourceStream resourceStream = new AbstractResourceStream() {
+
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public InputStream getInputStream() throws 
ResourceStreamNotFoundException {
+                return new ByteArrayInputStream(blob.getBytes());
+            }
+
+            @Override
+            public String getContentType() {
+                return blob.getMimeType().toString();
+            }
+
+            @Override
+            public void close() throws IOException {
+            }
+        };
+        return resourceStream;
+    }
+
+    private static IResourceStream resourceStreamFor(final Clob clob) {
+        final IResourceStream resourceStream = new 
StringResourceStream(clob.getChars(), clob.getMimeType().toString());
+        return resourceStream;
+    }
+
+    private static IRequestHandler handlerFor(final IResourceStream 
resourceStream, final NamedWithMimeType namedWithMimeType) {
+        final ResourceStreamRequestHandler handler =
+            new ResourceStreamRequestHandler(resourceStream, 
namedWithMimeType.getName());
+        handler.setContentDisposition(ContentDisposition.ATTACHMENT);
+        return handler;
+    }
+
+    // //////////////////////////////////////
+    
+    public List<ActionParameterMemento> primeArgumentModels() {
+        final ObjectAction objectAction = getActionMemento().getAction();
+
+        final List<ObjectActionParameter> parameters = 
objectAction.getParameters();
+        final List<ActionParameterMemento> mementos = 
buildParameterMementos(parameters);
+        for (final ActionParameterMemento apm : mementos) {
+            getArgumentModel(apm);
+        }
+        
+        return mementos;
+    }
+
+    
+    private static List<ActionParameterMemento> buildParameterMementos(final 
List<ObjectActionParameter> parameters) {
+        final List<ActionParameterMemento> parameterMementoList = 
Lists.transform(parameters, 
ObjectAdapterMemento.Functions.fromActionParameter());
+        // we copy into a new array list otherwise we get lazy evaluation =
+        // reference to a non-serializable object
+        return Lists.newArrayList(parameterMementoList);
+    }
+
+    //////////////////////////////////////////////////
+    // Dependencies (from context)
+    //////////////////////////////////////////////////
+    
+    private static OidMarshaller getOidMarshaller() {
+        return IsisContext.getOidMarshaller();
+    }
+
+    public ActionModel copy() {
+        return new ActionModel(this);
+    }
+
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPrompt.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPrompt.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPrompt.java
new file mode 100644
index 0000000..0e2747a
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPrompt.java
@@ -0,0 +1,71 @@
+/*
+ *  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.viewer.wicket.model.models;
+
+import java.io.Serializable;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+
+/**
+ * Decouples the {@link ActionModel} from its (modal window) prompt.
+ */
+public interface ActionPrompt extends Serializable {
+
+    /**
+     * Sets the component that should be used as a title for the modal window
+     *
+     * @param component The title component
+     * @param target The current Ajax request handler
+     */
+    void setTitle(Component component, AjaxRequestTarget target);
+
+    /**
+     * Sets the component that should be used as a body for the modal window
+     *
+     * @param component The body component
+     * @param target The current Ajax request handler
+     */
+    void setPanel(Component component, AjaxRequestTarget target);
+
+    /**
+     * Shows the modal window
+     *
+     * @param target The current Ajax request handler
+     */
+    void showPrompt(AjaxRequestTarget target);
+
+    /**
+     * @return the component id for the title component
+     */
+    String getTitleId();
+
+    /**
+     * @return the component id for the body component
+     */
+    String getContentId();
+
+    /**
+     * Closes the modal window
+     *
+     * @param target The current Ajax request handler
+     */
+    void closePrompt(AjaxRequestTarget target);
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPromptProvider.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPromptProvider.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPromptProvider.java
new file mode 100644
index 0000000..42fbda5
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPromptProvider.java
@@ -0,0 +1,44 @@
+/**
+ *  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.viewer.wicket.model.models;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.Page;
+
+public interface ActionPromptProvider {
+
+    public static class Util{
+
+        public static ActionPromptProvider getFrom(Component component) {
+            final Page page = component.getPage();
+            if(page == null) {
+                throw new IllegalArgumentException("Programming error: 
component must be added to a page in order to locate the ActionPromptProvider");
+            }
+            return getFrom(page);
+        }
+        public static ActionPromptProvider getFrom(Page page) {
+            if(page instanceof ActionPromptProvider) {
+                final ActionPromptProvider provider = (ActionPromptProvider) 
page;
+                return provider;
+            }
+            // else
+            throw new IllegalArgumentException("Programming error: all pages 
should inherit from PageAbstract, which serves as the ActionPromptProvider");
+        }
+    }
+
+    public ActionPrompt getActionPrompt();
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNode.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNode.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNode.java
new file mode 100644
index 0000000..089eb88
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNode.java
@@ -0,0 +1,270 @@
+/*
+ *  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.viewer.wicket.model.models;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.adapter.oid.Oid;
+import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller;
+import org.apache.isis.core.metamodel.adapter.oid.RootOid;
+import org.apache.isis.core.metamodel.spec.feature.Contributed;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+import org.apache.isis.viewer.wicket.model.mementos.PageParameterNames;
+
+public class BookmarkTreeNode implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    private final List<BookmarkTreeNode> children = Lists.newArrayList();
+    private final int depth;
+
+    private final RootOid oidNoVer;
+    private final String oidNoVerStr;
+    private final PageType pageType;
+    
+    private String title;
+    private PageParameters pageParameters;
+
+    public static BookmarkTreeNode newRoot(
+            BookmarkableModel<?> bookmarkableModel) {
+        return new BookmarkTreeNode(bookmarkableModel, 0);
+    }
+
+    private BookmarkTreeNode(
+            final BookmarkableModel<?> bookmarkableModel, 
+            final int depth) {
+        pageParameters = bookmarkableModel.getPageParameters();
+        RootOid oid = oidFrom(pageParameters);
+        this.oidNoVerStr = getOidMarshaller().marshalNoVersion(oid);
+        this.oidNoVer = getOidMarshaller().unmarshal(oidNoVerStr, 
RootOid.class);
+        
+        // replace oid with the noVer equivalent.
+        PageParameterNames.OBJECT_OID.removeFrom(pageParameters);
+        PageParameterNames.OBJECT_OID.addStringTo(pageParameters, 
getOidNoVerStr());
+        
+        this.title = bookmarkableModel.getTitle();
+        this.pageType = bookmarkableModel instanceof EntityModel ? 
PageType.ENTITY : PageType.ACTION_PROMPT;
+        this.depth = depth;
+        
+    }
+
+    public RootOid getOidNoVer() {
+        return oidNoVer;
+    }
+
+    public String getOidNoVerStr() {
+        return oidNoVerStr;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+    private void setTitle(String title) {
+        this.title = title;
+    }
+
+    public PageType getPageType() {
+        return pageType;
+    }
+
+    public List<BookmarkTreeNode> getChildren() {
+        return children;
+    }
+    public BookmarkTreeNode addChild(BookmarkableModel<?> childModel) {
+        final BookmarkTreeNode childNode = new BookmarkTreeNode(childModel, 
depth+1);
+        children.add(childNode);
+        return childNode;
+    }
+    
+    /**
+     * Whether or not the provided {@link BookmarkableModel} matches that 
contained
+     * within this node, or any of its children.
+     * 
+     * <p>
+     * If it does, then the matched node's title is updated to that of the 
provided
+     * {@link BookmarkableModel}.
+     * 
+     * <p>
+     * The {@link PageParameters} (used for matching) is 
+     * {@link BookmarkableModel#getPageParameters() obtained} from the {@link 
BookmarkableModel}.
+     * 
+     * @return - whether the provided candidate is found or was added to this 
node's tree.
+     */
+    public boolean matches(BookmarkableModel<?> candidateBookmarkableModel) {
+       if(candidateBookmarkableModel instanceof EntityModel) {
+               if(this.pageType != PageType.ENTITY) { 
+                       return false; 
+                       }
+                       return matchAndUpdateTitleFor((EntityModel) 
candidateBookmarkableModel);
+       } else if(candidateBookmarkableModel instanceof ActionModel) {
+               if(this.pageType != PageType.ACTION_PROMPT) { 
+                       return false; 
+                       }
+                       return matchFor((ActionModel) 
candidateBookmarkableModel);
+       } else {
+               return false;
+       }
+    }
+
+    /**
+     * Whether or not the provided {@link EntityModel} matches that contained
+     * within this node, or any of its children.
+     * 
+     * <p>
+     * If it does match, then the matched node's title is updated to that of 
the provided
+     * {@link EntityModel}.
+     * 
+     * @return - whether the provided candidate is found or was added to this 
node's tree.
+     */
+       private boolean matchAndUpdateTitleFor(final EntityModel 
candidateEntityModel) {
+               
+               // match only on the oid string
+               final String candidateOidStr = oidStrFrom(candidateEntityModel);
+        boolean inGraph = Objects.equal(this.oidNoVerStr, candidateOidStr);
+        if(inGraph) {
+            this.setTitle(candidateEntityModel.getTitle());
+        }
+
+        // and also match recursively down to all children and grandchildren.
+        if(candidateEntityModel.hasAsChildPolicy()) {
+            for(BookmarkTreeNode childNode: this.getChildren()) {
+                inGraph = childNode.matches(candidateEntityModel) || inGraph; 
// evaluate each
+            }
+            
+            if(!inGraph) {
+                inGraph = addToGraphIfParented(candidateEntityModel);
+            }
+        }
+        return inGraph;
+       }
+       
+    /**
+     * Whether or not the provided {@link ActionModel} matches that contained
+     * within this node (taking into account the action's arguments).
+     * 
+     * If it does match, then the matched node's title is updated to that of 
the provided
+     * {@link ActionModel}.
+     * <p>
+     * 
+     * @return - whether the provided candidate is found or was added to this 
node's tree.
+     */
+       private boolean matchFor(final ActionModel candidateActionModel) {
+               
+               // check if target object of the action is the same (the oid 
str)
+               final String candidateOidStr = oidStrFrom(candidateActionModel);
+               if(!Objects.equal(this.oidNoVerStr, candidateOidStr)) {
+                       return false;
+               }
+               
+               // check if args same
+        List<String> thisArgs = 
PageParameterNames.ACTION_ARGS.getListFrom(pageParameters);
+        PageParameters candidatePageParameters = 
candidateActionModel.getPageParameters();
+        List<String> candidateArgs = 
PageParameterNames.ACTION_ARGS.getListFrom(candidatePageParameters);
+        if(!Objects.equal(thisArgs, candidateArgs)) {
+               return false;
+        }
+
+        // ok, a match
+               return true;
+       }
+
+    private boolean addToGraphIfParented(BookmarkableModel<?> 
candidateBookmarkableModel) {
+        
+        boolean whetherAdded = false;
+        // TODO: this ought to be move into a responsibility of 
BookmarkableModel, perhaps, rather than downcasting
+        if(candidateBookmarkableModel instanceof EntityModel) {
+            EntityModel entityModel = (EntityModel) candidateBookmarkableModel;
+            final ObjectAdapter candidateAdapter = entityModel.getObject();
+            final List<ObjectAssociation> properties = 
candidateAdapter.getSpecification().getAssociations(Contributed.EXCLUDED, 
ObjectAssociation.Filters.REFERENCE_PROPERTIES);
+            for (ObjectAssociation objectAssoc : properties) {
+                final ObjectAdapter possibleParentAdapter = 
objectAssoc.get(candidateAdapter);
+                if(possibleParentAdapter == null) {
+                    continue;
+                } 
+                final Oid possibleParentOid = possibleParentAdapter.getOid();
+                if(possibleParentOid == null) {
+                    continue;
+                } 
+                final String possibleParentOidStr = 
possibleParentOid.enStringNoVersion(getOidMarshaller());
+                if(Objects.equal(this.oidNoVerStr, possibleParentOidStr)) {
+                    this.addChild(candidateBookmarkableModel);
+                    whetherAdded = true;
+                }
+            }
+        }
+        return whetherAdded;
+    }
+
+    public void appendGraphTo(List<BookmarkTreeNode> list) {
+        list.add(this);
+        for (BookmarkTreeNode childNode : children) {
+            childNode.appendGraphTo(list);
+        }
+    }
+
+    public int getDepth() {
+        return depth;
+    }
+
+    
+    // //////////////////////////////////////
+
+    public PageParameters getPageParameters() {
+        return pageParameters;
+    }
+    
+    // //////////////////////////////////////
+
+    public static RootOid oidFrom(final PageParameters pageParameters) {
+        String oidStr = 
PageParameterNames.OBJECT_OID.getStringFrom(pageParameters);
+        if(oidStr == null) {
+            return null;
+        }
+        try {
+            return getOidMarshaller().unmarshal(oidStr, RootOid.class);
+        } catch(Exception ex) {
+            return null;
+        }
+    }
+
+    public static String oidStrFrom(BookmarkableModel<?> 
candidateBookmarkableModel) {
+        final RootOid oid = 
oidFrom(candidateBookmarkableModel.getPageParameters());
+        return oid != null? getOidMarshaller().marshalNoVersion(oid): null;
+    }
+
+
+    // //////////////////////////////////////
+
+    protected static OidMarshaller getOidMarshaller() {
+        return IsisContext.getOidMarshaller();
+    }
+
+
+
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNodeComparator.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNodeComparator.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNodeComparator.java
new file mode 100644
index 0000000..638968e
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNodeComparator.java
@@ -0,0 +1,86 @@
+/*
+ *  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.viewer.wicket.model.models;
+
+import java.util.Comparator;
+
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller;
+import org.apache.isis.core.metamodel.adapter.oid.RootOid;
+import org.apache.isis.core.metamodel.spec.ObjectSpecId;
+import org.apache.isis.core.metamodel.spec.SpecificationLoader;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+import org.apache.isis.viewer.wicket.model.mementos.PageParameterNames;
+
+final class BookmarkTreeNodeComparator implements Comparator<BookmarkTreeNode> 
{
+    
+    @Override
+    public int compare(BookmarkTreeNode o1, BookmarkTreeNode o2) {
+        
+        final PageType pageType1 = o1.getPageType();
+        final PageType pageType2 = o2.getPageType();
+        
+        final int pageTypeComparison = pageType1.compareTo(pageType2);
+        if(pageTypeComparison != 0) {
+            return pageTypeComparison;
+        }
+        
+        final RootOid oid1 = o1.getOidNoVer();
+        final RootOid oid2 = o2.getOidNoVer();
+        
+        // sort by entity type
+        final String className1 = classNameOf(oid1);
+        final String className2 = classNameOf(oid2);
+        
+        final int classNameComparison = className1.compareTo(className2);
+        if(classNameComparison != 0) {
+            return classNameComparison;
+        }
+        
+        final String title1 = o1.getTitle();
+        final String title2 = o2.getTitle();
+        
+        return title1.compareTo(title2);
+    }
+
+    private String classNameOf(RootOid oid) {
+        ObjectSpecId objectSpecId = oid.getObjectSpecId();
+        return 
getSpecificationLoader().lookupBySpecId(objectSpecId).getIdentifier().getClassName();
+    }
+
+    private RootOid oidOf(PageParameters pp) {
+        String oidStr = PageParameterNames.OBJECT_OID.getStringFrom(pp);
+        return getOidMarshaller().unmarshal(oidStr, RootOid.class);
+    }
+    
+    //////////////////////////////////////////////////
+    // Dependencies (from context)
+    //////////////////////////////////////////////////
+    
+    protected OidMarshaller getOidMarshaller() {
+        return IsisContext.getOidMarshaller();
+    }
+    
+    protected SpecificationLoader getSpecificationLoader() {
+        return IsisContext.getSpecificationLoader();
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkableModel.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkableModel.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkableModel.java
new file mode 100644
index 0000000..1dfa2ec
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkableModel.java
@@ -0,0 +1,46 @@
+/*
+ *  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.viewer.wicket.model.models;
+
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+public abstract class BookmarkableModel<T> extends ModelAbstract<T>  {
+
+    private static final long serialVersionUID = 1L;
+
+    public BookmarkableModel() {
+        super();
+    }
+
+    public BookmarkableModel(T t) {
+        super(t);
+    }
+
+    
+    /**
+     * So can be bookmarked / added to <tt>BookmarkedPagesModel</tt>.
+     */
+    public abstract PageParameters getPageParameters();
+    
+    public abstract boolean hasAsRootPolicy();
+    
+    public abstract String getTitle();
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkedPagesModel.java
----------------------------------------------------------------------
diff --git 
a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkedPagesModel.java
 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkedPagesModel.java
new file mode 100644
index 0000000..1a5d614
--- /dev/null
+++ 
b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkedPagesModel.java
@@ -0,0 +1,151 @@
+/*
+ *  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.viewer.wicket.model.models;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.isis.core.commons.config.IsisConfiguration;
+import org.apache.isis.core.metamodel.adapter.oid.RootOid;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+
+
+public class BookmarkedPagesModel extends ModelAbstract<List<? extends 
BookmarkTreeNode>> {
+
+
+    private static final long serialVersionUID = 1L;
+
+    private static final BookmarkTreeNodeComparator COMPARATOR = new 
BookmarkTreeNodeComparator();
+
+    private static final String MAX_SIZE_KEY = 
"isis.viewer.wicket.bookmarkedPages.maxSize";
+    private static final int MAX_SIZE_DEFAULT_VALUE = 15;
+
+    private final List<BookmarkTreeNode> rootNodes = Lists.newArrayList();
+    
+    private transient PageParameters current;
+    
+    public void bookmarkPage(final BookmarkableModel<?> bookmarkableModel) {
+
+        // hack: remove any garbage that might've got stored in 'rootNodes'
+        cleanUpGarbage(rootNodes);
+        
+        final PageParameters candidatePP = 
bookmarkableModel.getPageParameters();
+        RootOid oid = BookmarkTreeNode.oidFrom(candidatePP);
+        if(oid == null) {
+            // ignore
+            return;
+        }
+
+        BookmarkTreeNode rootNode = null;
+        for (BookmarkTreeNode eachNode : rootNodes) {
+            if(eachNode.matches(bookmarkableModel)) {
+                rootNode = eachNode;
+            }
+        }
+        // MRU/LRU algorithm
+        if(rootNode != null) {
+            rootNodes.remove(rootNode);
+            rootNodes.add(0, rootNode);
+            current = candidatePP;
+        } else {
+            if (bookmarkableModel.hasAsRootPolicy()) {
+                rootNode = BookmarkTreeNode.newRoot(bookmarkableModel);
+                rootNodes.add(0, rootNode);
+                current = candidatePP;
+            }
+        }
+
+        trim(rootNodes, getMaxSize());
+    }
+
+    private int getMaxSize() {
+        return getConfiguration().getInteger(MAX_SIZE_KEY, 
MAX_SIZE_DEFAULT_VALUE);
+    }
+
+    private static void trim(List<?> list, int requiredSize) {
+        int numToRetain = Math.min(list.size(), requiredSize);
+        list.retainAll(list.subList(0, numToRetain));
+    }
+
+    @Override
+    protected List<BookmarkTreeNode> load() {
+        List<BookmarkTreeNode> depthFirstGraph = Lists.newArrayList();
+
+        List<BookmarkTreeNode> sortedNodes = Lists.newArrayList(rootNodes);
+        Collections.sort(sortedNodes, COMPARATOR);
+
+        for (BookmarkTreeNode rootNode : sortedNodes) {
+            rootNode.appendGraphTo(depthFirstGraph);
+        }
+        return depthFirstGraph;
+    }
+
+    public boolean isCurrent(PageParameters pageParameters) {
+        return Objects.equal(current, pageParameters);
+    }
+
+    private static void cleanUpGarbage(List<BookmarkTreeNode> rootNodes) {
+        final Iterator<BookmarkTreeNode> iter = rootNodes.iterator();
+        while(iter.hasNext()) {
+            BookmarkTreeNode node = iter.next();
+            // think this is redundant...
+            if(node.getOidNoVer() == null) {
+                iter.remove();
+            }
+        }
+    }
+
+    public void clear() {
+        rootNodes.clear();
+    }
+
+    public boolean isEmpty() {
+        return rootNodes.isEmpty();
+    }
+
+    public void remove(BookmarkTreeNode rootNode) {
+        this.rootNodes.remove(rootNode);
+    }
+
+    public void remove(EntityModel entityModel) {
+        BookmarkTreeNode rootNode = null;
+        for (BookmarkTreeNode eachNode : rootNodes) {
+            
if(eachNode.getOidNoVerStr().equals((entityModel).getObjectAdapterMemento().toString()))
 {
+                rootNode = eachNode;
+            }
+        }
+        if(rootNode != null) {
+            rootNodes.remove(rootNode);
+        }
+    }
+    
+    // //////////////////////////////////////
+
+    
+    protected IsisConfiguration getConfiguration() {
+        return IsisContext.getConfiguration();
+    }
+
+
+    
+}

Reply via email to