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); + } + }