diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java index d3a09813e3..f4da15df4e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java @@ -38,6 +38,8 @@ public class StringFilterPredicate implements SimpleKeyFilterPredicate { STARTS_WITH, ENDS_WITH, CONTAINS, - NOT_CONTAINS + NOT_CONTAINS, + IN, + NOT_IN } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index caed48758a..c653d77004 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -560,11 +560,47 @@ public class EntityKeyMapping { value = "%" + value + "%"; stringOperationQuery = String.format("%s not like :%s or %s is null)", operationField, paramName, operationField); break; + case IN: + stringOperationQuery = String.format("%s in (:%s))", operationField, paramName); + break; + case NOT_IN: + stringOperationQuery = String.format("%s not in (:%s))", operationField, paramName); + break; + } + switch (stringFilterPredicate.getOperation()) { + case IN: + case NOT_IN: + ctx.addStringListParameter(paramName, getListValuesWithoutQuote(value)); + break; + default: + ctx.addStringParameter(paramName, value); } - ctx.addStringParameter(paramName, value); return String.format("((%s is not null and %s)", field, stringOperationQuery); } + protected List getListValuesWithoutQuote(String value) { + List splitValues = List.of(value.trim().split("\\s*,\\s*")); + List result = new ArrayList<>(); + char lastWayInputValue = '#'; + for (String str : splitValues) { + char startWith = str.charAt(0); + char endWith = str.charAt(str.length() - 1); + + // if first value is not quote, so we return values after split + if (startWith != '\'' && startWith != '"') return splitValues; + + // if value is not in quote, so we return values after split + if (startWith != endWith) return splitValues; + + // if different way values, so don't replace quote and return values after split + if (lastWayInputValue != '#' && startWith != lastWayInputValue) return splitValues; + + result.add(str.substring(1, str.length() - 1)); + lastWayInputValue = startWith; + } + return result; + } + private String buildNumericPredicateQuery(QueryContext ctx, String field, NumericFilterPredicate numericFilterPredicate) { String paramName = getNextParameterName(field); ctx.addDoubleParameter(paramName, numericFilterPredicate.getValue().getValue()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index e58191b60d..9140c457e6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -1307,6 +1307,14 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { notEqualStrings.add(operationName); containsStrings.add(operationName); break; + case IN: + notEqualStrings.add(operationName); + notContainsStrings.add(operationName); + break; + case NOT_IN: + notEqualStrings.add(operationName); + notContainsStrings.add(operationName); + break; } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/query/EntityKeyMappingTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/query/EntityKeyMappingTest.java new file mode 100644 index 0000000000..01b41121e4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/query/EntityKeyMappingTest.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * 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 org.thingsboard.server.dao.sql.query; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.List; + +@RunWith(SpringRunner.class ) +@SpringBootTest(classes = EntityKeyMapping.class) +public class EntityKeyMappingTest { + + @Autowired + private EntityKeyMapping entityKeyMapping; + + private static final List result = List.of("device1", "device2", "device3"); + + @Test + public void testSplitToList() { + String value = "device1, device2, device3"; + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + } + + @Test + public void testReplaceSingleQuote() { + String value = "'device1', 'device2', 'device3'"; + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + } + + @Test + public void testReplaceDoubleQuote() { + String value = "\"device1\", \"device2\", \"device3\""; + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + } + + @Test + public void testSplitWithoutSpace() { + String value = "\"device1\" , \"device2\" , \"device3\""; + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + } + + @Test + public void testSaveSpacesBetweenString() { + String value = "device 1 , device 2 , device 3"; + List result = List.of("device 1", "device 2", "device 3"); + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + } + + @Test + public void testSaveQuoteInString() { + String value = "device ''1 , device \"\"2 , device \"'3"; + List result = List.of("device ''1", "device \"\"2", "device \"'3"); + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + } + + @Test + public void testNotDeleteQuoteWhenDifferentStyle() { + + String value = "\"device1\", 'device2', \"device3\""; + List result = List.of("\"device1\"", "'device2'", "\"device3\""); + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + + value = "'device1', \"device2\", \"device3\""; + result = List.of("'device1'", "\"device2\"", "\"device3\""); + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + + value = "device1, 'device2', \"device3\""; + result = List.of("device1", "'device2'", "\"device3\""); + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + + + value = "'device1', device2, \"device3\""; + result = List.of("'device1'", "device2", "\"device3\""); + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + + value = "device1, \"device2\", \"device3\""; + result = List.of("device1", "\"device2\"", "\"device3\""); + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + + + value = "\"device1\", device2, \"device3\""; + result = List.of("\"device1\"", "device2", "\"device3\""); + Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + } +} \ No newline at end of file diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index e504a3480d..d72aed9798 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -226,7 +226,9 @@ export enum StringOperation { STARTS_WITH = 'STARTS_WITH', ENDS_WITH = 'ENDS_WITH', CONTAINS = 'CONTAINS', - NOT_CONTAINS = 'NOT_CONTAINS' + NOT_CONTAINS = 'NOT_CONTAINS', + IN = 'IN', + NOT_IN = 'NOT_IN' } export const stringOperationTranslationMap = new Map( @@ -236,7 +238,9 @@ export const stringOperationTranslationMap = new Map( [StringOperation.STARTS_WITH, 'filter.operation.starts-with'], [StringOperation.ENDS_WITH, 'filter.operation.ends-with'], [StringOperation.CONTAINS, 'filter.operation.contains'], - [StringOperation.NOT_CONTAINS, 'filter.operation.not-contains'] + [StringOperation.NOT_CONTAINS, 'filter.operation.not-contains'], + [StringOperation.IN, 'filter.operation.in'], + [StringOperation.NOT_IN, 'filter.operation.not-in'] ] ); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 5c665e8ffe..5d3adba457 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2076,7 +2076,9 @@ "greater-or-equal": "greater or equal", "less-or-equal": "less or equal", "and": "and", - "or": "or" + "or": "or", + "in": "in", + "not-in": "not in" }, "ignore-case": "ignore case", "value": "Value",