diff --git a/qqq-backend-core/pom.xml b/qqq-backend-core/pom.xml index 9d0c44e7..17fdf949 100644 --- a/qqq-backend-core/pom.xml +++ b/qqq-backend-core/pom.xml @@ -65,7 +65,11 @@ aws-java-sdk-secretsmanager 1.12.385 - + + com.ibm.icu + icu4j + 77.1 + com.fasterxml.jackson.core jackson-databind @@ -157,16 +161,21 @@ 2.3 - + org.jsoup jsoup 1.15.3 - org.xhtmlrenderer - flying-saucer-pdf-openpdf - 9.1.22 + com.openhtmltopdf + openhtmltopdf-core + 1.0.10 + + + com.openhtmltopdf + openhtmltopdf-pdfbox + 1.0.10 diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunRecordScriptAutomationHandler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunRecordScriptAutomationHandler.java index 1b4d997d..2f77c623 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunRecordScriptAutomationHandler.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RunRecordScriptAutomationHandler.java @@ -83,7 +83,7 @@ public class RunRecordScriptAutomationHandler extends RecordAutomationHandler } QRecord scriptRevision = queryOutput.getRecords().get(0); - LOG.info("Running script against records", logPair("scriptRevisionId", scriptRevision.getValue("id")), logPair("scriptId", scriptRevision.getValue("scriptIdd"))); + LOG.debug("Running script against records", logPair("scriptRevisionId", scriptRevision.getValue("id")), logPair("scriptId", scriptRevision.getValue("scriptIdd"))); RunAdHocRecordScriptInput input = new RunAdHocRecordScriptInput(); input.setCodeReference(new AdHocScriptCodeReference().withScriptRevisionRecord(scriptRevision)); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java index ba1118a4..5351b53a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java @@ -649,7 +649,7 @@ public class PollingAutomationPerTableRunner implements Runnable input.setRecordList(records); input.setAction(action); - RecordAutomationHandler recordAutomationHandler = QCodeLoader.getRecordAutomationHandler(action); + RecordAutomationHandler recordAutomationHandler = QCodeLoader.getAdHoc(RecordAutomationHandler.class, action.getCodeReference()); recordAutomationHandler.execute(input); } } 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 b4d46ca7..82466d65 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,17 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.customizers; import java.lang.reflect.Constructor; import java.util.Optional; -import java.util.function.Function; -import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; -import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; -import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; -import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference; 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 com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction; import com.kingsrook.qqq.backend.core.utils.memoization.Memoization; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -43,9 +37,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* ** Utility to load code for running QQQ customizers. ** - ** TODO - redo all to go through method that memoizes class & constructor - ** lookup. That memoziation causes 1,000,000 such calls to go from ~500ms - ** to ~100ms. + ** That memoization causes 1,000,000 such calls to go from ~500ms to ~100ms. *******************************************************************************/ public class QCodeLoader { @@ -70,84 +62,6 @@ public class QCodeLoader - /******************************************************************************* - ** - *******************************************************************************/ - @SuppressWarnings("unchecked") - public static Function getFunction(QCodeReference codeReference) - { - if(codeReference == null) - { - return (null); - } - - if(!codeReference.getCodeType().equals(QCodeType.JAVA)) - { - /////////////////////////////////////////////////////////////////////////////////////// - // todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! // - /////////////////////////////////////////////////////////////////////////////////////// - throw (new IllegalArgumentException("Only JAVA customizers are supported at this time.")); - } - - try - { - Class customizerClass = Class.forName(codeReference.getName()); - return ((Function) customizerClass.getConstructor().newInstance()); - } - catch(Exception e) - { - LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference)); - - ////////////////////////////////////////////////////////////////////////////////////////////////////////// - // return null here - under the assumption that during normal run-time operations, we'll never hit here // - // as we'll want to validate all functions in the instance validator at startup time (and IT will throw // - // if it finds an invalid code reference // - ////////////////////////////////////////////////////////////////////////////////////////////////////////// - return (null); - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @SuppressWarnings("unchecked") - public static T getBackendStep(Class expectedType, QCodeReference codeReference) - { - if(codeReference == null) - { - return (null); - } - - if(!codeReference.getCodeType().equals(QCodeType.JAVA)) - { - /////////////////////////////////////////////////////////////////////////////////////// - // todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! // - /////////////////////////////////////////////////////////////////////////////////////// - throw (new IllegalArgumentException("Only JAVA BackendSteps are supported at this time.")); - } - - try - { - Class customizerClass = Class.forName(codeReference.getName()); - return ((T) customizerClass.getConstructor().newInstance()); - } - catch(Exception e) - { - LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference)); - - ////////////////////////////////////////////////////////////////////////////////////////////////////////// - // return null here - under the assumption that during normal run-time operations, we'll never hit here // - // as we'll want to validate all functions in the instance validator at startup time (and IT will throw // - // if it finds an invalid code reference // - ////////////////////////////////////////////////////////////////////////////////////////////////////////// - return (null); - } - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -177,7 +91,17 @@ public class QCodeLoader if(constructor.isPresent()) { - return ((T) constructor.get().newInstance()); + T t = (T) constructor.get().newInstance(); + + //////////////////////////////////////////////////////////////// + // if the object is initializable, then, well, initialize it! // + //////////////////////////////////////////////////////////////// + if(t instanceof InitializableViaCodeReference initializableViaCodeReference) + { + initializableViaCodeReference.initialize(codeReference); + } + + return t; } else { @@ -187,7 +111,7 @@ public class QCodeLoader } catch(Exception e) { - LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference)); + LOG.error("Error initializing codeReference", e, logPair("codeReference", codeReference)); ////////////////////////////////////////////////////////////////////////////////////////////////////////// // return null here - under the assumption that during normal run-time operations, we'll never hit here // @@ -198,67 +122,4 @@ public class QCodeLoader } } - - - /******************************************************************************* - ** - *******************************************************************************/ - public static RecordAutomationHandler getRecordAutomationHandler(TableAutomationAction action) throws QException - { - try - { - QCodeReference codeReference = action.getCodeReference(); - if(!codeReference.getCodeType().equals(QCodeType.JAVA)) - { - /////////////////////////////////////////////////////////////////////////////////////// - // todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! // - /////////////////////////////////////////////////////////////////////////////////////// - throw (new IllegalArgumentException("Only JAVA customizers are supported at this time.")); - } - - Class codeClass = Class.forName(codeReference.getName()); - Object codeObject = codeClass.getConstructor().newInstance(); - if(!(codeObject instanceof RecordAutomationHandler recordAutomationHandler)) - { - throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of RecordAutomationHandler")); - } - return (recordAutomationHandler); - } - catch(QException qe) - { - throw (qe); - } - catch(Exception e) - { - throw (new QException("Error getting record automation handler for action [" + action.getName() + "]", e)); - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - 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/dashboard/AbstractHTMLWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java index 2563079f..942e3ceb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java @@ -290,7 +290,18 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer /******************************************************************************* ** *******************************************************************************/ + @Deprecated(since = "call one that doesn't take input param") public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException + { + return linkRecordEdit(tableName, recordId); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String linkRecordEdit(String tableName, Serializable recordId) throws QException { String tablePath = QContext.getQInstance().getTablePath(tableName); return (tablePath + "/" + recordId + "/edit"); @@ -317,7 +328,17 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer /******************************************************************************* ** *******************************************************************************/ + @Deprecated(since = "call one that doesn't take input param") public static String linkProcessForFilter(AbstractActionInput input, String processName, QQueryFilter filter) throws QException + { + return linkProcessForFilter(processName, filter); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String linkProcessForFilter(String processName, QQueryFilter filter) throws QException { QProcessMetaData process = QContext.getQInstance().getProcess(processName); if(process == null) @@ -337,10 +358,21 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer + /******************************************************************************* ** *******************************************************************************/ + @Deprecated(since = "call one that doesn't take input param") public static String linkProcessForRecord(AbstractActionInput input, String processName, Serializable recordId) throws QException + { + return linkProcessForRecord(processName, recordId); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String linkProcessForRecord(String processName, Serializable recordId) throws QException { QProcessMetaData process = QContext.getQInstance().getProcess(processName); String tableName = process.getTableName(); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/AllowAllMetaDataFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/AllowAllMetaDataFilter.java index c64c8954..de94474e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/AllowAllMetaDataFilter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/AllowAllMetaDataFilter.java @@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; /******************************************************************************* ** a default implementation of MetaDataFilterInterface, that allows all the things *******************************************************************************/ +@Deprecated(since = "migrated to metaDataCustomizer") public class AllowAllMetaDataFilter implements MetaDataFilterInterface { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/DefaultNoopMetaDataActionCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/DefaultNoopMetaDataActionCustomizer.java new file mode 100644 index 00000000..f7d88faf --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/DefaultNoopMetaDataActionCustomizer.java @@ -0,0 +1,92 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.metadata; + + +import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** a default implementation of MetaDataFilterInterface, that is all noop. + *******************************************************************************/ +public class DefaultNoopMetaDataActionCustomizer implements MetaDataActionCustomizerInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowTable(MetaDataInput input, QTableMetaData table) + { + return (true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowProcess(MetaDataInput input, QProcessMetaData process) + { + return (true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowReport(MetaDataInput input, QReportMetaData report) + { + return (true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowApp(MetaDataInput input, QAppMetaData app) + { + return (true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget) + { + return (true); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java index 56c76928..8ecf4dcf 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java @@ -23,10 +23,12 @@ package com.kingsrook.qqq.backend.core.actions.metadata; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult; @@ -34,6 +36,7 @@ import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; @@ -65,7 +68,7 @@ public class MetaDataAction { private static final QLogger LOG = QLogger.getLogger(MetaDataAction.class); - private static Memoization metaDataFilterMemoization = new Memoization<>(); + private static Memoization metaDataActionCustomizerMemoization = new Memoization<>(); @@ -79,7 +82,7 @@ public class MetaDataAction MetaDataOutput metaDataOutput = new MetaDataOutput(); Map treeNodes = new LinkedHashMap<>(); - MetaDataFilterInterface filter = getMetaDataFilter(); + MetaDataActionCustomizerInterface customizer = getMetaDataActionCustomizer(); ///////////////////////////////////// // map tables to frontend metadata // @@ -90,7 +93,7 @@ public class MetaDataAction String tableName = entry.getKey(); QTableMetaData table = entry.getValue(); - if(!filter.allowTable(metaDataInput, table)) + if(!customizer.allowTable(metaDataInput, table)) { continue; } @@ -119,7 +122,7 @@ public class MetaDataAction String processName = entry.getKey(); QProcessMetaData process = entry.getValue(); - if(!filter.allowProcess(metaDataInput, process)) + if(!customizer.allowProcess(metaDataInput, process)) { continue; } @@ -144,7 +147,7 @@ public class MetaDataAction String reportName = entry.getKey(); QReportMetaData report = entry.getValue(); - if(!filter.allowReport(metaDataInput, report)) + if(!customizer.allowReport(metaDataInput, report)) { continue; } @@ -169,7 +172,7 @@ public class MetaDataAction String widgetName = entry.getKey(); QWidgetMetaDataInterface widget = entry.getValue(); - if(!filter.allowWidget(metaDataInput, widget)) + if(!customizer.allowWidget(metaDataInput, widget)) { continue; } @@ -206,7 +209,7 @@ public class MetaDataAction continue; } - if(!filter.allowApp(metaDataInput, app)) + if(!customizer.allowApp(metaDataInput, app)) { continue; } @@ -292,11 +295,22 @@ public class MetaDataAction metaDataOutput.setBranding(QContext.getQInstance().getBranding()); } - metaDataOutput.setEnvironmentValues(QContext.getQInstance().getEnvironmentValues()); + metaDataOutput.setEnvironmentValues(Objects.requireNonNullElse(QContext.getQInstance().getEnvironmentValues(), Collections.emptyMap())); - metaDataOutput.setHelpContents(QContext.getQInstance().getHelpContent()); + metaDataOutput.setHelpContents(Objects.requireNonNullElse(QContext.getQInstance().getHelpContent(), Collections.emptyMap())); - // todo post-customization - can do whatever w/ the result if you want? + try + { + customizer.postProcess(metaDataOutput); + } + catch(QUserFacingException e) + { + LOG.debug("User-facing exception thrown in meta-data customizer post-processing", e); + } + catch(Exception e) + { + LOG.warn("Unexpected error thrown in meta-data customizer post-processing", e); + } return metaDataOutput; } @@ -306,26 +320,36 @@ public class MetaDataAction /*************************************************************************** ** ***************************************************************************/ - private MetaDataFilterInterface getMetaDataFilter() + private MetaDataActionCustomizerInterface getMetaDataActionCustomizer() { - return metaDataFilterMemoization.getResult(QContext.getQInstance(), i -> + return metaDataActionCustomizerMemoization.getResult(QContext.getQInstance(), i -> { - MetaDataFilterInterface filter = null; - QCodeReference metaDataFilterReference = QContext.getQInstance().getMetaDataFilter(); - if(metaDataFilterReference != null) + MetaDataActionCustomizerInterface actionCustomizer = null; + QCodeReference metaDataActionCustomizerReference = QContext.getQInstance().getMetaDataActionCustomizer(); + if(metaDataActionCustomizerReference != null) { - filter = QCodeLoader.getAdHoc(MetaDataFilterInterface.class, metaDataFilterReference); - LOG.debug("Using new meta-data filter of type: " + filter.getClass().getSimpleName()); + actionCustomizer = QCodeLoader.getAdHoc(MetaDataActionCustomizerInterface.class, metaDataActionCustomizerReference); + LOG.debug("Using new meta-data actionCustomizer of type: " + actionCustomizer.getClass().getSimpleName()); } - if(filter == null) + if(actionCustomizer == null) { - filter = new AllowAllMetaDataFilter(); - LOG.debug("Using new default (allow-all) meta-data filter"); + QCodeReference metaDataFilterReference = QContext.getQInstance().getMetaDataFilter(); + if(metaDataFilterReference != null) + { + actionCustomizer = QCodeLoader.getAdHoc(MetaDataActionCustomizerInterface.class, metaDataFilterReference); + LOG.debug("Using new meta-data actionCustomizer (via metaDataFilter reference) of type: " + actionCustomizer.getClass().getSimpleName()); + } } - return (filter); - }).orElseThrow(() -> new QRuntimeException("Error getting metaDataFilter")); + if(actionCustomizer == null) + { + actionCustomizer = new DefaultNoopMetaDataActionCustomizer(); + LOG.debug("Using new default (allow-all) meta-data actionCustomizer"); + } + + return (actionCustomizer); + }).orElseThrow(() -> new QRuntimeException("Error getting MetaDataActionCustomizer")); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionCustomizerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionCustomizerInterface.java new file mode 100644 index 00000000..fb2c108b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionCustomizerInterface.java @@ -0,0 +1,78 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.metadata; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; +import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** Interface for customizations that can be injected by an application into + ** the MetaDataAction - e.g., loading applicable meta-data for a user into a + ** frontend. + *******************************************************************************/ +public interface MetaDataActionCustomizerInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + boolean allowTable(MetaDataInput input, QTableMetaData table); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean allowProcess(MetaDataInput input, QProcessMetaData process); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean allowReport(MetaDataInput input, QReportMetaData report); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean allowApp(MetaDataInput input, QAppMetaData app); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget); + + /*************************************************************************** + ** + ***************************************************************************/ + default void postProcess(MetaDataOutput metaDataOutput) throws QException + { + ///////////////////// + // noop by default // + ///////////////////// + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataFilterInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataFilterInterface.java index a7abb74d..a543764d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataFilterInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataFilterInterface.java @@ -22,43 +22,11 @@ package com.kingsrook.qqq.backend.core.actions.metadata; -import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; -import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; - - /******************************************************************************* ** *******************************************************************************/ -public interface MetaDataFilterInterface +@Deprecated(since = "migrated to metaDataCustomizer") +public interface MetaDataFilterInterface extends MetaDataActionCustomizerInterface { - /*************************************************************************** - ** - ***************************************************************************/ - boolean allowTable(MetaDataInput input, QTableMetaData table); - - /*************************************************************************** - ** - ***************************************************************************/ - boolean allowProcess(MetaDataInput input, QProcessMetaData process); - - /*************************************************************************** - ** - ***************************************************************************/ - boolean allowReport(MetaDataInput input, QReportMetaData report); - - /*************************************************************************** - ** - ***************************************************************************/ - boolean allowApp(MetaDataInput input, QAppMetaData app); - - /*************************************************************************** - ** - ***************************************************************************/ - boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget); - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java index 4e2f7e02..eebfe4e0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java @@ -54,6 +54,14 @@ public interface ExportStreamerInterface // noop in base class } + /*************************************************************************** + ** + ***************************************************************************/ + default void setExportStyleCustomizer(ExportStyleCustomizerInterface exportStyleCustomizer) + { + // noop in base class + } + /******************************************************************************* ** Called once per sheet, before any rows are available. Meant to write a ** header, for example. diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStyleCustomizerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStyleCustomizerInterface.java new file mode 100644 index 00000000..23d15ef3 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStyleCustomizerInterface.java @@ -0,0 +1,35 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.reporting; + + +/******************************************************************************* + ** interface for classes that can be used to customize visual style aspects of + ** exports/reports. + ** + ** Anticipates very different sub-interfaces based on the file type being generated, + ** and the capabilities of each. e.g., excel (bolds, fonts, cell merging) vs + ** json (different structure of objects). + *******************************************************************************/ +public interface ExportStyleCustomizerInterface +{ +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index b1e28903..f032b609 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -163,6 +163,17 @@ public class GenerateReportAction extends AbstractQActionFunction viewCustomizerFunction = QCodeLoader.getFunction(dataSourceTableView.getViewCustomizer()); + @SuppressWarnings("unchecked") + Function viewCustomizerFunction = QCodeLoader.getAdHoc(Function.class, dataSourceTableView.getViewCustomizer()); if(viewCustomizerFunction instanceof ReportViewCustomizer reportViewCustomizer) { reportViewCustomizer.setReportInput(reportInput); @@ -660,7 +672,7 @@ public class GenerateReportAction extends AbstractQActionFunction excelCellFormats; - private Map styles = new HashMap<>(); + private Map styles = new HashMap<>(); private int rowNo = 0; private int sheetIndex = 1; @@ -402,6 +405,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter dateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT)); styles.put("datetime", dateTimeStyle); + PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface(); styles.put("title", poiExcelStylerInterface.createStyleForTitle(workbook, createHelper)); styles.put("header", poiExcelStylerInterface.createStyleForHeader(workbook, createHelper)); styles.put("footer", poiExcelStylerInterface.createStyleForFooter(workbook, createHelper)); @@ -413,6 +417,11 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter XSSFCellStyle footerDateTimeStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper); footerDateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT)); styles.put("footer-datetime", footerDateTimeStyle); + + if(styleCustomizerInterface != null) + { + styleCustomizerInterface.customizeStyles(styles, workbook, createHelper); + } } @@ -458,7 +467,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter } else { - sheetWriter.beginSheet(); + sheetWriter.beginSheet(view, styleCustomizerInterface); //////////////////////////////////////////////// // put the title and header rows in the sheet // @@ -560,6 +569,16 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter + /*************************************************************************** + ** + ***************************************************************************/ + public static void setStyleForField(QRecord record, String fieldName, String styleName) + { + record.setDisplayValue(fieldName + ":excelStyle", styleName); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -567,12 +586,12 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter { sheetWriter.insertRow(rowNo++); - int styleIndex = -1; + int baseStyleIndex = -1; int dateStyleIndex = styles.get("date").getIndex(); int dateTimeStyleIndex = styles.get("datetime").getIndex(); if(isFooter) { - styleIndex = styles.get("footer").getIndex(); + baseStyleIndex = styles.get("footer").getIndex(); dateStyleIndex = styles.get("footer-date").getIndex(); dateTimeStyleIndex = styles.get("footer-datetime").getIndex(); } @@ -582,6 +601,13 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter { Serializable value = qRecord.getValue(field.getName()); + String overrideStyleName = qRecord.getDisplayValue(field.getName() + ":excelStyle"); + int styleIndex = baseStyleIndex; + if(overrideStyleName != null) + { + styleIndex = styles.get(overrideStyleName).getIndex(); + } + if(value != null) { if(value instanceof String s) @@ -706,7 +732,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter { if(!ReportType.PIVOT.equals(currentView.getType())) { - sheetWriter.endSheet(); + sheetWriter.endSheet(currentView, styleCustomizerInterface); } activeSheetWriter.flush(); @@ -815,7 +841,29 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter *******************************************************************************/ protected PoiExcelStylerInterface getStylerInterface() { + if(styleCustomizerInterface != null) + { + return styleCustomizerInterface.getExcelStyler(); + } + return (new PlainPoiExcelStyler()); } + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void setExportStyleCustomizer(ExportStyleCustomizerInterface exportStyleCustomizer) + { + if(exportStyleCustomizer instanceof ExcelPoiBasedStreamingStyleCustomizerInterface poiExcelStylerInterface) + { + this.styleCustomizerInterface = poiExcelStylerInterface; + } + else + { + LOG.debug("Supplied export style customizer is not an instance of ExcelPoiStyleCustomizerInterface, so will not be used for an excel export", logPair("exportStyleCustomizerClass", exportStyleCustomizer.getClass().getSimpleName())); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingStyleCustomizerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingStyleCustomizerInterface.java new file mode 100644 index 00000000..251e5ff7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingStyleCustomizerInterface.java @@ -0,0 +1,81 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.reporting.excel.poi; + + +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.reporting.ExportStyleCustomizerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + + +/******************************************************************************* + ** style customization points for Excel files generated via our streaming POI. + *******************************************************************************/ +public interface ExcelPoiBasedStreamingStyleCustomizerInterface extends ExportStyleCustomizerInterface +{ + /*************************************************************************** + ** slightly legacy way we did excel styles - but get an instance of object + ** that defaults "default" styles (header, footer, etc). + ***************************************************************************/ + default PoiExcelStylerInterface getExcelStyler() + { + return (new PlainPoiExcelStyler()); + } + + + /*************************************************************************** + ** either change "default" styles put in the styles map, or create new ones + ** which can then be applied to row/field values (cells) via: + ** ExcelPoiBasedStreamingExportStreamer.setStyleForField(row, fieldName, styleName); + ***************************************************************************/ + default void customizeStyles(Map styles, XSSFWorkbook workbook, CreationHelper createHelper) + { + ////////////////// + // noop default // + ////////////////// + } + + + /*************************************************************************** + ** for a given view (sheet), return a list of custom column widths. + ** any nulls in the list are ignored (so default width is used). + ***************************************************************************/ + default List getColumnWidthsForView(QReportView view) + { + return (null); + } + + + /*************************************************************************** + ** for a given view (sheet), return a list of any ranges which should be + ** merged, as in "A1:C1" (first three cells in first row). + ***************************************************************************/ + default List getMergedRangesForView(QReportView view) + { + return (null); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/StreamedSheetWriter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/StreamedSheetWriter.java index 903a9d63..aaf6b269 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/StreamedSheetWriter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/StreamedSheetWriter.java @@ -25,7 +25,10 @@ package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi; import java.io.IOException; import java.io.Writer; import java.util.HashMap; +import java.util.List; import java.util.Map; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import org.apache.poi.ss.util.CellReference; @@ -53,13 +56,33 @@ public class StreamedSheetWriter /******************************************************************************* ** *******************************************************************************/ - public void beginSheet() throws IOException + public void beginSheet(QReportView view, ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface) throws IOException { writer.write(""" - - """); + """); + if(styleCustomizerInterface != null && view != null) + { + List columnWidths = styleCustomizerInterface.getColumnWidthsForView(view); + if(CollectionUtils.nullSafeHasContents(columnWidths)) + { + writer.write(""); + for(int i = 0; i < columnWidths.size(); i++) + { + Integer width = columnWidths.get(i); + if(width != null) + { + writer.write(""" + + """.formatted(i + 1, i + 1, width)); + } + } + writer.write(""); + } + } + + writer.write(""); } @@ -67,11 +90,25 @@ public class StreamedSheetWriter /******************************************************************************* ** *******************************************************************************/ - public void endSheet() throws IOException + public void endSheet(QReportView view, ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface) throws IOException { - writer.write(""" - - """); + writer.write(""); + + if(styleCustomizerInterface != null && view != null) + { + List mergedRanges = styleCustomizerInterface.getMergedRangesForView(view); + if(CollectionUtils.nullSafeHasContents(mergedRanges)) + { + writer.write(String.format("", mergedRanges.size())); + for(String range : mergedRanges) + { + writer.write(String.format("", range)); + } + writer.write(""); + } + } + + writer.write(""); } @@ -151,7 +188,7 @@ public class StreamedSheetWriter { rs.append("""); } - else if (c < 32 && c != '\t' && c != '\n') + else if(c < 32 && c != '\t' && c != '\n') { rs.append(' '); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QJavaExecutor.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QJavaExecutor.java index 61451b38..ae583279 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QJavaExecutor.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QJavaExecutor.java @@ -53,7 +53,8 @@ public class QJavaExecutor implements QCodeExecutor Serializable output; try { - Function, Serializable> function = QCodeLoader.getFunction(codeReference); + @SuppressWarnings("unchecked") + Function, Serializable> function = QCodeLoader.getAdHoc(Function.class, codeReference); output = function.apply(context); } catch(Exception e) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfAction.java index a7f9096f..61d97489 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfAction.java @@ -26,22 +26,32 @@ import java.nio.file.Path; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfInput; import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfOutput; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.openhtmltopdf.css.constants.IdentValue; +import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver; +import com.openhtmltopdf.pdfboxout.PdfBoxRenderer; +import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; import org.jsoup.Jsoup; +import org.jsoup.helper.W3CDom; import org.jsoup.nodes.Document; -import org.xhtmlrenderer.layout.SharedContext; -import org.xhtmlrenderer.pdf.ITextRenderer; /******************************************************************************* ** Action to convert a string of HTML to a PDF! ** ** Much credit to https://www.baeldung.com/java-html-to-pdf - *******************************************************************************/ + ** + ** Updated in March 2025 to go from flying-saucer-pdf-openpdf lib to openhtmltopdf, + ** mostly to get support for max-height on images... + ********************************************************************************/ public class ConvertHtmlToPdfAction extends AbstractQActionFunction { + private static final QLogger LOG = QLogger.getLogger(ConvertHtmlToPdfAction.class); + + /******************************************************************************* ** @@ -58,35 +68,37 @@ public class ConvertHtmlToPdfAction extends AbstractQActionFunction entry : CollectionUtils.nonNullMap(input.getCustomFonts()).entrySet()) - { - renderer.getFontResolver().addFont(entry.getValue().toAbsolutePath().toString(), entry.getKey(), "UTF-8", true, null); - } + for(Map.Entry entry : CollectionUtils.nonNullMap(input.getCustomFonts()).entrySet()) + { + LOG.warn("Note: Custom fonts appear to not be working in this class at this time..."); + pdfBoxRenderer.getFontResolver().addFont( + entry.getValue().toAbsolutePath().toFile(), // Path to the TrueType font file + entry.getKey(), // Font family name to use in CSS + 400, // Font weight (e.g., 400 for normal, 700 for bold) + IdentValue.NORMAL, // Font style (e.g., NORMAL, ITALIC) + true, // Whether to subset the font + PdfBoxFontResolver.FontGroup.MAIN // ?? + ); + } - renderer.layout(); - renderer.createPDF(input.getOutputStream()); + pdfBoxRenderer.createPDF(); + } return (output); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/BasicCustomPossibleValueProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/BasicCustomPossibleValueProvider.java new file mode 100644 index 00000000..da1421bc --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/BasicCustomPossibleValueProvider.java @@ -0,0 +1,91 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.values; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; + + +/******************************************************************************* + ** Basic implementation of a possible value provider, for where there's a limited + ** set of possible source objects - so you just have to define how to make one + ** PV from a source object, how to list all of the source objects, and how to + ** look up a PV from an id. + *******************************************************************************/ +public abstract class BasicCustomPossibleValueProvider implements QCustomPossibleValueProvider +{ + + /*************************************************************************** + ** + ***************************************************************************/ + protected abstract QPossibleValue makePossibleValue(S sourceObject); + + /*************************************************************************** + ** + ***************************************************************************/ + protected abstract S getSourceObject(Serializable id); + + /*************************************************************************** + ** + ***************************************************************************/ + protected abstract List getAllSourceObjects(); + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QPossibleValue getPossibleValue(Serializable idValue) + { + S sourceObject = getSourceObject(idValue); + if(sourceObject == null) + { + return (null); + } + + return makePossibleValue(sourceObject); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List> search(SearchPossibleValueSourceInput input) throws QException + { + List> allPossibleValues = new ArrayList<>(); + List allSourceObjects = getAllSourceObjects(); + for(S sourceObject : allSourceObjects) + { + allPossibleValues.add(makePossibleValue(sourceObject)); + } + + return completeCustomPVSSearch(input, allPossibleValues); + } +} 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 80eeb6a7..1aa7adf5 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 @@ -341,7 +341,7 @@ public class QPossibleValueTranslator try { - QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource); + QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference()); return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value))); } catch(Exception e) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java index 6416ff50..42ddb559 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java @@ -424,7 +424,7 @@ public class SearchPossibleValueSourceAction { try { - QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource); + QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference()); List> possibleValues = customPossibleValueProvider.search(input); SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput(); 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 f0b82ab0..6895a258 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 @@ -1439,7 +1439,7 @@ public class QInstanceEnricher { try { - QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource); + QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference()); Method getPossibleValueMethod = customPossibleValueProvider.getClass().getDeclaredMethod("getPossibleValue", Serializable.class); Type returnType = getPossibleValueMethod.getGenericReturnType(); 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 b58be39b..8f7793ef 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 @@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph; +import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataActionCustomizerInterface; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataFilterInterface; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface; @@ -246,6 +247,11 @@ public class QInstanceValidator { validateSimpleCodeReference("Instance metaDataFilter ", qInstance.getMetaDataFilter(), MetaDataFilterInterface.class); } + + if(qInstance.getMetaDataActionCustomizer() != null) + { + validateSimpleCodeReference("Instance metaDataActionCustomizer ", qInstance.getMetaDataActionCustomizer(), MetaDataActionCustomizerInterface.class); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportInput.java index be901e94..6c26a7b9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportInput.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.function.Supplier; import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; @@ -44,6 +45,7 @@ public class ReportInput extends AbstractTableActionInput private ReportDestination reportDestination; private Supplier overrideExportStreamerSupplier; + private QCodeReference exportStyleCustomizer; @@ -208,4 +210,35 @@ public class ReportInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for exportStyleCustomizer + *******************************************************************************/ + public QCodeReference getExportStyleCustomizer() + { + return (this.exportStyleCustomizer); + } + + + + /******************************************************************************* + ** Setter for exportStyleCustomizer + *******************************************************************************/ + public void setExportStyleCustomizer(QCodeReference exportStyleCustomizer) + { + this.exportStyleCustomizer = exportStyleCustomizer; + } + + + + /******************************************************************************* + ** Fluent setter for exportStyleCustomizer + *******************************************************************************/ + public ReportInput withExportStyleCustomizer(QCodeReference exportStyleCustomizer) + { + this.exportStyleCustomizer = exportStyleCustomizer; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java index 9989f670..3a773a84 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java @@ -66,6 +66,7 @@ public enum WidgetType // record view/edit widgets // ////////////////////////////// CHILD_RECORD_LIST("childRecordList"), + CUSTOM_COMPONENT("customComponent"), DYNAMIC_FORM("dynamicForm"), DATA_BAG_VIEWER("dataBagViewer"), PIVOT_TABLE_SETUP("pivotTableSetup"), 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 cc3b0f6c..194e59da 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 @@ -468,7 +468,7 @@ public class QRecord implements Serializable *******************************************************************************/ public String getValueString(String fieldName) { - return (ValueUtils.getValueAsString(values.get(fieldName))); + return (ValueUtils.getValueAsString(getValue(fieldName))); } @@ -479,7 +479,7 @@ public class QRecord implements Serializable *******************************************************************************/ public Integer getValueInteger(String fieldName) { - return (ValueUtils.getValueAsInteger(values.get(fieldName))); + return (ValueUtils.getValueAsInteger(getValue(fieldName))); } @@ -490,7 +490,7 @@ public class QRecord implements Serializable *******************************************************************************/ public Long getValueLong(String fieldName) { - return (ValueUtils.getValueAsLong(values.get(fieldName))); + return (ValueUtils.getValueAsLong(getValue(fieldName))); } @@ -500,7 +500,7 @@ public class QRecord implements Serializable *******************************************************************************/ public BigDecimal getValueBigDecimal(String fieldName) { - return (ValueUtils.getValueAsBigDecimal(values.get(fieldName))); + return (ValueUtils.getValueAsBigDecimal(getValue(fieldName))); } @@ -510,7 +510,7 @@ public class QRecord implements Serializable *******************************************************************************/ public Boolean getValueBoolean(String fieldName) { - return (ValueUtils.getValueAsBoolean(values.get(fieldName))); + return (ValueUtils.getValueAsBoolean(getValue(fieldName))); } @@ -520,7 +520,7 @@ public class QRecord implements Serializable *******************************************************************************/ public LocalTime getValueLocalTime(String fieldName) { - return (ValueUtils.getValueAsLocalTime(values.get(fieldName))); + return (ValueUtils.getValueAsLocalTime(getValue(fieldName))); } @@ -530,7 +530,7 @@ public class QRecord implements Serializable *******************************************************************************/ public LocalDate getValueLocalDate(String fieldName) { - return (ValueUtils.getValueAsLocalDate(values.get(fieldName))); + return (ValueUtils.getValueAsLocalDate(getValue(fieldName))); } @@ -540,7 +540,7 @@ public class QRecord implements Serializable *******************************************************************************/ public byte[] getValueByteArray(String fieldName) { - return (ValueUtils.getValueAsByteArray(values.get(fieldName))); + return (ValueUtils.getValueAsByteArray(getValue(fieldName))); } @@ -550,7 +550,7 @@ public class QRecord implements Serializable *******************************************************************************/ public Instant getValueInstant(String fieldName) { - return (ValueUtils.getValueAsInstant(values.get(fieldName))); + return (ValueUtils.getValueAsInstant(getValue(fieldName))); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java index 880f4993..f681aa58 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java @@ -24,11 +24,20 @@ package com.kingsrook.qqq.backend.core.model.data; import java.io.Serializable; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -449,4 +458,157 @@ public abstract class QRecordEntity } + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getFieldNameFromGetter(Method getter) + { + String nameWithoutGet = getter.getName().replaceFirst("^get", ""); + if(nameWithoutGet.length() == 1) + { + return (nameWithoutGet.toLowerCase(Locale.ROOT)); + } + return (nameWithoutGet.substring(0, 1).toLowerCase(Locale.ROOT) + nameWithoutGet.substring(1)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean isGetter(Method method) + { + if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*")) + { + if(isSupportedFieldType(method.getReturnType()) || isSupportedAssociation(method.getReturnType(), method.getAnnotatedReturnType())) + { + return (true); + } + else + { + if(!method.getName().equals("getClass") && method.getAnnotation(QIgnore.class) == null) + { + LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported."); + } + } + } + return (false); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Optional getSetterForGetter(Class c, Method getter) + { + String setterName = getter.getName().replaceFirst("^get", "set"); + for(Method method : c.getMethods()) + { + if(method.getName().equals(setterName)) + { + if(method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(getter.getReturnType())) + { + return (Optional.of(method)); + } + else + { + LOG.info("Method [" + method.getName() + "] looks like a setter for [" + getter.getName() + "], but its parameters, [" + Arrays.toString(method.getParameterTypes()) + "], don't match the getter's return type [" + getter.getReturnType() + "]"); + } + } + } + return (Optional.empty()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean isSupportedFieldType(Class returnType) + { + // todo - more types!! + return (returnType.equals(String.class) + || returnType.equals(Integer.class) + || returnType.equals(Long.class) + || returnType.equals(int.class) + || returnType.equals(Boolean.class) + || returnType.equals(boolean.class) + || returnType.equals(BigDecimal.class) + || returnType.equals(Instant.class) + || returnType.equals(LocalDate.class) + || returnType.equals(LocalTime.class) + || returnType.equals(byte[].class)); + ///////////////////////////////////////////// + // note - this list has implications upon: // + // - QFieldType.fromClass // + // - QRecordEntityField.convertValueType // + ///////////////////////////////////////////// + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean isSupportedAssociation(Class returnType, AnnotatedType annotatedType) + { + Class listTypeParam = getListTypeParam(returnType, annotatedType); + return (listTypeParam != null && QRecordEntity.class.isAssignableFrom(listTypeParam)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Class getListTypeParam(Class listType, AnnotatedType annotatedType) + { + if(listType.equals(List.class)) + { + if(annotatedType instanceof AnnotatedParameterizedType apt) + { + AnnotatedType[] annotatedActualTypeArguments = apt.getAnnotatedActualTypeArguments(); + for(AnnotatedType annotatedActualTypeArgument : annotatedActualTypeArguments) + { + Type type = annotatedActualTypeArgument.getType(); + if(type instanceof Class c) + { + return (c); + } + } + } + } + + return (null); + } + + + /*************************************************************************** + ** + ***************************************************************************/ + public static String getTableName(Class entityClass) throws QException + { + try + { + Field tableNameField = entityClass.getDeclaredField("TABLE_NAME"); + String tableNameValue = (String) tableNameField.get(null); + return (tableNameValue); + } + catch(Exception e) + { + throw (new QException("Could not get TABLE_NAME from entity class: " + entityClass.getSimpleName(), e)); + } + } + + + /*************************************************************************** + ** named without the 'get' to avoid conflict w/ entity fields named that... + ***************************************************************************/ + public String tableName() throws QException + { + return (getTableName(this.getClass())); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordWithJoinedRecords.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordWithJoinedRecords.java index aa773687..70796b49 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordWithJoinedRecords.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordWithJoinedRecords.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.data; import java.io.Serializable; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.function.BiFunction; @@ -198,4 +199,62 @@ public class QRecordWithJoinedRecords extends QRecord return (rs); } + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public Map> getAssociatedRecords() + { + return mainRecord.getAssociatedRecords(); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QRecord withAssociatedRecord(String name, QRecord associatedRecord) + { + mainRecord.withAssociatedRecord(name, associatedRecord); + return (this); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QRecord withAssociatedRecords(Map> associatedRecords) + { + mainRecord.withAssociatedRecords(associatedRecords); + return (this); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void setAssociatedRecords(Map> associatedRecords) + { + mainRecord.setAssociatedRecords(associatedRecords); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QRecord withAssociatedRecords(String name, List associatedRecords) + { + mainRecord.withAssociatedRecords(name, associatedRecords); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/helpcontent/HelpContentMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/helpcontent/HelpContentMetaDataProvider.java index 45657a3c..9510eb19 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/helpcontent/HelpContentMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/helpcontent/HelpContentMetaDataProvider.java @@ -82,6 +82,7 @@ public class HelpContentMetaDataProvider table.getField("key").withFieldAdornment(AdornmentType.Size.LARGE.toAdornment()); table.getField("content").withFieldAdornment(AdornmentType.Size.LARGE.toAdornment()); table.getField("content").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("html"))); + table.getField("content").withGridColumns(12); if(backendDetailEnricher != null) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java index ec41e0b6..14a056ba 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java @@ -417,7 +417,7 @@ public class MetaDataProducerHelper return (null); } - ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName); + ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName, childTable.childJoin().orderBy()); producer.setSourceClass(entityClass); return producer; } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index cb70eaf4..7b1fe014 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -116,8 +116,11 @@ public class QInstance private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance(); private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone(); + @Deprecated(since = "migrated to metaDataCustomizer") private QCodeReference metaDataFilter = null; + private QCodeReference metaDataActionCustomizer = null; + ////////////////////////////////////////////////////////////////////////////////////// // todo - lock down the object (no more changes allowed) after it's been validated? // // if doing so, may need to copy all of the collections into read-only versions... // @@ -1495,6 +1498,7 @@ public class QInstance /******************************************************************************* ** Getter for metaDataFilter *******************************************************************************/ + @Deprecated(since = "migrated to metaDataCustomizer") public QCodeReference getMetaDataFilter() { return (this.metaDataFilter); @@ -1505,6 +1509,7 @@ public class QInstance /******************************************************************************* ** Setter for metaDataFilter *******************************************************************************/ + @Deprecated(since = "migrated to metaDataCustomizer") public void setMetaDataFilter(QCodeReference metaDataFilter) { this.metaDataFilter = metaDataFilter; @@ -1515,6 +1520,7 @@ public class QInstance /******************************************************************************* ** Fluent setter for metaDataFilter *******************************************************************************/ + @Deprecated(since = "migrated to metaDataCustomizer") public QInstance withMetaDataFilter(QCodeReference metaDataFilter) { this.metaDataFilter = metaDataFilter; @@ -1586,4 +1592,35 @@ public class QInstance return (this); } + + + /******************************************************************************* + ** Getter for metaDataActionCustomizer + *******************************************************************************/ + public QCodeReference getMetaDataActionCustomizer() + { + return (this.metaDataActionCustomizer); + } + + + + /******************************************************************************* + ** Setter for metaDataActionCustomizer + *******************************************************************************/ + public void setMetaDataActionCustomizer(QCodeReference metaDataActionCustomizer) + { + this.metaDataActionCustomizer = metaDataActionCustomizer; + } + + + + /******************************************************************************* + ** Fluent setter for metaDataActionCustomizer + *******************************************************************************/ + public QInstance withMetaDataActionCustomizer(QCodeReference metaDataActionCustomizer) + { + this.metaDataActionCustomizer = metaDataActionCustomizer; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/Banner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/Banner.java new file mode 100644 index 00000000..299955c6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/Banner.java @@ -0,0 +1,269 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.branding; + + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + + +/******************************************************************************* + ** element of BrandingMetaData - content to send to a frontend for showing a + ** user across the whole UI - e.g., what environment you're in, or a message + ** about your account - site announcements, etc. + *******************************************************************************/ +public class Banner implements Serializable, Cloneable +{ + private Severity severity; + private String textColor; + private String backgroundColor; + private String messageText; + private String messageHTML; + + private Map additionalStyles; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public enum Severity + { + INFO, WARNING, ERROR, SUCCESS + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public Banner clone() + { + try + { + Banner clone = (Banner) super.clone(); + + ////////////////////////////////////////////////////////////////////////////////////// + // copy mutable state here, so the clone can't change the internals of the original // + ////////////////////////////////////////////////////////////////////////////////////// + if(additionalStyles != null) + { + clone.setAdditionalStyles(new LinkedHashMap<>(additionalStyles)); + } + + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } + + + + /******************************************************************************* + ** Getter for textColor + *******************************************************************************/ + public String getTextColor() + { + return (this.textColor); + } + + + + /******************************************************************************* + ** Setter for textColor + *******************************************************************************/ + public void setTextColor(String textColor) + { + this.textColor = textColor; + } + + + + /******************************************************************************* + ** Fluent setter for textColor + *******************************************************************************/ + public Banner withTextColor(String textColor) + { + this.textColor = textColor; + return (this); + } + + + + /******************************************************************************* + ** Getter for backgroundColor + *******************************************************************************/ + public String getBackgroundColor() + { + return (this.backgroundColor); + } + + + + /******************************************************************************* + ** Setter for backgroundColor + *******************************************************************************/ + public void setBackgroundColor(String backgroundColor) + { + this.backgroundColor = backgroundColor; + } + + + + /******************************************************************************* + ** Fluent setter for backgroundColor + *******************************************************************************/ + public Banner withBackgroundColor(String backgroundColor) + { + this.backgroundColor = backgroundColor; + return (this); + } + + + + /******************************************************************************* + ** Getter for additionalStyles + *******************************************************************************/ + public Map getAdditionalStyles() + { + return (this.additionalStyles); + } + + + + /******************************************************************************* + ** Setter for additionalStyles + *******************************************************************************/ + public void setAdditionalStyles(Map additionalStyles) + { + this.additionalStyles = additionalStyles; + } + + + + /******************************************************************************* + ** Fluent setter for additionalStyles + *******************************************************************************/ + public Banner withAdditionalStyles(Map additionalStyles) + { + this.additionalStyles = additionalStyles; + return (this); + } + + + + /******************************************************************************* + ** Getter for messageText + *******************************************************************************/ + public String getMessageText() + { + return (this.messageText); + } + + + + /******************************************************************************* + ** Setter for messageText + *******************************************************************************/ + public void setMessageText(String messageText) + { + this.messageText = messageText; + } + + + + /******************************************************************************* + ** Fluent setter for messageText + *******************************************************************************/ + public Banner withMessageText(String messageText) + { + this.messageText = messageText; + return (this); + } + + + + /******************************************************************************* + ** Getter for messageHTML + *******************************************************************************/ + public String getMessageHTML() + { + return (this.messageHTML); + } + + + + /******************************************************************************* + ** Setter for messageHTML + *******************************************************************************/ + public void setMessageHTML(String messageHTML) + { + this.messageHTML = messageHTML; + } + + + + /******************************************************************************* + ** Fluent setter for messageHTML + *******************************************************************************/ + public Banner withMessageHTML(String messageHTML) + { + this.messageHTML = messageHTML; + return (this); + } + + + + /******************************************************************************* + ** Getter for severity + *******************************************************************************/ + public Severity getSeverity() + { + return (this.severity); + } + + + + /******************************************************************************* + ** Setter for severity + *******************************************************************************/ + public void setSeverity(Severity severity) + { + this.severity = severity; + } + + + + /******************************************************************************* + ** Fluent setter for severity + *******************************************************************************/ + public Banner withSeverity(Severity severity) + { + this.severity = severity; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/BannerSlot.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/BannerSlot.java new file mode 100644 index 00000000..162f6758 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/BannerSlot.java @@ -0,0 +1,31 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.branding; + + +/******************************************************************************* + ** interface to define keys for where banners should be displayed. + ** expect frontends to implement this interface with enums of known possible values + *******************************************************************************/ +public interface BannerSlot +{ +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/QBrandingMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/QBrandingMetaData.java index 6eb01b58..d354a8e3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/QBrandingMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/QBrandingMetaData.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.branding; +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; @@ -30,7 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; ** Meta-Data to define branding in a QQQ instance. ** *******************************************************************************/ -public class QBrandingMetaData implements TopLevelMetaDataInterface +public class QBrandingMetaData implements TopLevelMetaDataInterface, Cloneable, Serializable { private String companyName; private String companyUrl; @@ -39,9 +42,45 @@ public class QBrandingMetaData implements TopLevelMetaDataInterface private String icon; private String accentColor; + @Deprecated(since = "migrate to use banners map instead") private String environmentBannerText; + + @Deprecated(since = "migrate to use banners map instead") private String environmentBannerColor; + private Map banners; + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QBrandingMetaData clone() + { + try + { + QBrandingMetaData clone = (QBrandingMetaData) super.clone(); + + ////////////////////////////////////////////////////////////////////////////////////// + // copy mutable state here, so the clone can't change the internals of the original // + ////////////////////////////////////////////////////////////////////////////////////// + if(banners != null) + { + clone.banners = new LinkedHashMap<>(); + for(Map.Entry entry : this.banners.entrySet()) + { + clone.banners.put(entry.getKey(), entry.getValue().clone()); + } + } + + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } + /******************************************************************************* @@ -267,6 +306,7 @@ public class QBrandingMetaData implements TopLevelMetaDataInterface /******************************************************************************* ** Getter for environmentBannerText *******************************************************************************/ + @Deprecated(since = "migrate to use banners map instead") public String getEnvironmentBannerText() { return (this.environmentBannerText); @@ -277,6 +317,7 @@ public class QBrandingMetaData implements TopLevelMetaDataInterface /******************************************************************************* ** Setter for environmentBannerText *******************************************************************************/ + @Deprecated(since = "migrate to use banners map instead") public void setEnvironmentBannerText(String environmentBannerText) { this.environmentBannerText = environmentBannerText; @@ -287,6 +328,7 @@ public class QBrandingMetaData implements TopLevelMetaDataInterface /******************************************************************************* ** Fluent setter for environmentBannerText *******************************************************************************/ + @Deprecated(since = "migrate to use banners map instead") public QBrandingMetaData withEnvironmentBannerText(String environmentBannerText) { this.environmentBannerText = environmentBannerText; @@ -298,6 +340,7 @@ public class QBrandingMetaData implements TopLevelMetaDataInterface /******************************************************************************* ** Getter for environmentBannerColor *******************************************************************************/ + @Deprecated(since = "migrate to use banners map instead") public String getEnvironmentBannerColor() { return (this.environmentBannerColor); @@ -308,6 +351,7 @@ public class QBrandingMetaData implements TopLevelMetaDataInterface /******************************************************************************* ** Setter for environmentBannerColor *******************************************************************************/ + @Deprecated(since = "migrate to use banners map instead") public void setEnvironmentBannerColor(String environmentBannerColor) { this.environmentBannerColor = environmentBannerColor; @@ -318,6 +362,7 @@ public class QBrandingMetaData implements TopLevelMetaDataInterface /******************************************************************************* ** Fluent setter for environmentBannerColor *******************************************************************************/ + @Deprecated(since = "migrate to use banners map instead") public QBrandingMetaData withEnvironmentBannerColor(String environmentBannerColor) { this.environmentBannerColor = environmentBannerColor; @@ -334,4 +379,52 @@ public class QBrandingMetaData implements TopLevelMetaDataInterface { qInstance.setBranding(this); } + + + /******************************************************************************* + ** Getter for banners + *******************************************************************************/ + public Map getBanners() + { + return (this.banners); + } + + + + /******************************************************************************* + ** Setter for banners + *******************************************************************************/ + public void setBanners(Map banners) + { + this.banners = banners; + } + + + + /******************************************************************************* + ** Fluent setter for banners + *******************************************************************************/ + public QBrandingMetaData withBanners(Map banners) + { + this.banners = banners; + return (this); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public QBrandingMetaData withBanner(BannerSlot slot, Banner banner) + { + if(this.banners == null) + { + this.banners = new LinkedHashMap<>(); + } + this.banners.put(slot, banner); + + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/InitializableViaCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/InitializableViaCodeReference.java new file mode 100644 index 00000000..976231c3 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/InitializableViaCodeReference.java @@ -0,0 +1,38 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.code; + + +/******************************************************************************* + ** an object which is intended to be constructed via a CodeReference, and, + ** moreso, after it is created, then the initialize method here gets called, + ** passing the codeRefernce in - e.g., to do additional initalization of the + ** object, e.g., properties in a QCodeReferenceWithProperties + *******************************************************************************/ +public interface InitializableViaCodeReference +{ + /*************************************************************************** + ** + ***************************************************************************/ + void initialize(QCodeReference codeReference); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java index eb6a01b6..1f154390 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java @@ -30,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; ** Pointer to code to be ran by the qqq framework, e.g., for custom behavior - ** maybe process steps, maybe customization to a table, etc. *******************************************************************************/ -public class QCodeReference implements Serializable, QMetaDataObject +public class QCodeReference implements Serializable, Cloneable, QMetaDataObject { private String name; private QCodeType codeType; @@ -59,6 +59,25 @@ public class QCodeReference implements Serializable, QMetaDataObject + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QCodeReference clone() + { + try + { + QCodeReference clone = (QCodeReference) super.clone(); + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -180,5 +199,4 @@ public class QCodeReference implements Serializable, QMetaDataObject this.inlineCode = inlineCode; return (this); } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReferenceWithProperties.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReferenceWithProperties.java new file mode 100644 index 00000000..271127f3 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReferenceWithProperties.java @@ -0,0 +1,59 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.code; + + +import java.io.Serializable; +import java.util.Map; + + +/******************************************************************************* + ** a code reference that also has a map of properties. This object (with the + ** properties) will be passed in to the referenced object, if it implements + ** InitializableViaCodeReference. + *******************************************************************************/ +public class QCodeReferenceWithProperties extends QCodeReference +{ + private final Map properties; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public QCodeReferenceWithProperties(Class javaClass, Map properties) + { + super(javaClass); + this.properties = properties; + } + + + + /******************************************************************************* + ** Getter for properties + ** + *******************************************************************************/ + public Map getProperties() + { + return properties; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetAdHocValue.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetAdHocValue.java index 384dd684..88151665 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetAdHocValue.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetAdHocValue.java @@ -62,7 +62,8 @@ public class WidgetAdHocValue extends AbstractWidgetValueSource context.putAll(inputValues); } - Function function = QCodeLoader.getFunction(codeReference); + @SuppressWarnings("unchecked") + Function function = QCodeLoader.getAdHoc(Function.class, codeReference); Object result = function.apply(context); return (result); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index cac0bd22..ce59465a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -240,7 +240,7 @@ public class QFieldMetaData implements Cloneable, QMetaDataObject if(StringUtils.hasContent(fieldAnnotation.defaultValue())) { - ValueUtils.getValueAsFieldType(this.type, fieldAnnotation.defaultValue()); + withDefaultValue(ValueUtils.getValueAsFieldType(this.type, fieldAnnotation.defaultValue())); } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java index c6c6014f..0d8a88ad 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java @@ -55,6 +55,7 @@ public class QFrontendFieldMetaData implements Serializable private String possibleValueSourceName; private String displayFormat; private Serializable defaultValue; + private Integer maxLength; private List adornments; private List helpContents; @@ -85,6 +86,7 @@ public class QFrontendFieldMetaData implements Serializable this.defaultValue = fieldMetaData.getDefaultValue(); this.helpContents = fieldMetaData.getHelpContents(); this.inlinePossibleValueSource = fieldMetaData.getInlinePossibleValueSource(); + this.maxLength = fieldMetaData.getMaxLength(); for(FieldBehavior behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors())) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java index c435e0f4..4fe61dbf 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java @@ -48,6 +48,8 @@ public class QFrontendProcessMetaData private String label; private String tableName; private boolean isHidden; + private Integer minInputRecords; + private Integer maxInputRecords; private QIcon icon; @@ -72,6 +74,8 @@ public class QFrontendProcessMetaData this.tableName = processMetaData.getTableName(); this.isHidden = processMetaData.getIsHidden(); this.stepFlow = processMetaData.getStepFlow().toString(); + this.minInputRecords = processMetaData.getMinInputRecords(); + this.maxInputRecords = processMetaData.getMaxInputRecords(); if(includeSteps) { @@ -213,4 +217,27 @@ public class QFrontendProcessMetaData { return icon; } + + + + /******************************************************************************* + ** Getter for minInputRecords + ** + *******************************************************************************/ + public Integer getMinInputRecords() + { + return minInputRecords; + } + + + + /******************************************************************************* + ** Getter for maxInputRecords + ** + *******************************************************************************/ + public Integer getMaxInputRecords() + { + return maxInputRecords; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java index 0951edd1..3d98ee63 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java @@ -34,7 +34,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; ** Future may allow something like a "namespace", and/or multiple icons for ** use in different frontends, etc. *******************************************************************************/ -public class QIcon implements QMetaDataObject +public class QIcon implements Cloneable, QMetaDataObject { private String name; private String path; @@ -61,6 +61,25 @@ public class QIcon implements QMetaDataObject + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QIcon clone() + { + try + { + QIcon clone = (QIcon) super.clone(); + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } + + + /******************************************************************************* ** Getter for name ** @@ -157,6 +176,4 @@ public class QIcon implements QMetaDataObject this.color = color; return (this); } - - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/producers/ChildJoinFromRecordEntityGenericMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/producers/ChildJoinFromRecordEntityGenericMetaDataProducer.java index c1bc51cf..958b4900 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/producers/ChildJoinFromRecordEntityGenericMetaDataProducer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/producers/ChildJoinFromRecordEntityGenericMetaDataProducer.java @@ -24,11 +24,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.producers; import java.util.Objects; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildJoin; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -39,12 +41,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; ** ** e.g., Orders & LineItems - on the Order entity ** - @QMetaDataProducingEntity( - childTables = { @ChildTable( - childTableEntityClass = LineItem.class, - childJoin = @ChildJoin(enabled = true), - childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Order Lines")) - } + @QMetaDataProducingEntity( childTables = { @ChildTable( + childTableEntityClass = LineItem.class, + childJoin = @ChildJoin(enabled = true), + childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Order Lines")) + } ) public class Order extends QRecordEntity ** @@ -62,13 +63,16 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat private String parentTableName; // e.g., order private String foreignKeyFieldName; // e.g., orderId + private ChildJoin.OrderBy[] orderBys; + private Class sourceClass; + /*************************************************************************** ** ***************************************************************************/ - public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName) + public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName, ChildJoin.OrderBy[] orderBys) { Objects.requireNonNull(childTableName, "childTableName cannot be null"); Objects.requireNonNull(parentTableName, "parentTableName cannot be null"); @@ -77,6 +81,7 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat this.childTableName = childTableName; this.parentTableName = parentTableName; this.foreignKeyFieldName = foreignKeyFieldName; + this.orderBys = orderBys; } @@ -87,18 +92,39 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat @Override public QJoinMetaData produce(QInstance qInstance) throws QException { - QTableMetaData possibleValueTable = qInstance.getTable(parentTableName); - if(possibleValueTable == null) + QTableMetaData parentTable = qInstance.getTable(parentTableName); + if(parentTable == null) { throw (new QException("Could not find tableMetaData " + parentTableName)); } + QTableMetaData childTable = qInstance.getTable(childTableName); + if(childTable == null) + { + throw (new QException("Could not find tableMetaData " + childTable)); + } + QJoinMetaData join = new QJoinMetaData() .withLeftTable(parentTableName) .withRightTable(childTableName) .withInferredName() .withType(JoinType.ONE_TO_MANY) - .withJoinOn(new JoinOn(possibleValueTable.getPrimaryKeyField(), foreignKeyFieldName)); + .withJoinOn(new JoinOn(parentTable.getPrimaryKeyField(), foreignKeyFieldName)); + + if(orderBys != null && orderBys.length > 0) + { + for(ChildJoin.OrderBy orderBy : orderBys) + { + join.withOrderBy(new QFilterOrderBy(orderBy.fieldName(), orderBy.isAscending())); + } + } + else + { + ////////////////////////////////////////////////////////// + // by default, sort by the id of the child table... mmm // + ////////////////////////////////////////////////////////// + join.withOrderBy(new QFilterOrderBy(childTable.getPrimaryKeyField())); + } return (join); } @@ -126,6 +152,7 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat } + /******************************************************************************* ** Fluent setter for sourceClass ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/producers/annotations/ChildJoin.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/producers/annotations/ChildJoin.java index 2266679e..5439bf02 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/producers/annotations/ChildJoin.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/producers/annotations/ChildJoin.java @@ -35,4 +35,16 @@ import java.lang.annotation.RetentionPolicy; public @interface ChildJoin { boolean enabled(); + + OrderBy[] orderBy() default { }; + + /*************************************************************************** + ** + ***************************************************************************/ + @interface OrderBy + { + String fieldName(); + + boolean isAscending() default true; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java index 843c5241..712e1513 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java @@ -40,7 +40,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; ** (optionally along with queryJoins and queryInputCustomizer) is used. ** - else a staticDataSupplier is used. *******************************************************************************/ -public class QReportDataSource +public class QReportDataSource implements Cloneable { private String name; @@ -55,6 +55,39 @@ public class QReportDataSource + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QReportDataSource clone() + { + try + { + QReportDataSource clone = (QReportDataSource) super.clone(); + if(queryFilter != null) + { + clone.queryFilter = queryFilter.clone(); + } + + if(queryJoins != null) + { + clone.queryJoins = new ArrayList<>(); + for(QueryJoin join : queryJoins) + { + queryJoins.add(join.clone()); + } + } + + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } + + + /******************************************************************************* ** Getter for name ** @@ -274,6 +307,7 @@ public class QReportDataSource } + /******************************************************************************* ** Getter for customRecordSource *******************************************************************************/ @@ -303,5 +337,4 @@ public class QReportDataSource return (this); } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java index 649de4ee..c5b9bf9f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; @@ -37,7 +38,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* ** Meta-data definition of a report generated by QQQ *******************************************************************************/ -public class QReportMetaData implements QAppChildMetaData, MetaDataWithPermissionRules, TopLevelMetaDataInterface +public class QReportMetaData implements QAppChildMetaData, MetaDataWithPermissionRules, TopLevelMetaDataInterface, Cloneable { private String name; private String label; @@ -52,6 +53,72 @@ public class QReportMetaData implements QAppChildMetaData, MetaDataWithPermissio private QIcon icon; + private QCodeReference exportStyleCustomizer; + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QReportMetaData clone() + { + try + { + QReportMetaData clone = (QReportMetaData) super.clone(); + + ////////////////////////////// + // Deep copy mutable fields // + ////////////////////////////// + if(this.inputFields != null) + { + clone.inputFields = new ArrayList<>(); + for(QFieldMetaData inputField : this.inputFields) + { + clone.inputFields.add(inputField.clone()); + } + } + + if(this.dataSources != null) + { + clone.dataSources = new ArrayList<>(); + for(QReportDataSource dataSource : this.dataSources) + { + clone.dataSources.add(dataSource.clone()); + } + } + + if(this.views != null) + { + clone.views = new ArrayList<>(); + for(QReportView view : this.views) + { + clone.views.add(view.clone()); + } + } + + if(this.permissionRules != null) + { + clone.permissionRules = this.permissionRules.clone(); + } + + if(this.icon != null) + { + clone.icon = this.icon.clone(); + } + + if(this.exportStyleCustomizer != null) + { + clone.exportStyleCustomizer = this.exportStyleCustomizer.clone(); + } + + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError("Cloning not supported", e); + } + } + /******************************************************************************* @@ -397,4 +464,35 @@ public class QReportMetaData implements QAppChildMetaData, MetaDataWithPermissio qInstance.addReport(this); } + + /******************************************************************************* + ** Getter for exportStyleCustomizer + *******************************************************************************/ + public QCodeReference getExportStyleCustomizer() + { + return (this.exportStyleCustomizer); + } + + + + /******************************************************************************* + ** Setter for exportStyleCustomizer + *******************************************************************************/ + public void setExportStyleCustomizer(QCodeReference exportStyleCustomizer) + { + this.exportStyleCustomizer = exportStyleCustomizer; + } + + + + /******************************************************************************* + ** Fluent setter for exportStyleCustomizer + *******************************************************************************/ + public QReportMetaData withExportStyleCustomizer(QCodeReference exportStyleCustomizer) + { + this.exportStyleCustomizer = exportStyleCustomizer; + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesCustomPossibleValueProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesCustomPossibleValueProvider.java index e72d188f..e2d98b4a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesCustomPossibleValueProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesCustomPossibleValueProvider.java @@ -27,11 +27,9 @@ import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; -import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; +import com.kingsrook.qqq.backend.core.actions.values.BasicCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; -import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -40,26 +38,16 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils; ** possible-value source provider for the `Tables` PVS - a list of all tables ** in an application/qInstance. *******************************************************************************/ -public class TablesCustomPossibleValueProvider implements QCustomPossibleValueProvider +public class TablesCustomPossibleValueProvider extends BasicCustomPossibleValueProvider { /*************************************************************************** ** ***************************************************************************/ @Override - public QPossibleValue getPossibleValue(Serializable idValue) + protected QPossibleValue makePossibleValue(QTableMetaData sourceObject) { - QTableMetaData table = QContext.getQInstance().getTable(ValueUtils.getValueAsString(idValue)); - if(table != null && !table.getIsHidden()) - { - PermissionCheckResult permissionCheckResult = PermissionsHelper.getPermissionCheckResult(new QueryInput(table.getName()), table); - if(PermissionCheckResult.ALLOW.equals(permissionCheckResult)) - { - return (new QPossibleValue<>(table.getName(), table.getLabel())); - } - } - - return null; + return (new QPossibleValue<>(sourceObject.getName(), sourceObject.getLabel())); } @@ -68,22 +56,54 @@ public class TablesCustomPossibleValueProvider implements QCustomPossibleValuePr ** ***************************************************************************/ @Override - public List> search(SearchPossibleValueSourceInput input) throws QException + protected QTableMetaData getSourceObject(Serializable id) { - ///////////////////////////////////////////////////////////////////////////////////// - // build all of the possible values (note, will be filtered by user's permissions) // - ///////////////////////////////////////////////////////////////////////////////////// - List> allPossibleValues = new ArrayList<>(); + QTableMetaData table = QContext.getQInstance().getTable(ValueUtils.getValueAsString(id)); + return isTableAllowed(table) ? table : null; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected List getAllSourceObjects() + { + ArrayList rs = new ArrayList<>(); for(QTableMetaData table : QContext.getQInstance().getTables().values()) { - QPossibleValue possibleValue = getPossibleValue(table.getName()); - if(possibleValue != null) + if(isTableAllowed(table)) { - allPossibleValues.add(possibleValue); + rs.add(table); } } + return rs; + } - return completeCustomPVSSearch(input, allPossibleValues); + + /*************************************************************************** + ** + ***************************************************************************/ + private boolean isTableAllowed(QTableMetaData table) + { + if(table == null) + { + return (false); + } + + if(table.getIsHidden()) + { + return (false); + } + + PermissionCheckResult permissionCheckResult = PermissionsHelper.getPermissionCheckResult(new QueryInput(table.getName()), table); + if(!PermissionCheckResult.ALLOW.equals(permissionCheckResult)) + { + return (false); + } + + return (true); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java index 75087ae7..cdc9b6d3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; 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.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; @@ -45,6 +46,7 @@ public class TablesPossibleValueSourceMetaDataProvider { QPossibleValueSource possibleValueSource = new QPossibleValueSource() .withName(NAME) + .withIdType(QFieldType.STRING) .withType(QPossibleValueSourceType.CUSTOM) .withCustomCodeReference(new QCodeReference(TablesCustomPossibleValueProvider.class)) .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertExtractStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertExtractStep.java index dabfb3f4..694ae332 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertExtractStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertExtractStep.java @@ -55,8 +55,6 @@ public class BulkInsertExtractStep extends AbstractExtractStep @Override public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { - runBackendStepInput.traceMessage(BulkInsertStepUtils.getProcessTracerKeyRecordMessage(runBackendStepInput)); - int rowsAdded = 0; int originalLimit = Objects.requireNonNullElse(getLimit(), Integer.MAX_VALUE); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertLoadStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertLoadStep.java index 30f98c91..c60503fe 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertLoadStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertLoadStep.java @@ -41,6 +41,7 @@ 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.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface; +import com.kingsrook.qqq.backend.core.processes.implementations.general.ProcessSummaryWarningsAndErrorsRollup; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -77,19 +78,77 @@ public class BulkInsertLoadStep extends LoadViaInsertStep implements ProcessSumm QTableMetaData table = QContext.getQInstance().getTable(runBackendStepInput.getValueString("tableName")); + ///////////////////////////////////////////////////////////////////////////////////////////// + // the transform step builds summary lines that it predicts will insert successfully. // + // but those lines don't have ids, which we'd like to have (e.g., for a process trace that // + // might link to the built record). also, it's possible that there was a fail that only // + // happened in the actual insert, so, basically, re-do the summary here // + ///////////////////////////////////////////////////////////////////////////////////////////// + BulkInsertTransformStep transformStep = (BulkInsertTransformStep) getTransformStep(); + ProcessSummaryLine okSummary = transformStep.okSummary; + okSummary.setCount(0); + okSummary.setPrimaryKeys(new ArrayList<>()); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // but - since errors from the transform step don't even make it through to us here in the load step, // + // do re-use the ProcessSummaryWarningsAndErrorsRollup from transform step as follows: // + // clear out its warnings - we'll completely rebuild them here (with primary keys) // + // and add new error lines, e.g., in case of errors that only happened past the validation if possible. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = transformStep.processSummaryWarningsAndErrorsRollup; + processSummaryWarningsAndErrorsRollup.resetWarnings(); + List insertedRecords = runBackendStepOutput.getRecords(); for(QRecord insertedRecord : insertedRecords) { - if(CollectionUtils.nullSafeIsEmpty(insertedRecord.getErrors())) + Serializable primaryKey = insertedRecord.getValue(table.getPrimaryKeyField()); + if(CollectionUtils.nullSafeIsEmpty(insertedRecord.getErrors()) && primaryKey != null) { + ///////////////////////////////////////////////////////////////////////// + // if the record had no errors, and we have a primary key for it, then // + // keep track of the range of primary keys (first and last) // + ///////////////////////////////////////////////////////////////////////// if(firstInsertedPrimaryKey == null) { - firstInsertedPrimaryKey = insertedRecord.getValue(table.getPrimaryKeyField()); + firstInsertedPrimaryKey = primaryKey; } - lastInsertedPrimaryKey = insertedRecord.getValue(table.getPrimaryKeyField()); + lastInsertedPrimaryKey = primaryKey; + + if(!CollectionUtils.nullSafeIsEmpty(insertedRecord.getWarnings())) + { + ///////////////////////////////////////////////////////////////////////////// + // if there were warnings on the inserted record, put it in a warning line // + ///////////////////////////////////////////////////////////////////////////// + String message = insertedRecord.getWarnings().get(0).getMessage(); + processSummaryWarningsAndErrorsRollup.addWarning(message, primaryKey); + } + else + { + //////////////////////////////////////////////////////////////////////// + // if no warnings for the inserted record, then put it in the OK line // + //////////////////////////////////////////////////////////////////////// + okSummary.incrementCountAndAddPrimaryKey(primaryKey); + } + } + else + { + ////////////////////////////////////////////////////////////////////// + // else if there were errors or no primary key, build an error line // + ////////////////////////////////////////////////////////////////////// + String message = "Failed to insert"; + if(!CollectionUtils.nullSafeIsEmpty(insertedRecord.getErrors())) + { + ////////////////////////////////////////////////////////// + // use the error message from the record if we have one // + ////////////////////////////////////////////////////////// + message = insertedRecord.getErrors().get(0).getMessage(); + } + processSummaryWarningsAndErrorsRollup.addError(message, primaryKey); } } + + okSummary.pickMessage(true); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileMappingStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileMappingStep.java index c72df5fd..19e2dc79 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileMappingStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileMappingStep.java @@ -24,8 +24,12 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert; import java.io.File; import java.io.InputStream; +import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.tables.StorageAction; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -37,9 +41,14 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.mapp import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.mapping.BulkLoadTableStructureBuilder; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadFileRow; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile; +import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfileField; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadTableStructure; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.json.JSONObject; /******************************************************************************* @@ -72,7 +81,7 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep { @SuppressWarnings("unchecked") List headerValues = (List) runBackendStepOutput.getValue("headerValues"); - buildSuggestedMapping(headerValues, tableStructure, runBackendStepOutput); + buildSuggestedMapping(headerValues, getPrepopulatedValues(runBackendStepInput), tableStructure, runBackendStepOutput); } } @@ -81,10 +90,62 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep /*************************************************************************** ** ***************************************************************************/ - private void buildSuggestedMapping(List headerValues, BulkLoadTableStructure tableStructure, RunBackendStepOutput runBackendStepOutput) + private Map getPrepopulatedValues(RunBackendStepInput runBackendStepInput) + { + String prepopulatedValuesJson = runBackendStepInput.getValueString("prepopulatedValues"); + if(StringUtils.hasContent(prepopulatedValuesJson)) + { + Map rs = new LinkedHashMap<>(); + JSONObject jsonObject = JsonUtils.toJSONObject(prepopulatedValuesJson); + for(String key : jsonObject.keySet()) + { + rs.put(key, jsonObject.optString(key, null)); + } + return (rs); + } + + return (Collections.emptyMap()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void buildSuggestedMapping(List headerValues, Map prepopulatedValues, BulkLoadTableStructure tableStructure, RunBackendStepOutput runBackendStepOutput) { BulkLoadMappingSuggester bulkLoadMappingSuggester = new BulkLoadMappingSuggester(); BulkLoadProfile bulkLoadProfile = bulkLoadMappingSuggester.suggestBulkLoadMappingProfile(tableStructure, headerValues); + + if(CollectionUtils.nullSafeHasContents(prepopulatedValues)) + { + for(Map.Entry entry : prepopulatedValues.entrySet()) + { + String fieldName = entry.getKey(); + boolean foundFieldInProfile = false; + + for(BulkLoadProfileField bulkLoadProfileField : bulkLoadProfile.getFieldList()) + { + if(bulkLoadProfileField.getFieldName().equals(fieldName)) + { + foundFieldInProfile = true; + bulkLoadProfileField.setColumnIndex(null); + bulkLoadProfileField.setHeaderName(null); + bulkLoadProfileField.setDefaultValue(entry.getValue()); + break; + } + } + + if(!foundFieldInProfile) + { + BulkLoadProfileField bulkLoadProfileField = new BulkLoadProfileField(); + bulkLoadProfileField.setFieldName(fieldName); + bulkLoadProfileField.setDefaultValue(entry.getValue()); + bulkLoadProfile.getFieldList().add(bulkLoadProfileField); + } + } + } + runBackendStepOutput.addValue("bulkLoadProfile", bulkLoadProfile); runBackendStepOutput.addValue("suggestedBulkLoadProfile", bulkLoadProfile); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileUploadStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileUploadStep.java index ea4810a9..4b15b720 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileUploadStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileUploadStep.java @@ -52,6 +52,11 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep @Override public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { + //////////////////////////////////////////////////////////////////////////////////////// + // for headless-bulk load (e.g., sftp import), set up the process tracer's key record // + //////////////////////////////////////////////////////////////////////////////////////// + runBackendStepInput.traceMessage(BulkInsertStepUtils.getProcessTracerKeyRecordMessage(runBackendStepInput)); + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if user has come back here, clear out file (else the storageInput object that it is comes to the frontend, which isn't what we want!) // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertReceiveFileMappingStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertReceiveFileMappingStep.java index 94032fe5..aa460540 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertReceiveFileMappingStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertReceiveFileMappingStep.java @@ -46,6 +46,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.mode import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfileField; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.commons.lang3.BooleanUtils; @@ -77,10 +78,14 @@ public class BulkInsertReceiveFileMappingStep implements BackendStep ////////////////////////////////////////////////////////////////////////////// if(savedBulkLoadProfileRecord == null) { - throw (new QUserFacingException("Did not receive a saved bulk load profile record as input - unable to perform headless bulk load")); + throw (new QUserFacingException("Did not receive a Bulk Load Profile record as input. Unable to perform headless bulk load")); } SavedBulkLoadProfile savedBulkLoadProfile = new SavedBulkLoadProfile(savedBulkLoadProfileRecord); + if(!StringUtils.hasContent(savedBulkLoadProfile.getMappingJson())) + { + throw (new QUserFacingException("Bulk Load Profile record's Mapping is empty. Unable to perform headless bulk load")); + } try { @@ -88,7 +93,7 @@ public class BulkInsertReceiveFileMappingStep implements BackendStep } catch(Exception e) { - throw (new QUserFacingException("Error processing saved bulk load profile record - unable to perform headless bulk load", e)); + throw (new QUserFacingException("Error processing Bulk Load Profile record. Unable to perform headless bulk load", e)); } } else @@ -240,6 +245,11 @@ public class BulkInsertReceiveFileMappingStep implements BackendStep } } } + catch(QUserFacingException ufe) + { + LOG.warn("User-facing error in bulk insert receive mapping", ufe); + throw ufe; + } catch(Exception e) { LOG.warn("Error in bulk insert receive mapping", e); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java index 410fccf2..e3979a07 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertTransformStep.java @@ -75,9 +75,9 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils; *******************************************************************************/ public class BulkInsertTransformStep extends AbstractTransformStep { - private ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK); + ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK); - private ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("inserted") + ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("inserted") .withDoReplaceSingletonCountLinesWithSuffixOnly(false); private ListingHash errorToExampleRowValueMap = new ListingHash<>(); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/filehandling/XlsxFileToRows.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/filehandling/XlsxFileToRows.java index 289b90ee..0e543e86 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/filehandling/XlsxFileToRows.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/filehandling/XlsxFileToRows.java @@ -76,6 +76,23 @@ public class XlsxFileToRows extends AbstractIteratorBasedFileToRows sheet = workbook.getSheet(index); + + if(sheet.isEmpty()) + { + throw (new IOException("No sheet found for index: " + index)); + } + + rows = sheet.get().openStream(); + setIterator(rows.iterator()); + } + + /*************************************************************************** ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/BaseStreamedETLStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/BaseStreamedETLStep.java index 728d78f3..3b006bc5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/BaseStreamedETLStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/BaseStreamedETLStep.java @@ -53,7 +53,7 @@ public class BaseStreamedETLStep protected AbstractExtractStep getExtractStep(RunBackendStepInput runBackendStepInput) { QCodeReference codeReference = (QCodeReference) runBackendStepInput.getValue(StreamedETLWithFrontendProcess.FIELD_EXTRACT_CODE); - return (QCodeLoader.getBackendStep(AbstractExtractStep.class, codeReference)); + return (QCodeLoader.getAdHoc(AbstractExtractStep.class, codeReference)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java index cf405c4a..96ad93ad 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java @@ -22,13 +22,13 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend; -import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; /******************************************************************************* ** *******************************************************************************/ -public class CouldNotFindQueryFilterForExtractStepException extends QException +public class CouldNotFindQueryFilterForExtractStepException extends QUserFacingException { /******************************************************************************* ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java index 7269b75d..5391fe6b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java @@ -279,7 +279,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep return (new QQueryFilter().withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, idStrings))); } - throw (new CouldNotFindQueryFilterForExtractStepException("Could not find query filter for Extract step.")); + throw (new CouldNotFindQueryFilterForExtractStepException("No records were selected for running this process.")); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/ProcessSummaryWarningsAndErrorsRollup.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/ProcessSummaryWarningsAndErrorsRollup.java index 6296f31c..79dbe9ca 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/ProcessSummaryWarningsAndErrorsRollup.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/ProcessSummaryWarningsAndErrorsRollup.java @@ -195,7 +195,7 @@ public class ProcessSummaryWarningsAndErrorsRollup { if(otherWarningsSummary == null) { - otherWarningsSummary = new ProcessSummaryLine(Status.WARNING).withMessageSuffix("records had an other warning."); + otherWarningsSummary = buildOtherWarningsSummary(); } processSummaryLine = otherWarningsSummary; } @@ -214,6 +214,27 @@ public class ProcessSummaryWarningsAndErrorsRollup + /*************************************************************************** + ** + ***************************************************************************/ + private static ProcessSummaryLine buildOtherWarningsSummary() + { + return new ProcessSummaryLine(Status.WARNING).withMessageSuffix("records had an other warning."); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public void resetWarnings() + { + warningSummaries.clear(); + otherWarningsSummary = buildOtherWarningsSummary(); + } + + + /******************************************************************************* ** Wrapper around AlphaNumericComparator for ProcessSummaryLineInterface that ** extracts string messages out. diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java index 55ab7270..bc83b772 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java @@ -318,7 +318,7 @@ public class SavedReportToReportMetaDataAdapter /******************************************************************************* ** *******************************************************************************/ - private static QReportField makeQReportField(String fieldName, FieldAndJoinTable fieldAndJoinTable) + public static QReportField makeQReportField(String fieldName, FieldAndJoinTable fieldAndJoinTable) { QReportField reportField = new QReportField(); @@ -404,5 +404,5 @@ public class SavedReportToReportMetaDataAdapter /******************************************************************************* ** *******************************************************************************/ - private record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable) {} + public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable) {} } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/AbstractTableSyncTransformStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/AbstractTableSyncTransformStep.java index c8f6b82e..1565d3f3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/AbstractTableSyncTransformStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/AbstractTableSyncTransformStep.java @@ -356,12 +356,12 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt { if(existingRecord != null) { - LOG.info("Skipping storing existing record because this sync process is set to not perform updates"); + LOG.debug("Skipping storing existing record because this sync process is set to not perform updates"); willNotInsert.incrementCountAndAddPrimaryKey(sourcePrimaryKey); } else { - LOG.info("Skipping storing new record because this sync process is set to not perform inserts"); + LOG.debug("Skipping storing new record because this sync process is set to not perform inserts"); willNotUpdate.incrementCountAndAddPrimaryKey(sourcePrimaryKey); } continue; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java index 4d9f81e5..a29d9e24 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.ibm.icu.text.Transliterator; /******************************************************************************* @@ -463,6 +464,17 @@ public class StringUtils + /*************************************************************************** + ** + ***************************************************************************/ + public static String replaceNonAsciiCharacters(String s) + { + Transliterator transliterator = Transliterator.getInstance("Any-Latin; Latin-ASCII"); + return (transliterator.transliterate(s)); + } + + + /*************************************************************************** ** ***************************************************************************/ @@ -478,6 +490,51 @@ public class StringUtils + /*************************************************************************** + ** + ***************************************************************************/ + public static boolean safeEqualsIgnoreCase(String a, String b) + { + if(a == null && b == null) + { + return true; + } + if(a == null || b == null) + { + return false; + } + return (a.equalsIgnoreCase(b)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static String appendIncrementingSuffix(String input) + { + //////////////////////////////// + // remove any existing suffix // + //////////////////////////////// + String base = input.replaceAll(" \\(\\d+\\)$", ""); + if(input.matches(".* \\(\\d+\\)$")) + { + ////////////////////////// + // increment if matches // + ////////////////////////// + int current = Integer.parseInt(input.replaceAll(".* \\((\\d+)\\)$", "$1")); + return base + " (" + (current + 1) + ")"; + } + else + { + //////////////////////////////////// + // no match so put a 1 at the end // + //////////////////////////////////// + return base + " (1)"; + } + } + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Timer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Timer.java index 455b4cc8..0abe77cb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Timer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Timer.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.utils; +import java.time.Duration; import com.kingsrook.qqq.backend.core.logging.QLogger; import org.apache.logging.log4j.Level; @@ -79,9 +80,36 @@ public class Timer ** *******************************************************************************/ public void mark(String message) + { + mark(message, false); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void mark(String message, boolean prettyPrint) { long now = System.currentTimeMillis(); - LOG.log(level, String.format("%s: Last [%5d] Total [%5d] %s", name, (now - last), (now - start), message)); + + if(!prettyPrint) + { + LOG.log(level, String.format("%s: Last [%5d] Total [%5d] %s", name, (now - last), (now - start), message)); + } + else + { + + Duration lastDuration = Duration.ofMillis(now - last); + Duration totalDuration = Duration.ofMillis(now - start); + + LOG.log(level, String.format( + "%s: Last [%d hours, %d minutes, %d seconds, %d milliseconds] Total [%d hours, %d minutes, %d seconds, %d milliseconds] %s", + name, lastDuration.toHours(), lastDuration.toMinutesPart(), lastDuration.toSecondsPart(), lastDuration.toMillisPart(), + totalDuration.toHours(), totalDuration.toMinutesPart(), totalDuration.toSecondsPart(), totalDuration.toMillisPart(), + message)); + } + last = now; } } diff --git a/qqq-backend-core/src/main/resources/log4j2.xml b/qqq-backend-core/src/main/resources/log4j2.xml index e29de725..06e98981 100644 --- a/qqq-backend-core/src/main/resources/log4j2.xml +++ b/qqq-backend-core/src/main/resources/log4j2.xml @@ -18,13 +18,15 @@ - - - - - + + + + + + - + + diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoaderTest.java index 83cf8298..9c9d56a6 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoaderTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoaderTest.java @@ -22,12 +22,19 @@ package com.kingsrook.qqq.backend.core.actions.customizers; +import java.util.Map; import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.Timer; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; /******************************************************************************* @@ -80,6 +87,7 @@ class QCodeLoaderTest extends BaseTest } + /******************************************************************************* ** *******************************************************************************/ @@ -91,4 +99,50 @@ class QCodeLoaderTest extends BaseTest } } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCodeReferenceWithProperties() + { + assertNull(QCodeLoader.getAdHoc(SomeClass.class, new QCodeReference(SomeClass.class))); + + SomeClass someObject = QCodeLoader.getAdHoc(SomeClass.class, new QCodeReferenceWithProperties(SomeClass.class, Map.of("property", "someValue"))); + assertEquals("someValue", someObject.someProperty); + + SomeClass someOtherObject = QCodeLoader.getAdHoc(SomeClass.class, new QCodeReferenceWithProperties(SomeClass.class, Map.of("property", "someOtherValue"))); + assertEquals("someOtherValue", someOtherObject.someProperty); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class SomeClass implements InitializableViaCodeReference + { + private String someProperty; + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void initialize(QCodeReference codeReference) + { + if(codeReference instanceof QCodeReferenceWithProperties codeReferenceWithProperties) + { + someProperty = ValueUtils.getValueAsString(codeReferenceWithProperties.getProperties().get("property")); + } + + if(!StringUtils.hasContent(someProperty)) + { + throw new IllegalStateException("Missing property"); + } + } + } + } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java index 58d85069..4d75312d 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java @@ -364,6 +364,7 @@ class MetaDataActionTest extends BaseTest ** *******************************************************************************/ @Test + @Deprecated(since = "migrated to metaDataCustomizer") void testFilter() throws QException { QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(MetaDataAction.class); @@ -397,7 +398,7 @@ class MetaDataActionTest extends BaseTest // run again (with the same instance as before) to assert about memoization of the filter based on the QInstance // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// new MetaDataAction().execute(new MetaDataInput()); - assertThat(collectingLogger.getCollectedMessages()).filteredOn(clm -> clm.getMessage().contains("filter of type: DenyAllFilter")).hasSize(1); + assertThat(collectingLogger.getCollectedMessages()).filteredOn(clm -> clm.getMessage().contains("actionCustomizer (via metaDataFilter reference) of type: DenyAllFilter")).hasSize(1); QLogger.deactivateCollectingLoggerForClass(MetaDataAction.class); @@ -413,6 +414,59 @@ class MetaDataActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCustomizer() throws QException + { + QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(MetaDataAction.class); + + ////////////////////////////////////////////////////// + // run default version, and assert tables are found // + ////////////////////////////////////////////////////// + MetaDataOutput result = new MetaDataAction().execute(new MetaDataInput()); + assertFalse(result.getTables().isEmpty(), "should be some tables"); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // run again (with the same instance as before) to assert about memoization of the filter based on the QInstance // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + new MetaDataAction().execute(new MetaDataInput()); + assertThat(collectingLogger.getCollectedMessages()).filteredOn(clm -> clm.getMessage().contains("Using new default")).hasSize(1); + + ///////////////////////////////////////////////////////////// + // set up new instance to use a custom filter, to deny all // + ///////////////////////////////////////////////////////////// + QInstance instance = TestUtils.defineInstance(); + instance.setMetaDataActionCustomizer(new QCodeReference(DenyAllFilteringCustomizer.class)); + reInitInstanceInContext(instance); + + ///////////////////////////////////////////////////// + // re-run, and assert all tables are filtered away // + ///////////////////////////////////////////////////// + result = new MetaDataAction().execute(new MetaDataInput()); + assertTrue(result.getTables().isEmpty(), "should be no tables"); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // run again (with the same instance as before) to assert about memoization of the filter based on the QInstance // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + new MetaDataAction().execute(new MetaDataInput()); + assertThat(collectingLogger.getCollectedMessages()).filteredOn(clm -> clm.getMessage().contains("meta-data actionCustomizer of type: DenyAllFilteringCustomizer")).hasSize(1); + + QLogger.deactivateCollectingLoggerForClass(MetaDataAction.class); + + ///////////////////////////////////////////////////////////////////////////////// + // run now with the DefaultNoopMetaDataActionCustomizer, confirm we get tables // + ///////////////////////////////////////////////////////////////////////////////// + instance = TestUtils.defineInstance(); + instance.setMetaDataActionCustomizer(new QCodeReference(DefaultNoopMetaDataActionCustomizer.class)); + reInitInstanceInContext(instance); + result = new MetaDataAction().execute(new MetaDataInput()); + assertFalse(result.getTables().isEmpty(), "should be some tables"); + } + + + /*************************************************************************** ** ***************************************************************************/ @@ -462,6 +516,67 @@ class MetaDataActionTest extends BaseTest + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget) + { + return false; + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class DenyAllFilteringCustomizer implements MetaDataActionCustomizerInterface + { + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowTable(MetaDataInput input, QTableMetaData table) + { + return false; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowProcess(MetaDataInput input, QProcessMetaData process) + { + return false; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowReport(MetaDataInput input, QReportMetaData report) + { + return false; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowApp(MetaDataInput input, QAppMetaData app) + { + return false; + } + + + /*************************************************************************** ** ***************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java index c2594727..48d75f5e 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java @@ -38,6 +38,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.reporting.excel.TestExcelStyler; import com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel.ExcelFastexcelExportStreamer; import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.BoldHeaderAndFooterPoiExcelStyler; import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.ExcelPoiBasedStreamingExportStreamer; @@ -56,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; 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; @@ -490,6 +492,34 @@ public class GenerateReportActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void runXlsxWithStyleCustomizer() throws Exception + { + ReportFormat format = ReportFormat.XLSX; + String name = "/tmp/report-customized.xlsx"; + try(FileOutputStream fileOutputStream = new FileOutputStream(name)) + { + QInstance qInstance = QContext.getQInstance(); + qInstance.addReport(defineTableOnlyReport()); + insertPersonRecords(qInstance); + + ReportInput reportInput = new ReportInput(); + reportInput.setReportName(REPORT_NAME); + reportInput.setReportDestination(new ReportDestination().withReportFormat(format).withReportOutputStream(fileOutputStream)); + reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now())); + reportInput.setExportStyleCustomizer(new QCodeReference(TestExcelStyler.class)); + new GenerateReportAction().execute(reportInput); + System.out.println("Wrote File: " + name); + + LocalMacDevUtils.openFile(name); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/TestExcelStyler.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/TestExcelStyler.java new file mode 100644 index 00000000..332345dd --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/TestExcelStyler.java @@ -0,0 +1,76 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.reporting.excel; + + +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.ExcelPoiBasedStreamingStyleCustomizerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TestExcelStyler implements ExcelPoiBasedStreamingStyleCustomizerInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List getColumnWidthsForView(QReportView view) + { + return List.of(60, 50, 40); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List getMergedRangesForView(QReportView view) + { + return List.of("A1:B1"); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void customizeStyles(Map styles, XSSFWorkbook workbook, CreationHelper createHelper) + { + Font font = workbook.createFont(); + font.setFontHeightInPoints((short) 16); + font.setBold(true); + XSSFCellStyle cellStyle = workbook.createCellStyle(); + cellStyle.setFont(font); + styles.put("header", cellStyle); + } +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java index 4249275c..272a7ac4 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java @@ -79,6 +79,7 @@ class ConvertHtmlToPdfActionTest extends BaseTest

This is a test of converting HTML to PDF!!

+

This is   a line with • some entities <

(btw, is this in SF-Pro???)

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 8419b05a..d790ca59 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 @@ -40,6 +40,7 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarCh import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ParentWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.metadata.AllowAllMetaDataFilter; +import com.kingsrook.qqq.backend.core.actions.metadata.DefaultNoopMetaDataActionCustomizer; import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessActionTest; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface; @@ -160,6 +161,20 @@ public class QInstanceValidatorTest extends BaseTest } + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMetaDataActionCustomizer() + { + assertValidationFailureReasons((qInstance) -> qInstance.setMetaDataActionCustomizer(new QCodeReference(QInstanceValidator.class)), + "Instance metaDataActionCustomizer CodeReference is not of the expected type"); + + assertValidationSuccess((qInstance) -> qInstance.setMetaDataActionCustomizer(new QCodeReference(DefaultNoopMetaDataActionCustomizer.class))); + assertValidationSuccess((qInstance) -> qInstance.setMetaDataActionCustomizer(null)); + } + + /******************************************************************************* ** Test an instance with null backends - should throw. diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLineInterfaceAssert.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLineInterfaceAssert.java index 60d4561c..59b79181 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLineInterfaceAssert.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLineInterfaceAssert.java @@ -185,4 +185,13 @@ public class ProcessSummaryLineInterfaceAssert extends AbstractAssert Order.getTableName(Order.class)); + } + } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertFullProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertFullProcessTest.java index 2fe422fd..cb11631b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertFullProcessTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertFullProcessTest.java @@ -29,15 +29,26 @@ import java.util.List; import java.util.Map; import java.util.UUID; import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.tables.StorageAction; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryAssert; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.processes.Status; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData; +import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; +import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfileField; @@ -176,7 +187,15 @@ class BulkInsertFullProcessTest extends BaseTest assertThat(runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY)).isNotNull().isInstanceOf(List.class); assertThat(runProcessOutput.getException()).isEmpty(); - ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("Inserted Id values between 1 and 2"); + ProcessSummaryLineInterface okLine = ProcessSummaryAssert.assertThat(runProcessOutput) + .hasLineWithMessageContaining("Person Memory records were inserted") + .hasStatus(Status.OK) + .hasCount(2) + .getLine(); + assertEquals(List.of(1, 2), ((ProcessSummaryLine) okLine).getPrimaryKeys()); + + ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("records were processed from the file").hasStatus(Status.INFO); + ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("Inserted Id values between 1 and 2").hasStatus(Status.INFO); //////////////////////////////////// // query for the inserted records // @@ -201,6 +220,86 @@ class BulkInsertFullProcessTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSummaryLinePrimaryKeys() throws Exception + { + assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isEmpty(); + QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(PersonWarnOrErrorCustomizer.class)); + + ///////////////////////////////////////////////////////// + // start the process - expect to go to the upload step // + ///////////////////////////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + RunProcessOutput runProcessOutput = startProcess(runProcessInput); + String processUUID = runProcessOutput.getProcessUUID(); + + continueProcessPostUpload(runProcessInput, processUUID, simulateFileUploadForWarningCase()); + continueProcessPostFileMapping(runProcessInput); + continueProcessPostValueMapping(runProcessInput); + runProcessOutput = continueProcessPostReviewScreen(runProcessInput); + + ProcessSummaryLineInterface okLine = ProcessSummaryAssert.assertThat(runProcessOutput) + .hasLineWithMessageContaining("Person Memory record was inserted") + .hasStatus(Status.OK) + .hasCount(1) + .getLine(); + assertEquals(List.of(1), ((ProcessSummaryLine) okLine).getPrimaryKeys()); + + ProcessSummaryLineInterface warnTornadoLine = ProcessSummaryAssert.assertThat(runProcessOutput) + .hasLineWithMessageContaining("records were inserted, but had a warning: Tornado warning") + .hasStatus(Status.WARNING) + .hasCount(2) + .getLine(); + assertEquals(List.of(2, 3), ((ProcessSummaryLine) warnTornadoLine).getPrimaryKeys()); + + ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("record was inserted, but had a warning: Hurricane warning").hasStatus(Status.WARNING).hasCount(1); + ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("records were processed from the file").hasStatus(Status.INFO).hasCount(4); + ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("Inserted Id values between 1 and 4").hasStatus(Status.INFO); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSummaryLineErrors() throws Exception + { + assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isEmpty(); + QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(PersonWarnOrErrorCustomizer.class)); + + ///////////////////////////////////////////////////////// + // start the process - expect to go to the upload step // + ///////////////////////////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + RunProcessOutput runProcessOutput = startProcess(runProcessInput); + String processUUID = runProcessOutput.getProcessUUID(); + + continueProcessPostUpload(runProcessInput, processUUID, simulateFileUploadForErrorCase()); + continueProcessPostFileMapping(runProcessInput); + continueProcessPostValueMapping(runProcessInput); + runProcessOutput = continueProcessPostReviewScreen(runProcessInput); + + ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("Person Memory record was inserted.").hasStatus(Status.OK).hasCount(1); + + ProcessSummaryAssert.assertThat(runProcessOutput) + .hasLineWithMessageContaining("plane") + .hasStatus(Status.ERROR) + .hasCount(1); + + ProcessSummaryAssert.assertThat(runProcessOutput) + .hasLineWithMessageContaining("purifier") + .hasStatus(Status.ERROR) + .hasCount(1); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -301,6 +400,47 @@ class BulkInsertFullProcessTest extends BaseTest + /*************************************************************************** + ** + ***************************************************************************/ + private static StorageInput simulateFileUploadForWarningCase() throws Exception + { + String storageReference = UUID.randomUUID() + ".csv"; + StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(storageReference); + try(OutputStream outputStream = new StorageAction().createOutputStream(storageInput)) + { + outputStream.write((getPersonCsvHeaderUsingLabels() + """ + "0","2021-10-26 14:39:37","2021-10-26 14:39:37","John","Doe","1980-01-01","john@doe.com","Missouri",42 + "0","2021-10-26 14:39:37","2021-10-26 14:39:37","Tornado warning","Doe","1980-01-01","john@doe.com","Missouri",42 + "0","2021-10-26 14:39:37","2021-10-26 14:39:37","Tornado warning","Doey","1980-01-01","john@doe.com","Missouri",42 + "0","2021-10-26 14:39:37","2021-10-26 14:39:37","Hurricane warning","Doe","1980-01-01","john@doe.com","Missouri",42 + """).getBytes()); + } + return storageInput; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private static StorageInput simulateFileUploadForErrorCase() throws Exception + { + String storageReference = UUID.randomUUID() + ".csv"; + StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(storageReference); + try(OutputStream outputStream = new StorageAction().createOutputStream(storageInput)) + { + outputStream.write((getPersonCsvHeaderUsingLabels() + """ + "0","2021-10-26 14:39:37","2021-10-26 14:39:37","John","Doe","1980-01-01","john@doe.com","Missouri",42 + "0","2021-10-26 14:39:37","2021-10-26 14:39:37","not-pre-Error plane","Doe","1980-01-01","john@doe.com","Missouri",42 + "0","2021-10-26 14:39:37","2021-10-26 14:39:37","Error purifier","Doe","1980-01-01","john@doe.com","Missouri",42 + """).getBytes()); + } + return storageInput; + } + + + /*************************************************************************** ** ***************************************************************************/ @@ -331,4 +471,47 @@ class BulkInsertFullProcessTest extends BaseTest ))); } + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class PersonWarnOrErrorCustomizer implements TableCustomizerInterface + { + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public AbstractPreInsertCustomizer.WhenToRun whenToRunPreInsert(InsertInput insertInput, boolean isPreview) + { + return AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List preInsert(InsertInput insertInput, List records, boolean isPreview) throws QException + { + for(QRecord record : records) + { + if(record.getValueString("firstName").toLowerCase().contains("warn")) + { + record.addWarning(new QWarningMessage(record.getValueString("firstName"))); + } + else if(record.getValueString("firstName").toLowerCase().contains("error")) + { + if(isPreview && record.getValueString("firstName").toLowerCase().contains("not-pre-error")) + { + continue; + } + + record.addError(new BadInputStatusMessage(record.getValueString("firstName"))); + } + } + return records; + } + } } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileMappingStepTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileMappingStepTest.java index ea6e0e8f..c633c920 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileMappingStepTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertPrepareFileMappingStepTest.java @@ -22,8 +22,22 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Optional; import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.StorageAction; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput; +import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile; +import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfileField; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -66,4 +80,39 @@ class BulkInsertPrepareFileMappingStepTest extends BaseTest assertEquals("BAA", BulkInsertPrepareFileMappingStep.toHeaderLetter(2 * 26 * 26 + 26 + 0)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws Exception + { + String fileName = "personFile.csv"; + + StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(fileName); + OutputStream outputStream = new StorageAction().createOutputStream(storageInput); + outputStream.write(""" + name,noOfShoes + John,2 + Jane,4 + """.getBytes(StandardCharsets.UTF_8)); + outputStream.close(); + + RunProcessInput runProcessInput = new RunProcessInput(); + BulkInsertStepUtils.setStorageInputForTheFile(runProcessInput, storageInput); + runProcessInput.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY); + runProcessInput.addValue("prepopulatedValues", JsonUtils.toJson(Map.of("homeStateId", 1))); + + RunBackendStepInput runBackendStepInput = new RunBackendStepInput(runProcessInput.getProcessState()); + RunBackendStepOutput runBackendStepOutput = new RunBackendStepOutput(); + + new BulkInsertPrepareFileMappingStep().run(runBackendStepInput, runBackendStepOutput); + + BulkLoadProfile bulkLoadProfile = (BulkLoadProfile) runBackendStepOutput.getValue("suggestedBulkLoadProfile"); + Optional homeStateId = bulkLoadProfile.getFieldList().stream().filter(f -> f.getFieldName().equals("homeStateId")).findFirst(); + assertThat(homeStateId).isPresent(); + assertEquals("1", homeStateId.get().getDefaultValue()); + } + } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java index 5b27cb3b..391c9a77 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java @@ -336,6 +336,40 @@ class StringUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAppendIncrementingSuffix() + { + assertEquals("test (1)", StringUtils.appendIncrementingSuffix("test")); + assertEquals("test (2)", StringUtils.appendIncrementingSuffix("test (1)")); + assertEquals("test (a) (1)", StringUtils.appendIncrementingSuffix("test (a)")); + assertEquals("test (a32) (1)", StringUtils.appendIncrementingSuffix("test (a32)")); + assertEquals("test ((2)) (1)", StringUtils.appendIncrementingSuffix("test ((2))")); + assertEquals("test ((2)) (101)", StringUtils.appendIncrementingSuffix("test ((2)) (100)")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSafeEqualsIgnoreCase() + { + assertTrue(StringUtils.safeEqualsIgnoreCase(null, null)); + assertFalse(StringUtils.safeEqualsIgnoreCase("a", null)); + assertFalse(StringUtils.safeEqualsIgnoreCase(null, "a")); + assertTrue(StringUtils.safeEqualsIgnoreCase("a", "a")); + assertTrue(StringUtils.safeEqualsIgnoreCase("A", "a")); + assertFalse(StringUtils.safeEqualsIgnoreCase("a", "b")); + assertTrue(StringUtils.safeEqualsIgnoreCase("timothy d. chamberlain", "TIMOThy d. chaMberlain")); + assertTrue(StringUtils.safeEqualsIgnoreCase("timothy d. chamberlain", "timothy d. chamberlain")); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java index ee1f69f1..6738ac31 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java @@ -362,4 +362,4 @@ class ValueUtilsTest extends BaseTest assertEquals(QFieldType.TIME, ValueUtils.inferQFieldTypeFromValue(LocalTime.now(), null)); } -} \ No newline at end of file +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java index c4b478ab..bdb45aea 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java @@ -39,6 +39,7 @@ import java.util.function.Supplier; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; @@ -208,6 +209,11 @@ public class BaseAPIActionUtil return rs; } + catch(QNotFoundException qnfe) + { + LOG.info("Not found", qnfe); + throw (qnfe); + } catch(Exception e) { LOG.error("Error in API get", e); @@ -737,6 +743,10 @@ public class BaseAPIActionUtil case OAUTH2 -> request.setHeader("Authorization", "Bearer " + getOAuth2Token()); case API_KEY_QUERY_PARAM -> addApiKeyQueryParamToRequest(request); case CUSTOM -> handleCustomAuthorization(request); + case NONE -> + { + /* nothing to do here */ + } default -> throw new IllegalArgumentException("Unexpected authorization type: " + backendMetaData.getAuthorizationType()); } } @@ -1168,6 +1178,16 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** + *******************************************************************************/ + protected QHttpResponse getQHttpResponse(HttpResponse response) throws Exception + { + return (new QHttpResponse(response)); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1201,7 +1221,7 @@ public class BaseAPIActionUtil try(CloseableHttpResponse response = executeHttpRequest(request, httpClient)) { - QHttpResponse qResponse = new QHttpResponse(response); + QHttpResponse qResponse = getQHttpResponse(response); logOutboundApiCall(request, qResponse); diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/QHttpResponse.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/QHttpResponse.java index bb32cfd7..72069b3e 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/QHttpResponse.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/QHttpResponse.java @@ -40,6 +40,7 @@ public class QHttpResponse private String statusReasonPhrase; private List
headerList; private String content; + private byte[] contentBytes; @@ -53,11 +54,47 @@ public class QHttpResponse + /******************************************************************************* + ** Constructor for QHttpResponse that allows reading content as bytes + ** + *******************************************************************************/ + public QHttpResponse(HttpResponse httpResponse, boolean readContentAsBytes) throws Exception + { + if(!readContentAsBytes) + { + new QHttpResponse(httpResponse); + return; + } + + setGeneralHttpResponseData(httpResponse); + if(this.statusCode == null || this.statusCode != 204) + { + this.contentBytes = httpResponse.getEntity().getContent().readAllBytes(); + } + } + + + /******************************************************************************* ** Constructor for qHttpResponse ** *******************************************************************************/ public QHttpResponse(HttpResponse httpResponse) throws Exception + { + setGeneralHttpResponseData(httpResponse); + if(this.statusCode == null || this.statusCode != 204) + { + this.content = EntityUtils.toString(httpResponse.getEntity()); + } + } + + + + /******************************************************************************* + ** Sets data into this entity from an HttpResponse but doesnt read response data + ** + *******************************************************************************/ + private void setGeneralHttpResponseData(HttpResponse httpResponse) throws Exception { this.headerList = Arrays.asList(httpResponse.getAllHeaders()); if(httpResponse.getStatusLine() != null) @@ -69,11 +106,6 @@ public class QHttpResponse this.statusProtocolVersion = httpResponse.getStatusLine().getProtocolVersion().toString(); } } - - if(this.statusCode == null || this.statusCode != 204) - { - this.content = EntityUtils.toString(httpResponse.getEntity()); - } } @@ -242,4 +274,35 @@ public class QHttpResponse return (this); } + + + /******************************************************************************* + ** Getter for contentBytes + *******************************************************************************/ + public byte[] getContentBytes() + { + return (this.contentBytes); + } + + + + /******************************************************************************* + ** Setter for contentBytes + *******************************************************************************/ + public void setContentBytes(byte[] contentBytes) + { + this.contentBytes = contentBytes; + } + + + + /******************************************************************************* + ** Fluent setter for contentBytes + *******************************************************************************/ + public QHttpResponse withContentBytes(byte[] contentBytes) + { + this.contentBytes = contentBytes; + return (this); + } + } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java index 9a4f750c..614df755 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java @@ -33,5 +33,6 @@ public enum AuthorizationType BASIC_AUTH_USERNAME_PASSWORD, CUSTOM, OAUTH2, + NONE, API_KEY_QUERY_PARAM, } 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 db3d3d05..f6acc377 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 @@ -59,9 +59,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantSett import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsUtil; import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage; import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.ObjectUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier; import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields; import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractFilesystemBackendMetaData; @@ -124,10 +126,19 @@ public abstract class AbstractBaseFilesystemAction *******************************************************************************/ public abstract InputStream readFile(FILE file) throws IOException; + /*************************************************************************** + ** Legacy signature for this method - before table & record params were added. + ***************************************************************************/ + @Deprecated(since = "call the overload that takes table and record") + public void writeFile(QBackendMetaData backend, String path, byte[] contents) throws IOException + { + writeFile(backend, null, null, path, contents); + } + /******************************************************************************* ** Write a file - to be implemented in module-specific subclasses. *******************************************************************************/ - public abstract void writeFile(QBackendMetaData backend, String path, byte[] contents) throws IOException; + public abstract void writeFile(QBackendMetaData backend, QTableMetaData table, QRecord record, String path, byte[] contents) throws IOException; /******************************************************************************* ** Get a string that represents the full path to a file. @@ -287,8 +298,23 @@ public abstract class AbstractBaseFilesystemAction QueryOutput queryOutput = new QueryOutput(queryInput); - String requestedPath = null; - List files = listFiles(table, queryInput.getBackend(), requestedPath); + String requestedPath = null; + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if this is a query for a single file name, then get that file name in the requestedPath param for the listFiles call // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(queryInput.getFilter() != null) + { + for(QFilterCriteria criteria : CollectionUtils.nonNullList(queryInput.getFilter().getCriteria())) + { + if(criteria.getFieldName().equals(tableDetails.getFileNameFieldName()) && criteria.getOperator().equals(QCriteriaOperator.EQUALS)) + { + requestedPath = ValueUtils.getValueAsString(criteria.getValues().get(0)); + } + } + } + + List files = listFiles(table, queryInput.getBackend(), requestedPath); switch(tableDetails.getCardinality()) { @@ -632,7 +658,7 @@ public abstract class AbstractBaseFilesystemAction try { String fullPath = stripDuplicatedSlashes(getFullBasePath(table, backend) + File.separator + record.getValueString(tableDetails.getFileNameFieldName())); - writeFile(backend, fullPath, record.getValueByteArray(tableDetails.getContentsFieldName())); + writeFile(backend, table, record, fullPath, record.getValueByteArray(tableDetails.getContentsFieldName())); record.addBackendDetail(FilesystemRecordBackendDetailFields.FULL_PATH, fullPath); output.addRecord(record); } diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/AbstractFilesystemAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/AbstractFilesystemAction.java index cb7d9255..ea0f6880 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/AbstractFilesystemAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/AbstractFilesystemAction.java @@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -204,7 +205,7 @@ public class AbstractFilesystemAction extends AbstractBaseFilesystemAction ** Write a file - to be implemented in module-specific subclasses. *******************************************************************************/ @Override - public void writeFile(QBackendMetaData backend, String path, byte[] contents) throws IOException + public void writeFile(QBackendMetaData backend, QTableMetaData table, QRecord record, String path, byte[] contents) throws IOException { FileUtils.writeByteArrayToFile(new File(path), contents); } diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/importer/FilesystemImporterStep.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/importer/FilesystemImporterStep.java index bc0c3ec2..89402ab0 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/importer/FilesystemImporterStep.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/importer/FilesystemImporterStep.java @@ -363,7 +363,7 @@ public class FilesystemImporterStep implements BackendStep path = AbstractBaseFilesystemAction.stripDuplicatedSlashes(path); LOG.info("Archiving file", logPair("path", path), logPair("archiveBackendName", archiveBackend.getName()), logPair("archiveTableName", archiveTable.getName())); - archiveActionBase.writeFile(archiveBackend, path, bytes); + archiveActionBase.writeFile(archiveBackend, archiveTable, null, path, bytes); return (path); } 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 80c93181..80041e0d 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 @@ -111,10 +111,10 @@ public class FilesystemSyncStep implements BackendStep byte[] bytes = inputStream.readAllBytes(); String archivePath = archiveActionBase.getFullBasePath(archiveTable, archiveBackend); - archiveActionBase.writeFile(archiveBackend, archivePath + File.separator + sourceFileName, bytes); + archiveActionBase.writeFile(archiveBackend, archiveTable, null, archivePath + File.separator + sourceFileName, bytes); String processingPath = processingActionBase.getFullBasePath(processingTable, processingBackend); - processingActionBase.writeFile(processingBackend, processingPath + File.separator + sourceFileName, bytes); + processingActionBase.writeFile(processingBackend, processingTable, null, processingPath + File.separator + sourceFileName, bytes); syncedFileCount++; if(maxFilesToSync != null && syncedFileCount >= maxFilesToSync) diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/AbstractS3Action.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/AbstractS3Action.java index 3b8bcb09..03aa8d21 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/AbstractS3Action.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/AbstractS3Action.java @@ -24,8 +24,10 @@ package com.kingsrook.qqq.backend.module.filesystem.s3.actions; import java.io.IOException; import java.io.InputStream; +import java.net.URLConnection; import java.time.Instant; import java.util.List; +import java.util.Objects; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.s3.AmazonS3; @@ -34,6 +36,7 @@ import com.amazonaws.services.s3.model.S3ObjectSummary; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -42,6 +45,7 @@ import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFile import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractFilesystemTableBackendDetails; import com.kingsrook.qqq.backend.module.filesystem.exceptions.FilesystemException; import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3BackendMetaData; +import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3TableBackendDetails; import com.kingsrook.qqq.backend.module.filesystem.s3.utils.S3Utils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -195,14 +199,27 @@ public class AbstractS3Action extends AbstractBaseFilesystemAction URLConnection.guessContentTypeFromName(path); + case FROM_FIELD -> record == null ? null : record.getValueString(s3TableBackendDetails.getContentTypeFieldName()); + case HARDCODED -> s3TableBackendDetails.getHardcodedContentType(); + case NONE -> null; + }; + } + + getS3Utils().writeFile(bucketName, path, contents, contentType); } catch(Exception e) { @@ -277,5 +294,4 @@ public class AbstractS3Action extends AbstractBaseFilesystemAction + { + qInstanceValidator.assertCondition(!StringUtils.hasContent(hardcodedContentType), prefix + "hardcodedContentType should not be set when contentTypeStrategy is " + contentTypeStrategy); + + if(table != null && qInstanceValidator.assertCondition(StringUtils.hasContent(contentTypeFieldName), prefix + "contentTypeFieldName must be set when contentTypeStrategy is " + contentTypeStrategy)) + { + qInstanceValidator.assertCondition(table.getFields().containsKey(contentTypeFieldName), prefix + "contentTypeFieldName must be a valid field name in the table"); + } + } + case HARDCODED -> + { + qInstanceValidator.assertCondition(!StringUtils.hasContent(contentTypeFieldName), prefix + "contentTypeFieldName should not be set when contentTypeStrategy is " + contentTypeStrategy); + qInstanceValidator.assertCondition(StringUtils.hasContent(hardcodedContentType), prefix + "hardcodedContentType must be set when contentTypeStrategy is " + contentTypeStrategy); + } + case BASED_ON_FILE_NAME, NONE -> + { + qInstanceValidator.assertCondition(!StringUtils.hasContent(contentTypeFieldName), prefix + "contentTypeFieldName should not be set when contentTypeStrategy is " + contentTypeStrategy); + qInstanceValidator.assertCondition(!StringUtils.hasContent(hardcodedContentType), prefix + "hardcodedContentType should not be set when contentTypeStrategy is " + contentTypeStrategy); + } + default -> + { + throw new IllegalStateException("Unexpected value: " + contentTypeStrategy); + } + } + } + + /******************************************************************************* + ** Getter for contentTypeStrategy + *******************************************************************************/ + public ContentTypeStrategy getContentTypeStrategy() + { + return (this.contentTypeStrategy); + } + + + + /******************************************************************************* + ** Setter for contentTypeStrategy + *******************************************************************************/ + public void setContentTypeStrategy(ContentTypeStrategy contentTypeStrategy) + { + this.contentTypeStrategy = contentTypeStrategy; + } + + + + /******************************************************************************* + ** Fluent setter for contentTypeStrategy + *******************************************************************************/ + public S3TableBackendDetails withContentTypeStrategy(ContentTypeStrategy contentTypeStrategy) + { + this.contentTypeStrategy = contentTypeStrategy; + return (this); + } + + + + /******************************************************************************* + ** Getter for contentTypeFieldName + *******************************************************************************/ + public String getContentTypeFieldName() + { + return (this.contentTypeFieldName); + } + + + + /******************************************************************************* + ** Setter for contentTypeFieldName + *******************************************************************************/ + public void setContentTypeFieldName(String contentTypeFieldName) + { + this.contentTypeFieldName = contentTypeFieldName; + } + + + + /******************************************************************************* + ** Fluent setter for contentTypeFieldName + *******************************************************************************/ + public S3TableBackendDetails withContentTypeFieldName(String contentTypeFieldName) + { + this.contentTypeFieldName = contentTypeFieldName; + return (this); + } + + + + /******************************************************************************* + ** Getter for hardcodedContentType + *******************************************************************************/ + public String getHardcodedContentType() + { + return (this.hardcodedContentType); + } + + + + /******************************************************************************* + ** Setter for hardcodedContentType + *******************************************************************************/ + public void setHardcodedContentType(String hardcodedContentType) + { + this.hardcodedContentType = hardcodedContentType; + } + + + + /******************************************************************************* + ** Fluent setter for hardcodedContentType + *******************************************************************************/ + public S3TableBackendDetails withHardcodedContentType(String hardcodedContentType) + { + this.hardcodedContentType = hardcodedContentType; + return (this); + } + + } diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3Utils.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3Utils.java index 5f3b65d3..025e742a 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3Utils.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3Utils.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.module.filesystem.s3.utils; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.URI; +import java.net.URLEncoder; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.PathMatcher; @@ -175,7 +176,7 @@ public class S3Utils /////////////////////////////////////////// // skip files that do not match the glob // /////////////////////////////////////////// - if(!pathMatcher.matches(Path.of(URI.create("file:///" + key)))) + if(!pathMatcher.matches(Path.of(URI.create("file:///" + URLEncoder.encode(key))))) { // LOG.debug("Skipping file [{}] that does not match glob [{}]", key, glob); continue; @@ -204,10 +205,11 @@ public class S3Utils /******************************************************************************* ** Write a file *******************************************************************************/ - public void writeFile(String bucket, String key, byte[] contents) + public void writeFile(String bucket, String key, byte[] contents, String contentType) { ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(contents.length); + objectMetadata.setContentType(contentType); getAmazonS3().putObject(bucket, key, new ByteArrayInputStream(contents), objectMetadata); } diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/AbstractSFTPAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/AbstractSFTPAction.java index 8d5330ff..507012d3 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/AbstractSFTPAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/sftp/actions/AbstractSFTPAction.java @@ -325,20 +325,51 @@ public class AbstractSFTPAction extends AbstractBaseFilesystemAction record.getBackendDetailString(FilesystemRecordBackendDetailFields.FULL_PATH).contains("blobs")); @@ -73,6 +73,65 @@ public class S3InsertActionTest extends BaseS3Test S3Object object = getAmazonS3().getObject(BUCKET_NAME, fullPath); List lines = IOUtils.readLines(object.getObjectContent(), StandardCharsets.UTF_8); assertEquals("Hi, Bob.", lines.get(0)); + + ObjectMetadata objectMetadata = object.getObjectMetadata(); + assertEquals("text/plain", objectMetadata.getContentType()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testContentTypeFromField() throws QException + { + ((S3TableBackendDetails) QContext.getQInstance().getTable(TestUtils.TABLE_NAME_BLOB_S3) + .getBackendDetails()) + .withContentTypeStrategy(S3TableBackendDetails.ContentTypeStrategy.FROM_FIELD) + .withContentTypeFieldName("contentType"); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_BLOB_S3); + insertInput.setRecords(List.of( + new QRecord().withValue("fileName", "file2.txt").withValue("contentType", "myContentType/fake").withValue("contents", "Hi, Bob."))); + + S3InsertAction insertAction = new S3InsertAction(); + insertAction.setS3Utils(getS3Utils()); + InsertOutput insertOutput = insertAction.execute(insertInput); + + String fullPath = insertOutput.getRecords().get(0).getBackendDetailString(FilesystemRecordBackendDetailFields.FULL_PATH); + S3Object object = getAmazonS3().getObject(BUCKET_NAME, fullPath); + ObjectMetadata objectMetadata = object.getObjectMetadata(); + assertEquals("myContentType/fake", objectMetadata.getContentType()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testContentTypeHardcoded() throws QException + { + ((S3TableBackendDetails) QContext.getQInstance().getTable(TestUtils.TABLE_NAME_BLOB_S3) + .getBackendDetails()) + .withContentTypeStrategy(S3TableBackendDetails.ContentTypeStrategy.HARDCODED) + .withHardcodedContentType("your-content-type"); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_BLOB_S3); + insertInput.setRecords(List.of( + new QRecord().withValue("fileName", "file2.txt").withValue("contents", "Hi, Bob."))); + + S3InsertAction insertAction = new S3InsertAction(); + insertAction.setS3Utils(getS3Utils()); + InsertOutput insertOutput = insertAction.execute(insertInput); + + String fullPath = insertOutput.getRecords().get(0).getBackendDetailString(FilesystemRecordBackendDetailFields.FULL_PATH); + S3Object object = getAmazonS3().getObject(BUCKET_NAME, fullPath); + ObjectMetadata objectMetadata = object.getObjectMetadata(); + assertEquals("your-content-type", objectMetadata.getContentType()); } diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3QueryActionTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3QueryActionTest.java index 7e07b58d..a14fdb62 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3QueryActionTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3QueryActionTest.java @@ -62,6 +62,23 @@ public class S3QueryActionTest extends BaseS3Test + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testGet() throws QException + { + QueryInput queryInput = new QueryInput(TestUtils.TABLE_NAME_BLOB_S3) + .withFilter(new QQueryFilter(new QFilterCriteria("fileName", QCriteriaOperator.EQUALS, "BLOB-1.txt"))); + + S3QueryAction s3QueryAction = new S3QueryAction(); + s3QueryAction.setS3Utils(getS3Utils()); + QueryOutput queryOutput = s3QueryAction.execute(queryInput); + Assertions.assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows from query"); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/model/metadata/S3TableBackendDetailsTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/model/metadata/S3TableBackendDetailsTest.java new file mode 100644 index 00000000..be6c9046 --- /dev/null +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/model/metadata/S3TableBackendDetailsTest.java @@ -0,0 +1,177 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.s3.model.metadata; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; +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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; +import com.kingsrook.qqq.backend.module.filesystem.BaseTest; +import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.Cardinality; +import org.assertj.core.api.CollectionAssert; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for S3TableBackendDetails + *******************************************************************************/ +class S3TableBackendDetailsTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValidateContentTypeStrategyBasedOnFileNameOrNone() + { + ///////////////////////////////////////////// + // same validation rules for both of these // + ///////////////////////////////////////////// + for(S3TableBackendDetails.ContentTypeStrategy contentTypeStrategy : ListBuilder.of(null, S3TableBackendDetails.ContentTypeStrategy.BASED_ON_FILE_NAME, S3TableBackendDetails.ContentTypeStrategy.NONE)) + { + S3TableBackendDetails s3TableBackendDetails = getS3TableBackendDetails() + .withContentTypeStrategy(contentTypeStrategy); + QTableMetaData table = getQTableMetaData(); + + List errors = runValidation(s3TableBackendDetails, table); + CollectionAssert.assertThatCollection(errors) + .isEmpty(); + + s3TableBackendDetails.setHardcodedContentType("Test"); + s3TableBackendDetails.setContentTypeFieldName("Test"); + errors = runValidation(s3TableBackendDetails, table); + CollectionAssert.assertThatCollection(errors) + .hasSize(2) + .contains("Table testTable backend details - contentTypeFieldName should not be set when contentTypeStrategy is " + contentTypeStrategy) + .contains("Table testTable backend details - hardcodedContentType should not be set when contentTypeStrategy is " + contentTypeStrategy); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValidateContentTypeStrategyFromField() + { + S3TableBackendDetails s3TableBackendDetails = getS3TableBackendDetails() + .withContentTypeStrategy(S3TableBackendDetails.ContentTypeStrategy.FROM_FIELD); + QTableMetaData table = getQTableMetaData(); + + List errors = runValidation(s3TableBackendDetails, table); + CollectionAssert.assertThatCollection(errors) + .hasSize(1) + .contains("Table testTable backend details - contentTypeFieldName must be set when contentTypeStrategy is FROM_FIELD"); + + s3TableBackendDetails.setContentTypeFieldName("notAField"); + errors = runValidation(s3TableBackendDetails, table); + CollectionAssert.assertThatCollection(errors) + .hasSize(1) + .contains("Table testTable backend details - contentTypeFieldName must be a valid field name in the table"); + + table.addField(new QFieldMetaData("contentType", QFieldType.STRING)); + s3TableBackendDetails.setContentTypeFieldName("contentType"); + errors = runValidation(s3TableBackendDetails, table); + CollectionAssert.assertThatCollection(errors) + .isEmpty(); + + s3TableBackendDetails.setHardcodedContentType("hard"); + errors = runValidation(s3TableBackendDetails, table); + CollectionAssert.assertThatCollection(errors) + .hasSize(1) + .contains("Table testTable backend details - hardcodedContentType should not be set when contentTypeStrategy is FROM_FIELD"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValidateContentTypeStrategyHardcoded() + { + S3TableBackendDetails s3TableBackendDetails = getS3TableBackendDetails() + .withContentTypeStrategy(S3TableBackendDetails.ContentTypeStrategy.HARDCODED); + QTableMetaData table = getQTableMetaData(); + + List errors = runValidation(s3TableBackendDetails, table); + CollectionAssert.assertThatCollection(errors) + .hasSize(1) + .contains("Table testTable backend details - hardcodedContentType must be set when contentTypeStrategy is HARDCODED"); + + s3TableBackendDetails.setHardcodedContentType("Test"); + errors = runValidation(s3TableBackendDetails, table); + CollectionAssert.assertThatCollection(errors) + .isEmpty(); + + s3TableBackendDetails.setContentTypeFieldName("aField"); + errors = runValidation(s3TableBackendDetails, table); + CollectionAssert.assertThatCollection(errors) + .hasSize(1) + .contains("Table testTable backend details - contentTypeFieldName should not be set when contentTypeStrategy is HARDCODED"); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private static QTableMetaData getQTableMetaData() + { + QTableMetaData table = new QTableMetaData() + .withName("testTable") + .withField(new QFieldMetaData("contents", QFieldType.BLOB)) + .withField(new QFieldMetaData("fileName", QFieldType.STRING)); + return table; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private static S3TableBackendDetails getS3TableBackendDetails() + { + S3TableBackendDetails s3TableBackendDetails = new S3TableBackendDetails() + .withContentsFieldName("contents") + .withFileNameFieldName("fileName") + .withCardinality(Cardinality.ONE); + return s3TableBackendDetails; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private List runValidation(S3TableBackendDetails s3TableBackendDetails, QTableMetaData table) + { + QInstanceValidator validator = new QInstanceValidator(); + s3TableBackendDetails.validate(QContext.getQInstance(), table, validator); + return (validator.getErrors()); + } +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/ProcessSpecUtilsV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/ProcessSpecUtilsV1.java index e339d957..7387e0ed 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/ProcessSpecUtilsV1.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/ProcessSpecUtilsV1.java @@ -163,7 +163,6 @@ public class ProcessSpecUtilsV1 JSONObject outputJsonObject = convertResponseToJSONObject(response); String json = outputJsonObject.toString(3); - System.out.println(json); context.result(json); } @@ -308,8 +307,8 @@ public class ProcessSpecUtilsV1 private static void archiveUploadedFile(String processName, QUploadedFile qUploadedFile) { String fileName = QValueFormatter.formatDate(LocalDate.now()) - + File.separator + processName - + File.separator + qUploadedFile.getFilename(); + + File.separator + processName + + File.separator + qUploadedFile.getFilename(); InsertInput insertInput = new InsertInput(); insertInput.setTableName(QJavalinImplementation.getJavalinMetaData().getUploadedFileArchiveTableName());