diff --git a/Model/src/main/java/org/gusdb/wdk/core/api/JsonKeys.java b/Model/src/main/java/org/gusdb/wdk/core/api/JsonKeys.java index 667b21465..1532f01aa 100644 --- a/Model/src/main/java/org/gusdb/wdk/core/api/JsonKeys.java +++ b/Model/src/main/java/org/gusdb/wdk/core/api/JsonKeys.java @@ -91,6 +91,7 @@ public class JsonKeys { public static final String IS_AVAILABLE = "isAvailable"; public static final String NEW_BUILD = "newBuild"; public static final String REVISE_BUILD = "reviseBuild"; + public static final String SINGLE_RECORD_ONLY = "supportsSingleRecordOnly"; // patch command keys public static final String DELETE = "delete"; diff --git a/Model/src/main/java/org/gusdb/wdk/model/WdkModelException.java b/Model/src/main/java/org/gusdb/wdk/model/WdkModelException.java index 557379771..9f643146c 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/WdkModelException.java +++ b/Model/src/main/java/org/gusdb/wdk/model/WdkModelException.java @@ -1,5 +1,12 @@ package org.gusdb.wdk.model; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.gusdb.fgputil.functional.FunctionalInterfaces.ConsumerWithException; +import org.gusdb.fgputil.functional.FunctionalInterfaces.Procedure; +import org.gusdb.fgputil.functional.FunctionalInterfaces.SupplierWithException; + /** * This exception should be thrown out if the cause is not related to user's * input. For example, the cause of the exception can be a mistake in the model @@ -43,6 +50,64 @@ public static WdkModelException translateFrom(Exception e, String newMessage) { return (t instanceof WdkModelException ? (WdkModelException)t : new WdkModelException(newMessage, t)); } + /** + * Meant to be used with the Supplier version of unwrap(). This function will + * take a SupplierWithException, catch the exception and wrap it in a Runtime + * Exception, returning a Supplier appropriate for "naked" lambdas. + * + * @param type returned by the returned supplier + * @param supplier supplier with exception + * @return supplier without exception (wrapped in runtime exception) + */ + public static T wrap(SupplierWithException supplier) { + try { + return supplier.get(); + } + catch (Exception e) { + throw (e instanceof RuntimeException) ? (RuntimeException)e : new WdkRuntimeException(e); + } + } + + /** + * Meant to be used with wrap(). This function takes a non-throwing Supplier, + * calls it, catches any RuntimeExceptions, and throws a WdkModelException + * that is either 1) the underlying cause, if the cause is a WdkModelException, + * or 2) a new WdkModelException wrapping the exception. + * + * @param type returned by the supplier + * @param supplier a supplier + * @return the supplied value + * @throws WdkModelException if an exception occurs + */ + public static T unwrap(Supplier supplier) throws WdkModelException { + try { + return supplier.get(); + } + catch(Exception e) { + return unwrap(e); + } + } + + public static Consumer wrap(ConsumerWithException consumer) { + return val -> { + try { + consumer.accept(val); + } + catch (Exception e) { + throw (e instanceof RuntimeException) ? (RuntimeException)e : new WdkRuntimeException(e); + } + }; + } + + public static void unwrap(Procedure p) throws WdkModelException { + try { + p.perform(); + } + catch (Exception e) { + unwrap(e); + } + } + public static T unwrap(Exception e) throws WdkModelException { throw translateFrom(e, e.getMessage()); } diff --git a/Model/src/main/java/org/gusdb/wdk/model/analysis/ExternalAnalyzer.java b/Model/src/main/java/org/gusdb/wdk/model/analysis/ExternalAnalyzer.java index 607896e6a..3dc0927b5 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/analysis/ExternalAnalyzer.java +++ b/Model/src/main/java/org/gusdb/wdk/model/analysis/ExternalAnalyzer.java @@ -26,6 +26,7 @@ import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkUserException; import org.gusdb.wdk.model.answer.AnswerValue; +import org.gusdb.wdk.model.question.Question; import org.gusdb.wdk.model.record.Field; import org.gusdb.wdk.model.record.RecordClass; import org.gusdb.wdk.model.record.TableField; @@ -150,7 +151,7 @@ public ExecutionStatus runAnalysis(AnswerValue answerValue, StatusLogger log) // if hasHeader and display value map requested, dump if (hasHeader && determineBooleanProperty(DUMP_HEADER_DISPLAY_MAP_PROP_KEY, DUMP_HEADER_DISPLAY_MAP_BY_DEFAULT)) { - dumpHeaderDisplayMap(answerValue.getQuestion().getRecordClass(), storageDir); + dumpHeaderDisplayMap(answerValue.getQuestion(), storageDir); } try { @@ -197,7 +198,8 @@ protected static void writeContentToFile(String storageDir, String fileName, Str } } - private void dumpHeaderDisplayMap(RecordClass recordClass, String storageDir) throws WdkModelException { + private void dumpHeaderDisplayMap(Question question, String storageDir) throws WdkModelException { + RecordClass recordClass = question.getRecordClass(); List attributeNames = getConfiguredFields(EXTRACTED_ATTRIBS_PROP_KEY); List tableNames = getConfiguredFields(EXTRACTED_TABLES_PROP_KEY); File mappingOutFile = Paths.get(storageDir, HEADER_MAPPING_FILE_NAME).toFile(); @@ -212,7 +214,7 @@ private void dumpHeaderDisplayMap(RecordClass recordClass, String storageDir) th writeField(out, attr.get(), ""); } for (String tableName : tableNames) { - TableField table = recordClass.getTableFieldMap().get(tableName); + TableField table = question.getTableFieldMap().get(tableName); if (table == null) { LOG.warn("Table '" + tableName + "', specified in analysis plugin, is not valid for record class '" + recordClass.getFullName() + "'."); continue; 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 e5c492ec1..29e02b9e9 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 @@ -368,7 +368,10 @@ public String getAnswerTableSql(Query tableQuery) public String getTableFieldResultSql(TableField tableField) throws WdkModelException { // has to get a clean copy of the attribute query, without pk params appended - Query tableQuery = tableField.getUnwrappedQuery(); + Query tableQuery = tableField.getQuery().leftOrElseThrow(() -> + new WdkModelException("Table field " + tableField.getFullName() + + " does not reference a SqlQuery, required for this function.")) + .getUnwrappedQuery(); // get and run the paged table query sql LOG.debug("AnswerValue: getTableFieldResultSql(): going to getPagedTableSql()"); @@ -376,6 +379,7 @@ public String getTableFieldResultSql(TableField tableField) throws WdkModelExcep return getAnswerTableSql(tableQuery); } + // NOTE: this method is overridden in SingleRecordAnswerValue to support tables with process queries; can assume SQL queries here public ResultList getTableFieldResultList(TableField tableField) throws WdkModelException { return getTableFieldResultList(tableField, getTableFieldResultSql(tableField)); } @@ -388,7 +392,7 @@ public ResultList getTableFieldResultList(TableField tableField, String customSq ResultSet resultSet = null; try { LOG.debug("AnswerValue: getTableFieldResultList(): returning SQL for TableField '" + tableField.getName() + "': \n" + customSql); - resultSet = SqlUtils.executeQuery(dataSource, customSql, tableField.getUnwrappedQuery().getFullName() + "_table"); + resultSet = SqlUtils.executeQuery(dataSource, customSql, tableField.getQueryFullName() + "_table"); } catch (SQLException e) { throw new WdkModelException(e); diff --git a/Model/src/main/java/org/gusdb/wdk/model/answer/TableFieldProcessQueryResult.java b/Model/src/main/java/org/gusdb/wdk/model/answer/TableFieldProcessQueryResult.java new file mode 100644 index 000000000..a06e9caf7 --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/answer/TableFieldProcessQueryResult.java @@ -0,0 +1,44 @@ +package org.gusdb.wdk.model.answer; + +import java.util.Map; + +import org.gusdb.wdk.model.WdkModelException; +import org.gusdb.wdk.model.answer.single.SingleRecordQuestionParam; +import org.gusdb.wdk.model.dbms.ArrayResultList; +import org.gusdb.wdk.model.query.ProcessQuery; +import org.gusdb.wdk.model.query.ProcessQueryInstance; +import org.gusdb.wdk.model.query.spec.QueryInstanceSpec; +import org.gusdb.wdk.model.record.TableField; +import org.gusdb.wdk.model.user.StepContainer; +import org.gusdb.wdk.model.user.User; +import org.json.JSONArray; + +public class TableFieldProcessQueryResult { + + public static ArrayResultList getResultList(User user, TableField tableField, Map _pkMap) throws WdkModelException { + + if (tableField.hasSqlQuery()) { + throw new WdkModelException("Table field " + tableField.getFullName() + " does not reference a ProcessQuery, required for this function."); + } + + // create param map + Map params = Map.of( + + // add primary key param + SingleRecordQuestionParam.PRIMARY_KEY_PARAM_NAME, + new JSONArray(_pkMap.values()).toString(), + + // add table name param + TableField.TABLE_NAME_PARAM_NAME, + tableField.getName() + ); + + // create process query instance to fetch results + ProcessQueryInstance queryInstance = (ProcessQueryInstance)ProcessQuery.makeQueryInstance( + QueryInstanceSpec.builder().putAll(params) + .buildRunnable(user, tableField.getQuery().rightOrElseThrow(WdkModelException::new), StepContainer.emptyContainer())); + + return queryInstance.getUncachedResults(); + } + +} \ No newline at end of file diff --git a/Model/src/main/java/org/gusdb/wdk/model/answer/factory/DynamicRecordInstanceList.java b/Model/src/main/java/org/gusdb/wdk/model/answer/factory/DynamicRecordInstanceList.java index 2e623e03f..c453b9756 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/answer/factory/DynamicRecordInstanceList.java +++ b/Model/src/main/java/org/gusdb/wdk/model/answer/factory/DynamicRecordInstanceList.java @@ -212,20 +212,22 @@ public void integrateTableQuery(TableField tableField) throws WdkModelException, // make table values PrimaryKeyDefinition pkDef = _answerValue.getQuestion().getRecordClass().getPrimaryKeyDefinition(); - Query tableQuery = tableField.getWrappedQuery(); while (resultList.next()) { PrimaryKeyValue primaryKey = new PrimaryKeyValue(pkDef, resultList); DynamicRecordInstance record = get(primaryKey); if (record == null) { - StringBuffer error = new StringBuffer(); - error.append("Paged table query [" + tableQuery.getFullName()); + StringBuilder error = new StringBuilder(); + error.append("Paged table query [" + tableField.getQueryFullName()); error.append("] returned rows that doesn't match the paged "); error.append("records. ("); error.append(primaryKey.getValuesAsString()); - error.append(").\nPaged table SQL:\n" + _answerValue.getAnswerTableSql(tableQuery)); - error.append("\n" + "Paged ID SQL:\n" + _answerValue.getPagedIdSql()); + if (tableField.hasSqlQuery()) { + Query tableQuery = tableField.getQuery().getLeft().getWrappedQuery(); + error.append(").\nPaged table SQL:\n" + _answerValue.getAnswerTableSql(tableQuery)); + error.append("\n" + "Paged ID SQL:\n" + _answerValue.getPagedIdSql()); + } throw new WdkModelException(error.toString()); } @@ -233,7 +235,7 @@ public void integrateTableQuery(TableField tableField) throws WdkModelException, // initialize a row in table value tableValue.initializeRow(resultList); } - LOG.debug("Table query [" + tableQuery + "] integrated."); + LOG.debug("Table query [" + tableField.getQueryFullName() + "] integrated."); } } diff --git a/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordAnswerValue.java b/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordAnswerValue.java index 389d11c8d..d3551f69f 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordAnswerValue.java +++ b/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordAnswerValue.java @@ -17,11 +17,14 @@ import org.gusdb.wdk.model.WdkUserException; import org.gusdb.wdk.model.answer.AnswerValue; import org.gusdb.wdk.model.answer.ResultSizeFactory; +import org.gusdb.wdk.model.answer.TableFieldProcessQueryResult; import org.gusdb.wdk.model.answer.spec.AnswerSpec; +import org.gusdb.wdk.model.dbms.ResultList; import org.gusdb.wdk.model.record.DynamicRecordInstance; import org.gusdb.wdk.model.record.PrimaryKeyIterator; import org.gusdb.wdk.model.record.RecordClass; import org.gusdb.wdk.model.record.RecordInstance; +import org.gusdb.wdk.model.record.TableField; public class SingleRecordAnswerValue extends AnswerValue { @@ -151,4 +154,14 @@ public boolean cacheInitiallyExistedForSpec() throws WdkModelException { // does not use WDK cache return false; } + + @Override + public ResultList getTableFieldResultList(TableField tableField) throws WdkModelException { + if (tableField.hasSqlQuery()) { + return super.getTableFieldResultList(tableField); + } + else { + return TableFieldProcessQueryResult.getResultList(_requestingUser, tableField, _pkMap); + } + } } diff --git a/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordQuestion.java b/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordQuestion.java index 7f0b2edf2..f22b151e5 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordQuestion.java +++ b/Model/src/main/java/org/gusdb/wdk/model/answer/single/SingleRecordQuestion.java @@ -10,6 +10,7 @@ import org.gusdb.wdk.model.question.DynamicAttributeSet; import org.gusdb.wdk.model.question.Question; import org.gusdb.wdk.model.record.RecordClass; +import org.gusdb.wdk.model.record.TableField; public class SingleRecordQuestion extends Question { @@ -67,4 +68,9 @@ public boolean isBoolean() { public SingleRecordQuestionParam getParam() { return _param; } + + @Override + public Map getTableFieldMap() { + return getRecordClass().getTableFieldMap(true); + } } diff --git a/Model/src/main/java/org/gusdb/wdk/model/answer/stream/FileBasedRecordStream.java b/Model/src/main/java/org/gusdb/wdk/model/answer/stream/FileBasedRecordStream.java index a7be7e394..37084351e 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/answer/stream/FileBasedRecordStream.java +++ b/Model/src/main/java/org/gusdb/wdk/model/answer/stream/FileBasedRecordStream.java @@ -400,7 +400,7 @@ private static Map>> assembleTableFiles(A */ private static TwoTuple> writeTableFile(AnswerValue answerValue, Path tempDir, TableField table) throws WdkModelException { Timer t = new Timer(); - LOG.debug("writeTableFile(): Starting table: " + table.getName() + "(query: " + table.getWrappedQuery().getName() + ")"); + LOG.debug("writeTableFile(): Starting table: " + table.getName() + "(query: " + table.getQueryFullName() + ")"); // Appending table designation to query name for file to more easily distinguish // these files from those supporting attribute queries and to avoid name collisions. @@ -413,9 +413,9 @@ private static TwoTuple> writeTableFile(AnswerValue answerValu resultList = answerValue.getTableFieldResultList(table); // Transfer the result list content to the CSV file provided. - LOG.debug("writeTableFile(): Starting iteration over result list for query " + table.getWrappedQuery().getName() + ": " + t.getElapsedString()); + LOG.debug("writeTableFile(): Starting iteration over result list for query " + table.getQueryFullName() + ": " + t.getElapsedString()); assembleCsvFile(filePath, columnNames, resultList); - LOG.debug("writeTableFile(): Finished iteration over result list for query " + table.getWrappedQuery().getName() + ": " + t.getElapsedString()); + LOG.debug("writeTableFile(): Finished iteration over result list for query " + table.getQueryFullName() + ": " + t.getElapsedString()); // open file permissions and return the path to the temporary CSV file filePath.toFile().setWritable(true, false); diff --git a/Model/src/main/java/org/gusdb/wdk/model/dbms/ArrayResultList.java b/Model/src/main/java/org/gusdb/wdk/model/dbms/ArrayResultList.java index 38d9452a4..102cd095d 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/dbms/ArrayResultList.java +++ b/Model/src/main/java/org/gusdb/wdk/model/dbms/ArrayResultList.java @@ -29,8 +29,11 @@ public class ArrayResultList implements ResultList, WsfResponseListener { * @throws WdkModelException * if the result has fewer columns than the column definition */ - public ArrayResultList(Map columns) throws WdkModelException { - _columns = new LinkedHashMap(columns); + public ArrayResultList(List columns) throws WdkModelException { + _columns = new LinkedHashMap(); + for (int i = 0; i < columns.size(); i++) { + _columns.put(columns.get(i), i); + } _rows = new ArrayList<>(); _attachments = new LinkedHashMap<>(); _rowIndex = -1; diff --git a/Model/src/main/java/org/gusdb/wdk/model/query/ProcessQueryInstance.java b/Model/src/main/java/org/gusdb/wdk/model/query/ProcessQueryInstance.java index 6fad22683..9e10017b1 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/query/ProcessQueryInstance.java +++ b/Model/src/main/java/org/gusdb/wdk/model/query/ProcessQueryInstance.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.apache.log4j.Logger; import org.gusdb.fgputil.db.SqlUtils; @@ -21,6 +22,7 @@ import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkDelayedResultException; import org.gusdb.wdk.model.WdkModelException; +import org.gusdb.wdk.model.dbms.ArrayResultList; import org.gusdb.wdk.model.dbms.CacheFactory; import org.gusdb.wdk.model.dbms.ResultList; import org.gusdb.wdk.model.query.spec.QueryInstanceSpec; @@ -223,6 +225,13 @@ private String buildCacheInsertSql( return sql.append(")").toString(); } + public ArrayResultList getUncachedResults() throws WdkModelException { + List columns = Arrays.stream(_query.getColumns()).map(Column::getName).collect(Collectors.toList()); + ArrayResultList resultsListener = new ArrayResultList(columns); + invokeWsf(resultsListener); + return resultsListener; + } + private void invokeWsf(WsfResponseListener listener) throws WdkModelException { long start = System.currentTimeMillis(); diff --git a/Model/src/main/java/org/gusdb/wdk/model/question/Question.java b/Model/src/main/java/org/gusdb/wdk/model/question/Question.java index 0790c3ad4..bf97e03ac 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/question/Question.java +++ b/Model/src/main/java/org/gusdb/wdk/model/question/Question.java @@ -305,7 +305,7 @@ public void addDynamicAttributeSet(DynamicAttributeSet dynamicAttributes) { public Map getFields() { Map fields = new LinkedHashMap<>(); Map attributes = getAttributeFieldMap(); - Map tables = _recordClass.getTableFieldMap(); + Map tables = getTableFieldMap(); fields.putAll(attributes); fields.putAll(tables); @@ -1134,4 +1134,8 @@ public String getNameForLogging() { return getFullName(); } + public Map getTableFieldMap() { + return getRecordClass().getTableFieldMap(false); + } + } diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/DynamicRecordInstance.java b/Model/src/main/java/org/gusdb/wdk/model/record/DynamicRecordInstance.java index 46484a37c..e60e7342b 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/DynamicRecordInstance.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/DynamicRecordInstance.java @@ -60,7 +60,7 @@ public DynamicRecordInstance(User user, Question question, DynamicRecordInstance @Override protected Collection getAvailableTableFields() { - return _recordClass.getTableFieldMap().values(); + return _recordClass.getTableFieldMap(true).values(); } private void fillColumnAttributeValues(Query attributeQuery) diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/DynamicTableValue.java b/Model/src/main/java/org/gusdb/wdk/model/record/DynamicTableValue.java index 0bcea94f2..e2aa0c8f6 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/DynamicTableValue.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/DynamicTableValue.java @@ -1,15 +1,20 @@ package org.gusdb.wdk.model.record; +import static org.gusdb.wdk.model.WdkModelException.unwrap; +import static org.gusdb.wdk.model.WdkModelException.wrap; + import java.util.Iterator; import org.apache.log4j.Logger; import org.gusdb.fgputil.Timer; +import org.gusdb.fgputil.functional.Either; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkRuntimeException; import org.gusdb.wdk.model.answer.AnswerValue; +import org.gusdb.wdk.model.answer.TableFieldProcessQueryResult; import org.gusdb.wdk.model.dbms.ResultList; +import org.gusdb.wdk.model.query.ProcessQuery; import org.gusdb.wdk.model.query.Query; -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.user.StepContainer; @@ -19,27 +24,26 @@ public class DynamicTableValue extends TableValue { private static final Logger LOG = Logger.getLogger(DynamicTableValue.class); - private final QueryInstance _queryInstance; + private final Either _query; + private final PrimaryKeyValue _primaryKey; + private final User _user; private boolean _rowsLoaded = false; public DynamicTableValue(PrimaryKeyValue primaryKey, TableField tableField, User user) throws WdkModelException { super(tableField); - - Query query = tableField.getWrappedQuery(); - if (query instanceof SqlQuery) - query = AnswerValue.addPartKeysToAttrOrTableSqlQuery((SqlQuery)query, getTableField().getRecordClass(), primaryKey); - - // create query instance; TableValue will initialize rows by itself - _queryInstance = Query.makeQueryInstance(QueryInstanceSpec.builder() - .putAll(primaryKey.getValues()).buildRunnable(user, query, StepContainer.emptyContainer())); + _primaryKey = primaryKey; + _user = user; + _query = unwrap(() -> tableField.getQuery().mapLeft(qp -> wrap(() -> + AnswerValue.addPartKeysToAttrOrTableSqlQuery( + qp.getWrappedQuery(), getTableField().getRecordClass(), primaryKey)))); } private void loadRowsFromQuery() { - try (ResultList resultList = _queryInstance.getResults()) { + try (ResultList resultList = getResultList(_query, _primaryKey, _user)) { int rowCount = 0; Timer t = new Timer(); - Integer maxRows = _queryInstance.getQuery().getWdkModel().getModelConfig().getMaxTableValueRows(); + Integer maxRows = _tableField.getWdkModel().getModelConfig().getMaxTableValueRows(); while (resultList.next()) { LOG.trace("Row " + (++rowCount) + ": fetched in " + t.getElapsedStringAndRestart()); if (rowCount > maxRows) @@ -53,6 +57,17 @@ private void loadRowsFromQuery() { LOG.debug("Table value rows loaded."); } + // special processing for table values since process queries serving table fields should not be cached + private ResultList getResultList(Either query, PrimaryKeyValue primaryKey, User user) throws WdkModelException { + return query.isLeft() + // create query instance; TableValue will initialize rows by itself + ? Query.makeQueryInstance(QueryInstanceSpec.builder() + .putAll(primaryKey.getValues()) + .buildRunnable(user, query.getLeft(), StepContainer.emptyContainer())) + .getResults() + : TableFieldProcessQueryResult.getResultList(user, _tableField, primaryKey.getRawValues()); + } + @Override public Iterator iterator() { if (!_rowsLoaded) { 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 ba1b49eb0..af982564b 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 @@ -3,6 +3,7 @@ import static org.gusdb.fgputil.FormatUtil.NL; import static org.gusdb.fgputil.functional.Functions.fSwallow; import static org.gusdb.fgputil.functional.Functions.mapToList; +import static org.gusdb.wdk.model.WdkModelException.wrap; import java.io.PrintWriter; import java.lang.reflect.InvocationTargetException; @@ -572,36 +573,19 @@ public String getFullName() { return fullName; } - public Map getTableFieldMap() { - return getTableFieldMap(FieldScope.ALL); - } + public Map getTableFieldMap(boolean includeSingleRecordTables) { - public Map getTableFieldMap(FieldScope scope) { Map fields = new LinkedHashMap<>(); for (TableField field : tableFieldsMap.values()) - if (scope.isFieldInScope(field)) + if (includeSingleRecordTables || !field.isForSingleRecordOnly()) fields.put(field.getName(), field); return fields; } - // used by report maker, adding display names in map so later the tables show - // sorted by display name - public Map getTableFieldMap(FieldScope scope, boolean useDisplayNamesAsKeys) { - if (!useDisplayNamesAsKeys) - return getTableFieldMap(scope); - - Map fields = new LinkedHashMap<>(); - for (TableField field : tableFieldsMap.values()) - if (scope.isFieldInScope(field)) - fields.put(field.getDisplayName(), field); - - return fields; - } - - public TableField[] getTableFields() { - Map tables = getTableFieldMap(); + public TableField[] getTableFields(boolean includeSingleRecordTables) { + Map tables = getTableFieldMap(includeSingleRecordTables); TableField[] array = new TableField[tables.size()]; tables.values().toArray(array); return array; @@ -634,7 +618,7 @@ public Field[] getFields() { // copy attribute fields attributeFieldMap.values().toArray(fields); // copy table fields - TableField[] tableFields = getTableFields(); + TableField[] tableFields = getTableFields(true); System.arraycopy(tableFields, 0, fields, attributeCount, tableCount); return fields; } @@ -1002,17 +986,21 @@ private void resolveTableFieldReferences(WdkModel wdkModel) throws WdkModelExcep for (TableField tableField : tableFieldsMap.values()) { tableField.resolveReferences(wdkModel); - SqlQuery query = tableField.getUnwrappedQuery(); - if (_partitionKeysSqlQuery == null && query.getSql().contains(SqlQuery.PARTITION_KEYS_MACRO)) { - throw new WdkModelException("Table query " + query.getFullName() - + "contains the macro " + SqlQuery.PARTITION_KEYS_MACRO - + " but record class " + getName() + " does not define a partition key query ref"); - } + // skip the following for process queries + WdkModelException.unwrap(() -> tableField.getQuery().ifLeft(wrap(qp -> { + SqlQuery query = qp.getUnwrappedQuery(); - SqlQuery tableQuery = RecordClass.prepareQuery(wdkModel, query, paramNames); - tableQueries.put(query.getFullName(), tableQuery); + if (_partitionKeysSqlQuery == null && query.getSql().contains(SqlQuery.PARTITION_KEYS_MACRO)) { + throw new WdkModelException("Table query " + query.getFullName() + + "contains the macro " + SqlQuery.PARTITION_KEYS_MACRO + + " but record class " + getName() + " does not define a partition key query ref"); + } + + SqlQuery tableQuery = RecordClass.prepareQuery(wdkModel, query, paramNames); + tableQueries.put(query.getFullName(), tableQuery); + }))); } } diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/StaticRecordInstance.java b/Model/src/main/java/org/gusdb/wdk/model/record/StaticRecordInstance.java index e3d384cf4..2aa087ba2 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/StaticRecordInstance.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/StaticRecordInstance.java @@ -149,4 +149,5 @@ public IdAttributeValue getIdAttributeValue(IdAttributeField field) { public void removeTableValue(String tableName) { _tableValueCache.remove(tableName); } + } diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/TableField.java b/Model/src/main/java/org/gusdb/wdk/model/record/TableField.java index 28a82aa74..9d6bdd2a1 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/TableField.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/TableField.java @@ -1,6 +1,7 @@ package org.gusdb.wdk.model.record; import static org.gusdb.wdk.model.AttributeMetaQueryHandler.getDynamicallyDefinedAttributes; +import static org.gusdb.wdk.model.answer.single.SingleRecordQuestionParam.PRIMARY_KEY_PARAM_NAME; import java.io.PrintWriter; import java.util.ArrayList; @@ -13,13 +14,19 @@ import org.gusdb.fgputil.SortDirection; import org.gusdb.fgputil.SortDirectionSpec; +import org.gusdb.fgputil.functional.Either; import org.gusdb.wdk.model.AttributeMetaQueryHandler; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkModelText; +import org.gusdb.wdk.model.WdkRuntimeException; +import org.gusdb.wdk.model.answer.single.SingleRecordQuestion; import org.gusdb.wdk.model.query.Column; +import org.gusdb.wdk.model.query.ProcessQuery; import org.gusdb.wdk.model.query.Query; import org.gusdb.wdk.model.query.SqlQuery; +import org.gusdb.wdk.model.query.param.StringParam; +import org.gusdb.wdk.model.question.Question; import org.gusdb.wdk.model.record.attribute.AttributeField; import org.gusdb.wdk.model.record.attribute.AttributeFieldContainer; import org.gusdb.wdk.model.record.attribute.DerivedAttributeField; @@ -37,10 +44,16 @@ */ public class TableField extends Field implements AttributeFieldContainer { + public class QueryPair { + public SqlQuery getWrappedQuery() { return _wrappedQuery; } + public SqlQuery getUnwrappedQuery() { return _unwrappedQuery; } + } + private String _queryTwoPartName; private String _attributeMetaQueryTwoPartName; private SqlQuery _unwrappedQuery; private SqlQuery _wrappedQuery; + private ProcessQuery _processQuery; private List _attributeFieldList = new ArrayList<>(); private Map _attributeFieldMap = new LinkedHashMap<>(); @@ -49,9 +62,12 @@ public class TableField extends Field implements AttributeFieldContainer { private String _categoryName; private String _clientSortingOrderString; private List> _clientSortingOrderList = new ArrayList<>(); + public static final String SORT_ASCENDING = "ASC"; public static final String SORT_DESCENDING = "DESC"; + public static final String TABLE_NAME_PARAM_NAME = "tableName"; + private RecordClass _recordClass; public void setRecordClass(RecordClass recordClass) { @@ -62,12 +78,28 @@ public RecordClass getRecordClass() { return _recordClass; } - public SqlQuery getUnwrappedQuery() { - return _unwrappedQuery; + public Either getQuery() { + return hasSqlQuery() ? Either.left(new QueryPair()) : Either.right(getProcessQuery()); } - public Query getWrappedQuery() { - return _wrappedQuery; + private ProcessQuery getProcessQuery() { + // NOTE: the assignment of the context question must be done after the + // single record questions are assigned (which is after resolveReferences()) + // is called. Rather than add another step to the model parse process, + // this is done here on-the-fly. + if (_processQuery.getContextQuestion() == null) { + try { + // process queries in table values are only allowed for single-record questions + String singleRecordQuestionName = SingleRecordQuestion.getQuestionName(_recordClass); + Question singleRecordQuestion = _recordClass.getWdkModel() + .getQuestionByName(singleRecordQuestionName).orElseThrow(); + _processQuery.setContextQuestion(singleRecordQuestion); + } + catch (WdkModelException e) { + throw new WdkRuntimeException("Error while looking up single value question for table field process query", e); + } + } + return _processQuery; } public void setQueryRef(String queryRef) { @@ -78,6 +110,10 @@ public String getQueryRef() { return _queryTwoPartName; } + public boolean isForSingleRecordOnly() { + return !hasSqlQuery(); + } + /** * an optional comma delimited list of column names to tell client how to sort this table. * each element of the list is of the form "column_name ASC|DESC" @@ -148,62 +184,96 @@ public void resolveReferences(WdkModel wdkModel) throws WdkModelException { super.resolveReferences(wdkModel); // resolve Query + Query query; try { - _unwrappedQuery = (SqlQuery)wdkModel.resolveReference(_queryTwoPartName); + query = (Query)wdkModel.resolveReference(_queryTwoPartName); } catch (ClassCastException e) { - throw new WdkModelException("Query '" + _queryTwoPartName + + throw new WdkModelException("Reference '" + _queryTwoPartName + "' referenced as a table query in table '" + getName() + "' in record class '" + getRecordClass().getFullName() + - "' must be a SqlQuery."); + "' is not a Query."); } - // validate the table query - _recordClass.validateBulkQuery(_unwrappedQuery); - - // prepare the query and add primary key params - String[] paramNames = _recordClass.getPrimaryKeyDefinition().getColumnRefs(); - _wrappedQuery = RecordClass.prepareQuery(wdkModel, _unwrappedQuery, paramNames); - - // match table query's columns to declared attribute fields (ok to have extra columns) - Map columns = _wrappedQuery.getColumnMap(); - for (AttributeField field : _attributeFieldMap.values()) { - field.setContainer(this); - if (field instanceof QueryColumnAttributeField) { - Column column = columns.get(field.getName()); - if (column == null) { - throw new WdkModelException("Table " + _name + " in recordclass " + - _recordClass.getFullName() + " declares a column attribute '" + field.getName() + - "' that does not appear in the columns of the referenced table query (" + - _unwrappedQuery.getFullName() + ")."); + // special processing for SQL queries + if (query instanceof SqlQuery) { + + _unwrappedQuery = (SqlQuery)query; + + // validate the table query + _recordClass.validateBulkQuery(_unwrappedQuery); + + // prepare the query and add primary key params + String[] paramNames = _recordClass.getPrimaryKeyDefinition().getColumnRefs(); + _wrappedQuery = RecordClass.prepareQuery(wdkModel, _unwrappedQuery, paramNames); + + // match table query's columns to declared attribute fields (ok to have extra columns) + Map columns = _wrappedQuery.getColumnMap(); + for (AttributeField field : _attributeFieldMap.values()) { + field.setContainer(this); + if (field instanceof QueryColumnAttributeField) { + Column column = columns.get(field.getName()); + if (column == null) { + throw new WdkModelException("Table " + _name + " in recordclass " + + _recordClass.getFullName() + " declares a column attribute '" + field.getName() + + "' that does not appear in the columns of the referenced table query (" + + _unwrappedQuery.getFullName() + ")."); + } + ((QueryColumnAttributeField) field).setColumn(column); } - ((QueryColumnAttributeField) field).setColumn(column); + field.resolveReferences(wdkModel); } - field.resolveReferences(wdkModel); - } - // Continue only if a table attribute meta query reference is provided. - if (_attributeMetaQueryTwoPartName != null) { - for (Map row : getDynamicallyDefinedAttributes(_attributeMetaQueryTwoPartName, - wdkModel)) { - AttributeField attributeField = new QueryColumnAttributeField(); + // Continue only if a table attribute meta query reference is provided. + if (_attributeMetaQueryTwoPartName != null) { + for (Map row : getDynamicallyDefinedAttributes(_attributeMetaQueryTwoPartName, + wdkModel)) { + AttributeField attributeField = new QueryColumnAttributeField(); - // Need to call this explicitly since this attribute field originates from the database - attributeField.excludeResources(wdkModel.getProjectId()); + // Need to call this explicitly since this attribute field originates from the database + attributeField.excludeResources(wdkModel.getProjectId()); - // Populate the attributeField with the attribute meta data - AttributeMetaQueryHandler.populate(attributeField, row); + // Populate the attributeField with the attribute meta data + AttributeMetaQueryHandler.populate(attributeField, row); - // Add the new attributeField to the map - _attributeFieldMap.put(attributeField.getName(), attributeField); + // Add the new attributeField to the map + _attributeFieldMap.put(attributeField.getName(), attributeField); + } } } + // special processing for process queries + else { + + // make copy of dereferenced query for this table field + _processQuery = (ProcessQuery)query.clone(); + + // confirm no params are present; PK and table name params are added below + if (_processQuery.getParams().length != 0) { + throw new WdkModelException("Process query " + _processQuery.getFullName() + + " referenced by table field " + _name + " must have zero parameters. Params '" + + PRIMARY_KEY_PARAM_NAME + "' and '" + TABLE_NAME_PARAM_NAME + "' will be added."); + } + + // add parameters to the query + _processQuery.addParam(createProcessQueryParam(PRIMARY_KEY_PARAM_NAME, wdkModel)); + _processQuery.addParam(createProcessQueryParam(TABLE_NAME_PARAM_NAME, wdkModel)); + } + unpackAndValidateClientSortingOrder(); _resolved = true; } + private StringParam createProcessQueryParam(String name, WdkModel wdkModel) throws WdkModelException { + StringParam param = new StringParam(); + param.setName(name); + param.setAllowEmpty(false); + param.setNoTranslation(true); + param.resolveReferences(wdkModel); + return param; + } + private String getTableNameForErrMsg() { return " of recordClass " + _recordClass.getFullName(); } @@ -306,4 +376,12 @@ public String getNameForLogging() { return _recordClass.getFullName() + "." + getName(); } + public boolean hasSqlQuery() { + return _processQuery == null; + } + + public String getQueryFullName() { + return hasSqlQuery() ? _wrappedQuery.getFullName() : _processQuery.getFullName(); + } + } diff --git a/Model/src/main/java/org/gusdb/wdk/model/report/config/AnswerDetailsFactory.java b/Model/src/main/java/org/gusdb/wdk/model/report/config/AnswerDetailsFactory.java index b28c362d9..20e2e09fc 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/report/config/AnswerDetailsFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/report/config/AnswerDetailsFactory.java @@ -144,7 +144,7 @@ private static Map parseTableJson(JSONObject specJson, Quest // see if property value is a String, if so, it could be a special value try { if (RETURN_ALL_TABLES.equals(specJson.getString("tables"))) { - return question.getRecordClass().getTableFieldMap(); + return question.getTableFieldMap(); } throw new ReporterConfigException("Illegal string found for " + "tables property. Must be an array or '" @@ -227,7 +227,7 @@ private static List> parseSorting(JSONArray so private static Map parseTableArray(JSONArray tablesJson, Question question) throws ReporterConfigException { - Map availableTables = question.getRecordClass().getTableFieldMap(); + Map availableTables = question.getTableFieldMap(); Map tables = new LinkedHashMap<>(); for (int i = 0; i < tablesJson.length(); i++) { String tableName = tablesJson.getString(i); diff --git a/Service/src/main/java/org/gusdb/wdk/service/formatter/RecordClassFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/RecordClassFormatter.java index 6ffdb41b0..9d62e8a8f 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/formatter/RecordClassFormatter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/formatter/RecordClassFormatter.java @@ -81,7 +81,7 @@ public static JSONObject getRecordClassJson(RecordClass recordClass, .put(JsonKeys.RECORD_ID_ATTRIBUTE_NAME, recordClass.getIdAttributeField().getName()) .put(JsonKeys.ATTRIBUTES, AttributeFieldFormatter.getAttributesJson( recordClass.getAttributeFieldMap().values(), expandAttributes)) - .put(JsonKeys.TABLES, TableFieldFormatter.getTablesJson(recordClass.getTableFieldMap().values(), + .put(JsonKeys.TABLES, TableFieldFormatter.getTablesJson(recordClass.getTableFieldMap(true).values(), FieldScope.ALL, expandTables, expandTableAttributes)); } diff --git a/Service/src/main/java/org/gusdb/wdk/service/formatter/TableFieldFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/TableFieldFormatter.java index 021c3d042..dbf11be19 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/formatter/TableFieldFormatter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/formatter/TableFieldFormatter.java @@ -53,7 +53,8 @@ public static JSONObject getTableJson(TableField table, boolean expandAttributes .put(JsonKeys.IS_IN_REPORT, FieldScope.REPORT_MAKER.isFieldInScope(table)) .put(JsonKeys.NAME, table.getName()) .put(JsonKeys.PROPERTIES, table.getPropertyLists()) - .put(JsonKeys.TYPE, table.getType()); + .put(JsonKeys.TYPE, table.getType()) + .put(JsonKeys.SINGLE_RECORD_ONLY, table.isForSingleRecordOnly()); } private static JSONArray getClientSortSpecJson(TableField tableField) { diff --git a/Service/src/main/java/org/gusdb/wdk/service/request/RecordRequest.java b/Service/src/main/java/org/gusdb/wdk/service/request/RecordRequest.java index c28e2a074..6e39479db 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/request/RecordRequest.java +++ b/Service/src/main/java/org/gusdb/wdk/service/request/RecordRequest.java @@ -94,7 +94,7 @@ private static List parseAttributeNames(JSONArray attributeNames, Record private static List parseTableNames(JSONArray tableNames, RecordClass recordClass) throws WdkUserException { - Map allowedTables = recordClass.getTableFieldMap(); + Map allowedTables = recordClass.getTableFieldMap(true); List namesList = new ArrayList(); for (int i = 0; i < tableNames.length(); i++) { String name = tableNames.getString(i);