diff --git a/pom.xml b/pom.xml index ea1003a4..e551b021 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ - 0.4.0-SNAPSHOT + 0.5.0-SNAPSHOT UTF-8 UTF-8 diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java deleted file mode 100644 index 4da39af3..00000000 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/Customizers.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2022. 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.actions.customizers; - - -/******************************************************************************* - ** Standard place where the names of QQQ Customization points are defined. - *******************************************************************************/ -public interface Customizers -{ - String POST_QUERY_RECORD = "postQueryRecord"; - -} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java index 31192c75..63b763e4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java @@ -24,8 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.customizers; import java.util.Optional; import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -43,15 +46,14 @@ public class QCodeLoader /******************************************************************************* ** *******************************************************************************/ - public static Function getTableCustomizerFunction(QTableMetaData table, String customizerName) + public static Optional> getTableCustomizerFunction(QTableMetaData table, String customizerName) { Optional codeReference = table.getCustomizer(customizerName); if(codeReference.isPresent()) { - return (QCodeLoader.getFunction(codeReference.get())); + return (Optional.ofNullable(QCodeLoader.getFunction(codeReference.get()))); } - - return null; + return (Optional.empty()); } @@ -131,4 +133,31 @@ public class QCodeLoader } } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QCustomPossibleValueProvider getCustomPossibleValueProvider(QPossibleValueSource possibleValueSource) throws QException + { + try + { + Class codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName()); + Object codeObject = codeClass.getConstructor().newInstance(); + if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider)) + { + throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider")); + } + return (customPossibleValueProvider); + } + catch(QException qe) + { + throw (qe); + } + catch(Exception e) + { + throw (new QException("Error getting custom possible value provider for PVS [" + possibleValueSource.getName() + "]", e)); + } + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizer.java new file mode 100644 index 00000000..9367a0c1 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizer.java @@ -0,0 +1,61 @@ +package com.kingsrook.qqq.backend.core.actions.customizers; + + +import java.util.function.Consumer; + + +/******************************************************************************* + ** Object used by TableCustomizers enum (and similar enums in backend modules) + ** to assist with definition and validation of Customizers applied to tables. + *******************************************************************************/ +public class TableCustomizer +{ + private final String role; + private final Class expectedType; + private final Consumer validationFunction; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public TableCustomizer(String role, Class expectedType, Consumer validationFunction) + { + this.role = role; + this.expectedType = expectedType; + this.validationFunction = validationFunction; + } + + + + /******************************************************************************* + ** Getter for role + ** + *******************************************************************************/ + public String getRole() + { + return role; + } + + + + /******************************************************************************* + ** Getter for expectedType + ** + *******************************************************************************/ + public Class getExpectedType() + { + return expectedType; + } + + + + /******************************************************************************* + ** Getter for validationFunction + ** + *******************************************************************************/ + public Consumer getValidationFunction() + { + return validationFunction; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizers.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizers.java new file mode 100644 index 00000000..e5899fad --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/TableCustomizers.java @@ -0,0 +1,85 @@ +package com.kingsrook.qqq.backend.core.actions.customizers; + + +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** Enum definition of possible table customizers - "roles" for custom code that + ** can be applied to tables. + ** + ** Works with TableCustomizer (singular version of this name) objects, during + ** instance validation, to provide validation of the referenced code (and to + ** make such validation from sub-backend-modules possible in the future). + ** + ** The idea of the 3rd argument here is to provide a way that we can enforce + ** the type-parameters for the custom code. E.g., if it's a Function - how + ** can we check at run-time that the type-params are correct? We couldn't find + ** how to do this "reflectively", so we can instead try to run the custom code, + ** passing it objects of the type that this customizer expects, and a validation + ** error will raise upon ClassCastException... This maybe could improve! + *******************************************************************************/ +public enum TableCustomizers +{ + POST_QUERY_RECORD(new TableCustomizer("postQueryRecord", Function.class, ((Object x) -> + { + Function function = (Function) x; + QRecord output = function.apply(new QRecord()); + }))); + + + private final TableCustomizer tableCustomizer; + + + + /******************************************************************************* + ** + *******************************************************************************/ + TableCustomizers(TableCustomizer tableCustomizer) + { + this.tableCustomizer = tableCustomizer; + } + + + + /******************************************************************************* + ** Get the TableCustomer for a given role (e.g., the role used in meta-data, not + ** the enum-constant name). + *******************************************************************************/ + public static TableCustomizers forRole(String name) + { + for(TableCustomizers value : values()) + { + if(value.tableCustomizer.getRole().equals(name)) + { + return (value); + } + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for tableCustomizer + ** + *******************************************************************************/ + public TableCustomizer getTableCustomizer() + { + return tableCustomizer; + } + + + + /******************************************************************************* + ** get the role from the tableCustomizer + ** + *******************************************************************************/ + public String getRole() + { + return (tableCustomizer.getRole()); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java index 3703a841..2ed70d09 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.utils.SleepUtils; import org.apache.logging.log4j.LogManager; @@ -42,12 +43,42 @@ public class RecordPipe private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(1_000); + private Consumer> postRecordActions = null; + + ///////////////////////////////////// + // See usage below for explanation // + ///////////////////////////////////// + private List singleRecordListForPostRecordActions = new ArrayList<>(); + /******************************************************************************* - ** Add a record to the pipe - ** Returns true iff the record fit in the pipe; false if the pipe is currently full. + ** Add a record to the pipe. Will block if the pipe is full. *******************************************************************************/ public void addRecord(QRecord record) + { + if(postRecordActions != null) + { + //////////////////////////////////////////////////////////////////////////////////// + // the initial use-case of this method is to call QueryAction.postRecordActions // + // that method requires that the list param be modifiable. Originally we used // + // List.of here - but that is immutable, so, instead use this single-record-list // + // (which we'll create as a field in this class, to avoid always re-constructing) // + //////////////////////////////////////////////////////////////////////////////////// + singleRecordListForPostRecordActions.add(record); + postRecordActions.accept(singleRecordListForPostRecordActions); + record = singleRecordListForPostRecordActions.remove(0); + } + + doAddRecord(record); + } + + + + /******************************************************************************* + ** Private internal version of add record - assumes the postRecordActions have + ** already ran. + *******************************************************************************/ + private void doAddRecord(QRecord record) { boolean offerResult = queue.offer(record); @@ -66,7 +97,15 @@ public class RecordPipe *******************************************************************************/ public void addRecords(List records) { - records.forEach(this::addRecord); + if(postRecordActions != null) + { + postRecordActions.accept(records); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure to go to the private version of doAddRecord - to avoid re-running the post-actions // + ////////////////////////////////////////////////////////////////////////////////////////////////// + records.forEach(this::doAddRecord); } @@ -101,4 +140,14 @@ public class RecordPipe return (queue.size()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setPostRecordActions(Consumer> postRecordActions) + { + this.postRecordActions = postRecordActions; + } + } 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 83ec1dd1..55eb17c9 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 @@ -22,12 +22,18 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; 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.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; @@ -38,6 +44,14 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; *******************************************************************************/ public class QueryAction { + private Optional> postQueryRecordCustomizer; + + private QueryInput queryInput; + private QValueFormatter qValueFormatter; + private QPossibleValueTranslator qPossibleValueTranslator; + + + /******************************************************************************* ** *******************************************************************************/ @@ -45,6 +59,14 @@ public class QueryAction { ActionHelper.validateSession(queryInput); + postQueryRecordCustomizer = QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole()); + this.queryInput = queryInput; + + if(queryInput.getRecordPipe() != null) + { + queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions); + } + QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); // todo pre-customization - just get to modify the request? @@ -53,20 +75,42 @@ public class QueryAction if(queryInput.getRecordPipe() == null) { - if(queryInput.getShouldGenerateDisplayValues()) - { - QValueFormatter qValueFormatter = new QValueFormatter(); - qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); - } - - if(queryInput.getShouldTranslatePossibleValues()) - { - QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession()); - qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); - } + postRecordActions(queryOutput.getRecords()); } return queryOutput; } + + + /******************************************************************************* + ** Run the necessary actions on a list of records (which must be a mutable list - e.g., + ** not one created via List.of()). This may include setting display values, + ** translating possible values, and running post-record customizations. + *******************************************************************************/ + public void postRecordActions(List records) + { + if(this.postQueryRecordCustomizer.isPresent()) + { + records.replaceAll(t -> postQueryRecordCustomizer.get().apply(t)); + } + + if(queryInput.getShouldGenerateDisplayValues()) + { + if(qValueFormatter == null) + { + qValueFormatter = new QValueFormatter(); + } + qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records); + } + + if(queryInput.getShouldTranslatePossibleValues()) + { + if(qPossibleValueTranslator == null) + { + qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession()); + } + qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java index 603951ee..5c50cf2c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -31,8 +31,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; 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; @@ -63,8 +63,10 @@ public class QPossibleValueTranslator private final QInstance qInstance; private final QSession session; - // top-level keys are pvsNames (not table names) - // 2nd-level keys are pkey values from the PVS table + /////////////////////////////////////////////////////// + // top-level keys are pvsNames (not table names) // + // 2nd-level keys are pkey values from the PVS table // + /////////////////////////////////////////////////////// private Map> possibleValueCache; @@ -120,9 +122,6 @@ public class QPossibleValueTranslator return (null); } - // todo - memoize!!! - // todo - bulk!!! - String resultValue = null; if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM)) { @@ -154,22 +153,14 @@ public class QPossibleValueTranslator /******************************************************************************* ** *******************************************************************************/ - private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource) { - try + for(QPossibleValue possibleValue : possibleValueSource.getEnumValues()) { - Class codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName()); - Object codeObject = codeClass.getConstructor().newInstance(); - if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider)) + if(possibleValue.getId().equals(value)) { - throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider")); + return (formatPossibleValue(possibleValueSource, possibleValue)); } - - return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value))); - } - catch(Exception e) - { - LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e); } return (null); @@ -205,6 +196,26 @@ public class QPossibleValueTranslator + /******************************************************************************* + ** + *******************************************************************************/ + private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource) + { + try + { + QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource); + return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value))); + } + catch(Exception e) + { + LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e); + } + + return (null); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -256,26 +267,11 @@ public class QPossibleValueTranslator - /******************************************************************************* - ** - *******************************************************************************/ - private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource) - { - for(QPossibleValue possibleValue : possibleValueSource.getEnumValues()) - { - if(possibleValue.getId().equals(value)) - { - return (formatPossibleValue(possibleValueSource, possibleValue)); - } - } - - return (null); - } - - - /******************************************************************************* ** prime the cache (e.g., by doing bulk-queries) for table-based PVS's + ** + ** @param table the table that the records are from + ** @param records the records that have the possible value id's (e.g., foreign keys) *******************************************************************************/ void primePvsCache(QTableMetaData table, List records) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index 27ccde27..5ac811a9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -25,10 +25,8 @@ package com.kingsrook.qqq.backend.core.actions.values; import java.io.Serializable; import java.util.List; 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.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; @@ -37,7 +35,7 @@ import org.apache.logging.log4j.Logger; /******************************************************************************* ** Utility to apply display formats to values for records and fields. - ** Note that this includes handling PossibleValues. + ** *******************************************************************************/ public class QValueFormatter { @@ -45,15 +43,6 @@ public class QValueFormatter - /******************************************************************************* - ** - *******************************************************************************/ - public QValueFormatter() - { - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -67,16 +56,6 @@ public class QValueFormatter return (null); } - // todo - is this appropriate, with this class and possibleValueTransaltor being decoupled - to still do standard formatting here? - // alternatively, shold we return null here? - // /////////////////////////////////////////////// - // // if the field has a possible value, use it // - // /////////////////////////////////////////////// - // if(field.getPossibleValueSourceName() != null) - // { - // return (this.possibleValueTranslator.translatePossibleValue(field, value)); - // } - //////////////////////////////////////////////////////// // if the field has a display format, try to apply it // //////////////////////////////////////////////////////// @@ -198,5 +177,4 @@ public class QValueFormatter } } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 119daa34..19beae30 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -229,7 +229,21 @@ public class QInstanceEnricher return (name.substring(0, 1).toUpperCase(Locale.ROOT)); } - return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z0-9]+)", " $1").replaceAll("([0-9])([A-Za-z])", "$1 $2")); + String suffix = name.substring(1) + + ////////////////////////////////////////////////////////////////////// + // Put a space before capital letters or numbers embedded in a name // + // e.g., omethingElse -> omething Else; umber1 -> umber 1 // + ////////////////////////////////////////////////////////////////////// + .replaceAll("([A-Z0-9]+)", " $1") + + //////////////////////////////////////////////////////////////// + // put a space between numbers and words that come after them // + // e.g., umber1dad -> number 1 dad // + //////////////////////////////////////////////////////////////// + .replaceAll("([0-9])([A-Za-z])", "$1 $2"); + + return (name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 66959a31..4b7c4c2b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -22,13 +22,20 @@ package com.kingsrook.qqq.backend.core.instances; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; @@ -38,6 +45,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -52,6 +61,11 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; *******************************************************************************/ public class QInstanceValidator { + private static final Logger LOG = LogManager.getLogger(QInstanceValidator.class); + + private boolean printWarnings = false; + + /******************************************************************************* ** @@ -202,12 +216,130 @@ public class QInstanceValidator assertCondition(errors, table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table."); } } + + if(table.getCustomizers() != null) + { + for(Map.Entry entry : table.getCustomizers().entrySet()) + { + validateTableCustomizer(errors, tableName, entry.getKey(), entry.getValue()); + } + } }); } } + /******************************************************************************* + ** + *******************************************************************************/ + private void validateTableCustomizer(List errors, String tableName, String customizerName, QCodeReference codeReference) + { + String prefix = "Table " + tableName + ", customizer " + customizerName + ": "; + + if(!preAssertionsForCodeReference(errors, codeReference, prefix)) + { + return; + } + + ////////////////////////////////////////////////////////////////////////////// + // make sure (at this time) that it's a java type, then do some java checks // + ////////////////////////////////////////////////////////////////////////////// + if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time.")) + { + /////////////////////////////////////// + // make sure the class can be loaded // + /////////////////////////////////////// + Class customizerClass = getClassForCodeReference(errors, codeReference, prefix); + if(customizerClass != null) + { + ////////////////////////////////////////////////// + // make sure the customizer can be instantiated // + ////////////////////////////////////////////////// + Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass); + + TableCustomizers tableCustomizer = TableCustomizers.forRole(customizerName); + if(tableCustomizer == null) + { + //////////////////////////////////////////////////////////////////////////////////////////////////// + // todo - in the future, load customizers from backend-modules (e.g., FilesystemTableCustomizers) // + //////////////////////////////////////////////////////////////////////////////////////////////////// + warn(prefix + "Unrecognized table customizer name (at least at backend-core level)"); + } + else + { + //////////////////////////////////////////////////////////////////////// + // make sure the customizer instance can be cast to the expected type // + //////////////////////////////////////////////////////////////////////// + if(customizerInstance != null && tableCustomizer.getTableCustomizer().getExpectedType() != null) + { + Object castedObject = getCastedObject(errors, prefix, tableCustomizer.getTableCustomizer().getExpectedType(), customizerInstance); + + Consumer validationFunction = tableCustomizer.getTableCustomizer().getValidationFunction(); + if(castedObject != null && validationFunction != null) + { + try + { + validationFunction.accept(castedObject); + } + catch(ClassCastException e) + { + errors.add(prefix + "Error validating customizer type parameters: " + e.getMessage()); + } + catch(Exception e) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // mmm, calling customizers w/ random data is expected to often throw, so, this check is iffy at best... // + // if we run into more trouble here, we might consider disabling the whole "validation function" check. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + } + } + } + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private T getCastedObject(List errors, String prefix, Class expectedType, Object customizerInstance) + { + T castedObject = null; + try + { + castedObject = expectedType.cast(customizerInstance); + } + catch(ClassCastException e) + { + errors.add(prefix + "CodeReference could not be casted to the expected type: " + expectedType); + } + return castedObject; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Object getInstanceOfCodeReference(List errors, String prefix, Class customizerClass) + { + Object customizerInstance = null; + try + { + customizerInstance = customizerClass.getConstructor().newInstance(); + } + catch(InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) + { + errors.add(prefix + "Instance of CodeReference could not be created: " + e); + } + return customizerInstance; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -313,7 +445,6 @@ public class QInstanceValidator qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) -> { assertCondition(errors, Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + "."); - assertCondition(errors, possibleValueSource.getIdType() != null, "Missing an idType for possibleValueSource: " + pvsName); if(assertCondition(errors, possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName)) { //////////////////////////////////////////////////////////////////////////////////////////////// @@ -347,6 +478,7 @@ public class QInstanceValidator if(assertCondition(errors, possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference.")) { assertCondition(errors, QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider."); + validateCustomPossibleValueSourceCode(errors, pvsName, possibleValueSource.getCustomCodeReference()); } } default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType()); @@ -358,6 +490,87 @@ public class QInstanceValidator + /******************************************************************************* + ** + *******************************************************************************/ + private void validateCustomPossibleValueSourceCode(List errors, String pvsName, QCodeReference codeReference) + { + String prefix = "PossibleValueSource " + pvsName + " custom code reference: "; + + if(!preAssertionsForCodeReference(errors, codeReference, prefix)) + { + return; + } + + ////////////////////////////////////////////////////////////////////////////// + // make sure (at this time) that it's a java type, then do some java checks // + ////////////////////////////////////////////////////////////////////////////// + if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time.")) + { + /////////////////////////////////////// + // make sure the class can be loaded // + /////////////////////////////////////// + Class customizerClass = getClassForCodeReference(errors, codeReference, prefix); + if(customizerClass != null) + { + ////////////////////////////////////////////////// + // make sure the customizer can be instantiated // + ////////////////////////////////////////////////// + Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass); + + //////////////////////////////////////////////////////////////////////// + // make sure the customizer instance can be cast to the expected type // + //////////////////////////////////////////////////////////////////////// + if(customizerInstance != null) + { + getCastedObject(errors, prefix, QCustomPossibleValueProvider.class, customizerInstance); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Class getClassForCodeReference(List errors, QCodeReference codeReference, String prefix) + { + Class customizerClass = null; + try + { + customizerClass = Class.forName(codeReference.getName()); + } + catch(ClassNotFoundException e) + { + errors.add(prefix + "Class for CodeReference could not be found."); + } + return customizerClass; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private boolean preAssertionsForCodeReference(List errors, QCodeReference codeReference, String prefix) + { + boolean okay = true; + if(!assertCondition(errors, StringUtils.hasContent(codeReference.getName()), prefix + " is missing a code reference name")) + { + okay = false; + } + + if(!assertCondition(errors, codeReference.getCodeType() != null, prefix + " is missing a code type")) + { + okay = false; + } + + return (okay); + } + + + /******************************************************************************* ** Check if an app's child list can recursively be traversed without finding a ** duplicate, which would indicate a cycle (e.g., an error) @@ -410,4 +623,16 @@ public class QInstanceValidator return (condition); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private void warn(String message) + { + if(printWarnings) + { + LOG.info("Validation warning: " + message); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java index 9412e0d0..a9e19342 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java @@ -24,13 +24,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.io.Serializable; import java.util.List; -import java.util.function.Function; -import com.kingsrook.qqq.backend.core.actions.customizers.Customizers; -import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -39,12 +34,8 @@ import org.apache.logging.log4j.Logger; *******************************************************************************/ public class QueryOutput extends AbstractActionOutput implements Serializable { - private static final Logger LOG = LogManager.getLogger(QueryOutput.class); - private QueryOutputStorageInterface storage; - private Function postQueryRecordCustomizer; - /******************************************************************************* @@ -61,8 +52,6 @@ public class QueryOutput extends AbstractActionOutput implements Serializable { storage = new QueryOutputList(); } - - postQueryRecordCustomizer = (Function) QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD); } @@ -76,36 +65,16 @@ public class QueryOutput extends AbstractActionOutput implements Serializable *******************************************************************************/ public void addRecord(QRecord record) { - record = runPostQueryRecordCustomizer(record); storage.addRecord(record); } - /******************************************************************************* - ** - *******************************************************************************/ - public QRecord runPostQueryRecordCustomizer(QRecord record) - { - if(this.postQueryRecordCustomizer != null) - { - record = this.postQueryRecordCustomizer.apply(record); - } - return record; - } - - - /******************************************************************************* ** add a list of records to this output *******************************************************************************/ public void addRecords(List records) { - if(this.postQueryRecordCustomizer != null) - { - records.replaceAll(t -> this.postQueryRecordCustomizer.apply(t)); - } - storage.addRecords(records); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index bf51c6fb..77cdd6e8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -334,7 +334,7 @@ public class QRecord implements Serializable *******************************************************************************/ public LocalTime getValueLocalTime(String fieldName) { - return ((LocalTime) ValueUtils.getValueAsLocalTime(values.get(fieldName))); + return (ValueUtils.getValueAsLocalTime(values.get(fieldName))); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PVSValueFormatAndFields.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PVSValueFormatAndFields.java new file mode 100644 index 00000000..f8ea0eb9 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PVSValueFormatAndFields.java @@ -0,0 +1,55 @@ +package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; + + +import java.util.List; + + +/******************************************************************************* + ** Define some standard ways to format the value portion of a PossibleValueSource. + ** + ** Can be passed to short-cut {set,with}ValueFormatAndFields methods in QPossibleValueSource + ** class, or the format & field properties can be extracted and passed to regular field-level setters. + *******************************************************************************/ +public enum PVSValueFormatAndFields +{ + LABEL_ONLY("%s", "label"), + LABEL_PARENS_ID("%s (%s)", "label", "id"), + ID_COLON_LABEL("%s: %s", "id", "label"); + + + private final String format; + private final List fields; + + + + /******************************************************************************* + ** + *******************************************************************************/ + PVSValueFormatAndFields(String format, String... fields) + { + this.format = format; + this.fields = List.of(fields); + } + + + + /******************************************************************************* + ** Getter for format + ** + *******************************************************************************/ + public String getFormat() + { + return format; + } + + + + /******************************************************************************* + ** Getter for fields + ** + *******************************************************************************/ + public List getFields() + { + return fields; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java index 24018b3a..fc61b2df 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/PossibleValueEnum.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; /******************************************************************************* + ** Interface to be implemented by enums which can be used as a PossibleValueSource. ** *******************************************************************************/ public interface PossibleValueEnum diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java index 735a22e4..4b15b193 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java @@ -25,44 +25,24 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues; import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; /******************************************************************************* - ** Meta-data to represent a single field in a table. + ** Meta-data to represent a "Possible value" - e.g., a translation of a foreign + ** key and/or a limited set of "possible values" for a field (e.g., from a foreign + ** table or an enum). ** *******************************************************************************/ public class QPossibleValueSource { private String name; private QPossibleValueSourceType type; - private QFieldType idType = QFieldType.INTEGER; - private String valueFormat = ValueFormat.DEFAULT; - private List valueFields = ValueFields.DEFAULT; + private String valueFormat = PVSValueFormatAndFields.LABEL_ONLY.getFormat(); + private List valueFields = PVSValueFormatAndFields.LABEL_ONLY.getFields(); private String valueFormatIfNotFound = null; private List valueFieldsIfNotFound = null; - - - public interface ValueFormat - { - String DEFAULT = "%s"; - String LABEL_ONLY = "%s"; - String LABEL_PARENS_ID = "%s (%s)"; - String ID_COLON_LABEL = "%s: %s"; - } - - - - public interface ValueFields - { - List DEFAULT = List.of("label"); - List LABEL_ONLY = List.of("label"); - List LABEL_PARENS_ID = List.of("label", "id"); - List ID_COLON_LABEL = List.of("id", "label"); - } - // todo - optimization hints, such as "table is static, fully cache" or "table is small, so we can pull the whole thing into memory?" ////////////////////// @@ -154,40 +134,6 @@ public class QPossibleValueSource - /******************************************************************************* - ** Getter for idType - ** - *******************************************************************************/ - public QFieldType getIdType() - { - return idType; - } - - - - /******************************************************************************* - ** Setter for idType - ** - *******************************************************************************/ - public void setIdType(QFieldType idType) - { - this.idType = idType; - } - - - - /******************************************************************************* - ** Fluent setter for idType - ** - *******************************************************************************/ - public QPossibleValueSource withIdType(QFieldType idType) - { - this.idType = idType; - return (this); - } - - - /******************************************************************************* ** Getter for valueFormat ** @@ -407,6 +353,9 @@ public class QPossibleValueSource /******************************************************************************* + ** This is the easiest way to add the values from an enum to a PossibleValueSource. + ** Make sure the enum implements PossibleValueEnum - then call as: + ** myPossibleValueSource.withValuesFromEnum(MyEnum.values())); ** *******************************************************************************/ public > QPossibleValueSource withValuesFromEnum(T[] values) @@ -453,4 +402,26 @@ public class QPossibleValueSource return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setValueFormatAndFields(PVSValueFormatAndFields valueFormatAndFields) + { + this.valueFormat = valueFormatAndFields.getFormat(); + this.valueFields = valueFormatAndFields.getFields(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QPossibleValueSource withValueFormatAndFields(PVSValueFormatAndFields valueFormatAndFields) + { + setValueFormatAndFields(valueFormatAndFields); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index ebb3e36a..0d1cbcef 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -30,6 +30,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField; @@ -85,6 +86,17 @@ public class QTableMetaData implements QAppChildMetaData, Serializable + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return ("QTableMetaData[" + name + "]"); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -451,6 +463,16 @@ public class QTableMetaData implements QAppChildMetaData, Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData withCustomizer(TableCustomizer tableCustomizer, QCodeReference customizer) + { + return (withCustomizer(tableCustomizer.getRole(), customizer)); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index f195edcf..90eedd28 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -174,10 +174,13 @@ public class MemoryRecordStore *******************************************************************************/ public Integer count(CountInput input) { - Map tableData = getTableData(input.getTable()); - List records = new ArrayList<>(tableData.values()); - // todo - filtering (call query) - return (records.size()); + QueryInput queryInput = new QueryInput(input.getInstance()); + queryInput.setSession(input.getSession()); + queryInput.setTableName(input.getTableName()); + queryInput.setFilter(input.getFilter()); + List queryResult = query(queryInput); + + return (queryResult.size()); } @@ -192,27 +195,43 @@ public class MemoryRecordStore return (new ArrayList<>()); } - QTableMetaData table = input.getTable(); - Map tableData = getTableData(table); - Integer nextSerial = nextSerials.get(table.getName()); + QTableMetaData table = input.getTable(); + Map tableData = getTableData(table); + + //////////////////////////////////////// + // grab the next unique serial to use // + //////////////////////////////////////// + Integer nextSerial = nextSerials.get(table.getName()); if(nextSerial == null) { nextSerial = 1; - while(tableData.containsKey(nextSerial)) - { - nextSerial++; - } + } + + while(tableData.containsKey(nextSerial)) + { + nextSerial++; } List outputRecords = new ArrayList<>(); QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); for(QRecord record : input.getRecords()) { + ///////////////////////////////////////////////// + // set the next serial in the record if needed // + ///////////////////////////////////////////////// if(record.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER)) { record.setValue(primaryKeyField.getName(), nextSerial++); } + /////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure that if the user supplied a serial, greater than the one we had, that we skip ahead // + /////////////////////////////////////////////////////////////////////////////////////////////////// + if(primaryKeyField.getType().equals(QFieldType.INTEGER) && record.getValueInteger(primaryKeyField.getName()) > nextSerial) + { + nextSerial = record.getValueInteger(primaryKeyField.getName()) + 1; + } + tableData.put(record.getValue(primaryKeyField.getName()), record); if(returnInsertedRecords) { @@ -220,6 +239,8 @@ public class MemoryRecordStore } } + nextSerials.put(table.getName(), nextSerial); + return (outputRecords); } @@ -256,10 +277,6 @@ public class MemoryRecordStore outputRecords.add(record); } } - else - { - outputRecords.add(record); - } } return (outputRecords); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index 9850719b..4d422746 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -423,7 +423,7 @@ public class ValueUtils /******************************************************************************* ** *******************************************************************************/ - public static Object getValueAsLocalTime(Serializable value) + public static LocalTime getValueAsLocalTime(Serializable value) { try { 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 bdb3b21b..a936e6b4 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 @@ -22,6 +22,8 @@ package com.kingsrook.qqq.backend.core.actions.tables; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; @@ -76,4 +78,50 @@ class QueryActionTest assertThat(record.getDisplayValues()).isNotEmpty(); } } + + + + /******************************************************************************* + ** Test running with a recordPipe - using the shape table, which uses the memory + ** backend, which is known to do an addAll to the query output. + ** + *******************************************************************************/ + @Test + public void testRecordPipeShapeTable() throws QException + { + TestUtils.insertDefaultShapes(TestUtils.defineInstance()); + + RecordPipe pipe = new RecordPipe(); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_SHAPE); + queryInput.setRecordPipe(pipe); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); + + List records = pipe.consumeAvailableRecords(); + assertThat(records).isNotEmpty(); + } + + + /******************************************************************************* + ** Test running with a recordPipe - using the person table, which uses the mock + ** backend, which is known to do a single-add (not addAll) to the query output. + ** + *******************************************************************************/ + @Test + public void testRecordPipePersonTable() throws QException + { + RecordPipe pipe = new RecordPipe(); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_PERSON); + queryInput.setRecordPipe(pipe); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); + + List records = pipe.consumeAvailableRecords(); + assertThat(records).isNotEmpty(); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java index 2d4dbbf6..fd79e473 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java @@ -25,30 +25,42 @@ package com.kingsrook.qqq.backend.core.actions.values; import java.math.BigDecimal; import java.util.Collections; import java.util.List; -import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; 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.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; 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.possiblevalues.PVSValueFormatAndFields; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.BeforeEach; 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 QPossibleValueTranslator *******************************************************************************/ public class QPossibleValueTranslatorTest { + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() + { + MemoryRecordStore.getInstance().reset(); + MemoryRecordStore.resetStatistics(); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -90,22 +102,20 @@ public class QPossibleValueTranslatorTest ///////////////////////////////////////////////////////////////// // assert the LABEL_ONLY format (when called out specifically) // ///////////////////////////////////////////////////////////////// - possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_ONLY); - possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_ONLY); + possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); assertEquals("IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); /////////////////////////////////////// // assert the LABEL_PARAMS_ID format // /////////////////////////////////////// - possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_PARENS_ID); - possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_PARENS_ID); + possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID); assertEquals("IL (1)", possibleValueTranslator.translatePossibleValue(stateField, 1)); ////////////////////////////////////// // assert the ID_COLON_LABEL format // ////////////////////////////////////// - possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.ID_COLON_LABEL); - possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.ID_COLON_LABEL); + possibleValueSource.setValueFormat(PVSValueFormatAndFields.ID_COLON_LABEL.getFormat()); + possibleValueSource.setValueFields(PVSValueFormatAndFields.ID_COLON_LABEL.getFields()); assertEquals("1: IL", possibleValueTranslator.translatePossibleValue(stateField, 1)); } @@ -123,16 +133,7 @@ public class QPossibleValueTranslatorTest QFieldMetaData shapeField = qInstance.getTable(TestUtils.TABLE_NAME_PERSON).getField("favoriteShapeId"); QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(shapeField.getPossibleValueSourceName()); - List shapeRecords = List.of( - new QRecord().withTableName(shapeTable.getName()).withValue("id", 1).withValue("name", "Triangle"), - new QRecord().withTableName(shapeTable.getName()).withValue("id", 2).withValue("name", "Square"), - new QRecord().withTableName(shapeTable.getName()).withValue("id", 3).withValue("name", "Circle")); - - InsertInput insertInput = new InsertInput(qInstance); - insertInput.setSession(new QSession()); - insertInput.setTableName(shapeTable.getName()); - insertInput.setRecords(shapeRecords); - new InsertAction().execute(insertInput); + TestUtils.insertDefaultShapes(qInstance); ////////////////////////////////////////////////////////////////////////// // assert the default formatting for a not-found value is a null string // @@ -156,8 +157,7 @@ public class QPossibleValueTranslatorTest /////////////////////////////////////// // assert the LABEL_PARAMS_ID format // /////////////////////////////////////// - possibleValueSource.setValueFormat(QPossibleValueSource.ValueFormat.LABEL_PARENS_ID); - possibleValueSource.setValueFields(QPossibleValueSource.ValueFields.LABEL_PARENS_ID); + possibleValueSource.setValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID); assertEquals("Circle (3)", possibleValueTranslator.translatePossibleValue(shapeField, 3)); /////////////////////////////////////////////////////////// @@ -195,19 +195,113 @@ public class QPossibleValueTranslatorTest + /******************************************************************************* + ** Make sure that if we have 2 different PVS's pointed at the same 1 table, + ** that we avoid re-doing queries, and that we actually get different (formatted) values. + *******************************************************************************/ + @Test + void testPossibleValueTableMultiplePvsForATable() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData shapeTable = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); + + //////////////////////////////////////////////////////////////////// + // define a second version of the Shape PVS, with a unique format // + //////////////////////////////////////////////////////////////////// + qInstance.addPossibleValueSource(new QPossibleValueSource() + .withName("shapeV2") + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TestUtils.TABLE_NAME_SHAPE) + .withValueFormat("%d: %s") + .withValueFields(List.of("id", "label")) + ); + + ////////////////////////////////////////////////////// + // use that PVS in a new column on the person table // + ////////////////////////////////////////////////////// + personTable.addField(new QFieldMetaData("currentShapeId", QFieldType.INTEGER) + .withPossibleValueSourceName("shapeV2") + ); + + TestUtils.insertDefaultShapes(qInstance); + + /////////////////////////////////////////////////////// + // define a list of persons pointing at those shapes // + /////////////////////////////////////////////////////// + List personRecords = List.of( + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1).withValue("currentShapeId", 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 1).withValue("currentShapeId", 3), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2).withValue("currentShapeId", 3), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue("favoriteShapeId", 2).withValue("currentShapeId", 3) + ); + + ///////////////////////// + // translate the PVS's // + ///////////////////////// + MemoryRecordStore.setCollectStatistics(true); + new QPossibleValueTranslator(qInstance, new QSession()).translatePossibleValuesInRecords(personTable, personRecords); + + ///////////////////////////////// + // assert only 1 query was ran // + ///////////////////////////////// + assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query"); + + //////////////////////////////////////// + // assert expected values and formats // + //////////////////////////////////////// + assertEquals("Triangle", personRecords.get(0).getDisplayValue("favoriteShapeId")); + assertEquals("2: Square", personRecords.get(0).getDisplayValue("currentShapeId")); + assertEquals("Triangle", personRecords.get(1).getDisplayValue("favoriteShapeId")); + assertEquals("3: Circle", personRecords.get(1).getDisplayValue("currentShapeId")); + assertEquals("Square", personRecords.get(2).getDisplayValue("favoriteShapeId")); + assertEquals("3: Circle", personRecords.get(2).getDisplayValue("currentShapeId")); + } + + + + /******************************************************************************* + ** Make sure that if we have 2 different PVS's pointed at the same 1 table, + ** that we avoid re-doing queries, and that we actually get different (formatted) values. + *******************************************************************************/ + @Test + void testCustomPossibleValue() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); + String fieldName = "customValue"; + + ////////////////////////////////////////////////////////////// + // define a list of persons with values in the custom field // + ////////////////////////////////////////////////////////////// + List personRecords = List.of( + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, 1), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, 2), + new QRecord().withTableName(TestUtils.TABLE_NAME_PERSON).withValue(fieldName, "Buckle my shoe") + ); + + ///////////////////////// + // translate the PVS's // + ///////////////////////// + new QPossibleValueTranslator(qInstance, new QSession()).translatePossibleValuesInRecords(personTable, personRecords); + + //////////////////////////////////////// + // assert expected values and formats // + //////////////////////////////////////// + assertEquals("Custom[1]", personRecords.get(0).getDisplayValue(fieldName)); + assertEquals("Custom[2]", personRecords.get(1).getDisplayValue(fieldName)); + assertEquals("Custom[Buckle my shoe]", personRecords.get(2).getDisplayValue(fieldName)); + } + + + /******************************************************************************* ** *******************************************************************************/ @Test void testSetDisplayValuesInRecords() { - QTableMetaData table = new QTableMetaData() - .withRecordLabelFormat("%s %s") - .withRecordLabelFields("firstName", "lastName") - .withField(new QFieldMetaData("firstName", QFieldType.STRING)) - .withField(new QFieldMetaData("lastName", QFieldType.STRING)) - .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) - .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(TestUtils.POSSIBLE_VALUE_SOURCE_STATE)); + QTableMetaData table = TestUtils.defineTablePerson(); ///////////////////////////////////////////////////////////////// // first, make sure it doesn't crash with null or empty inputs // diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java index a81e5b53..7dff6bad 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java @@ -292,6 +292,11 @@ class CsvToQRecordAdapterTest void testByteOrderMarker() { CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // note - there's a zero-width non-breaking-space character (0xFEFF or some-such) // + // at the start of this string!! You may not be able to see it, depending on where you view this file. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// List records = csvToQRecordAdapter.buildRecordsFromCsv(""" id,firstName 1,John""", TestUtils.defineTablePerson(), null); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java index 2404c915..c86e9fca 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java @@ -140,6 +140,7 @@ class QInstanceEnricherTest assertEquals("Field 20", QInstanceEnricher.nameToLabel("field20")); assertEquals("Something USA", QInstanceEnricher.nameToLabel("somethingUSA")); assertEquals("Number 1 Dad", QInstanceEnricher.nameToLabel("number1Dad")); + assertEquals("Number 417 Dad", QInstanceEnricher.nameToLabel("number417Dad")); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index d4c21bd8..ee2464a9 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -27,9 +27,14 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +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.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; 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.layout.QAppMetaData; @@ -114,12 +119,13 @@ class QInstanceValidatorTest @Test public void test_validateNullTables() { - assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> + assertValidationFailureReasons((qInstance) -> { qInstance.setTables(null); qInstance.setProcesses(null); }, - "At least 1 table must be defined"); + "At least 1 table must be defined", + "Unrecognized table shape for possibleValueSource shape"); } @@ -131,12 +137,13 @@ class QInstanceValidatorTest @Test public void test_validateEmptyTables() { - assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> + assertValidationFailureReasons((qInstance) -> { qInstance.setTables(new HashMap<>()); qInstance.setProcesses(new HashMap<>()); }, - "At least 1 table must be defined"); + "At least 1 table must be defined", + "Unrecognized table shape for possibleValueSource shape"); } @@ -191,7 +198,6 @@ class QInstanceValidatorTest - /******************************************************************************* ** *******************************************************************************/ @@ -265,6 +271,138 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableCustomizers() + { + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference()), + "missing a code reference name", "missing a code type"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(null, QCodeType.JAVA, null)), + "missing a code reference name"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("", QCodeType.JAVA, null)), + "missing a code reference name"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("Test", null, null)), + "missing a code type"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("Test", QCodeType.JAVA, QCodeUsage.CUSTOMIZER)), + "Class for CodeReference could not be found"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerWithNoVoidConstructor.class, QCodeUsage.CUSTOMIZER)), + "Instance of CodeReference could not be created"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerThatIsNotAFunction.class, QCodeUsage.CUSTOMIZER)), + "CodeReference could not be casted"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameters.class, QCodeUsage.CUSTOMIZER)), + "Error validating customizer type parameters"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameter1.class, QCodeUsage.CUSTOMIZER)), + "Error validating customizer type parameters"); + + assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerFunctionWithIncorrectTypeParameter2.class, QCodeUsage.CUSTOMIZER)), + "Error validating customizer type parameters"); + + assertValidationSuccess((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerValid.class, QCodeUsage.CUSTOMIZER))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerWithNoVoidConstructor + { + public CustomizerWithNoVoidConstructor(boolean b) + { + + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerWithOnlyPrivateConstructor + { + private CustomizerWithOnlyPrivateConstructor() + { + + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerThatIsNotAFunction + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerFunctionWithIncorrectTypeParameters implements Function + { + @Override + public String apply(String s) + { + return null; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerFunctionWithIncorrectTypeParameter1 implements Function + { + @Override + public QRecord apply(String s) + { + return null; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerFunctionWithIncorrectTypeParameter2 implements Function + { + @Override + public String apply(QRecord s) + { + return "Test"; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CustomizerValid implements Function + { + @Override + public QRecord apply(QRecord record) + { + return null; + } + } + + + /******************************************************************************* ** Test that if a field specifies a backend that doesn't exist, that it fails. ** @@ -443,18 +581,6 @@ class QInstanceValidatorTest - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testPossibleValueSourceMissingIdType() - { - assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_STATE).setIdType(null), - "Missing an idType for possibleValueSource"); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -516,7 +642,9 @@ class QInstanceValidatorTest "is missing a customCodeReference"); assertValidationFailureReasons((qInstance) -> qInstance.getPossibleValueSource(TestUtils.POSSIBLE_VALUE_SOURCE_CUSTOM).setCustomCodeReference(new QCodeReference()), - "not a possibleValueProvider"); + "not a possibleValueProvider", + "missing a code reference name", + "missing a code type"); } @@ -561,7 +689,8 @@ class QInstanceValidatorTest { if(!allowExtraReasons) { - assertEquals(reasons.length, e.getReasons().size(), "Expected number of validation failure reasons\nExpected: " + String.join(",", reasons) + "\nActual: " + e.getReasons()); + int noOfReasons = e.getReasons() == null ? 0 : e.getReasons().size(); + assertEquals(reasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", reasons) + "\nActual reasons: " + e.getReasons()); } for(String reason : reasons) @@ -573,6 +702,25 @@ class QInstanceValidatorTest + /******************************************************************************* + ** Assert that an instance is valid! + *******************************************************************************/ + private void assertValidationSuccess(Consumer setup) + { + try + { + QInstance qInstance = TestUtils.defineInstance(); + setup.accept(qInstance); + new QInstanceValidator().validate(qInstance); + } + catch(QInstanceValidationException e) + { + fail("Expected no validation errors, but received: " + e.getMessage()); + } + } + + + /******************************************************************************* ** utility method for asserting that a specific reason string is found within ** the list of reasons in the QInstanceValidationException. diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java index 97ab5288..9bc5081f 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java @@ -24,7 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory; import java.util.List; import java.util.function.Function; -import com.kingsrook.qqq.backend.core.actions.customizers.Customizers; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -36,6 +36,9 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +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.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; @@ -68,6 +71,7 @@ class MemoryBackendModuleTest void beforeAndAfter() { MemoryRecordStore.getInstance().reset(); + MemoryRecordStore.resetStatistics(); } @@ -122,8 +126,6 @@ class MemoryBackendModuleTest assertEquals(3, new CountAction().execute(countInput).getCount()); - // todo - filters in query - ////////////////// // do an update // ////////////////// @@ -152,6 +154,24 @@ class MemoryBackendModuleTest assertEquals(3, new CountAction().execute(countInput).getCount()); + ///////////////////////// + // do a filtered query // + ///////////////////////// + queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(table.getName()); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.IN, List.of(1, 3)))); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size()); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + + ///////////////////////// + // do a filtered count // + ///////////////////////// + countInput.setFilter(queryInput.getFilter()); + assertEquals(2, new CountAction().execute(countInput).getCount()); + ///////////////// // do a delete // ///////////////// @@ -173,6 +193,57 @@ class MemoryBackendModuleTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSerials() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE); + QSession session = new QSession(); + + ////////////////// + // do an insert // + ////////////////// + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(session); + insertInput.setTableName(table.getName()); + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 1"))); + new InsertAction().execute(insertInput); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 2"))); + new InsertAction().execute(insertInput); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 3"))); + new InsertAction().execute(insertInput); + + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(new QSession()); + queryInput.setTableName(table.getName()); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2))); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3))); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("id", 4).withValue("name", "Shape 4"))); + new InsertAction().execute(insertInput); + queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(4))); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("id", 6).withValue("name", "Shape 6"))); + new InsertAction().execute(insertInput); + queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(6))); + + insertInput.setRecords(List.of(new QRecord().withTableName(table.getName()).withValue("name", "Shape 7"))); + new InsertAction().execute(insertInput); + queryOutput = new QueryAction().execute(queryInput); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(7))); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -215,7 +286,7 @@ class MemoryBackendModuleTest /////////////////////////////////// // add a customizer to the table // /////////////////////////////////// - table.withCustomizer(Customizers.POST_QUERY_RECORD, new QCodeReference(ShapeTestCustomizer.class, QCodeUsage.CUSTOMIZER)); + table.withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(ShapeTestCustomizer.class, QCodeUsage.CUSTOMIZER)); ////////////////// // do an insert // diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 6b12b83b..2fba3749 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -27,9 +27,11 @@ import java.util.List; import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarChart; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; +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.values.QCustomPossibleValueProvider; 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.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -464,6 +466,25 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + public static void insertDefaultShapes(QInstance qInstance) throws QException + { + List shapeRecords = List.of( + new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 1).withValue("name", "Triangle"), + new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 2).withValue("name", "Square"), + new QRecord().withTableName(TABLE_NAME_SHAPE).withValue("id", 3).withValue("name", "Circle")); + + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(new QSession()); + insertInput.setTableName(TABLE_NAME_SHAPE); + insertInput.setRecords(shapeRecords); + new InsertAction().execute(insertInput); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java index 61730733..3da02b62 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java @@ -206,12 +206,10 @@ public abstract class AbstractBaseFilesystemAction { new CsvToQRecordAdapter().buildRecordsFromCsv(queryInput.getRecordPipe(), fileContents, table, null, (record -> { - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // since the CSV adapter is the one responsible for putting records into the pipe (rather than the queryOutput), // - // we must do some of QueryOutput's normal job here - and run the runPostQueryRecordCustomizer // - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////// + // Before the records go into the pipe, make sure their backend details are added to them // + //////////////////////////////////////////////////////////////////////////////////////////// addBackendDetailsToRecord(record, file); - queryOutput.runPostQueryRecordCustomizer(record); })); } else @@ -308,7 +306,7 @@ public abstract class AbstractBaseFilesystemAction *******************************************************************************/ private String customizeFileContentsAfterReading(QTableMetaData table, String fileContents) throws QException { - Optional optionalCustomizer = table.getCustomizer(FilesystemCustomizers.POST_READ_FILE); + Optional optionalCustomizer = table.getCustomizer(FilesystemTableCustomizers.POST_READ_FILE.getRole()); if(optionalCustomizer.isEmpty()) { return (fileContents); diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java deleted file mode 100644 index 8e2416e0..00000000 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemCustomizers.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2022. 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.module.filesystem.base.actions; - - -import com.kingsrook.qqq.backend.core.actions.customizers.Customizers; - - -/******************************************************************************* - ** Standard place where the names of QQQ Customization points for filesystem-based - ** backends are defined. - *******************************************************************************/ -public interface FilesystemCustomizers extends Customizers -{ - String POST_READ_FILE = "postReadFile"; -} diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemTableCustomizers.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemTableCustomizers.java new file mode 100644 index 00000000..06157ceb --- /dev/null +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/FilesystemTableCustomizers.java @@ -0,0 +1,72 @@ +package com.kingsrook.qqq.backend.module.filesystem.base.actions; + + +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum FilesystemTableCustomizers +{ + POST_READ_FILE(new TableCustomizer("postReadFile", Function.class, ((Object x) -> + { + Function function = (Function) x; + String output = function.apply(new String()); + }))); + + private final TableCustomizer tableCustomizer; + + + + /******************************************************************************* + ** + *******************************************************************************/ + FilesystemTableCustomizers(TableCustomizer tableCustomizer) + { + this.tableCustomizer = tableCustomizer; + } + + + + /******************************************************************************* + ** Get the FilesystemTableCustomer for a given role (e.g., the role used in meta-data, not + ** the enum-constant name). + *******************************************************************************/ + public static FilesystemTableCustomizers forRole(String name) + { + for(FilesystemTableCustomizers value : values()) + { + if(value.tableCustomizer.getRole().equals(name)) + { + return (value); + } + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for tableCustomizer + ** + *******************************************************************************/ + public TableCustomizer getTableCustomizer() + { + return tableCustomizer; + } + + + + /******************************************************************************* + ** get the role from the tableCustomizer + ** + *******************************************************************************/ + public String getRole() + { + return (tableCustomizer.getRole()); + } + +} diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java index 8464bb22..1ea36990 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java @@ -91,7 +91,7 @@ public class FilesystemSyncStep implements BackendStep String sourceFileName = sourceEntry.getKey(); if(!archiveFiles.contains(sourceFileName)) { - LOG.info("Syncing file [" + sourceFileName + "] to [" + archiveTable.getName() + "] and [" + processingTable.getName() + "]"); + LOG.info("Syncing file [" + sourceFileName + "] to [" + archiveTable + "] and [" + processingTable + "]"); InputStream inputStream = sourceActionBase.readFile(sourceEntry.getValue()); byte[] bytes = inputStream.readAllBytes(); diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java deleted file mode 100644 index e9bea180..00000000 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/FilesystemModuleJunitExtension.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2022. 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.module.filesystem; - - -/******************************************************************************* - ** - *******************************************************************************/ -public class FilesystemModuleJunitExtension // implements Extension, BeforeAllCallback, AfterAllCallback -{ -} diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java index 2c488900..46cfabe2 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemCountActionTest.java @@ -22,22 +22,16 @@ package com.kingsrook.qqq.backend.module.filesystem.local.actions; -import java.util.function.Function; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.module.filesystem.TestUtils; -import com.kingsrook.qqq.backend.module.filesystem.base.actions.FilesystemCustomizers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; /******************************************************************************* - ** + ** Unit test for FilesystemCountAction *******************************************************************************/ public class FilesystemCountActionTest extends FilesystemActionTest { @@ -55,35 +49,4 @@ public class FilesystemCountActionTest extends FilesystemActionTest Assertions.assertEquals(3, countOutput.getCount(), "Unfiltered count should find all rows"); } - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - public void testCountWithFileCustomizer() throws QException - { - CountInput countInput = new CountInput(); - QInstance instance = TestUtils.defineInstance(); - - QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_LOCAL_FS_JSON); - table.withCustomizer(FilesystemCustomizers.POST_READ_FILE, new QCodeReference(ValueUpshifter.class, QCodeUsage.CUSTOMIZER)); - - countInput.setInstance(instance); - countInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName()); - CountOutput countOutput = new FilesystemCountAction().execute(countInput); - Assertions.assertEquals(3, countOutput.getCount(), "Unfiltered count should find all rows"); - } - - - - public static class ValueUpshifter implements Function - { - @Override - public String apply(String s) - { - return (s.replaceAll("kingsrook.com", "KINGSROOK.COM")); - } - } - } \ No newline at end of file diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java index be40e86a..f6646085 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemQueryActionTest.java @@ -32,7 +32,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.module.filesystem.TestUtils; import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields; -import com.kingsrook.qqq.backend.module.filesystem.base.actions.FilesystemCustomizers; +import com.kingsrook.qqq.backend.module.filesystem.base.actions.FilesystemTableCustomizers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -71,7 +71,7 @@ public class FilesystemQueryActionTest extends FilesystemActionTest QInstance instance = TestUtils.defineInstance(); QTableMetaData table = instance.getTable(TestUtils.TABLE_NAME_PERSON_LOCAL_FS_JSON); - table.withCustomizer(FilesystemCustomizers.POST_READ_FILE, new QCodeReference(ValueUpshifter.class, QCodeUsage.CUSTOMIZER)); + table.withCustomizer(FilesystemTableCustomizers.POST_READ_FILE.getRole(), new QCodeReference(ValueUpshifter.class, QCodeUsage.CUSTOMIZER)); queryInput.setInstance(instance); queryInput.setTableName(TestUtils.defineLocalFilesystemJSONPersonTable().getName()); diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java index 18b38403..ce9b3ae5 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java @@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; import org.apache.commons.io.IOUtils; -import static junit.framework.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index a780c379..dad72655 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -198,6 +198,12 @@ public class QJavalinImplementation try { QInstance newQInstance = qInstanceHotSwapSupplier.get(); + if(newQInstance == null) + { + LOG.warn("Got a null qInstance from hotSwapSupplier. Not hot-swapping."); + return; + } + new QInstanceValidator().validate(newQInstance); QJavalinImplementation.qInstance = newQInstance; LOG.info("Swapped qInstance"); diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index 831ecd25..04cbb11c 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -51,7 +51,7 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.apache.commons.io.IOUtils; -import static junit.framework.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* diff --git a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java index 434b9d39..fb2aced0 100644 --- a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java +++ b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java @@ -44,7 +44,7 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.apache.commons.io.IOUtils; -import static junit.framework.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java index d524c6cd..78884a30 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java @@ -22,7 +22,6 @@ package com.kingsrook.sampleapp; -import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.frontend.picocli.QPicoCliImplementation; @@ -37,7 +36,8 @@ public class SampleCli *******************************************************************************/ public static void main(String[] args) { - new SampleCli().run(args); + int exitCode = new SampleCli().run(args); + System.exit(exitCode); } @@ -45,31 +45,20 @@ public class SampleCli /******************************************************************************* ** *******************************************************************************/ - private void run(String[] args) + int run(String[] args) { try { - int exitCode = runForExitCode(args); - System.exit(exitCode); + QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); + + return (qPicoCliImplementation.runCli("my-sample-cli", args)); } catch(Exception e) { e.printStackTrace(); - System.exit(-1); + return (-1); } } - - - /******************************************************************************* - ** - *******************************************************************************/ - int runForExitCode(String[] args) throws QException - { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); - QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); - int exitCode = qPicoCliImplementation.runCli("my-sample-cli", args); - return exitCode; - } - } diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java index aa5b049b..e88ec6c5 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java @@ -22,7 +22,6 @@ package com.kingsrook.sampleapp; -import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import io.javalin.Javalin; @@ -71,6 +70,24 @@ public class SampleJavalinServer config.enableCorsForAllOrigins(); }).start(PORT); javalinService.routes(qJavalinImplementation.getRoutes()); + + ///////////////////////////////////////////////////////////////// + // set the server to hot-swap the q instance before all routes // + ///////////////////////////////////////////////////////////////// + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> + { + try + { + return (SampleMetaDataProvider.defineInstance()); + } + catch(Exception e) + { + LOG.warn("Error hot-swapping meta data", e); + return (null); + } + }); + javalinService.before(QJavalinImplementation::hotSwapQInstance); + javalinService.after(ctx -> ctx.res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000")); } diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java index ab4546cc..be51bead 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java @@ -25,6 +25,7 @@ package com.kingsrook.sampleapp; import com.kingsrook.qqq.backend.core.exceptions.QException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; /******************************************************************************* @@ -37,10 +38,21 @@ class SampleCliTest ** *******************************************************************************/ @Test - void test() throws QException + void testExitSuccess() throws QException { - int exitCode = new SampleCli().runForExitCode(new String[] { "--meta-data" }); + int exitCode = new SampleCli().run(new String[] { "--meta-data" }); assertEquals(0, exitCode); } + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNotExitSuccess() throws QException + { + int exitCode = new SampleCli().run(new String[] { "asdfasdf" }); + assertNotEquals(0, exitCode); + } + } \ No newline at end of file diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java index 6192bb38..08b5c6ea 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java @@ -43,7 +43,6 @@ import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemQuery import com.kingsrook.qqq.backend.module.filesystem.local.model.metadata.FilesystemTableBackendDetails; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; -import junit.framework.Assert; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Assertions; @@ -84,7 +83,7 @@ class SampleMetaDataProviderTest try(Connection connection = connectionManager.getConnection(SampleMetaDataProvider.defineRdbmsBackend())) { InputStream primeTestDatabaseSqlStream = SampleMetaDataProviderTest.class.getResourceAsStream("/" + sqlFileName); - Assert.assertNotNull(primeTestDatabaseSqlStream); + assertNotNull(primeTestDatabaseSqlStream); List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList(); String joinedSQL = String.join("\n", lines);