From 19b4cf7fcf4009e81191dcd26223098f7c0e8916 Mon Sep 17 00:00:00 2001
From: buczekp <buczekp>
Date: Tue, 8 Jun 2010 12:58:18 +0000
Subject: [PATCH] [LMS-1536] added permlinks to attachments; fixed dependencies

SVN: 16339
---
 .../client/web/client/application/Client.java |  12 +-
 .../client/web/client/application/Dict.java   |   2 +
 .../locator/AbstractViewLocatorResolver.java  |  20 +++
 .../AttachmentDownloadLocatorResolver.java    | 136 ++++++++++++++++++
 .../locator/BrowserLocatorResolver.java       |  15 --
 .../locator/PermlinkLocatorResolver.java      |  11 --
 .../locator/SearchLocatorResolver.java        |  23 +--
 .../application/locator/ViewLocator.java      |  12 +-
 .../ui/attachment/AttachmentBrowser.java      |  42 +++---
 .../attachment/AttachmentDownloadHelper.java  |  50 +++++++
 .../specific/AttachmentColDefKind.java        |   9 ++
 .../web/client/dto/AttachmentVersions.java    |   5 +
 .../openbis/generic/server/CommonServer.java  |   6 +-
 .../generic/shared/basic/BasicConstant.java   |   8 ++
 .../shared/basic/PermlinkUtilities.java       |  26 +++-
 .../shared/basic/SearchlinkUtilities.java     |  10 +-
 .../generic/shared/basic/dto/Attachment.java  |  13 +-
 .../basic/dto/BasicProjectIdentifier.java     |   2 +-
 .../shared/dto/AttachmentHolderPE.java        |  12 +-
 .../generic/shared/dto/ExperimentPE.java      |   5 +-
 .../openbis/generic/shared/dto/ProjectPE.java |  12 +-
 .../openbis/generic/shared/dto/SamplePE.java  |   5 +-
 .../translator/AttachmentTranslator.java      |  19 ++-
 .../translator/ExperimentTranslator.java      |   2 +-
 .../shared/translator/ProjectTranslator.java  |   2 +-
 .../shared/translator/SampleTranslator.java   |   3 +-
 .../experiment/GenericExperimentViewer.java   |   6 +-
 .../cisd/openbis/public/common-dictionary.js  |   1 +
 .../ViewLocatorResolverRegistryTest.java      |  30 ++++
 29 files changed, 389 insertions(+), 110 deletions(-)
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/AttachmentDownloadLocatorResolver.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/attachment/AttachmentDownloadHelper.java

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/Client.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/Client.java
index 58a032a8de9..59d2b57a9ac 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/Client.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/Client.java
@@ -35,6 +35,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientServiceAs
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.AppController;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.AppEvents;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.LoginController;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.AttachmentDownloadLocatorResolver;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.BrowserLocatorResolver;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.MaterialLocatorResolver;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.OpenViewAction;
@@ -50,6 +51,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IDele
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.WindowUtils;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ApplicationInfo;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.SessionContext;
+import ch.systemsx.cisd.openbis.generic.shared.basic.BasicConstant;
 
 /**
  * The {@link EntryPoint} implementation.
@@ -59,10 +61,6 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.dto.SessionContext;
  */
 public class Client implements EntryPoint, ValueChangeHandler<String>
 {
-    public static final String SIMPLE = "simple";
-
-    public static final String VIEW_MODE_KEY = "viewMode";
-
     /** name of the URL parameter which decides if looging is switched on or off */
     private static final String LOGGING_PARAM = "log";
 
@@ -113,8 +111,9 @@ public class Client implements EntryPoint, ValueChangeHandler<String>
 
     private boolean isSimpleMode()
     {
-        String viewModeParameter = Window.Location.getParameter(VIEW_MODE_KEY);
-        return viewModeParameter != null && viewModeParameter.equals(SIMPLE);
+        String viewModeParameter = Window.Location.getParameter(BasicConstant.VIEW_MODE_KEY);
+        return viewModeParameter != null
+                && viewModeParameter.equals(BasicConstant.VIEW_MODE_SIMPLE);
     }
 
     private boolean isLoggingEnabled()
@@ -305,6 +304,7 @@ public class Client implements EntryPoint, ValueChangeHandler<String>
         // as it handles the same action in a more specific way.
         handlerRegistry.registerHandler(new MaterialLocatorResolver(context));
         handlerRegistry.registerHandler(new ProjectLocatorResolver(context));
+        handlerRegistry.registerHandler(new AttachmentDownloadLocatorResolver(context));
         handlerRegistry.registerHandler(new PermlinkLocatorResolver(context));
 
         handlerRegistry.registerHandler(new SearchLocatorResolver(context));
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/Dict.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/Dict.java
index 25e25d47eed..7f0a51be3d8 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/Dict.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/Dict.java
@@ -829,6 +829,8 @@ public abstract class Dict
 
     public static final String BUTTON_FILTERS = "button_filters";
 
+    public static final String PERMLINK = "permlink_column_name";
+
     public static final String SHOW_DETAILS_LINK = "show_details_link_column_name";
 
     public static final String SHOW_DETAILS_LINK_TEXT_VALUE = "show_details_link_text_value";
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/AbstractViewLocatorResolver.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/AbstractViewLocatorResolver.java
index 32b7f779168..f8883f6ea98 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/AbstractViewLocatorResolver.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/AbstractViewLocatorResolver.java
@@ -1,6 +1,7 @@
 package ch.systemsx.cisd.openbis.generic.client.web.client.application.locator;
 
 import ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind;
 
 /**
  * Default implementation of the IViewLocatorHandler interface. Designed to be subclassed.
@@ -39,4 +40,23 @@ public abstract class AbstractViewLocatorResolver implements IViewLocatorResolve
             throw new UserFailureException("Missing URL parameter: " + parameter);
         }
     }
+
+    protected static final EntityKind getEntityKind(ViewLocator locator)
+    {
+        String entityKindValueOrNull = locator.tryGetEntity();
+        checkRequiredParameter(entityKindValueOrNull, ViewLocator.ENTITY_PARAMETER);
+        return getEntityKind(entityKindValueOrNull);
+    }
+
+    protected static final EntityKind getEntityKind(String entityKindValueOrNull)
+    {
+        try
+        {
+            return EntityKind.valueOf(entityKindValueOrNull);
+        } catch (IllegalArgumentException exception)
+        {
+            throw new UserFailureException("Invalid '" + ViewLocator.ENTITY_PARAMETER
+                    + "' URL parameter value.");
+        }
+    }
 }
\ No newline at end of file
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/AttachmentDownloadLocatorResolver.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/AttachmentDownloadLocatorResolver.java
new file mode 100644
index 00000000000..64fbc78c14b
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/AttachmentDownloadLocatorResolver.java
@@ -0,0 +1,136 @@
+package ch.systemsx.cisd.openbis.generic.client.web.client.application.locator;
+
+import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientServiceAsync;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.AbstractAsyncCallback;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.attachment.AttachmentDownloadHelper;
+import ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException;
+import ch.systemsx.cisd.openbis.generic.shared.basic.IAttachmentHolder;
+import ch.systemsx.cisd.openbis.generic.shared.basic.IEntityInformationHolder;
+import ch.systemsx.cisd.openbis.generic.shared.basic.PermlinkUtilities;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AttachmentHolderKind;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind;
+
+/**
+ * ViewLocatorHandler used for downloading attachment. We use permIds to identify attachment holder
+ * and we don't have permIds for projects so these locators work only for samples and experiments.
+ * 
+ * @author Piotr Buczek
+ */
+public class AttachmentDownloadLocatorResolver extends AbstractViewLocatorResolver
+{
+    private final IViewContext<ICommonClientServiceAsync> viewContext;
+
+    public AttachmentDownloadLocatorResolver(IViewContext<ICommonClientServiceAsync> viewContext)
+    {
+        super(PermlinkUtilities.DOWNLOAD_ATTACHMENT_ACTION);
+        this.viewContext = viewContext;
+    }
+
+    public void resolve(ViewLocator locator) throws UserFailureException
+    {
+        String entityKindValueOrNull = locator.tryGetEntity();
+        checkRequiredParameter(entityKindValueOrNull, ViewLocator.ENTITY_PARAMETER);
+        AttachmentHolderKind attachmentHolderKind = getAttachmentHolderKind(entityKindValueOrNull);
+
+        EntityKind entityKind = getEntityKind(locator);
+        // valid only for samples and experiments
+        String permIdValueOrNull =
+                locator.getParameters().get(PermlinkUtilities.PERM_ID_PARAMETER_KEY);
+        String fileNameOrNull = locator.getParameters().get(PermlinkUtilities.FILE_NAME_KEY);
+        String versionOrNull = locator.getParameters().get(PermlinkUtilities.VERSION_KEY);
+
+        checkRequiredParameter(permIdValueOrNull, PermlinkUtilities.PERM_ID_PARAMETER_KEY);
+        checkRequiredParameter(fileNameOrNull, PermlinkUtilities.FILE_NAME_KEY);
+        checkRequiredParameter(versionOrNull, PermlinkUtilities.VERSION_KEY);
+        try
+        {
+            int version = Integer.parseInt(versionOrNull);
+            downloadAttachment(entityKind, attachmentHolderKind, permIdValueOrNull, fileNameOrNull,
+                    version);
+        } catch (NumberFormatException e)
+        {
+            throw new UserFailureException("URL parameter '" + PermlinkUtilities.VERSION_KEY
+                    + "' value is expected to be a number.");
+        }
+    }
+
+    private void downloadAttachment(EntityKind entityKind,
+            AttachmentHolderKind attachmentHolderKind, String permId, String fileName, int version)
+    {
+        viewContext.getService()
+                .getEntityInformationHolder(
+                        entityKind,
+                        permId,
+                        new AttachmentDownloadCallback(viewContext, attachmentHolderKind, fileName,
+                                version));
+    }
+
+    private AttachmentHolderKind getAttachmentHolderKind(String entityKind)
+    {
+        try
+        {
+            AttachmentHolderKind holderKind = AttachmentHolderKind.valueOf(entityKind);
+            if (holderKind == AttachmentHolderKind.PROJECT)
+            {
+                throw new UserFailureException(
+                        "Download of attachments is not supported for projects.");
+            }
+            return holderKind;
+        } catch (IllegalArgumentException exception)
+        {
+            throw new UserFailureException("Invalid '" + ViewLocator.ENTITY_PARAMETER
+                    + "' URL parameter value.");
+        }
+    }
+
+    public class AttachmentDownloadCallback extends AbstractAsyncCallback<IEntityInformationHolder>
+    {
+
+        private final AttachmentHolderKind attachmentHolderKind;
+
+        private final String fileName;
+
+        private final int version;
+
+        private AttachmentDownloadCallback(final IViewContext<?> viewContext,
+                AttachmentHolderKind attachmentHolderKind, final String fileName, final int version)
+        {
+            super(viewContext);
+            this.attachmentHolderKind = attachmentHolderKind;
+            this.fileName = fileName;
+            this.version = version;
+        }
+
+        @Override
+        public void process(final IEntityInformationHolder result)
+        {
+            AttachmentDownloadHelper.download(fileName, version, createAttachmentHolder(result));
+        }
+
+        private IAttachmentHolder createAttachmentHolder(final IEntityInformationHolder result)
+        {
+            return new IAttachmentHolder()
+                {
+
+                    public AttachmentHolderKind getAttachmentHolderKind()
+                    {
+                        return attachmentHolderKind;
+                    }
+
+                    public Long getId()
+                    {
+                        return result.getId();
+                    }
+
+                    public String getCode()
+                    {
+                        return result.getCode();
+                    }
+
+                };
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/BrowserLocatorResolver.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/BrowserLocatorResolver.java
index 867c9dfb29e..e2dc3671132 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/BrowserLocatorResolver.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/BrowserLocatorResolver.java
@@ -5,7 +5,6 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewConte
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.ComponentProvider;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DispatcherHelper;
 import ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException;
-import ch.systemsx.cisd.openbis.generic.shared.basic.PermlinkUtilities;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind;
 
 /**
@@ -73,18 +72,4 @@ public class BrowserLocatorResolver extends AbstractViewLocatorResolver
         DispatcherHelper.dispatchNaviEvent(new ComponentProvider(viewContext).getMaterialBrowser());
     }
 
-    private EntityKind getEntityKind(ViewLocator locator)
-    {
-        try
-        {
-            String entityKindValueOrNull = locator.tryGetEntity();
-            checkRequiredParameter(entityKindValueOrNull,
-                    PermlinkUtilities.ENTITY_KIND_PARAMETER_KEY);
-            return EntityKind.valueOf(entityKindValueOrNull);
-        } catch (IllegalArgumentException exception)
-        {
-            throw new UserFailureException("Invalid '"
-                    + PermlinkUtilities.ENTITY_KIND_PARAMETER_KEY + "' URL parameter value.");
-        }
-    }
 }
\ No newline at end of file
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/PermlinkLocatorResolver.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/PermlinkLocatorResolver.java
index c69afd9eb8d..165b9420a56 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/PermlinkLocatorResolver.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/PermlinkLocatorResolver.java
@@ -50,15 +50,4 @@ public class PermlinkLocatorResolver extends AbstractViewLocatorResolver
         OpenEntityDetailsTabHelper.open(viewContext, entityKind, permIdValue);
     }
 
-    private EntityKind getEntityKind(String entityKindValueOrNull)
-    {
-        try
-        {
-            return EntityKind.valueOf(entityKindValueOrNull);
-        } catch (IllegalArgumentException exception)
-        {
-            throw new UserFailureException("Invalid '"
-                    + PermlinkUtilities.ENTITY_KIND_PARAMETER_KEY + "' URL parameter value.");
-        }
-    }
 }
\ No newline at end of file
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/SearchLocatorResolver.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/SearchLocatorResolver.java
index 6ca789f4b1b..05f2da34d0c 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/SearchLocatorResolver.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/SearchLocatorResolver.java
@@ -7,7 +7,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientServiceAs
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
 import ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException;
 import ch.systemsx.cisd.openbis.generic.shared.basic.AttributeSearchFieldKindProvider;
-import ch.systemsx.cisd.openbis.generic.shared.basic.PermlinkUtilities;
+import ch.systemsx.cisd.openbis.generic.shared.basic.SearchlinkUtilities;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DetailedSearchCriteria;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DetailedSearchCriterion;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DetailedSearchField;
@@ -22,7 +22,7 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SearchCriteriaConnectio
  */
 public class SearchLocatorResolver extends AbstractViewLocatorResolver
 {
-    public static final String SEARCH_ACTION = "SEARCH";
+    public static final String SEARCH_ACTION = SearchlinkUtilities.SEARCH_ACTION;
 
     protected static final String DEFAULT_SEARCH_STRING = "*";
 
@@ -49,12 +49,7 @@ public class SearchLocatorResolver extends AbstractViewLocatorResolver
     {
         // Extract the search criteria from the ViewLocator and dispatch to a resolver that can
         // handle the entity type.
-        String searchEntityKindValueOrNull = locator.tryGetEntity();
-        if (null == searchEntityKindValueOrNull)
-            return;
-
-        // Find the specific resolver based on entity type
-        EntityKind entityKind = getEntityKind(searchEntityKindValueOrNull);
+        EntityKind entityKind = getEntityKind(locator);
 
         DetailedSearchCriteria searchCriteria =
                 new ViewLocatorToDetailedSearchCriteriaConverter(locator, entityKind)
@@ -75,18 +70,6 @@ public class SearchLocatorResolver extends AbstractViewLocatorResolver
         }
     }
 
-    protected EntityKind getEntityKind(String entityKindValueOrNull)
-    {
-        try
-        {
-            return EntityKind.valueOf(entityKindValueOrNull);
-        } catch (IllegalArgumentException exception)
-        {
-            throw new UserFailureException("Invalid '"
-                    + PermlinkUtilities.ENTITY_KIND_PARAMETER_KEY + "' URL parameter value.");
-        }
-    }
-
     protected static class ViewLocatorToDetailedSearchCriteriaConverter
     {
         private final ViewLocator locator;
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/ViewLocator.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/ViewLocator.java
index 295f18ae18e..4aab5d665d0 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/ViewLocator.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/locator/ViewLocator.java
@@ -21,6 +21,7 @@ import java.util.HashMap;
 import java.util.Map;
 
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.StringUtils;
+import ch.systemsx.cisd.openbis.generic.shared.basic.BasicConstant;
 import ch.systemsx.cisd.openbis.generic.shared.basic.PermlinkUtilities;
 
 /**
@@ -41,10 +42,12 @@ public class ViewLocator
 
     private static final String PARAMETER_SEPARATOR = "&";
 
-    public static final String ACTION_PARAMETER = "ACTION";
+    public static final String ACTION_PARAMETER = BasicConstant.LOCATOR_ACTION_PARAMETER;
 
     public static final String ENTITY_PARAMETER = PermlinkUtilities.ENTITY_KIND_PARAMETER_KEY;
 
+    public static final String PERM_ID_PARAMETER = PermlinkUtilities.PERM_ID_PARAMETER_KEY;
+
     public static final String PERMLINK_ACTION = "VIEW";
 
     private static final String GWT_PARAMETER = "gwt.codesvr";
@@ -136,10 +139,13 @@ public class ViewLocator
             } else if (ENTITY_PARAMETER.equalsIgnoreCase(paramPair[0]))
             {
                 entityOrNull = paramPair[1];
-            } else if (PermlinkUtilities.PERM_ID_PARAMETER_KEY.equalsIgnoreCase(paramPair[0]))
+            } else if (PERM_ID_PARAMETER.equalsIgnoreCase(paramPair[0]))
             {
                 // Permlink URLs have an implied action
-                actionOrNull = PERMLINK_ACTION;
+                if (actionOrNull == null)
+                {
+                    actionOrNull = PERMLINK_ACTION;
+                }
                 parameters.put(paramPair[0], paramPair[1]);
             } else
             {
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/attachment/AttachmentBrowser.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/attachment/AttachmentBrowser.java
index 43440281eeb..6707bee9222 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/attachment/AttachmentBrowser.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/attachment/AttachmentBrowser.java
@@ -62,6 +62,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.columns
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.columns.specific.AttachmentColDefKind;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.field.DescriptionField;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.AbstractSimpleBrowserGrid;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ColumnDefsAndConfigs;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ColumnListener;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.IBrowserGridActionInvoker;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ICellListener;
@@ -69,7 +70,6 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ID
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget.AbstractRegistrationDialog;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IDelegatedAction;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IMessageProvider;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.WindowUtils;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.lang.StringEscapeUtils;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.AttachmentVersions;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DefaultResultSetConfig;
@@ -79,7 +79,6 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.GridRowModel;
 import ch.systemsx.cisd.openbis.generic.shared.basic.IAttachmentHolder;
 import ch.systemsx.cisd.openbis.generic.shared.basic.IColumnDefinition;
 import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
-import ch.systemsx.cisd.openbis.generic.shared.basic.URLMethodWithParameters;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Attachment;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AttachmentHolderKind;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatabaseModificationKind;
@@ -129,7 +128,7 @@ public class AttachmentBrowser extends AbstractSimpleBrowserGrid<AttachmentVersi
                             final String fileName = rowItem.getCurrent().getFileName();
                             final int version = rowItem.getCurrent().getVersion();
 
-                            downloadAttachment(fileName, version, attachmentHolder);
+                            AttachmentDownloadHelper.download(fileName, version, attachmentHolder);
                         }
                     });
         registerLinkClickListenerFor(AttachmentColDefKind.VERSION.id(),
@@ -147,6 +146,18 @@ public class AttachmentBrowser extends AbstractSimpleBrowserGrid<AttachmentVersi
                     });
     }
 
+    @Override
+    protected ColumnDefsAndConfigs<AttachmentVersions> createColumnsDefinition()
+    {
+        ColumnDefsAndConfigs<AttachmentVersions> schema = super.createColumnsDefinition();
+        if (attachmentHolder.getAttachmentHolderKind() != AttachmentHolderKind.PROJECT)
+        {
+            schema.setGridCellRendererFor(AttachmentColDefKind.PERMLINK.id(), LinkRenderer
+                    .createExternalLinkRenderer(viewContext.getMessage(Dict.PERMLINK)));
+        }
+        return schema;
+    }
+
     private static String createGridId(final IAttachmentHolder holder)
     {
         return createGridId(TechId.create(holder), holder.getAttachmentHolderKind());
@@ -225,7 +236,7 @@ public class AttachmentBrowser extends AbstractSimpleBrowserGrid<AttachmentVersi
                     AttachmentVersions versions = selectedItem.getBaseObject();
                     final String fileName = versions.getCurrent().getFileName();
                     final int version = versions.getCurrent().getVersion();
-                    downloadAttachment(fileName, version, attachmentHolder);
+                    AttachmentDownloadHelper.download(fileName, version, attachmentHolder);
                 }
             };
     }
@@ -277,26 +288,6 @@ public class AttachmentBrowser extends AbstractSimpleBrowserGrid<AttachmentVersi
         showVersionsPanel(fileName, versions, active);
     }
 
-    private static void downloadAttachment(String fileName, int version, IAttachmentHolder holder)
-    {
-        WindowUtils.openWindow(createURL(version, fileName, holder));
-    }
-
-    private final static String createURL(final int version, final String fileName,
-            final IAttachmentHolder attachmentHolder)
-    {
-        URLMethodWithParameters methodWithParameters =
-                new URLMethodWithParameters(GenericConstants.ATTACHMENT_DOWNLOAD_SERVLET_NAME);
-        methodWithParameters.addParameter(GenericConstants.VERSION_PARAMETER, version);
-        methodWithParameters.addParameter(GenericConstants.FILE_NAME_PARAMETER, fileName);
-        methodWithParameters.addParameter(GenericConstants.ATTACHMENT_HOLDER_PARAMETER,
-                attachmentHolder.getAttachmentHolderKind().name());
-        // NOTE: this exp.getId() could be null if exp is a proxy
-        methodWithParameters.addParameter(GenericConstants.TECH_ID_PARAMETER, attachmentHolder
-                .getId());
-        return methodWithParameters.toString();
-    }
-
     private void showVersionsPanel(final String fileName, final List<Attachment> versions,
             boolean inBackground)
     {
@@ -443,7 +434,8 @@ public class AttachmentBrowser extends AbstractSimpleBrowserGrid<AttachmentVersi
                                                 (Attachment) selectedItem
                                                         .get(ModelDataPropertyNames.OBJECT);
                                         int version = selectedAttachment.getVersion();
-                                        downloadAttachment(fileName, version, attachmentHolder);
+                                        AttachmentDownloadHelper.download(fileName, version,
+                                                attachmentHolder);
                                     }
                                     attachmentGrid.getSelectionModel().deselectAll();
                                 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/attachment/AttachmentDownloadHelper.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/attachment/AttachmentDownloadHelper.java
new file mode 100644
index 00000000000..acd394b49ea
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/attachment/AttachmentDownloadHelper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010 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.client.web.client.application.ui.attachment;
+
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.GenericConstants;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.WindowUtils;
+import ch.systemsx.cisd.openbis.generic.shared.basic.IAttachmentHolder;
+import ch.systemsx.cisd.openbis.generic.shared.basic.URLMethodWithParameters;
+
+/**
+ * Utility class for downloading attachments.
+ * 
+ * @author Piotr Buczek
+ */
+public class AttachmentDownloadHelper
+{
+    public static void download(String fileName, int version, IAttachmentHolder holder)
+    {
+        WindowUtils.openWindow(createURL(version, fileName, holder));
+    }
+
+    private final static String createURL(final int version, final String fileName,
+            final IAttachmentHolder attachmentHolder)
+    {
+        URLMethodWithParameters methodWithParameters =
+                new URLMethodWithParameters(GenericConstants.ATTACHMENT_DOWNLOAD_SERVLET_NAME);
+        methodWithParameters.addParameter(GenericConstants.VERSION_PARAMETER, version);
+        methodWithParameters.addParameter(GenericConstants.FILE_NAME_PARAMETER, fileName);
+        methodWithParameters.addParameter(GenericConstants.ATTACHMENT_HOLDER_PARAMETER,
+                attachmentHolder.getAttachmentHolderKind().name());
+        // NOTE: this exp.getId() could be null if exp is a proxy
+        methodWithParameters.addParameter(GenericConstants.TECH_ID_PARAMETER, attachmentHolder
+                .getId());
+        return methodWithParameters.toString();
+    }
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/columns/specific/AttachmentColDefKind.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/columns/specific/AttachmentColDefKind.java
index 2067ae41d1c..b4d616930e3 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/columns/specific/AttachmentColDefKind.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/columns/specific/AttachmentColDefKind.java
@@ -35,6 +35,15 @@ public enum AttachmentColDefKind implements IColumnDefinitionKind<AttachmentVers
             }
         }),
 
+    PERMLINK(new AbstractColumnDefinitionKind<AttachmentVersions>(Dict.PERMLINK)
+        {
+            @Override
+            public String tryGetValue(AttachmentVersions entity)
+            {
+                return entity.getPermlink();
+            }
+        }),
+
     VERSION(new AbstractColumnDefinitionKind<AttachmentVersions>(Dict.VERSION)
         {
             @Override
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/AttachmentVersions.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/AttachmentVersions.java
index 892765c1ea3..0ece01f7341 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/AttachmentVersions.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/AttachmentVersions.java
@@ -59,6 +59,11 @@ public class AttachmentVersions implements Comparable<AttachmentVersions>, IsSer
         return versions;
     }
 
+    public String getPermlink()
+    {
+        return getCurrent().getPermlink();
+    }
+
     // serialization
 
     @SuppressWarnings("unused")
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
index b7ab64635c1..bae6235f866 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
@@ -1175,7 +1175,7 @@ public final class CommonServer extends AbstractCommonServer<ICommonServer> impl
             IExperimentBO experimentBO = businessObjectFactory.createExperimentBO(session);
             experimentBO.loadDataByTechId(experimentId);
             return AttachmentTranslator.translate(listHolderAttachments(session, experimentBO
-                    .getExperiment()));
+                    .getExperiment()), session.getBaseIndexURL());
         } catch (final DataAccessException ex)
         {
             throw createUserFailureException(ex);
@@ -1190,7 +1190,7 @@ public final class CommonServer extends AbstractCommonServer<ICommonServer> impl
             ISampleBO sampleBO = businessObjectFactory.createSampleBO(session);
             sampleBO.loadDataByTechId(sampleId);
             return AttachmentTranslator.translate(listHolderAttachments(session, sampleBO
-                    .getSample()));
+                    .getSample()), session.getBaseIndexURL());
         } catch (final DataAccessException ex)
         {
             throw createUserFailureException(ex);
@@ -1205,7 +1205,7 @@ public final class CommonServer extends AbstractCommonServer<ICommonServer> impl
             IProjectBO projectBO = businessObjectFactory.createProjectBO(session);
             projectBO.loadDataByTechId(projectId);
             return AttachmentTranslator.translate(listHolderAttachments(session, projectBO
-                    .getProject()));
+                    .getProject()), null);
         } catch (final DataAccessException ex)
         {
             throw createUserFailureException(ex);
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/BasicConstant.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/BasicConstant.java
index 9a308a3ea24..5b793c119c0 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/BasicConstant.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/BasicConstant.java
@@ -58,6 +58,14 @@ public class BasicConstant
      */
     public static final String RENDERED_CANONICAL_DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss ZZZZ";
 
+    // constants used for link creation and handling
+
+    public static final String VIEW_MODE_KEY = "viewMode";
+
+    public static final String VIEW_MODE_SIMPLE = "simple";
+
+    public static final String LOCATOR_ACTION_PARAMETER = "action";
+
     private BasicConstant()
     {
     }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/PermlinkUtilities.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/PermlinkUtilities.java
index 6ea49d5d5e6..57707ae9cf8 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/PermlinkUtilities.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/PermlinkUtilities.java
@@ -16,7 +16,7 @@
 
 package ch.systemsx.cisd.openbis.generic.shared.basic;
 
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.Client;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AttachmentHolderKind;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind;
 
 /**
@@ -37,11 +37,33 @@ public class PermlinkUtilities
             final EntityKind entityKind, final String permId)
     {
         URLMethodWithParameters ulrWithParameters = new URLMethodWithParameters(baseIndexURL);
-        ulrWithParameters.addParameter(Client.VIEW_MODE_KEY, Client.SIMPLE);
+        ulrWithParameters.addParameter(BasicConstant.VIEW_MODE_KEY, BasicConstant.VIEW_MODE_SIMPLE);
         ulrWithParameters.startHistoryToken();
         ulrWithParameters.addParameter(ENTITY_KIND_PARAMETER_KEY, entityKind.name());
         ulrWithParameters.addParameter(PERM_ID_PARAMETER_KEY, permId);
         return ulrWithParameters.toString();
     }
 
+    public static final String DOWNLOAD_ATTACHMENT_ACTION = "DOWNLOAD_ATTACHMENT";
+
+    public static final String FILE_NAME_KEY = "file";
+
+    public static final String VERSION_KEY = "version";
+
+    public final static String createAttachmentPermlinkURL(final String baseIndexURL,
+            final String fileName, final int version, final AttachmentHolderKind entityKind,
+            final String permId)
+    {
+        URLMethodWithParameters ulrWithParameters = new URLMethodWithParameters(baseIndexURL);
+        ulrWithParameters.addParameter(BasicConstant.VIEW_MODE_KEY, BasicConstant.VIEW_MODE_SIMPLE);
+        ulrWithParameters.startHistoryToken();
+        ulrWithParameters.addParameter(BasicConstant.LOCATOR_ACTION_PARAMETER,
+                DOWNLOAD_ATTACHMENT_ACTION);
+        ulrWithParameters.addParameter(ENTITY_KIND_PARAMETER_KEY, entityKind.name());
+        ulrWithParameters.addParameter(PERM_ID_PARAMETER_KEY, permId);
+        ulrWithParameters.addParameter(FILE_NAME_KEY, fileName);
+        ulrWithParameters.addParameter(VERSION_KEY, version);
+        return ulrWithParameters.toString();
+    }
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/SearchlinkUtilities.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/SearchlinkUtilities.java
index cb86aacc3ad..294e542ee41 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/SearchlinkUtilities.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/SearchlinkUtilities.java
@@ -16,8 +16,6 @@
 
 package ch.systemsx.cisd.openbis.generic.shared.basic;
 
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.SearchLocatorResolver;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.ViewLocator;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind;
 
 /**
@@ -30,14 +28,16 @@ public class SearchlinkUtilities
     /** The HTTP URL parameter used to specify the entity identifier. */
     public static final String CODE_PARAMETER_KEY = "code";
 
+    public static final String SEARCH_ACTION = "SEARCH";
+
     public final static String createSearchlinkURL(final String baseIndexURL,
             final EntityKind entityKind, final String searchString)
     {
         URLMethodWithParameters ulrWithParameters = new URLMethodWithParameters(baseIndexURL);
         ulrWithParameters.startHistoryToken();
-        ulrWithParameters.addParameter(ViewLocator.ACTION_PARAMETER,
-                SearchLocatorResolver.SEARCH_ACTION);
-        ulrWithParameters.addParameter(ViewLocator.ENTITY_PARAMETER, entityKind.name());
+        ulrWithParameters.addParameter(BasicConstant.LOCATOR_ACTION_PARAMETER, SEARCH_ACTION);
+        ulrWithParameters.addParameter(PermlinkUtilities.ENTITY_KIND_PARAMETER_KEY, entityKind
+                .name());
         ulrWithParameters.addParameter(CODE_PARAMETER_KEY, searchString);
         return ulrWithParameters.toString();
     }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/Attachment.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/Attachment.java
index 29e640ec807..d1bd6f0305d 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/Attachment.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/Attachment.java
@@ -16,7 +16,6 @@
 
 package ch.systemsx.cisd.openbis.generic.shared.basic.dto;
 
-
 /**
  * GWT version of AttachmentPE
  * 
@@ -34,6 +33,8 @@ public class Attachment extends AbstractRegistrationHolder implements Comparable
 
     private int version;
 
+    private String permlink;
+
     public Attachment()
     {
     }
@@ -84,4 +85,14 @@ public class Attachment extends AbstractRegistrationHolder implements Comparable
         this.version = version;
     }
 
+    public String getPermlink()
+    {
+        return permlink;
+    }
+
+    public void setPermlink(String permlink)
+    {
+        this.permlink = permlink;
+    }
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/BasicProjectIdentifier.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/BasicProjectIdentifier.java
index 9bb4503b03f..6055de8a340 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/BasicProjectIdentifier.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/BasicProjectIdentifier.java
@@ -113,7 +113,7 @@ public class BasicProjectIdentifier implements IsSerializable, Serializable
     @Override
     public String toString()
     {
-        return instanceCode + "/" + spaceCode + "/" + projectCode;
+        return (instanceCode != null ? instanceCode : "") + "/" + spaceCode + "/" + projectCode;
     }
 
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/AttachmentHolderPE.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/AttachmentHolderPE.java
index ff195003b67..0f3c9ac8e7d 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/AttachmentHolderPE.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/AttachmentHolderPE.java
@@ -25,6 +25,8 @@ import javax.persistence.MappedSuperclass;
 import javax.persistence.Transient;
 
 import ch.systemsx.cisd.openbis.generic.shared.IServer;
+import ch.systemsx.cisd.openbis.generic.shared.basic.IPermIdHolder;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AttachmentHolderKind;
 import ch.systemsx.cisd.openbis.generic.shared.util.HibernateUtils;
 
 /**
@@ -33,7 +35,7 @@ import ch.systemsx.cisd.openbis.generic.shared.util.HibernateUtils;
  * @author Izabela Adamczyk
  */
 @MappedSuperclass
-public abstract class AttachmentHolderPE implements Serializable, IIdentifierHolder
+public abstract class AttachmentHolderPE implements Serializable, IIdentifierHolder, IPermIdHolder
 
 {
     //
@@ -66,7 +68,13 @@ public abstract class AttachmentHolderPE implements Serializable, IIdentifierHol
     abstract protected Set<AttachmentPE> getInternalAttachments();
 
     @Transient
-    abstract public String getHolderName();
+    abstract public AttachmentHolderKind getAttachmentHolderKind();
+
+    @Transient
+    public final String getHolderName()
+    {
+        return getAttachmentHolderKind().name().toLowerCase();
+    }
 
     //
     //
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentPE.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentPE.java
index a13d98210f6..7a63db90b72 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentPE.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentPE.java
@@ -65,6 +65,7 @@ import ch.systemsx.cisd.common.collections.UnmodifiableListDecorator;
 import ch.systemsx.cisd.common.collections.UnmodifiableSetDecorator;
 import ch.systemsx.cisd.common.utilities.ModifiedShortPrefixToStringStyle;
 import ch.systemsx.cisd.openbis.generic.shared.IServer;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AttachmentHolderKind;
 import ch.systemsx.cisd.openbis.generic.shared.dto.hibernate.SearchFieldConstants;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.IdentifierHelper;
@@ -514,9 +515,9 @@ public class ExperimentPE extends AttachmentHolderPE implements IEntityPropertie
 
     @Override
     @Transient
-    public String getHolderName()
+    public AttachmentHolderKind getAttachmentHolderKind()
     {
-        return "experiment";
+        return AttachmentHolderKind.EXPERIMENT;
     }
 
     @NotNull(message = ValidationMessages.CODE_NOT_NULL_MESSAGE)
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ProjectPE.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ProjectPE.java
index 932186c97a0..d2a4d5c743a 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ProjectPE.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ProjectPE.java
@@ -61,6 +61,7 @@ import ch.systemsx.cisd.common.collections.UnmodifiableListDecorator;
 import ch.systemsx.cisd.common.utilities.ModifiedShortPrefixToStringStyle;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.GenericConstants;
 import ch.systemsx.cisd.openbis.generic.shared.IServer;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AttachmentHolderKind;
 import ch.systemsx.cisd.openbis.generic.shared.dto.hibernate.SearchFieldConstants;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.IdentifierHelper;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ProjectIdentifier;
@@ -317,9 +318,16 @@ public final class ProjectPE extends AttachmentHolderPE implements Comparable<Pr
 
     @Override
     @Transient
-    public String getHolderName()
+    public AttachmentHolderKind getAttachmentHolderKind()
     {
-        return "project";
+        return AttachmentHolderKind.PROJECT;
+    }
+
+    @Transient
+    public String getPermId()
+    {
+        // fake permId containing project identifier
+        return getIdentifier();
     }
 
     @Override
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePE.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePE.java
index 4d544d44878..131d094c8ba 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePE.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePE.java
@@ -63,6 +63,7 @@ import org.hibernate.validator.Pattern;
 import ch.systemsx.cisd.common.collections.UnmodifiableSetDecorator;
 import ch.systemsx.cisd.common.utilities.ModifiedShortPrefixToStringStyle;
 import ch.systemsx.cisd.openbis.generic.shared.IServer;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AttachmentHolderKind;
 import ch.systemsx.cisd.openbis.generic.shared.dto.hibernate.SearchFieldConstants;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.IdentifierHelper;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier;
@@ -550,9 +551,9 @@ public class SamplePE extends AttachmentHolderPE implements IIdAndCodeHolder, Co
 
     @Override
     @Transient
-    public String getHolderName()
+    public AttachmentHolderKind getAttachmentHolderKind()
     {
-        return "sample";
+        return AttachmentHolderKind.SAMPLE;
     }
 
     @Override
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/AttachmentTranslator.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/AttachmentTranslator.java
index c5f434141fa..957da57de66 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/AttachmentTranslator.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/AttachmentTranslator.java
@@ -22,10 +22,12 @@ import java.util.List;
 
 import org.apache.commons.lang.StringEscapeUtils;
 
+import ch.systemsx.cisd.openbis.generic.shared.basic.PermlinkUtilities;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Attachment;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AttachmentWithContent;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewAttachment;
 import ch.systemsx.cisd.openbis.generic.shared.dto.AttachmentContentPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.AttachmentHolderPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.AttachmentPE;
 
 /**
@@ -41,13 +43,21 @@ public final class AttachmentTranslator
         // Can not be instantiated.
     }
 
-    public final static Attachment translate(final AttachmentPE attachment)
+    private final static Attachment translate(final AttachmentPE attachment,
+            final String baseIndexURL)
     {
         if (attachment == null)
         {
             return null;
         }
         final Attachment result = new Attachment();
+        final AttachmentHolderPE holder = attachment.getParent();
+        if (baseIndexURL != null)
+        {
+            result.setPermlink(PermlinkUtilities.createAttachmentPermlinkURL(baseIndexURL,
+                    attachment.getFileName(), attachment.getVersion(), holder
+                            .getAttachmentHolderKind(), holder.getPermId()));
+        }
         result.setFileName(StringEscapeUtils.escapeHtml(attachment.getFileName()));
         result.setTitle(StringEscapeUtils.escapeHtml(attachment.getTitle()));
         result.setDescription(StringEscapeUtils.escapeHtml(attachment.getDescription()));
@@ -74,7 +84,8 @@ public final class AttachmentTranslator
         return result;
     }
 
-    public final static List<Attachment> translate(final Collection<AttachmentPE> attachments)
+    public final static List<Attachment> translate(final Collection<AttachmentPE> attachments,
+            String baseIndexURL)
     {
         if (attachments == null)
         {
@@ -83,7 +94,7 @@ public final class AttachmentTranslator
         final List<Attachment> result = new ArrayList<Attachment>();
         for (final AttachmentPE attachment : attachments)
         {
-            result.add(translate(attachment));
+            result.add(translate(attachment, baseIndexURL));
         }
         return result;
     }
@@ -95,7 +106,7 @@ public final class AttachmentTranslator
         content.setValue(attachment.getContent());
         return createAttachmentPE(attachment, fileName, content);
     }
-    
+
     private static String getFileName(String filePath)
     {
         int lastIndexOfSeparator = filePath.replace('\\', '/').lastIndexOf('/');
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ExperimentTranslator.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ExperimentTranslator.java
index 9b6b219730a..d406f249924 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ExperimentTranslator.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ExperimentTranslator.java
@@ -80,7 +80,7 @@ public final class ExperimentTranslator
                     break;
                 case ATTACHMENTS:
                     result.setAttachments(AttachmentTranslator.translate(experiment
-                            .getAttachments()));
+                            .getAttachments(), baseIndexURL));
                     break;
                 default:
                     break;
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ProjectTranslator.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ProjectTranslator.java
index 06a2be81352..3d03bf6c93e 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ProjectTranslator.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ProjectTranslator.java
@@ -75,7 +75,7 @@ public final class ProjectTranslator
             attachments = DtoConverters.createUnmodifiableEmptyList();
         } else
         {
-            attachments = AttachmentTranslator.translate(project.getAttachments());
+            attachments = AttachmentTranslator.translate(project.getAttachments(), null);
         }
         result.setAttachments(attachments);
         return result;
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/SampleTranslator.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/SampleTranslator.java
index 6e3c40a54f4..58c9979a13f 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/SampleTranslator.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/SampleTranslator.java
@@ -113,7 +113,8 @@ public final class SampleTranslator
                 attachments = DtoConverters.createUnmodifiableEmptyList();
             } else
             {
-                attachments = AttachmentTranslator.translate(samplePE.getAttachments());
+                attachments =
+                        AttachmentTranslator.translate(samplePE.getAttachments(), baseIndexURL);
             }
             result.setAttachments(attachments);
         }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/experiment/GenericExperimentViewer.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/experiment/GenericExperimentViewer.java
index 99c5f6dbe39..f5fe64d478f 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/experiment/GenericExperimentViewer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/client/application/experiment/GenericExperimentViewer.java
@@ -69,7 +69,7 @@ public class GenericExperimentViewer extends AbstractViewer<Experiment> implemen
     protected final IIdAndCodeHolder experimentId;
 
     protected final BasicEntityType experimentType;
-    
+
     protected Experiment experiment;
 
     private ExperimentPropertiesPanel propertiesPanelOrNull;
@@ -204,8 +204,8 @@ public class GenericExperimentViewer extends AbstractViewer<Experiment> implemen
 
     private AttachmentVersionsSection createAttachmentsSection()
     {
-        final IAttachmentHolder newExperiment = asExperimentAttachmentHolder(experimentId);
-        return new AttachmentVersionsSection(viewContext.getCommonViewContext(), newExperiment);
+        final IAttachmentHolder attachmentHolder = asExperimentAttachmentHolder(experimentId);
+        return new AttachmentVersionsSection(viewContext.getCommonViewContext(), attachmentHolder);
     }
 
     private static IAttachmentHolder asExperimentAttachmentHolder(
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/public/common-dictionary.js b/openbis/source/java/ch/systemsx/cisd/openbis/public/common-dictionary.js
index 697173b0b65..fc5402ed5ea 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/public/common-dictionary.js
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/public/common-dictionary.js
@@ -29,6 +29,7 @@ var common = {
   load_in_progress: "Loading...",
   details_title: "{0} {1}",
   edit_title: "Edit {0} {1}",
+  permlink_column_name: "Permlink",
   show_details_link_column_name: "Show Details Link",
   show_details_link_text_value: "Permlink",
   table_operations: "Table:",
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/clientnonjs/ViewLocatorResolverRegistryTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/clientnonjs/ViewLocatorResolverRegistryTest.java
index cbd57b58dbd..a6aa7ace107 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/clientnonjs/ViewLocatorResolverRegistryTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/clientnonjs/ViewLocatorResolverRegistryTest.java
@@ -27,6 +27,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientServiceAs
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.AbstractAsyncCallback;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.AbstractViewLocatorResolver;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.AttachmentDownloadLocatorResolver;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.MaterialLocatorResolver;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.OpenViewAction;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.locator.PermlinkLocatorResolver;
@@ -270,6 +271,34 @@ public class ViewLocatorResolverRegistryTest extends AssertJUnit
         context.assertIsSatisfied();
     }
 
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testResolveDownloadAttachmentLocator()
+    {
+        initializeLocatorHandlerRegistry(registry);
+        final String SAMPLE_PERM_ID = "20100104150239401-871";
+        context.checking(new Expectations()
+            {
+                {
+                    allowing(viewContext).getService();
+                    will(returnValue(commonService));
+
+                    one(commonService).getEntityInformationHolder(with(EntityKind.SAMPLE),
+                            with(SAMPLE_PERM_ID),
+                            with(Expectations.any(AbstractAsyncCallback.class)));
+                }
+
+            });
+
+        ViewLocator locator =
+                new ViewLocator("action=DOWNLOAD_ATTACHMENT&entity=SAMPLE&permId=" + SAMPLE_PERM_ID
+                        + "&file=fileName.txt&version=1");
+        OpenViewAction action = new OpenViewAction(registry, locator);
+        action.execute();
+
+        context.assertIsSatisfied();
+    }
+
     @SuppressWarnings("unchecked")
     @Test
     public void testResolveSearchLocator()
@@ -349,6 +378,7 @@ public class ViewLocatorResolverRegistryTest extends AssertJUnit
     {
         handlerRegistry.registerHandler(new MaterialLocatorResolver(viewContext));
         handlerRegistry.registerHandler(new ProjectLocatorResolver(viewContext));
+        handlerRegistry.registerHandler(new AttachmentDownloadLocatorResolver(viewContext));
         handlerRegistry.registerHandler(new PermlinkLocatorResolver(viewContext));
         handlerRegistry.registerHandler(new SearchLocatorResolver(viewContext));
     }
-- 
GitLab