From 0eab461918e7b8236beac5e7be71e579c55abc5a Mon Sep 17 00:00:00 2001
From: pkupczyk <pkupczyk>
Date: Wed, 6 Feb 2013 13:46:52 +0000
Subject: [PATCH] SP-422 / BIS-280 : Error when performing batch import of
 material

SVN: 28303
---
 .../ui/AbstractBatchRegistrationForm.java     | 266 ++++++++++++++++++
 .../generic/server/AbstractASyncAction.java   |  56 ++++
 .../dto/AsyncBatchRegistrationResult.java     |  49 ++++
 .../dto/MaterialBatchUpdateResultMessage.java |  59 ++++
 .../web/client/IGenericClientService.java     |   6 +-
 .../client/IGenericClientServiceAsync.java    |   9 +-
 .../client/application/GeneralImportForm.java | 224 +--------------
 ...AbstractMaterialBatchRegistrationForm.java | 192 +------------
 .../GenericMaterialBatchRegistrationForm.java |  12 +-
 .../GenericMaterialBatchUpdateForm.java       |  13 +-
 .../web/server/GenericClientService.java      |  61 ++--
 .../plugin/generic/server/GenericServer.java  | 121 +++++---
 .../generic/server/GenericServerLogger.java   |  26 +-
 .../plugin/generic/shared/IGenericServer.java |  17 ++
 .../web/server/GenericClientServiceTest.java  |   6 +-
 ...atchMaterialRegistrationAndUpdateTest.java |   6 +-
 16 files changed, 627 insertions(+), 496 deletions(-)
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/AbstractBatchRegistrationForm.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractASyncAction.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AsyncBatchRegistrationResult.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/MaterialBatchUpdateResultMessage.java

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/AbstractBatchRegistrationForm.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/AbstractBatchRegistrationForm.java
new file mode 100644
index 00000000000..3e80fa63a64
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/AbstractBatchRegistrationForm.java
@@ -0,0 +1,266 @@
+package ch.systemsx.cisd.openbis.generic.client.web.client.application.ui;
+
+import static ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DatabaseModificationAwareField.wrapUnaware;
+
+import java.util.List;
+
+import com.extjs.gxt.ui.client.Style.Scroll;
+import com.extjs.gxt.ui.client.event.BaseEvent;
+import com.extjs.gxt.ui.client.event.ButtonEvent;
+import com.extjs.gxt.ui.client.event.Events;
+import com.extjs.gxt.ui.client.event.FormEvent;
+import com.extjs.gxt.ui.client.event.Listener;
+import com.extjs.gxt.ui.client.event.SelectionListener;
+import com.extjs.gxt.ui.client.widget.form.CheckBox;
+import com.extjs.gxt.ui.client.widget.form.Field;
+import com.extjs.gxt.ui.client.widget.form.FileUploadField;
+import com.extjs.gxt.ui.client.widget.form.LabelField;
+import com.extjs.gxt.ui.client.widget.form.TextField;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.AbstractImagePrototype;
+
+import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientServiceAsync;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.Dict;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.FormPanelListener;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.GenericConstants;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.LinkRenderer;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.file.BasicFileFieldManager;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget.FieldUtil;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.WindowUtils;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BatchRegistrationResult;
+
+public abstract class AbstractBatchRegistrationForm extends AbstractRegistrationForm
+{
+    public final class BatchRegistrationCallback extends
+            AbstractRegistrationForm.AbstractRegistrationCallback<List<BatchRegistrationResult>>
+    {
+        public BatchRegistrationCallback(final IViewContext<?> viewContext)
+        {
+            super(viewContext);
+        }
+
+        @Override
+        protected String createSuccessfullRegistrationInfo(
+                final List<BatchRegistrationResult> result)
+        {
+            final StringBuilder builder = new StringBuilder();
+            for (final BatchRegistrationResult batchRegistrationResult : result)
+            {
+                builder.append("<b>" + batchRegistrationResult.getFileName() + "</b>: ");
+                builder.append(batchRegistrationResult.getMessage());
+                builder.append("<br />");
+            }
+            return builder.toString();
+        }
+    }
+
+    private static final String FIELD_LABEL_TEMPLATE = "File";
+
+    private static final int NUMBER_OF_FIELDS = 1;
+
+    protected final String sessionKey;
+
+    protected final LabelField templateField;
+
+    protected final BasicFileFieldManager fileFieldsManager;
+
+    protected final TextField<String> emailField;
+
+    protected final CheckBox asynchronous;
+
+    protected final IViewContext<ICommonClientServiceAsync> viewContext;
+
+    public AbstractBatchRegistrationForm(IViewContext<ICommonClientServiceAsync> viewContext,
+            String id, String sessionKey)
+    {
+        super(viewContext, id);
+        setResetButtonVisible(true);
+        this.sessionKey = sessionKey;
+        this.viewContext = viewContext;
+        setScrollMode(Scroll.AUTO);
+        asynchronous = createEmailCheckBox();
+        emailField =
+                createEmailField(viewContext.getModel().getSessionContext().getUser()
+                        .getUserEmail());
+        templateField = createTemplateField();
+        fileFieldsManager =
+                new BasicFileFieldManager(sessionKey, NUMBER_OF_FIELDS, FIELD_LABEL_TEMPLATE);
+        fileFieldsManager.setMandatory();
+        addUploadFeatures(sessionKey);
+    }
+
+    @Override
+    protected final void onRender(final Element target, final int index)
+    {
+        super.onRender(target, index);
+        addFormFields();
+    }
+
+    private CheckBox createEmailCheckBox()
+    {
+        final CheckBox checkBox = new CheckBox();
+        checkBox.setFieldLabel("Send confirmation?");
+        checkBox.setBoxLabel("");
+        checkBox.setValue(true);
+        checkBox.addListener(Events.Change, new Listener<BaseEvent>()
+            {
+                @Override
+                public void handleEvent(BaseEvent be)
+                {
+                    if (checkBox.getValue())
+                    {
+                        formPanel.remove(asynchronous);
+                        for (FileUploadField attachmentField : fileFieldsManager.getFields())
+                        {
+                            formPanel.remove(wrapUnaware((Field<?>) attachmentField).get());
+                        }
+                        addOnlyFormFields(true);
+                    } else
+                    {
+                        formPanel.remove(emailField);
+                    }
+                    formPanel.layout();
+                }
+            });
+        return checkBox;
+    }
+
+    private TextField<String> createEmailField(String userEmail)
+    {
+        TextField<String> field = new TextField<String>();
+        field.setAllowBlank(false);
+        field.setFieldLabel("Email");
+        FieldUtil.markAsMandatory(field);
+        field.setValue(userEmail);
+        field.setValidateOnBlur(true);
+        field.setRegex(GenericConstants.EMAIL_REGEX);
+        field.getMessages().setRegexText("Expected email address format: user@domain.com");
+        AbstractImagePrototype infoIcon =
+                AbstractImagePrototype.create(viewContext.getImageBundle().getInfoIcon());
+        FieldUtil.addInfoIcon(field,
+                "All relevant notifications will be send to this email address",
+                infoIcon.createImage());
+        return field;
+    }
+
+    protected LabelField createTemplateField()
+    {
+        LabelField result =
+                new LabelField(LinkRenderer.renderAsLink(viewContext
+                        .getMessage(Dict.FILE_TEMPLATE_LABEL)));
+        result.sinkEvents(Event.ONCLICK);
+        result.addListener(Events.OnClick, new Listener<BaseEvent>()
+            {
+                @Override
+                public void handleEvent(BaseEvent be)
+                {
+
+                    WindowUtils.openWindow(createTemplateUrl());
+                }
+            });
+        return result;
+    }
+
+    protected String createTemplateUrl()
+    {
+        return null;
+    }
+
+    @Override
+    protected void submitValidForm()
+    {
+    }
+
+    @Override
+    protected void resetFieldsAfterSave()
+    {
+        for (FileUploadField attachmentField : fileFieldsManager.getFields())
+        {
+            attachmentField.reset();
+        }
+        updateDirtyCheckAfterSave();
+    }
+
+    protected void addOnlyFormFields(boolean forceAddEmailField)
+    {
+        formPanel.add(asynchronous);
+        if (forceAddEmailField || asynchronous.getValue())
+        {
+            formPanel.add(emailField);
+        }
+        if (templateField != null)
+        {
+            formPanel.add(templateField);
+        }
+        for (FileUploadField attachmentField : fileFieldsManager.getFields())
+        {
+            formPanel.add(wrapUnaware((Field<?>) attachmentField).get());
+        }
+    }
+
+    private final void addFormFields()
+    {
+        addOnlyFormFields(false);
+
+        formPanel.addListener(Events.BeforeSubmit, new Listener<FormEvent>()
+            {
+                @Override
+                public void handleEvent(FormEvent be)
+                {
+                    infoBox.displayProgress(messageProvider.getMessage(Dict.PROGRESS_UPLOADING));
+                }
+            });
+
+        formPanel.addListener(Events.Submit, new FormPanelListener(infoBox)
+            {
+                @Override
+                protected void onSuccessfullUpload()
+                {
+                    infoBox.displayProgress(messageProvider.getMessage(Dict.PROGRESS_PROCESSING));
+                    save();
+                }
+
+                @Override
+                protected void setUploadEnabled()
+                {
+                    AbstractBatchRegistrationForm.this.setUploadEnabled(true);
+                }
+            });
+        redefineSaveListeners();
+    }
+
+    void redefineSaveListeners()
+    {
+        saveButton.removeAllListeners();
+        addSaveButtonConfirmationListener();
+        saveButton.addSelectionListener(new SelectionListener<ButtonEvent>()
+            {
+                @Override
+                public final void componentSelected(final ButtonEvent ce)
+                {
+                    if (formPanel.isValid())
+                    {
+                        if (fileFieldsManager.filesDefined() > 0)
+                        {
+                            setUploadEnabled(false);
+                            formPanel.submit();
+                        } else
+                        {
+                            save();
+                        }
+                    }
+                }
+            });
+    }
+
+    protected abstract void save();
+
+    @Override
+    protected void setUploadEnabled(boolean enabled)
+    {
+        super.setUploadEnabled(enabled);
+        infoBoxResetListener.setEnabled(enabled);
+    }
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractASyncAction.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractASyncAction.java
new file mode 100644
index 00000000000..1a935057bcd
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractASyncAction.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013 ETH Zuerich, CISD
+ *
+ * Licensed 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 ch.systemsx.cisd.openbis.generic.server;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+
+/**
+ * @author pkupczyk
+ */
+public abstract class AbstractASyncAction implements IASyncAction
+{
+
+    @Override
+    public boolean doAction(Writer messageWriter)
+    {
+        try
+        {
+            doActionOrThrowException(messageWriter);
+        } catch (RuntimeException ex)
+        {
+            try
+            {
+                messageWriter.write(getName() + " has failed with a following exception: ");
+                messageWriter.write(ex.getMessage());
+                messageWriter.write("\n\nPlease correct the error or contact your administrator.");
+            } catch (IOException writingEx)
+            {
+                throw new UserFailureException(writingEx.getMessage()
+                        + " when trying to throw exception: " + ex.getMessage(), ex);
+            }
+            throw ex;
+        }
+        return true;
+
+    }
+
+    protected abstract void doActionOrThrowException(Writer messageWriter);
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AsyncBatchRegistrationResult.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AsyncBatchRegistrationResult.java
new file mode 100644
index 00000000000..7246e0361dc
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AsyncBatchRegistrationResult.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013 ETH Zuerich, CISD
+ *
+ * Licensed 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 ch.systemsx.cisd.openbis.generic.shared.basic.dto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author pkupczyk
+ */
+public class AsyncBatchRegistrationResult extends BatchRegistrationResult
+{
+
+    private static final long serialVersionUID = 1L;
+
+    // GWT
+    @SuppressWarnings("unused")
+    private AsyncBatchRegistrationResult()
+    {
+    }
+
+    public AsyncBatchRegistrationResult(String fileName)
+    {
+        super(fileName,
+                "When the import is complete the confirmation or failure report will be sent by email.");
+    }
+
+    public static final List<BatchRegistrationResult> singletonList(String fileName)
+    {
+        List<BatchRegistrationResult> list = new ArrayList<BatchRegistrationResult>();
+        list.add(new AsyncBatchRegistrationResult(fileName));
+        return list;
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/MaterialBatchUpdateResultMessage.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/MaterialBatchUpdateResultMessage.java
new file mode 100644
index 00000000000..919cb967089
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/MaterialBatchUpdateResultMessage.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2013 ETH Zuerich, CISD
+ *
+ * Licensed 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 ch.systemsx.cisd.openbis.generic.shared.basic.dto;
+
+import java.util.List;
+
+/**
+ * @author pkupczyk
+ */
+public class MaterialBatchUpdateResultMessage
+{
+
+    private String message;
+
+    public MaterialBatchUpdateResultMessage(List<NewMaterialsWithTypes> materials, int updateCount,
+            boolean ignoreUnregisteredMaterials)
+    {
+        message = updateCount + " material(s) updated";
+        if (ignoreUnregisteredMaterials)
+        {
+            int ignoredCount = -updateCount;
+            for (NewMaterialsWithTypes m : materials)
+            {
+                ignoredCount += m.getNewEntities().size();
+            }
+            if (ignoredCount > 0)
+            {
+                message += ", " + ignoredCount + " ignored.";
+            } else
+            {
+                message += ", non ignored.";
+            }
+        } else
+        {
+            message += ".";
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return message;
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/IGenericClientService.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/IGenericClientService.java
index a1d1d97c09d..bdd13b1c66f 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/IGenericClientService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/IGenericClientService.java
@@ -132,7 +132,8 @@ public interface IGenericClientService extends IClientService
      *            of throwing the exception and breaking the whole operation).
      */
     public List<BatchRegistrationResult> registerMaterials(final MaterialType materialType,
-            boolean updateExisting, final String sessionKey) throws UserFailureException;
+            boolean updateExisting, final String sessionKey, boolean async, String userEmail)
+            throws UserFailureException;
 
     /**
      * Updates materials from a file which has been previously uploaded.
@@ -141,7 +142,8 @@ public interface IGenericClientService extends IClientService
      *            be ignored if they are not already registered.
      */
     public List<BatchRegistrationResult> updateMaterials(MaterialType materialType,
-            String sessionKey, boolean ignoreUnregisteredMaterials) throws UserFailureException;
+            String sessionKey, boolean ignoreUnregisteredMaterials, boolean async, String userEmail)
+            throws UserFailureException;
 
     /**
      * Updates experiment.
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/IGenericClientServiceAsync.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/IGenericClientServiceAsync.java
index 96795032980..5466f324721 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/IGenericClientServiceAsync.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/IGenericClientServiceAsync.java
@@ -102,16 +102,17 @@ public interface IGenericClientServiceAsync extends IClientServiceAsync
             NewExperiment newExp, AsyncCallback<Void> assyncCallback) throws UserFailureException;
 
     /**
-     * @see IGenericClientService#registerMaterials(MaterialType, boolean, String)
+     * @see IGenericClientService#registerMaterials(MaterialType, boolean, String, boolean, String)
      */
     public void registerMaterials(MaterialType materialType, boolean updateExisting,
-            String sessionKey, final AsyncCallback<List<BatchRegistrationResult>> asyncCallback);
+            String sessionKey, boolean async, String userEmail,
+            final AsyncCallback<List<BatchRegistrationResult>> asyncCallback);
 
     /**
-     * @see IGenericClientService#updateMaterials(MaterialType, String, boolean)
+     * @see IGenericClientService#updateMaterials(MaterialType, String, boolean, boolean, String)
      */
     public void updateMaterials(MaterialType materialType, String sessionKey,
-            boolean ignoreUnregisteredMaterials,
+            boolean ignoreUnregisteredMaterials, boolean async, String userEmail,
             final AsyncCallback<List<BatchRegistrationResult>> asyncCallback);
 
     /**
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/GeneralImportForm.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/GeneralImportForm.java
index ec4f787fbbf..8a836c3a6d6 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/GeneralImportForm.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/GeneralImportForm.java
@@ -1,243 +1,35 @@
 package ch.systemsx.cisd.openbis.plugin.generic.client.web.client.application;
 
-import static ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DatabaseModificationAwareField.wrapUnaware;
+import com.extjs.gxt.ui.client.widget.form.LabelField;
 
-import java.util.List;
-
-import com.extjs.gxt.ui.client.Style.Scroll;
-import com.extjs.gxt.ui.client.event.BaseEvent;
-import com.extjs.gxt.ui.client.event.ButtonEvent;
-import com.extjs.gxt.ui.client.event.Events;
-import com.extjs.gxt.ui.client.event.FormEvent;
-import com.extjs.gxt.ui.client.event.Listener;
-import com.extjs.gxt.ui.client.event.SelectionListener;
-import com.extjs.gxt.ui.client.widget.form.CheckBox;
-import com.extjs.gxt.ui.client.widget.form.Field;
-import com.extjs.gxt.ui.client.widget.form.FileUploadField;
-import com.extjs.gxt.ui.client.widget.form.TextField;
-import com.google.gwt.user.client.Element;
-import com.google.gwt.user.client.ui.AbstractImagePrototype;
-
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.Dict;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.FormPanelListener;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.GenericConstants;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.AbstractRegistrationForm;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.file.BasicFileFieldManager;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget.FieldUtil;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BatchRegistrationResult;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.AbstractBatchRegistrationForm;
 import ch.systemsx.cisd.openbis.plugin.generic.client.web.client.IGenericClientServiceAsync;
 
-public class GeneralImportForm extends AbstractRegistrationForm
+public class GeneralImportForm extends AbstractBatchRegistrationForm
 {
-    protected final class RegisterOrUpdateSamplesAndMaterialsCallback extends
-            AbstractRegistrationForm.AbstractRegistrationCallback<List<BatchRegistrationResult>>
-    {
-        RegisterOrUpdateSamplesAndMaterialsCallback(
-                final IViewContext<IGenericClientServiceAsync> viewContext)
-        {
-            super(viewContext);
-        }
-
-        @Override
-        protected String createSuccessfullRegistrationInfo(
-                final List<BatchRegistrationResult> result)
-        {
-            final StringBuilder builder = new StringBuilder();
-            for (final BatchRegistrationResult batchRegistrationResult : result)
-            {
-                builder.append("<b>" + batchRegistrationResult.getFileName() + "</b>: ");
-                builder.append(batchRegistrationResult.getMessage());
-                builder.append("<br />");
-            }
-            return builder.toString();
-        }
-    }
-
-    private static final String FIELD_LABEL_TEMPLATE = "File";
-
-    private static final int NUMBER_OF_FIELDS = 1;
-
-    private final String sessionKey;
-
-    private final BasicFileFieldManager fileFieldsManager;
-
-    private final TextField<String> emailField;
 
-    private final CheckBox asynchronous;
+    protected final IViewContext<IGenericClientServiceAsync> genericViewContext;
 
-    private final IViewContext<IGenericClientServiceAsync> genericViewContext;
-
-    /**
-     * @param genericViewContext
-     * @param id
-     */
     public GeneralImportForm(IViewContext<IGenericClientServiceAsync> genericViewContext,
             String id, String sessionKey)
     {
-        super(genericViewContext, id);
-        setResetButtonVisible(true);
-        this.sessionKey = sessionKey;
+        super(genericViewContext.getCommonViewContext(), id, sessionKey);
         this.genericViewContext = genericViewContext;
-        setScrollMode(Scroll.AUTO);
-        asynchronous = createCheckBox();
-        emailField =
-                createEmailField(genericViewContext.getModel().getSessionContext().getUser()
-                        .getUserEmail());
-        fileFieldsManager =
-                new BasicFileFieldManager(sessionKey, NUMBER_OF_FIELDS, FIELD_LABEL_TEMPLATE);
-        fileFieldsManager.setMandatory();
-        addUploadFeatures(sessionKey);
     }
 
     @Override
-    protected final void onRender(final Element target, final int index)
-    {
-        super.onRender(target, index);
-        addFormFields();
-    }
-
-    private CheckBox createCheckBox()
-    {
-        final CheckBox checkBox = new CheckBox();
-        checkBox.setFieldLabel("Send confirmation?");
-        checkBox.setBoxLabel("");
-        checkBox.setValue(true);
-        checkBox.addListener(Events.Change, new Listener<BaseEvent>()
-            {
-                @Override
-                public void handleEvent(BaseEvent be)
-                {
-                    if (checkBox.getValue())
-                    {
-                        formPanel.remove(asynchronous);
-                        for (FileUploadField attachmentField : fileFieldsManager.getFields())
-                        {
-                            formPanel.remove(wrapUnaware((Field<?>) attachmentField).get());
-                        }
-                        addOnlyFormFields(true);
-                    } else
-                    {
-                        formPanel.remove(emailField);
-                    }
-                    formPanel.layout();
-                }
-            });
-        return checkBox;
-    }
-
-    private TextField<String> createEmailField(String userEmail)
+    protected LabelField createTemplateField()
     {
-        TextField<String> field = new TextField<String>();
-        field.setAllowBlank(false);
-        field.setFieldLabel("Email");
-        FieldUtil.markAsMandatory(field);
-        field.setValue(userEmail);
-        field.setValidateOnBlur(true);
-        field.setRegex(GenericConstants.EMAIL_REGEX);
-        field.getMessages().setRegexText("Expected email address format: user@domain.com");
-        AbstractImagePrototype infoIcon =
-                AbstractImagePrototype.create(genericViewContext.getImageBundle().getInfoIcon());
-        FieldUtil.addInfoIcon(field,
-                "All relevant notifications will be send to this email address",
-                infoIcon.createImage());
-        return field;
+        return null;
     }
 
     @Override
-    protected void submitValidForm()
-    {
-    }
-
-    @Override
-    protected void resetFieldsAfterSave()
-    {
-        for (FileUploadField attachmentField : fileFieldsManager.getFields())
-        {
-            attachmentField.reset();
-        }
-        updateDirtyCheckAfterSave();
-    }
-
-    private final void addOnlyFormFields(boolean forceAddEmailField)
-    {
-        formPanel.add(asynchronous);
-        if (forceAddEmailField || asynchronous.getValue())
-        {
-            formPanel.add(emailField);
-        }
-        for (FileUploadField attachmentField : fileFieldsManager.getFields())
-        {
-            formPanel.add(wrapUnaware((Field<?>) attachmentField).get());
-        }
-    }
-
-    private final void addFormFields()
-    {
-        addOnlyFormFields(false);
-
-        formPanel.addListener(Events.BeforeSubmit, new Listener<FormEvent>()
-            {
-                @Override
-                public void handleEvent(FormEvent be)
-                {
-                    infoBox.displayProgress(messageProvider.getMessage(Dict.PROGRESS_UPLOADING));
-                }
-            });
-
-        formPanel.addListener(Events.Submit, new FormPanelListener(infoBox)
-            {
-                @Override
-                protected void onSuccessfullUpload()
-                {
-                    infoBox.displayProgress(messageProvider.getMessage(Dict.PROGRESS_PROCESSING));
-                    save();
-                }
-
-                @Override
-                protected void setUploadEnabled()
-                {
-                    GeneralImportForm.this.setUploadEnabled(true);
-                }
-            });
-        redefineSaveListeners();
-    }
-
-    void redefineSaveListeners()
-    {
-        saveButton.removeAllListeners();
-        addSaveButtonConfirmationListener();
-        saveButton.addSelectionListener(new SelectionListener<ButtonEvent>()
-            {
-                @Override
-                public final void componentSelected(final ButtonEvent ce)
-                {
-                    if (formPanel.isValid())
-                    {
-                        if (fileFieldsManager.filesDefined() > 0)
-                        {
-                            setUploadEnabled(false);
-                            formPanel.submit();
-                        } else
-                        {
-                            save();
-                        }
-                    }
-                }
-            });
-    }
-
     protected void save()
     {
         genericViewContext.getService().registerOrUpdateSamplesAndMaterials(sessionKey, null, true,
                 asynchronous.getValue(), emailField.getValue(),
-                new RegisterOrUpdateSamplesAndMaterialsCallback(genericViewContext));
+                new BatchRegistrationCallback(genericViewContext));
     }
 
-    @Override
-    protected void setUploadEnabled(boolean enabled)
-    {
-        super.setUploadEnabled(enabled);
-        infoBoxResetListener.setEnabled(enabled);
-    }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/AbstractMaterialBatchRegistrationForm.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/AbstractMaterialBatchRegistrationForm.java
index cbafc87a185..d26069627c9 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/AbstractMaterialBatchRegistrationForm.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/AbstractMaterialBatchRegistrationForm.java
@@ -16,35 +16,11 @@
 
 package ch.systemsx.cisd.openbis.plugin.generic.client.web.client.application.material;
 
-import static ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DatabaseModificationAwareField.wrapUnaware;
-
-import java.util.List;
-
-import com.extjs.gxt.ui.client.Style.Scroll;
-import com.extjs.gxt.ui.client.event.BaseEvent;
-import com.extjs.gxt.ui.client.event.ButtonEvent;
-import com.extjs.gxt.ui.client.event.Events;
-import com.extjs.gxt.ui.client.event.FormEvent;
-import com.extjs.gxt.ui.client.event.Listener;
-import com.extjs.gxt.ui.client.event.SelectionListener;
-import com.extjs.gxt.ui.client.widget.form.Field;
-import com.extjs.gxt.ui.client.widget.form.FileUploadField;
-import com.extjs.gxt.ui.client.widget.form.FormPanel;
-import com.extjs.gxt.ui.client.widget.form.LabelField;
-import com.google.gwt.user.client.Element;
-import com.google.gwt.user.client.Event;
-
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.Dict;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.FormPanelListener;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.GenericConstants;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.UrlParamsHelper;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.LinkRenderer;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.AbstractRegistrationForm;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.file.BasicFileFieldManager;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.WindowUtils;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.AbstractBatchRegistrationForm;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BatchOperationKind;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BatchRegistrationResult;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.MaterialType;
 import ch.systemsx.cisd.openbis.plugin.generic.client.web.client.IGenericClientServiceAsync;
@@ -52,172 +28,30 @@ import ch.systemsx.cisd.openbis.plugin.generic.client.web.client.IGenericClientS
 /**
  * @author Franz-Josef Elmer
  */
-abstract class AbstractMaterialBatchRegistrationForm extends AbstractRegistrationForm
+abstract class AbstractMaterialBatchRegistrationForm extends AbstractBatchRegistrationForm
 {
-    private static final String FIELD_LABEL_TEMPLATE = "File";
-
-    private static final int NUMBER_OF_FIELDS = 1;
-
-    private static String createId(String sessionKey)
-    {
-        return GenericConstants.ID_PREFIX + sessionKey;
-    }
-
     protected final MaterialType materialType;
 
-    protected final IViewContext<IGenericClientServiceAsync> viewContext;
-
-    private final BasicFileFieldManager fileFieldsManager;
-
     private final BatchOperationKind batchOperationKind;
 
-    AbstractMaterialBatchRegistrationForm(IViewContext<IGenericClientServiceAsync> viewContext,
-            String sessionKey, BatchOperationKind batchOperationKind, MaterialType materialType)
+    protected final IViewContext<IGenericClientServiceAsync> genericViewContext;
+
+    AbstractMaterialBatchRegistrationForm(
+            IViewContext<IGenericClientServiceAsync> genericViewContext, String sessionKey,
+            BatchOperationKind batchOperationKind, MaterialType materialType)
     {
-        super(viewContext.getCommonViewContext(), createId(sessionKey));
-        this.viewContext = viewContext;
+        super(genericViewContext.getCommonViewContext(), GenericConstants.ID_PREFIX + sessionKey,
+                sessionKey);
+        this.genericViewContext = genericViewContext;
         this.batchOperationKind = batchOperationKind;
         this.materialType = materialType;
-        setScrollMode(Scroll.AUTO);
-        fileFieldsManager =
-                new BasicFileFieldManager(sessionKey, NUMBER_OF_FIELDS, FIELD_LABEL_TEMPLATE);
-        fileFieldsManager.setMandatory();
-        addUploadFeatures(sessionKey);
-    }
-
-    /**
-     * Adds additional fields to the form panel. File upload field will be added automatically after
-     * specific fields.
-     */
-    protected void addSpecificFormFields(FormPanel form)
-    {
-    }
-
-    /**
-     * Perform registration on the service
-     */
-    protected abstract void save();
-
-    @Override
-    protected void submitValidForm()
-    {
     }
 
     @Override
-    protected final void onRender(final Element target, final int index)
+    protected String createTemplateUrl()
     {
-        super.onRender(target, index);
-        addFormFields();
-    }
-
-    private final void addFormFields()
-    {
-        addSpecificFormFields(formPanel);
-        formPanel.add(createTemplateField());
-        for (FileUploadField attachmentField : fileFieldsManager.getFields())
-        {
-            formPanel.add(wrapUnaware((Field<?>) attachmentField).get());
-        }
-
-        formPanel.addListener(Events.BeforeSubmit, new Listener<FormEvent>()
-            {
-                @Override
-                public void handleEvent(FormEvent be)
-                {
-                    infoBox.displayProgress(messageProvider.getMessage(Dict.PROGRESS_UPLOADING));
-                }
-            });
-        formPanel.addListener(Events.Submit, new FormPanelListener(infoBox)
-            {
-                @Override
-                protected void onSuccessfullUpload()
-                {
-                    viewContext.log("Save in AbstractMaterialBatchRegistrationForm.addFormFields");
-                    infoBox.displayProgress(messageProvider.getMessage(Dict.PROGRESS_PROCESSING));
-                    save();
-                }
-
-                @Override
-                protected void setUploadEnabled()
-                {
-                    AbstractMaterialBatchRegistrationForm.this.setUploadEnabled(true);
-                }
-            });
-        redefineSaveListeners();
-    }
-
-    private LabelField createTemplateField()
-    {
-        LabelField result =
-                new LabelField(LinkRenderer.renderAsLink(viewContext
-                        .getMessage(Dict.FILE_TEMPLATE_LABEL)));
-        result.sinkEvents(Event.ONCLICK);
-        result.addListener(Events.OnClick, new Listener<BaseEvent>()
-            {
-                @Override
-                public void handleEvent(BaseEvent be)
-                {
-                    WindowUtils.openWindow(UrlParamsHelper.createTemplateURL(EntityKind.MATERIAL,
-                            materialType, false, false, batchOperationKind));
-                }
-            });
-        return result;
-    }
-
-    void redefineSaveListeners()
-    {
-        saveButton.removeAllListeners();
-        addSaveButtonConfirmationListener();
-        saveButton.addSelectionListener(new SelectionListener<ButtonEvent>()
-            {
-                @Override
-                public final void componentSelected(final ButtonEvent ce)
-                {
-                    if (formPanel.isValid())
-                    {
-                        if (fileFieldsManager.filesDefined() > 0)
-                        {
-                            setUploadEnabled(false);
-                            formPanel.submit();
-                        } else
-                        {
-                            viewContext
-                                    .log("Save in AbstractMaterialBatchRegistrationForm.redefineSaveListners");
-                            save();
-                        }
-                    }
-                }
-            });
-    }
-
-    @Override
-    protected void setUploadEnabled(boolean enabled)
-    {
-        super.setUploadEnabled(enabled);
-        infoBoxResetListener.setEnabled(enabled);
-    }
-
-    protected final class RegisterMaterialsCallback extends
-            AbstractRegistrationForm.AbstractRegistrationCallback<List<BatchRegistrationResult>>
-    {
-        RegisterMaterialsCallback(final IViewContext<IGenericClientServiceAsync> viewContext)
-        {
-            super(viewContext);
-        }
-
-        @Override
-        protected String createSuccessfullRegistrationInfo(
-                final List<BatchRegistrationResult> result)
-        {
-            final StringBuilder builder = new StringBuilder();
-            for (final BatchRegistrationResult batchRegistrationResult : result)
-            {
-                builder.append("<b>" + batchRegistrationResult.getFileName() + "</b>: ");
-                builder.append(batchRegistrationResult.getMessage());
-                builder.append("<br />");
-            }
-            return builder.toString();
-        }
+        return UrlParamsHelper.createTemplateURL(EntityKind.MATERIAL, materialType, false, false,
+                batchOperationKind);
     }
 
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/GenericMaterialBatchRegistrationForm.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/GenericMaterialBatchRegistrationForm.java
index ee39eed66f3..5dc42dc8d35 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/GenericMaterialBatchRegistrationForm.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/GenericMaterialBatchRegistrationForm.java
@@ -16,8 +16,6 @@
 
 package ch.systemsx.cisd.openbis.plugin.generic.client.web.client.application.material;
 
-import com.extjs.gxt.ui.client.widget.form.FormPanel;
-
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.Dict;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.GenericConstants;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
@@ -55,16 +53,18 @@ public final class GenericMaterialBatchRegistrationForm extends
     }
 
     @Override
-    protected void addSpecificFormFields(FormPanel form)
+    protected void addOnlyFormFields(boolean forceAddEmailField)
     {
-        form.add(updateExistingCheckbox);
+        formPanel.add(updateExistingCheckbox);
+        super.addOnlyFormFields(forceAddEmailField);
     }
 
     @Override
     protected void save()
     {
         boolean updateExisting = updateExistingCheckbox.getValue();
-        viewContext.getService().registerMaterials(materialType, updateExisting, SESSION_KEY,
-                new RegisterMaterialsCallback(viewContext));
+        genericViewContext.getService().registerMaterials(materialType, updateExisting,
+                SESSION_KEY, asynchronous.getValue(), emailField.getValue(),
+                new BatchRegistrationCallback(genericViewContext));
     }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/GenericMaterialBatchUpdateForm.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/GenericMaterialBatchUpdateForm.java
index f238ca76ab9..1d7c09da8c3 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/GenericMaterialBatchUpdateForm.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/material/GenericMaterialBatchUpdateForm.java
@@ -16,8 +16,6 @@
 
 package ch.systemsx.cisd.openbis.plugin.generic.client.web.client.application.material;
 
-import com.extjs.gxt.ui.client.widget.form.FormPanel;
-
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.Dict;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.field.CheckBoxField;
@@ -47,16 +45,17 @@ public class GenericMaterialBatchUpdateForm extends AbstractMaterialBatchRegistr
     }
 
     @Override
-    protected void addSpecificFormFields(FormPanel form)
+    protected void addOnlyFormFields(boolean forceAddEmailField)
     {
-        form.add(ignoreUnregisteredMaterialsCheckBox);
+        formPanel.add(ignoreUnregisteredMaterialsCheckBox);
+        super.addOnlyFormFields(forceAddEmailField);
     }
 
     @Override
     protected void save()
     {
-        viewContext.getService().updateMaterials(materialType, SESSION_KEY,
-                ignoreUnregisteredMaterialsCheckBox.getValue(),
-                new RegisterMaterialsCallback(viewContext));
+        genericViewContext.getService().updateMaterials(materialType, SESSION_KEY,
+                ignoreUnregisteredMaterialsCheckBox.getValue(), asynchronous.getValue(),
+                emailField.getValue(), new BatchRegistrationCallback(genericViewContext));
     }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientService.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientService.java
index 66323643ee1..ae900189d1f 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientService.java
@@ -41,6 +41,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.server.translator.UserFailure
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.db.exception.SampleUniqueCodeViolationExceptionAbstract;
 import ch.systemsx.cisd.openbis.generic.shared.IServer;
 import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AsyncBatchRegistrationResult;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BatchOperationKind;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BatchRegistrationResult;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetType;
@@ -52,6 +53,7 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExperimentUpdateResult;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExperimentUpdates;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExternalData;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityProperty;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.MaterialBatchUpdateResultMessage;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.MaterialType;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewAttachment;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewDataSetsWithTypes;
@@ -239,12 +241,9 @@ public class GenericClientService extends AbstractClientService implements IGene
                     genericServer.registerOrUpdateSamplesAndMaterialsAsync(sessionToken,
                             samplesInfo.getSamples(), materialsInfo.getMaterials(), userEmail);
                 }
