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