diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/common/search/StringFieldMatcher.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/common/search/StringFieldMatcher.java
index c9148b98bb046f942aad66733142052e6a39c5c0..9ba2a7c0690827d9d26beae6ec42b7b9e1e343cd 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/common/search/StringFieldMatcher.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/common/search/StringFieldMatcher.java
@@ -16,6 +16,10 @@
 
 package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.common.search;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.AbstractStringValue;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.AnyStringValue;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.ISearchCriteria;
@@ -26,47 +30,114 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.StringFieldSearchC
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.StringStartsWithValue;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
 
-public abstract class StringFieldMatcher<OBJECT> extends SimpleFieldMatcher<OBJECT>
+public abstract class StringFieldMatcher<OBJECT> extends Matcher<OBJECT>
 {
 
     @Override
-    protected boolean isMatching(IOperationContext context, OBJECT object, ISearchCriteria criteria)
+    public List<OBJECT> getMatching(IOperationContext context, List<OBJECT> objects, ISearchCriteria criteria)
     {
-        AbstractStringValue fieldValue = ((StringFieldSearchCriteria) criteria).getFieldValue();
+        AbstractStringValue searchValueObject = ((StringFieldSearchCriteria) criteria).getFieldValue();
 
-        if (fieldValue == null || fieldValue.getValue() == null || fieldValue instanceof AnyStringValue)
+        if (searchValueObject == null || searchValueObject.getValue() == null || searchValueObject instanceof AnyStringValue)
         {
-            return true;
+            return objects;
         }
 
-        String actualValue = getFieldValue(object);
+        String searchValue = searchValueObject.getValue().toLowerCase();
+        Pattern equalsPattern = createPattern(searchValue);
+        Pattern startsPattern = createPattern(searchValue + "*");
+        Pattern endsPattern = createPattern("*" + searchValue);
+        Pattern containsPattern = createPattern("*" + searchValue + "*");
 
-        if (actualValue == null)
-        {
-            actualValue = "";
-        } else
+        List<OBJECT> matches = new ArrayList<OBJECT>();
+
+        for (OBJECT object : objects)
         {
-            actualValue = actualValue.toLowerCase();
+            String actualValue = getFieldValue(object);
+
+            if (actualValue == null)
+            {
+                actualValue = "";
+            } else
+            {
+                actualValue = actualValue.toLowerCase();
+            }
+
+            boolean match;
+
+            if (searchValueObject instanceof StringEqualToValue)
+            {
+                match = equalsPattern.matcher(actualValue).matches();
+            } else if (searchValueObject instanceof StringContainsValue)
+            {
+                match = containsPattern.matcher(actualValue).matches();
+            } else if (searchValueObject instanceof StringStartsWithValue)
+            {
+                match = startsPattern.matcher(actualValue).matches();
+            } else if (searchValueObject instanceof StringEndsWithValue)
+            {
+                match = endsPattern.matcher(actualValue).matches();
+            } else
+            {
+                throw new IllegalArgumentException("Unknown string value: " + criteria.getClass());
+            }
+
+            if (match)
+            {
+                matches.add(object);
+            }
         }
 
-        String searchedValue = fieldValue.getValue().toLowerCase();
+        return matches;
+    }
 
-        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)
+    // Logic:
+    //
+    // - replace '*' (matches any characters) and '?' (matches exactly one character) wild cards with appropriate regexp wild cards
+    // - quote all other characters from the searched value to be treated as a normal text (even if they are regarded special in regexp world)
+    //
+    // Examples:
+    //
+    // abc => \Qabc\E
+    // *abc => .*\Qabc\E
+    // ?abc => .\Qabc\E
+    // .abc => \Q.abc\E
+    // *ab?c?d => .*\Qab\E.\Qc\E.\Qd\E
+    //
+
+    private static Pattern createPattern(String searchValue)
+    {
+        StringBuilder pattern = new StringBuilder();
+        StringBuilder part = new StringBuilder();
+
+        for (char c : searchValue.toCharArray())
         {
-            return actualValue.endsWith(searchedValue);
-        } else
+            if (c == '*' || c == '?')
+            {
+                if (part.length() > 0)
+                {
+                    pattern.append(Pattern.quote(part.toString()));
+                    part = new StringBuilder();
+                }
+                if (c == '*')
+                {
+                    pattern.append(".*");
+                } else
+                {
+                    pattern.append(".");
+                }
+            } else
+            {
+                part.append(c);
+            }
+        }
+
+        if (part.length() > 0)
         {
-            throw new IllegalArgumentException("Unknown string value: " + criteria.getClass());
+            pattern.append(Pattern.quote(part.toString()));
         }
+
+        return Pattern.compile(pattern.toString());
     }
 
     protected abstract String getFieldValue(OBJECT object);
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchSampleTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchSampleTest.java
index 1b5f4a72b34f920dd91c313d8b7876ebafeb82ca..88572a7c19952e473df8197c7635ebc6c015325b 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchSampleTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchSampleTest.java
@@ -188,6 +188,14 @@ public class SearchSampleTest extends AbstractSampleTest
         criteria.withType().withCode().thatEquals("REINFECT_PLATE");
         testSearch(TEST_USER, criteria, "/CISD/RP1-A2X", "/CISD/RP1-B1X", "/CISD/RP2-A1X");
     }