-                List<BatchRegistrationResult> results = new ArrayList<BatchRegistrationResult>();
-                results.add(new BatchRegistrationResult(uploadedFiles.iterable().iterator().next()
-                        .getOriginalFilename(),
-                        "When the import is complete the confirmation or failure report will be sent by email."));
 
-                return results;
+                String fileName = uploadedFiles.iterable().iterator().next().getOriginalFilename();
+                return AsyncBatchRegistrationResult.singletonList(fileName);
             } else
             {
                 if (materialsInfo.getMaterials().isEmpty())
@@ -403,13 +402,23 @@ public class GenericClientService extends AbstractClientService implements IGene
 
     @Override
     public final List<BatchRegistrationResult> registerMaterials(final MaterialType materialType,
-            boolean updateExisting, final String sessionKey)
+            boolean updateExisting, final String sessionKey, boolean async, String userEmail)
     {
         String sessionToken = getSessionToken();
         BatchMaterialsOperation results =
                 parseMaterials(sessionKey, materialType, null, updateExisting);
+        String fileName = results.getResultList().get(0).getFileName();
         List<NewMaterialsWithTypes> materials = results.getMaterials();
-        genericServer.registerOrUpdateMaterials(sessionToken, materials);
+
+        if (async)
+        {
+            genericServer.registerOrUpdateMaterialsAsync(sessionToken, materials, userEmail);
+            return AsyncBatchRegistrationResult.singletonList(fileName);
+        } else
+        {
+            genericServer.registerOrUpdateMaterials(sessionToken, materials);
+        }
+
         return results.getResultList();
     }
 
