From c23e83d372105132cbe37368964818a4bed3ef3e Mon Sep 17 00:00:00 2001
From: alaskowski <alaskowski@ethz.ch>
Date: Wed, 12 Jul 2023 13:36:34 +0200
Subject: [PATCH] SSDM-55: Adding multivalued property handling for Samples

---
 .../generic/shared/api/v1/dto/Sample.java     |   7 +-
 .../UpdateEntityPropertyExecutor.java         | 393 ++++++++++++------
 .../property/PropertyTranslator.java          |  35 +-
 .../helper/AbstractXLSExportHelper.java       |   2 +-
 .../PropertyAssignmentImportHelper.java       |   3 +-
 .../helper/PropertyTypeImportHelper.java      |  81 +++-
 .../api/v1/sort/SampleSearchResultSorter.java |   1 +
 .../samplelister/SampleLister.java            |  80 ++--
 .../search/sort/IEntitySearchResult.java      |   1 +
 .../sort/SearchResultSorterByScore.java       |  17 +-
 .../generic/shared/dto/DataSetPropertyPE.java |   3 +-
 .../generic/shared/dto/SamplePropertyPE.java  |   3 +-
 .../sort/SearchResultSorterTestHelper.java    |   1 +
 13 files changed, 417 insertions(+), 210 deletions(-)

diff --git a/api-openbis-java/source/java/ch/systemsx/cisd/openbis/generic/shared/api/v1/dto/Sample.java b/api-openbis-java/source/java/ch/systemsx/cisd/openbis/generic/shared/api/v1/dto/Sample.java
index acf8d4ff29f..c233660629f 100644
--- a/api-openbis-java/source/java/ch/systemsx/cisd/openbis/generic/shared/api/v1/dto/Sample.java
+++ b/api-openbis-java/source/java/ch/systemsx/cisd/openbis/generic/shared/api/v1/dto/Sample.java
@@ -198,7 +198,12 @@ public final class Sample implements Serializable, IIdentifierHolder, IIdHolder
 
         public void putProperty(String propCode, String value)
         {
-            properties.put(propCode, value);
+            if(properties.containsKey(propCode)) {
+                properties.put(propCode, properties.get(propCode) + ", " + value);
+            } else
+            {
+                properties.put(propCode, value);
+            }
         }
 
         public List<Metaproject> getMetaprojects()
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/property/UpdateEntityPropertyExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/property/UpdateEntityPropertyExecutor.java
index cb5b2fb7c1b..59d6f11f15f 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/property/UpdateEntityPropertyExecutor.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/property/UpdateEntityPropertyExecutor.java
@@ -16,14 +16,8 @@
 package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.property;
 
 import java.io.Serializable;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.Map.Entry;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -77,7 +71,7 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
 
     @Autowired
     private IManagedPropertyEvaluatorFactory managedPropertyEvaluatorFactory;
-    
+
     @Autowired
     private IMapSampleByIdExecutor mapSampleByIdExecutor;
 
@@ -89,7 +83,8 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
     {
     }
 
-    public UpdateEntityPropertyExecutor(IDAOFactory daoFactory, IManagedPropertyEvaluatorFactory managedPropertyEvaluatorFactory)
+    public UpdateEntityPropertyExecutor(IDAOFactory daoFactory,
+            IManagedPropertyEvaluatorFactory managedPropertyEvaluatorFactory)
     {
         this.daoFactory = daoFactory;
         this.managedPropertyEvaluatorFactory = managedPropertyEvaluatorFactory;
@@ -99,7 +94,8 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
     public void update(final IOperationContext context,
             final MapBatch<? extends IPropertiesHolder, ? extends IEntityInformationWithPropertiesHolder> holderToEntityMap)
     {
-        final MapBatch<IEntityInformationWithPropertiesHolder, Map<String, Serializable>> entityToPropertiesMap =
+        final MapBatch<IEntityInformationWithPropertiesHolder, Map<String, Serializable>>
+                entityToPropertiesMap =
                 getEntityToPropertiesMap(holderToEntityMap);
 
         if (entityToPropertiesMap == null || entityToPropertiesMap.isEmpty())
@@ -107,117 +103,170 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
             return;
         }
 
-        MapBatch<IEntityInformationWithPropertiesHolder, Map<String, ISampleId>> entityToSamplePropertiesMap =
+        MapBatch<IEntityInformationWithPropertiesHolder, Map<String, ISampleId[]>>
+                entityToSamplePropertiesMap =
                 extractAndRemoveSampleProperties(holderToEntityMap, entityToPropertiesMap);
         updateSampleProps(context, entityToSamplePropertiesMap);
 
-        final Map<EntityKind, EntityPropertiesConverter> converters = new HashMap<EntityKind, EntityPropertiesConverter>();
+        final Map<EntityKind, EntityPropertiesConverter> converters =
+                new HashMap<EntityKind, EntityPropertiesConverter>();
 
-        new MapBatchProcessor<IEntityInformationWithPropertiesHolder, Map<String, Serializable>>(context, entityToPropertiesMap)
+        new MapBatchProcessor<IEntityInformationWithPropertiesHolder, Map<String, Serializable>>(
+                context, entityToPropertiesMap)
+        {
+            @Override
+            public void process(IEntityInformationWithPropertiesHolder propertiesHolder,
+                    Map<String, Serializable> properties)
             {
-                @Override
-                public void process(IEntityInformationWithPropertiesHolder propertiesHolder, Map<String, Serializable> properties)
-                {
-                    EntityKind entityKind = propertiesHolder.getEntityType().getEntityKind();
-
-                    if (converters.get(entityKind) == null)
-                    {
-                        EntityPropertiesConverter converter =
-                                new EntityPropertiesConverter(entityKind, daoFactory, entityInformationProvider, managedPropertyEvaluatorFactory);
-                        converters.put(entityKind, converter);
-                    }
-
-                    update(context, propertiesHolder, properties, converters.get(entityKind));
-                }
+                EntityKind entityKind = propertiesHolder.getEntityType().getEntityKind();
 
-                @Override
-                public IProgress createProgress(IEntityInformationWithPropertiesHolder propertiesHolder,
-                        Map<String, Serializable> properties,
-                        int objectIndex, int totalObjectCount)
+                if (converters.get(entityKind) == null)
                 {
-                    return new UpdatePropertyProgress(propertiesHolder, properties, objectIndex, totalObjectCount);
+                    EntityPropertiesConverter converter =
+                            new EntityPropertiesConverter(entityKind, daoFactory,
+                                    entityInformationProvider, managedPropertyEvaluatorFactory);
+                    converters.put(entityKind, converter);
                 }
-            };
+
+                update(context, propertiesHolder, properties, converters.get(entityKind));
+            }
+
+            @Override
+            public IProgress createProgress(IEntityInformationWithPropertiesHolder propertiesHolder,
+                    Map<String, Serializable> properties,
+                    int objectIndex, int totalObjectCount)
+            {
+                return new UpdatePropertyProgress(propertiesHolder, properties, objectIndex,
+                        totalObjectCount);
+            }
+        };
     }
 
     private void updateSampleProps(IOperationContext context,
-            MapBatch<IEntityInformationWithPropertiesHolder, Map<String, ISampleId>> entityToSamplePropertiesMap)
+            MapBatch<IEntityInformationWithPropertiesHolder, Map<String, ISampleId[]>> entityToSamplePropertiesMap)
     {
         if (entityToSamplePropertiesMap != null && entityToSamplePropertiesMap.isEmpty() == false)
         {
             Map<ISampleId, SamplePE> samplesById = getSamples(context, entityToSamplePropertiesMap);
-            new MapBatchProcessor<IEntityInformationWithPropertiesHolder, Map<String, ISampleId>>(context, entityToSamplePropertiesMap)
+            new MapBatchProcessor<IEntityInformationWithPropertiesHolder, Map<String, ISampleId[]>>(
+                    context, entityToSamplePropertiesMap)
+            {
+                @Override
+                public void process(IEntityInformationWithPropertiesHolder entity,
+                        Map<String, ISampleId[]> properties)
                 {
-                    @Override
-                    public void process(IEntityInformationWithPropertiesHolder entity, Map<String, ISampleId> properties)
-                    {
-                        Map<String, EntityPropertyPE> existingPropertiesByCode = getExistingPropertiesByCode(entity);
-                        Map<String, EntityTypePropertyTypePE> entityTypePropertyTypeByPropertyType =
-                                getEntityTypePropertyTypeByPropertyType(entity);
-                        updateProperties(context, entity, properties, samplesById, existingPropertiesByCode,
-                                entityTypePropertyTypeByPropertyType);
-                    }
+                    Map<String, List<EntityPropertyPE>> existingPropertiesByCode =
+                            getExistingPropertiesByCode(entity);
+                    Map<String, EntityTypePropertyTypePE> entityTypePropertyTypeByPropertyType =
+                            getEntityTypePropertyTypeByPropertyType(entity);
+                    updateProperties(context, entity, properties, samplesById,
+                            existingPropertiesByCode,
+                            entityTypePropertyTypeByPropertyType);
+                }
 
-                    @Override
-                    public IProgress createProgress(IEntityInformationWithPropertiesHolder key, Map<String, ISampleId> value, int objectIndex,
-                            int totalObjectCount)
-                    {
-                        return new EntityProgress("update properties of type sample", key, objectIndex, totalObjectCount);
-                    }
-                };
+                @Override
+                public IProgress createProgress(IEntityInformationWithPropertiesHolder key,
+                        Map<String, ISampleId[]> value, int objectIndex,
+                        int totalObjectCount)
+                {
+                    return new EntityProgress("update properties of type sample", key, objectIndex,
+                            totalObjectCount);
+                }
+            };
         }
     }
 