+    
+    @Test
+    public void testSearchWithTypeWithCodeWithWildcard()
+    {
+        SampleSearchCriteria criteria = new SampleSearchCriteria();
+        criteria.withType().withCode().thatEquals("REINFECT_PLAT*");
+        testSearch(TEST_USER, criteria, "/CISD/RP1-A2X", "/CISD/RP1-B1X", "/CISD/RP2-A1X");
+    }
 
     @Test
     public void testSearchWithTypeWithSemanticAnnotationsWithIdThatEquals()
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchSampleTypeTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchSampleTypeTest.java
index 0282572ccbbf9762635ddacb8f39b6c7b4b3724f..97ef58ea92957719450f905cf29c580a3496d318 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchSampleTypeTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchSampleTypeTest.java
@@ -147,6 +147,22 @@ public class SearchSampleTypeTest extends AbstractTest
         v3api.logout(sessionToken);
     }
 
+    @Test
+    public void testSearchWithCodeThatEqualsWithStarWildcard()
+    {
+        SampleTypeSearchCriteria criteria = new SampleTypeSearchCriteria();
+        criteria.withCode().thatEquals("*_PLATE");
+        testSearch(criteria, "CELL_PLATE", "DILUTION_PLATE", "DYNAMIC_PLATE", "MASTER_PLATE", "REINFECT_PLATE");
+    }
+
+    @Test
+    public void testSearchWithCodeThatEqualsWithQuestionMarkWildcard()
+    {
+        SampleTypeSearchCriteria criteria = new SampleTypeSearchCriteria();
+        criteria.withCode().thatEquals("????????_PLATE");
+        testSearch(criteria, "DILUTION_PLATE", "REINFECT_PLATE");
+    }
+
     @Test
     public void testSearchWithCodeThatStartsWithD()
     {
@@ -165,6 +181,54 @@ public class SearchSampleTypeTest extends AbstractTest
         v3api.logout(sessionToken);
     }
 
+    @Test
+    public void testSearchWithCodeThatStartsWithStarWildcard()
+    {
+        SampleTypeSearchCriteria criteria = new SampleTypeSearchCriteria();
+        criteria.withCode().thatStartsWith("D*N_");
+        testSearch(criteria, "DELETION_TEST", "DILUTION_PLATE");
+    }
+
+    @Test
+    public void testSearchWithCodeThatStartsWithQuestionMarkWildcard()
+    {
+        SampleTypeSearchCriteria criteria = new SampleTypeSearchCriteria();
+        criteria.withCode().thatStartsWith("D??????_");
+        testSearch(criteria, "DYNAMIC_PLATE");
+    }
+
+    @Test
+    public void testSearchWithCodeThatEndsWithStarWildcard()
+    {
+        SampleTypeSearchCriteria criteria = new SampleTypeSearchCriteria();
+        criteria.withCode().thatEndsWith("_P*E");
+        testSearch(criteria, "CELL_PLATE", "DILUTION_PLATE", "DYNAMIC_PLATE", "MASTER_PLATE", "REINFECT_PLATE");
+    }
+
+    @Test
+    public void testSearchWithCodeThatEndsWithQuestionMarkWildcard()
+    {
+        SampleTypeSearchCriteria criteria = new SampleTypeSearchCriteria();
+        criteria.withCode().thatEndsWith("_???T");
+        testSearch(criteria, "DELETION_TEST");
+    }
+
+    @Test
+    public void testSearchWithCodeThatContainsWithStarWildcard()
+    {
+        SampleTypeSearchCriteria criteria = new SampleTypeSearchCriteria();
+        criteria.withCode().thatContains("POS*BLE");
+        testSearch(criteria, "IMPOSSIBLE", "IMPOSSIBLE_TO_UPDATE");
+    }
+
+    @Test
+    public void testSearchWithCodeThatContainsWithQuestionMarkWildcard()
+    {
+        SampleTypeSearchCriteria criteria = new SampleTypeSearchCriteria();
+        criteria.withCode().thatContains("R??_");
+        testSearch(criteria, "CONTROL_LAYOUT");
+    }
+
     @Test
     public void testSearchWithCodesThatIn()
     {
@@ -573,4 +637,17 @@ public class SearchSampleTypeTest extends AbstractTest
         }
     }
 
+    private void testSearch(SampleTypeSearchCriteria criteria, String... expectedCodes)
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        SearchResult<SampleType> searchResult = v3api.searchSampleTypes(sessionToken, criteria, new SampleTypeFetchOptions());
+
+        List<SampleType> types = searchResult.getObjects();
+        List<String> codes = extractCodes(types);
+        Collections.sort(codes);
+        assertEquals(codes.toString(), Arrays.toString(expectedCodes));
+        v3api.logout(sessionToken);
+    }
+
 }