@@ -428,35 +437,27 @@ public class GenericClientService extends AbstractClientService implements IGene
 
     @Override
     public List<BatchRegistrationResult> updateMaterials(MaterialType materialType,
-            String sessionKey, boolean ignoreUnregisteredMaterials)
+            String sessionKey, boolean ignoreUnregisteredMaterials, boolean async, String userEmail)
     {
         String sessionToken = getSessionToken();
-
         BatchMaterialsOperation results = parseMaterials(sessionKey, materialType, null, true);
-        int updateCount =
-                genericServer.updateMaterials(sessionToken, results.getMaterials(),
-                        ignoreUnregisteredMaterials);
-        String message = updateCount + " material(s) updated";
-        if (ignoreUnregisteredMaterials)
-        {
-            int ignoredCount = -updateCount;
-            for (NewMaterialsWithTypes m : results.getMaterials())
-            {
-                ignoredCount += m.getNewEntities().size();
-            }
-            if (ignoredCount > 0)
-            {
-                message += ", " + ignoredCount + " ignored.";
-            } else
-            {
-                message += ", non ignored.";
-            }
+        String fileName = results.getResultList().get(0).getFileName();
+
+        if (async)
+        {
+            genericServer.updateMaterialsAsync(sessionToken, results.getMaterials(),
+                    ignoreUnregisteredMaterials, userEmail);
+            return AsyncBatchRegistrationResult.singletonList(fileName);
         } else
         {
-            message += ".";
+            int updateCount =
+                    genericServer.updateMaterials(sessionToken, results.getMaterials(),
+                            ignoreUnregisteredMaterials);
+            MaterialBatchUpdateResultMessage message =
+                    new MaterialBatchUpdateResultMessage(results.getMaterials(), updateCount,
+                            ignoreUnregisteredMaterials);
+            return Arrays.asList(new BatchRegistrationResult(fileName, message.toString()));
         }
-        return Arrays.asList(new BatchRegistrationResult(results.getResultList().get(0)
-                .getFileName(), message));
     }
 
     private ExperimentLoader parseExperiments(String sessionKey)
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/server/GenericServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/server/GenericServer.java
index 64920eb3707..5983cb5bf2d 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/server/GenericServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/server/GenericServer.java
@@ -33,10 +33,11 @@ import org.springframework.stereotype.Component;
 
 import ch.rinn.restrictions.Private;
 import ch.systemsx.cisd.authentication.ISessionManager;
+import ch.systemsx.cisd.base.exceptions.CheckedExceptionTunnel;
 import ch.systemsx.cisd.common.exceptions.UserFailureException;
 import ch.systemsx.cisd.openbis.common.spring.IInvocationLoggerContext;
+import ch.systemsx.cisd.openbis.generic.server.AbstractASyncAction;
 import ch.systemsx.cisd.openbis.generic.server.AbstractServer;
-import ch.systemsx.cisd.openbis.generic.server.IASyncAction;
 import ch.systemsx.cisd.openbis.generic.server.MaterialHelper;
 import ch.systemsx.cisd.openbis.generic.server.authorization.annotation.AuthorizationGuard;
 import ch.systemsx.cisd.openbis.generic.server.authorization.annotation.Capability;
@@ -76,6 +77,7 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExternalData;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityProperty;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ListOrSearchSampleCriteria;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ListSampleCriteria;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.MaterialBatchUpdateResultMessage;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewAttachment;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewBasicExperiment;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewDataSet;
@@ -683,6 +685,45 @@ public final class GenericServer extends AbstractServer<IGenericServer> implemen
         return count;
     }
 
