diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java index 41015e57..db395ff9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java @@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -177,8 +178,8 @@ public class AuditAction extends AbstractQActionFunction key = new Pair<>(tableName, nameValue); if(!cachedFetches.containsKey(key)) @@ -287,7 +288,7 @@ public class AuditAction extends AbstractQActionFunction recordList = CollectionUtils.nonNullList(input.getRecordList()).stream() .filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors())).toList(); - AuditLevel auditLevel = getAuditLevel(tableActionInput); + AuditLevel auditLevel = getAuditLevel(table); if(auditLevel == null || auditLevel.equals(AuditLevel.NONE) || CollectionUtils.nullSafeIsEmpty(recordList)) { ///////////////////////////////////////////// @@ -269,7 +272,54 @@ public class DMLAuditAction extends AbstractQActionFunction auditTrees = recordList.stream().map(r -> + new QRecord() + .withValue("rootAuditTableId", auditTableId) + .withValue("rootRecordId", r.getValueInteger(table.getPrimaryKeyField())) + ).toList(); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName("audit tree"); // todo - from entity + insertInput.setRecords(auditTrees); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + */ + } + + for(String auditTreeParentTableName : CollectionUtils.nonNullList(getAuditTreeParentTableNames(table))) + { + Integer rootAuditTableId = auditAction.getIdForName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE, auditTreeParentTableName); + Integer nodeAuditTableId = auditAction.getIdForName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE, table.getName()); + + List auditTreeNodes = new ArrayList<>(); + for(QRecord record : recordList) + { + Serializable rootRecordId = record.getValue("orderId"); // todo - figure this out, from joins... + + auditTreeNodes.add(new QRecord() + .withValue("rootAuditTableId", rootAuditTableId) + .withValue("rootRecordId", rootRecordId) + .withValue("nodeAuditTableId", nodeAuditTableId) + .withValue("nodeRecordId", record.getValue(table.getPrimaryKeyField())) + ); + } + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TREE); + insertInput.setRecords(auditTreeNodes); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + } + } + long end = System.currentTimeMillis(); LOG.trace("Audit performance", logPair("auditLevel", String.valueOf(auditLevel)), logPair("recordCount", recordList.size()), logPair("millis", (end - start))); } @@ -390,9 +440,8 @@ public class DMLAuditAction extends AbstractQActionFunction getAuditTreeParentTableNames(QTableMetaData table) + { + if(table.getAuditRules() == null) + { + return (null); + } + + return (table.getAuditRules().getAuditTreeParentTableNames()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java index 56b4b963..127eb66c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java @@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; +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.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; @@ -36,9 +37,13 @@ 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.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; +import com.kingsrook.qqq.backend.core.processes.implementations.audits.GetRecordAuditsStep; /******************************************************************************* @@ -50,6 +55,10 @@ public class AuditsMetaDataProvider public static final String TABLE_NAME_AUDIT_USER = "auditUser"; public static final String TABLE_NAME_AUDIT = "audit"; public static final String TABLE_NAME_AUDIT_DETAIL = "auditDetail"; + public static final String TABLE_NAME_AUDIT_TREE = "auditTree"; + + public static final String AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_ROOT = "auditTreeJoinAuditTableForRoot"; + public static final String AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_NODE = "auditTreeJoinAuditTableForNode"; @@ -61,6 +70,21 @@ public class AuditsMetaDataProvider defineStandardAuditTables(instance, backendName, backendDetailEnricher); defineStandardAuditPossibleValueSources(instance); defineStandardAuditJoins(instance); + defineProcessGetRecordAudits(instance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void defineProcessGetRecordAudits(QInstance instance) + { + instance.addProcess(new QProcessMetaData() + .withName("getRecordAudits") + .withStepList(List.of(new QBackendStepMetaData() + .withName("step") + .withCode(new QCodeReference(GetRecordAuditsStep.class))))); } @@ -91,6 +115,20 @@ public class AuditsMetaDataProvider .withType(JoinType.ONE_TO_MANY) .withJoinOn(new JoinOn("id", "auditId"))); + instance.addJoin(new QJoinMetaData() + .withLeftTable(TABLE_NAME_AUDIT_TREE) + .withRightTable(TABLE_NAME_AUDIT_TABLE) + .withName(AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_ROOT) + .withType(JoinType.MANY_TO_ONE) + .withJoinOn(new JoinOn("rootAuditTableId", "id"))); + + instance.addJoin(new QJoinMetaData() + .withLeftTable(TABLE_NAME_AUDIT_TREE) + .withRightTable(TABLE_NAME_AUDIT_TABLE) + .withName(AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_NODE) + .withType(JoinType.MANY_TO_ONE) + .withJoinOn(new JoinOn("nodeAuditTableId", "id"))); + } @@ -141,6 +179,7 @@ public class AuditsMetaDataProvider rs.add(enrich(backendDetailEnricher, defineAuditTableTable(backendName))); rs.add(enrich(backendDetailEnricher, defineAuditTable(backendName))); rs.add(enrich(backendDetailEnricher, defineAuditDetailTable(backendName))); + rs.add(enrich(backendDetailEnricher, defineAuditTreeTable(backendName))); return (rs); } @@ -217,6 +256,10 @@ public class AuditsMetaDataProvider .withRecordLabelFormat("%s %s") .withRecordLabelFields("auditTableId", "recordId") .withPrimaryKeyField("id") + .withAssociation(new Association() + .withName("auditDetails") + .withAssociatedTableName(TABLE_NAME_AUDIT_DETAIL) + .withJoinName(QJoinMetaData.makeInferredJoinName(TABLE_NAME_AUDIT, TABLE_NAME_AUDIT_DETAIL))) .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withField(new QFieldMetaData("auditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_TABLE)) .withField(new QFieldMetaData("auditUserId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_USER)) @@ -249,4 +292,26 @@ public class AuditsMetaDataProvider .withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineAuditTreeTable(String backendName) + { + return new QTableMetaData() + .withName(TABLE_NAME_AUDIT_TREE) + .withBackendName(backendName) + .withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE)) + .withRecordLabelFormat("%s") + .withRecordLabelFields("id") + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("rootAuditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT)) + .withField(new QFieldMetaData("rootRecordId", QFieldType.INTEGER)) + .withField(new QFieldMetaData("nodeAuditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT)) + .withField(new QFieldMetaData("nodeRecordId", QFieldType.INTEGER)) + .withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java index f7cf4bfa..35ef9821 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.audits; +import java.util.List; + + /******************************************************************************* ** *******************************************************************************/ @@ -29,6 +32,9 @@ public class QAuditRules { private AuditLevel auditLevel; + private boolean isAuditTreeRoot = false; + private List auditTreeParentTableNames = null; + /******************************************************************************* @@ -71,4 +77,66 @@ public class QAuditRules return (this); } + + + /******************************************************************************* + ** Getter for isAuditTreeRoot + *******************************************************************************/ + public boolean getIsAuditTreeRoot() + { + return (this.isAuditTreeRoot); + } + + + + /******************************************************************************* + ** Setter for isAuditTreeRoot + *******************************************************************************/ + public void setIsAuditTreeRoot(boolean isAuditTreeRoot) + { + this.isAuditTreeRoot = isAuditTreeRoot; + } + + + + /******************************************************************************* + ** Fluent setter for isAuditTreeRoot + *******************************************************************************/ + public QAuditRules withIsAuditTreeRoot(boolean isAuditTreeRoot) + { + this.isAuditTreeRoot = isAuditTreeRoot; + return (this); + } + + + + /******************************************************************************* + ** Getter for auditTreeParentTableNames + *******************************************************************************/ + public List getAuditTreeParentTableNames() + { + return (this.auditTreeParentTableNames); + } + + + + /******************************************************************************* + ** Setter for auditTreeParentTableNames + *******************************************************************************/ + public void setAuditTreeParentTableNames(List auditTreeParentTableNames) + { + this.auditTreeParentTableNames = auditTreeParentTableNames; + } + + + + /******************************************************************************* + ** Fluent setter for auditTreeParentTableNames + *******************************************************************************/ + public QAuditRules withAuditTreeParentTableNames(List auditTreeParentTableNames) + { + this.auditTreeParentTableNames = auditTreeParentTableNames; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/audits/GetRecordAuditsStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/audits/GetRecordAuditsStep.java new file mode 100644 index 00000000..23dbd0f8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/audits/GetRecordAuditsStep.java @@ -0,0 +1,199 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.processes.implementations.audits; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import com.google.gson.reflect.TypeToken; +import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.CountAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput; +import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; +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.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** This is a single-step process used to look up audits for a record. + *******************************************************************************/ +public class GetRecordAuditsStep implements BackendStep +{ + private static final QLogger LOG = QLogger.getLogger(GetRecordAuditsStep.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + try + { + String tableName = runBackendStepInput.getValueString("tableName"); + Integer recordId = runBackendStepInput.getValueInteger("recordId"); + String sortDirection = runBackendStepInput.getValueString("sortDirection"); + + Integer limit = 1000; + + ///////////////////////////////////////// + // make sure user may query this table // + ///////////////////////////////////////// + PermissionsHelper.checkTablePermissionThrowing(new QueryInput().withTableName(tableName), TablePermissionSubType.READ); + PermissionsHelper.checkTablePermissionThrowing(new QueryInput().withTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT), TablePermissionSubType.READ); + + ///////////////////////////////////////////////////////////////////////// + // set up filter for audits - always start with the record in question // + // possibly add in other pairs of table/recordId based on the tree... // + // combine all of those options in a filter of OR's // + ///////////////////////////////////////////////////////////////////////// + QQueryFilter auditRecordFilter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR); + + auditRecordFilter.addSubFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE + ".name", QCriteriaOperator.EQUALS, tableName)) + .withCriteria(new QFilterCriteria("recordId", QCriteriaOperator.EQUALS, recordId))); + + Set otherAuditTableIds = new HashSet<>(); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the table is part of an audit tree, then find other table/recordId pairs to look for in the audits query // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + QTableMetaData table = QContext.getQInstance().getTable(tableName); + QAuditRules auditRules = Objects.requireNonNullElseGet(table.getAuditRules(), QAuditRules::new); + if(auditRules.getIsAuditTreeRoot()) + { + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TREE); + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("rootTable.name", QCriteriaOperator.EQUALS, tableName))); + + queryInput.withQueryJoin(new QueryJoin(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE) + .withJoinMetaData(QContext.getQInstance().getJoin(AuditsMetaDataProvider.AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_ROOT)) + .withAlias("rootTable")); + + for(QRecord auditTreeRecord : new QueryAction().execute(queryInput).getRecords()) + { + otherAuditTableIds.add(auditTreeRecord.getValueInteger("nodeAuditTableId")); + auditRecordFilter.addSubFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("auditTableId", QCriteriaOperator.EQUALS, auditTreeRecord.getValue("nodeAuditTableId"))) + .withCriteria(new QFilterCriteria("recordId", QCriteriaOperator.EQUALS, auditTreeRecord.getValue("nodeRecordId")))); + } + } + //////////////////////////////////////////////// + // todo - reverse (e.g., for child, non-root) // + //////////////////////////////////////////////// + + /////////////////////// + // select the audits // + /////////////////////// + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT); + queryInput.setShouldGenerateDisplayValues(true); + queryInput.setShouldTranslatePossibleValues(true); + queryInput.setIncludeAssociations(true); + queryInput.setFilter(new QQueryFilter() + .withLimit(limit) + .withSubFilters(List.of(auditRecordFilter)) + .withOrderBy(new QFilterOrderBy("timestamp", "asc".equals(sortDirection))) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + ArrayList audits = CollectionUtils.useOrWrap(queryOutput.getRecords(), new TypeToken<>() {}); // todo - share simple-class format change, verify in checkstyle + runBackendStepOutput.addValue("audits", audits); + + /////////////////////////////////////////////////////// + // look up count if needed, else use audit list size // + /////////////////////////////////////////////////////// + if(audits.size() >= limit) + { + CountInput countInput = new CountInput(); + countInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT); + countInput.setFilter(new QQueryFilter().withSubFilters(List.of(auditRecordFilter))); + CountOutput countOutput = new CountAction().execute(countInput); + runBackendStepOutput.addValue("count", countOutput.getCount()); + } + else + { + runBackendStepOutput.addValue("count", audits.size()); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // put map of auditTableId to table names, and table name to (full) table meta data in result // + //////////////////////////////////////////////////////////////////////////////////////////////// + if(CollectionUtils.nullSafeHasContents(otherAuditTableIds)) + { + QueryInput auditTableQueryInput = new QueryInput(); + auditTableQueryInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE); + auditTableQueryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, new ArrayList<>(otherAuditTableIds)))); + QueryOutput auditTableQueryOutput = new QueryAction().execute(auditTableQueryInput); + + HashMap auditTableMap = auditTableQueryOutput.getRecords().stream().collect(Collectors.toMap(r -> r.getValueInteger("id"), r -> r, (a, b) -> a, HashMap::new)); + runBackendStepOutput.addValue("auditTableMap", auditTableMap); + + HashMap tableMetaDataMap = new HashMap<>(); + for(QRecord auditTable : auditTableMap.values()) + { + String subTableName = auditTable.getValueString("name"); + + TableMetaDataInput tableMetaDataInput = new TableMetaDataInput(); + tableMetaDataInput.setTableName(subTableName); + TableMetaDataOutput tableMetaDataOutput = new TableMetaDataAction().execute(tableMetaDataInput); + + tableMetaDataMap.put(subTableName, tableMetaDataOutput.getTable()); + } + + runBackendStepOutput.addValue("tableMetaDataMap", tableMetaDataMap); + } + } + catch(Exception e) + { + throw new QException("Error getting record audits", e); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java index 08526658..feeeb2ae 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java @@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.utils; import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -34,6 +36,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.gson.reflect.TypeToken; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -550,4 +553,27 @@ public class CollectionUtils return (rs); } + + + /******************************************************************************* + ** For cases where you have a Collection (of an unknown type), and you know you + ** want/need it in a specific concrete type (say, ArrayList), but you don't want + ** to just blindly copy it (e.g., as that may be expensive), call this method. + ** + ** ArrayList myStrings = CollectionUtils.useOrWrap(yourStrings, new TypeToken<>() {}); + ** + ** Note that you may always just pass `new TypeToken() {}` as the 2nd arg - then + ** the compiler will infer the type (T) based on the variable you're assigning into. + *******************************************************************************/ + public static > T useOrWrap(Collection collection, TypeToken typeToken) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException + { + Class targetClass = (Class) typeToken.getRawType(); + if(targetClass.isInstance(collection)) + { + return (targetClass.cast(collection)); + } + + Constructor constructor = targetClass.getConstructor(Collection.class); + return (constructor.newInstance(collection)); + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/CollectionUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/CollectionUtilsTest.java index 860a4ba6..33769386 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/CollectionUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/CollectionUtilsTest.java @@ -22,18 +22,23 @@ package com.kingsrook.qqq.backend.core.utils; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.function.Function; +import com.google.gson.reflect.TypeToken; import com.kingsrook.qqq.backend.core.BaseTest; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -544,4 +549,26 @@ class CollectionUtilsTest extends BaseTest assertEquals(List.of(1, 2, 3), CollectionUtils.mergeLists(null, List.of(1, 2, 3), null)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException + { + { + List originalList = new ArrayList<>(List.of("A", "B", "C")); + ArrayList reallyArrayList = CollectionUtils.useOrWrap(originalList, new TypeToken<>() {}); + assertSame(originalList, reallyArrayList); + } + + { + List originalList = new LinkedList<>(List.of("A", "B", "C")); + ArrayList reallyArrayList = CollectionUtils.useOrWrap(originalList, new TypeToken<>() {}); + assertNotSame(originalList, reallyArrayList); + assertEquals(ArrayList.class, reallyArrayList.getClass()); + } + } + }