From 23f55de3856e3bcee07dbc21a76f9eb7e83939a4 Mon Sep 17 00:00:00 2001
From: pkupczyk <pkupczyk>
Date: Fri, 27 Mar 2015 16:03:18 +0000
Subject: [PATCH] SSDM-1677 : V3 AS API - finish up projects and materials -
 searchProjects

SVN: 33759
---
 .../server/api/v3/ApplicationServerApi.java   |  67 ++++--
 .../api/v3/ApplicationServerApiLogger.java    |   8 +
 .../AbstractSearchObjectManuallyExecutor.java | 191 ++++++++++++++++++
 .../project/ISearchProjectExecutor.java       |  29 +++
 .../project/SearchProjectExecutor.java        | 149 ++++++++++++++
 .../executor/space/SearchSpaceExecutor.java   | 136 +++----------
 .../systemtest/api/v3/SearchProjectTest.java  | 163 +++++++++++++++
 .../systemtest/api/v3/SearchSpaceTest.java    |   8 +-
 .../shared/api/v3/IApplicationServerApi.java  |   3 +
 .../v3/dto/search/ProjectSearchCriterion.java |  10 +
 10 files changed, 634 insertions(+), 130 deletions(-)
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/common/AbstractSearchObjectManuallyExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/ISearchProjectExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/SearchProjectExecutor.java
 create mode 100644 openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchProjectTest.java

diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java
index bfc052c454a..c3dacc5ec38 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java
@@ -45,6 +45,7 @@ import ch.ethz.sis.openbis.generic.server.api.v3.executor.material.IMapMaterialB
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.material.IUpdateMaterialExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.project.ICreateProjectExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.project.IMapProjectByIdExecutor;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.project.ISearchProjectExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.project.IUpdateProjectExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.sample.ICreateSampleExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.sample.IDeleteSampleExecutor;
@@ -114,6 +115,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.operation.IOperation;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.operation.IOperationResult;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.DataSetSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ExperimentSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ProjectSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SampleSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SpaceSearchCriterion;
 import ch.systemsx.cisd.openbis.common.spring.IInvocationLoggerContext;
@@ -162,6 +164,9 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Autowired
     private ICreateSampleExecutor createSampleExecutor;
 
+    @Autowired
+    private ICreateMaterialExecutor createMaterialExecutor;
+
     @Autowired
     private IUpdateSpaceExecutor updateSpaceExecutor;
 
@@ -175,10 +180,10 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     private IUpdateSampleExecutor updateSampleExecutor;
 
     @Autowired
-    private IUpdateMaterialExecutor updateMaterialExecutor;
+    private IUpdateDataSetExecutor updateDataSetExecutor;
 
     @Autowired
-    private IUpdateDataSetExecutor updateDataSetExecutor;
+    private IUpdateMaterialExecutor updateMaterialExecutor;
 
     @Autowired
     private IMapSpaceByIdExecutor mapSpaceByIdExecutor;
@@ -189,9 +194,6 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Autowired
     private IMapExperimentByIdExecutor mapExperimentByIdExecutor;
 
-    @Autowired
-    private IDeleteDataSetExecutor deleteDataSetExecutor;
-
     @Autowired
     private IMapSampleByIdExecutor mapSampleByIdExecutor;
 
@@ -201,14 +203,11 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Autowired
     private IMapMaterialByIdExecutor mapMaterialByIdExecutor;
 
-    @Autowired
-    private ICreateMaterialExecutor createMaterialExecutor;
-
     @Autowired
     private ISearchSpaceExecutor searchSpaceExecutor;
 
     @Autowired
-    private IDeleteMaterialExecutor deleteMaterialExecutor;
+    private ISearchProjectExecutor searchProjectExecutor;
 
     @Autowired
     private ISearchExperimentExecutor searchExperimentExecutor;
@@ -228,6 +227,12 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Autowired
     private IDeleteSampleExecutor deleteSampleExecutor;
 
+    @Autowired
+    private IDeleteDataSetExecutor deleteDataSetExecutor;
+
+    @Autowired
+    private IDeleteMaterialExecutor deleteMaterialExecutor;
+
     @Autowired
     private IListDeletionExecutor listDeletionExecutor;
 
@@ -298,6 +303,10 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     }
 
     @Override
