From 4fd68f9195555b9eccac3e79a677cf489bfd3e61 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 18 Jun 2025 15:37:15 -0500 Subject: [PATCH] Initial checkin --- .../actions/customizers/MultiCustomizer.java | 226 ++++++++++++++++++ .../customizers/MultiCustomizerTest.java | 141 +++++++++++ 2 files changed, 367 insertions(+) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/MultiCustomizer.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/customizers/MultiCustomizerTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/MultiCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/MultiCustomizer.java new file mode 100644 index 00000000..24992700 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/MultiCustomizer.java @@ -0,0 +1,226 @@ +/* + * 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.customizers; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.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.collections.MapBuilder; + + +/******************************************************************************* + ** Implementation of TableCustomizerInterface that runs multiple other customizers + *******************************************************************************/ +public class MultiCustomizer implements InitializableViaCodeReference, TableCustomizerInterface +{ + private static final String KEY_CODE_REFERENCES = "codeReferences"; + + private List customizers = new ArrayList<>(); + + + /*************************************************************************** + * Factory method that builds a {@link QCodeReferenceWithProperties} that will + * allow this multi-customizer to be assigned to a table, and to track + * in that code ref's properties, the "sub" QCodeReferences to be used. + * + * Added to a table as in: + *
+    * table.withCustomizer(TableCustomizers.POST_INSERT_RECORD,
+    *    MultiCustomizer.of(QCodeReference(x), QCodeReference(y)));
+    * 
+ * + * @param codeReferences + * one or more {@link QCodeReference objects} to run when this customizer + * runs. note that they will run in the order provided in this list. + ***************************************************************************/ + public static QCodeReferenceWithProperties of(QCodeReference... codeReferences) + { + ArrayList list = new ArrayList<>(Arrays.stream(codeReferences).toList()); + return (new QCodeReferenceWithProperties(MultiCustomizer.class, MapBuilder.of(KEY_CODE_REFERENCES, list))); + } + + + /*************************************************************************** + * Add an additional table customizer code reference to an existing + * codeReference, e.g., constructed by the `of` factory method. + * + * @see #of(QCodeReference...) + ***************************************************************************/ + public static void addTableCustomizer(QCodeReferenceWithProperties existingMultiCustomizerCodeReference, QCodeReference codeReference) + { + ArrayList list = (ArrayList) existingMultiCustomizerCodeReference.getProperties().computeIfAbsent(KEY_CODE_REFERENCES, key -> new ArrayList<>()); + list.add(codeReference); + } + + + + /*************************************************************************** + * When this class is instantiated by the QCodeLoader, initialize the + * sub-customizer objects. + ***************************************************************************/ + @Override + public void initialize(QCodeReference codeReference) + { + if(codeReference instanceof QCodeReferenceWithProperties codeReferenceWithProperties) + { + Serializable codeReferencesPropertyValue = codeReferenceWithProperties.getProperties().get(KEY_CODE_REFERENCES); + if(codeReferencesPropertyValue instanceof List list) + { + for(Object o : list) + { + if(o instanceof QCodeReference reference) + { + TableCustomizerInterface customizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, reference); + customizers.add(customizer); + } + } + } + else + { + LOG.warn("Property KEY_CODE_REFERENCES [" + KEY_CODE_REFERENCES + "] must be a List."); + } + } + + if(customizers.isEmpty()) + { + LOG.info("No TableCustomizers were specified for MultiCustomizer."); + } + } + + + + /*************************************************************************** + * run postQuery method over all sub-customizers + ***************************************************************************/ + @Override + public List postQuery(QueryOrGetInputInterface queryInput, List records) throws QException + { + for(TableCustomizerInterface customizer : customizers) + { + records = customizer.postQuery(queryInput, records); + } + return records; + } + + + + /*************************************************************************** + * run preInsert method over all sub-customizers + ***************************************************************************/ + @Override + public List preInsert(InsertInput insertInput, List records, boolean isPreview) throws QException + { + for(TableCustomizerInterface customizer : customizers) + { + records = customizer.preInsert(insertInput, records, isPreview); + } + return records; + } + + + + /*************************************************************************** + * run postInsert method over all sub-customizers + ***************************************************************************/ + @Override + public List postInsert(InsertInput insertInput, List records) throws QException + { + for(TableCustomizerInterface customizer : customizers) + { + records = customizer.postInsert(insertInput, records); + } + return records; + } + + + + /*************************************************************************** + * run preUpdate method over all sub-customizers + ***************************************************************************/ + @Override + public List preUpdate(UpdateInput updateInput, List records, boolean isPreview, Optional> oldRecordList) throws QException + { + for(TableCustomizerInterface customizer : customizers) + { + records = customizer.preUpdate(updateInput, records, isPreview, oldRecordList); + } + return records; + } + + + + /*************************************************************************** + * run postUpdate method over all sub-customizers + ***************************************************************************/ + @Override + public List postUpdate(UpdateInput updateInput, List records, Optional> oldRecordList) throws QException + { + for(TableCustomizerInterface customizer : customizers) + { + records = customizer.postUpdate(updateInput, records, oldRecordList); + } + return records; + } + + + + /*************************************************************************** + * run preDelete method over all sub-customizers + ***************************************************************************/ + @Override + public List preDelete(DeleteInput deleteInput, List records, boolean isPreview) throws QException + { + for(TableCustomizerInterface customizer : customizers) + { + records = customizer.preDelete(deleteInput, records, isPreview); + } + return records; + } + + + + /*************************************************************************** + * run postDelete method over all sub-customizers + ***************************************************************************/ + @Override + public List postDelete(DeleteInput deleteInput, List records) throws QException + { + for(TableCustomizerInterface customizer : customizers) + { + records = customizer.postDelete(deleteInput, records); + } + return records; + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/customizers/MultiCustomizerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/customizers/MultiCustomizerTest.java new file mode 100644 index 00000000..64508617 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/customizers/MultiCustomizerTest.java @@ -0,0 +1,141 @@ +/* + * 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.customizers; + + +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +/******************************************************************************* + ** Unit test for MultiCustomizer + *******************************************************************************/ +class MultiCustomizerTest extends BaseTest +{ + private static List events = new ArrayList<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + @AfterEach + void beforeAndAfterEach() + { + events.clear(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withCustomizer(TableCustomizers.PRE_INSERT_RECORD, MultiCustomizer.of( + new QCodeReference(CustomizerA.class), + new QCodeReference(CustomizerB.class) + )); + reInitInstanceInContext(qInstance); + + new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord())); + assertThat(events).hasSize(2) + .contains("CustomizerA.preInsert") + .contains("CustomizerB.preInsert"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAddingMore() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + + QCodeReferenceWithProperties multiCustomizer = MultiCustomizer.of(new QCodeReference(CustomizerA.class)); + MultiCustomizer.addTableCustomizer(multiCustomizer, new QCodeReference(CustomizerB.class)); + + qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).withCustomizer(TableCustomizers.PRE_INSERT_RECORD, multiCustomizer); + reInitInstanceInContext(qInstance); + + new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord())); + assertThat(events).hasSize(2) + .contains("CustomizerA.preInsert") + .contains("CustomizerB.preInsert"); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class CustomizerA implements TableCustomizerInterface + { + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List preInsert(InsertInput insertInput, List records, boolean isPreview) throws QException + { + events.add("CustomizerA.preInsert"); + return (records); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class CustomizerB implements TableCustomizerInterface + { + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List preInsert(InsertInput insertInput, List records, boolean isPreview) throws QException + { + events.add("CustomizerB.preInsert"); + return (records); + } + } + +} \ No newline at end of file