diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/WhiteSpaceBehavior.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/WhiteSpaceBehavior.java
new file mode 100644
index 00000000..3beaf3d1
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/WhiteSpaceBehavior.java
@@ -0,0 +1,174 @@
+/*
+ * 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.ArrayList;
+import java.util.Collections;
+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 whitespace of string values.
+ *******************************************************************************/
+public enum WhiteSpaceBehavior implements FieldBehavior, FieldBehaviorForFrontend, FieldFilterBehavior
+{
+ NONE(null),
+ REMOVE_ALL_WHITESPACE((String s) -> s.chars().filter(c -> !Character.isWhitespace(c)).collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString()),
+ TRIM((String s) -> s.trim()),
+ TRIM_LEFT((String s) -> s.stripLeading()),
+ TRIM_RIGHT((String s) -> s.stripTrailing());
+
+
+ private final Function function;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ WhiteSpaceBehavior(Function function)
+ {
+ this.function = function;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public WhiteSpaceBehavior 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 REMOVE_ALL_WHITESPACE, TRIM, TRIM_LEFT, TRIM_RIGHT -> 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);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @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 WhiteSpaceBehavior 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/WhiteSpaceBehaviorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/WhiteSpaceBehaviorTest.java
new file mode 100644
index 00000000..29071874
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/fields/WhiteSpaceBehaviorTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for WhiteSpaceBehavior
+ *******************************************************************************/
+class WhiteSpaceBehaviorTest extends BaseTest
+{
+ public static final String FIELD = "firstName";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNone()
+ {
+ assertNull(applyToRecord(WhiteSpaceBehavior.NONE, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+ assertNull(applyToRecord(WhiteSpaceBehavior.NONE, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+ assertEquals("John", applyToRecord(WhiteSpaceBehavior.NONE, new QRecord().withValue(FIELD, "John"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+
+ assertEquals(ListBuilder.of("J. ohn", null, "Jane\n"), applyToRecords(WhiteSpaceBehavior.NONE, List.of(
+ new QRecord().withValue(FIELD, "J. ohn"),
+ new QRecord(),
+ new QRecord().withValue(FIELD, "Jane\n")),
+ ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testRemoveWhiteSpace()
+ {
+ assertNull(applyToRecord(WhiteSpaceBehavior.REMOVE_ALL_WHITESPACE, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+ assertNull(applyToRecord(WhiteSpaceBehavior.REMOVE_ALL_WHITESPACE, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+ assertEquals("doobeedoobeedoo", applyToRecord(WhiteSpaceBehavior.REMOVE_ALL_WHITESPACE, new QRecord().withValue(FIELD, "doo bee doo\n bee doo"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+
+ assertEquals(ListBuilder.of("thisistheway", null, "thatwastheway"), applyToRecords(WhiteSpaceBehavior.REMOVE_ALL_WHITESPACE, List.of(
+ new QRecord().withValue(FIELD, "this is\rthe way \t"),
+ new QRecord(),
+ new QRecord().withValue(FIELD, "that was the way\n")),
+ ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testTrimWhiteSpace()
+ {
+ assertNull(applyToRecord(WhiteSpaceBehavior.TRIM, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+ assertNull(applyToRecord(WhiteSpaceBehavior.TRIM, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+ assertEquals("doo bee doo\n bee doo", applyToRecord(WhiteSpaceBehavior.TRIM, new QRecord().withValue(FIELD, " doo bee doo\n bee doo\r \n\n"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+
+ assertEquals(ListBuilder.of("this is\rthe way", null, "that was the way"), applyToRecords(WhiteSpaceBehavior.TRIM, List.of(
+ new QRecord().withValue(FIELD, " this is\rthe way \t"),
+ new QRecord(),
+ new QRecord().withValue(FIELD, "that was the way\n")),
+ ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testTrimLeftWhiteSpace()
+ {
+ assertNull(applyToRecord(WhiteSpaceBehavior.TRIM_LEFT, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+ assertNull(applyToRecord(WhiteSpaceBehavior.TRIM_LEFT, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+ assertEquals("doo bee doo\n bee doo\r \n\n", applyToRecord(WhiteSpaceBehavior.TRIM_LEFT, new QRecord().withValue(FIELD, " doo bee doo\n bee doo\r \n\n"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+
+ assertEquals(ListBuilder.of("this is\rthe way \t", null, "that was the way\n"), applyToRecords(WhiteSpaceBehavior.TRIM_LEFT, List.of(
+ new QRecord().withValue(FIELD, " this is\rthe way \t"),
+ new QRecord(),
+ new QRecord().withValue(FIELD, " \n that was the way\n")),
+ ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testTrimRightWhiteSpace()
+ {
+ assertNull(applyToRecord(WhiteSpaceBehavior.TRIM_RIGHT, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+ assertNull(applyToRecord(WhiteSpaceBehavior.TRIM_RIGHT, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+ assertEquals(" doo bee doo\n bee doo", applyToRecord(WhiteSpaceBehavior.TRIM_RIGHT, new QRecord().withValue(FIELD, " doo bee doo\n bee doo\r \n\n"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
+
+ assertEquals(ListBuilder.of(" this is\rthe way", null, " \n that was the way"), applyToRecords(WhiteSpaceBehavior.TRIM_RIGHT, List.of(
+ new QRecord().withValue(FIELD, " this is\rthe way \t"),
+ new QRecord(),
+ new QRecord().withValue(FIELD, " \n that was the way\n")),
+ ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QRecord applyToRecord(WhiteSpaceBehavior behavior, QRecord record, ValueBehaviorApplier.Action action)
+ {
+ return (applyToRecords(behavior, List.of(record), action).get(0));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private List applyToRecords(WhiteSpaceBehavior 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 testValidation()
+ {
+ QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_SHAPE);
+
+ ///////////////////////////////////////////
+ // should be no errors on a string field //
+ ///////////////////////////////////////////
+ assertTrue(WhiteSpaceBehavior.TRIM.validateBehaviorConfiguration(table, table.getField("name")).isEmpty());
+
+ //////////////////////////////////////////
+ // should be an error on a number field //
+ //////////////////////////////////////////
+ assertEquals(1, WhiteSpaceBehavior.REMOVE_ALL_WHITESPACE.validateBehaviorConfiguration(table, table.getField("id")).size());
+
+ /////////////////////////////////////////
+ // NONE should be allowed on any field //
+ /////////////////////////////////////////
+ assertTrue(WhiteSpaceBehavior.NONE.validateBehaviorConfiguration(table, table.getField("id")).isEmpty());
+ }
+
+}
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiFieldUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiFieldUtils.java
new file mode 100644
index 00000000..79235273
--- /dev/null
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiFieldUtils.java
@@ -0,0 +1,106 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2023. 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.api.actions;
+
+
+import com.kingsrook.qqq.api.model.APIVersionRange;
+import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
+import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import org.apache.commons.lang.BooleanUtils;
+
+
+/*******************************************************************************
+ ** utility methods for working with fields
+ **
+ *******************************************************************************/
+public class ApiFieldUtils
+{
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static boolean isIncluded(String apiName, QFieldMetaData field)
+ {
+ ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
+ if(apiFieldMetaData != null && BooleanUtils.isTrue(apiFieldMetaData.getIsExcluded()))
+ {
+ return (false);
+ }
+
+ return (true);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static APIVersionRange getApiVersionRangeForRemovedField(String apiName, QFieldMetaData field)
+ {
+ ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
+ if(apiFieldMetaData != null && apiFieldMetaData.getInitialVersion() != null)
+ {
+ if(StringUtils.hasContent(apiFieldMetaData.getFinalVersion()))
+ {
+ return (APIVersionRange.betweenAndIncluding(apiFieldMetaData.getInitialVersion(), apiFieldMetaData.getFinalVersion()));
+ }
+ else
+ {
+ throw (new IllegalStateException("RemovedApiFieldMetaData for field [" + field.getName() + "] did not specify a finalVersion."));
+ }
+ }
+ else
+ {
+ throw (new IllegalStateException("RemovedApiFieldMetaData for field [" + field.getName() + "] did not specify an initialVersion."));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static APIVersionRange getApiVersionRange(String apiName, QFieldMetaData field)
+ {
+ ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
+ if(apiFieldMetaData != null && apiFieldMetaData.getInitialVersion() != null)
+ {
+ return (APIVersionRange.afterAndIncluding(apiFieldMetaData.getInitialVersion()));
+ }
+
+ return (APIVersionRange.none());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static ApiFieldMetaData getApiFieldMetaData(String apiName, QFieldMetaData field)
+ {
+ return ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData());
+ }
+}
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java
index 247242b1..c6eda4c5 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java
@@ -111,7 +111,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tags)
+ private Path generateProcessSpecPathObject(ApiInstanceMetaData apiInstanceMetaData, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData, List tags, APIVersion apiVersion)
{
String description = apiProcessMetaData.getDescription();
if(!StringUtils.hasContent(description))
@@ -927,7 +927,10 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData());
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/