Adding table-cacheOf concept; ability to add a child record from child-list widget

This commit is contained in:
2022-12-05 10:24:15 -06:00
parent 3691ad87e5
commit 060da69afb
32 changed files with 1766 additions and 109 deletions

View File

@ -57,9 +57,14 @@ import com.kingsrook.qqq.backend.core.utils.QLogger;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.module.api.exceptions.OAuthCredentialsException;
import com.kingsrook.qqq.backend.module.api.exceptions.OAuthExpiredTokenException;
import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException;
import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
@ -69,6 +74,9 @@ import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONObject;
@ -122,9 +130,11 @@ public class BaseAPIActionUtil
{
try
{
String urlSuffix = buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey());
String url = buildTableUrl(table);
HttpGet request = new HttpGet(url + urlSuffix);
String urlSuffix = getInput.getPrimaryKey() != null
? buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey())
: buildUrlSuffixForSingleRecordGet(getInput.getUniqueKey());
String url = buildTableUrl(table);
HttpGet request = new HttpGet(url + urlSuffix);
GetOutput rs = new GetOutput();
QHttpResponse response = makeRequest(table, request);
@ -468,6 +478,8 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected void handleResponseError(QTableMetaData table, HttpRequestBase request, QHttpResponse response) throws QException
{
checkForOAuthExpiredToken(table, request, response);
int statusCode = response.getStatusCode();
String resultString = response.getContent();
String errorMessage = "HTTP " + request.getMethod() + " for table [" + table.getName() + "] failed with status " + statusCode + ": " + resultString;
@ -486,6 +498,22 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
protected void checkForOAuthExpiredToken(QTableMetaData table, HttpRequestBase request, QHttpResponse response) throws OAuthExpiredTokenException
{
if(backendMetaData.getAuthorizationType().equals(AuthorizationType.OAUTH2))
{
if(response.getStatusCode().equals(HttpStatus.SC_UNAUTHORIZED)) // 401
{
throw (new OAuthExpiredTokenException("Expired token indicated by response: " + response));
}
}
}
/*******************************************************************************
** method to build up a query string based on a given QFilter object
**
@ -511,13 +539,29 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
public String buildUrlSuffixForSingleRecordGet(Map<String, Serializable> uniqueKey) throws QException
{
QTableMetaData table = actionInput.getTable();
QQueryFilter filter = new QQueryFilter();
for(Map.Entry<String, Serializable> entry : uniqueKey.entrySet())
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
}
return (buildQueryStringForGet(filter, 1, 0, table.getFields()));
}
/*******************************************************************************
** As part of making a request - set up its authorization header (not just
** strictly "Authorization", but whatever is needed for auth).
**
** Can be overridden if an API uses an authorization type we don't natively support.
*******************************************************************************/
protected void setupAuthorizationInRequest(HttpRequestBase request)
protected void setupAuthorizationInRequest(HttpRequestBase request) throws QException
{
switch(backendMetaData.getAuthorizationType())
{
@ -533,6 +577,10 @@ public class BaseAPIActionUtil
request.addHeader("API-Key", backendMetaData.getApiKey());
break;
case OAUTH2:
request.setHeader("Authorization", "Bearer " + getOAuth2Token());
break;
default:
throw new IllegalArgumentException("Unexpected authorization type: " + backendMetaData.getAuthorizationType());
}
@ -540,6 +588,66 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
public String getOAuth2Token() throws OAuthCredentialsException
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// check for the access token in the backend meta data. if it's not there, then issue a request for a token. //
// this is not generally meant to be put in the meta data by the app programmer - rather, we're just using //
// it as a "cheap & easy" way to "cache" the token within our process's memory... //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
String accessToken = ValueUtils.getValueAsString(backendMetaData.getCustomValue("accessToken"));
if(!StringUtils.hasContent(accessToken))
{
String fullURL = backendMetaData.getBaseUrl() + "oauth/token";
String postBody = "grant_type=client_credentials&client_id=" + backendMetaData.getClientId() + "&client_secret=" + backendMetaData.getClientSecret();
LOG.info(session, "Fetching OAuth2 token from " + fullURL);
try(CloseableHttpClient client = HttpClients.custom().setConnectionManager(new PoolingHttpClientConnectionManager()).build())
{
HttpPost request = new HttpPost(fullURL);
request.setEntity(new StringEntity(postBody));
request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
HttpResponse response = client.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
String resultString = EntityUtils.toString(entity);
if(statusCode != HttpStatus.SC_OK)
{
throw (new OAuthCredentialsException("Did not receive successful response when requesting oauth token [" + statusCode + "]: " + resultString));
}
JSONObject resultJSON = new JSONObject(resultString);
accessToken = (resultJSON.getString("access_token"));
LOG.debug(session, "Fetched access token: " + accessToken);
///////////////////////////////////////////////////////////////////////////////////////////////////
// stash the access token in the backendMetaData, from which it will be used for future requests //
///////////////////////////////////////////////////////////////////////////////////////////////////
backendMetaData.withCustomValue("accessToken", accessToken);
}
catch(OAuthCredentialsException oce)
{
throw (oce);
}
catch(Exception e)
{
String errorMessage = "Error getting OAuth Token";
LOG.warn(session, errorMessage, e);
throw (new OAuthCredentialsException(errorMessage, e));
}
}
return (accessToken);
}
/*******************************************************************************
** As part of making a request - set up its content-type header.
*******************************************************************************/
@ -727,8 +835,9 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected QHttpResponse makeRequest(QTableMetaData table, HttpRequestBase request) throws QException
{
int sleepMillis = getInitialRateLimitBackoffMillis();
int rateLimitsCaught = 0;
int sleepMillis = getInitialRateLimitBackoffMillis();
int rateLimitsCaught = 0;
boolean caughtAnOAuthExpiredToken = false;
while(true)
{
@ -768,6 +877,25 @@ public class BaseAPIActionUtil
return (qResponse);
}
}
catch(OAuthCredentialsException oce)
{
LOG.error(session, "OAuth Credential failure for [" + table.getName() + "]");
throw (oce);
}
catch(OAuthExpiredTokenException oete)
{
if(!caughtAnOAuthExpiredToken)
{
LOG.info(session, "OAuth Expired token for [" + table.getName() + "] - retrying");
backendMetaData.withCustomValue("accessToken", null);
caughtAnOAuthExpiredToken = true;
}
else
{
LOG.info(session, "OAuth Expired token for [" + table.getName() + "] even after a retry. Giving up.");
throw (oete);
}
}
catch(RateLimitException rle)
{
rateLimitsCaught++;
@ -781,6 +909,13 @@ public class BaseAPIActionUtil
SleepUtils.sleep(sleepMillis, TimeUnit.MILLISECONDS);
sleepMillis *= 2;
}
catch(QException qe)
{
///////////////////////////////////////////////////////////////
// re-throw exceptions that QQQ or application code produced //
///////////////////////////////////////////////////////////////
throw (qe);
}
catch(Exception e)
{
String message = "An unknown error occurred trying to make an HTTP request to [" + request.getURI() + "] on table [" + table.getName() + "].";
@ -906,7 +1041,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected int getInitialRateLimitBackoffMillis()
{
return (0);
return (500);
}
@ -916,7 +1051,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected int getMaxAllowedRateLimitErrors()
{
return (0);
return (3);
}

View File

@ -0,0 +1,52 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.api.exceptions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
** Exception to be thrown during OAuth Token generation.
**
*******************************************************************************/
public class OAuthCredentialsException extends QException
{
/*******************************************************************************
**
*******************************************************************************/
public OAuthCredentialsException(String message)
{
super(message);
}
/*******************************************************************************
**
*******************************************************************************/
public OAuthCredentialsException(String errorMessage, Exception e)
{
super(errorMessage, e);
}
}

View File

@ -0,0 +1,54 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.api.exceptions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
** Exception to be thrown by a request that uses OAuth, if the current token
** is expired. Generally should signal that the token needs refreshed, and the
** request should be tried again.
**
*******************************************************************************/
public class OAuthExpiredTokenException extends QException
{
/*******************************************************************************
**
*******************************************************************************/
public OAuthExpiredTokenException(String message)
{
super(message);
}
/*******************************************************************************
**
*******************************************************************************/
public OAuthExpiredTokenException(String errorMessage, Exception e)
{
super(errorMessage, e);
}
}

View File

@ -22,10 +22,13 @@
package com.kingsrook.qqq.backend.module.api.exceptions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
**
*******************************************************************************/
public class RateLimitException extends Exception
public class RateLimitException extends QException
{
/*******************************************************************************

View File

@ -30,5 +30,6 @@ public enum AuthorizationType
API_KEY_HEADER,
BASIC_AUTH_API_KEY,
BASIC_AUTH_USERNAME_PASSWORD,
OAUTH2,
}

View File

@ -39,6 +39,8 @@ public class APIBackendMetaData extends QBackendMetaData
{
private String baseUrl;
private String apiKey;
private String clientId;
private String clientSecret;
private String username;
private String password;
@ -156,6 +158,74 @@ public class APIBackendMetaData extends QBackendMetaData
/*******************************************************************************
** Getter for clientId
**
*******************************************************************************/
public String getClientId()
{
return clientId;
}
/*******************************************************************************
** Setter for clientId
**
*******************************************************************************/
public void setClientId(String clientId)
{
this.clientId = clientId;
}
/*******************************************************************************
** Fluent setter for clientId
**
*******************************************************************************/
public APIBackendMetaData withClientId(String clientId)
{
this.clientId = clientId;
return (this);
}
/*******************************************************************************
** Getter for clientSecret
**
*******************************************************************************/
public String getClientSecret()
{
return clientSecret;
}
/*******************************************************************************
** Setter for clientSecret
**
*******************************************************************************/
public void setClientSecret(String clientSecret)
{
this.clientSecret = clientSecret;
}
/*******************************************************************************
** Fluent setter for clientSecret
**
*******************************************************************************/
public APIBackendMetaData withClientSecret(String clientSecret)
{
this.clientSecret = clientSecret;
return (this);
}
/*******************************************************************************
** Getter for username
**
@ -391,4 +461,16 @@ public class APIBackendMetaData extends QBackendMetaData
{
qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), "Missing baseUrl for API backend: " + getName());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean requiresPrimaryKeyOnTables()
{
return (false);
}
}