+    @Override
+    @RolesAllowed(RoleWithHierarchy.INSTANCE_ADMIN)
+    @Capability("WRITE_MATERIAL")
+    public void updateMaterialsAsync(final String sessionToken,
+            final List<NewMaterialsWithTypes> newMaterials,
+            final boolean ignoreUnregisteredMaterials, final String userEmail)
+            throws UserFailureException
+    {
+        assert sessionToken != null : "Unspecified session token.";
+        checkSession(sessionToken);
+
+        executeASync(userEmail, new AbstractASyncAction()
+            {
+                @Override
+                public String getName()
+                {
+                    return "Material Batch Update";
+                }
+
+                @Override
+                protected void doActionOrThrowException(Writer messageWriter)
+                {
+                    try
+                    {
+                        int updateCount =
+                                genericServer.updateMaterials(sessionToken, newMaterials,
+                                        ignoreUnregisteredMaterials);
+                        MaterialBatchUpdateResultMessage message =
+                                new MaterialBatchUpdateResultMessage(newMaterials, updateCount,
+                                        ignoreUnregisteredMaterials);
+                        messageWriter.write(message.toString());
+                    } catch (IOException e)
+                    {
+                        CheckedExceptionTunnel.wrapIfNecessary(e);
+                    }
+                }
+            });
+    }
+
     @Override
     @RolesAllowed(RoleWithHierarchy.SPACE_OBSERVER)
     public AttachmentWithContent getProjectFileAttachment(String sessionToken,
@@ -792,6 +833,32 @@ public final class GenericServer extends AbstractServer<IGenericServer> implemen
         }
     }
 
