From b2cf1cc83b012f2f3ab6e8a2ffb34f6792805acc Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 24 Jun 2024 16:09:53 -0500 Subject: [PATCH 1/6] CE-1402 New CaseChangeBehavior, and adding field behaviors to read operations (mostly) and filters and frontend if so specified --- .../core/actions/tables/AggregateAction.java | 12 + .../core/actions/tables/CountAction.java | 8 + .../core/actions/tables/GetAction.java | 93 ++++++++ .../core/actions/tables/QueryAction.java | 9 + .../actions/values/ValueBehaviorApplier.java | 174 ++++++++++++++ .../metadata/fields/CaseChangeBehavior.java | 143 ++++++++++++ .../fields/FieldBehaviorForFrontend.java | 34 +++ .../metadata/fields/FieldDisplayBehavior.java | 3 +- .../metadata/fields/FieldFilterBehavior.java | 43 ++++ .../frontend/QFrontendFieldMetaData.java | 28 +++ .../core/actions/tables/GetActionTest.java | 49 ++++ .../core/actions/tables/QueryActionTest.java | 42 ++++ .../values/ValueBehaviorApplierTest.java | 68 +++++- .../fields/CaseChangeBehaviorTest.java | 215 ++++++++++++++++++ 14 files changed, 919 insertions(+), 2 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehavior.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldBehaviorForFrontend.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldFilterBehavior.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehaviorTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java index 42ade1f5..3da1b487 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java @@ -22,9 +22,12 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.util.Collections; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface; import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput; @@ -58,6 +61,11 @@ public class AggregateAction QTableMetaData table = aggregateInput.getTable(); QBackendMetaData backend = aggregateInput.getBackend(); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // apply any available field behaviors to the filter (noting that, if anything changes, a new filter is returned) // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + aggregateInput.setFilter(ValueBehaviorApplier.applyFieldBehaviorsToFilter(QContext.getQInstance(), table, aggregateInput.getFilter(), Collections.emptySet())); + QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, aggregateInput.getFilter()); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); @@ -67,6 +75,10 @@ public class AggregateAction aggregateInterface.setQueryStat(queryStat); AggregateOutput aggregateOutput = aggregateInterface.execute(aggregateInput); + // todo, maybe, not real important? ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.READ, QContext.getQInstance(), table, aggregateOutput.getResults(), null); + // issue being, the signature there... it takes a list of QRecords, which aren't what we have... + // do we want to ... idk, refactor all these behavior deals? hmm... maybe a new interface/ for ones that do reads? not sure. + QueryStatManager.getInstance().add(queryStat); return aggregateOutput; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/CountAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/CountAction.java index 394df6f2..5a25c396 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/CountAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/CountAction.java @@ -22,9 +22,12 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.util.Collections; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; @@ -58,6 +61,11 @@ public class CountAction QTableMetaData table = countInput.getTable(); QBackendMetaData backend = countInput.getBackend(); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // apply any available field behaviors to the filter (noting that, if anything changes, a new filter is returned) // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + countInput.setFilter(ValueBehaviorApplier.applyFieldBehaviorsToFilter(QContext.getQInstance(), table, countInput.getFilter(), Collections.emptySet())); + QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, countInput.getFilter()); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java index e2524591..8c7ef656 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java @@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -34,8 +36,10 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface; import com.kingsrook.qqq.backend.core.actions.tables.helpers.GetActionCacheHelper; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; @@ -45,11 +49,16 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldFilterBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ObjectUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; +import com.kingsrook.qqq.backend.core.utils.memoization.Memoization; /******************************************************************************* @@ -58,11 +67,15 @@ import com.kingsrook.qqq.backend.core.utils.ObjectUtils; *******************************************************************************/ public class GetAction { + private static final QLogger LOG = QLogger.getLogger(GetAction.class); + private Optional postGetRecordCustomizer; private GetInput getInput; private QPossibleValueTranslator qPossibleValueTranslator; + private Memoization, List>> getFieldFilterBehaviorMemoization = new Memoization<>(); + /******************************************************************************* @@ -105,6 +118,8 @@ public class GetAction usingDefaultGetInterface = true; } + getInput = applyFieldBehaviors(getInput); + getInterface.validateInput(getInput); getOutput = getInterface.execute(getInput); @@ -130,6 +145,82 @@ public class GetAction + /******************************************************************************* + ** + *******************************************************************************/ + private GetInput applyFieldBehaviors(GetInput getInput) + { + QTableMetaData table = getInput.getTable(); + + try + { + if(getInput.getPrimaryKey() != null) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the input has a primary key, get its behaviors, then apply, and update the pkey in the input if the value is different // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + List> fieldFilterBehaviors = getFieldFilterBehaviors(table, table.getPrimaryKeyField()); + for(FieldFilterBehavior fieldFilterBehavior : CollectionUtils.nonNullList(fieldFilterBehaviors)) + { + QFilterCriteria pkeyCriteria = new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey()); + QFilterCriteria updatedCriteria = ValueBehaviorApplier.apply(pkeyCriteria, QContext.getQInstance(), table, table.getField(table.getPrimaryKeyField()), fieldFilterBehavior); + if(updatedCriteria != pkeyCriteria) + { + getInput.setPrimaryKey(updatedCriteria.getValues().get(0)); + } + } + } + else if(getInput.getUniqueKey() != null) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the input has a unique key, get its behaviors, then apply, and update the ukey values in the input if any are different // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Map updatedUniqueKey = new HashMap<>(getInput.getUniqueKey()); + for(String fieldName : getInput.getUniqueKey().keySet()) + { + List> fieldFilterBehaviors = getFieldFilterBehaviors(table, fieldName); + for(FieldFilterBehavior fieldFilterBehavior : CollectionUtils.nonNullList(fieldFilterBehaviors)) + { + QFilterCriteria ukeyCriteria = new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, updatedUniqueKey.get(fieldName)); + QFilterCriteria updatedCriteria = ValueBehaviorApplier.apply(ukeyCriteria, QContext.getQInstance(), table, table.getField(table.getPrimaryKeyField()), fieldFilterBehavior); + updatedUniqueKey.put(fieldName, updatedCriteria.getValues().get(0)); + } + } + getInput.setUniqueKey(updatedUniqueKey); + } + } + catch(Exception e) + { + LOG.warn("Error applying field behaviors to get input - will run with original inputs", e); + } + + return (getInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List> getFieldFilterBehaviors(QTableMetaData tableMetaData, String fieldName) + { + Pair key = new Pair<>(tableMetaData.getName(), fieldName); + return getFieldFilterBehaviorMemoization.getResult(key, (p) -> + { + List> rs = new ArrayList<>(); + for(FieldBehavior fieldBehavior : tableMetaData.getFields().get(fieldName).getBehaviors()) + { + if(fieldBehavior instanceof FieldFilterBehavior fieldFilterBehavior) + { + rs.add(fieldFilterBehavior); + } + } + return (rs); + }).orElse(null); + } + + + /******************************************************************************* ** shorthand way to call for the most common use-case, when you just want the ** output record to be returned. @@ -255,6 +346,8 @@ public class GetAction returnRecord = postGetRecordCustomizer.get().postQuery(getInput, List.of(record)).get(0); } + ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.READ, QContext.getQInstance(), getInput.getTable(), List.of(record), null); + if(getInput.getShouldTranslatePossibleValues()) { if(qPossibleValueTranslator == null) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index 58cc53a3..4661117e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -41,6 +42,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryActionCacheHel import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; @@ -117,6 +119,11 @@ public class QueryAction } } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // apply any available field behaviors to the filter (noting that, if anything changes, a new filter is returned) // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + queryInput.setFilter(ValueBehaviorApplier.applyFieldBehaviorsToFilter(QContext.getQInstance(), table, queryInput.getFilter(), Collections.emptySet())); + QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, queryInput.getFilter()); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); @@ -284,6 +291,8 @@ public class QueryAction records = postQueryRecordCustomizer.get().postQuery(queryInput, records); } + ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.READ, QContext.getQInstance(), queryInput.getTable(), records, null); + if(queryInput.getShouldTranslatePossibleValues()) { if(qPossibleValueTranslator == null) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java index 0338e34b..2baebf71 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java @@ -22,12 +22,18 @@ package com.kingsrook.qqq.backend.core.actions.values; +import java.io.Serializable; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Set; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldFilterBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -46,6 +52,7 @@ public class ValueBehaviorApplier { INSERT, UPDATE, + READ, FORMATTING } @@ -97,4 +104,171 @@ public class ValueBehaviorApplier } } + + + /******************************************************************************* + ** apply field behaviors (of FieldFilterBehavior type) to a QQueryFilter. + ** note that, we don't like to ever edit a QQueryFilter itself (e.g., as it might + ** have come from meta-data, or it might have some immutable structures in it). + ** So, if any changes are needed, they'll be returned in a clone. + ** So, either way, you should use this method like: + * + ** QQueryFilter myFilter = // wherever I got my filter from + ** myFilter = ValueBehaviorApplier.applyFieldBehaviorsToFilter(QContext.getInstance, table, myFilter, null); + ** // e.g., always re-assign over top of your filter. + *******************************************************************************/ + public static QQueryFilter applyFieldBehaviorsToFilter(QInstance instance, QTableMetaData table, QQueryFilter filter, Set> behaviorsToOmit) + { + //////////////////////////////////////////////// + // for null or empty filter, return the input // + //////////////////////////////////////////////// + if(filter == null || !filter.hasAnyCriteria()) + { + return (filter); + } + + /////////////////////////////////////////////////////////////////// + // track if we need to make & return a clone. // + // which will be the case if we get back any different criteria, // + // or any different sub-filters, than what we originally had. // + /////////////////////////////////////////////////////////////////// + boolean needToUseClone = false; + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // make a new criteria list, and a new subFilter list - either null, if the source was null, or a new array list // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + List newCriteriaList = filter.getCriteria() == null ? null : new ArrayList<>(); + List newSubFilters = filter.getSubFilters() == null ? null : new ArrayList<>(); + + ////////////////////////////////////////////////////////////////////////////// + // for each criteria, if its field has any applicable behaviors, apply them // + ////////////////////////////////////////////////////////////////////////////// + for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria())) + { + QFieldMetaData field = table.getFields().get(criteria.getFieldName()); + if(field == null && criteria.getFieldName() != null && criteria.getFieldName().contains(".")) + { + String[] parts = criteria.getFieldName().split("\\."); + if(parts.length == 2) + { + QTableMetaData joinTable = instance.getTable(parts[0]); + if(joinTable != null) + { + field = joinTable.getFields().get(parts[1]); + } + } + } + + if(field != null) + { + for(FieldBehavior fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors())) + { + boolean applyBehavior = true; + if(behaviorsToOmit != null && behaviorsToOmit.contains(fieldBehavior)) + { + applyBehavior = false; + } + + if(applyBehavior && fieldBehavior instanceof FieldFilterBehavior filterBehavior) + { + ////////////////////////////////////////////////////////////////////// + // call to apply the behavior on the criteria - which will return a // + // new criteria if any values are changed, else the input criteria // + ////////////////////////////////////////////////////////////////////// + QFilterCriteria newCriteria = apply(criteria, instance, table, field, filterBehavior); + + if(newCriteria != criteria) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the new criteria is not the same as the old criteria, mark that we need to make and return a clone. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + newCriteriaList.add(newCriteria); + needToUseClone = true; + } + else + { + newCriteriaList.add(criteria); + } + } + } + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // similar to above - iterate over the subfilters, making a recursive call, and tracking if we // + // got back the same object (in which case, there are no changes, and we don't need to clone), // + // or a different object (in which case, we do need a clone, because there were changes). // + ///////////////////////////////////////////////////////////////////////////////////////////////// + for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters())) + { + QQueryFilter newSubFilter = applyFieldBehaviorsToFilter(instance, table, subFilter, behaviorsToOmit); + if(newSubFilter != subFilter) + { + newSubFilters.add(newSubFilter); + needToUseClone = true; + } + else + { + newSubFilters.add(subFilter); + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // if we need to return a clone, then do so, replacing the lists with the ones we built in here // + ////////////////////////////////////////////////////////////////////////////////////////////////// + if(needToUseClone) + { + QQueryFilter cloneFilter = filter.clone(); + cloneFilter.setCriteria(newCriteriaList); + cloneFilter.setSubFilters(newSubFilters); + return (cloneFilter); + } + + ///////////////////////////////////////////////////////////////////////////// + // else, if no clone needed (e.g., no changes), return the original filter // + ///////////////////////////////////////////////////////////////////////////// + return (filter); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QFilterCriteria apply(QFilterCriteria criteria, QInstance instance, QTableMetaData table, QFieldMetaData field, FieldFilterBehavior filterBehavior) + { + if(criteria == null || CollectionUtils.nullSafeIsEmpty(criteria.getValues())) + { + return (criteria); + } + + List newValues = new ArrayList<>(); + boolean changedAny = false; + + for(Serializable value : criteria.getValues()) + { + Serializable newValue = filterBehavior.applyToFilterCriteriaValue(value, instance, table, field); + if(!Objects.equals(value, newValue)) + { + newValues.add(newValue); + changedAny = true; + } + else + { + newValues.add(value); + } + } + + if(changedAny) + { + QFilterCriteria clone = criteria.clone(); + clone.setValues(newValues); + return (clone); + } + else + { + return (criteria); + } + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehavior.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehavior.java new file mode 100644 index 00000000..56b71ee8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehavior.java @@ -0,0 +1,143 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.fields; + + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** Field behavior that changes the case of string values. + *******************************************************************************/ +public enum CaseChangeBehavior implements FieldBehavior, FieldBehaviorForFrontend, FieldFilterBehavior +{ + NONE(null), + TO_UPPER_CASE((String s) -> s.toUpperCase()), + TO_LOWER_CASE((String s) -> s.toLowerCase()); + + + private final Function function; + + + + /******************************************************************************* + ** + *******************************************************************************/ + CaseChangeBehavior(Function function) + { + this.function = function; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public CaseChangeBehavior getDefault() + { + return (NONE); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void apply(ValueBehaviorApplier.Action action, List recordList, QInstance instance, QTableMetaData table, QFieldMetaData field) + { + if(this.equals(NONE)) + { + return; + } + + switch(this) + { + case TO_UPPER_CASE, TO_LOWER_CASE -> applyFunction(recordList, table, field); + default -> throw new IllegalStateException("Unexpected enum value: " + this); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void applyFunction(List recordList, QTableMetaData table, QFieldMetaData field) + { + String fieldName = field.getName(); + for(QRecord record : CollectionUtils.nonNullList(recordList)) + { + String value = record.getValueString(fieldName); + if(value != null && function != null) + { + record.setValue(fieldName, function.apply(value)); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Serializable applyToFilterCriteriaValue(Serializable value, QInstance instance, QTableMetaData table, QFieldMetaData field) + { + if(this.equals(NONE) || function == null) + { + return (value); + } + + if(value instanceof String s) + { + String newValue = function.apply(s); + if(!Objects.equals(value, newValue)) + { + return (newValue); + } + } + + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean allowMultipleBehaviorsOfThisType() + { + return (false); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldBehaviorForFrontend.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldBehaviorForFrontend.java new file mode 100644 index 00000000..5cb16227 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldBehaviorForFrontend.java @@ -0,0 +1,34 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.fields; + + +import java.io.Serializable; + + +/******************************************************************************* + ** Marker interface for a field behavior which you might want to send to a + ** frontend (e.g., so it can edit values to match what'll happen in the backend). + *******************************************************************************/ +public interface FieldBehaviorForFrontend extends Serializable +{ +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldDisplayBehavior.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldDisplayBehavior.java index c5150557..5ea1cfa3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldDisplayBehavior.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldDisplayBehavior.java @@ -23,7 +23,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields; /******************************************************************************* - ** + ** Interface to mark a field behavior as one to be used during generating + ** display values. *******************************************************************************/ public interface FieldDisplayBehavior> extends FieldBehavior { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldFilterBehavior.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldFilterBehavior.java new file mode 100644 index 00000000..7ee10a80 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/FieldFilterBehavior.java @@ -0,0 +1,43 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.fields; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** Interface to mark a field behavior as one to be used before a query filter + ** is executed. + *******************************************************************************/ +public interface FieldFilterBehavior> extends FieldBehavior +{ + + /******************************************************************************* + ** Apply the filter to a value from a criteria. + ** If you don't want to change the input value, return the parameter. + *******************************************************************************/ + Serializable applyToFilterCriteriaValue(Serializable value, QInstance instance, QTableMetaData table, QFieldMetaData field); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java index dbcf52b7..447b2ec5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java @@ -23,13 +23,17 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment; +import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehaviorForFrontend; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* @@ -53,6 +57,8 @@ public class QFrontendFieldMetaData private List adornments; private List helpContents; + private List fieldBehaviors; + ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // ////////////////////////////////////////////////////////////////////////////////// @@ -75,6 +81,18 @@ public class QFrontendFieldMetaData this.adornments = fieldMetaData.getAdornments(); this.defaultValue = fieldMetaData.getDefaultValue(); this.helpContents = fieldMetaData.getHelpContents(); + + for(FieldBehavior behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors())) + { + if(behavior instanceof FieldBehaviorForFrontend fbff) + { + if(fieldBehaviors == null) + { + fieldBehaviors = new ArrayList<>(); + } + fieldBehaviors.add(fbff); + } + } } @@ -198,4 +216,14 @@ public class QFrontendFieldMetaData return helpContents; } + + + /******************************************************************************* + ** Getter for fieldBehaviors + ** + *******************************************************************************/ + public List getFieldBehaviors() + { + return fieldBehaviors; + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java index 6eef7570..504769bd 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java @@ -22,15 +22,28 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.util.List; +import java.util.Map; +import java.util.Set; import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.CaseChangeBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; +import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; /******************************************************************************* @@ -71,4 +84,40 @@ class GetActionTest extends BaseTest assertNotNull(result.getRecord()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFilterFieldBehaviors() throws QException + { + ///////////////////////////////////////////////////////////////////////// + // insert one shape with a mixed-case name, one with an all-lower name // + ///////////////////////////////////////////////////////////////////////// + new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecords(List.of( + new QRecord().withValue("name", "Triangle"), + new QRecord().withValue("name", "square") + ))); + + /////////////////////////////////////////////////////////////////////////// + // now set the shape table's name field to have a to-lower-case behavior // + /////////////////////////////////////////////////////////////////////////// + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + table.withUniqueKey(new UniqueKey("name")); + QFieldMetaData field = table.getField("name"); + field.setBehaviors(Set.of(CaseChangeBehavior.TO_LOWER_CASE)); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // confirm that if we query for "Triangle", we can't find it (because query will to-lower-case the criteria) // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertNull(GetAction.execute(TestUtils.TABLE_NAME_SHAPE, Map.of("name", "Triangle"))); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // confirm that if we query for "SQUARE", we do find it (because query will to-lower-case the criteria) // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertNotNull(GetAction.execute(TestUtils.TABLE_NAME_SHAPE, Map.of("name", "sQuArE"))); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java index 67c337ab..1866385f 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.Set; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; @@ -32,13 +33,18 @@ import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.CaseChangeBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.querystats.QueryStat; import com.kingsrook.qqq.backend.core.model.querystats.QueryStatMetaDataProvider; import com.kingsrook.qqq.backend.core.model.session.QSession; @@ -499,4 +505,40 @@ class QueryActionTest extends BaseTest insertInput.setRecords(recordList); new InsertAction().execute(insertInput); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFilterFieldBehaviors() throws QException + { + ///////////////////////////////////////////////////////////////////////// + // insert one shape with a mixed-case name, one with an all-lower name // + ///////////////////////////////////////////////////////////////////////// + new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecords(List.of( + new QRecord().withValue("name", "Triangle"), + new QRecord().withValue("name", "square") + ))); + + /////////////////////////////////////////////////////////////////////////// + // now set the shape table's name field to have a to-lower-case behavior // + /////////////////////////////////////////////////////////////////////////// + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QFieldMetaData field = table.getField("name"); + field.setBehaviors(Set.of(CaseChangeBehavior.TO_LOWER_CASE)); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // confirm that if we query for "Triangle", we can't find it (because query will to-lower-case the criteria) // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(0, QueryAction.execute(TestUtils.TABLE_NAME_SHAPE, new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle"))).size()); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // confirm that if we query for "SQUARE", we do find it (because query will to-lower-case the criteria) // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(1, QueryAction.execute(TestUtils.TABLE_NAME_SHAPE, new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "SqUaRe"))).size()); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java index 65c40b22..5e5b95b5 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java @@ -27,11 +27,15 @@ import java.util.Optional; import java.util.Set; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.CaseChangeBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -39,7 +43,9 @@ import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -255,4 +261,64 @@ class ValueBehaviorApplierTest extends BaseTest } } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFilters() + { + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QFieldMetaData field = table.getField("name"); + field.setBehaviors(Set.of(CaseChangeBehavior.TO_LOWER_CASE)); + + assertNull(ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, null, null)); + + QQueryFilter emptyFilter = new QQueryFilter(); + assertSame(emptyFilter, ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, emptyFilter, null)); + + QQueryFilter hasCriteriaButNotUpdated = new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 1)); + assertSame(hasCriteriaButNotUpdated, ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, hasCriteriaButNotUpdated, null)); + + QQueryFilter hasSubFiltersButNotUpdated = new QQueryFilter().withSubFilters(List.of(new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 1)))); + assertSame(hasSubFiltersButNotUpdated, ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, hasSubFiltersButNotUpdated, null)); + + QQueryFilter hasCriteriaWithoutValues = new QQueryFilter().withSubFilters(List.of(new QQueryFilter().withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS)))); + assertSame(hasCriteriaWithoutValues, ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, hasCriteriaWithoutValues, null)); + + QQueryFilter hasCriteriaAndSubFiltersButNotUpdated = new QQueryFilter() + .withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 1)) + .withSubFilters(List.of(new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 1)))); + assertSame(hasCriteriaAndSubFiltersButNotUpdated, ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, hasCriteriaAndSubFiltersButNotUpdated, null)); + + QQueryFilter hasCriteriaToUpdate = new QQueryFilter().withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")); + QQueryFilter hasCriteriaUpdated = ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, hasCriteriaToUpdate, null); + assertNotSame(hasCriteriaToUpdate, hasCriteriaUpdated); + assertEquals("triangle", hasCriteriaUpdated.getCriteria().get(0).getValues().get(0)); + assertEquals(hasCriteriaToUpdate.getSubFilters(), hasCriteriaUpdated.getSubFilters()); + + QQueryFilter hasSubFilterToUpdate = new QQueryFilter().withSubFilter(new QQueryFilter().withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Oval"))); + QQueryFilter hasSubFilterUpdated = ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, hasSubFilterToUpdate, null); + assertNotSame(hasSubFilterToUpdate, hasSubFilterUpdated); + assertEquals("oval", hasSubFilterUpdated.getSubFilters().get(0).getCriteria().get(0).getValues().get(0)); + assertEquals(hasSubFilterToUpdate.getCriteria(), hasSubFilterUpdated.getCriteria()); + + QQueryFilter hasCriteriaAndSubFilterToUpdate = new QQueryFilter() + .withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Square")) + .withSubFilter(new QQueryFilter().withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Circle"))); + QQueryFilter hasCriteriaAndSubFilterUpdated = ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, hasCriteriaAndSubFilterToUpdate, null); + assertNotSame(hasCriteriaAndSubFilterToUpdate, hasCriteriaAndSubFilterUpdated); + assertEquals("square", hasCriteriaAndSubFilterUpdated.getCriteria().get(0).getValues().get(0)); + assertEquals("circle", hasCriteriaAndSubFilterUpdated.getSubFilters().get(0).getCriteria().get(0).getValues().get(0)); + + QQueryFilter hasMultiValueCriteriaToUpdate = new QQueryFilter().withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle", "Square")); + QQueryFilter hasMultiValueCriteriaUpdated = ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, hasMultiValueCriteriaToUpdate, null); + assertNotSame(hasMultiValueCriteriaToUpdate, hasMultiValueCriteriaUpdated); + assertEquals(List.of("triangle", "square"), hasMultiValueCriteriaUpdated.getCriteria().get(0).getValues()); + assertEquals(hasMultiValueCriteriaToUpdate.getSubFilters(), hasMultiValueCriteriaUpdated.getSubFilters()); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehaviorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehaviorTest.java new file mode 100644 index 00000000..148413ae --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehaviorTest.java @@ -0,0 +1,215 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.fields; + + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for CaseChangeBehavior + *******************************************************************************/ +class CaseChangeBehaviorTest extends BaseTest +{ + public static final String FIELD = "firstName" ; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNone() + { + assertNull(applyToRecord(CaseChangeBehavior.NONE, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD)); + assertNull(applyToRecord(CaseChangeBehavior.NONE, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD)); + assertEquals("John", applyToRecord(CaseChangeBehavior.NONE, new QRecord().withValue(FIELD, "John"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD)); + + assertEquals(ListBuilder.of("John", null, "Jane"), applyToRecords(CaseChangeBehavior.NONE, List.of( + new QRecord().withValue(FIELD, "John"), + new QRecord(), + new QRecord().withValue(FIELD, "Jane")), + ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testToUpperCase() + { + assertNull(applyToRecord(CaseChangeBehavior.TO_UPPER_CASE, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD)); + assertNull(applyToRecord(CaseChangeBehavior.TO_UPPER_CASE, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD)); + assertEquals("JOHN", applyToRecord(CaseChangeBehavior.TO_UPPER_CASE, new QRecord().withValue(FIELD, "John"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD)); + + assertEquals(ListBuilder.of("JOHN", null, "JANE"), applyToRecords(CaseChangeBehavior.TO_UPPER_CASE, List.of( + new QRecord().withValue(FIELD, "John"), + new QRecord(), + new QRecord().withValue(FIELD, "Jane")), + ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testToLowerCase() + { + assertNull(applyToRecord(CaseChangeBehavior.TO_LOWER_CASE, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD)); + assertNull(applyToRecord(CaseChangeBehavior.TO_LOWER_CASE, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD)); + assertEquals("john", applyToRecord(CaseChangeBehavior.TO_LOWER_CASE, new QRecord().withValue(FIELD, "John"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD)); + + assertEquals(ListBuilder.of("john", null, "jane"), applyToRecords(CaseChangeBehavior.TO_LOWER_CASE, List.of( + new QRecord().withValue(FIELD, "John"), + new QRecord(), + new QRecord().withValue(FIELD, "Jane")), + ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QRecord applyToRecord(CaseChangeBehavior behavior, QRecord record, ValueBehaviorApplier.Action action) + { + return (applyToRecords(behavior, List.of(record), action).get(0)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List applyToRecords(CaseChangeBehavior behavior, List records, ValueBehaviorApplier.Action action) + { + QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + behavior.apply(action, records, QContext.getQInstance(), table, table.getField(FIELD)); + return (records); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReads() throws QException + { + TestUtils.insertDefaultShapes(QContext.getQInstance()); + + List records = QueryAction.execute(TestUtils.TABLE_NAME_SHAPE, null); + assertEquals(Set.of("Triangle", "Square", "Circle"), records.stream().map(r -> r.getValueString("name")).collect(Collectors.toSet())); + + QFieldMetaData field = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_SHAPE).getField("name"); + field.setBehaviors(Set.of(CaseChangeBehavior.TO_UPPER_CASE)); + + records = QueryAction.execute(TestUtils.TABLE_NAME_SHAPE, null); + assertEquals(Set.of("TRIANGLE", "SQUARE", "CIRCLE"), records.stream().map(r -> r.getValueString("name")).collect(Collectors.toSet())); + + field.setBehaviors(Set.of(CaseChangeBehavior.TO_LOWER_CASE)); + assertEquals("triangle", GetAction.execute(TestUtils.TABLE_NAME_SHAPE, 1).getValueString("name")); + + field.setBehaviors(Set.of(CaseChangeBehavior.NONE)); + assertEquals("Triangle", GetAction.execute(TestUtils.TABLE_NAME_SHAPE, 1).getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWrites() throws QException + { + Integer id = 100; + + QFieldMetaData field = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_SHAPE).getField("name"); + field.setBehaviors(Set.of(CaseChangeBehavior.TO_UPPER_CASE)); + new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", id).withValue("name", "Octagon"))); + + ////////////////////////////////////////////////////////////////////////////////// + // turn off the to-upper-case behavior, so we'll see what was actually inserted // + ////////////////////////////////////////////////////////////////////////////////// + field.setBehaviors(Collections.emptySet()); + assertEquals("OCTAGON", GetAction.execute(TestUtils.TABLE_NAME_SHAPE, id).getValueString("name")); + + //////////////////////////////////////////// + // change to toLowerCase and do an update // + //////////////////////////////////////////// + field.setBehaviors(Set.of(CaseChangeBehavior.TO_LOWER_CASE)); + new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", id).withValue("name", "Octagon"))); + + //////////////////////////////////////////////////////////////////////////////////// + // turn off the to-lower-case behavior, so we'll see what was actually udpated to // + //////////////////////////////////////////////////////////////////////////////////// + field.setBehaviors(Collections.emptySet()); + assertEquals("octagon", GetAction.execute(TestUtils.TABLE_NAME_SHAPE, id).getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFilter() + { + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QFieldMetaData field = table.getField("name"); + field.setBehaviors(Set.of(CaseChangeBehavior.TO_UPPER_CASE)); + assertEquals("SQUARE", CaseChangeBehavior.TO_UPPER_CASE.applyToFilterCriteriaValue("square", qInstance, table, field)); + + field.setBehaviors(Set.of(CaseChangeBehavior.TO_LOWER_CASE)); + assertEquals("triangle", CaseChangeBehavior.TO_LOWER_CASE.applyToFilterCriteriaValue("Triangle", qInstance, table, field)); + + field.setBehaviors(Set.of(CaseChangeBehavior.NONE)); + assertEquals("Circle", CaseChangeBehavior.NONE.applyToFilterCriteriaValue("Circle", qInstance, table, field)); + } + +} \ No newline at end of file From dc84a9ef55d0e1c7253958e0b806aa0cc8771703 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jun 2024 08:15:59 -0500 Subject: [PATCH 2/6] CE-1402 add instance validation to CaseChangeBehavior --- .../metadata/fields/CaseChangeBehavior.java | 29 +++++++++++++++++++ .../fields/CaseChangeBehaviorTest.java | 27 +++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehavior.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehavior.java index 56b71ee8..3bf140f4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehavior.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehavior.java @@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Function; @@ -140,4 +142,31 @@ public enum CaseChangeBehavior implements FieldBehavior, Fie return (false); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public List validateBehaviorConfiguration(QTableMetaData tableMetaData, QFieldMetaData fieldMetaData) + { + if(this == NONE) + { + return Collections.emptyList(); + } + + List errors = new ArrayList<>(); + String errorSuffix = " field [" + fieldMetaData.getName() + "] in table [" + tableMetaData.getName() + "]"; + + if(fieldMetaData.getType() != null) + { + if(!fieldMetaData.getType().isStringLike()) + { + errors.add("A CaseChange was a applied to a non-String-like field:" + errorSuffix); + } + } + + return (errors); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehaviorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehaviorTest.java index 148413ae..b5851024 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehaviorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/CaseChangeBehaviorTest.java @@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* @@ -212,4 +213,30 @@ class CaseChangeBehaviorTest extends BaseTest assertEquals("Circle", CaseChangeBehavior.NONE.applyToFilterCriteriaValue("Circle", qInstance, table, field)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValidation() + { + QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_SHAPE); + + /////////////////////////////////////////// + // should be no errors on a string field // + /////////////////////////////////////////// + assertTrue(CaseChangeBehavior.TO_UPPER_CASE.validateBehaviorConfiguration(table, table.getField("name")).isEmpty()); + + ////////////////////////////////////////// + // should be an error on a number field // + ////////////////////////////////////////// + assertEquals(1, CaseChangeBehavior.TO_LOWER_CASE.validateBehaviorConfiguration(table, table.getField("id")).size()); + + ///////////////////////////////////////// + // NONE should be allowed on any field // + ///////////////////////////////////////// + assertTrue(CaseChangeBehavior.NONE.validateBehaviorConfiguration(table, table.getField("id")).isEmpty()); + } + } \ No newline at end of file From 82201286d4e1a5c7a972ef2c4f782a4e888aa502 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jun 2024 08:40:51 -0500 Subject: [PATCH 3/6] CE-1402 Make consistent naming 'behaviors', not 'fieldBehaviors' --- .../metadata/frontend/QFrontendFieldMetaData.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java index 447b2ec5..54b8aba6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java @@ -57,7 +57,7 @@ public class QFrontendFieldMetaData private List adornments; private List helpContents; - private List fieldBehaviors; + private List behaviors; ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // @@ -86,11 +86,11 @@ public class QFrontendFieldMetaData { if(behavior instanceof FieldBehaviorForFrontend fbff) { - if(fieldBehaviors == null) + if(behaviors == null) { - fieldBehaviors = new ArrayList<>(); + behaviors = new ArrayList<>(); } - fieldBehaviors.add(fbff); + behaviors.add(fbff); } } } @@ -222,8 +222,8 @@ public class QFrontendFieldMetaData ** Getter for fieldBehaviors ** *******************************************************************************/ - public List getFieldBehaviors() + public List getBehaviors() { - return fieldBehaviors; + return behaviors; } } From 1eb078d91642e6d318b6aa72edfcb1a9534b5284 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jun 2024 10:31:56 -0500 Subject: [PATCH 4/6] CE-1402 avoid NPE getting behaviors --- .../kingsrook/qqq/backend/core/actions/tables/GetAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java index 8c7ef656..7fc8bec8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java @@ -208,7 +208,7 @@ public class GetAction return getFieldFilterBehaviorMemoization.getResult(key, (p) -> { List> rs = new ArrayList<>(); - for(FieldBehavior fieldBehavior : tableMetaData.getFields().get(fieldName).getBehaviors()) + for(FieldBehavior fieldBehavior : CollectionUtils.nonNullCollection(tableMetaData.getFields().get(fieldName).getBehaviors())) { if(fieldBehavior instanceof FieldFilterBehavior fieldFilterBehavior) { From 7cbd6705e1060352396ee5729779da7b24fe9a6f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jun 2024 10:32:33 -0500 Subject: [PATCH 5/6] CE-1402 Fix (with test) applying field filter behaviors --- .../actions/values/ValueBehaviorApplier.java | 18 ++-- .../values/ValueBehaviorApplierTest.java | 82 ++++++++++++++++++- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java index 2baebf71..62442fe1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplier.java @@ -159,6 +159,7 @@ public class ValueBehaviorApplier } } + QFilterCriteria criteriaToUse = criteria; if(field != null) { for(FieldBehavior fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors())) @@ -175,23 +176,20 @@ public class ValueBehaviorApplier // call to apply the behavior on the criteria - which will return a // // new criteria if any values are changed, else the input criteria // ////////////////////////////////////////////////////////////////////// - QFilterCriteria newCriteria = apply(criteria, instance, table, field, filterBehavior); + criteriaToUse = apply(criteriaToUse, instance, table, field, filterBehavior); - if(newCriteria != criteria) + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the new criteria is not the same as the old criteria, mark that we need to make and return a clone. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(criteriaToUse != criteria) { - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // if the new criteria is not the same as the old criteria, mark that we need to make and return a clone. // - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - newCriteriaList.add(newCriteria); needToUseClone = true; } - else - { - newCriteriaList.add(criteria); - } } } } + + newCriteriaList.add(criteriaToUse); } ///////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java index 5e5b95b5..338bb103 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/ValueBehaviorApplierTest.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions.values; +import java.io.Serializable; import java.util.List; import java.util.Optional; import java.util.Set; @@ -35,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.CaseChangeBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldFilterBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -164,7 +166,7 @@ class ValueBehaviorApplierTest extends BaseTest QRecord record = new QRecord().withValue("firstName", "Homer").withValue("lastName", "Simpson").withValue("ssn", "0123456789"); ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, qInstance, table, List.of(record), null); - + assertEquals("HOMER", record.getDisplayValue("firstName")); assertNull(record.getDisplayValue("lastName")); // noop will literally do nothing, not even pass value through. assertEquals("0123456789", record.getValueString("ssn")); // formatting action should not run the too-long truncate behavior @@ -314,11 +316,87 @@ class ValueBehaviorApplierTest extends BaseTest assertEquals("square", hasCriteriaAndSubFilterUpdated.getCriteria().get(0).getValues().get(0)); assertEquals("circle", hasCriteriaAndSubFilterUpdated.getSubFilters().get(0).getCriteria().get(0).getValues().get(0)); - QQueryFilter hasMultiValueCriteriaToUpdate = new QQueryFilter().withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle", "Square")); + QQueryFilter hasMultiValueCriteriaToUpdate = new QQueryFilter().withCriteria(new QFilterCriteria("name", QCriteriaOperator.IN, "Triangle", "Square")); QQueryFilter hasMultiValueCriteriaUpdated = ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, hasMultiValueCriteriaToUpdate, null); assertNotSame(hasMultiValueCriteriaToUpdate, hasMultiValueCriteriaUpdated); assertEquals(List.of("triangle", "square"), hasMultiValueCriteriaUpdated.getCriteria().get(0).getValues()); assertEquals(hasMultiValueCriteriaToUpdate.getSubFilters(), hasMultiValueCriteriaUpdated.getSubFilters()); + + QQueryFilter hasMultipleCriteriaOnlyToUpdate = new QQueryFilter() + .withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Square")) + .withCriteria(new QFilterCriteria("id", QCriteriaOperator.IS_NOT_BLANK)); + + QQueryFilter hasMultipleCriteriaOnlyOneUpdated = ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, hasMultipleCriteriaOnlyToUpdate, null); + assertNotSame(hasMultipleCriteriaOnlyToUpdate, hasMultipleCriteriaOnlyOneUpdated); + assertEquals(2, hasMultipleCriteriaOnlyOneUpdated.getCriteria().size()); + assertEquals(List.of("square"), hasMultipleCriteriaOnlyOneUpdated.getCriteria().get(0).getValues()); + assertEquals(hasMultipleCriteriaOnlyToUpdate.getSubFilters(), hasMultipleCriteriaOnlyOneUpdated.getSubFilters()); + + ////////////////////////////////////////////////////////// + // set 2 behaviors on the field - make sure both happen // + ////////////////////////////////////////////////////////// + field.setBehaviors(Set.of(CaseChangeBehavior.TO_LOWER_CASE, new AppendSomethingBehavior("-x"))); + QQueryFilter criteriaValueToUpdateTwice = new QQueryFilter().withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")); + QQueryFilter criteriaValueUpdatedTwice = ValueBehaviorApplier.applyFieldBehaviorsToFilter(qInstance, table, criteriaValueToUpdateTwice, null); + assertNotSame(criteriaValueToUpdateTwice, criteriaValueUpdatedTwice); + assertEquals("triangle-x", criteriaValueUpdatedTwice.getCriteria().get(0).getValues().get(0)); + assertEquals(criteriaValueToUpdateTwice.getSubFilters(), criteriaValueUpdatedTwice.getSubFilters()); + } + + + + /*************************************************************************** + * + ***************************************************************************/ + public static class AppendSomethingBehavior implements FieldBehavior, FieldFilterBehavior + { + private String something; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public AppendSomethingBehavior(String something) + { + this.something = something; + } + + + + /*************************************************************************** + * + ***************************************************************************/ + @Override + public Serializable applyToFilterCriteriaValue(Serializable value, QInstance instance, QTableMetaData table, QFieldMetaData field) + { + return value + something; + } + + + + /*************************************************************************** + * + ***************************************************************************/ + @Override + public AppendSomethingBehavior getDefault() + { + return null; + } + + + + /*************************************************************************** + * + ***************************************************************************/ + @Override + public void apply(ValueBehaviorApplier.Action action, List recordList, QInstance instance, QTableMetaData table, QFieldMetaData field) + { + ////////// + // noop // + ////////// + } } } From 9e9f2668785e82f64334c158dfa2fc339636a705 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jun 2024 13:31:13 -0500 Subject: [PATCH 6/6] CE-1402 Add CaseChangeBehavior sub-section --- docs/metaData/Fields.adoc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/metaData/Fields.adoc b/docs/metaData/Fields.adoc index eb20fa84..da4f0993 100644 --- a/docs/metaData/Fields.adoc +++ b/docs/metaData/Fields.adoc @@ -117,3 +117,19 @@ new QTableMetaData().withName("flights").withFields(List.of( .withBehavior(new DateTimeDisplayValueBehavior() .withDefaultZoneId("UTC")) ---- + +===== CaseChangeBehavior +A field can be made to always go through a toUpperCase or toLowerCase transformation, both before it is stored in a backend, +and after it is read from a backend, by adding a CaseChangeBehavior to it: + +[source,java] +.Examples of using CaseChangeBehavior +---- +new QTableMetaData().withName("item").withFields(List.of( + + new QFieldMetaData("sku", QFieldType.STRING) + .withBehavior(CaseChangeBehavior.TO_UPPER_CASE)), + + new QFieldMetaData("username", QFieldType.STRING) + .withBehavior(CaseChangeBehavior.TO_LOWER_CASE)), +----