diff --git a/platform-dao/src/test/java/ua/com/fielden/platform/dao/dynamic/DynamicQueryBuilderSqlTest.java b/platform-dao/src/test/java/ua/com/fielden/platform/dao/dynamic/DynamicQueryBuilderSqlTest.java index a65f46a218..81bacf534e 100644 --- a/platform-dao/src/test/java/ua/com/fielden/platform/dao/dynamic/DynamicQueryBuilderSqlTest.java +++ b/platform-dao/src/test/java/ua/com/fielden/platform/dao/dynamic/DynamicQueryBuilderSqlTest.java @@ -1,5 +1,6 @@ package ua.com.fielden.platform.dao.dynamic; +import static java.util.Arrays.stream; import static org.junit.Assert.assertEquals; import static ua.com.fielden.platform.entity.query.fluent.EntityQueryUtils.cond; import static ua.com.fielden.platform.entity.query.fluent.EntityQueryUtils.select; @@ -54,6 +55,7 @@ import ua.com.fielden.platform.test.CommonTestEntityModuleWithPropertyFactory; import ua.com.fielden.platform.test.EntityModuleWithPropertyFactory; import ua.com.fielden.platform.types.Money; +import ua.com.fielden.platform.utils.CollectionUtil; import ua.com.fielden.platform.utils.IDates; /** @@ -557,7 +559,27 @@ public void query_composition_for_property_of_type_string_without_wildchards_in_ } @Test - public void query_composition_for_property_of_type_string_with_wildchards_in_crit_value_preserves_the_original_placement_of_wildcards_and_does_not_inject_them_at_the_beginning_and_end() { + public void query_composition_for_property_of_type_string_without_wildcards_and_with_whitespaces_in_crit_value_should_not_trim_whitespaces_and_should_automatically_injects_wildcards_at_the_beginning_and_end() { + //"entityProp.stringProp" + set_up(); + final QueryProperty property = queryProperties.get("stringProp"); + property.setValue(" Some string value "); + + final String cbn = property.getConditionBuildingName(); + + final ICompleted> expected = // + /**/iJoin.where().condition(cond() // + /* */.condition(cond().prop(cbn).isNotNull().and() // + /* */.condition(cond().prop(cbn).iLike().anyOfValues(new Object[] { "% Some string value %" }).model()) // + /* */.model()) // + /**/.model()); // + final ICompleted> actual = createQuery(masterKlass, new ArrayList<>(queryProperties.values()), dates); + + assertEquals("Incorrect query model has been built.", expected.model(), actual.model()); + } + + @Test + public void query_composition_for_property_of_type_string_with_wildcards_in_crit_value_preserves_the_original_placement_of_wildcards_and_does_not_inject_them_at_the_beginning_and_end() { //"entityProp.stringProp" set_up(); final QueryProperty property = queryProperties.get("stringProp"); @@ -578,7 +600,28 @@ public void query_composition_for_property_of_type_string_with_wildchards_in_cri } @Test - public void query_composition_for_property_of_type_string_with_and_without_wildchards_in_crit_values_retain_original_whildcards_and_autoinject_wildcards_at_the_beginning_and_end_for_values_with_no_wildcards() { + public void query_composition_for_property_of_type_string_with_wildcards_and_whitespaces_in_crit_value_should_not_trim_whitespaces_and_should_preserves_the_original_placement_of_wildcards_and_does_not_inject_them_at_the_beginning_and_end() { + //"entityProp.stringProp" + set_up(); + final QueryProperty property = queryProperties.get("stringProp"); + final String critValue = " Some string value* , *Some string value , Some string *values, *Some string value* "; + property.setValue(critValue); + + final String cbn = property.getConditionBuildingName(); + + final ICompleted> expected = // + /**/iJoin.where().condition(cond() // + /* */.condition(cond().prop(cbn).isNotNull().and() // + /* */.condition(cond().prop(cbn).iLike().anyOfValues(critValue.replace("*", "%").split(",")).model()) // + /* */.model()) // + /**/.model()); // + final ICompleted> actual = createQuery(masterKlass, new ArrayList<>(queryProperties.values()), dates); + + assertEquals("Incorrect query model has been built.", expected.model(), actual.model()); + } + + @Test + public void query_composition_for_property_of_type_string_with_and_without_wildcards_in_crit_values_retain_original_whildcards_and_autoinject_wildcards_at_the_beginning_and_end_for_values_with_no_wildcards() { //"entityProp.stringProp" set_up(); final QueryProperty property = queryProperties.get("stringProp"); @@ -597,6 +640,25 @@ public void query_composition_for_property_of_type_string_with_and_without_wildc assertEquals("Incorrect query model has been built.", expected.model(), actual.model()); } + @Test + public void query_composition_for_property_of_type_string_with_and_without_wildhards_and_with_whitespaces_in_crit_values_should_not_trim_whitespaces_and_should_retain_original_whildcards_and_autoinject_wildcards_at_the_beginning_and_end_for_values_with_no_wildcards() { + //"entityProp.stringProp" + set_up(); + final QueryProperty property = queryProperties.get("stringProp"); + property.setValue(" Some string value,*Some string value , Some string *value , *Some string* value"); + + final String cbn = property.getConditionBuildingName(); + + final ICompleted> expected = // + /**/iJoin.where().condition(cond() // + /* */.condition(cond().prop(cbn).isNotNull().and() // + /* */.condition(cond().prop(cbn).iLike().anyOfValues(new String[] {"% Some string value%", "%Some string value ", " Some string %value ", " %Some string% value"}).model()) // + /* */.model()) // + /**/.model()); // + final ICompleted> actual = createQuery(masterKlass, new ArrayList<>(queryProperties.values()), dates); + + assertEquals("Incorrect query model has been built.", expected.model(), actual.model()); + } @Test public void query_composition_for_properties_of_entity_type_with_wildcard_selection_crit_value_uses_iLike_operator() { @@ -619,6 +681,28 @@ public void query_composition_for_properties_of_entity_type_with_wildcard_select assertEquals("Incorrect query model has been built.", expected.model(), actual.model()); } + @Test + public void query_composition_for_selection_crit_of_entity_value_with_wildcards_and_whitespaces_should_trim_whitespaces() { + final String propertyName = "entityProp"; + + set_up(); + final QueryProperty property = queryProperties.get(propertyName); + property.setValue(Arrays.asList(" some val 1* ", " some val 2* ")); + + final String cbn = property.getConditionBuildingName(); + final List critValuesWithWildcard = CollectionUtil.listOf("some val 1*", "some val 2*"); + + final ICompleted> expected = // + /**/iJoin.where().condition(cond() // + /* */.condition(cond().prop(getPropertyNameWithoutKeyPart(cbn)).isNotNull().and() // + /* */.condition(cond().prop(cbn).iLike().anyOfValues(DynamicQueryBuilder.prepCritValuesForEntityTypedProp(critValuesWithWildcard)).model()) // + /* */.model()) // + /**/.model()); // + final ICompleted> actual = createQuery(masterKlass, new ArrayList<>(queryProperties.values()), dates); + + assertEquals("Incorrect query model has been built.", expected.model(), actual.model()); + } + @Test public void query_composition_for_properties_of_entity_type_without_wildcard_selection_crit_value_uses_in_operator_with_subselect() { final String propertyName = "entityProp"; @@ -643,6 +727,30 @@ public void query_composition_for_properties_of_entity_type_without_wildcard_sel assertEquals("Incorrect query model has been built.", expected.model(), actual.model()); } + @Test + public void query_composition_for_selection_crit_of_entity_value_with_whitespaces_should_trim_whitespaces() { + final String propertyName = "entityProp"; + + set_up(); + final QueryProperty property = queryProperties.get(propertyName); + final String[] critValues = new String[] {" some val 1 ", " some val 2 "}; + property.setValue(Arrays.asList(critValues)); + + + final String cbn = property.getConditionBuildingName(); + final String cbnNoKey = cbn.substring(0, cbn.length() - 4); // cut off ".key" from the name + + final ICompleted> expected = // + /**/iJoin.where().condition(cond() // + /* */.condition(cond().prop(getPropertyNameWithoutKeyPart(cbn)).isNotNull().and() // + /* */.condition(cond().prop(cbnNoKey).in().model(select(SlaveEntity.class).where().prop("key").in().values("some val 1", "some val 2").model()).model()) // + /* */.model()) // + /**/.model()); // + final ICompleted> actual = createQuery(masterKlass, new ArrayList<>(queryProperties.values()), dates); + + assertEquals("Incorrect query model has been built.", expected.model(), actual.model()); + } + @Test public void query_composition_for_properties_of_entity_type_with_and_without_wildcard_selection_crit_value_uses_combination_of_in_operator_with_subselect_and_iLike_operator() { final String propertyName = "entityProp"; @@ -678,6 +786,41 @@ public void query_composition_for_properties_of_entity_type_with_and_without_wil assertEquals("Incorrect query model has been built.", expected.model(), actual.model()); } + @Test + public void query_composition_for_selection_crit_of_entity_typed_values_with_some_wildcard_and_whitespaces_also_trims_whitespaces() { + final String propertyName = "entityProp"; + + set_up(); + final QueryProperty property = queryProperties.get(propertyName); + final String[] critValues = new String[] {"some val 1 ", " some val 2* ", " some val 3*", " some val 4",}; + property.setValue(Arrays.asList(critValues)); + + final String[] critValuesWithWildcard = new String[] {"some val 2*", "some val 3*"}; + + final String cbn = property.getConditionBuildingName(); + final String cbnNoKey = cbn.substring(0, cbn.length() - 4); // cut off ".key" from the name + + + final EntityResultQueryModel subSelect = select(SlaveEntity.class).where().prop("key").in().values("some val 1", "some val 4").model(); + final ConditionModel whereCondition = cond() + .condition( + cond().prop(getPropertyNameWithoutKeyPart(cbn)).isNotNull() + .and() + .condition( + cond().prop(cbnNoKey).in().model(subSelect) + .or().prop(cbn).iLike().anyOfValues(prepCritValuesForEntityTypedProp(Arrays.asList(critValuesWithWildcard))) + .model()) + .model()) + .model(); + + final ICompleted> expected = // + /**/iJoin.where().condition(whereCondition); + + final ICompleted> actual = createQuery(masterKlass, new ArrayList<>(queryProperties.values()), dates); + + assertEquals("Incorrect query model has been built.", expected.model(), actual.model()); + } + //////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////// 2. Property level (Negation / Null) ///////////////////// diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/DynamicQueryBuilder.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/DynamicQueryBuilder.java index 9f84fa84a4..fa0a8d08ce 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/DynamicQueryBuilder.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/entity_centre/review/DynamicQueryBuilder.java @@ -27,6 +27,7 @@ import static ua.com.fielden.platform.utils.EntityUtils.isString; import static ua.com.fielden.platform.utils.EntityUtils.isUnionEntityType; import static ua.com.fielden.platform.utils.MiscUtilities.prepare; +import static ua.com.fielden.platform.utils.MiscUtilities.prepareStringExpression; import static ua.com.fielden.platform.utils.Pair.pair; import java.lang.reflect.Field; @@ -40,6 +41,7 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; @@ -849,7 +851,7 @@ public static String[] prepCritValuesForStringTypedProp(final String criteria) { if (!crits[index].contains("*")) { crits[index] = "*" + crits[index] + "*"; } - crits[index] = prepare(crits[index]); + crits[index] = prepareStringExpression(crits[index]); } return crits; } @@ -863,9 +865,9 @@ public static String[] prepCritValuesForStringTypedProp(final String criteria) { */ private static String prepCritValuesForSingleStringTypedProp(final String criteria) { if (!criteria.contains("*")) { - return prepare("*" + criteria + "*"); + return prepareStringExpression("*" + criteria + "*"); } - return prepare(criteria); + return prepareStringExpression(criteria); } /** @@ -878,6 +880,16 @@ public static String[] prepCritValuesForEntityTypedProp(final List crite return prepare(criteria); } + /** + * Creates new array based on the passed list of string. This method also trims every element of the passed list. + * + * @param criteria + * @return + */ + public static String[] prepExectCritValuesForEntityTypedProp(final List criteria) { + return criteria.stream().map(crit -> crit.trim()).toArray(String[]::new); + } + /** * Returns true if the type is supported in dynamic criteria, false otherwise. * @@ -1115,7 +1127,7 @@ private static ConditionModel propertyDescriptorLike(final String propertyNameWi final Map> searchVals = searchValues.stream().collect(groupingBy(str -> str.contains("*"))); final Set>> matchedPropDescriptors = new LinkedHashSet<>(); concat( - searchVals.getOrDefault(false, emptyList()).stream(), + stream(prepExectCritValuesForEntityTypedProp(searchVals.getOrDefault(false, emptyList()))), stream(prepCritValuesForEntityTypedProp(searchVals.getOrDefault(true, emptyList()))) ).forEach(val -> matchedPropDescriptors.addAll(new PojoValueMatcher<>(allPropertyDescriptors, KEY, allPropertyDescriptors.size()).findMatches(val))); return matchedPropDescriptors.isEmpty() @@ -1138,13 +1150,13 @@ private static ConditionModel propertyLike(final String propertyNameWithKey, fin if (exactAndWildcardSearchVals.containsKey(false) && exactAndWildcardSearchVals.containsKey(true)) { // both exact and whildcard search values are present return cond() // Condition for exact search values; union entities need ".id" to help EQL. - .prop(propertyNameWithoutKey + (isUnionEntityType(propType) ? ".id" : "")).in().model(select(propType).where().prop(KEY).in().values(exactAndWildcardSearchVals.get(false).toArray()).model()) + .prop(propertyNameWithoutKey + (isUnionEntityType(propType) ? ".id" : "")).in().model(select(propType).where().prop(KEY).in().values(prepExectCritValuesForEntityTypedProp(exactAndWildcardSearchVals.get(false))).model()) // Condition for wildcard search values. .or().prop(propertyNameWithKey).iLike().anyOfValues(prepCritValuesForEntityTypedProp(exactAndWildcardSearchVals.get(true))).model(); } else if (exactAndWildcardSearchVals.containsKey(false) && !exactAndWildcardSearchVals.containsKey(true)) { // only exact search values are present return cond() // Condition for exact search values; union entities need ".id" to help EQL. - .prop(propertyNameWithoutKey + (isUnionEntityType(propType) ? ".id" : "")).in().model(select(propType).where().prop(KEY).in().values(exactAndWildcardSearchVals.get(false).toArray()).model()).model(); + .prop(propertyNameWithoutKey + (isUnionEntityType(propType) ? ".id" : "")).in().model(select(propType).where().prop(KEY).in().values(prepExectCritValuesForEntityTypedProp(exactAndWildcardSearchVals.get(false))).model()).model(); } else { // only whildcard search values are present return cond() // Condition for wildcard search values. diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/utils/MiscUtilities.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/utils/MiscUtilities.java index cb3af598f5..a7798026f8 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/utils/MiscUtilities.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/utils/MiscUtilities.java @@ -105,6 +105,19 @@ public static String prepare(final String autocompleterExp) { return autocompleterExp.replace("*", "%").trim(); } + /** + * Converts auto-completer-like regular expression for string typed property into normal regular expression (simply replaces all '*' with '%' characters) without trimming it + * + * @param stringExpr - The expression for string property + * @return converted into SQL like regular expression without trimming stringExpr + */ + public static String prepareStringExpression(final String stringExpr) { + if ("*".equals(stringExpr)) { + return null; + } + return stringExpr.replace("*", "%"); + } + /** * Converts the content of the input stream into a string. * diff --git a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/EntityResourceUtils.java b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/EntityResourceUtils.java index 2295e8a5df..0db32c3263 100644 --- a/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/EntityResourceUtils.java +++ b/platform-pojo-bl/src/main/java/ua/com/fielden/platform/web/utils/EntityResourceUtils.java @@ -2,6 +2,7 @@ import static java.lang.Class.forName; import static java.lang.String.format; +import static java.util.Arrays.asList; import static java.util.Locale.getDefault; import static java.util.Optional.empty; import static java.util.Optional.of; @@ -24,6 +25,7 @@ import static ua.com.fielden.platform.utils.EntityUtils.isCompositeEntity; import static ua.com.fielden.platform.utils.EntityUtils.isEntityType; import static ua.com.fielden.platform.utils.EntityUtils.isUnionEntityType; +import static ua.com.fielden.platform.utils.MiscUtilities.prepare; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; @@ -258,7 +260,7 @@ public static > M apply(final Map mo */ @SuppressWarnings("unused") private static > void logPropertyApplication(final String actionCaption, final boolean apply, final boolean shortLog, final Class type, final String name, final boolean isEntityStale, final Map valAndOrigVal, final M entity, final String... propertiesToLogArray) { - final Set propertiesToLog = new LinkedHashSet<>(Arrays.asList(propertiesToLogArray)); + final Set propertiesToLog = new LinkedHashSet<>(asList(propertiesToLogArray)); if (propertiesToLog.contains(name)) { final StringBuilder builder = new StringBuilder(actionCaption); builder.append(":\t"); @@ -611,7 +613,7 @@ private static Class determinePropertyType(final Class type, final String } final ParameterizedType parameterizedEntityType = (ParameterizedType) type.getAnnotatedSuperclass().getType(); if (parameterizedEntityType.getActualTypeArguments().length != 1 || !(parameterizedEntityType.getActualTypeArguments()[0] instanceof Class)) { - throw Result.failure(new IllegalStateException(format("The type parameters %s of functional entity %s (for collection modification) is malformed.", Arrays.asList(parameterizedEntityType.getActualTypeArguments()), type.getSimpleName()))); + throw Result.failure(new IllegalStateException(format("The type parameters %s of functional entity %s (for collection modification) is malformed.", asList(parameterizedEntityType.getActualTypeArguments()), type.getSimpleName()))); } propertyType = (Class) parameterizedEntityType.getActualTypeArguments()[0]; } else { @@ -762,7 +764,7 @@ private static > Object convert(final Class type, public static AbstractEntity findAndFetchBy(final String searchString, final Class> entityType, final Optional optActiveProp, final fetch> fetch, final IEntityDao> companion) { if (isCompositeEntity(entityType)) { //logger.debug(format("KEY-based restoration of value: type [%s] property [%s] propertyType [%s] id [%s] reflectedValue [%s].", type.getSimpleName(), propertyName, entityPropertyType.getSimpleName(), reflectedValueId, reflectedValue)); - final String compositeKeyAsString = MiscUtilities.prepare(prepSearchStringForCompositeKey(entityType, searchString)); + final String compositeKeyAsString = prepare(prepSearchStringForCompositeKey(entityType, searchString)); final EntityResultQueryModel> model = select(entityType).where().prop(KEY).iLike().val(compositeKeyAsString).model().setFilterable(true); final QueryExecutionModel, EntityResultQueryModel>> qem = from(model).with(fetch).model(); try { @@ -798,7 +800,7 @@ public static AbstractEntity findAndFetchBy(final String searchString, final } } else { //logger.debug(format("KEY-based restoration of value: type [%s] property [%s] propertyType [%s] id [%s] reflectedValue [%s].", type.getSimpleName(), propertyName, entityPropertyType.getSimpleName(), reflectedValueId, reflectedValue)); - final String[] keys = MiscUtilities.prepare(Arrays.asList(searchString)); + final String[] keys = prepare(asList(searchString)); final String key; if (keys.length > 1) { throw new IllegalArgumentException(format("Value [%s] does not represent a single key value, which is required for coversion to an instance of type [%s].", searchString, entityType.getName())); diff --git a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/EntityAutocompletionResource.java b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/EntityAutocompletionResource.java index c8a8d6afd9..7b8e1b69f9 100644 --- a/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/EntityAutocompletionResource.java +++ b/platform-web-resources/src/main/java/ua/com/fielden/platform/web/resources/webui/EntityAutocompletionResource.java @@ -151,7 +151,7 @@ public Representation post(final Representation envelope) { */ public static T2 prepSearchString(final CentreContextHolder centreContextHolder, final boolean shouldUpperCase) { final String searchStringVal = (String) centreContextHolder.getCustomObject().get("@@searchString"); // custom property inside paramsHolder - final Optional maybeSearchString = ofNullable(prepare(searchStringVal.contains("*") || searchStringVal.contains("%") ? searchStringVal : "*" + searchStringVal + "*")); + final Optional maybeSearchString = ofNullable(prepare(searchStringVal.contains("*") || searchStringVal.contains("%") ? searchStringVal.trim() : "*" + searchStringVal.trim() + "*")); final String searchString = maybeSearchString.map(str -> shouldUpperCase ? str.toUpperCase() : str).orElse("%"); final Optional maybeDataPage = ofNullable((Integer) centreContextHolder.getCustomObject().get("@@dataPage")); final int dataPageNo = maybeDataPage.orElse(1); diff --git a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/editors/tg-entity-editor.js b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/editors/tg-entity-editor.js index 72bbf37c07..03a5d94219 100644 --- a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/editors/tg-entity-editor.js +++ b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/editors/tg-entity-editor.js @@ -780,7 +780,7 @@ export class TgEntityEditor extends TgEditor { let inputText = ''; // default value if (this.multi === false) { // assign the actual search string - inputText = ignoreInputText === true ? defaultSearchQuery : this._prepInput(this.decoratedInput().value) || defaultSearchQuery; + inputText = ignoreInputText === true ? defaultSearchQuery : this._prepInput(this.decoratedInput().value.trim()) || defaultSearchQuery; } else { // The following manipulations with indexes are required in case of multi selection // in order to determine what part of the input text should be used for search and