+    @Override
+    @RolesAllowed(RoleWithHierarchy.INSTANCE_ADMIN)
+    @Capability("WRITE_MATERIAL")
+    public void registerOrUpdateMaterialsAsync(final String sessionToken,
+            final List<NewMaterialsWithTypes> materials, final String userEmail)
+            throws UserFailureException
+    {
+        assert sessionToken != null : "Unspecified session token.";
+        checkSession(sessionToken);
+
+        executeASync(userEmail, new AbstractASyncAction()
+            {
+                @Override
+                public String getName()
+                {
+                    return "Material Batch Registration";
+                }
+
+                @Override
+                protected void doActionOrThrowException(Writer messageWriter)
+                {
+                    genericServer.registerOrUpdateMaterials(sessionToken, materials);
+                }
+            });
+    }
+
     @Override
     @RolesAllowed(RoleWithHierarchy.SPACE_USER)
     @Capability("WRITE_EXPERIMENT_SAMPLE")
@@ -957,7 +1024,7 @@ public final class GenericServer extends AbstractServer<IGenericServer> implemen
             final List<NewMaterialsWithTypes> newMaterialsWithType, String userEmail)
             throws UserFailureException
     {
-        executeASync(userEmail, new IASyncAction()
+        executeASync(userEmail, new AbstractASyncAction()
             {
                 @Override
                 public String getName()
@@ -966,29 +1033,10 @@ public final class GenericServer extends AbstractServer<IGenericServer> implemen
                 }
 
                 @Override
-                public boolean doAction(Writer messageWriter)
+                protected void doActionOrThrowException(Writer messageWriter)
                 {
-                    try
-                    {
-                        genericServer.registerOrUpdateSamplesAndMaterials(sessionToken,
-                                newSamplesWithType, newMaterialsWithType);
-                    } catch (RuntimeException ex)
-                    {
-                        try
-                        {
-                            messageWriter.write(getName()
-                                    + " has failed with a following exception: ");
-                            messageWriter.write(ex.getMessage());
-                            messageWriter
-                                    .write("\n\nPlease correct the error or contact your administrator.");
-                        } catch (IOException writingEx)
-                        {
-                            throw new UserFailureException(writingEx.getMessage()
-                                    + " when trying to throw exception: " + ex.getMessage(), ex);
-                        }
-                        throw ex;
-                    }
-                    return true;
+                    genericServer.registerOrUpdateSamplesAndMaterials(sessionToken,
+                            newSamplesWithType, newMaterialsWithType);
                 }
             });
     }
