From ee848ab3ce6a46ce8d71909155239b631cc457fb Mon Sep 17 00:00:00 2001
From: buczekp <buczekp>
Date: Mon, 21 Mar 2011 09:34:14 +0000
Subject: [PATCH] [LMS-2148] first implementation of search for parents with
 tests

SVN: 20416
---
 .../openbis/generic/server/CommonServer.java  |  53 +-----
 .../business/DetailedSearchManager.java       | 162 ++++++++++++++++++
 .../bo/samplelister/ISampleLister.java        |  27 ++-
 .../bo/samplelister/ISampleListingQuery.java  |   9 +
 .../bo/samplelister/SampleLister.java         |  74 +++++++-
 .../api/v1/dto/SearchableEntityKind.java      |   4 +-
 .../api/v1/GeneralInformationServiceTest.java |  42 +++++
 .../api/v1/GeneralInformationServiceTest.java |  34 +++-
 8 files changed, 340 insertions(+), 65 deletions(-)
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/DetailedSearchManager.java

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 73099156c69..d021068b88f 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
@@ -38,6 +38,7 @@ import ch.systemsx.cisd.authentication.ISessionManager;
 import ch.systemsx.cisd.authentication.Principal;
 import ch.systemsx.cisd.common.exceptions.UserFailureException;
 import ch.systemsx.cisd.common.spring.IInvocationLoggerContext;
+import ch.systemsx.cisd.openbis.generic.server.business.DetailedSearchManager;
 import ch.systemsx.cisd.openbis.generic.server.business.IPropertiesBatchManager;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.DataAccessExceptionTranslator;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.IAttachmentBO;
@@ -475,68 +476,22 @@ public final class CommonServer extends AbstractCommonServer<ICommonServerForInt
         return sampleLister.list(new ListOrSearchSampleCriteria(criteria));
     }
 
-    public List<Sample> searchForSamples(String sessionToken, DetailedSearchCriteria criteria)
-    {
-        final Session session = getSession(sessionToken);
-        try
-        {
-            final Collection<Long> sampleIds =
-                    findSampleIds(criteria, Collections.<DetailedSearchSubCriteria> emptyList());
-            final ISampleLister sampleLister = businessObjectFactory.createSampleLister(session);
-            return sampleLister.list(new ListOrSearchSampleCriteria(sampleIds));
-        } catch (final DataAccessException ex)
-        {
-            throw createUserFailureException(ex);
-        }
-    }
-
     public List<Sample> searchForSamples(String sessionToken, DetailedSearchCriteria criteria,
             List<DetailedSearchSubCriteria> subCriterias)
     {
         final Session session = getSession(sessionToken);
         try
         {
-            final Collection<Long> sampleIds = findSampleIds(criteria, subCriterias);
             final ISampleLister sampleLister = businessObjectFactory.createSampleLister(session);
-            return sampleLister.list(new ListOrSearchSampleCriteria(sampleIds));
+            final IHibernateSearchDAO searchDAO = getDAOFactory().getHibernateSearchDAO();
+            return new DetailedSearchManager(searchDAO, sampleLister).searchForSamples(criteria,
+                    subCriterias);
         } catch (final DataAccessException ex)
         {
             throw createUserFailureException(ex);
         }
     }
 
