From 50cdcba5900183fab49ac7466893459005c78982 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Sat, 31 Oct 2026 21:54:18 -0400 Subject: [PATCH 1/2] Small alias plugin refactor and new superclass to base alias plugins on non-alias queries --- .../gusdb/wdk/model/answer/AnswerValue.java | 2 +- .../single/SingleRecordQuestionParam.java | 3 +- .../model/record/PrimaryKeyDefinition.java | 11 +- .../wdk/model/record/PrimaryKeyValue.java | 10 ++ .../gusdb/wdk/model/record/RecordClass.java | 2 +- ...AliasQueryBasedPrimaryKeyAliasPlugin.java} | 8 +- .../PassThroughPrimaryKeyAliasPlugin.java} | 7 +- .../{ => alias}/PrimaryKeyAliasPlugin.java | 3 +- .../QueryBasedPrimaryKeyAliasPlugin.java | 135 ++++++++++++++++++ 9 files changed, 166 insertions(+), 15 deletions(-) rename Model/src/main/java/org/gusdb/wdk/model/record/{QueryBasedPrimaryKeyAliasPlugin.java => alias/AliasQueryBasedPrimaryKeyAliasPlugin.java} (92%) rename Model/src/main/java/org/gusdb/wdk/model/record/{GenericRecordPrimaryKeyAliasPlugin.java => alias/PassThroughPrimaryKeyAliasPlugin.java} (82%) rename Model/src/main/java/org/gusdb/wdk/model/record/{ => alias}/PrimaryKeyAliasPlugin.java (76%) create mode 100644 Model/src/main/java/org/gusdb/wdk/model/record/alias/QueryBasedPrimaryKeyAliasPlugin.java diff --git a/Model/src/main/java/org/gusdb/wdk/model/answer/AnswerValue.java b/Model/src/main/java/org/gusdb/wdk/model/answer/AnswerValue.java index 29e02b9e9b..297f47f8d3 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/answer/AnswerValue.java +++ b/Model/src/main/java/org/gusdb/wdk/model/answer/AnswerValue.java @@ -1185,7 +1185,7 @@ public static SqlQuery addPartKeysToAttrOrTableSqlQuery (SqlQuery attrOrTableSql if (partKeySqlQuery == null) return attrOrTableSqlQuery; PrimaryKeyDefinition pkd = recordClass.getPrimaryKeyDefinition(); - String idSql = "select " + pkd.createSelectClause(primaryKey.getValues()) + + String idSql = "select " + PrimaryKeyDefinition.createSelectClause(primaryKey.getValues()) + recordClass.getWdkModel().getAppDb().getPlatform().getDummyTable(); String partSql = partKeySqlQuery.getSql(); diff --git a/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordQuestionParam.java b/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordQuestionParam.java index aebe99eb46..f640838ef2 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordQuestionParam.java +++ b/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordQuestionParam.java @@ -40,9 +40,8 @@ public SingleRecordQuestionParam(RecordClass recordClass) { @Override public String toInternalValue(RunnableObj ctxVals) throws WdkModelException { final String stable = ctxVals.get().get(_param.getName()); - PrimaryKeyDefinition pkDef = recordClass.getPrimaryKeyDefinition(); Map pk = Functions.mapValues(parseParamValue(stable), entry -> (String)entry.getValue()); - return pkDef.createSelectClause(pk); + return PrimaryKeyDefinition.createSelectClause(pk); } }); } diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyDefinition.java b/Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyDefinition.java index 48eb5c954b..b7b32da9b5 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyDefinition.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyDefinition.java @@ -20,8 +20,11 @@ import org.gusdb.wdk.model.WdkModelBase; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkModelText; +import org.gusdb.wdk.model.WdkRuntimeException; import org.gusdb.wdk.model.query.Column; import org.gusdb.wdk.model.query.Query; +import org.gusdb.wdk.model.record.alias.AliasQueryBasedPrimaryKeyAliasPlugin; +import org.gusdb.wdk.model.record.alias.PrimaryKeyAliasPlugin; import org.gusdb.wdk.model.record.attribute.PkColumnAttributeField; import org.gusdb.wdk.model.user.User; @@ -135,9 +138,9 @@ public void resolveReferences(WdkModel model) throws WdkModelException { // if alias query specified, resolve/prepare it and create query based plugin if (_aliasQueryRef != null) { - Query aliasQuery = QueryBasedPrimaryKeyAliasPlugin + Query aliasQuery = AliasQueryBasedPrimaryKeyAliasPlugin .prepareAliasQuery(_aliasQueryRef, getColumnRefs(), _recordClass); - _aliasPlugin = new QueryBasedPrimaryKeyAliasPlugin(aliasQuery); + _aliasPlugin = new AliasQueryBasedPrimaryKeyAliasPlugin(aliasQuery); } // otherwise, resolve the configured alias plugin @@ -149,7 +152,7 @@ public void resolveReferences(WdkModel model) throws WdkModelException { .getDeclaredConstructor() .newInstance(); } - catch (InstantiationException | IllegalAccessException | IllegalArgumentException | + catch (WdkRuntimeException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException | ClassNotFoundException e) { throw new WdkModelException("Failed instantiating aliasPlugin for class " + _aliasPluginClassName, e); } @@ -199,7 +202,7 @@ public String createJoinClause(String subQuery1Name, String subQuery2Name) { .collect(Collectors.joining(" AND ")); } - public String createSelectClause(Map pkValues) { + public static String createSelectClause(Map pkValues) { List valStrings = new ArrayList<>(); for(String key : pkValues.keySet()) valStrings.add("'" + pkValues.get(key) + "' as " + key); return String.join(", ", valStrings); diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyValue.java b/Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyValue.java index 0acc28da22..eb354e2261 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyValue.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyValue.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.log4j.Logger; import org.gusdb.fgputil.FormatUtil; @@ -116,6 +117,14 @@ public static boolean rawValuesDiffer(Map map1, Map tablePrefix + entry.getKey() + " = '" + entry.getValue() + "'") + .collect(Collectors.joining(" AND ")); + } + @Override public boolean equals(Object obj) { return (obj instanceof PrimaryKeyValue) && @@ -149,4 +158,5 @@ public static List toStringArrays( ).toArray(new String[pkValue._pkDef.getColumnRefs().length]) ); } + } diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/RecordClass.java b/Model/src/main/java/org/gusdb/wdk/model/record/RecordClass.java index 5fe4ed8413..1872fdb0a2 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/RecordClass.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/RecordClass.java @@ -1037,7 +1037,7 @@ public static StepFilter resolveStepFilterReferenceByName(String name, WdkModel * A bulk query is either an original attribute or table query, that is, it either doesn't any param, or * just one param with the name of Utilities.PARAM_USER_ID. */ - void validateBulkQuery(Query query) throws WdkModelException { + public void validateBulkQuery(Query query) throws WdkModelException { validateQuery(query); // Further limit the attribute/table query to have only user_id param diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/QueryBasedPrimaryKeyAliasPlugin.java b/Model/src/main/java/org/gusdb/wdk/model/record/alias/AliasQueryBasedPrimaryKeyAliasPlugin.java similarity index 92% rename from Model/src/main/java/org/gusdb/wdk/model/record/QueryBasedPrimaryKeyAliasPlugin.java rename to Model/src/main/java/org/gusdb/wdk/model/record/alias/AliasQueryBasedPrimaryKeyAliasPlugin.java index e82fdaefaa..64b9088e17 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/QueryBasedPrimaryKeyAliasPlugin.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/alias/AliasQueryBasedPrimaryKeyAliasPlugin.java @@ -1,4 +1,4 @@ -package org.gusdb.wdk.model.record; +package org.gusdb.wdk.model.record.alias; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -14,6 +14,8 @@ import org.gusdb.wdk.model.query.QueryInstance; import org.gusdb.wdk.model.query.SqlQuery; import org.gusdb.wdk.model.query.spec.QueryInstanceSpec; +import org.gusdb.wdk.model.record.RecordClass; +import org.gusdb.wdk.model.record.RecordNotFoundException; import org.gusdb.wdk.model.user.StepContainer; import org.gusdb.wdk.model.user.User; @@ -23,13 +25,13 @@ * * @author rdoherty */ -public class QueryBasedPrimaryKeyAliasPlugin implements PrimaryKeyAliasPlugin { +public class AliasQueryBasedPrimaryKeyAliasPlugin implements PrimaryKeyAliasPlugin { private static final String ALIAS_OLD_KEY_COLUMN_PREFIX = "old_"; private Query _aliasQuery; - public QueryBasedPrimaryKeyAliasPlugin(Query aliasQuery) { + public AliasQueryBasedPrimaryKeyAliasPlugin(Query aliasQuery) { _aliasQuery = aliasQuery; } diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/GenericRecordPrimaryKeyAliasPlugin.java b/Model/src/main/java/org/gusdb/wdk/model/record/alias/PassThroughPrimaryKeyAliasPlugin.java similarity index 82% rename from Model/src/main/java/org/gusdb/wdk/model/record/GenericRecordPrimaryKeyAliasPlugin.java rename to Model/src/main/java/org/gusdb/wdk/model/record/alias/PassThroughPrimaryKeyAliasPlugin.java index 77506cf9fd..dd83adb63e 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/GenericRecordPrimaryKeyAliasPlugin.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/alias/PassThroughPrimaryKeyAliasPlugin.java @@ -1,23 +1,24 @@ -package org.gusdb.wdk.model.record; +package org.gusdb.wdk.model.record.alias; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.gusdb.wdk.model.WdkModelException; +import org.gusdb.wdk.model.record.RecordNotFoundException; import org.gusdb.wdk.model.user.User; /** * All record class primary key attributes need to be associated either with an alias query * or an alias query so that the primary key may be determined to be valid. An invalid * primary key will result in a 404 to the client. A valid primary key that otherwise has - * an issue will result in a 500 to the client. In some cases, there is no way to access + * an issue will result in a 500 to the client. In some cases, there is no way to assess * the validity of a primary key. In those cases, we need a simple plugin that simply * returns the primary key given by the client. * * @author crisl-adm */ -public class GenericRecordPrimaryKeyAliasPlugin implements PrimaryKeyAliasPlugin { +public class PassThroughPrimaryKeyAliasPlugin implements PrimaryKeyAliasPlugin { @Override public List> getPrimaryKey(User user, Map inputPkValues) diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyAliasPlugin.java b/Model/src/main/java/org/gusdb/wdk/model/record/alias/PrimaryKeyAliasPlugin.java similarity index 76% rename from Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyAliasPlugin.java rename to Model/src/main/java/org/gusdb/wdk/model/record/alias/PrimaryKeyAliasPlugin.java index ec6f0394d0..7649b3f506 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/PrimaryKeyAliasPlugin.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/alias/PrimaryKeyAliasPlugin.java @@ -1,9 +1,10 @@ -package org.gusdb.wdk.model.record; +package org.gusdb.wdk.model.record.alias; import java.util.List; import java.util.Map; import org.gusdb.wdk.model.WdkModelException; +import org.gusdb.wdk.model.record.RecordNotFoundException; import org.gusdb.wdk.model.user.User; public interface PrimaryKeyAliasPlugin { diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/alias/QueryBasedPrimaryKeyAliasPlugin.java b/Model/src/main/java/org/gusdb/wdk/model/record/alias/QueryBasedPrimaryKeyAliasPlugin.java new file mode 100644 index 0000000000..9a40f4a8a5 --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/record/alias/QueryBasedPrimaryKeyAliasPlugin.java @@ -0,0 +1,135 @@ +package org.gusdb.wdk.model.record.alias; + +import static org.gusdb.fgputil.FormatUtil.NL; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +import org.apache.log4j.Logger; +import org.gusdb.fgputil.db.pool.DatabaseInstance; +import org.gusdb.fgputil.db.runner.SQLRunner; +import org.gusdb.fgputil.functional.Functions; +import org.gusdb.wdk.model.Reference; +import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.model.WdkModelException; +import org.gusdb.wdk.model.WdkRuntimeException; +import org.gusdb.wdk.model.dbms.ResultList; +import org.gusdb.wdk.model.dbms.SqlResultList; +import org.gusdb.wdk.model.query.Query; +import org.gusdb.wdk.model.query.SqlQuery; +import org.gusdb.wdk.model.query.param.Param; +import org.gusdb.wdk.model.record.PrimaryKeyDefinition; +import org.gusdb.wdk.model.record.PrimaryKeyValue; +import org.gusdb.wdk.model.record.RecordNotFoundException; +import org.gusdb.wdk.model.user.User; + +public abstract class QueryBasedPrimaryKeyAliasPlugin implements PrimaryKeyAliasPlugin { + + private static final Logger LOG = Logger.getLogger(QueryBasedPrimaryKeyAliasPlugin.class); + + private static final Pattern DEFAULT_ID_REGEX = Pattern.compile("^[\\w\\-\\|\\.\\:\\(\\)]+$"); + + // required + protected abstract String getRecordClassFullName(); + protected abstract String getQueryFullName(); + + // supply PK col value regex but allow subclasses to override + protected boolean pkColValuePassesRegex(String name, String value) { + return DEFAULT_ID_REGEX.matcher(value).find(); + } + + @Override + public List> getPrimaryKey(User user, Map inputPkValues) + throws WdkModelException, RecordNotFoundException { + + WdkModel wdkModel = user.getWdkModel(); + DatabaseInstance appDb = wdkModel.getAppDb(); + + // find the PK def for this lookup + PrimaryKeyDefinition pkDef = wdkModel + .getRecordClassByFullName(getRecordClassFullName()) + .orElseThrow(() -> new WdkModelException("Recordclass " + getRecordClassFullName() + " could not be found in this model.")) + .getPrimaryKeyDefinition(); + + PrimaryKeyValue pkValue = Functions.mapException( + () -> new PrimaryKeyValue(pkDef, inputPkValues), + e -> new RecordNotFoundException(e.getMessage())); + + LOG.debug("Sequence alias plugin called with pk: " + pkValue.getValuesAsString()); + + // check for SQL injection + for (Entry pkCol : pkValue.getValues().entrySet()) { + if (!pkColValuePassesRegex(pkCol.getKey(), pkCol.getValue())) { + throw new RecordNotFoundException("Value submitted for column " + pkCol.getKey() + " is not valid."); + } + } + + Reference queryRef = new Reference(getQueryFullName()); + Query query = wdkModel.getQuerySet(queryRef.getSetName()).getQuery(queryRef.getElementName()); + + if (!(query instanceof SqlQuery)) { + throw new WdkModelException(queryRef.getTwoPartName() + " is not a valid query. " + + getClass().getName() + " must specify a SqlQuery reference."); + } + + String querySql = ((SqlQuery)query).getSql(); + + // disallow partition keys macro; we want all partitions and this encourages + // model writer to choose an unpartitioned table which is more performant + if (querySql.contains(SqlQuery.PARTITION_KEYS_MACRO)) { + throw new WdkModelException("Query '" + queryRef.getTwoPartName() + + " referenced by " + getClass().getName() + " cannot contain the" + + " partition keys macro (" + SqlQuery.PARTITION_KEYS_MACRO + ")"); + } + + // Get params of specified query + Map paramMap = query.getParamMap(); + String sql = switch (paramMap.size()) { + + case 0 -> + // if zero params, assume attribute query returning all rows with PKs + "SELECT " + String.join(", ", pkDef.getColumnRefs()) + + " FROM ( " + querySql + " )" + + " WHERE " + pkValue.createWhereClause(""); + + case 1 -> + // if one param, assume ID search query with single dataset param; + paramMap.values().iterator().next().replaceSql(querySql, null, + // fill in dataset param internal value with a hard-coded select of a single row of PK cols + "SELECT " + PrimaryKeyDefinition.createSelectClause(pkValue.getValues()) + appDb.getPlatform().getDummyTable()); + + default -> + throw new WdkModelException("Specified query for alias plugin does not contain 0 or 1 parameters."); + }; + + LOG.debug("Supplied full_id passed regex. Will call the following SQL: " + NL + sql); + + List> foundPks = new SQLRunner(appDb.getDataSource(), sql).executeQuery(rs -> { + try { + List> rows = new ArrayList<>(); + ResultList result = new SqlResultList(rs); + while (result.next()) { + PrimaryKeyValue row = new PrimaryKeyValue(pkDef, result); + LOG.debug("Found mapped ID: " + row.getValuesAsString()); + rows.add(row.getRawValues()); + } + return rows; + } + catch(WdkModelException e) { + throw new WdkRuntimeException(e); + } + }); + + if (foundPks.isEmpty()) { + throw new RecordNotFoundException("Requested primary key '" + pkValue.getValuesAsString() + "' does not map to any sequence."); + } + + return foundPks; + + } + + +} From 0d3650a8da690fc508b6375f14105def90eed7bd Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Sat, 31 Oct 2026 22:23:07 -0400 Subject: [PATCH 2/2] Handle attribute queries that have ID_SQL macros --- .../record/alias/QueryBasedPrimaryKeyAliasPlugin.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/alias/QueryBasedPrimaryKeyAliasPlugin.java b/Model/src/main/java/org/gusdb/wdk/model/record/alias/QueryBasedPrimaryKeyAliasPlugin.java index 9a40f4a8a5..ac2419a336 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/alias/QueryBasedPrimaryKeyAliasPlugin.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/alias/QueryBasedPrimaryKeyAliasPlugin.java @@ -13,6 +13,7 @@ import org.gusdb.fgputil.db.runner.SQLRunner; import org.gusdb.fgputil.functional.Functions; import org.gusdb.wdk.model.Reference; +import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkRuntimeException; @@ -75,7 +76,9 @@ public List> getPrimaryKey(User user, Map in getClass().getName() + " must specify a SqlQuery reference."); } - String querySql = ((SqlQuery)query).getSql(); + // use hard-coded select of a single row of PK cols to represent both ID SQL and dataset param internal value + String idSelectSql = "SELECT " + PrimaryKeyDefinition.createSelectClause(pkValue.getValues()) + appDb.getPlatform().getDummyTable(); + String querySql = ((SqlQuery)query).getSql().replace(Utilities.MACRO_ID_SQL, idSelectSql); // disallow partition keys macro; we want all partitions and this encourages // model writer to choose an unpartitioned table which is more performant @@ -97,9 +100,7 @@ public List> getPrimaryKey(User user, Map in case 1 -> // if one param, assume ID search query with single dataset param; - paramMap.values().iterator().next().replaceSql(querySql, null, - // fill in dataset param internal value with a hard-coded select of a single row of PK cols - "SELECT " + PrimaryKeyDefinition.createSelectClause(pkValue.getValues()) + appDb.getPlatform().getDummyTable()); + paramMap.values().iterator().next().replaceSql(querySql, null, idSelectSql); default -> throw new WdkModelException("Specified query for alias plugin does not contain 0 or 1 parameters.");