@@ -1001,7 +1049,7 @@ public final class GenericServer extends AbstractServer<IGenericServer> implemen
             final List<NewSamplesWithTypes> newSamplesWithType, String userEmail)
             throws UserFailureException
     {
-        executeASync(userEmail, new IASyncAction()
+        executeASync(userEmail, new AbstractASyncAction()
             {
                 @Override
                 public String getName()
@@ -1010,28 +1058,9 @@ public final class GenericServer extends AbstractServer<IGenericServer> implemen
                 }
 
                 @Override
-                public boolean doAction(Writer messageWriter)
+                protected void doActionOrThrowException(Writer messageWriter)
                 {
-                    try
-                    {
-                        genericServer.registerOrUpdateSamples(sessionToken, newSamplesWithType);
-                    } catch (RuntimeException ex)
-                    {
-                        try
-                        {
-                            messageWriter.write(getName()
-                                    + " has failed with a following exception: ");
-                            messageWriter.write(ex.getMessage());
-                            messageWriter
-                                    .write("\n\nPlease correct the error or contact your administrator.");
-                        } catch (IOException writingEx)
-                        {
-                            throw new UserFailureException(writingEx.getMessage()
-                                    + " when trying to throw exception: " + ex.getMessage(), ex);
-                        }
-                        throw ex;
-                    }
-                    return true;
+                    genericServer.registerOrUpdateSamples(sessionToken, newSamplesWithType);
                 }
             });
     }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/server/GenericServerLogger.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/server/GenericServerLogger.java
