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 2013/03/01 12:21:53 UTC

git commit: ISIS-351: concurrency exception on 'OK' now handled fine

Updated Branches:
  refs/heads/master 325504c40 -> e8b5d02e3


ISIS-351: concurrency exception on 'OK' now handled fine

If two users 'edit' the same object at the same time, then the first
user to save ('OK') succeeds fine.  For the second, when they hit
'OK', the change is detected, the updated data is reloaded, and an
error is shown via feedback is notified.


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

Branch: refs/heads/master
Commit: e8b5d02e3ac360d68d9f5b6249b90f5df9a17fcb
Parents: 325504c
Author: Dan Haywood <da...@apache.org>
Authored: Fri Mar 1 11:17:00 2013 +0000
Committer: Dan Haywood <da...@apache.org>
Committed: Fri Mar 1 11:17:00 2013 +0000

----------------------------------------------------------------------
 .../integration/wicket/WebRequestCycleForIsis.java |    2 +-
 .../wicket/ui/components/actions/ActionPanel.java  |   20 +-
 .../entity/properties/EntityPropertiesForm.java    |  169 +++++++++------
 .../viewer/wicket/ui/pages/error/ErrorPage.java    |   38 ++--
 .../ui/panels/AjaxButtonWithPreSubmitHook.java     |   29 ---
 .../wicket/ui/panels/ButtonWithPreSubmitHook.java  |   29 ---
 .../ui/panels/ButtonWithPreValidateHook.java       |   29 +++
 .../isis/viewer/wicket/ui/panels/FormAbstract.java |   23 ++-
 .../ui/panels/IFormSubmitterWithPreSubmitHook.java |   23 --
 .../panels/IFormSubmitterWithPreValidateHook.java  |   23 ++
 .../services/ServicesInjectorDefault.java          |   18 ++-
 .../container/DomainObjectContainerDefault.java    |   25 ++-
 ...omainObjectContainerDefaultTest_recognizes.java |   47 +++--
 .../adaptermanager/AdapterManagerDefault.java      |   17 +-
 14 files changed, 277 insertions(+), 215 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/component/viewer/wicket/impl/src/main/java/org/apache/isis/viewer/wicket/viewer/integration/wicket/WebRequestCycleForIsis.java
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/impl/src/main/java/org/apache/isis/viewer/wicket/viewer/integration/wicket/WebRequestCycleForIsis.java b/component/viewer/wicket/impl/src/main/java/org/apache/isis/viewer/wicket/viewer/integration/wicket/WebRequestCycleForIsis.java
index 94bb006..1da58cc 100644
--- a/component/viewer/wicket/impl/src/main/java/org/apache/isis/viewer/wicket/viewer/integration/wicket/WebRequestCycleForIsis.java
+++ b/component/viewer/wicket/impl/src/main/java/org/apache/isis/viewer/wicket/viewer/integration/wicket/WebRequestCycleForIsis.java
@@ -146,7 +146,7 @@ public class WebRequestCycleForIsis extends AbstractRequestCycleListener {
             exceptionRecognizers = Collections.emptyList();
         }
         String message = new ExceptionRecognizerComposite(exceptionRecognizers).recognize(ex);
