From 4003323b88e199254c3405f5bb3e481434209ee4 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 18:55:34 -0500 Subject: [PATCH] Working version of ApiScriptUtils. Moved actual api imlpementation out of javalin class, into implemnetation class. --- .../scripts/RunAdHocRecordScriptAction.java | 32 +- .../qqq/api/actions/ApiImplementation.java | 1118 +++++++++++++++++ .../qqq/api/javalin/QJavalinApiHandler.java | 1031 +-------------- .../com/kingsrook/qqq/api/model/APILog.java | 4 +- ....java => ApiInstanceMetaDataProvider.java} | 19 +- .../qqq/api/utils/ApiScriptUtils.java | 249 ++++ 6 files changed, 1453 insertions(+), 1000 deletions(-) create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java rename qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/{APILogMetaDataProvider.java => ApiInstanceMetaDataProvider.java} (94%) create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java index d38e4d63..8313d8b0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions.scripts; +import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -55,6 +56,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.scripts.Script; import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -126,7 +128,7 @@ public class RunAdHocRecordScriptAction executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils()); } - executeCodeInput.getContext().put("api", new ScriptApi()); + addApiUtilityToContext(executeCodeInput.getContext(), scriptRevision); executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!! @@ -158,6 +160,34 @@ public class RunAdHocRecordScriptAction + /******************************************************************************* + ** Try to (dynamically) load the ApiScriptUtils object from the api middleware + ** module -- in case the runtime doesn't have that module deployed (e.g, not in + ** the project pom). + *******************************************************************************/ + private void addApiUtilityToContext(Map context, ScriptRevision scriptRevision) + { + try + { + Class apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils"); + Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor().newInstance(); + context.put("api", (Serializable) apiScriptUtilsObject); + } + catch(ClassNotFoundException e) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this is the only exception we're kinda expecting here - so catch for it specifically, and just log.trace - others, warn // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + LOG.trace("Couldn't load ApiScriptUtils class - qqq-middleware-api not on the classpath?"); + } + catch(Exception e) + { + LOG.warn("Error adding api utility to script context", e, logPair("scriptRevisionId", scriptRevision.getId())); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java new file mode 100644 index 00000000..e58533cd --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -0,0 +1,1118 @@ +/* + * 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.api.actions; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import com.kingsrook.qqq.api.javalin.QBadRequestException; +import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; +import com.kingsrook.qqq.api.model.metadata.ApiOperation; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +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.tables.CountAction; +import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.logging.LogPair; +import com.kingsrook.qqq.backend.core.logging.QLogger; +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.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; +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.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.QueryOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; +import org.apache.commons.lang.BooleanUtils; +import org.eclipse.jetty.http.HttpStatus; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiImplementation +{ + private static final QLogger LOG = QLogger.getLogger(ApiImplementation.class); + + ///////////////////////////////////// + // key: Pair // + ///////////////////////////////////// + private static Map, Map> tableApiNameMap = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Map query(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, Map> paramMap) throws QException + { + List badRequestMessages = new ArrayList<>(); + + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.QUERY_BY_QUERY_STRING); + String tableName = table.getName(); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(tableName); + queryInput.setIncludeAssociations(true); + + PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ); + + String pageSizeParam = getSingleParam(paramMap, "pageSize"); + String pageNoParam = getSingleParam(paramMap, "pageNo"); + String booleanOperator = getSingleParam(paramMap, "booleanOperator"); + String includeCountParam = getSingleParam(paramMap, "includeCount"); + String orderBy = getSingleParam(paramMap, "orderBy"); + + Integer pageSize = 50; + if(StringUtils.hasContent(pageSizeParam)) + { + try + { + pageSize = ValueUtils.getValueAsInteger(pageSizeParam); + } + catch(Exception e) + { + badRequestMessages.add("Could not parse pageSize as an integer"); + } + } + if(pageSize < 1 || pageSize > 1000) + { + badRequestMessages.add("pageSize must be between 1 and 1000."); + } + + Integer pageNo = 1; + if(StringUtils.hasContent(pageNoParam)) + { + try + { + pageNo = ValueUtils.getValueAsInteger(pageNoParam); + } + catch(Exception e) + { + badRequestMessages.add("Could not parse pageNo as an integer"); + } + } + if(pageNo < 1) + { + badRequestMessages.add("pageNo must be greater than 0."); + } + + queryInput.setLimit(pageSize); + queryInput.setSkip((pageNo - 1) * pageSize); + + // queryInput.setQueryJoins(processQueryJoinsParam(context)); + + QQueryFilter filter = new QQueryFilter(); + if("and".equalsIgnoreCase(booleanOperator)) + { + filter.setBooleanOperator(QQueryFilter.BooleanOperator.AND); + } + else if("or".equalsIgnoreCase(booleanOperator)) + { + filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR); + } + else if(StringUtils.hasContent(booleanOperator)) + { + badRequestMessages.add("booleanOperator must be either AND or OR."); + } + + boolean includeCount = true; + if("true".equalsIgnoreCase(includeCountParam)) + { + includeCount = true; + } + else if("false".equalsIgnoreCase(includeCountParam)) + { + includeCount = false; + } + else if(StringUtils.hasContent(includeCountParam)) + { + badRequestMessages.add("includeCount must be either true or false"); + } + + if(StringUtils.hasContent(orderBy)) + { + for(String orderByPart : orderBy.split(",")) + { + orderByPart = orderByPart.trim(); + String[] orderByNameDirection = orderByPart.split(" +"); + boolean asc = true; + if(orderByNameDirection.length == 2) + { + if("asc".equalsIgnoreCase(orderByNameDirection[1])) + { + asc = true; + } + else if("desc".equalsIgnoreCase(orderByNameDirection[1])) + { + asc = false; + } + else + { + badRequestMessages.add("orderBy direction for field " + orderByNameDirection[0] + " must be either ASC or DESC."); + } + } + else if(orderByNameDirection.length > 2) + { + badRequestMessages.add("Unrecognized format for orderBy clause: " + orderByPart + ". Expected: fieldName [ASC|DESC]."); + } + + try + { + QFieldMetaData field = table.getField(orderByNameDirection[0]); + filter.withOrderBy(new QFilterOrderBy(field.getName(), asc)); + } + catch(Exception e) + { + badRequestMessages.add("Unrecognized orderBy field name: " + orderByNameDirection[0] + "."); + } + } + } + else + { + filter.withOrderBy(new QFilterOrderBy(table.getPrimaryKeyField(), false)); + } + + Set nonFilterParams = Set.of("pageSize", "pageNo", "orderBy", "booleanOperator", "includeCount"); + + //////////////////////////// + // look for filter params // + //////////////////////////// + for(Map.Entry> entry : paramMap.entrySet()) + { + String name = entry.getKey(); + List values = entry.getValue(); + + if(nonFilterParams.contains(name)) + { + continue; + } + + try + { + QFieldMetaData field = table.getField(name); + for(String value : values) + { + if(StringUtils.hasContent(value)) + { + try + { + filter.addCriteria(parseQueryParamToCriteria(name, value)); + } + catch(Exception e) + { + badRequestMessages.add(e.getMessage()); + } + } + } + } + catch(Exception e) + { + badRequestMessages.add("Unrecognized filter criteria field: " + name); + } + } + + ////////////////////////////////////////// + // no more badRequest checks below here // + ////////////////////////////////////////// + if(!badRequestMessages.isEmpty()) + { + if(badRequestMessages.size() == 1) + { + throw (new QBadRequestException(badRequestMessages.get(0))); + } + else + { + throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages))); + } + } + + ////////////////// + // do the query // + ////////////////// + QueryAction queryAction = new QueryAction(); + queryInput.setFilter(filter); + QueryOutput queryOutput = queryAction.execute(queryInput); + + Map output = new LinkedHashMap<>(); + output.put("pageNo", pageNo); + output.put("pageSize", pageSize); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // map record fields for api // + // note - don't put them in the output until after the count, just because that looks a little nicer, i think // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ArrayList> records = new ArrayList<>(); + for(QRecord record : queryOutput.getRecords()) + { + records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version)); + } + + ///////////////////////////// + // optionally do the count // + ///////////////////////////// + if(includeCount) + { + CountInput countInput = new CountInput(); + countInput.setTableName(tableName); + countInput.setFilter(filter); + CountOutput countOutput = new CountAction().execute(countInput); + output.put("count", countOutput.getCount()); + } + + output.put("records", records); + + return (output); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Map insert(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.INSERT); + String tableName = table.getName(); + + InsertInput insertInput = new InsertInput(); + + insertInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT); + + try + { + if(!StringUtils.hasContent(body)) + { + throw (new QBadRequestException("Missing required POST body")); + } + + JSONTokener jsonTokener = new JSONTokener(body.trim()); + JSONObject jsonObject = new JSONObject(jsonTokener); + + insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false))); + + if(jsonTokener.more()) + { + throw (new QBadRequestException("Body contained more than a single JSON object.")); + } + } + catch(QBadRequestException qbre) + { + throw (qbre); + } + catch(Exception e) + { + throw (new QBadRequestException("Body could not be parsed as a JSON object: " + e.getMessage(), e)); + } + + InsertAction insertAction = new InsertAction(); + InsertOutput insertOutput = insertAction.execute(insertInput); + + List errors = insertOutput.getRecords().get(0).getErrors(); + if(CollectionUtils.nullSafeHasContents(errors)) + { + boolean isBadRequest = areAnyErrorsBadRequest(errors); + + String message = "Error inserting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors); + if(isBadRequest) + { + throw (new QBadRequestException(message)); + } + else + { + throw (new QException(message)); + } + } + + LinkedHashMap outputRecord = new LinkedHashMap<>(); + outputRecord.put(table.getPrimaryKeyField(), insertOutput.getRecords().get(0).getValue(table.getPrimaryKeyField())); + return (outputRecord); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List> bulkInsert(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_INSERT); + String tableName = table.getName(); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT); + + ///////////////// + // build input // + ///////////////// + try + { + if(!StringUtils.hasContent(body)) + { + throw (new QBadRequestException("Missing required POST body")); + } + + ArrayList recordList = new ArrayList<>(); + insertInput.setRecords(recordList); + + JSONTokener jsonTokener = new JSONTokener(body.trim()); + JSONArray jsonArray = new JSONArray(jsonTokener); + + for(int i = 0; i < jsonArray.length(); i++) + { + JSONObject jsonObject = jsonArray.getJSONObject(i); + recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false)); + } + + if(jsonTokener.more()) + { + throw (new QBadRequestException("Body contained more than a single JSON array.")); + } + + if(recordList.isEmpty()) + { + throw (new QBadRequestException("No records were found in the POST body")); + } + } + catch(QBadRequestException qbre) + { + throw (qbre); + } + catch(Exception e) + { + throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); + } + + ////////////// + // execute! // + ////////////// + InsertAction insertAction = new InsertAction(); + InsertOutput insertOutput = insertAction.execute(insertInput); + + /////////////////////////////////////// + // process records to build response // + /////////////////////////////////////// + List> response = new ArrayList<>(); + for(QRecord record : insertOutput.getRecords()) + { + LinkedHashMap outputRecord = new LinkedHashMap<>(); + response.add(outputRecord); + + List errors = record.getErrors(); + if(CollectionUtils.nullSafeHasContents(errors)) + { + outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); + outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); + outputRecord.put("error", "Error inserting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); + } + else + { + outputRecord.put("statusCode", HttpStatus.Code.CREATED.getCode()); + outputRecord.put("statusText", HttpStatus.Code.CREATED.getMessage()); + outputRecord.put(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField())); + } + } + + return (response); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Map get(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String primaryKey) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.GET); + String tableName = table.getName(); + + GetInput getInput = new GetInput(); + getInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ); + + getInput.setPrimaryKey(primaryKey); + getInput.setIncludeAssociations(true); + + GetAction getAction = new GetAction(); + GetOutput getOutput = getAction.execute(getInput); + + /////////////////////////////////////////////////////// + // throw a not found error if the record isn't found // + /////////////////////////////////////////////////////// + QRecord record = getOutput.getRecord(); + if(record == null) + { + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + } + + Map outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version); + return (outputRecord); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void update(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String primaryKey, String body) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.UPDATE); + String tableName = table.getName(); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); + + try + { + if(!StringUtils.hasContent(body)) + { + throw (new QBadRequestException("Missing required PATCH body")); + } + + JSONTokener jsonTokener = new JSONTokener(body.trim()); + JSONObject jsonObject = new JSONObject(jsonTokener); + + QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false); + qRecord.setValue(table.getPrimaryKeyField(), primaryKey); + updateInput.setRecords(List.of(qRecord)); + + if(jsonTokener.more()) + { + throw (new QBadRequestException("Body contained more than a single JSON object.")); + } + } + catch(QBadRequestException qbre) + { + throw (qbre); + } + catch(Exception e) + { + throw (new QBadRequestException("Body could not be parsed as a JSON object: " + e.getMessage(), e)); + } + + UpdateAction updateAction = new UpdateAction(); + UpdateOutput updateOutput = updateAction.execute(updateInput); + + List errors = updateOutput.getRecords().get(0).getErrors(); + if(CollectionUtils.nullSafeHasContents(errors)) + { + if(areAnyErrorsNotFound(errors)) + { + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + } + else + { + boolean isBadRequest = areAnyErrorsBadRequest(errors); + + String message = "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors); + if(isBadRequest) + { + throw (new QBadRequestException(message)); + } + else + { + throw (new QException(message)); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List> bulkUpdate(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_UPDATE); + String tableName = table.getName(); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); + + ///////////////// + // build input // + ///////////////// + try + { + if(!StringUtils.hasContent(body)) + { + throw (new QBadRequestException("Missing required PATCH body")); + } + + ArrayList recordList = new ArrayList<>(); + updateInput.setRecords(recordList); + + JSONTokener jsonTokener = new JSONTokener(body.trim()); + JSONArray jsonArray = new JSONArray(jsonTokener); + + for(int i = 0; i < jsonArray.length(); i++) + { + JSONObject jsonObject = jsonArray.getJSONObject(i); + recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, true)); + } + + if(jsonTokener.more()) + { + throw (new QBadRequestException("Body contained more than a single JSON array.")); + } + + if(recordList.isEmpty()) + { + throw (new QBadRequestException("No records were found in the PATCH body")); + } + } + catch(QBadRequestException qbre) + { + throw (qbre); + } + catch(Exception e) + { + throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); + } + + ////////////// + // execute! // + ////////////// + UpdateAction updateAction = new UpdateAction(); + UpdateOutput updateOutput = updateAction.execute(updateInput); + + /////////////////////////////////////// + // process records to build response // + /////////////////////////////////////// + List> response = new ArrayList<>(); + int i = 0; + for(QRecord record : updateOutput.getRecords()) + { + LinkedHashMap outputRecord = new LinkedHashMap<>(); + response.add(outputRecord); + + try + { + QRecord inputRecord = updateInput.getRecords().get(i); + Serializable primaryKey = inputRecord.getValue(table.getPrimaryKeyField()); + outputRecord.put(table.getPrimaryKeyField(), primaryKey); + } + catch(Exception e) + { + ////////// + // omit // + ////////// + } + + List errors = record.getErrors(); + if(CollectionUtils.nullSafeHasContents(errors)) + { + outputRecord.put("error", "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); + if(areAnyErrorsNotFound(errors)) + { + outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode()); + outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage()); + } + else + { + outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); + outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); + } + } + else + { + outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode()); + outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage()); + } + + i++; + } + + return (response); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void delete(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String primaryKey) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.DELETE); + String tableName = table.getName(); + + DeleteInput deleteInput = new DeleteInput(); + deleteInput.setTableName(tableName); + deleteInput.setPrimaryKeys(List.of(primaryKey)); + + PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE); + + /////////////////// + // do the delete // + /////////////////// + DeleteAction deleteAction = new DeleteAction(); + DeleteOutput deleteOutput = deleteAction.execute(deleteInput); + if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors())) + { + if(areAnyErrorsNotFound(deleteOutput.getRecordsWithErrors().get(0).getErrors())) + { + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + } + else + { + throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors()))); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List> bulkDelete(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_DELETE); + String tableName = table.getName(); + + DeleteInput deleteInput = new DeleteInput(); + deleteInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE); + + ///////////////// + // build input // + ///////////////// + try + { + if(!StringUtils.hasContent(body)) + { + throw (new QBadRequestException("Missing required DELETE body")); + } + + ArrayList primaryKeyList = new ArrayList<>(); + deleteInput.setPrimaryKeys(primaryKeyList); + + JSONTokener jsonTokener = new JSONTokener(body.trim()); + JSONArray jsonArray = new JSONArray(jsonTokener); + + for(int i = 0; i < jsonArray.length(); i++) + { + Object object = jsonArray.get(i); + if(object instanceof JSONArray || object instanceof JSONObject) + { + throw (new QBadRequestException("One or more elements inside the DELETE body JSONArray was not a primitive value")); + } + primaryKeyList.add(String.valueOf(object)); + } + + if(jsonTokener.more()) + { + throw (new QBadRequestException("Body contained more than a single JSON array.")); + } + + if(primaryKeyList.isEmpty()) + { + throw (new QBadRequestException("No primary keys were found in the DELETE body")); + } + } + catch(QBadRequestException qbre) + { + throw (qbre); + } + catch(Exception e) + { + throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); + } + + ////////////// + // execute! // + ////////////// + DeleteAction deleteAction = new DeleteAction(); + DeleteOutput deleteOutput = deleteAction.execute(deleteInput); + + /////////////////////////////////////// + // process records to build response // + /////////////////////////////////////// + List> response = new ArrayList<>(); + + List recordsWithErrors = deleteOutput.getRecordsWithErrors(); + Map> primaryKeyToErrorsMap = new HashMap<>(); + for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors)) + { + String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField()); + primaryKeyToErrorsMap.put(primaryKey, recordWithError.getErrors()); + } + + for(Serializable primaryKey : deleteInput.getPrimaryKeys()) + { + LinkedHashMap outputRecord = new LinkedHashMap<>(); + response.add(outputRecord); + outputRecord.put(table.getPrimaryKeyField(), primaryKey); + + String primaryKeyString = ValueUtils.getValueAsString(primaryKey); + List errors = primaryKeyToErrorsMap.get(primaryKeyString); + if(CollectionUtils.nullSafeHasContents(errors)) + { + outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); + if(areAnyErrorsNotFound(errors)) + { + outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode()); + outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage()); + } + else + { + outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); + outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); + } + } + else + { + outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode()); + outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage()); + } + } + + return (response); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static String getSingleParam(Map> paramMap, String name) + { + if(CollectionUtils.nullSafeHasContents(paramMap.get(name))) + { + return (paramMap.get(name).get(0)); + } + + return (null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private enum Operator + { + /////////////////////////////////////////////////////////////////////////////////// + // order of these is important (e.g., because some are a sub-string of others!!) // + /////////////////////////////////////////////////////////////////////////////////// + EQ("=", QCriteriaOperator.EQUALS, QCriteriaOperator.NOT_EQUALS, 1), + LTE("<=", QCriteriaOperator.LESS_THAN_OR_EQUALS, null, 1), + GTE(">=", QCriteriaOperator.GREATER_THAN_OR_EQUALS, null, 1), + LT("<", QCriteriaOperator.LESS_THAN, null, 1), + GT(">", QCriteriaOperator.GREATER_THAN, null, 1), + EMPTY("EMPTY", QCriteriaOperator.IS_BLANK, QCriteriaOperator.IS_NOT_BLANK, 0), + BETWEEN("BETWEEN ", QCriteriaOperator.BETWEEN, QCriteriaOperator.NOT_BETWEEN, 2), + IN("IN ", QCriteriaOperator.IN, QCriteriaOperator.NOT_IN, null), + LIKE("LIKE ", QCriteriaOperator.LIKE, QCriteriaOperator.NOT_LIKE, 1); + + + private final String prefix; + private final QCriteriaOperator positiveOperator; + private final QCriteriaOperator negativeOperator; + private final Integer noOfValues; // null means many (IN) + + + + /******************************************************************************* + ** + *******************************************************************************/ + Operator(String prefix, QCriteriaOperator positiveOperator, QCriteriaOperator negativeOperator, Integer noOfValues) + { + this.prefix = prefix; + this.positiveOperator = positiveOperator; + this.negativeOperator = negativeOperator; + this.noOfValues = noOfValues; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QFilterCriteria parseQueryParamToCriteria(String name, String value) throws QException + { + /////////////////////////////////// + // process & discard a leading ! // + /////////////////////////////////// + boolean isNot = false; + if(value.startsWith("!") && value.length() > 1) + { + isNot = true; + value = value.substring(1); + } + + ////////////////////////// + // look for an operator // + ////////////////////////// + Operator selectedOperator = null; + for(Operator op : Operator.values()) + { + if(value.startsWith(op.prefix)) + { + selectedOperator = op; + if(selectedOperator.negativeOperator == null && isNot) + { + throw (new QBadRequestException("Unsupported operator: !" + selectedOperator.prefix)); + } + break; + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // if an operator was found, strip it away from the value for figuring out the values part // + ///////////////////////////////////////////////////////////////////////////////////////////// + if(selectedOperator != null) + { + value = value.substring(selectedOperator.prefix.length()); + } + else + { + //////////////////////////////////////////////////////////////// + // else - assume the default operator, and use the full value // + //////////////////////////////////////////////////////////////// + selectedOperator = Operator.EQ; + } + + //////////////////////////////////// + // figure out the criteria values // + // todo - quotes? // + //////////////////////////////////// + List criteriaValues; + if(selectedOperator.noOfValues == null) + { + criteriaValues = Arrays.asList(value.split(",")); + } + else if(selectedOperator.noOfValues == 1) + { + criteriaValues = ListBuilder.of(value); + } + else if(selectedOperator.noOfValues == 0) + { + if(StringUtils.hasContent(value)) + { + throw (new QBadRequestException("Unexpected value after operator " + selectedOperator.prefix + " for field " + name)); + } + criteriaValues = null; + } + else if(selectedOperator.noOfValues == 2) + { + criteriaValues = Arrays.asList(value.split(",")); + if(criteriaValues.size() != 2) + { + throw (new QBadRequestException("Operator " + selectedOperator.prefix + " for field " + name + " requires 2 values (received " + criteriaValues.size() + ")")); + } + } + else + { + throw (new QException("Unexpected noOfValues [" + selectedOperator.noOfValues + "] in operator [" + selectedOperator + "]")); + } + + return (new QFilterCriteria(name, isNot ? selectedOperator.negativeOperator : selectedOperator.positiveOperator, criteriaValues)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QTableMetaData validateTableAndVersion(String path, ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, ApiOperation operation) throws QNotFoundException + { + QNotFoundException qNotFoundException = new QNotFoundException("Could not find any resources at path " + path); + + QTableMetaData table = getTableByApiName(apiInstanceMetaData.getName(), version, tableApiName); + LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("tableApiName", tableApiName), logPair("operation", operation) }; + + if(table == null) + { + LOG.info("404 because table is null (tableApiName=" + tableApiName + ")", logPairs); + throw (qNotFoundException); + } + + if(BooleanUtils.isTrue(table.getIsHidden())) + { + LOG.info("404 because table isHidden", logPairs); + throw (qNotFoundException); + } + + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer == null) + { + LOG.info("404 because table apiMetaDataContainer is null", logPairs); + throw (qNotFoundException); + } + + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName()); + if(apiTableMetaData == null) + { + LOG.info("404 because table apiMetaData is null", logPairs); + throw (qNotFoundException); + } + + if(BooleanUtils.isTrue(apiTableMetaData.getIsExcluded())) + { + LOG.info("404 because table is excluded", logPairs); + throw (qNotFoundException); + } + + if(!operation.isOperationEnabled(List.of(apiInstanceMetaData, apiTableMetaData))) + { + LOG.info("404 because api operation is not enabled", logPairs); + throw (qNotFoundException); + } + + if(!table.isCapabilityEnabled(QContext.getQInstance().getBackendForTable(table.getName()), operation.getCapability())) + { + LOG.info("404 because table capability is not enabled", logPairs); + throw (qNotFoundException); + } + + APIVersion requestApiVersion = new APIVersion(version); + List supportedVersions = apiInstanceMetaData.getSupportedVersions(); + if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) + { + LOG.info("404 because requested version is not supported", logPairs); + throw (qNotFoundException); + } + + if(!apiTableMetaData.getApiVersionRange().includes(requestApiVersion)) + { + LOG.info("404 because table version range does not include requested version", logPairs); + throw (qNotFoundException); + } + + return (table); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QTableMetaData getTableByApiName(String apiName, String version, String tableApiName) + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // tableApiNameMap is a map of (apiName,apiVersion) => Map. // + // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // + // the second level is keyed by tableApiNames. // + ///////////////////////////////////////////////////////////////////////////////////////////// + Pair key = new Pair<>(apiName, version); + if(tableApiNameMap.get(key) == null) + { + Map map = new HashMap<>(); + + for(QTableMetaData table : QContext.getQInstance().getTables().values()) + { + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer != null) + { + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiName); + if(apiTableMetaData != null) + { + String name = table.getName(); + if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) + { + name = apiTableMetaData.getApiTableName(); + } + map.put(name, table); + } + } + } + + tableApiNameMap.put(key, map); + } + + return (tableApiNameMap.get(key).get(tableApiName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean areAnyErrorsBadRequest(List errors) + { + boolean isBadRequest = errors.stream().anyMatch(e -> + e.contains("Missing value in required field") + || e.contains("You do not have permission") + ); + return isBadRequest; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean areAnyErrorsNotFound(List errors) + { + return errors.stream().anyMatch(e -> e.startsWith(UpdateAction.NOT_FOUND_ERROR_PREFIX) || e.startsWith(DeleteAction.NOT_FOUND_ERROR_PREFIX)); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index 0cb92760..a4375bd4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -27,35 +27,25 @@ import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.Base64; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; +import com.kingsrook.qqq.api.actions.ApiImplementation; import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction; -import com.kingsrook.qqq.api.actions.QRecordApiAdapter; import com.kingsrook.qqq.api.model.APILog; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; -import com.kingsrook.qqq.api.model.metadata.APILogMetaDataProvider; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; -import com.kingsrook.qqq.api.model.metadata.ApiOperation; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; -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.tables.CountAction; -import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; -import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; -import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.AccessTokenException; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; @@ -64,30 +54,17 @@ import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; -import com.kingsrook.qqq.backend.core.logging.LogPair; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; -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.delete.DeleteInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; 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.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.QueryOutput; -import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; 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.authentication.Auth0AuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QUser; @@ -96,10 +73,7 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; -import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; -import com.kingsrook.qqq.backend.core.utils.ValueUtils; -import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; @@ -110,9 +84,6 @@ import io.javalin.http.Context; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.BooleanUtils; import org.eclipse.jetty.http.HttpStatus; -import org.json.JSONArray; -import org.json.JSONObject; -import org.json.JSONTokener; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.javalin.QJavalinImplementation.SLOW_LOG_THRESHOLD_MS; @@ -126,11 +97,6 @@ public class QJavalinApiHandler private static QInstance qInstance; - ///////////////////////////////////// - // key: Pair // - ///////////////////////////////////// - private static Map, Map> tableApiNameMap = new HashMap<>(); - private static Map apiLogUserIdCache = new HashMap<>(); @@ -432,7 +398,6 @@ public class QJavalinApiHandler context.status(HttpStatus.Code.OK.getCode()); context.result(accessToken); QJavalinAccessLogger.logEndSuccess(); - return; } catch(AccessTokenException aae) { @@ -446,7 +411,6 @@ public class QJavalinApiHandler context.status(aae.getStatusCode()); context.result(aae.getMessage()); QJavalinAccessLogger.logEndSuccess(); - return; } //////////////////////////////////////////////////////// @@ -647,38 +611,10 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.GET); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiGet", logPair("table", tableApiName), logPair("primaryKey", primaryKey)); - GetInput getInput = new GetInput(); - - setupSession(context, getInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiGet", logPair("table", tableName), logPair("primaryKey", primaryKey)); - - getInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ); - - // todo - validate that the primary key is of the proper type (e.g,. not a string for an id field) - // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error) - - getInput.setPrimaryKey(primaryKey); - getInput.setIncludeAssociations(true); - - GetAction getAction = new GetAction(); - GetOutput getOutput = getAction.execute(getInput); - - /////////////////////////////////////////////////////// - // throw a not found error if the record isn't found // - /////////////////////////////////////////////////////// - QRecord record = getOutput.getRecord(); - if(record == null) - { - throw (new QNotFoundException("Could not find " + table.getLabel() + " with " - + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); - } - - Map outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version); + Map outputRecord = ApiImplementation.get(apiInstanceMetaData, version, tableApiName, primaryKey); QJavalinAccessLogger.logEndSuccess(); String resultString = JsonUtils.toJson(outputRecord); @@ -769,7 +705,7 @@ public class QJavalinApiHandler *******************************************************************************/ private static Integer getApiLogUserId(QSession qSession) throws QException { - String tableName = APILogMetaDataProvider.TABLE_NAME_API_LOG_USER; + String tableName = ApiInstanceMetaDataProvider.TABLE_NAME_API_LOG_USER; if(qSession == null) { @@ -874,7 +810,7 @@ public class QJavalinApiHandler private static Integer fetchApiLogUserIdFromName(String name) throws QException { GetInput getInput = new GetInput(); - getInput.setTableName(APILogMetaDataProvider.TABLE_NAME_API_LOG_USER); + getInput.setTableName(ApiInstanceMetaDataProvider.TABLE_NAME_API_LOG_USER); getInput.setUniqueKey(Map.of("name", name)); GetOutput getOutput = new GetAction().execute(getInput); if(getOutput.getRecord() != null) @@ -892,228 +828,20 @@ public class QJavalinApiHandler *******************************************************************************/ private static void doQuery(Context context, ApiInstanceMetaData apiInstanceMetaData) { - String version = context.pathParam("version"); - String tableApiName = context.pathParam("tableName"); - QQueryFilter filter = null; - APILog apiLog = newAPILog(context); + String version = context.pathParam("version"); + String tableApiName = context.pathParam("tableName"); + + QQueryFilter filter = null; + APILog apiLog = newAPILog(context); try { - List badRequestMessages = new ArrayList<>(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableApiName)); - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.QUERY_BY_QUERY_STRING); - String tableName = table.getName(); + Map output = ApiImplementation.query(apiInstanceMetaData, version, tableApiName, context.queryParamMap()); - QueryInput queryInput = new QueryInput(); - setupSession(context, queryInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName)); - - queryInput.setTableName(tableName); - queryInput.setIncludeAssociations(true); - - PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ); - - Integer pageSize = 50; - if(StringUtils.hasContent(context.queryParam("pageSize"))) - { - try - { - pageSize = ValueUtils.getValueAsInteger(context.queryParam("pageSize")); - } - catch(Exception e) - { - badRequestMessages.add("Could not parse pageSize as an integer"); - } - } - if(pageSize < 1 || pageSize > 1000) - { - badRequestMessages.add("pageSize must be between 1 and 1000."); - } - - Integer pageNo = 1; - if(StringUtils.hasContent(context.queryParam("pageNo"))) - { - try - { - pageNo = ValueUtils.getValueAsInteger(context.queryParam("pageNo")); - } - catch(Exception e) - { - badRequestMessages.add("Could not parse pageNo as an integer"); - } - } - if(pageNo < 1) - { - badRequestMessages.add("pageNo must be greater than 0."); - } - - queryInput.setLimit(pageSize); - queryInput.setSkip((pageNo - 1) * pageSize); - - // queryInput.setQueryJoins(processQueryJoinsParam(context)); - - filter = new QQueryFilter(); - if("and".equalsIgnoreCase(context.queryParam("booleanOperator"))) - { - filter.setBooleanOperator(QQueryFilter.BooleanOperator.AND); - } - else if("or".equalsIgnoreCase(context.queryParam("booleanOperator"))) - { - filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR); - } - else if(StringUtils.hasContent(context.queryParam("booleanOperator"))) - { - badRequestMessages.add("booleanOperator must be either AND or OR."); - } - - boolean includeCount = true; - if("true".equalsIgnoreCase(context.queryParam("includeCount"))) - { - includeCount = true; - } - else if("false".equalsIgnoreCase(context.queryParam("includeCount"))) - { - includeCount = false; - } - else if(StringUtils.hasContent(context.queryParam("includeCount"))) - { - badRequestMessages.add("includeCount must be either true or false"); - } - - String orderBy = context.queryParam("orderBy"); - if(StringUtils.hasContent(orderBy)) - { - for(String orderByPart : orderBy.split(",")) - { - orderByPart = orderByPart.trim(); - String[] orderByNameDirection = orderByPart.split(" +"); - boolean asc = true; - if(orderByNameDirection.length == 2) - { - if("asc".equalsIgnoreCase(orderByNameDirection[1])) - { - asc = true; - } - else if("desc".equalsIgnoreCase(orderByNameDirection[1])) - { - asc = false; - } - else - { - badRequestMessages.add("orderBy direction for field " + orderByNameDirection[0] + " must be either ASC or DESC."); - } - } - else if(orderByNameDirection.length > 2) - { - badRequestMessages.add("Unrecognized format for orderBy clause: " + orderByPart + ". Expected: fieldName [ASC|DESC]."); - } - - try - { - QFieldMetaData field = table.getField(orderByNameDirection[0]); - filter.withOrderBy(new QFilterOrderBy(field.getName(), asc)); - } - catch(Exception e) - { - badRequestMessages.add("Unrecognized orderBy field name: " + orderByNameDirection[0] + "."); - } - } - } - else - { - filter.withOrderBy(new QFilterOrderBy(table.getPrimaryKeyField(), false)); - } - - Set nonFilterParams = Set.of("pageSize", "pageNo", "orderBy", "booleanOperator", "includeCount"); - - //////////////////////////// - // look for filter params // - //////////////////////////// - for(Map.Entry> entry : context.queryParamMap().entrySet()) - { - String name = entry.getKey(); - List values = entry.getValue(); - - if(nonFilterParams.contains(name)) - { - continue; - } - - try - { - QFieldMetaData field = table.getField(name); - for(String value : values) - { - if(StringUtils.hasContent(value)) - { - try - { - filter.addCriteria(parseQueryParamToCriteria(name, value)); - } - catch(Exception e) - { - badRequestMessages.add(e.getMessage()); - } - } - } - } - catch(Exception e) - { - badRequestMessages.add("Unrecognized filter criteria field: " + name); - } - } - - ////////////////////////////////////////// - // no more badRequest checks below here // - ////////////////////////////////////////// - if(!badRequestMessages.isEmpty()) - { - if(badRequestMessages.size() == 1) - { - throw (new QBadRequestException(badRequestMessages.get(0))); - } - else - { - throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages))); - } - } - - ////////////////// - // do the query // - ////////////////// - QueryAction queryAction = new QueryAction(); - queryInput.setFilter(filter); - QueryOutput queryOutput = queryAction.execute(queryInput); - - Map output = new LinkedHashMap<>(); - output.put("pageNo", pageNo); - output.put("pageSize", pageSize); - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // map record fields for api // - // note - don't put them in the output until after the count, just because that looks a little nicer, i think // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ArrayList> records = new ArrayList<>(); - for(QRecord record : queryOutput.getRecords()) - { - records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version)); - } - - ///////////////////////////// - // optionally do the count // - ///////////////////////////// - if(includeCount) - { - CountInput countInput = new CountInput(); - countInput.setTableName(tableName); - countInput.setFilter(filter); - CountOutput countOutput = new CountAction().execute(countInput); - output.put("count", countOutput.getCount()); - } - - output.put("records", records); - - QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS)); + QJavalinAccessLogger.logEndSuccess(logPair("recordCount", () -> ((List) output.get("records")).size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS)); String resultString = JsonUtils.toJson(output); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); @@ -1127,246 +855,6 @@ public class QJavalinApiHandler - /******************************************************************************* - ** - *******************************************************************************/ - private static QTableMetaData validateTableAndVersion(Context context, ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, ApiOperation operation) throws QNotFoundException - { - QNotFoundException qNotFoundException = new QNotFoundException("Could not find any resources at path " + context.path()); - - QTableMetaData table = getTableByApiName(apiInstanceMetaData.getName(), version, tableApiName); - LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("tableApiName", tableApiName), logPair("operation", operation) }; - - if(table == null) - { - LOG.info("404 because table is null", logPairs); - throw (qNotFoundException); - } - - if(BooleanUtils.isTrue(table.getIsHidden())) - { - LOG.info("404 because table isHidden", logPairs); - throw (qNotFoundException); - } - - ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); - if(apiTableMetaDataContainer == null) - { - LOG.info("404 because table apiMetaDataContainer is null", logPairs); - throw (qNotFoundException); - } - - ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName()); - if(apiTableMetaData == null) - { - LOG.info("404 because table apiMetaData is null", logPairs); - throw (qNotFoundException); - } - - if(BooleanUtils.isTrue(apiTableMetaData.getIsExcluded())) - { - LOG.info("404 because table is excluded", logPairs); - throw (qNotFoundException); - } - - if(!operation.isOperationEnabled(List.of(apiInstanceMetaData, apiTableMetaData))) - { - LOG.info("404 because api operation is not enabled", logPairs); - throw (qNotFoundException); - } - - if(!table.isCapabilityEnabled(qInstance.getBackendForTable(table.getName()), operation.getCapability())) - { - LOG.info("404 because table capability is not enabled", logPairs); - throw (qNotFoundException); - } - - APIVersion requestApiVersion = new APIVersion(version); - List supportedVersions = apiInstanceMetaData.getSupportedVersions(); - if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) - { - LOG.info("404 because requested version is not supported", logPairs); - throw (qNotFoundException); - } - - if(!apiTableMetaData.getApiVersionRange().includes(requestApiVersion)) - { - LOG.info("404 because table version range does not include requested version", logPairs); - throw (qNotFoundException); - } - - return (table); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static QTableMetaData getTableByApiName(String apiName, String version, String tableApiName) - { - ///////////////////////////////////////////////////////////////////////////////////////////// - // tableApiNameMap is a map of (apiName,apiVersion) => Map. // - // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // - // the second level is keyed by tableApiNames. // - ///////////////////////////////////////////////////////////////////////////////////////////// - Pair key = new Pair<>(apiName, version); - if(tableApiNameMap.get(key) == null) - { - Map map = new HashMap<>(); - - for(QTableMetaData table : qInstance.getTables().values()) - { - ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); - if(apiTableMetaDataContainer != null) - { - ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiName); - if(apiTableMetaData != null) - { - String name = table.getName(); - if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) - { - name = apiTableMetaData.getApiTableName(); - } - map.put(name, table); - } - } - } - - tableApiNameMap.put(key, map); - } - - return (tableApiNameMap.get(key).get(tableApiName)); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private enum Operator - { - /////////////////////////////////////////////////////////////////////////////////// - // order of these is important (e.g., because some are a sub-string of others!!) // - /////////////////////////////////////////////////////////////////////////////////// - EQ("=", QCriteriaOperator.EQUALS, QCriteriaOperator.NOT_EQUALS, 1), - LTE("<=", QCriteriaOperator.LESS_THAN_OR_EQUALS, null, 1), - GTE(">=", QCriteriaOperator.GREATER_THAN_OR_EQUALS, null, 1), - LT("<", QCriteriaOperator.LESS_THAN, null, 1), - GT(">", QCriteriaOperator.GREATER_THAN, null, 1), - EMPTY("EMPTY", QCriteriaOperator.IS_BLANK, QCriteriaOperator.IS_NOT_BLANK, 0), - BETWEEN("BETWEEN ", QCriteriaOperator.BETWEEN, QCriteriaOperator.NOT_BETWEEN, 2), - IN("IN ", QCriteriaOperator.IN, QCriteriaOperator.NOT_IN, null), - LIKE("LIKE ", QCriteriaOperator.LIKE, QCriteriaOperator.NOT_LIKE, 1); - - - private final String prefix; - private final QCriteriaOperator positiveOperator; - private final QCriteriaOperator negativeOperator; - private final Integer noOfValues; // null means many (IN) - - - - /******************************************************************************* - ** - *******************************************************************************/ - Operator(String prefix, QCriteriaOperator positiveOperator, QCriteriaOperator negativeOperator, Integer noOfValues) - { - this.prefix = prefix; - this.positiveOperator = positiveOperator; - this.negativeOperator = negativeOperator; - this.noOfValues = noOfValues; - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static QFilterCriteria parseQueryParamToCriteria(String name, String value) throws QException - { - /////////////////////////////////// - // process & discard a leading ! // - /////////////////////////////////// - boolean isNot = false; - if(value.startsWith("!") && value.length() > 1) - { - isNot = true; - value = value.substring(1); - } - - ////////////////////////// - // look for an operator // - ////////////////////////// - Operator selectedOperator = null; - for(Operator op : Operator.values()) - { - if(value.startsWith(op.prefix)) - { - selectedOperator = op; - if(selectedOperator.negativeOperator == null && isNot) - { - throw (new QBadRequestException("Unsupported operator: !" + selectedOperator.prefix)); - } - break; - } - } - - ///////////////////////////////////////////////////////////////////////////////////////////// - // if an operator was found, strip it away from the value for figuring out the values part // - ///////////////////////////////////////////////////////////////////////////////////////////// - if(selectedOperator != null) - { - value = value.substring(selectedOperator.prefix.length()); - } - else - { - //////////////////////////////////////////////////////////////// - // else - assume the default operator, and use the full value // - //////////////////////////////////////////////////////////////// - selectedOperator = Operator.EQ; - } - - //////////////////////////////////// - // figure out the criteria values // - // todo - quotes? // - //////////////////////////////////// - List criteriaValues; - if(selectedOperator.noOfValues == null) - { - criteriaValues = Arrays.asList(value.split(",")); - } - else if(selectedOperator.noOfValues == 1) - { - criteriaValues = ListBuilder.of(value); - } - else if(selectedOperator.noOfValues == 0) - { - if(StringUtils.hasContent(value)) - { - throw (new QBadRequestException("Unexpected value after operator " + selectedOperator.prefix + " for field " + name)); - } - criteriaValues = null; - } - else if(selectedOperator.noOfValues == 2) - { - criteriaValues = Arrays.asList(value.split(",")); - if(criteriaValues.size() != 2) - { - throw (new QBadRequestException("Operator " + selectedOperator.prefix + " for field " + name + " requires 2 values (received " + criteriaValues.size() + ")")); - } - } - else - { - throw (new QException("Unexpected noOfValues [" + selectedOperator.noOfValues + "] in operator [" + selectedOperator + "]")); - } - - return (new QFilterCriteria(name, isNot ? selectedOperator.negativeOperator : selectedOperator.positiveOperator, criteriaValues)); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -1378,65 +866,10 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.INSERT); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiInsert", logPair("table", tableApiName)); - InsertInput insertInput = new InsertInput(); - - setupSession(context, insertInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiInsert", logPair("table", tableName)); - - insertInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT); - - try - { - if(!StringUtils.hasContent(context.body())) - { - throw (new QBadRequestException("Missing required POST body")); - } - - JSONTokener jsonTokener = new JSONTokener(context.body().trim()); - JSONObject jsonObject = new JSONObject(jsonTokener); - - insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false))); - - if(jsonTokener.more()) - { - throw (new QBadRequestException("Body contained more than a single JSON object.")); - } - } - catch(QBadRequestException qbre) - { - throw (qbre); - } - catch(Exception e) - { - throw (new QBadRequestException("Body could not be parsed as a JSON object: " + e.getMessage(), e)); - } - - InsertAction insertAction = new InsertAction(); - InsertOutput insertOutput = insertAction.execute(insertInput); - - List errors = insertOutput.getRecords().get(0).getErrors(); - if(CollectionUtils.nullSafeHasContents(errors)) - { - boolean isBadRequest = areAnyErrorsBadRequest(errors); - - String message = "Error inserting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors); - if(isBadRequest) - { - throw (new QBadRequestException(message)); - } - else - { - throw (new QException(message)); - } - } - - LinkedHashMap outputRecord = new LinkedHashMap<>(); - outputRecord.put(table.getPrimaryKeyField(), insertOutput.getRecords().get(0).getValue(table.getPrimaryKeyField())); + Map outputRecord = ApiImplementation.insert(apiInstanceMetaData, version, tableApiName, context.body()); QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.CREATED.getCode()); @@ -1453,20 +886,6 @@ public class QJavalinApiHandler - /******************************************************************************* - ** - *******************************************************************************/ - private static boolean areAnyErrorsBadRequest(List errors) - { - boolean isBadRequest = errors.stream().anyMatch(e -> - e.contains("Missing value in required field") - || e.contains("You do not have permission") - ); - return isBadRequest; - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -1478,90 +897,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_INSERT); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiBulkInsert", logPair("table", tableApiName)); - InsertInput insertInput = new InsertInput(); + List> response = ApiImplementation.bulkInsert(apiInstanceMetaData, version, tableApiName, context.body()); - setupSession(context, insertInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiBulkInsert", logPair("table", tableName)); - - insertInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT); - - ///////////////// - // build input // - ///////////////// - try - { - if(!StringUtils.hasContent(context.body())) - { - throw (new QBadRequestException("Missing required POST body")); - } - - ArrayList recordList = new ArrayList<>(); - insertInput.setRecords(recordList); - - JSONTokener jsonTokener = new JSONTokener(context.body().trim()); - JSONArray jsonArray = new JSONArray(jsonTokener); - - for(int i = 0; i < jsonArray.length(); i++) - { - JSONObject jsonObject = jsonArray.getJSONObject(i); - recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false)); - } - - if(jsonTokener.more()) - { - throw (new QBadRequestException("Body contained more than a single JSON array.")); - } - - if(recordList.isEmpty()) - { - throw (new QBadRequestException("No records were found in the POST body")); - } - } - catch(QBadRequestException qbre) - { - throw (qbre); - } - catch(Exception e) - { - throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); - } - - ////////////// - // execute! // - ////////////// - InsertAction insertAction = new InsertAction(); - InsertOutput insertOutput = insertAction.execute(insertInput); - - /////////////////////////////////////// - // process records to build response // - /////////////////////////////////////// - List> response = new ArrayList<>(); - for(QRecord record : insertOutput.getRecords()) - { - LinkedHashMap outputRecord = new LinkedHashMap<>(); - response.add(outputRecord); - - List errors = record.getErrors(); - if(CollectionUtils.nullSafeHasContents(errors)) - { - outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); - outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); - outputRecord.put("error", "Error inserting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); - } - else - { - outputRecord.put("statusCode", HttpStatus.Code.CREATED.getCode()); - outputRecord.put("statusText", HttpStatus.Code.CREATED.getMessage()); - outputRecord.put(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField())); - } - } - - QJavalinAccessLogger.logEndSuccess(logPair("recordCount", insertInput.getRecords().size())); + QJavalinAccessLogger.logEndSuccess(logPair("recordCount", response.size())); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); String resultString = JsonUtils.toJson(response); context.result(resultString); @@ -1587,113 +928,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_UPDATE); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiBulkUpdate", logPair("table", tableApiName)); - UpdateInput updateInput = new UpdateInput(); + List> response = ApiImplementation.bulkUpdate(apiInstanceMetaData, version, tableApiName, context.body()); - setupSession(context, updateInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiBulkUpdate", logPair("table", tableName)); - - updateInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); - - ///////////////// - // build input // - ///////////////// - try - { - if(!StringUtils.hasContent(context.body())) - { - throw (new QBadRequestException("Missing required PATCH body")); - } - - ArrayList recordList = new ArrayList<>(); - updateInput.setRecords(recordList); - - JSONTokener jsonTokener = new JSONTokener(context.body().trim()); - JSONArray jsonArray = new JSONArray(jsonTokener); - - for(int i = 0; i < jsonArray.length(); i++) - { - JSONObject jsonObject = jsonArray.getJSONObject(i); - recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, true)); - } - - if(jsonTokener.more()) - { - throw (new QBadRequestException("Body contained more than a single JSON array.")); - } - - if(recordList.isEmpty()) - { - throw (new QBadRequestException("No records were found in the PATCH body")); - } - } - catch(QBadRequestException qbre) - { - throw (qbre); - } - catch(Exception e) - { - throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); - } - - ////////////// - // execute! // - ////////////// - UpdateAction updateAction = new UpdateAction(); - UpdateOutput updateOutput = updateAction.execute(updateInput); - - /////////////////////////////////////// - // process records to build response // - /////////////////////////////////////// - List> response = new ArrayList<>(); - int i = 0; - for(QRecord record : updateOutput.getRecords()) - { - LinkedHashMap outputRecord = new LinkedHashMap<>(); - response.add(outputRecord); - - try - { - QRecord inputRecord = updateInput.getRecords().get(i); - Serializable primaryKey = inputRecord.getValue(table.getPrimaryKeyField()); - outputRecord.put(table.getPrimaryKeyField(), primaryKey); - } - catch(Exception e) - { - ////////// - // omit // - ////////// - } - - List errors = record.getErrors(); - if(CollectionUtils.nullSafeHasContents(errors)) - { - outputRecord.put("error", "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); - if(areAnyErrorsNotFound(errors)) - { - outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode()); - outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage()); - } - else - { - outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); - outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); - } - } - else - { - outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode()); - outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage()); - } - - i++; - } - - QJavalinAccessLogger.logEndSuccess(logPair("recordCount", updateInput.getRecords().size())); + QJavalinAccessLogger.logEndSuccess(logPair("recordCount", response.size())); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); String resultString = JsonUtils.toJson(response); context.result(resultString); @@ -1708,16 +948,6 @@ public class QJavalinApiHandler - /******************************************************************************* - ** - *******************************************************************************/ - private static boolean areAnyErrorsNotFound(List errors) - { - return errors.stream().anyMatch(e -> e.startsWith(UpdateAction.NOT_FOUND_ERROR_PREFIX) || e.startsWith(DeleteAction.NOT_FOUND_ERROR_PREFIX)); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -1729,112 +959,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_DELETE); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiBulkDelete", logPair("table", tableApiName)); - DeleteInput deleteInput = new DeleteInput(); + List> response = ApiImplementation.bulkDelete(apiInstanceMetaData, version, tableApiName, context.body()); - setupSession(context, deleteInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiBulkDelete", logPair("table", tableName)); - - deleteInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE); - - ///////////////// - // build input // - ///////////////// - try - { - if(!StringUtils.hasContent(context.body())) - { - throw (new QBadRequestException("Missing required DELETE body")); - } - - ArrayList primaryKeyList = new ArrayList<>(); - deleteInput.setPrimaryKeys(primaryKeyList); - - JSONTokener jsonTokener = new JSONTokener(context.body().trim()); - JSONArray jsonArray = new JSONArray(jsonTokener); - - for(int i = 0; i < jsonArray.length(); i++) - { - Object object = jsonArray.get(i); - if(object instanceof JSONArray || object instanceof JSONObject) - { - throw (new QBadRequestException("One or more elements inside the DELETE body JSONArray was not a primitive value")); - } - primaryKeyList.add(String.valueOf(object)); - } - - if(jsonTokener.more()) - { - throw (new QBadRequestException("Body contained more than a single JSON array.")); - } - - if(primaryKeyList.isEmpty()) - { - throw (new QBadRequestException("No primary keys were found in the DELETE body")); - } - } - catch(QBadRequestException qbre) - { - throw (qbre); - } - catch(Exception e) - { - throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); - } - - ////////////// - // execute! // - ////////////// - DeleteAction deleteAction = new DeleteAction(); - DeleteOutput deleteOutput = deleteAction.execute(deleteInput); - - /////////////////////////////////////// - // process records to build response // - /////////////////////////////////////// - List> response = new ArrayList<>(); - - List recordsWithErrors = deleteOutput.getRecordsWithErrors(); - Map> primaryKeyToErrorsMap = new HashMap<>(); - for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors)) - { - String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField()); - primaryKeyToErrorsMap.put(primaryKey, recordWithError.getErrors()); - } - - for(Serializable primaryKey : deleteInput.getPrimaryKeys()) - { - LinkedHashMap outputRecord = new LinkedHashMap<>(); - response.add(outputRecord); - outputRecord.put(table.getPrimaryKeyField(), primaryKey); - - String primaryKeyString = ValueUtils.getValueAsString(primaryKey); - List errors = primaryKeyToErrorsMap.get(primaryKeyString); - if(CollectionUtils.nullSafeHasContents(errors)) - { - outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); - if(areAnyErrorsNotFound(errors)) - { - outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode()); - outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage()); - } - else - { - outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); - outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); - } - } - else - { - outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode()); - outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage()); - } - } - - QJavalinAccessLogger.logEndSuccess(logPair("recordCount", deleteInput.getPrimaryKeys().size())); + QJavalinAccessLogger.logEndSuccess(logPair("recordCount", response.size())); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); String resultString = JsonUtils.toJson(response); context.result(resultString); @@ -1861,71 +991,10 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.UPDATE); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiUpdate", logPair("table", tableApiName)); - UpdateInput updateInput = new UpdateInput(); - - setupSession(context, updateInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiUpdate", logPair("table", tableName)); - - updateInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); - - try - { - if(!StringUtils.hasContent(context.body())) - { - throw (new QBadRequestException("Missing required PATCH body")); - } - - JSONTokener jsonTokener = new JSONTokener(context.body().trim()); - JSONObject jsonObject = new JSONObject(jsonTokener); - - QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false); - qRecord.setValue(table.getPrimaryKeyField(), primaryKey); - updateInput.setRecords(List.of(qRecord)); - - if(jsonTokener.more()) - { - throw (new QBadRequestException("Body contained more than a single JSON object.")); - } - } - catch(QBadRequestException qbre) - { - throw (qbre); - } - catch(Exception e) - { - throw (new QBadRequestException("Body could not be parsed as a JSON object: " + e.getMessage(), e)); - } - - UpdateAction updateAction = new UpdateAction(); - UpdateOutput updateOutput = updateAction.execute(updateInput); - - List errors = updateOutput.getRecords().get(0).getErrors(); - if(CollectionUtils.nullSafeHasContents(errors)) - { - if(areAnyErrorsNotFound(errors)) - { - throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); - } - else - { - boolean isBadRequest = areAnyErrorsBadRequest(errors); - - String message = "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors); - if(isBadRequest) - { - throw (new QBadRequestException(message)); - } - else - { - throw (new QException(message)); - } - } - } + ApiImplementation.update(apiInstanceMetaData, version, tableApiName, primaryKey, context.body()); QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.NO_CONTENT.getCode()); @@ -1952,35 +1021,10 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.DELETE); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiDelete", logPair("table", tableApiName)); - DeleteInput deleteInput = new DeleteInput(); - - setupSession(context, deleteInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiDelete", logPair("table", tableName)); - - deleteInput.setTableName(tableName); - deleteInput.setPrimaryKeys(List.of(primaryKey)); - - PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE); - - /////////////////// - // do the delete // - /////////////////// - DeleteAction deleteAction = new DeleteAction(); - DeleteOutput deleteOutput = deleteAction.execute(deleteInput); - if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors())) - { - if(areAnyErrorsNotFound(deleteOutput.getRecordsWithErrors().get(0).getErrors())) - { - throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); - } - else - { - throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors()))); - } - } + ApiImplementation.delete(apiInstanceMetaData, version, tableApiName, primaryKey); QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.NO_CONTENT.getCode()); @@ -2018,6 +1062,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ + @SuppressWarnings("UnnecessaryReturnStatement") public static void handleException(HttpStatus.Code statusCode, Context context, Exception e, APILog apiLog) { QBadRequestException badRequestException = ExceptionUtils.findClassInRootChain(e, QBadRequestException.class); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java index 3086e0d7..8f6cbcc3 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java @@ -26,7 +26,7 @@ import java.io.Serializable; import java.time.Instant; import java.util.HashMap; import java.util.Map; -import com.kingsrook.qqq.api.model.metadata.APILogMetaDataProvider; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException; import com.kingsrook.qqq.backend.core.model.data.QField; @@ -49,7 +49,7 @@ public class APILog extends QRecordEntity @QField(isEditable = false) private Instant timestamp; - @QField(possibleValueSourceName = APILogMetaDataProvider.TABLE_NAME_API_LOG_USER, label = "User") + @QField(possibleValueSourceName = ApiInstanceMetaDataProvider.TABLE_NAME_API_LOG_USER, label = "User") private Integer apiLogUserId; @QField(possibleValueSourceName = "apiMethod") diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataProvider.java similarity index 94% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java rename to qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataProvider.java index cf0d4fd7..0d7779bc 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataProvider.java @@ -50,7 +50,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; /******************************************************************************* ** *******************************************************************************/ -public class APILogMetaDataProvider +public class ApiInstanceMetaDataProvider { public static final String TABLE_NAME_API_LOG = "apiLog"; public static final String TABLE_NAME_API_LOG_USER = "apiLogUser"; @@ -105,21 +105,32 @@ public class APILogMetaDataProvider new QPossibleValue<>(500, "500 (Internal Server Error)") ))); + //////////////////////////////////////////////////////////////////////////// + // loop over api names and versions, building out possible values sources // + //////////////////////////////////////////////////////////////////////////// + List> apiNamePossibleValues = new ArrayList<>(); List> apiVersionPossibleValues = new ArrayList<>(); - //////////////////////////////////////////////////////////////////////////////////////////////////// - // todo... this, this whole thing, should probably have "which api" as another field too... ugh. // - //////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + // todo... apiName should maybe be a field on apiLog table, eh? // + ////////////////////////////////////////////////////////////////// TreeSet allVersions = new TreeSet<>(); ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(instance); for(Map.Entry entry : apiInstanceMetaDataContainer.getApis().entrySet()) { + apiNamePossibleValues.add(new QPossibleValue<>(entry.getKey())); + ApiInstanceMetaData apiInstanceMetaData = entry.getValue(); allVersions.addAll(apiInstanceMetaData.getPastVersions()); allVersions.addAll(apiInstanceMetaData.getSupportedVersions()); allVersions.addAll(apiInstanceMetaData.getFutureVersions()); } + instance.addPossibleValueSource(new QPossibleValueSource() + .withName("apiName") + .withType(QPossibleValueSourceType.ENUM) + .withEnumValues(apiNamePossibleValues)); + for(APIVersion version : allVersions) { apiVersionPossibleValues.add(new QPossibleValue<>(version.toString())); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java new file mode 100644 index 00000000..fefd6c56 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java @@ -0,0 +1,249 @@ +/* + * 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.api.utils; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.actions.ApiImplementation; +import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; + + +/******************************************************************************* + ** Object injected into script context, for interfacing with a QQQ API. + *******************************************************************************/ +public class ApiScriptUtils implements Serializable +{ + private String apiName; + private String apiVersion; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiScriptUtils() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiScriptUtils(String apiName, String apiVersion) + { + setApiName(apiName); + this.apiVersion = apiVersion; + } + + + + /******************************************************************************* + ** Setter for apiName + ** + *******************************************************************************/ + public void setApiName(String apiName) + { + ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance()); + if(apiInstanceMetaDataContainer.getApis().containsKey(apiName)) + { + this.apiName = apiName; + } + else + { + throw (new IllegalArgumentException("[" + apiName + "] is not a valid API name. Valid values are: " + apiInstanceMetaDataContainer.getApis().keySet())); + } + } + + + + /******************************************************************************* + ** Setter for apiVersion + ** + *******************************************************************************/ + public void setApiVersion(String apiVersion) + { + if(apiName == null) + { + throw (new IllegalArgumentException("You must set apiName before setting apiVersion.")); + } + + ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance()); + ApiInstanceMetaData apiInstanceMetaData = apiInstanceMetaDataContainer.getApis().get(apiName); + if(apiInstanceMetaData.getSupportedVersions().contains(new APIVersion(apiVersion))) + { + this.apiVersion = apiVersion; + } + else + { + throw (new IllegalArgumentException("[" + apiVersion + "] is not a supported version for this API. Supported versions are: " + apiInstanceMetaData.getSupportedVersions())); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateApiNameAndVersion(String description) + { + if(apiName == null || apiVersion == null) + { + throw (new IllegalStateException("Both apiName and apiVersion must be set before calling this method (" + description + ").")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Map get(String tableApiName, Object primaryKey) throws QException + { + validateApiNameAndVersion("get(" + tableApiName + "," + primaryKey + ")"); + return (ApiImplementation.get(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Map query(String urlPart) throws QException + { + validateApiNameAndVersion("query(" + urlPart + ")"); + String[] urlParts = urlPart.split("\\?", 2); + Map> paramMap = parseQueryString(urlParts.length > 1 ? urlParts[1] : null); + return (ApiImplementation.query(getApiInstanceMetaData(), apiVersion, urlParts[0], paramMap)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Map insert(String tableApiName, Object body) throws QException + { + validateApiNameAndVersion("insert(" + tableApiName + ")"); + return (ApiImplementation.insert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List> bulkInsert(String tableApiName, Object body) throws QException + { + validateApiNameAndVersion("bulkInsert(" + tableApiName + ")"); + return (ApiImplementation.bulkInsert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void update(String tableApiName, Object primaryKey, Object body) throws QException + { + validateApiNameAndVersion("update(" + tableApiName + "," + primaryKey + ")"); + ApiImplementation.update(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey), String.valueOf(body)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List> bulkUpdate(String tableApiName, Object body) throws QException + { + validateApiNameAndVersion("bulkUpdate(" + tableApiName + ")"); + return (ApiImplementation.bulkUpdate(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void delete(String tableApiName, Object primaryKey) throws QException + { + validateApiNameAndVersion("delete(" + tableApiName + "," + primaryKey + ")"); + ApiImplementation.delete(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List> bulkDelete(String tableApiName, Object body) throws QException + { + validateApiNameAndVersion("bulkDelete(" + tableApiName + ")"); + return (ApiImplementation.bulkDelete(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private ApiInstanceMetaData getApiInstanceMetaData() + { + ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance()); + ApiInstanceMetaData apiInstanceMetaData = apiInstanceMetaDataContainer.getApiInstanceMetaData(apiName); + return apiInstanceMetaData; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Map> parseQueryString(String queryString) + { + Map> paramMap = new LinkedHashMap<>(); + if(queryString != null) + { + for(String nameValuePair : queryString.split("&")) + { + String[] nameValue = nameValuePair.split("=", 2); + if(nameValue.length == 2) + { + paramMap.computeIfAbsent(nameValue[0], (k) -> new ArrayList<>()); + paramMap.get(nameValue[0]).add(nameValue[1]); + } + } + } + return paramMap; + } +}