index 7bd50d63cee..8a751d9a928 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/server/GenericServerLogger.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/server/GenericServerLogger.java
@@ -120,10 +120,21 @@ final class GenericServerLogger extends AbstractServerLogger implements IGeneric
     public int updateMaterials(String sessionToken, List<NewMaterialsWithTypes> newMaterials,
             boolean ignoreUnregisteredMaterials) throws UserFailureException
     {
-        logTracking(sessionToken, "update_materials", getMaterials(newMaterials));
+        logTracking(sessionToken, "update_materials",
+                "MATERIALS(%S) IGNORE_UNREGISTERED_MATERIALS(%S)", getMaterials(newMaterials),
+                ignoreUnregisteredMaterials);
         return 0;
     }
 
+    @Override
+    public void updateMaterialsAsync(String sessionToken, List<NewMaterialsWithTypes> newMaterials,
+            boolean ignoreUnregisteredMaterials, String userEmail) throws UserFailureException
+    {
+        logTracking(sessionToken, "update_materials_async",
+                "MATERIALS(%S) IGNORE_UNREGISTERED_MATERIALS(%S) USER_EMAIL(%S)",
+                getMaterials(newMaterials), ignoreUnregisteredMaterials, userEmail);
+    }
+
     @Override
     public AttachmentWithContent getExperimentFileAttachment(final String sessionToken,
             final TechId experimentId, final String filename, final Integer versionOrNull)
