diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index 3bd8cbcd20..232473348e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -19,7 +19,10 @@ import com.google.common.base.Splitter; import org.apache.commons.lang3.RandomStringUtils; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; +import java.util.List; import java.util.function.Function; import static org.apache.commons.lang3.StringUtils.repeat; @@ -156,6 +159,10 @@ public class StringUtils { } public static boolean equalsAny(String string, String... otherStrings) { + return equalsAny(string, Arrays.asList(otherStrings)); + } + + public static boolean equalsAny(String string, List otherStrings) { for (String otherString : otherStrings) { if (equals(string, otherString)) { return true; @@ -245,4 +252,27 @@ public class StringUtils { return string.substring(0, maxLength) + truncationMarkerFunc.apply(truncatedSymbols); } + public static List splitByCommaWithoutQuotes(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; + } + } 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 f5874cbd08..b928e8790e 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 @@ -47,6 +47,8 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; + @Data public class EntityKeyMapping { @@ -574,7 +576,7 @@ public class EntityKeyMapping { switch (stringFilterPredicate.getOperation()) { case IN: case NOT_IN: - ctx.addStringListParameter(paramName, getListValuesWithoutQuote(value)); + ctx.addStringListParameter(paramName, splitByCommaWithoutQuotes(value)); break; default: ctx.addStringParameter(paramName, value); @@ -582,30 +584,7 @@ public class EntityKeyMapping { 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) { + private String buildNumericPredicateQuery(QueryContext ctx, String field, NumericFilterPredicate numericFilterPredicate) { String paramName = getNextParameterName(field); ctx.addDoubleParameter(paramName, numericFilterPredicate.getValue().getValue()); String numericOperationQuery = ""; 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 index b9baffef1c..2ccab3cf29 100644 --- 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 @@ -24,6 +24,8 @@ import org.springframework.test.context.junit4.SpringRunner; import java.util.List; +import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; + @RunWith(SpringRunner.class ) @SpringBootTest(classes = EntityKeyMapping.class) public class EntityKeyMappingTest { @@ -36,39 +38,39 @@ public class EntityKeyMappingTest { @Test public void testSplitToList() { String value = "device1, device2, device3"; - Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + Assert.assertEquals(splitByCommaWithoutQuotes(value), result); } @Test public void testReplaceSingleQuote() { String value = "'device1', 'device2', 'device3'"; - Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + Assert.assertEquals(splitByCommaWithoutQuotes(value), result); } @Test public void testReplaceDoubleQuote() { String value = "\"device1\", \"device2\", \"device3\""; - Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + Assert.assertEquals(splitByCommaWithoutQuotes(value), result); } @Test public void testSplitWithoutSpace() { String value = "\"device1\" , \"device2\" , \"device3\""; - Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + Assert.assertEquals(splitByCommaWithoutQuotes(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); + Assert.assertEquals(splitByCommaWithoutQuotes(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); + Assert.assertEquals(splitByCommaWithoutQuotes(value), result); } @Test @@ -76,28 +78,28 @@ public class EntityKeyMappingTest { String value = "\"device1\", 'device2', \"device3\""; List result = List.of("\"device1\"", "'device2'", "\"device3\""); - Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + Assert.assertEquals(splitByCommaWithoutQuotes(value), result); value = "'device1', \"device2\", \"device3\""; result = List.of("'device1'", "\"device2\"", "\"device3\""); - Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + Assert.assertEquals(splitByCommaWithoutQuotes(value), result); value = "device1, 'device2', \"device3\""; result = List.of("device1", "'device2'", "\"device3\""); - Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + Assert.assertEquals(splitByCommaWithoutQuotes(value), result); value = "'device1', device2, \"device3\""; result = List.of("'device1'", "device2", "\"device3\""); - Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + Assert.assertEquals(splitByCommaWithoutQuotes(value), result); value = "device1, \"device2\", \"device3\""; result = List.of("device1", "\"device2\"", "\"device3\""); - Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + Assert.assertEquals(splitByCommaWithoutQuotes(value), result); value = "\"device1\", device2, \"device3\""; result = List.of("\"device1\"", "device2", "\"device3\""); - Assert.assertEquals(entityKeyMapping.getListValuesWithoutQuote(value), result); + Assert.assertEquals(splitByCommaWithoutQuotes(value), result); } } \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java index b3322c049c..ada54ad26e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java @@ -50,6 +50,9 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import static org.thingsboard.server.common.data.StringUtils.equalsAny; +import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; + @Data @Slf4j class AlarmRuleState { @@ -467,6 +470,10 @@ class AlarmRuleState { return !val.equals(predicateValue); case NOT_CONTAINS: return !val.contains(predicateValue); + case IN: + return equalsAny(val, splitByCommaWithoutQuotes(predicateValue)); + case NOT_IN: + return !equalsAny(val, splitByCommaWithoutQuotes(predicateValue)); default: throw new RuntimeException("Operation not supported: " + predicate.getOperation()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/AlarmRuleStateTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/AlarmRuleStateTest.java new file mode 100644 index 0000000000..e4b2856dc3 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/AlarmRuleStateTest.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2024 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.rule.engine.profile; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.thingsboard.server.common.data.device.profile.AlarmCondition; +import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; +import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; +import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; +import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.StringFilterPredicate; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public class AlarmRuleStateTest { + + private static Stream testEvalCondition() { + return Stream.of( + Arguments.of(StringFilterPredicate.StringOperation.IN, "test,value", "test", AlarmEvalResult.TRUE), + Arguments.of(StringFilterPredicate.StringOperation.IN, "test,value", "teeeeest", AlarmEvalResult.FALSE), + Arguments.of(StringFilterPredicate.StringOperation.NOT_IN, "test,value", "test", AlarmEvalResult.FALSE), + Arguments.of(StringFilterPredicate.StringOperation.NOT_IN, "test,value", "teeeeest", AlarmEvalResult.TRUE), + Arguments.of(StringFilterPredicate.StringOperation.CONTAINS, "test value", "test value", AlarmEvalResult.TRUE), + Arguments.of(StringFilterPredicate.StringOperation.CONTAINS, "test value", "test", AlarmEvalResult.FALSE), + Arguments.of(StringFilterPredicate.StringOperation.NOT_CONTAINS, "test value", "test", AlarmEvalResult.TRUE), + Arguments.of(StringFilterPredicate.StringOperation.NOT_CONTAINS, "test value", "test value", AlarmEvalResult.FALSE), + Arguments.of(StringFilterPredicate.StringOperation.EQUAL, "test value", "test value", AlarmEvalResult.TRUE), + Arguments.of(StringFilterPredicate.StringOperation.EQUAL, "test value", "test", AlarmEvalResult.FALSE), + Arguments.of(StringFilterPredicate.StringOperation.NOT_EQUAL, "test value", "test", AlarmEvalResult.TRUE), + Arguments.of(StringFilterPredicate.StringOperation.NOT_EQUAL, "test value", "test value", AlarmEvalResult.FALSE), + Arguments.of(StringFilterPredicate.StringOperation.ENDS_WITH, "test value", "some test value", AlarmEvalResult.TRUE), + Arguments.of(StringFilterPredicate.StringOperation.ENDS_WITH, "test value", "some test value2", AlarmEvalResult.FALSE), + Arguments.of(StringFilterPredicate.StringOperation.STARTS_WITH, "test value", "test value attribute", AlarmEvalResult.TRUE), + Arguments.of(StringFilterPredicate.StringOperation.STARTS_WITH, "test value", "test", AlarmEvalResult.FALSE) + ); + } + + @ParameterizedTest + @MethodSource + public void testEvalCondition(StringFilterPredicate.StringOperation operation, String predicateValue, String attributeValue, AlarmEvalResult evalResult) { + AlarmConditionFilterKey alarmConditionFilterKey = new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "stringKey"); + + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(predicateValue)); + + AlarmConditionFilter alarmConditionFilter = new AlarmConditionFilter(); + alarmConditionFilter.setKey(alarmConditionFilterKey); + alarmConditionFilter.setPredicate(predicate); + alarmConditionFilter.setValueType(EntityKeyValueType.STRING); + + List condition = new ArrayList<>(); + condition.add(alarmConditionFilter); + + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setSpec(new SimpleAlarmConditionSpec()); + alarmCondition.setCondition(condition); + + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + + AlarmRuleState alarmRuleState = new AlarmRuleState(null, alarmRule, null, null, null); + + Set entityKeys = new HashSet<>(List.of(alarmConditionFilterKey)); + DataSnapshot result = new DataSnapshot(entityKeys); + result.putValue(alarmConditionFilterKey, System.currentTimeMillis(), EntityKeyValue.fromString(attributeValue)); + Assertions.assertEquals(evalResult, alarmRuleState.eval(result)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html index 4e7a499895..c6ae39f295 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html @@ -41,7 +41,7 @@ -
filter.default-value
+
{{ hintText | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts index 30260b26ea..2f3d29df91 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts @@ -32,7 +32,8 @@ import { EntityKeyValueType, FilterPredicateValue, getDynamicSourcesForAllowUser, - inheritModeForDynamicValueSourceType + inheritModeForDynamicValueSourceType, + StringOperation } from '@shared/models/query/query.models'; @Component({ @@ -81,6 +82,15 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, Vali return this.onlyUserDynamicSourceValue; } + @Input() + set operation(operation: StringOperation) { + if (operation && (operation === StringOperation.IN || operation === StringOperation.NOT_IN)) { + this.hintText = 'filter.default-comma-separated-values'; + } else { + this.hintText = 'filter.default-value'; + } + } + @Input() valueType: EntityKeyValueType; @@ -98,6 +108,8 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, Vali inheritMode = false; + hintText = 'filter.default-value'; + private propagateChange = null; private propagateChangePending = false; diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html index 2ce80373e8..c2888fa06f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html @@ -31,6 +31,7 @@ [onlyUserDynamicSource]="onlyUserDynamicSource" fxFlex="70" [valueType]="valueTypeEnum.STRING" + [operation]="stringFilterPredicateFormGroup.get('operation').value" formControlName="value">
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 3344e46305..288db8c9d2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2670,6 +2670,7 @@ "current-user": "Current user", "current-device": "Current device", "default-value": "Default value", + "default-comma-separated-values": "Default comma-separated values", "dynamic-source-type": "Dynamic source type", "dynamic-value": "Dynamic value", "no-dynamic-value": "No dynamic value",