-    private void updateProperties(final IOperationContext context, IEntityInformationWithPropertiesHolder entity,
-            Map<String, ISampleId> properties, Map<ISampleId, SamplePE> samplesById,
-            Map<String, EntityPropertyPE> existingPropertiesByCode,
+    private void updateProperties(final IOperationContext context,
+            IEntityInformationWithPropertiesHolder entity,
+            Map<String, ISampleId[]> properties, Map<ISampleId, SamplePE> samplesById,
+            Map<String, List<EntityPropertyPE>> existingPropertiesByCode,
             Map<String, EntityTypePropertyTypePE> entityTypePropertyTypeByPropertyType)
     {
-        for (Entry<String, ISampleId> property : properties.entrySet())
+        for (Entry<String, ISampleId[]> property : properties.entrySet())
         {
             String propertyCode = property.getKey();
             EntityTypePropertyTypePE etpt = entityTypePropertyTypeByPropertyType.get(propertyCode);
             if (etpt == null)
             {
-                throw new UserFailureException("Not a property of data type SAMPLE: " + propertyCode);
+                throw new UserFailureException(
+                        "Not a property of data type SAMPLE: " + propertyCode);
             }
-            EntityPropertyPE existingProperty = existingPropertiesByCode.get(propertyCode);
-            ISampleId sampleId = property.getValue();
-            SamplePE sample = validateAndGetSample(samplesById, sampleId, etpt, propertyCode);
-            if (existingProperty != null)
-            {
-                if (sample == null)
-                {
-                    if (existingProperty.getEntityTypePropertyType().isMandatory() == false)
-                    {
-                        entity.removeProperty(existingProperty);
-                    } else
-                    {
-                        throw new UserFailureException("Property " + propertyCode + " of entity type "
-                                + etpt.getEntityType().getCode()
-                                + " can not be deleted because it is mandatory.");
-                    }
-                } else if (existingProperty instanceof EntityPropertyWithSampleDataTypePE)
-                {
-                    ((EntityPropertyWithSampleDataTypePE) existingProperty).setSampleValue(sample);
+            List<EntityPropertyPE> existingProperties = existingPropertiesByCode.getOrDefault(propertyCode, new ArrayList<>());
+            ISampleId[] sampleIds = property.getValue();
+
+            if(sampleIds.length > 1 && !etpt.getPropertyType().isMultiValue()) {
+                throw new UserFailureException("Property " + propertyCode + " of entity type "
+                        + etpt.getEntityType().getCode()
+                        + " is a single-value property. It can not accept multiple values.");
+            }
+
+            if(sampleIds.length == 1 && sampleIds[0] == null) {
+                if (etpt.isMandatory()) {
+                    throw new UserFailureException(
+                            "Property " + propertyCode + " of entity type "
+                                    + etpt.getEntityType().getCode()
+                                    + " can not be deleted because it is mandatory.");
                 }
-            } else if (sample != null)
-            {
-                EntityPropertyPE entityProperty = EntityPropertyPE.createEntityProperty(entity.getEntityKind());
-                if (entityProperty instanceof EntityPropertyWithSampleDataTypePE)
-                {
-                    PersonPE registrator = context.getSession().tryGetPerson();
-                    entityProperty.setRegistrator(registrator);
-                    entityProperty.setAuthor(registrator);
-                    entityProperty.setEntityTypePropertyType(etpt);
-                    ((EntityPropertyWithSampleDataTypePE) entityProperty).setSampleValue(sample);
-                    entity.addProperty(entityProperty);
+                existingProperties.forEach(entity::removeProperty);
+                continue;
+            }
+
+            List<EntityPropertyPE> newProperties = new ArrayList<>();
+            for (ISampleId sampleId : sampleIds) {
+                SamplePE sample = validateAndGetSample(samplesById, sampleId, etpt, propertyCode);
+                if(sample != null) {
+                    EntityPropertyWithSampleDataTypePE existingProperty = null;
+                    for(EntityPropertyPE propertyPE : existingProperties) {
+                        EntityPropertyWithSampleDataTypePE prop = (EntityPropertyWithSampleDataTypePE) propertyPE;
+                        if(prop.getSampleValue().getPermId().equals(sample.getPermId())) {
+                            existingProperty = prop;
+                            break;
+                        }
+                    }
+
+                    if(existingProperty != null) {
+                        existingProperty.setSampleValue(sample);
+                        newProperties.add(existingProperty);
+                    } else {
+                        EntityPropertyPE entityProperty =
+                                    EntityPropertyPE.createEntityProperty(entity.getEntityKind());
+                        if (entityProperty instanceof EntityPropertyWithSampleDataTypePE)
+                        {
+                            PersonPE registrator = context.getSession().tryGetPerson();
+                            entityProperty.setRegistrator(registrator);
+                            entityProperty.setAuthor(registrator);
+                            entityProperty.setEntityTypePropertyType(etpt);
+                            ((EntityPropertyWithSampleDataTypePE) entityProperty).setSampleValue(
+                                    sample);
+                        }
+                        newProperties.add(entityProperty);
+                    }
+                } else {
+                    throw new UserFailureException("Sample " + sampleId + " Does not exists!");
                 }
+
             }
+
+            if(!newProperties.isEmpty()) {
+                existingProperties.forEach(entity::removeProperty);
+                newProperties.forEach(entity::addProperty);
+            }
+
+        }
+    }
+
+    private EntityPropertyPE tryFindSampleProperty(List<EntityPropertyPE> properties, SamplePE sample) {
+        if(sample == null || properties == null) {
+            return null;
         }
+        return properties.stream()
+                .filter(x -> x.getEntity().getCode().equals(sample.getCode()))
+                .findFirst()
+                .orElse(null);
     }
 
-    private SamplePE validateAndGetSample(Map<ISampleId, SamplePE> samplesById, ISampleId sampleId, EntityTypePropertyTypePE etpt,
+    private SamplePE validateAndGetSample(Map<ISampleId, SamplePE> samplesById, ISampleId sampleId,
+            EntityTypePropertyTypePE etpt,
             String propertyCode)
     {
         if (sampleId == null)
@@ -230,7 +279,8 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
             throw new UserFailureException("Unknown sample: " + sampleId);
         }
         SampleTypePE sampleType = etpt.getPropertyType().getSampleType();
-        if (sampleType != null && sampleType.getCode().equals(sample.getSampleType().getCode()) == false)
+        if (sampleType != null && sampleType.getCode()
+                .equals(sample.getSampleType().getCode()) == false)
         {
             throw new UserFailureException("Property " + propertyCode + " is not a sample of type "
                     + sampleType.getCode() + " but of type " + sample.getSampleType().getCode());
@@ -238,20 +288,25 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
         return sample;
     }
 
-    private Map<String, EntityPropertyPE> getExistingPropertiesByCode(IEntityInformationWithPropertiesHolder entity)
+    private Map<String, List<EntityPropertyPE>> getExistingPropertiesByCode(
+            IEntityInformationWithPropertiesHolder entity)
     {
-        Map<String, EntityPropertyPE> existingPropertiesByCode = new HashMap<>();
+        Map<String, List<EntityPropertyPE>> existingPropertiesByCode = new HashMap<>();
         for (EntityPropertyPE entityProperty : entity.getProperties())
         {
-            PropertyTypePE propertyType = entityProperty.getEntityTypePropertyType().getPropertyType();
-            existingPropertiesByCode.put(propertyType.getCode(), entityProperty);
+            String propertyCode =
+                    entityProperty.getEntityTypePropertyType().getPropertyType().getCode();
+            existingPropertiesByCode.computeIfAbsent(propertyCode, s -> new ArrayList<>());
+            existingPropertiesByCode.get(propertyCode).add(entityProperty);
         }
         return existingPropertiesByCode;
     }
 
-    private Map<String, EntityTypePropertyTypePE> getEntityTypePropertyTypeByPropertyType(IEntityInformationWithPropertiesHolder entity)
+    private Map<String, EntityTypePropertyTypePE> getEntityTypePropertyTypeByPropertyType(
+            IEntityInformationWithPropertiesHolder entity)
     {
-        Map<String, EntityTypePropertyTypePE> entityTypePropertyTypeByPropertyType = new HashMap<>();
+        Map<String, EntityTypePropertyTypePE> entityTypePropertyTypeByPropertyType =
+                new HashMap<>();
         for (EntityTypePropertyTypePE etpt : entity.getEntityType().getEntityTypePropertyTypes())
         {
             PropertyTypePE propertyType = etpt.getPropertyType();
@@ -264,43 +319,52 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
     }
 
     private Map<ISampleId, SamplePE> getSamples(final IOperationContext context,
-            MapBatch<IEntityInformationWithPropertiesHolder, Map<String, ISampleId>> entityToSamplePropertiesMap)
+            MapBatch<IEntityInformationWithPropertiesHolder, Map<String, ISampleId[]>> entityToSamplePropertiesMap)
     {
         Set<ISampleId> sampleIds = getSampleIds(entityToSamplePropertiesMap);
         Map<ISampleId, SamplePE> samplesById = mapSampleByIdExecutor.map(context, sampleIds);
-        Set<Long> validateSampleTechIds = sampleAuthorizationValidator.validate(context.getSession().tryGetPerson(),
-                samplesById.values().stream().map(SamplePE::getId).collect(Collectors.toSet()));
-        return samplesById.entrySet().stream().filter(e -> validateSampleTechIds.contains(e.getValue().getId()))
+        Set<Long> validateSampleTechIds =
+                sampleAuthorizationValidator.validate(context.getSession().tryGetPerson(),
+                        samplesById.values().stream().map(SamplePE::getId)
+                                .collect(Collectors.toSet()));
+        return samplesById.entrySet().stream()
+                .filter(e -> validateSampleTechIds.contains(e.getValue().getId()))
                 .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
     }
 
-    private Set<ISampleId> getSampleIds(MapBatch<IEntityInformationWithPropertiesHolder, Map<String, ISampleId>> entityToSamplePropertiesMap)
+    private Set<ISampleId> getSampleIds(
+            MapBatch<IEntityInformationWithPropertiesHolder, Map<String, ISampleId[]>> entityToSamplePropertiesMap)
     {
         Set<ISampleId> sampleIds = new HashSet<>();
-        for (Map<String, ISampleId> map : entityToSamplePropertiesMap.getObjects().values())
+        for (Map<String, ISampleId[]> map : entityToSamplePropertiesMap.getObjects().values())
         {
-            Collection<ISampleId> values = map.values();
-            for (ISampleId sample : values)
+            Collection<ISampleId[]> values = map.values();
+            for (ISampleId[] samples : values)
             {
-                if (sample != null)
+                for (ISampleId sample : samples)
                 {
-                    sampleIds.add(sample);
+                    if (sample != null)
+                    {
+                        sampleIds.add(sample);
+                    }
                 }
             }
         }
         return sampleIds;
     }
 
-    private MapBatch<IEntityInformationWithPropertiesHolder, Map<String, ISampleId>> extractAndRemoveSampleProperties(
+    private MapBatch<IEntityInformationWithPropertiesHolder, Map<String, ISampleId[]>> extractAndRemoveSampleProperties(
             MapBatch<? extends IPropertiesHolder, ? extends IEntityInformationWithPropertiesHolder> holderToEntityMap,
             MapBatch<IEntityInformationWithPropertiesHolder, Map<String, Serializable>> entityToPropertiesMap)
     {
-        Map<IEntityInformationWithPropertiesHolder, Map<String, ISampleId>> map = new HashMap<>();
-        Map<IEntityInformationWithPropertiesHolder, Map<String, Serializable>> objects = entityToPropertiesMap.getObjects();
+        Map<IEntityInformationWithPropertiesHolder, Map<String, ISampleId[]>> map = new HashMap<>();
+        Map<IEntityInformationWithPropertiesHolder, Map<String, Serializable>> objects =
+                entityToPropertiesMap.getObjects();
         for (Entry<IEntityInformationWithPropertiesHolder, Map<String, Serializable>> entry : objects.entrySet())
         {
             IEntityInformationWithPropertiesHolder entity = entry.getKey();
-            Collection<? extends EntityTypePropertyTypePE> etpts = entity.getEntityType().getEntityTypePropertyTypes();
+            Collection<? extends EntityTypePropertyTypePE> etpts =
+                    entity.getEntityType().getEntityTypePropertyTypes();
 
             for (EntityTypePropertyTypePE etpt : etpts)
             {
@@ -310,27 +374,50 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
                     Map<String, Serializable> properties = entry.getValue();
                     if (properties.containsKey(code))
                     {
-                        String sample = (String) properties.remove(code);
-                        ISampleId sampleId = null;
-                        if (sample != null)
+                        Serializable value = properties.remove(code);
+                        ISampleId[] samples;
+                        if (value != null && value.getClass().isArray())
+                        {
+                            List<ISampleId> tmp = new ArrayList<>();
+                            for (Serializable sample : (Serializable[]) value)
+                            {
+                                tmp.add(getSampleFromProperty(sample));
+                            }
+                            samples = tmp.toArray(new ISampleId[0]);
+                        } else
                         {
-                            sampleId = sample.startsWith("/") ? new SampleIdentifier(sample) : new SamplePermId(sample);
+                            samples = new ISampleId[] { getSampleFromProperty(value) };
                         }
-                        Map<String, ISampleId> props = map.get(entity);
+
+                        Map<String, ISampleId[]> props = map.get(entity);
                         if (props == null)
                         {
                             props = new HashMap<>();
                             map.put(entity, props);
                         }
-                        props.put(code, sampleId);
+                        props.put(code, samples);
                     }
                 }
             }
         }
-        return new MapBatch<>(holderToEntityMap.getBatchIndex(), holderToEntityMap.getFromObjectIndex(),
+        return new MapBatch<>(holderToEntityMap.getBatchIndex(),
+                holderToEntityMap.getFromObjectIndex(),
                 holderToEntityMap.getToObjectIndex(), map, holderToEntityMap.getTotalObjectCount());
     }
 
+    private ISampleId getSampleFromProperty(Serializable sampleProperty)
+    {
+        String sample = (String) sampleProperty;
+        ISampleId sampleId = null;
+        if (sample != null)
+        {
+            sampleId = sample.startsWith("/") ?
+                    new SampleIdentifier(sample) :
+                    new SamplePermId(sample);
+        }
+        return sampleId;
+    }
+
     private MapBatch<IEntityInformationWithPropertiesHolder, Map<String, Serializable>> getEntityToPropertiesMap(
             final MapBatch<? extends IPropertiesHolder, ? extends IEntityInformationWithPropertiesHolder> holderToEntityMap)
     {
@@ -339,7 +426,8 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
             return null;
         }
 
-        Map<IEntityInformationWithPropertiesHolder, Map<String, Serializable>> entityToPropertiesMap =
+        Map<IEntityInformationWithPropertiesHolder, Map<String, Serializable>>
+                entityToPropertiesMap =
                 new HashMap<>();
 
         for (Map.Entry<? extends IPropertiesHolder, ? extends IEntityInformationWithPropertiesHolder> entry : holderToEntityMap.getObjects()
@@ -359,12 +447,15 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
             return null;
         }
 
-        return new MapBatch<IEntityInformationWithPropertiesHolder, Map<String, Serializable>>(holderToEntityMap.getBatchIndex(),
-                holderToEntityMap.getFromObjectIndex(), holderToEntityMap.getToObjectIndex(), entityToPropertiesMap,
+        return new MapBatch<IEntityInformationWithPropertiesHolder, Map<String, Serializable>>(
+                holderToEntityMap.getBatchIndex(),
+                holderToEntityMap.getFromObjectIndex(), holderToEntityMap.getToObjectIndex(),
+                entityToPropertiesMap,
                 holderToEntityMap.getTotalObjectCount());
     }
 
-    private void update(IOperationContext context, IEntityPropertiesHolder propertiesHolder, Map<String, Serializable> properties,
+    private void update(IOperationContext context, IEntityPropertiesHolder propertiesHolder,
+            Map<String, Serializable> properties,
             EntityPropertiesConverter converter)
     {
         List<IEntityProperty> entityProperties = new LinkedList<IEntityProperty>();
@@ -374,26 +465,30 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
         }
 
         Set<? extends EntityPropertyPE> existingProperties = propertiesHolder.getProperties();
-        Map<String, Object> existingPropertyValuesByCode = new HashMap<String, Object>();
-
+        Map<String, List<Object>> existingPropertyValuesByCode =
+                new HashMap<String, List<Object>>();
         for (EntityPropertyPE existingProperty : existingProperties)
         {
             String propertyCode =
                     existingProperty.getEntityTypePropertyType().getPropertyType().getCode();
-            existingPropertyValuesByCode.put(propertyCode, getValue(existingProperty));
+            existingPropertyValuesByCode.computeIfAbsent(propertyCode, s -> new ArrayList<>());
+            existingPropertyValuesByCode.get(propertyCode).add(getValue(existingProperty));
         }
 
         Set<? extends EntityPropertyPE> convertedProperties =
-                convertProperties(context, propertiesHolder.getEntityType(), existingProperties, entityProperties, converter);
+                convertProperties(context, propertiesHolder.getEntityType(), existingProperties,
+                        entityProperties, converter);
 
-        if (isEquals(existingPropertyValuesByCode, convertedProperties) == false)
+        if (isEqualsMultiple(existingPropertyValuesByCode, convertedProperties) == false)
         {
             propertiesHolder.setProperties(convertedProperties);
         }
     }
 
-    private <T extends EntityPropertyPE> Set<T> convertProperties(IOperationContext context, final EntityTypePE type,
-            final Set<T> existingProperties, List<IEntityProperty> properties, EntityPropertiesConverter converter)
+    private <T extends EntityPropertyPE> Set<T> convertProperties(IOperationContext context,
+            final EntityTypePE type,
+            final Set<T> existingProperties, List<IEntityProperty> properties,
+            EntityPropertiesConverter converter)
     {
         Set<String> propertiesToUpdate = new HashSet<String>();
         if (properties != null)
@@ -438,4 +533,50 @@ public class UpdateEntityPropertyExecutor implements IUpdateEntityPropertyExecut
         return existingPropertyValuesByCode.isEmpty();
     }
 
+    private boolean isEqualsMultiple(Map<String, List<Object>> existingPropertyValuesByCode,
+            Set<? extends EntityPropertyPE> properties)
+    {
+        for (EntityPropertyPE property : properties)
+        {
+            List<Object> existingValueList =
+                    existingPropertyValuesByCode.get(property.getEntityTypePropertyType()
+                            .getPropertyType().getCode());
+            if (existingValueList == null || existingValueList.isEmpty())
+            {
+                return false;
+            }
+            boolean flag = false;
+            Object valToRemove = null;
+            for (Object value : existingValueList)
+            {
+                Object propertyValue = getValue(property);
+                if (propertyValue == null)
+                {
+                    // TODO: Add logic for sample property
+                    // we have some non-EntityPropertyPE property.
+                    return false;
+                }
+                if (value.equals(propertyValue))
+                {
+                    flag = true;
+                    valToRemove = value;
+                    break;
+                }
+            }
+            if (flag)
+            {
+                existingValueList.remove(valToRemove);
+                if (existingValueList.isEmpty())
+                {
+                    existingPropertyValuesByCode.remove(property.getEntityTypePropertyType()
+                            .getPropertyType().getCode());
+                }
+            } else
+            {
+                return false;
+            }
+        }
+        return existingPropertyValuesByCode.isEmpty();
+    }
+
 }
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/property/PropertyTranslator.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/property/PropertyTranslator.java
index 9867b6bba97..bc908803a2b 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/property/PropertyTranslator.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/property/PropertyTranslator.java
@@ -78,17 +78,8 @@ public abstract class PropertyTranslator extends
             {
                 if(objectProperties.containsKey(record.propertyCode)) {
                     Serializable current = objectProperties.get(record.propertyCode);
-                    Serializable[] vocabs;
-                    if(current.getClass().isArray()) {
-                        Serializable[] values = (Serializable[]) current;
-                        vocabs = new Serializable[values.length + 1];
-                        System.arraycopy(values, 0, vocabs, 0, values.length);
-                        vocabs[values.length] = record.vocabularyPropertyValue;
-                    } else {
-                        vocabs = new Serializable[] {current, record.vocabularyPropertyValue};
-                    }
-                    objectProperties.put(record.propertyCode, vocabs);
-
+                    Serializable newValue = composeMultiValueProperty(current, record.vocabularyPropertyValue);
+                    objectProperties.put(record.propertyCode, newValue);
                 } else {
                     objectProperties.put(record.propertyCode, record.vocabularyPropertyValue);
                 }
@@ -96,7 +87,14 @@ public abstract class PropertyTranslator extends
             {
                 if (visibaleSamples.contains(record.sample_id))
                 {
-                    objectProperties.put(record.propertyCode, record.sample_perm_id);
+                    if(objectProperties.containsKey(record.propertyCode)) {
+                        Serializable current = objectProperties.get(record.propertyCode);
+                        Serializable newValue = composeMultiValueProperty(current, record.sample_perm_id);
+                        objectProperties.put(record.propertyCode, newValue);
+                    } else
+                    {
+                        objectProperties.put(record.propertyCode, record.sample_perm_id);
+                    }
                 }
             } else if (record.integerArrayPropertyValue != null)
             {
@@ -126,6 +124,19 @@ public abstract class PropertyTranslator extends
         return properties;
     }
 
+    private Serializable composeMultiValueProperty(Serializable current, Serializable newValue) {
+        Serializable[] result;
+        if(current.getClass().isArray()) {
+            Serializable[] values = (Serializable[]) current;
+            result = new Serializable[values.length + 1];
+            System.arraycopy(values, 0, result, 0, values.length);
+            result[values.length] = newValue;
+        } else {
+            result = new Serializable[] {current, newValue};
+        }
+        return result;
+    }
+
     private String convertArrayToString(String[] array)
     {
         return Stream.of(array)
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/helper/AbstractXLSExportHelper.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/helper/AbstractXLSExportHelper.java
index 9052b96c808..a441db2f1bd 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/helper/AbstractXLSExportHelper.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/helper/AbstractXLSExportHelper.java
@@ -49,7 +49,7 @@ public abstract class AbstractXLSExportHelper<ENTITY_TYPE extends IEntityType> i
 
     protected static final String[] ENTITY_ASSIGNMENT_COLUMNS = new String[] { "Version", "Code", "Mandatory",
             "Show in edit views", "Section", "Property label", "Data type", "Vocabulary code", "Description",
-            "Metadata", "Dynamic script" };
+            "Metadata", "Dynamic script", "Multivalued" };
 
     protected static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(BasicConstant.DATE_HOURS_MINUTES_SECONDS_PATTERN);
 
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/importer/helper/PropertyAssignmentImportHelper.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/importer/helper/PropertyAssignmentImportHelper.java
index 507464ab8d7..bf22e1a7aa5 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/importer/helper/PropertyAssignmentImportHelper.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/importer/helper/PropertyAssignmentImportHelper.java
@@ -69,7 +69,8 @@ public class PropertyAssignmentImportHelper extends BasicImportHelper
         DynamicScript("Dynamic script", false),
         OntologyId("Ontology Id", false),
         OntologyVersion("Ontology Version", false),
-        OntologyAnnotationId("Ontology Annotation Id", false);
+        OntologyAnnotationId("Ontology Annotation Id", false),
+        MultiValued("Multivalued", false),;
 
         private final String headerName;
 
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/importer/helper/PropertyTypeImportHelper.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/importer/helper/PropertyTypeImportHelper.java
index 4ba863ad085..dfda7051787 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/importer/helper/PropertyTypeImportHelper.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/importer/helper/PropertyTypeImportHelper.java
@@ -47,13 +47,16 @@ import static ch.ethz.sis.openbis.generic.server.xls.importer.utils.PropertyType
 
 public class PropertyTypeImportHelper extends BasicImportHelper
 {
-    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION, PropertyTypeImportHelper.class);
+    private static final Logger operationLog =
+            LogFactory.getLogger(LogCategory.OPERATION, PropertyTypeImportHelper.class);
 
-    private enum Attribute implements IAttribute {
+    private enum Attribute implements IAttribute
+    {
         Version("Version", true),
         Code("Code", true),
         Mandatory("Mandatory", false),
-        DefaultValue("Default Value", false),  // Ignored, only used by PropertyAssignmentImportHelper
+        DefaultValue("Default Value",
+                false),  // Ignored, only used by PropertyAssignmentImportHelper
         ShowInEditViews("Show in edit views", false),
         Section("Section", false),
         PropertyLabel("Property label", true),
@@ -64,21 +67,26 @@ public class PropertyTypeImportHelper extends BasicImportHelper
         DynamicScript("Dynamic script", false),
         OntologyId("Ontology Id", false),
         OntologyVersion("Ontology Version", false),
-        OntologyAnnotationId("Ontology Annotation Id", false);
+        OntologyAnnotationId("Ontology Annotation Id", false),
+        MultiValued("Multivalued", false);
 
         private final String headerName;
 
         private final boolean mandatory;
 
-        Attribute(String headerName, boolean mandatory) {
+        Attribute(String headerName, boolean mandatory)
+        {
             this.headerName = headerName;
             this.mandatory = mandatory;
         }
 
-        public String getHeaderName() {
+        public String getHeaderName()
+        {
             return headerName;
         }
-        public boolean isMandatory() {
+
+        public boolean isMandatory()
+        {
             return mandatory;
         }
     }
@@ -91,7 +99,8 @@ public class PropertyTypeImportHelper extends BasicImportHelper
 
     private final AttributeValidator<Attribute> attributeValidator;
 
-    public PropertyTypeImportHelper(DelayedExecutionDecorator delayedExecutor, ImportModes mode, ImportOptions options, Map<String, Integer> versions)
+    public PropertyTypeImportHelper(DelayedExecutionDecorator delayedExecutor, ImportModes mode,
+            ImportOptions options, Map<String, Integer> versions)
     {
         super(mode, options);
         this.versions = versions;
@@ -101,7 +110,8 @@ public class PropertyTypeImportHelper extends BasicImportHelper
     }
 
     @Override
-    protected void validateLine(Map<String, Integer> headers, List<String> values) {
+    protected void validateLine(Map<String, Integer> headers, List<String> values)
+    {
         // Validate Unambiguous
         String code = getValueByColumnName(headers, values, Attribute.Code);
         String propertyLabel = getValueByColumnName(headers, values, Attribute.PropertyLabel);
@@ -110,31 +120,37 @@ public class PropertyTypeImportHelper extends BasicImportHelper
         String vocabularyCode = getValueByColumnName(headers, values, Attribute.VocabularyCode);
         String metadata = getValueByColumnName(headers, values, Attribute.Metadata);
 
-        String propertyData = code + propertyLabel + description + dataType + vocabularyCode + metadata;
+        String propertyData =
+                code + propertyLabel + description + dataType + vocabularyCode + metadata;
         if (this.propertyCache.get(code) == null)
         {
             this.propertyCache.put(code, propertyData);
         }
         if (!propertyData.equals(this.propertyCache.get(code)))
         {
-            throw new UserFailureException("Unambiguous property " + code + " found, has been declared before with different attributes.");
+            throw new UserFailureException(
+                    "Unambiguous property " + code + " found, has been declared before with different attributes.");
         }
     }
 
-    @Override protected ImportTypes getTypeName()
+    @Override
+    protected ImportTypes getTypeName()
     {
         return ImportTypes.PROPERTY_TYPE;
     }
 
-    @Override protected boolean isNewVersion(Map<String, Integer> header, List<String> values)
+    @Override
+    protected boolean isNewVersion(Map<String, Integer> header, List<String> values)
     {
         String newVersion = getValueByColumnName(header, values, Attribute.Version);
         String code = getValueByColumnName(header, values, Attribute.Code);
 
-        return VersionUtils.isNewVersion(newVersion, VersionUtils.getStoredVersion(versions, ImportTypes.PROPERTY_TYPE.getType(), code));
+        return VersionUtils.isNewVersion(newVersion,
+                VersionUtils.getStoredVersion(versions, ImportTypes.PROPERTY_TYPE.getType(), code));
     }
 
-    @Override protected void updateVersion(Map<String, Integer> header, List<String> values)
+    @Override
+    protected void updateVersion(Map<String, Integer> header, List<String> values)
     {
         String version = getValueByColumnName(header, values, Attribute.Version);
         String code = getValueByColumnName(header, values, Attribute.Code);
@@ -142,7 +158,8 @@ public class PropertyTypeImportHelper extends BasicImportHelper
         VersionUtils.updateVersion(version, versions, ImportTypes.PROPERTY_TYPE.getType(), code);
     }
 
-    @Override protected boolean isObjectExist(Map<String, Integer> header, List<String> values)
+    @Override
+    protected boolean isObjectExist(Map<String, Integer> header, List<String> values)
     {
         String code = getValueByColumnName(header, values, Attribute.Code);
         PropertyTypeFetchOptions fetchOptions = new PropertyTypeFetchOptions();
@@ -152,7 +169,9 @@ public class PropertyTypeImportHelper extends BasicImportHelper
         return delayedExecutor.getPropertyType(propertyTypePermId, fetchOptions) != null;
     }
 
-    @Override protected void createObject(Map<String, Integer> header, List<String> values, int page, int line)
+    @Override
+    protected void createObject(Map<String, Integer> header, List<String> values, int page,
+            int line)
     {
         String code = getValueByColumnName(header, values, Attribute.Code);
         String propertyLabel = getValueByColumnName(header, values, Attribute.PropertyLabel);
@@ -160,6 +179,7 @@ public class PropertyTypeImportHelper extends BasicImportHelper
         String dataType = getValueByColumnName(header, values, Attribute.DataType);
         String vocabularyCode = getValueByColumnName(header, values, Attribute.VocabularyCode);
         String metadata = getValueByColumnName(header, values, Attribute.Metadata);
+        String multiValued = getValueByColumnName(header, values, Attribute.MultiValued);
 
         PropertyTypeCreation creation = new PropertyTypeCreation();
         creation.setCode(code);
@@ -169,7 +189,8 @@ public class PropertyTypeImportHelper extends BasicImportHelper
         if (dataType.startsWith(SAMPLE_DATA_TYPE_PREFIX))
         {
             creation.setDataType(DataType.SAMPLE);
-            if (dataType.contains(SAMPLE_DATA_TYPE_MANDATORY_TYPE)) {
+            if (dataType.contains(SAMPLE_DATA_TYPE_MANDATORY_TYPE))
+            {
                 String sampleType = dataType.split(SAMPLE_DATA_TYPE_MANDATORY_TYPE)[1];
                 creation.setSampleTypeId(new EntityTypePermId(sampleType, EntityKind.SAMPLE));
             }
@@ -188,10 +209,20 @@ public class PropertyTypeImportHelper extends BasicImportHelper
             creation.setMetaData(JSONHandler.parseMetaData(metadata));
         }
 
+        if (multiValued != null && !multiValued.isEmpty())
+        {
+            creation.setMultiValue(Boolean.parseBoolean(multiValued));
+        } else
+        {
+            creation.setMultiValue(false);
+        }
+
         delayedExecutor.createPropertyType(creation, page, line);
     }
 
-    @Override protected void updateObject(Map<String, Integer> header, List<String> values, int page, int line)
+    @Override
+    protected void updateObject(Map<String, Integer> header, List<String> values, int page,
+            int line)
     {
         String code = getValueByColumnName(header, values, Attribute.Code);
         String propertyLabel = getValueByColumnName(header, values, Attribute.PropertyLabel);
@@ -228,12 +259,14 @@ public class PropertyTypeImportHelper extends BasicImportHelper
         PropertyTypeFetchOptions propertyTypeFetchOptions = new PropertyTypeFetchOptions();
         propertyTypeFetchOptions.withVocabulary();
         propertyTypeFetchOptions.withSampleType();
-        PropertyType propertyType = delayedExecutor.getPropertyType(propertyTypePermId, propertyTypeFetchOptions);
+        PropertyType propertyType =
+                delayedExecutor.getPropertyType(propertyTypePermId, propertyTypeFetchOptions);
         if (vocabularyCode != null && !vocabularyCode.isEmpty())
         {
             if (vocabularyCode.equals(propertyType.getVocabulary().getCode()) == false)
             {
-                operationLog.warn("PROPERTY TYPE [" + code+ "] : Vocabulary types can't be updated. Ignoring the update.");
+                operationLog.warn(
+                        "PROPERTY TYPE [" + code + "] : Vocabulary types can't be updated. Ignoring the update.");
                 //   throw new UserFailureException("Vocabulary types can't be updated.");
             }
         }
@@ -246,7 +279,8 @@ public class PropertyTypeImportHelper extends BasicImportHelper
             }
             if (dataType.equals(currentDataType) == false)
             {
-                operationLog.warn("PROPERTY TYPE [" + code + "] : Data Types can't be converted with Master Data XLS. Ignoring the update.");
+                operationLog.warn(
+                        "PROPERTY TYPE [" + code + "] : Data Types can't be converted with Master Data XLS. Ignoring the update.");
                 // update.convertToDataType(DataType.valueOf(dataType));
             }
         }
@@ -257,7 +291,8 @@ public class PropertyTypeImportHelper extends BasicImportHelper
         delayedExecutor.updatePropertyType(update, page, line);
     }
 
-    @Override protected void validateHeader(Map<String, Integer> headers)
+    @Override
+    protected void validateHeader(Map<String, Integer> headers)
     {
         attributeValidator.validateHeaders(Attribute.values(), headers);
     }
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/api/v1/sort/SampleSearchResultSorter.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/api/v1/sort/SampleSearchResultSorter.java
index 6495e1ad986..e1fbc4b9f31 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/api/v1/sort/SampleSearchResultSorter.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/api/v1/sort/SampleSearchResultSorter.java
@@ -15,6 +15,7 @@
  */
 package ch.systemsx.cisd.openbis.generic.server.api.v1.sort;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/fetchoptions/samplelister/SampleLister.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/fetchoptions/samplelister/SampleLister.java
index 84da811402e..1afb97ab4e5 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/fetchoptions/samplelister/SampleLister.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/fetchoptions/samplelister/SampleLister.java
@@ -18,6 +18,7 @@ package ch.systemsx.cisd.openbis.generic.server.business.bo.fetchoptions.samplel
 import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
 import it.unimi.dsi.fastutil.longs.LongSet;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -54,41 +55,41 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE;
 public class SampleLister implements ISampleLister
 {
     private static final Comparator<Sample> SAMPLE_COMPARATOR = new Comparator<Sample>()
+    {
+        @Override
+        public int compare(Sample s1, Sample s2)
         {
-            @Override
-            public int compare(Sample s1, Sample s2)
-            {
-                return s1.getIdentifier().compareTo(s2.getIdentifier());
-            }
-        };
+            return s1.getIdentifier().compareTo(s2.getIdentifier());
+        }
+    };
 
     private static final Comparator<SampleRecord> SAMPLE_COMPARATOR2 =
             new Comparator<SampleRecord>()
+            {
+                @Override
+                public int compare(SampleRecord s1, SampleRecord s2)
                 {
-                    @Override
-                    public int compare(SampleRecord s1, SampleRecord s2)
-                    {
-                        return getIdentifier(s1).compareTo(getIdentifier(s2));
-                    }
+                    return getIdentifier(s1).compareTo(getIdentifier(s2));
+                }
 
-                    private String getIdentifier(SampleRecord sampleRecord)
-                    {
-                        String spaceCode = sampleRecord.sp_code;
-                        String sampleCode = sampleRecord.s_code;
-                        return spaceCode == null ? "/" + sampleCode : "/" + spaceCode + "/"
-                                + sampleCode;
-                    }
-                };
+                private String getIdentifier(SampleRecord sampleRecord)
+                {
+                    String spaceCode = sampleRecord.sp_code;
+                    String sampleCode = sampleRecord.s_code;
+                    return spaceCode == null ? "/" + sampleCode : "/" + spaceCode + "/"
+                            + sampleCode;
+                }
+            };
 
     private static final IKeyExtractor<Long, SampleRecord> ID_EXTRACTOR =
             new IKeyExtractor<Long, SampleRecord>()
+            {
+                @Override
+                public Long getKey(SampleRecord s)
                 {
-                    @Override
-                    public Long getKey(SampleRecord s)
-                    {
-                        return s.s_id;
-                    }
-                };
+                    return s.s_id;
+                }
+            };
 
     private final ISampleListingQuery query;
 
@@ -300,7 +301,13 @@ public class SampleLister implements ISampleLister
                 {
                     sampleRecord.properties = new HashMap<String, String>();
                 }
-                sampleRecord.properties.put(propertyRecord.code, propertyRecord.getValue());
+                String value = propertyRecord.getValue();
+                if (sampleRecord.properties.containsKey(propertyRecord.code))
+                {
+                    String ss = sampleRecord.properties.get(propertyRecord.code);
+                    value = ss + ", " + value;
+                }
+                sampleRecord.properties.put(propertyRecord.code, value);
             }
         }
     }
@@ -320,13 +327,13 @@ public class SampleLister implements ISampleLister
         TableMap<Long, MetaprojectRecord> metaprojectRecords =
                 new TableMap<Long, MetaprojectRecord>(metaprojects,
                         new IKeyExtractor<Long, MetaprojectRecord>()
+                        {
+                            @Override
+                            public Long getKey(MetaprojectRecord mr)
                             {
-                                @Override
-                                public Long getKey(MetaprojectRecord mr)
-                                {
-                                    return mr.id;
-                                }
-                            });
+                                return mr.id;
+                            }
+                        });
 
         for (EntityMetaprojectRelationRecord record : sampleMetaprojectRelations)
         {
@@ -369,7 +376,8 @@ public class SampleLister implements ISampleLister
         initializer.setCode(sampleCode);
         if (sampleRecord.samp_proj_code != null)
         {
-            initializer.setIdentifier("/" + spaceCode + "/" + sampleRecord.samp_proj_code + "/" + sampleCode);
+            initializer.setIdentifier(
+                    "/" + spaceCode + "/" + sampleRecord.samp_proj_code + "/" + sampleCode);
         } else
         {
             initializer.setIdentifier(spaceCode == null ? "/" + sampleCode : "/" + spaceCode + "/"
@@ -453,8 +461,10 @@ public class SampleLister implements ISampleLister
             EnumSet<SampleFetchOption> fetchOptions)
     {
         EnumSet<SampleFetchOption> result =
-                EnumSet.of(fetchOptions.contains(SampleFetchOption.PROPERTIES) ? SampleFetchOption.PROPERTIES
-                        : SampleFetchOption.BASIC);
+                EnumSet.of(fetchOptions.contains(SampleFetchOption.PROPERTIES) ?
+                        SampleFetchOption.PROPERTIES
+                        :
+                        SampleFetchOption.BASIC);
         if (fetchOptions.contains(SampleFetchOption.METAPROJECTS))
         {
             result.add(SampleFetchOption.METAPROJECTS);
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/IEntitySearchResult.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/IEntitySearchResult.java
index 5907d76a510..eaf7d2054ca 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/IEntitySearchResult.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/IEntitySearchResult.java
@@ -15,6 +15,7 @@
  */
 package ch.systemsx.cisd.openbis.generic.server.business.search.sort;
 
+import java.io.Serializable;
 import java.util.Map;
 
 /**
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/SearchResultSorterByScore.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/SearchResultSorterByScore.java
index fbdb7b5cd00..eb8042518c0 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/SearchResultSorterByScore.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/SearchResultSorterByScore.java
@@ -15,6 +15,7 @@
  */
 package ch.systemsx.cisd.openbis.generic.server.business.search.sort;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -128,7 +129,7 @@ public class SearchResultSorterByScore implements ISearchResultSorter
             {
                 for (String propertykey : entity.getProperties().keySet())
                 {
-                    String propertyValue = entity.getProperties().get(propertykey);
+                    Serializable propertyValue = entity.getProperties().get(propertykey);
                     if (isPartialMatch(propertyValue, partialTerm))
                     { // If property matches partially
                         score += 100 * boost.getPropertyBoost(propertykey);
@@ -234,22 +235,24 @@ public class SearchResultSorterByScore implements ISearchResultSorter
         return term.replace("*", "").replace("?", "");
     }
 
-    public boolean isExactMatch(String value, String term)
+    public boolean isExactMatch(Serializable value, String term)
     {
-        if (value != null && term != null)
+        if (value != null && !value.getClass().isArray() && term != null)
         {
-            return value.equalsIgnoreCase(term);
+            String valueStr = (String) value;
+            return valueStr.equalsIgnoreCase(term);
         } else
         {
             return false;
         }
     }
 
-    public boolean isPartialMatch(String value, Pattern pattern)
+    public boolean isPartialMatch(Serializable value, Pattern pattern)
     {
-        if (value != null && pattern != null)
+        if (value != null && !value.getClass().isArray() && pattern != null)
         {
-            return pattern.matcher(value).matches();
+            String valueStr = (String) value;
+            return pattern.matcher(valueStr).matches();
         } else
         {
             return false;
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataSetPropertyPE.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataSetPropertyPE.java
index e7aee769008..41846f6c8fa 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataSetPropertyPE.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataSetPropertyPE.java
@@ -39,8 +39,7 @@ import ch.systemsx.cisd.openbis.generic.shared.IServer;
  * @author Izabela Adamczyk
  */
 @Entity
-@Table(name = TableNames.DATA_SET_PROPERTIES_TABLE, uniqueConstraints = @UniqueConstraint(columnNames =
-{ ColumnNames.DATA_SET_COLUMN, ColumnNames.DATA_SET_TYPE_PROPERTY_TYPE_COLUMN }))
+@Table(name = TableNames.DATA_SET_PROPERTIES_TABLE)
 @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
 public class DataSetPropertyPE extends EntityPropertyWithSampleDataTypePE
 {
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePropertyPE.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePropertyPE.java
index a50820b537f..fbcd69e366e 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePropertyPE.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePropertyPE.java
@@ -39,8 +39,7 @@ import ch.systemsx.cisd.openbis.generic.shared.IServer;
  * @author Izabela Adamczyk
  */
 @Entity
-@Table(name = TableNames.SAMPLE_PROPERTIES_TABLE, uniqueConstraints = {
-        @UniqueConstraint(columnNames = { ColumnNames.SAMPLE_COLUMN, ColumnNames.SAMPLE_TYPE_PROPERTY_TYPE_COLUMN }) })
+@Table(name = TableNames.SAMPLE_PROPERTIES_TABLE)
 @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
 public class SamplePropertyPE extends EntityPropertyWithSampleDataTypePE
 {
diff --git a/server-application-server/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/SearchResultSorterTestHelper.java b/server-application-server/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/SearchResultSorterTestHelper.java
index 4f00bd4ab60..adf5deb0cba 100644
--- a/server-application-server/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/SearchResultSorterTestHelper.java
+++ b/server-application-server/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/business/search/sort/SearchResultSorterTestHelper.java
@@ -15,6 +15,7 @@
  */
 package ch.systemsx.cisd.openbis.generic.server.business.search.sort;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
-- 
GitLab