@@ -223,6 +234,19 @@ final class GenericServerLogger extends AbstractServerLogger implements IGeneric
         }
     }
 
+    @Override
+    public void registerOrUpdateMaterialsAsync(String sessionToken,
+            List<NewMaterialsWithTypes> materials, String userEmail)
+    {
+        for (NewMaterialsWithTypes materialsWithType : materials)
+        {
+            logTracking(sessionToken, "registerOrUpdateMaterialsAsync",
+                    "type(%s) numberOfMaterials(%s) userEmail(%s)", materialsWithType
+                            .getEntityType().getCode(), materialsWithType.getNewEntities().size(),
+                    userEmail);
+        }
+    }
+
     @Override
     public void registerOrUpdateSamples(String sessionToken,
             List<NewSamplesWithTypes> newSamplesWithType) throws UserFailureException
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/shared/IGenericServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/shared/IGenericServer.java
index ec2afef2575..1f341e64b7d 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/shared/IGenericServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/shared/IGenericServer.java
@@ -193,6 +193,14 @@ public interface IGenericServer extends IServer
     public int updateMaterials(String sessionToken, List<NewMaterialsWithTypes> newMaterials,
             boolean ignoreUnregisteredMaterials) throws UserFailureException;
 
+    /**
+     * Asynchronously updates materials in batch.
+     */
+    @Transactional
+    @DatabaseCreateOrDeleteModification(value = ObjectKind.MATERIAL)
+    public void updateMaterialsAsync(String sessionToken, List<NewMaterialsWithTypes> newMaterials,
+            boolean ignoreUnregisteredMaterials, String userEmail) throws UserFailureException;
+
     /**
      * Registers new materials or if they exist updates in batch their properties (properties which
      * are not mentioned stay unchanged).
@@ -202,6 +210,15 @@ public interface IGenericServer extends IServer
     public void registerOrUpdateMaterials(String sessionToken, List<NewMaterialsWithTypes> materials)
             throws UserFailureException;
 
+    /**
+     * Asynchronously registers new materials or if they exist updates in batch their properties
+     * (properties which are not mentioned stay unchanged).
+     */
+    @Transactional
+    @DatabaseCreateOrDeleteModification(value = ObjectKind.MATERIAL)
+    public void registerOrUpdateMaterialsAsync(String sessionToken,
+            List<NewMaterialsWithTypes> materials, String userEmail) throws UserFailureException;
+
     /**
      * Returns attachment described by given sample identifier, filename and version.
      */
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientServiceTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientServiceTest.java
index 53d9b14ed5b..752d6d236cb 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientServiceTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientServiceTest.java
@@ -615,7 +615,7 @@ public final class GenericClientServiceTest extends AbstractClientServiceTest
 
         List<BatchRegistrationResult> results =
                 genericClientService.updateMaterials(MATERIAL_TYPE, sessionKey,
-                        ignoreUnregisteredMaterials);
+                        ignoreUnregisteredMaterials, false, null);
 
         assertEquals(1, results.size());
         assertEquals(updateCount + " material(s) updated.", results.get(0).getMessage());
@@ -632,7 +632,7 @@ public final class GenericClientServiceTest extends AbstractClientServiceTest
 
         List<BatchRegistrationResult> results =
                 genericClientService.updateMaterials(MATERIAL_TYPE, sessionKey,
-                        ignoreUnregisteredMaterials);
+                        ignoreUnregisteredMaterials, false, null);
 
         assertEquals(1, results.size());
         assertEquals(updateCount + " material(s) updated, non ignored.", results.get(0)
@@ -650,7 +650,7 @@ public final class GenericClientServiceTest extends AbstractClientServiceTest
 
         List<BatchRegistrationResult> results =
                 genericClientService.updateMaterials(MATERIAL_TYPE, sessionKey,
-                        ignoreUnregisteredMaterials);
+                        ignoreUnregisteredMaterials, false, null);
 
         assertEquals(1, results.size());
         assertEquals(updateCount + " material(s) updated, 1 ignored.", results.get(0).getMessage());
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/BatchMaterialRegistrationAndUpdateTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/BatchMaterialRegistrationAndUpdateTest.java
index e212fc98434..5260a073087 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/BatchMaterialRegistrationAndUpdateTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/BatchMaterialRegistrationAndUpdateTest.java
@@ -263,7 +263,8 @@ public class BatchMaterialRegistrationAndUpdateTest extends SystemTestCase
         uploadFile("my-file", materialBatchData);
         MaterialType materialType = new MaterialType();
         materialType.setCode(materialTypeCode);
-        return genericClientService.registerMaterials(materialType, false, SESSION_KEY);
+        return genericClientService
+                .registerMaterials(materialType, false, SESSION_KEY, false, null);
     }
 
     private List<BatchRegistrationResult> updateMaterials(String materialBatchData,
@@ -272,7 +273,8 @@ public class BatchMaterialRegistrationAndUpdateTest extends SystemTestCase
         uploadFile("my-file", materialBatchData);
         MaterialType materialType = new MaterialType();
         materialType.setCode(materialTypeCode);
-        return genericClientService.updateMaterials(materialType, SESSION_KEY, ignoreUnregistered);
+        return genericClientService.updateMaterials(materialType, SESSION_KEY, ignoreUnregistered,
+                false, null);
     }
 
 }
-- 
GitLab