-    private Collection<Long> findSampleIds(DetailedSearchCriteria criteria,
-            List<DetailedSearchSubCriteria> subCriterias)
-    {
-        final IHibernateSearchDAO searchDAO = getDAOFactory().getHibernateSearchDAO();
-        // for now we connect all sub criteria with logical AND
-        List<DetailedSearchAssociationCriteria> associations =
-                new ArrayList<DetailedSearchAssociationCriteria>();
-        for (DetailedSearchSubCriteria subCriteria : subCriterias)
-        {
-            associations.add(findAssociatedEntities(subCriteria));
-        }
-        final Collection<Long> sampleIds =
-                searchDAO.searchForEntityIds(criteria,
-                        DtoConverters.convertEntityKind(EntityKind.SAMPLE), associations);
-        return sampleIds;
-    }
-
-    private DetailedSearchAssociationCriteria findAssociatedEntities(
-            DetailedSearchSubCriteria subCriteria)
-    {
-        final IHibernateSearchDAO searchDAO = getDAOFactory().getHibernateSearchDAO();
-        // for now we don't support sub criteria of sub criteria
-        List<DetailedSearchAssociationCriteria> associations = Collections.emptyList();
-        final Collection<Long> associatedIds =
-                searchDAO.searchForEntityIds(subCriteria.getCriteria(),
-                        DtoConverters.convertEntityKind(subCriteria.getTargetEntityKind()),
-                        associations);
-
-        return new DetailedSearchAssociationCriteria(subCriteria.getTargetEntityKind(),
-                associatedIds);
-    }
-
     public final List<ExternalData> listSampleExternalData(final String sessionToken,
             final TechId sampleId, final boolean showOnlyDirectlyConnected)
     {
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/DetailedSearchManager.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/DetailedSearchManager.java
new file mode 100644
index 00000000000..7889984dd9e
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/DetailedSearchManager.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.generic.server.business;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.springframework.dao.DataAccessException;
+
+import ch.systemsx.cisd.openbis.generic.server.business.bo.samplelister.ISampleLister;
+import ch.systemsx.cisd.openbis.generic.server.dataaccess.IHibernateSearchDAO;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DetailedSearchAssociationCriteria;
+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.DetailedSearchSubCriteria;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ListOrSearchSampleCriteria;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Sample;
+import ch.systemsx.cisd.openbis.generic.shared.translator.DtoConverters;
+
+/**
+ * Manages detailed search with complex search criteria.
+ * 
+ * @author Piotr Buczek
+ */
+public class DetailedSearchManager
+{
+    private final IHibernateSearchDAO searchDAO;
+
+    private final ISampleLister sampleLister;
+
+    public DetailedSearchManager(IHibernateSearchDAO searchDAO, ISampleLister sampleLister)
+    {
+        this.searchDAO = searchDAO;
+        this.sampleLister = sampleLister;
+    }
+
+    public List<Sample> searchForSamples(DetailedSearchCriteria criteria,
+            List<DetailedSearchSubCriteria> subCriterias) throws DataAccessException
+    {
+        final DetailedSearchCriteria parentCriteria = new DetailedSearchCriteria();
+        final List<DetailedSearchSubCriteria> otherSubCriterias =
+                new ArrayList<DetailedSearchSubCriteria>();
+        groupSampleSubCriteria(subCriterias, parentCriteria, otherSubCriterias);
+
+        final List<Long> mainSampleIds = findSampleIds(criteria, otherSubCriterias);
+
+        final Set<Long> filteredSampleIds;
+        if (parentCriteria.isEmpty())
+        {
+            filteredSampleIds = new HashSet<Long>(mainSampleIds);
+        } else
+        {
+            final List<Long> parentSampleIds =
+                    findSampleIds(parentCriteria,
+                            Collections.<DetailedSearchSubCriteria> emptyList());
+
+            filteredSampleIds = new HashSet<Long>();
+
+            if (mainSampleIds.size() > parentSampleIds.size())
+            {
+                // search for connections
+                Map<Long, Set<Long>> parentToChildIds =
+                        sampleLister.listChildrenIds(parentSampleIds);
+                for (Set<Long> childrenIds : parentToChildIds.values())
+                {
+                    filteredSampleIds.addAll(childrenIds);
+                }
+                // filter main parents
+                filteredSampleIds.retainAll(mainSampleIds);
+            } else
+            {
+                // search for connections
+                Map<Long, Set<Long>> childToParentIds = sampleLister.listParentIds(mainSampleIds);
+
+                // filter main parents
+                for (Entry<Long, Set<Long>> entry : childToParentIds.entrySet())
+                {
+                    Long childId = entry.getKey();
+                    Set<Long> parentIds = entry.getValue();
+                    parentIds.retainAll(parentSampleIds);
+                    if (parentIds.isEmpty() == false)
+                    {
+                        filteredSampleIds.add(childId);
+                    }
+                }
+            }
+        }
+        return sampleLister.list(new ListOrSearchSampleCriteria(filteredSampleIds));
+    }
+
+    private void groupSampleSubCriteria(List<DetailedSearchSubCriteria> allSubCriterias,
+            DetailedSearchCriteria parentCriteria, List<DetailedSearchSubCriteria> otherSubCriterias)
+    {
+        parentCriteria.setCriteria(new ArrayList<DetailedSearchCriterion>());
+        for (DetailedSearchSubCriteria subCriteria : allSubCriterias)
+        {
+            if (subCriteria.getTargetEntityKind() == EntityKind.SAMPLE)
+            {
+                // merge all parent sub criteria into one
+                parentCriteria.getCriteria().addAll(subCriteria.getCriteria().getCriteria());
+                parentCriteria.setConnection(subCriteria.getCriteria().getConnection());
+                parentCriteria.setUseWildcardSearchMode(subCriteria.getCriteria()
+                        .isUseWildcardSearchMode());
+            } else
+            {
+                otherSubCriterias.add(subCriteria);
+            }
+        }
+    }
+
+    private List<Long> findSampleIds(DetailedSearchCriteria criteria,
+            List<DetailedSearchSubCriteria> subCriterias)
+    {
+        // for now we connect all sub criteria with logical AND
+        List<DetailedSearchAssociationCriteria> associations =
+                new ArrayList<DetailedSearchAssociationCriteria>();
+        for (DetailedSearchSubCriteria subCriteria : subCriterias)
+        {
+            associations.add(findAssociatedEntities(subCriteria));
+        }
+        final List<Long> sampleIds =
+                searchDAO.searchForEntityIds(criteria,
+                        DtoConverters.convertEntityKind(EntityKind.SAMPLE), associations);
+        return sampleIds;
+    }
+
+    private DetailedSearchAssociationCriteria findAssociatedEntities(
+            DetailedSearchSubCriteria subCriteria)
+    {
+        // for now we don't support sub criteria of sub criteria
+        List<DetailedSearchAssociationCriteria> associations = Collections.emptyList();
+        final Collection<Long> associatedIds =
+                searchDAO.searchForEntityIds(subCriteria.getCriteria(),
+                        DtoConverters.convertEntityKind(subCriteria.getTargetEntityKind()),
+                        associations);
+
+        return new DetailedSearchAssociationCriteria(subCriteria.getTargetEntityKind(),
+                associatedIds);
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/ISampleLister.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/ISampleLister.java
index ea822fab056..14f10d8f3f9 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/ISampleLister.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/ISampleLister.java
@@ -16,7 +16,10 @@
 
 package ch.systemsx.cisd.openbis.generic.server.business.bo.samplelister;
 
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import ch.systemsx.cisd.common.collections.IValidator;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ListOrSearchSampleCriteria;
@@ -36,10 +39,10 @@ public interface ISampleLister
      * Returns a sorted list of {@link Sample}s that match given criteria.
      */
     public List<Sample> list(ListOrSearchSampleCriteria criteria);
-    
+
     /**
-     * Returns the id of the relationship type of specified code. If code starts with an '$'
-     * it is interpreted as an internally defined relationship type.
+     * Returns the id of the relationship type of specified code. If code starts with an '$' it is
+     * interpreted as an internally defined relationship type.
      * 
      * @throws IllegalArgumentException if code not known.
      */
@@ -50,11 +53,21 @@ public interface ISampleLister
      * specified criteria.
      */
     public List<SampleSkeleton> listSampleBy(IValidator<SampleSkeleton> criteria);
-    
+
     /**
-     * Returns all sample relation ships as skeletons (thats is, only primary and foreign keys) fulfilling
-     * specified criteria.
+     * Returns all sample relation ships as skeletons (thats is, only primary and foreign keys)
+     * fulfilling specified criteria.
+     * 
+     * @deprecated This way of loading relationships is slow. There is no filtering on DB level. If
+     *             the <code>criteria</code> use only a collection of parent/children ids than use
+     *             listChildrenIds/listParentIds.
      */
-    public List<SampleRelationShipSkeleton> listSampleRelationShipsBy(IValidator<SampleRelationShipSkeleton> criteria);
+    @Deprecated
+    public List<SampleRelationShipSkeleton> listSampleRelationShipsBy(
+            IValidator<SampleRelationShipSkeleton> criteria);
+
+    public Map<Long, Set<Long>> listParentIds(Collection<Long> childrenIds);
+
+    public Map<Long, Set<Long>> listChildrenIds(Collection<Long> parentIds);
 
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/ISampleListingQuery.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/ISampleListingQuery.java
index d405ed3d043..e114836c3af 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/ISampleListingQuery.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/ISampleListingQuery.java
@@ -101,6 +101,15 @@ public interface ISampleListingQuery extends TransactionQuery, IPropertyListingQ
     public DataIterator<SampleRelationRecord> getParentRelations(long relationshipId,
             LongSet childrenSampleIds);
 
+    /**
+     * Returns the child sample ids of the specified parent sample ids in specified relationship.
+     */
+    @Select(sql = "SELECT * FROM sample_relationships "
+            + "    WHERE relationship_id=?{1} AND sample_id_parent = any(?{2})", parameterBindings =
+        { TypeMapper.class/* default */, LongSetMapper.class }, fetchSize = FETCH_SIZE)
+    public DataIterator<SampleRelationRecord> getChildrenRelations(long relationshipId,
+            LongSet parentSampleIds);
+
     @Select(sql = "select id, saty_id, space_id, dbin_id, expe_id from samples", fetchSize = FETCH_SIZE)
     public DataIterator<SampleRecord> getSampleSkeletons();
 
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/SampleLister.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/SampleLister.java
index c1fc5173275..39ad734c765 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/SampleLister.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/samplelister/SampleLister.java
@@ -16,8 +16,15 @@
 
 package ch.systemsx.cisd.openbis.generic.server.business.bo.samplelister;
 
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import net.lemnik.eodsql.DataIterator;
 
@@ -25,6 +32,7 @@ import ch.rinn.restrictions.Friend;
 import ch.systemsx.cisd.common.collections.IValidator;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.common.entity.SecondaryEntityDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDAOFactory;
+import ch.systemsx.cisd.openbis.generic.shared.basic.BasicConstant;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ListOrSearchSampleCriteria;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Sample;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SampleRelationShipSkeleton;
@@ -95,7 +103,8 @@ public class SampleLister implements ISampleLister
     public List<SampleRelationShipSkeleton> listSampleRelationShipsBy(
             IValidator<SampleRelationShipSkeleton> criteria)
     {
-        DataIterator<SampleRelationRecord> records = dao.getQuery().getSampleRelationshipSkeletons();
+        DataIterator<SampleRelationRecord> records =
+                dao.getQuery().getSampleRelationshipSkeletons();
         List<SampleRelationShipSkeleton> result = new ArrayList<SampleRelationShipSkeleton>();
         for (SampleRelationRecord record : records)
         {
@@ -111,4 +120,67 @@ public class SampleLister implements ISampleLister
         return result;
     }
 
+    public Map<Long, Set<Long>> listParentIds(Collection<Long> childrenIds)
+    {
+        LongOpenHashSet ids = new LongOpenHashSet();
+        for (Long id : childrenIds)
+        {
+            ids.add(id);
+        }
+        final long relationshipTypeID =
+                getRelationshipTypeID(BasicConstant.PARENT_CHILD_INTERNAL_RELATIONSHIP);
+        DataIterator<SampleRelationRecord> relationships =
+                dao.getQuery().getParentRelations(relationshipTypeID, ids);
+        Map<Long, Set<Long>> map = new LinkedHashMap<Long, Set<Long>>();
+        for (SampleRelationRecord relationship : relationships)
+        {
+            Set<Long> parents = map.get(relationship.sample_id_child);
+            if (parents == null)
+            {
+                parents = new LinkedHashSet<Long>();
+                map.put(relationship.sample_id_child, parents);
+            }
+            parents.add(relationship.sample_id_parent);
+        }
+        return map;
+    }
+
+    public Map<Long, Set<Long>> listChildrenIds(Collection<Long> parentIds)
+    {
+        LongOpenHashSet ids = new LongOpenHashSet();
+        for (Long id : parentIds)
+        {
+            ids.add(id);
+        }
+        final long relationshipTypeID =
+                getRelationshipTypeID(BasicConstant.PARENT_CHILD_INTERNAL_RELATIONSHIP);
+        DataIterator<SampleRelationRecord> relationships =
+                dao.getQuery().getChildrenRelations(relationshipTypeID, ids);
+        Map<Long, Set<Long>> map = new LinkedHashMap<Long, Set<Long>>();
+        for (SampleRelationRecord relationship : relationships)
+        {
+            Set<Long> children = map.get(relationship.sample_id_parent);
+            if (children == null)
+            {
+                children = new LinkedHashSet<Long>();
+                map.put(relationship.sample_id_parent, children);
+            }
+            children.add(relationship.sample_id_child);
+        }
+        return map;
+    }
+
+    public Set<Long> listChildrenIdsSet(Collection<Long> parentIds)
+    {
+        LongOpenHashSet ids = new LongOpenHashSet();
+        for (Long id : parentIds)
+        {
+            ids.add(id);
+        }
+        final long relationshipTypeID =
+                getRelationshipTypeID(BasicConstant.PARENT_CHILD_INTERNAL_RELATIONSHIP);
+        DataIterator<Long> resultIterator = dao.getQuery().getChildrenIds(relationshipTypeID, ids);
+        return new LongOpenHashSet(resultIterator);
+    }
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/api/v1/dto/SearchableEntityKind.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/api/v1/dto/SearchableEntityKind.java
index 0e253d3d619..9b40de745aa 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/api/v1/dto/SearchableEntityKind.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/api/v1/dto/SearchableEntityKind.java
@@ -7,5 +7,7 @@ package ch.systemsx.cisd.openbis.generic.shared.api.v1.dto;
  */
 public enum SearchableEntityKind
 {
-    SAMPLE, EXPERIMENT
+    SAMPLE, EXPERIMENT,
+    // sample subcriteria
+    SAMPLE_CONTAINER, SAMPLE_PARENT, // SAMPLE_CHILD
 }
\ No newline at end of file
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/api/v1/GeneralInformationServiceTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/api/v1/GeneralInformationServiceTest.java
index 71594f34970..30f638ea36d 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/api/v1/GeneralInformationServiceTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/api/v1/GeneralInformationServiceTest.java
@@ -240,6 +240,30 @@ public class GeneralInformationServiceTest extends AbstractServerTestCase
         context.assertIsSatisfied();
     }
 
+    @Test
+    public void testSearchForSamplesWithParent()
+    {
+        prepareGetSession();
+        final RecordingMatcher<DetailedSearchCriteria> detailedSearchCriteriaMatcher =
+                RecordingMatcher.create();
+        final RecordingMatcher<List<DetailedSearchSubCriteria>> detailedSearchSubCriteriaMatcher =
+                RecordingMatcher.create();
+        prepareSearchForSamples(detailedSearchCriteriaMatcher, detailedSearchSubCriteriaMatcher);
+        List<Sample> result =
+                service.searchForSamples(SESSION_TOKEN, createSearchCriteriaForSampleWithParent());
+        assertEquals(1, result.size());
+        Sample resultSample = result.get(0);
+        assertEquals("/space/code", resultSample.getIdentifier());
+        assertEquals("ATTRIBUTE CODE: a code AND "
+                + "PROPERTY MY_PROPERTY2: a property value (with wildcards)",
+                detailedSearchCriteriaMatcher.recordedObject().toString());
+        // check experiment subcriteria
+        assertEquals("[SAMPLE: ATTRIBUTE CODE: parent code AND "
+                + "PROPERTY PARENT_PROPERTY: parent property value (with wildcards)]",
+                detailedSearchSubCriteriaMatcher.recordedObject().toString());
+        context.assertIsSatisfied();
+    }
+
     private void prepareSearchForSamples(
             final RecordingMatcher<DetailedSearchCriteria> detailedSearchCriteriaMatcher,
             final RecordingMatcher<List<DetailedSearchSubCriteria>> detailedSearchSubCriteriaMatcher)
@@ -439,6 +463,16 @@ public class GeneralInformationServiceTest extends AbstractServerTestCase
         return sc;
     }
 
+    private SearchCriteria createSearchCriteriaForSampleParent()
+    {
+        SearchCriteria sc = new SearchCriteria();
+        sc.addMatchClause(MatchClause
+                .createAttributeMatch(MatchClauseAttribute.CODE, "parent code"));
+        sc.addMatchClause(MatchClause.createPropertyMatch("PARENT_PROPERTY",
+                "parent property value"));
+        return sc;
+    }
+
     private SearchCriteria createSearchCriteriaForExperiment()
     {
         SearchCriteria sc = new SearchCriteria();
@@ -457,6 +491,14 @@ public class GeneralInformationServiceTest extends AbstractServerTestCase
         return mainCriteria;
     }
 
+    private SearchCriteria createSearchCriteriaForSampleWithParent()
+    {
+        SearchCriteria mainCriteria = createSearchCriteriaForSample();
+        SearchCriteria parentCriteria = createSearchCriteriaForSampleParent();
+        mainCriteria.addSubCriteria(SearchSubCriteria.createSampleCriteria(parentCriteria));
+        return mainCriteria;
+    }
+
     private void assertSpaceAndProjects(String expectedSpaceCode, String expectedProjects,
             SpaceWithProjectsAndRoleAssignments space)
     {
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/api/v1/GeneralInformationServiceTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/api/v1/GeneralInformationServiceTest.java
index 86669c03159..bd91ac98b39 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/api/v1/GeneralInformationServiceTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/api/v1/GeneralInformationServiceTest.java
@@ -135,6 +135,7 @@ public class GeneralInformationServiceTest extends SystemTestCase
     {
         // Search for Samples with only experiment's code limiting the results
         SearchCriteria sc = new SearchCriteria();
+        sc.addMatchClause(MatchClause.createAttributeMatch(MatchClauseAttribute.CODE, "*"));
         SearchCriteria ec = new SearchCriteria();
         ec.addMatchClause(MatchClause.createAttributeMatch(MatchClauseAttribute.CODE, "EXP-TEST-1"));
         sc.addSubCriteria(SearchSubCriteria.createExperimentCriteria(ec));
@@ -148,6 +149,7 @@ public class GeneralInformationServiceTest extends SystemTestCase
     {
         // Search for Samples with only experiment's property limiting the results
         SearchCriteria sc = new SearchCriteria();
+        sc.addMatchClause(MatchClause.createAttributeMatch(MatchClauseAttribute.CODE, "*"));
         SearchCriteria ec = new SearchCriteria();
         ec.addMatchClause(MatchClause.createPropertyMatch("DESCRIPTION", "A simple experiment"));
         sc.addSubCriteria(SearchSubCriteria.createExperimentCriteria(ec));
@@ -156,20 +158,38 @@ public class GeneralInformationServiceTest extends SystemTestCase
     }
 
     @Test
-    public void testSearchForSamplesByPropertyAndExperimentCode()
+    public void testSearchForSamplesByParentCode()
+    {
+        // Search for Samples with only parent's code limiting the results
+        SearchCriteria sc = new SearchCriteria();
+        sc.addMatchClause(MatchClause.createAttributeMatch(MatchClauseAttribute.CODE, "*"));
+        SearchCriteria pc = new SearchCriteria();
+        pc.addMatchClause(MatchClause.createAttributeMatch(MatchClauseAttribute.CODE, "MP002-1"));
+        sc.addSubCriteria(SearchSubCriteria.createSampleCriteria(pc));
+        List<Sample> result = generalInformationService.searchForSamples(sessionToken, sc);
+        assertEquals(2, result.size());
+    }
+
+    @Test
+    public void testSearchForSamplesByExperimentAndParentCode()
     {
         // Search for Samples first
         SearchCriteria sc = new SearchCriteria();
-        sc.addMatchClause(MatchClause.createPropertyMatch("ORGANISM", "*"));
+        sc.addMatchClause(MatchClause.createAttributeMatch(MatchClauseAttribute.CODE, "*"));
         List<Sample> result = generalInformationService.searchForSamples(sessionToken, sc);
-        assertEquals(4, result.size());
-        // Add experiment criteria limiting results to only 1
+        assertEquals(true, result.size() > 1000);
+        // Add experiment criteria limiting results to 7
         SearchCriteria ec = new SearchCriteria();
-        ec.addMatchClause(MatchClause.createAttributeMatch(MatchClauseAttribute.CODE, "EXP-TEST-1"));
+        ec.addMatchClause(MatchClause.createAttributeMatch(MatchClauseAttribute.CODE, "EXP-REUSE"));
         sc.addSubCriteria(SearchSubCriteria.createExperimentCriteria(ec));
         List<Sample> result2 = generalInformationService.searchForSamples(sessionToken, sc);
-        assertEquals(1, result2.size());
-        assertEquals("/CISD/NEMO/EXP-TEST-1", result2.get(0).getExperimentIdentifierOrNull());
+        assertEquals(7, result2.size());
+        // Add parent criteria limiting results to only 2
+        SearchCriteria pc = new SearchCriteria();
+        pc.addMatchClause(MatchClause.createAttributeMatch(MatchClauseAttribute.CODE, "DP1-A"));
+        sc.addSubCriteria(SearchSubCriteria.createSampleCriteria(pc));
+        List<Sample> result3 = generalInformationService.searchForSamples(sessionToken, sc);
+        assertEquals(2, result3.size());
     }
 
     @Test
-- 
GitLab