-        final ErrorPage page = message != null ? new ErrorPage(message) : new ErrorPage(ex);
+        final ErrorPage page = message != null ? new ErrorPage(message, ex) : new ErrorPage(ex);
         return page;
     }
 

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/actions/ActionPanel.java
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/actions/ActionPanel.java b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/actions/ActionPanel.java
index a397f33..7f1d30d 100644
--- a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/actions/ActionPanel.java
+++ b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/actions/ActionPanel.java
@@ -213,25 +213,29 @@ public class ActionPanel extends PanelAbstract<ActionModel> implements ActionExe
     }
 
     private String recognizeException(RuntimeException ex, Component feedbackComponent) {
+        
+        // REVIEW: this code is similar to stuff in EntityPropertiesForm, perhaps move up to superclass?
+        // REVIEW: similar code also in WebRequestCycleForIsis; combine?
+        
         // see if the exception is recognized as being a non-serious error
         // (nb: similar code in WebRequestCycleForIsis, as a fallback)
         List<ExceptionRecognizer> exceptionRecognizers = getServicesInjector().lookupServices(ExceptionRecognizer.class);
-        String message = new ExceptionRecognizerComposite(exceptionRecognizers).recognize(ex);
-        if(message != null) {
+        String recognizedErrorIfAny = new ExceptionRecognizerComposite(exceptionRecognizers).recognize(ex);
+        if(recognizedErrorIfAny != null) {
+
             // recognized
             if(feedbackComponent != null) {
-                feedbackComponent.error(message);
-            } else {
-                 // use notification mechanism otherwise
-                 getMessageBroker().setApplicationError(message);
+                feedbackComponent.error(recognizedErrorIfAny);
             }
+            getMessageBroker().setApplicationError(recognizedErrorIfAny);
 
+            getTransactionManager().getTransaction().clearAbortCause();
+            
             // there's no need to abort the transaction, it will have already been done
             // (in IsisTransactionManager#executeWithinTransaction(...)).
-            getTransactionManager().getTransaction().clearAbortCause();
 
         }
-        return message;
+        return recognizedErrorIfAny;
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.java
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.java b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.java
index 347943f..cd2a027 100644
--- a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.java
+++ b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.java
@@ -47,8 +47,7 @@ import org.apache.isis.viewer.wicket.model.util.ObjectSpecifications;
 import org.apache.isis.viewer.wicket.ui.ComponentType;
 import org.apache.isis.viewer.wicket.ui.components.widgets.formcomponent.CancelHintRequired;
 import org.apache.isis.viewer.wicket.ui.notifications.JGrowlBehaviour;
-import org.apache.isis.viewer.wicket.ui.panels.AjaxButtonWithPreSubmitHook;
-import org.apache.isis.viewer.wicket.ui.panels.ButtonWithPreSubmitHook;
+import org.apache.isis.viewer.wicket.ui.panels.ButtonWithPreValidateHook;
 import org.apache.isis.viewer.wicket.ui.panels.FormAbstract;
 import org.apache.isis.viewer.wicket.ui.util.EvenOrOddCssClassAppenderFactory;
 import org.apache.wicket.Component;
@@ -65,9 +64,13 @@ import org.apache.wicket.markup.html.form.validation.AbstractFormValidator;
 import org.apache.wicket.markup.html.panel.ComponentFeedbackPanel;
 import org.apache.wicket.markup.html.panel.FeedbackPanel;
 import org.apache.wicket.markup.repeater.RepeatingView;
+import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.util.visit.IVisit;
 import org.apache.wicket.util.visit.IVisitor;
+import org.apache.wicket.validation.IValidatable;
+import org.apache.wicket.validation.IValidator;
+import org.apache.wicket.validation.ValidationError;
 
 class EntityPropertiesForm extends FormAbstract<ObjectAdapter> {
 
@@ -172,12 +175,15 @@ class EntityPropertiesForm extends FormAbstract<ObjectAdapter> {
 
             @Override
             public void validate() {
+
+                // same logic as in cancelButton; should this be factored out?
                 try {
                     getEntityModel().load(ConcurrencyChecking.CHECK);
                 } catch(ConcurrencyException ex) {
                     getMessageBroker().addMessage("Object changed by " + ex.getOid().getVersion().getUser() + ", automatically reloading");
                     getEntityModel().load(ConcurrencyChecking.NO_CHECK);
                 }
+                
                 super.validate();
             }
             
@@ -194,23 +200,52 @@ class EntityPropertiesForm extends FormAbstract<ObjectAdapter> {
         };
         add(editButton);
 
-        okButton = new ButtonWithPreSubmitHook(ID_OK_BUTTON, Model.of("OK")) {
+        
+        okButton = new ButtonWithPreValidateHook(ID_OK_BUTTON, Model.of("OK")) {
             private static final long serialVersionUID = 1L;
 
-            @Override
-            public void preSubmit() {
 
+            @Override
+            public String preValidate() {
+                // attempt to load with concurrency checking, catching recognized exceptions
                 try {
-                    getEntityModel().getObjectAdapterMemento().getObjectAdapter(ConcurrencyChecking.CHECK);
-                } catch(ConcurrencyException ex){
-                    // simplify this?
-                    Session.get().getFeedbackMessages().add(new FeedbackMessage(EntityPropertiesForm.this, ex.getMessage(), FeedbackMessage.ERROR));
+                    getEntityModel().load(ConcurrencyChecking.CHECK); // could have also just called #getObject(), since CHECK is the default
+
+                } catch(RuntimeException ex){
+                    String recognizedErrorMessage = recognizeException(ex);
+                    if(recognizedErrorMessage == null) {
+                        throw ex;
+                    }
+
+                    // reload
+                    getEntityModel().load(ConcurrencyChecking.NO_CHECK);
+                    
+                    getForm().clearInput();
+                    getEntityModel().resetPropertyModels();
+                    
+                    toViewMode(null);
+                    toEditMode(null);
+                    
+                    return recognizedErrorMessage;
                 }
+                
+                return null;
             }
 
             @Override
             public void validate() {
-                super.validate();
+
+                // add in any error message that we might have recognized from above
+                EntityPropertiesForm form = EntityPropertiesForm.this;
+                String preValidationErrorIfAny = form.getPreValidationErrorIfAny();
+                
+                if(preValidationErrorIfAny != null) {
+                    feedbackOrNotifyAnyRecognizedError(preValidationErrorIfAny, form);
+                    // skip validation, because would relate to old values
+                } else {
+                    // run Wicket's validation
+                    super.validate();
+                }
             }
             
             @Override
@@ -239,7 +274,11 @@ class EntityPropertiesForm extends FormAbstract<ObjectAdapter> {
                 try {
                     EntityPropertiesForm.this.getTransactionManager().flushTransaction();
                 } catch(RuntimeException ex) {
-                    String message = recognizeException(ex, EntityPropertiesForm.this);
+                    
+                    // There's no need to abort the transaction here, as it will have already been done
+                    // (in IsisTransactionManager#executeWithinTransaction(...)).
+
+                    String message = recognizeExceptionAndNotify(ex, EntityPropertiesForm.this);
                     if(message == null) {
                         throw ex;
                     }
@@ -258,8 +297,20 @@ class EntityPropertiesForm extends FormAbstract<ObjectAdapter> {
 
         };
         add(okButton);
+        
+        okButton.add(new IValidator<String>(){
+
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public void validate(IValidatable<String> validatable) {
+
 
-        cancelButton = new AjaxButtonWithPreSubmitHook(ID_CANCEL_BUTTON, Model.of("Cancel")) {
+                //validatable.error(new ValidationError("testing 1,2,3"));
+            }
+        });
+
+        cancelButton = new AjaxButton(ID_CANCEL_BUTTON, Model.of("Cancel")) {
             private static final long serialVersionUID = 1L;
             
             {
@@ -268,6 +319,8 @@ class EntityPropertiesForm extends FormAbstract<ObjectAdapter> {
 
             @Override
             public void validate() {
+
+                // same logic as in editButton; should this be factored out?
                 try {
                     getEntityModel().load(ConcurrencyChecking.CHECK);
                 } catch(ConcurrencyException ex) {
@@ -278,13 +331,6 @@ class EntityPropertiesForm extends FormAbstract<ObjectAdapter> {
             }
             
             @Override
-            public void preSubmit() {
-                
-                // NO LONGER WORKS...
-                // getEntityModel().getObjectAdapterMemento().getObjectAdapter(ConcurrencyChecking.NO_CHECK);
-            }
-
-            @Override
             protected void onSubmit(final AjaxRequestTarget target, final Form<?> form) {
                 Session.get().getFeedbackMessages().clear();
                 getForm().clearInput();
@@ -321,25 +367,37 @@ class EntityPropertiesForm extends FormAbstract<ObjectAdapter> {
         cancelButton.add(new JGrowlBehaviour());
     }
 
-    private String recognizeException(RuntimeException ex, Component feedbackComponent) {
+    private String recognizeExceptionAndNotify(RuntimeException ex, Component feedbackComponentIfAny) {
+        
         // see if the exception is recognized as being a non-serious error
-        // (nb: similar code in WebRequestCycleForIsis, as a fallback)
-        List<ExceptionRecognizer> exceptionRecognizers = getServicesInjector().lookupServices(ExceptionRecognizer.class);
-        String message = new ExceptionRecognizerComposite(exceptionRecognizers).recognize(ex);
-        if(message != null) {
-            // recognized
-            if(feedbackComponent != null) {
-                feedbackComponent.error(message);
-            } else {
-                 // use notification mechanism otherwise
-                 getMessageBroker().setApplicationError(message);
-            }
+        
+        String recognizedErrorMessageIfAny = recognizeException(ex);
+        feedbackOrNotifyAnyRecognizedError(recognizedErrorMessageIfAny, feedbackComponentIfAny);
 
-            // there's no need to abort the transaction, it will have already been done
-            // (in IsisTransactionManager#executeWithinTransaction(...)).
-            getTransactionManager().getTransaction().clearAbortCause();
+        return recognizedErrorMessageIfAny;
+    }
 
+    private void feedbackOrNotifyAnyRecognizedError(String recognizedErrorMessageIfAny, Component feedbackComponentIfAny) {
+        if(recognizedErrorMessageIfAny == null) {
+            return;
         }
+        
+        if(feedbackComponentIfAny != null) {
+            feedbackComponentIfAny.error(recognizedErrorMessageIfAny);
+        }
+        getMessageBroker().addWarning(recognizedErrorMessageIfAny);
+
+        // we clear the abort cause because we've handled rendering the exception
+        getTransactionManager().getTransaction().clearAbortCause();
+    }
+
+    private String recognizeException(RuntimeException ex) {
+        
+        // REVIEW: this code is similar to stuff in EntityPropertiesForm, perhaps move up to superclass?
+        // REVIEW: similar code also in WebRequestCycleForIsis; combine?
+        
+        final List<ExceptionRecognizer> exceptionRecognizers = getServicesInjector().lookupServices(ExceptionRecognizer.class);
+        final String message = new ExceptionRecognizerComposite(exceptionRecognizers).recognize(ex);
         return message;
     }
 
@@ -352,35 +410,22 @@ class EntityPropertiesForm extends FormAbstract<ObjectAdapter> {
     }
 
     private void addValidator() {
-        add(new AbstractFormValidator() {
 
-            private static final long serialVersionUID = 1L;
-
-            @Override
-            public FormComponent<?>[] getDependentFormComponents() {
-                return new FormComponent<?>[0];
-            }
-
-            @Override
-            public void validate(final Form<?> form) {
-                final EntityModel entityModel = (EntityModel) getModel();
-                String invalidReasonIfAny;
-                try {
-                    final ObjectAdapter adapter = entityModel.getObject();
-                    
-                    final ValidateObjectFacet facet = adapter.getSpecification().getFacet(ValidateObjectFacet.class);
-                    if (facet == null) {
-                        return;
-                    }
-                    invalidReasonIfAny = facet.invalidReason(adapter);
-                } catch(ConcurrencyException ex) {
-                    invalidReasonIfAny = ex.getMessage();
-                }
-                if (invalidReasonIfAny != null) {
-                    Session.get().getFeedbackMessages().add(new FeedbackMessage(form, invalidReasonIfAny, FeedbackMessage.ERROR));
-                }
-            }
-        });
+        // no longer used, instead using the PreValidate stuff.
+        
+//        add(new AbstractFormValidator() {
+//
+//            private static final long serialVersionUID = 1L;
+//
+//            @Override
+//            public FormComponent<?>[] getDependentFormComponents() {
+//                return new FormComponent<?>[0];
+//            }
+//
+//            @Override
+//            public void validate(final Form<?> form) {
+//            }
+//        });
     }
 
     private EntityModel getEntityModel() {

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/pages/error/ErrorPage.java
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/pages/error/ErrorPage.java b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/pages/error/ErrorPage.java
index 679aaf4..d91891b 100644
--- a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/pages/error/ErrorPage.java
+++ b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/pages/error/ErrorPage.java
@@ -22,7 +22,9 @@ package org.apache.isis.viewer.wicket.ui.pages.error;
 import java.io.Serializable;
 import java.util.List;
 
+import org.apache.isis.core.commons.lang.StringUtils;
 import org.apache.isis.viewer.wicket.ui.pages.PageAbstract;
+import org.apache.wicket.Component;
 import org.apache.wicket.MarkupContainer;
 import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation;
 import org.apache.wicket.behavior.AttributeAppender;
@@ -35,6 +37,7 @@ import org.apache.wicket.markup.html.list.ListView;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.JavaScriptResourceReference;
 
+import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
 import com.google.common.collect.Lists;
 
@@ -78,8 +81,8 @@ public class ErrorPage extends PageAbstract {
     /**
      * For recognized messages.
      */
-    public ErrorPage(String message) {
-        this(message, null, null);
+    public ErrorPage(String message, Exception ex) {
+        this(message, null, asStackTrace(ex));
     }
 
 
@@ -99,23 +102,20 @@ public class ErrorPage extends PageAbstract {
         MarkupContainer container = new WebMarkupContainer(ID_EXCEPTION_DETAIL);
         add(container);
         
-        if(exceptionMessage != null) {
-            container.add(new Label(ID_EXCEPTION_MESSAGE, exceptionMessage));
-            container.add(new ListView<Detail>(ID_STACK_TRACE_ELEMENT, stackTraceDetail) {
-                private static final long serialVersionUID = 1L;
-                
-                @Override
-                protected void populateItem(ListItem<Detail> item) {
-                    final Detail detail = item.getModelObject();
-                    Label label = new Label(ID_LINE, detail.line);
-                    item.add(new AttributeAppender("class", detail.type.name().toLowerCase()));
-                    item.add(label);
-                }
-            });
-        } else {
-            // don't bother adding children, they won't ever be rendered.
-            container.setVisible(false);
-        }
+        container.add(
+                new Label(ID_EXCEPTION_MESSAGE, Strings.nullToEmpty(exceptionMessage)).setVisible(exceptionMessage != null));
+        
+        container.add(new ListView<Detail>(ID_STACK_TRACE_ELEMENT, stackTraceDetail) {
+            private static final long serialVersionUID = 1L;
+            
+            @Override
+            protected void populateItem(ListItem<Detail> item) {
+                final Detail detail = item.getModelObject();
+                Label label = new Label(ID_LINE, detail.line);
+                item.add(new AttributeAppender("class", detail.type.name().toLowerCase()));
+                item.add(label);
+            }
+        });
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/AjaxButtonWithPreSubmitHook.java
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/AjaxButtonWithPreSubmitHook.java b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/AjaxButtonWithPreSubmitHook.java
deleted file mode 100644
index a0eaa74..0000000
--- a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/AjaxButtonWithPreSubmitHook.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- *  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.ui.panels;
-
-import org.apache.wicket.ajax.markup.html.form.AjaxButton;
-import org.apache.wicket.model.IModel;
-
-public abstract class AjaxButtonWithPreSubmitHook extends AjaxButton implements IFormSubmitterWithPreSubmitHook {
-    private static final long serialVersionUID = 1L;
-    public AjaxButtonWithPreSubmitHook(String id, IModel<String> model) {
-        super(id, model);
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/ButtonWithPreSubmitHook.java
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/ButtonWithPreSubmitHook.java b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/ButtonWithPreSubmitHook.java
deleted file mode 100644
index 4a17199..0000000
--- a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/ButtonWithPreSubmitHook.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- *  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.ui.panels;
-
-import org.apache.wicket.markup.html.form.Button;
-import org.apache.wicket.model.IModel;
-
-public abstract class ButtonWithPreSubmitHook extends Button implements IFormSubmitterWithPreSubmitHook {
-    private static final long serialVersionUID = 1L;
-    public ButtonWithPreSubmitHook(String id, IModel<String> model) {
-        super(id, model);
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/ButtonWithPreValidateHook.java
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/ButtonWithPreValidateHook.java b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/ButtonWithPreValidateHook.java
new file mode 100644
index 0000000..95cf7c7
--- /dev/null
+++ b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/ButtonWithPreValidateHook.java
@@ -0,0 +1,29 @@
+/*
+ *  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.ui.panels;
+
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.model.IModel;
+
+public abstract class ButtonWithPreValidateHook extends Button implements IFormSubmitterWithPreValidateHook {
+    private static final long serialVersionUID = 1L;
+    public ButtonWithPreValidateHook(String id, IModel<String> model) {
+        super(id, model);
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/FormAbstract.java
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/FormAbstract.java b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/FormAbstract.java
index 7bed893..625d46a 100644
--- a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/FormAbstract.java
+++ b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/FormAbstract.java
@@ -84,13 +84,28 @@ public abstract class FormAbstract<T> extends Form<T> implements IHeaderContribu
     // process() override
     // ///////////////////////////////////////////////////////////////////
 
+    private String preValidationErrorIfAny;
+    /**
+     * Temporarily made available during {@link #process(IFormSubmitter)},
+     * for the benefit of any form validation.
+     */
+    protected String getPreValidationErrorIfAny() {
+        return preValidationErrorIfAny;
+    }
+    
     @Override
     public void process(IFormSubmitter submittingComponent) {
-        if(submittingComponent instanceof IFormSubmitterWithPreSubmitHook) {
-            IFormSubmitterWithPreSubmitHook componentWithPreSubmitHook = (IFormSubmitterWithPreSubmitHook) submittingComponent;
-            componentWithPreSubmitHook.preSubmit();
+        try {
+            
+            if(submittingComponent instanceof IFormSubmitterWithPreValidateHook) {
+                IFormSubmitterWithPreValidateHook componentWithPreSubmitHook = (IFormSubmitterWithPreValidateHook) submittingComponent;
+                preValidationErrorIfAny = componentWithPreSubmitHook.preValidate();
+            }
+            super.process(submittingComponent);
+            
+        } finally {
+            preValidationErrorIfAny = null;
         }
-        super.process(submittingComponent);
     }
     
 

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/IFormSubmitterWithPreSubmitHook.java
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/IFormSubmitterWithPreSubmitHook.java b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/IFormSubmitterWithPreSubmitHook.java
deleted file mode 100644
index 4952087..0000000
--- a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/IFormSubmitterWithPreSubmitHook.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- *  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.ui.panels;
-
-public interface IFormSubmitterWithPreSubmitHook {
-    void preSubmit();
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/IFormSubmitterWithPreValidateHook.java
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/IFormSubmitterWithPreValidateHook.java b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/IFormSubmitterWithPreValidateHook.java
new file mode 100644
index 0000000..b5e3c21
--- /dev/null
+++ b/component/viewer/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/IFormSubmitterWithPreValidateHook.java
@@ -0,0 +1,23 @@
+/*
+ *  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.ui.panels;
+
+public interface IFormSubmitterWithPreValidateHook {
+    String preValidate();
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/ServicesInjectorDefault.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/ServicesInjectorDefault.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/ServicesInjectorDefault.java
index 46e047b..0b656a8 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/ServicesInjectorDefault.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/ServicesInjectorDefault.java
@@ -26,6 +26,7 @@ import static org.hamcrest.CoreMatchers.nullValue;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -228,16 +229,25 @@ public class ServicesInjectorDefault implements ServicesInjectorSpi {
     @SuppressWarnings("unchecked")
     @Override
     public <T> List<T> lookupServices(Class<T> serviceClass) {
-        locateAndCache(services, serviceClass);
+        locateAndCache(serviceClass);
         return (List<T>) servicesByType.get(serviceClass);
     };
 
-    private void locateAndCache(List<Object> services, Class<?> serviceClass) {
+    private void locateAndCache(Class<?> serviceClass) {
         if(servicesByType.containsKey(serviceClass)) {
            return; 
         }
 
-        servicesByType.put(serviceClass, Lists.newArrayList(Iterables.filter(services, ofType(serviceClass))));
+        List<Object> matchingServices = Lists.newArrayList();
+        addMatchingTo(serviceClass, services, matchingServices);
+        addMatchingTo(serviceClass, Collections.<Object>singletonList(container), matchingServices);
+        
+        servicesByType.put(serviceClass, matchingServices);
+    }
+
+    private void addMatchingTo(Class<?> type, List<Object> candidates, List<Object> filteredServicesAndContainer) {
+        Iterable<Object> filteredServices = Iterables.filter(candidates, ofType(type));
+        filteredServicesAndContainer.addAll(Lists.newArrayList(filteredServices));
     }
 
     private static final Predicate<Object> ofType(final Class<?> cls) {
@@ -249,6 +259,4 @@ public class ServicesInjectorDefault implements ServicesInjectorSpi {
         };
     }
 
-
-
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/container/DomainObjectContainerDefault.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/container/DomainObjectContainerDefault.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/container/DomainObjectContainerDefault.java
index b65a121..f1efbab 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/container/DomainObjectContainerDefault.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/container/DomainObjectContainerDefault.java
@@ -31,6 +31,7 @@ import org.apache.isis.applib.query.QueryFindAllInstances;
 import org.apache.isis.applib.security.RoleMemento;
 import org.apache.isis.applib.security.UserMemento;
 import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer;
+import org.apache.isis.applib.services.exceprecog.ExceptionRecognizerComposite;
 import org.apache.isis.applib.services.exceprecog.ExceptionRecognizerForType;
 import org.apache.isis.applib.services.exceprecog.ExceptionRecognizerGeneral;
 import org.apache.isis.applib.services.exceprecog.RecognizedException;
@@ -54,6 +55,7 @@ import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
 import org.apache.isis.core.metamodel.adapter.mgr.AdapterManagerAware;
 import org.apache.isis.core.metamodel.adapter.oid.AggregatedOid;
 import org.apache.isis.core.metamodel.adapter.util.AdapterUtils;
+import org.apache.isis.core.metamodel.adapter.version.ConcurrencyException;
 import org.apache.isis.core.metamodel.consent.InteractionResult;
 import org.apache.isis.core.metamodel.services.container.query.QueryFindByPattern;
 import org.apache.isis.core.metamodel.services.container.query.QueryFindByTitle;
@@ -438,22 +440,33 @@ public class  DomainObjectContainerDefault implements DomainObjectContainer, Que
 
     
     ///////////////////////////////////////////////////////////////
-    // ExceptionRecognitionService
+    // ExceptionRecognizer
     ///////////////////////////////////////////////////////////////
 
-    private final ExceptionRecognizer recogService = new ExceptionRecognizerForType(RecognizedException.class);
+    static class ExceptionRecognizerForConcurrencyException extends ExceptionRecognizerForType {
+        public ExceptionRecognizerForConcurrencyException() {
+            super(ConcurrencyException.class, prefix("Another user has just changed this data"));
+        }
+    }
+    
+    private final ExceptionRecognizer recognizer = 
+            new ExceptionRecognizerComposite(
+                    new ExceptionRecognizerForType(RecognizedException.class),
+                    new ExceptionRecognizerForConcurrencyException()
+                );
     
     /**
      * Framework-provided implementation of {@link ExceptionRecognizer},
-     * which will automatically recognize any {@link RecognizedException}.
+     * which will automatically recognize any {@link RecognizedException}s or
+     * any {@link ConcurrencyException}s.
      */
     @Override
     public String recognize(Throwable ex) {
-        return getRecogService().recognize(ex);
+        return getRecognizer().recognize(ex);
     }
     
-    ExceptionRecognizer getRecogService() {
-        return recogService;
+    ExceptionRecognizer getRecognizer() {
+        return recognizer;
     }
 
     

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/container/DomainObjectContainerDefaultTest_recognizes.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/container/DomainObjectContainerDefaultTest_recognizes.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/container/DomainObjectContainerDefaultTest_recognizes.java
index 9def9ff..6e6d07e 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/container/DomainObjectContainerDefaultTest_recognizes.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/container/DomainObjectContainerDefaultTest_recognizes.java
@@ -1,49 +1,54 @@
 package org.apache.isis.core.metamodel.services.container;
 
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.junit.Assert.assertThat;
 
-import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer;
 import org.apache.isis.applib.services.exceprecog.RecognizedException;
-import org.apache.isis.core.unittestsupport.jmock.auto.Mock;
+import org.apache.isis.core.metamodel.adapter.oid.RootOidDefault;
+import org.apache.isis.core.metamodel.adapter.version.ConcurrencyException;
+import org.apache.isis.core.metamodel.spec.ObjectSpecId;
 import org.apache.isis.core.unittestsupport.jmocking.JUnitRuleMockery2;
 import org.apache.isis.core.unittestsupport.jmocking.JUnitRuleMockery2.Mode;
-import org.jmock.Expectations;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
 public class DomainObjectContainerDefaultTest_recognizes {
 
+    static class SomeRandomException extends Exception {
+        private static final long serialVersionUID = 1L;
+    }
+
     @Rule
     public JUnitRuleMockery2 context = JUnitRuleMockery2.createFor(Mode.INTERFACES_AND_CLASSES);
 
-    @Mock
-    private ExceptionRecognizer mockERS;
-
-    private RecognizedException ex;
+    private Exception ex;
     
     private DomainObjectContainerDefault container;
     
     @Before
     public void setUp() throws Exception {
-        ex = new RecognizedException("foo");
-        container = new DomainObjectContainerDefault() {
-            @Override
-            ExceptionRecognizer getRecogService() {
-                return mockERS;
-            }
-        };
+        container = new DomainObjectContainerDefault();
     }
     
     @Test
-    public void delegates() throws Exception {
-        context.checking(new Expectations() {
-            {
-                one(mockERS).recognize(ex);
-            }
-        });
-        container.recognize(ex);
+    public void whenConcurrencyException_is_recognized() throws Exception {
+        ex = new ConcurrencyException("foo", RootOidDefault.create(ObjectSpecId.of("CUS"), "123"));
+        assertThat(container.recognize(ex), is(not(nullValue())));
+    }
+
+    @Test
+    public void whenRecognizedException_is_recognized() throws Exception {
+        ex = new RecognizedException("foo");
+        assertThat(container.recognize(ex), is(not(nullValue())));
+    }
+
+    @Test
+    public void whenSomeRandomException_is_not_recognized() throws Exception {
+        ex = new SomeRandomException();
+        assertThat(container.recognize(ex), is(nullValue()));
     }
+    
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/e8b5d02e/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/adaptermanager/AdapterManagerDefault.java
----------------------------------------------------------------------
diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/adaptermanager/AdapterManagerDefault.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/adaptermanager/AdapterManagerDefault.java
index c87cd7b..9f2c7f6 100644
--- a/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/adaptermanager/AdapterManagerDefault.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/adaptermanager/AdapterManagerDefault.java
@@ -293,14 +293,14 @@ public class AdapterManagerDefault implements AdapterManagerSpi {
 
         // attempt to locate adapter for the Oid
         ObjectAdapter adapter = getAdapterFor(typedOid);
-        if (adapter != null) {
-            return adapter;
-        } 
-        
-        final Object pojo = pojoRecreator.recreatePojo(typedOid);
-        adapter = mapRecreatedPojo(typedOid, pojo);
-        
-        final Oid adapterOid = adapter.getOid();
+        if (adapter == null) {
+            // else recreate
+            final Object pojo = pojoRecreator.recreatePojo(typedOid);
+            adapter = mapRecreatedPojo(typedOid, pojo);
+        }
+
+        // sync versions of original, with concurrency checking if required
+        Oid adapterOid = adapter.getOid();
         if(adapterOid instanceof RootOid) {
             final RootOid recreatedOid = (RootOid) adapterOid;
             final RootOid originalOid = (RootOid) typedOid;
@@ -312,6 +312,7 @@ public class AdapterManagerDefault implements AdapterManagerSpi {
                 originalOid.setVersion(recreatedOid.getVersion());
             }
         }
+
         return adapter;
     }