From 7c393721538b3b895d81cf6cd10013e6673622d2 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 7 Mar 2025 14:39:39 -0600 Subject: [PATCH 01/18] Initial checkin of Banners under QBrandingMetaData - includes migration from (now deprecated) MetaDataFilterInterface to MetaDataActionCustomizerInterface (stored on the QInstance and used by MetaDataAction) - includes migration from (now deprecated) environmentBannerText and environmentBannerColor in QBrandingMetaData to now be implemented as a banner --- .../metadata/AllowAllMetaDataFilter.java | 1 + .../DefaultNoopMetaDataActionCustomizer.java | 92 ++++++ .../core/actions/metadata/MetaDataAction.java | 68 +++-- .../MetaDataActionCustomizerInterface.java | 78 +++++ .../metadata/MetaDataFilterInterface.java | 36 +-- .../core/instances/QInstanceValidator.java | 6 + .../core/model/metadata/QInstance.java | 37 +++ .../core/model/metadata/branding/Banner.java | 269 ++++++++++++++++++ .../model/metadata/branding/BannerSlot.java | 31 ++ .../metadata/branding/QBrandingMetaData.java | 95 ++++++- .../actions/metadata/MetaDataActionTest.java | 117 +++++++- .../instances/QInstanceValidatorTest.java | 15 + 12 files changed, 787 insertions(+), 58 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/DefaultNoopMetaDataActionCustomizer.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionCustomizerInterface.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/Banner.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/branding/BannerSlot.java 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/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index b123858e..c73f9b8e 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; @@ -245,6 +246,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/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index def809b9..8e4ce83f 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/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/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. From 0f8ad2fb78af0ec1b566214268e3201c803f6a41 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:04:16 -0500 Subject: [PATCH 02/18] Allow a map of prepopulatedValues to be provided as an input value, to set defaultValues for fields --- .../BulkInsertPrepareFileMappingStep.java | 65 ++++++++++++++++++- .../BulkInsertPrepareFileMappingStepTest.java | 49 ++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) 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/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 From 244239f05348032ba1de4bf9aed3563c2b4ba11e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:04:52 -0500 Subject: [PATCH 03/18] Try to get better message in front of users if streamed ETL process is init'ed with no records --- .../CouldNotFindQueryFilterForExtractStepException.java | 4 ++-- .../etl/streamedwithfrontend/ExtractViaQueryStep.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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.")); } From 08ed9a5aad26dcbadfe29680af086187782d8755 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:18:28 -0500 Subject: [PATCH 04/18] Add style customizer to report action, with excel poi implementation for columnWidths, more cell styles, merged ranges --- .../reporting/ExportStreamerInterface.java | 8 ++ .../ExportStyleCustomizerInterface.java | 35 +++++++++ .../reporting/GenerateReportAction.java | 8 +- .../excel/poi/StreamedSheetWriter.java | 49 ++++++++++-- .../model/actions/reporting/ReportInput.java | 33 ++++++++ .../reporting/excel/TestExcelStyler.java | 76 +++++++++++++++++++ 6 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStyleCustomizerInterface.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/TestExcelStyler.java 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..07863fd0 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,12 @@ public class GenerateReportAction extends AbstractQActionFunction - - """); + """); + if(styleCustomizerInterface != null && view != null) + { + List columnWidths = styleCustomizerInterface.getColumnWidthsForView(view); + if(CollectionUtils.nullSafeHasContents(columnWidths)) + { + writer.write(""); + for(int i = 0; i < columnWidths.size(); i++) + { + writer.write(""" + + """.formatted(i + 1, i + 1, columnWidths.get(i))); + } + writer.write(""); + } + } + + writer.write(""); } @@ -67,11 +86,25 @@ public class StreamedSheetWriter /******************************************************************************* ** *******************************************************************************/ - public void endSheet() throws IOException + public void endSheet(QReportView view, ExcelPoiStyleCustomizerInterface 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 +184,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/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/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..c45061db --- /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.ExcelPoiStyleCustomizerInterface; +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 ExcelPoiStyleCustomizerInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @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); + } +} From b863d626887a5971aa9dd6e0e24da3f34dd071dc Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:42:53 -0500 Subject: [PATCH 05/18] Add style customizer to report action, with excel poi implementation for columnWidths, more cell styles, merged ranges --- .../reporting/GenerateReportAction.java | 5 ++ .../ExcelPoiBasedStreamingExportStreamer.java | 60 ++++++++++++-- ...asedStreamingStyleCustomizerInterface.java | 81 +++++++++++++++++++ .../reporting/GenerateReportActionTest.java | 30 +++++++ 4 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingStyleCustomizerInterface.java 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 07863fd0..eacc31ec 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 @@ -168,6 +168,11 @@ 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/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); + } + } + + + /******************************************************************************* ** *******************************************************************************/ From 9aa25b4f1472b407e8c0da5301fbbfe63ce50641 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:43:40 -0500 Subject: [PATCH 06/18] Add exportStyleCustomizer to QReportMetaData, plus clonable here and on child metadata --- .../core/model/metadata/layout/QIcon.java | 23 +++- .../metadata/reporting/QReportDataSource.java | 37 ++++++- .../metadata/reporting/QReportMetaData.java | 100 +++++++++++++++++- 3 files changed, 154 insertions(+), 6 deletions(-) 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 72117679..71c26660 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 @@ -31,7 +31,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout; ** Future may allow something like a "namespace", and/or multiple icons for ** use in different frontends, etc. *******************************************************************************/ -public class QIcon +public class QIcon implements Cloneable { private String name; private String path; @@ -58,6 +58,25 @@ public class QIcon + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QIcon clone() + { + try + { + QIcon clone = (QIcon) super.clone(); + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } + + + /******************************************************************************* ** Getter for name ** @@ -154,6 +173,4 @@ public class QIcon this.color = color; return (this); } - - } 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); + } + + } From 14398d2c949451c2e3ef4b41c2f9073983dc40f6 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:44:44 -0500 Subject: [PATCH 07/18] Open up makeQReportField to be public (as well as FieldAndJoinTable, which, in some other branch I believe was removed from this class, so, anticipate a conflict over that?) --- .../savedreports/SavedReportToReportMetaDataAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) {} } From 75fdff031a53cd80505ac6457a5e2eccb5f1b187 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:45:29 -0500 Subject: [PATCH 08/18] Renamed ExcelPoiStyleCustomizerInterface to ExcelPoiBasedStreamingStyleCustomizerInterface; support (by skipping) null column widths --- .../reporting/excel/poi/StreamedSheetWriter.java | 14 +++++++++----- .../actions/reporting/excel/TestExcelStyler.java | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) 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 65bf7aac..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 @@ -56,7 +56,7 @@ public class StreamedSheetWriter /******************************************************************************* ** *******************************************************************************/ - public void beginSheet(QReportView view, ExcelPoiStyleCustomizerInterface styleCustomizerInterface) throws IOException + public void beginSheet(QReportView view, ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface) throws IOException { writer.write(""" @@ -70,9 +70,13 @@ public class StreamedSheetWriter writer.write(""); for(int i = 0; i < columnWidths.size(); i++) { - writer.write(""" - - """.formatted(i + 1, i + 1, columnWidths.get(i))); + Integer width = columnWidths.get(i); + if(width != null) + { + writer.write(""" + + """.formatted(i + 1, i + 1, width)); + } } writer.write(""); } @@ -86,7 +90,7 @@ public class StreamedSheetWriter /******************************************************************************* ** *******************************************************************************/ - public void endSheet(QReportView view, ExcelPoiStyleCustomizerInterface styleCustomizerInterface) throws IOException + public void endSheet(QReportView view, ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface) throws IOException { writer.write(""); 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 index c45061db..332345dd 100644 --- 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 @@ -24,7 +24,7 @@ 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.ExcelPoiStyleCustomizerInterface; +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; @@ -35,7 +35,7 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook; /******************************************************************************* ** *******************************************************************************/ -public class TestExcelStyler implements ExcelPoiStyleCustomizerInterface +public class TestExcelStyler implements ExcelPoiBasedStreamingStyleCustomizerInterface { /*************************************************************************** From 36ff5eea0241742c77675319e4a92f49bd748bd1 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:46:09 -0500 Subject: [PATCH 09/18] Add an openSheet(index) method --- .../insert/filehandling/XlsxFileToRows.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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()); + } + + /*************************************************************************** ** From 116a4e883ba95f29c61f15aa831ea06daf666eec Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:46:42 -0500 Subject: [PATCH 10/18] Bugfix - processing fieldAnnotation.defaultValue was throwing away the value, not actually setting it in the fieldMetaData --- .../qqq/backend/core/model/metadata/fields/QFieldMetaData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a24bb992..0bbbb078 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 @@ -238,7 +238,7 @@ public class QFieldMetaData implements Cloneable if(StringUtils.hasContent(fieldAnnotation.defaultValue())) { - ValueUtils.getValueAsFieldType(this.type, fieldAnnotation.defaultValue()); + withDefaultValue(ValueUtils.getValueAsFieldType(this.type, fieldAnnotation.defaultValue())); } } } From e4d52a0443b927b7c09f016dfd8518c5c4173019 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:47:12 -0500 Subject: [PATCH 11/18] Include field maxLength attribute in what's sent to frontend --- .../core/model/metadata/frontend/QFrontendFieldMetaData.java | 2 ++ 1 file changed, 2 insertions(+) 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())) { From 38cdb948767f2eac25bd8b95a7a7611036ee2256 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:47:32 -0500 Subject: [PATCH 12/18] Include process min/max input record attributes in what's sent to frontend --- .../frontend/QFrontendProcessMetaData.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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; + } + } From ae4e269b889aab8f8506403b3c802552b79bef4c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:48:15 -0500 Subject: [PATCH 13/18] Add static getTableName(Class) and instance.tableName() methods. --- .../core/model/data/QRecordEntity.java | 27 +++++++++++++++++++ .../core/model/data/QRecordEntityTest.java | 19 +++++++++++++ 2 files changed, 46 insertions(+) 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 c07446a0..d18a9615 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 @@ -583,4 +583,31 @@ public abstract class QRecordEntity 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/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java index 38beaae4..f1111aa9 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java @@ -41,6 +41,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -566,4 +567,22 @@ class QRecordEntityTest extends BaseTest assertEquals(0, order.getLineItems().size()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableName() throws QException + { + assertEquals(Item.TABLE_NAME, QRecordEntity.getTableName(Item.class)); + assertEquals(Item.TABLE_NAME, Item.getTableName(Item.class)); + assertEquals(Item.TABLE_NAME, new Item().tableName()); + + ////////////////////////////////// + // no TABLE_NAME in Order class // + ////////////////////////////////// + assertThatThrownBy(() -> Order.getTableName(Order.class)); + } + } \ No newline at end of file From d033d3f464329587e1045bdec037a7e0e67b4937 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 11:37:23 -0500 Subject: [PATCH 14/18] Add QCodeReferenceWithProperties and InitializableViaCodeReference; also, refactor QCodeLoader to eliminate most of the specialized methods - in favor of generally using getAdHoc (now that just needs a better name, lol) --- .../PollingAutomationPerTableRunner.java | 2 +- .../core/actions/customizers/QCodeLoader.java | 167 ++---------------- .../reporting/GenerateReportAction.java | 3 +- .../core/actions/scripts/QJavaExecutor.java | 3 +- .../values/QPossibleValueTranslator.java | 2 +- .../SearchPossibleValueSourceAction.java | 2 +- .../core/instances/QInstanceEnricher.java | 2 +- .../code/InitializableViaCodeReference.java | 38 ++++ .../model/metadata/code/QCodeReference.java | 22 ++- .../code/QCodeReferenceWithProperties.java | 59 +++++++ .../dashboard/nocode/WidgetAdHocValue.java | 3 +- .../BaseStreamedETLStep.java | 2 +- .../actions/customizers/QCodeLoaderTest.java | 54 ++++++ 13 files changed, 196 insertions(+), 163 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/InitializableViaCodeReference.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReferenceWithProperties.java 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/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index eacc31ec..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 @@ -222,7 +222,8 @@ 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); 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/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 14dda867..eb54ff6b 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 @@ -1425,7 +1425,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/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 5c475a4e..5beba2ea 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 @@ -29,7 +29,7 @@ import java.io.Serializable; ** 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 +public class QCodeReference implements Serializable, Cloneable { private String name; private QCodeType codeType; @@ -58,6 +58,25 @@ public class QCodeReference implements Serializable + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QCodeReference clone() + { + try + { + QCodeReference clone = (QCodeReference) super.clone(); + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -179,5 +198,4 @@ public class QCodeReference implements Serializable 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/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/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 From 4acc185698c820a1dbf0b98dcd9cf9155421ead7 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 11:38:38 -0500 Subject: [PATCH 15/18] Add org.apache.http Logger level of INFO; inline all empty Logger xml elements --- .../src/main/resources/log4j2.xml | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/qqq-backend-core/src/main/resources/log4j2.xml b/qqq-backend-core/src/main/resources/log4j2.xml index 8883de80..93b87325 100644 --- a/qqq-backend-core/src/main/resources/log4j2.xml +++ b/qqq-backend-core/src/main/resources/log4j2.xml @@ -18,21 +18,14 @@ - - - - - - - - - - - - - - - + + + + + + + + From aca199e91e80409404d0a13bde24f3f7eb9a2d6c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 19 Mar 2025 16:43:03 -0500 Subject: [PATCH 16/18] Deprecated methods that take unused AbstractActionInput --- .../dashboard/AbstractHTMLWidgetRenderer.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) 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(); From 916c8c3ba69a2ef2fca9c4b6e3ab2b7a2b234c27 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 19 Mar 2025 16:43:23 -0500 Subject: [PATCH 17/18] Add support for orderBys on child-joins --- .../metadata/MetaDataProducerHelper.java | 2 +- ...omRecordEntityGenericMetaDataProducer.java | 47 +++++++++++++++---- .../producers/annotations/ChildJoin.java | 9 ++++ 3 files changed, 47 insertions(+), 11 deletions(-) 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/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..41f6158d 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,13 @@ import java.lang.annotation.RetentionPolicy; public @interface ChildJoin { boolean enabled(); + + OrderBy[] orderBy() default { }; + + @interface OrderBy + { + String fieldName(); + + boolean isAscending() default true; + } } From 8f0d117b13981c2e04c71fda0197b62276bb3f15 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 19 Mar 2025 16:51:41 -0500 Subject: [PATCH 18/18] Checkstyle! --- .../core/model/metadata/producers/annotations/ChildJoin.java | 3 +++ 1 file changed, 3 insertions(+) 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 41f6158d..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 @@ -38,6 +38,9 @@ public @interface ChildJoin OrderBy[] orderBy() default { }; + /*************************************************************************** + ** + ***************************************************************************/ @interface OrderBy { String fieldName();