diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java index 81c39178..5bde561c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateOutput.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.update; +import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -54,4 +55,18 @@ public class UpdateOutput extends AbstractActionOutput { this.records = records; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addRecord(QRecord record) + { + if(this.records == null) + { + this.records = new ArrayList<>(); + } + this.records.add(record); + } } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java index a2ee45e2..4a79a30d 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java @@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.module.api.actions.APICountAction; import com.kingsrook.qqq.backend.module.api.actions.APIGetAction; import com.kingsrook.qqq.backend.module.api.actions.APIInsertAction; import com.kingsrook.qqq.backend.module.api.actions.APIQueryAction; +import com.kingsrook.qqq.backend.module.api.actions.APIUpdateAction; /******************************************************************************* @@ -124,7 +125,7 @@ public class APIBackendModule implements QBackendModuleInterface @Override public UpdateInterface getUpdateInterface() { - return (null); //return (new RDBMSUpdateAction()); + return (new APIUpdateAction()); } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java new file mode 100644 index 00000000..7d1f2a86 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java @@ -0,0 +1,194 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.api.actions; + + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class APIUpdateAction extends AbstractAPIAction implements UpdateInterface +{ + private static final Logger LOG = LogManager.getLogger(APIUpdateAction.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public UpdateOutput execute(UpdateInput updateInput) throws QException + { + UpdateOutput updateOutput = new UpdateOutput(); + updateOutput.setRecords(new ArrayList<>()); + + if(CollectionUtils.nullSafeIsEmpty(updateInput.getRecords())) + { + LOG.info("Update request called with 0 records. Returning with no-op"); + return (updateOutput); + } + + QTableMetaData table = updateInput.getTable(); + preAction(updateInput); + + HttpClientConnectionManager connectionManager = null; + try + { + connectionManager = new PoolingHttpClientConnectionManager(); + + // todo - supports bulk put? + + for(QRecord record : updateInput.getRecords()) + { + putRecords(updateOutput, table, connectionManager, record); + + if(updateInput.getRecords().size() > 1 && apiActionUtil.getMillisToSleepAfterEveryCall() > 0) + { + SleepUtils.sleep(apiActionUtil.getMillisToSleepAfterEveryCall(), TimeUnit.MILLISECONDS); + } + } + + return (updateOutput); + } + catch(Exception e) + { + LOG.warn("Error in API Insert for [" + table.getName() + "]", e); + throw new QException("Error executing update: " + e.getMessage(), e); + } + finally + { + if(connectionManager != null) + { + connectionManager.shutdown(); + } + } + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void putRecords(UpdateOutput updateOutput, QTableMetaData table, HttpClientConnectionManager connectionManager, QRecord record) throws RateLimitException + { + int sleepMillis = apiActionUtil.getInitialRateLimitBackoffMillis(); + int rateLimitsCaught = 0; + while(true) + { + try + { + putOneTime(updateOutput, table, connectionManager, record); + return; + } + catch(RateLimitException rle) + { + rateLimitsCaught++; + if(rateLimitsCaught > apiActionUtil.getMaxAllowedRateLimitErrors()) + { + LOG.warn("Giving up PUT to [" + table.getName() + "] after too many rate-limit errors (" + apiActionUtil.getMaxAllowedRateLimitErrors() + ")"); + record.addError("Error: " + rle.getMessage()); + updateOutput.addRecord(record); + return; + } + + LOG.info("Caught RateLimitException [#" + rateLimitsCaught + "] PUT'ing to [" + table.getName() + "] - sleeping [" + sleepMillis + "]..."); + SleepUtils.sleep(sleepMillis, TimeUnit.MILLISECONDS); + sleepMillis *= 2; + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void putOneTime(UpdateOutput insertOutput, QTableMetaData table, HttpClientConnectionManager connectionManager, QRecord record) throws RateLimitException + { + try + { + CloseableHttpClient client = HttpClients.custom().setConnectionManager(connectionManager).build(); + + String url = buildTableUrl(table); + url += record.getValueString("number"); + HttpPut request = new HttpPut(url); + apiActionUtil.setupAuthorizationInRequest(request); + apiActionUtil.setupContentTypeInRequest(request); + apiActionUtil.setupAdditionalHeaders(request); + + request.setEntity(apiActionUtil.recordToEntity(table, record)); + + HttpResponse response = client.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + if(statusCode == 429) + { + throw (new RateLimitException(EntityUtils.toString(response.getEntity()))); + } + + QRecord outputRecord = apiActionUtil.processPostResponse(table, record, response); + insertOutput.addRecord(outputRecord); + } + catch(RateLimitException rle) + { + throw (rle); + } + catch(Exception e) + { + LOG.warn("Error posting to [" + table.getName() + "]", e); + record.addError("Error: " + e.getMessage()); + insertOutput.addRecord(record); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected String buildTableUrl(QTableMetaData table) + { + return (backendMetaData.getBaseUrl() + "/orders/SalesOrder/"); + } + +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java index bd718f3d..e78fe58f 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java @@ -231,6 +231,14 @@ public class BaseAPIActionUtil { JSONObject body = recordToJsonObject(table, record); String json = body.toString(); + + String tablePath = getBackendDetails(table).getTablePath(); + if(tablePath != null) + { + body = new JSONObject(); + body.put(tablePath, new JSONObject(json)); + json = body.toString(); + } LOG.debug(json); return (new StringEntity(json)); }