+    @Transactional
+    @RolesAllowed({ RoleWithHierarchy.INSTANCE_ADMIN, RoleWithHierarchy.SPACE_ETL_SERVER })
+    @Capability("WRITE_MATERIAL")
+    @DatabaseCreateOrDeleteModification(value = ObjectKind.MATERIAL)
     public List<MaterialPermId> createMaterials(String sessionToken, List<MaterialCreation> newMaterials)
     {
         Session session = getSession(sessionToken);
@@ -315,6 +324,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Transactional
     @RolesAllowed({ RoleWithHierarchy.SPACE_POWER_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
     @Capability("REGISTER_PROJECT")
+    @DatabaseCreateOrDeleteModification(value = ObjectKind.PROJECT)
     public List<ProjectPermId> createProjects(String sessionToken, List<ProjectCreation> creations)
     {
         Session session = getSession(sessionToken);
@@ -331,8 +341,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
 
     @Override
     @Transactional
-    @RolesAllowed(
-    { RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
+    @RolesAllowed({ RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
     @Capability("WRITE_EXPERIMENT")
     @DatabaseCreateOrDeleteModification(value = ObjectKind.EXPERIMENT)
     public List<ExperimentPermId> createExperiments(String sessionToken,
@@ -355,8 +364,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
 
     @Override
     @Transactional
-    @RolesAllowed(
-    { RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
+    @RolesAllowed({ RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
     @Capability("WRITE_SAMPLE")
     @DatabaseCreateOrDeleteModification(value = ObjectKind.SAMPLE)
     public List<SamplePermId> createSamples(String sessionToken,
@@ -401,6 +409,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Transactional
     @RolesAllowed({ RoleWithHierarchy.SPACE_POWER_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
     @Capability("WRITE_PROJECT")
+    @DatabaseUpdateModification(value = ObjectKind.PROJECT)
     public void updateProjects(String sessionToken, List<ProjectUpdate> projectUpdates)
     {
         Session session = getSession(sessionToken);
@@ -417,8 +426,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
 
     @Override
     @Transactional
-    @RolesAllowed(
-    { RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
+    @RolesAllowed({ RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
     @Capability("WRITE_EXPERIMENT")
     @DatabaseUpdateModification(value = ObjectKind.EXPERIMENT)
     public void updateExperiments(String sessionToken, List<ExperimentUpdate> experimentUpdates)
@@ -441,8 +449,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
 
     @Override
     @Transactional
-    @RolesAllowed(
-    { RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
+    @RolesAllowed({ RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
     @Capability("WRITE_SAMPLE")
     @DatabaseUpdateModification(value = ObjectKind.SAMPLE)
     public void updateSamples(String sessionToken, List<SampleUpdate> updates)
@@ -465,8 +472,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
 
     @Override
     @Transactional
-    @RolesAllowed(
-    { RoleWithHierarchy.INSTANCE_ADMIN, RoleWithHierarchy.INSTANCE_ETL_SERVER })
+    @RolesAllowed({ RoleWithHierarchy.INSTANCE_ADMIN, RoleWithHierarchy.INSTANCE_ETL_SERVER })
     @Capability("UPDATE_MATERIAL")
     @DatabaseUpdateModification(value = ObjectKind.MATERIAL)
     public void updateMaterials(String sessionToken, List<MaterialUpdate> updates)
@@ -580,6 +586,8 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     }
 
     @Override
+    @Transactional(readOnly = true)
+    @RolesAllowed({ RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
     public Map<IMaterialId, Material> mapMaterials(String sessionToken, List<? extends IMaterialId> materialIds, MaterialFetchOptions fetchOptions)
     {
         Session session = getSession(sessionToken);
@@ -613,6 +621,27 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
         }
     }
 
+    @Override
+    @Transactional(readOnly = true)
+    @RolesAllowed({ RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
+    public List<Project> searchProjects(String sessionToken, ProjectSearchCriterion searchCriterion, ProjectFetchOptions fetchOptions)
+    {
+        Session session = getSession(sessionToken);
+        OperationContext context = new OperationContext(session);
+
+        try
+        {
+            List<ProjectPE> projects = searchProjectExecutor.search(context, searchCriterion);
+
+            Map<ProjectPE, Project> translatedMap =
+                    new ProjectTranslator(new TranslationContext(session, managedPropertyEvaluatorFactory), fetchOptions).translate(projects);
+            return new ArrayList<Project>(translatedMap.values());
+        } catch (Throwable t)
+        {
+            throw ExceptionUtils.create(context, t);
+        }
+    }
+
     @Override
     @Transactional(readOnly = true)
     @RolesAllowed({ RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java
index 0c63b8d938b..cbb7e4e4783 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java
@@ -66,6 +66,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.operation.IOperation;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.operation.IOperationResult;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.DataSetSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ExperimentSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ProjectSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SampleSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SpaceSearchCriterion;
 import ch.systemsx.cisd.authentication.ISessionManager;
@@ -238,6 +239,13 @@ public class ApplicationServerApiLogger extends AbstractServerLogger implements
         return null;
     }
 
+    @Override
+    public List<Project> searchProjects(String sessionToken, ProjectSearchCriterion searchCriterion, ProjectFetchOptions fetchOptions)
+    {
+        logAccess(sessionToken, "search-for-projects", "SEARCH_CRITERION:\n%s\nFETCH_OPTIONS:\n%s\n", searchCriterion, fetchOptions);
+        return null;
+    }
+
     @Override
     public List<Experiment> searchExperiments(String sessionToken, ExperimentSearchCriterion searchCriterion, ExperimentFetchOptions fetchOptions)
     {
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/common/AbstractSearchObjectManuallyExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/common/AbstractSearchObjectManuallyExecutor.java
new file mode 100644
index 00000000000..0805c380266
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/common/AbstractSearchObjectManuallyExecutor.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2015 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.ethz.sis.openbis.generic.server.api.v3.executor.common;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.AbstractObjectSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.AbstractStringValue;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.AnyStringValue;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ISearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchOperator;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.StringContainsValue;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.StringEndsWithValue;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.StringEqualToValue;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.StringFieldSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.StringStartsWithValue;
+import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDAOFactory;
+
+/**
+ * @author pkupczyk
+ */
+public abstract class AbstractSearchObjectManuallyExecutor<CRITERION extends AbstractObjectSearchCriterion<?>, OBJECT> implements
+        ISearchObjectExecutor<CRITERION, OBJECT>
+{
+
+    @Autowired
+    protected IDAOFactory daoFactory;
+
+    protected abstract List<OBJECT> listAll();
+
+    protected abstract Matcher getMatcher(ISearchCriterion criterion);
+
+    @Override
+    public List<OBJECT> search(IOperationContext context, CRITERION criterion)
+    {
+        if (context == null)
+        {
+            throw new IllegalArgumentException("Context cannot be null");
+        }
+        if (criterion == null)
+        {
+            throw new IllegalArgumentException("Criterion cannot be null");
+        }
+
+        return getMatching(context, listAll(), criterion);
+    }
+
+    private List<OBJECT> getMatching(IOperationContext context, List<OBJECT> objects, CRITERION criterion)
+    {
+        if (criterion.getCriteria() == null || criterion.getCriteria().isEmpty())
+        {
+            return objects;
+        } else
+        {
+            List<List<OBJECT>> partialMatches = new LinkedList<List<OBJECT>>();
+
+            for (ISearchCriterion subCriterion : criterion.getCriteria())
+            {
+                Matcher matcher = getMatcher(subCriterion);
+
+                List<OBJECT> partialMatch = matcher.getMatching(context, objects, subCriterion);
+                if (partialMatch == null)
+                {
+                    partialMatch = Collections.emptyList();
+                }
+                partialMatches.add(partialMatch);
+            }
+
+            if (SearchOperator.AND.equals(criterion.getOperator()))
+            {
+                Set<OBJECT> matches = new HashSet<OBJECT>(partialMatches.get(0));
+                for (List<OBJECT> partialMatch : partialMatches)
+                {
+                    matches.retainAll(partialMatch);
+                }
+                return new ArrayList<OBJECT>(matches);
+            } else if (SearchOperator.OR.equals(criterion.getOperator()))
+            {
+                Set<OBJECT> matches = new HashSet<OBJECT>();
+                for (List<OBJECT> partialMatch : partialMatches)
+                {
+                    matches.addAll(partialMatch);
+                }
+                return new ArrayList<OBJECT>(matches);
+            } else
+            {
+                throw new IllegalArgumentException("Unknown search operator: " + criterion.getOperator());
+            }
+        }
+    }
+
+    protected abstract class Matcher
+    {
+
+        public abstract List<OBJECT> getMatching(IOperationContext context, List<OBJECT> objects, ISearchCriterion criterion);
+
+    }
+
+    protected abstract class SimpleFieldMatcher extends Matcher
+    {
+
+        @Override
+        public List<OBJECT> getMatching(IOperationContext context, List<OBJECT> objects, ISearchCriterion criterion)
+        {
+            List<OBJECT> matches = new ArrayList<OBJECT>();
+
+            for (OBJECT object : objects)
+            {
+                if (isMatching(context, object, criterion))
+                {
+                    matches.add(object);
+                }
+            }
+
+            return matches;
+        }
+
+        protected abstract boolean isMatching(IOperationContext context, OBJECT object, ISearchCriterion criterion);
+
+    }
+
+    protected abstract class StringFieldMatcher extends SimpleFieldMatcher
+    {
+
+        @Override
+        protected boolean isMatching(IOperationContext context, OBJECT object, ISearchCriterion criterion)
+        {
+            AbstractStringValue fieldValue = ((StringFieldSearchCriterion) criterion).getFieldValue();
+
+            if (fieldValue == null || fieldValue.getValue() == null || fieldValue instanceof AnyStringValue)
+            {
+                return true;
+            }
+
+            String actualValue = getFieldValue(object);
+
+            if (actualValue == null)
+            {
+                actualValue = "";
+            } else
+            {
+                actualValue = actualValue.toLowerCase();
+            }
+
+            String searchedValue = fieldValue.getValue().toLowerCase();
+
+            if (fieldValue instanceof StringEqualToValue)
+            {
+                return actualValue.equals(searchedValue);
+            } else if (fieldValue instanceof StringContainsValue)
+            {
+                return actualValue.contains(searchedValue);
+            } else if (fieldValue instanceof StringStartsWithValue)
+            {
+                return actualValue.startsWith(searchedValue);
+            } else if (fieldValue instanceof StringEndsWithValue)
+            {
+                return actualValue.endsWith(searchedValue);
+            } else
+            {
+                throw new IllegalArgumentException("Unknown string value: " + criterion.getClass());
+            }
+        }
+
+        protected abstract String getFieldValue(OBJECT object);
+
+    }
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/ISearchProjectExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/ISearchProjectExecutor.java
new file mode 100644
index 00000000000..84aa8393db7
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/ISearchProjectExecutor.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2014 ETH Zuerich, Scientific IT Services
+ *
+ * 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.ethz.sis.openbis.generic.server.api.v3.executor.project;
+
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.common.ISearchObjectExecutor;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ProjectSearchCriterion;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
+
+/**
+ * @author pkupczyk
+ */
+public interface ISearchProjectExecutor extends ISearchObjectExecutor<ProjectSearchCriterion, ProjectPE>
+{
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/SearchProjectExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/SearchProjectExecutor.java
new file mode 100644
index 00000000000..0d48b706d34
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/SearchProjectExecutor.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2015 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.ethz.sis.openbis.generic.server.api.v3.executor.project;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.common.AbstractSearchObjectManuallyExecutor;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.space.ISearchSpaceExecutor;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectIdentifier;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectPermId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.CodeSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ISearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.IdSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.PermIdSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ProjectSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SpaceSearchCriterion;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
+
+/**
+ * @author pkupczyk
+ */
+@Component
+public class SearchProjectExecutor extends AbstractSearchObjectManuallyExecutor<ProjectSearchCriterion, ProjectPE> implements ISearchProjectExecutor
+{
+
+    @Autowired
+    private ISearchSpaceExecutor searchSpaceExecutor;
+
+    @Override
+    protected List<ProjectPE> listAll()
+    {
+        return daoFactory.getProjectDAO().listAllEntities();
+    }
+
+    @Override
+    protected Matcher getMatcher(ISearchCriterion criterion)
+    {
+        if (criterion instanceof IdSearchCriterion<?>)
+        {
+            return new IdMatcher();
+        } else if (criterion instanceof CodeSearchCriterion)
+        {
+            return new CodeMatcher();
+        } else if (criterion instanceof PermIdSearchCriterion)
+        {
+            return new PermIdMatcher();
+        } else if (criterion instanceof SpaceSearchCriterion)
+        {
+            return new SpaceMatcher();
+        } else
+        {
+            throw new IllegalArgumentException("Unknown search criterion: " + criterion.getClass());
+        }
+    }
+
+    private class IdMatcher extends SimpleFieldMatcher
+    {
+
+        @Override
+        protected boolean isMatching(IOperationContext context, ProjectPE object, ISearchCriterion criterion)
+        {
+            Object id = ((IdSearchCriterion<?>) criterion).getId();
+
+            if (id == null)
+            {
+                return true;
+            } else if (id instanceof ProjectPermId)
+            {
+                return object.getPermId().equals(((ProjectPermId) id).getPermId());
+            } else if (id instanceof ProjectIdentifier)
+            {
+                return object.getIdentifier().equals(((ProjectIdentifier) id).getIdentifier());
+            } else
+            {
+                throw new IllegalArgumentException("Unknown id: " + criterion.getClass());
+            }
+        }
+
+    }
+
+    private class CodeMatcher extends StringFieldMatcher
+    {
+
+        @Override
+        protected String getFieldValue(ProjectPE object)
+        {
+            return object.getCode();
+        }
+
+    }
+
+    private class PermIdMatcher extends StringFieldMatcher
+    {
+
+        @Override
+        protected String getFieldValue(ProjectPE object)
+        {
+            return object.getPermId();
+        }
+
+    }
+
+    private class SpaceMatcher extends Matcher
+    {
+
+        @Override
+        public List<ProjectPE> getMatching(IOperationContext context, List<ProjectPE> objects, ISearchCriterion criterion)
+        {
+            List<SpacePE> spaceList = searchSpaceExecutor.search(context, (SpaceSearchCriterion) criterion);
+            Set<SpacePE> spaceSet = new HashSet<SpacePE>(spaceList);
+
+            List<ProjectPE> matches = new ArrayList<ProjectPE>();
+
+            for (ProjectPE object : objects)
+            {
+                if (spaceSet.contains(object.getSpace()))
+                {
+                    matches.add(object);
+                }
+            }
+
+            return matches;
+        }
+
+    }
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/space/SearchSpaceExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/space/SearchSpaceExecutor.java
index ca7e9de1c59..d308e4eaa69 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/space/SearchSpaceExecutor.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/space/SearchSpaceExecutor.java
@@ -16,157 +16,79 @@
 
 package ch.ethz.sis.openbis.generic.server.api.v3.executor.space;
 
-import java.util.ArrayList;
 import java.util.List;
 
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.common.AbstractSearchObjectManuallyExecutor;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.SpacePermId;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.AbstractStringValue;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.AnyStringValue;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.CodeSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ISearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.IdSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.PermIdSearchCriterion;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SearchOperator;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SpaceSearchCriterion;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.StringContainsValue;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.StringEndsWithValue;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.StringEqualToValue;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.StringFieldSearchCriterion;
-import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.StringStartsWithValue;
-import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDAOFactory;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
 
 /**
  * @author pkupczyk
  */
 @Component
-public class SearchSpaceExecutor implements ISearchSpaceExecutor
+public class SearchSpaceExecutor extends AbstractSearchObjectManuallyExecutor<SpaceSearchCriterion, SpacePE> implements ISearchSpaceExecutor
 {
 
-    @Autowired
-    protected IDAOFactory daoFactory;
+    @Override
+    protected List<SpacePE> listAll()
+    {
+        return daoFactory.getSpaceDAO().listAllEntities();
+    }
 
     @Override
-    public List<SpacePE> search(IOperationContext context, SpaceSearchCriterion criterion)
+    protected Matcher getMatcher(ISearchCriterion criterion)
     {
-        if (context == null)
+        if (criterion instanceof IdSearchCriterion<?>)
         {
-            throw new IllegalArgumentException("Context cannot be null");
-        }
-        if (criterion == null)
+            return new IdMatcher();
+        } else if (criterion instanceof PermIdSearchCriterion || criterion instanceof CodeSearchCriterion)
         {
-            throw new IllegalArgumentException("Criterion cannot be null");
-        }
-
-        List<SpacePE> allSpaces = daoFactory.getSpaceDAO().listAllEntities();
-        List<SpacePE> matchingSpaces = new ArrayList<SpacePE>();
-
-        for (SpacePE space : allSpaces)
+            return new CodeMatcher();
+        } else
         {
-            if (isMatching(space, criterion))
-            {
-                matchingSpaces.add(space);
-            }
+            throw new IllegalArgumentException("Unknown search criterion: " + criterion.getClass());
         }
-
-        return matchingSpaces;
     }
 
-    private boolean isMatching(SpacePE space, SpaceSearchCriterion criterion)
+    private class IdMatcher extends SimpleFieldMatcher
     {
-        if (criterion.getCriteria() == null || criterion.getCriteria().isEmpty())
-        {
-            return true;
-        } else
-        {
-            boolean matchingAll = true;
-            boolean matchingAny = false;
 
-            for (ISearchCriterion subCriterion : criterion.getCriteria())
-            {
-                boolean matching = isMatching(space, subCriterion);
-
-                matchingAll = matchingAll && matching;
-                matchingAny = matchingAny || matching;
-            }
+        @Override
+        protected boolean isMatching(IOperationContext context, SpacePE object, ISearchCriterion criterion)
+        {
+            Object id = ((IdSearchCriterion<?>) criterion).getId();
 
-            if (SearchOperator.AND.equals(criterion.getOperator()))
+            if (id == null)
             {
-                return matchingAll;
-            } else if (SearchOperator.OR.equals(criterion.getOperator()))
+                return true;
+            } else if (id instanceof SpacePermId)
             {
-                return matchingAny;
+                return object.getCode().equals(((SpacePermId) id).getPermId());
             } else
             {
-                throw new IllegalArgumentException("Unknown search operator: " + criterion.getOperator());
+                throw new IllegalArgumentException("Unknown id: " + id.getClass());
             }
         }
-    }
-
-    private boolean isMatching(SpacePE space, ISearchCriterion criterion)
-    {
-        if (criterion == null)
-        {
-            return true;
-        } else if (criterion instanceof IdSearchCriterion<?>)
-        {
-            return isMatchingId(space, (IdSearchCriterion<?>) criterion);
-        } else if (criterion instanceof PermIdSearchCriterion || criterion instanceof CodeSearchCriterion)
-        {
-            return isMatchingCode(space, (StringFieldSearchCriterion) criterion);
-        } else
-        {
-            throw new IllegalArgumentException("Unknown search criterion: " + criterion.getClass());
-        }
-    }
 
-    private boolean isMatchingId(SpacePE space, IdSearchCriterion<?> criterion)
-    {
-        Object id = criterion.getId();
-
-        if (id == null)
-        {
-            return true;
-        } else if (id instanceof SpacePermId)
-        {
-            return space.getCode().equals(((SpacePermId) id).getPermId());
-        } else
-        {
-            throw new IllegalArgumentException("Unknown search criterion: " + criterion.getClass());
-        }
     }
 
-    private boolean isMatchingCode(SpacePE space, StringFieldSearchCriterion criterion)
+    private class CodeMatcher extends StringFieldMatcher
     {
-        AbstractStringValue fieldValue = criterion.getFieldValue();
 
-        if (fieldValue == null || fieldValue.getValue() == null || fieldValue instanceof AnyStringValue)
+        @Override
+        protected String getFieldValue(SpacePE object)
         {
-            return true;
+            return object.getCode();
         }
 
-        String code = space.getCode().toLowerCase();
-        String value = fieldValue.getValue().toLowerCase();
-
-        if (fieldValue instanceof StringEqualToValue)
-        {
-            return code.equals(value);
-        } else if (fieldValue instanceof StringContainsValue)
-        {
-            return code.contains(value);
-        } else if (fieldValue instanceof StringStartsWithValue)
-        {
-            return code.startsWith(value);
-        } else if (fieldValue instanceof StringEndsWithValue)
-        {
-            return code.endsWith(value);
-        } else
-        {
-            throw new IllegalArgumentException("Unknown search criterion: " + criterion.getClass());
-        }
     }
+
 }
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchProjectTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchProjectTest.java
new file mode 100644
index 00000000000..b6d2cf528ba
--- /dev/null
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchProjectTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2014 ETH Zuerich, Scientific IT Services
+ *
+ * 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.ethz.sis.openbis.systemtest.api.v3;
+
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.Project;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.project.ProjectFetchOptions;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectIdentifier;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectPermId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.SpacePermId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ProjectSearchCriterion;
+
+/**
+ * @author pkupczyk
+ */
+public class SearchProjectTest extends AbstractTest
+{
+
+    @Test
+    public void testSearchWithIdSetToPermId()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withId().thatEquals(new ProjectPermId("20120814110011738-105"));
+        testSearch(TEST_USER, criterion, "/TEST-SPACE/TEST-PROJECT");
+    }
+
+    @Test
+    public void testSearchWithIdSetToIdentifier()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withId().thatEquals(new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT"));
+        testSearch(TEST_USER, criterion, "/TEST-SPACE/TEST-PROJECT");
+    }
+
+    @Test
+    public void testSearchWithIdSetToNonexistentPermId()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withId().thatEquals(new ProjectPermId("IDONTEXIST"));
+        testSearch(TEST_USER, criterion);
+    }
+
+    @Test
+    public void testSearchWithIdSetToNonexistentIdentifier()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withId().thatEquals(new ProjectIdentifier("/IDONT/EXIST"));
+        testSearch(TEST_USER, criterion);
+    }
+
+    @Test
+    public void testSearchWithPermIdThatEquals()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withPermId().thatEquals("20120814110011738-105");
+        testSearch(TEST_USER, criterion, "/TEST-SPACE/TEST-PROJECT");
+    }
+
+    @Test
+    public void testSearchWithCodeThatEquals()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withCode().thatEquals("test-PROJECT");
+        testSearch(TEST_USER, criterion, "/TEST-SPACE/TEST-PROJECT");
+    }
+
+    @Test
+    public void testSearchWithCodeThatContains()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withCode().thatContains("pRoJ");
+        testSearch(TEST_USER, criterion, "/TESTGROUP/TESTPROJ", "/TEST-SPACE/TEST-PROJECT", "/TEST-SPACE/PROJECT-TO-DELETE");
+    }
+
+    @Test
+    public void testSearchWithCodeThatStartsWith()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withCode().thatStartsWith("n");
+        testSearch(TEST_USER, criterion, "/CISD/NEMO", "/CISD/NOE", "/TEST-SPACE/NOE");
+    }
+
+    @Test
+    public void testSearchWithCodeThatEndsWith()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withCode().thatEndsWith("t");
+        testSearch(TEST_USER, criterion, "/CISD/DEFAULT", "/TEST-SPACE/TEST-PROJECT");
+    }
+
+    @Test
+    public void testSearchWithSpaceWithIdThatEquals()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withSpace().withId().thatEquals(new SpacePermId("CISD"));
+        testSearch(TEST_USER, criterion, "/CISD/DEFAULT", "/CISD/NEMO", "/CISD/NOE");
+    }
+
+    @Test
+    public void testSearchWithSpaceWithCodeThatStartsWith()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withSpace().withCode().thatStartsWith("TEST");
+        testSearch(TEST_USER, criterion, "/TESTGROUP/TESTPROJ", "/TEST-SPACE/TEST-PROJECT", "/TEST-SPACE/NOE", "/TEST-SPACE/PROJECT-TO-DELETE");
+    }
+
+    @Test
+    public void testSearchWithAndOperator()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withAndOperator();
+        criterion.withCode().thatContains("TEST");
+        criterion.withCode().thatContains("PROJECT");
+        testSearch(TEST_USER, criterion, "/TEST-SPACE/TEST-PROJECT");
+    }
+
+    @Test
+    public void testSearchWithOrOperator()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withOrOperator();
+        criterion.withPermId().thatEquals("20120814110011738-101");
+        criterion.withPermId().thatEquals("20120814110011738-105");
+        testSearch(TEST_USER, criterion, "/CISD/DEFAULT", "/TEST-SPACE/TEST-PROJECT");
+    }
+
+    @Test
+    public void testSearchWithProjectUnauthorized()
+    {
+        ProjectSearchCriterion criterion = new ProjectSearchCriterion();
+        criterion.withId().thatEquals(new ProjectIdentifier("/CISD/DEFAULT"));
+        testSearch(TEST_USER, criterion, "/CISD/DEFAULT");
+        testSearch(TEST_SPACE_USER, criterion);
+    }
+
+    private void testSearch(String user, ProjectSearchCriterion criterion, String... expectedIdentifiers)
+    {
+        String sessionToken = v3api.login(user, PASSWORD);
+
+        List<Project> projects = v3api.searchProjects(sessionToken, criterion, new ProjectFetchOptions());
+
+        assertProjectIdentifiers(projects, expectedIdentifiers);
+        v3api.logout(sessionToken);
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSpaceTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSpaceTest.java
index 362c981e622..d15cdb8d8d0 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSpaceTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/SearchSpaceTest.java
@@ -83,7 +83,7 @@ public class SearchSpaceTest extends AbstractTest
     public void testSearchWithCodeThatEquals()
     {
         SpaceSearchCriterion criterion = new SpaceSearchCriterion();
-        criterion.withPermId().thatEquals("test-SPACE");
+        criterion.withCode().thatEquals("test-SPACE");
         testSearch(TEST_USER, criterion, "TEST-SPACE");
     }
 
@@ -91,7 +91,7 @@ public class SearchSpaceTest extends AbstractTest
     public void testSearchWithCodeThatContains()
     {
         SpaceSearchCriterion criterion = new SpaceSearchCriterion();
-        criterion.withPermId().thatContains("ST-sPa");
+        criterion.withCode().thatContains("ST-sPa");
         testSearch(TEST_USER, criterion, "TEST-SPACE");
     }
 
@@ -99,7 +99,7 @@ public class SearchSpaceTest extends AbstractTest
     public void testSearchWithCodeThatStartsWith()
     {
         SpaceSearchCriterion criterion = new SpaceSearchCriterion();
-        criterion.withPermId().thatStartsWith("c");
+        criterion.withCode().thatStartsWith("c");
         testSearch(TEST_USER, criterion, "CISD");
     }
 
@@ -107,7 +107,7 @@ public class SearchSpaceTest extends AbstractTest
     public void testSearchWithCodeThatEndsWith()
     {
         SpaceSearchCriterion criterion = new SpaceSearchCriterion();
-        criterion.withPermId().thatEndsWith("e");
+        criterion.withCode().thatEndsWith("e");
         testSearch(TEST_USER, criterion, "TEST-SPACE");
     }
 
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java
index ad7e9c0775a..05716d0c42b 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java
@@ -65,6 +65,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.operation.IOperation;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.operation.IOperationResult;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.DataSetSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ExperimentSearchCriterion;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.ProjectSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SampleSearchCriterion;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.search.SpaceSearchCriterion;
 import ch.systemsx.cisd.common.api.IRpcService;
@@ -165,6 +166,8 @@ public interface IApplicationServerApi extends IRpcService
 
     public List<Space> searchSpaces(String sessionToken, SpaceSearchCriterion searchCriterion, SpaceFetchOptions fetchOptions);
 
+    public List<Project> searchProjects(String sessionToken, ProjectSearchCriterion searchCriterion, ProjectFetchOptions fetchOptions);
+
     // REPLACES:
     // - ServiceForDataStoreServer.listExperimentsForProjects(List<ProjectIdentifier>, ExperimentFetchOptions)
     // - ServiceForDataStoreServer.listExperiments(ProjectIdentifier)
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/search/ProjectSearchCriterion.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/search/ProjectSearchCriterion.java
index 1d659494c93..758bd9aaf82 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/search/ProjectSearchCriterion.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/search/ProjectSearchCriterion.java
@@ -47,6 +47,16 @@ public class ProjectSearchCriterion extends AbstractObjectSearchCriterion<IProje
         return with(new SpaceSearchCriterion());
     }
 
+    public ProjectSearchCriterion withOrOperator()
+    {
+        return (ProjectSearchCriterion) withOperator(SearchOperator.OR);
+    }
+
+    public ProjectSearchCriterion withAndOperator()
+    {
+        return (ProjectSearchCriterion) withOperator(SearchOperator.AND);
+    }
+
     @Override
     protected SearchCriterionToStringBuilder createBuilder()
     {
-- 
GitLab