mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
Adding table-cacheOf concept; ability to add a child record from child-list widget
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
|
@ -30,5 +30,6 @@ public enum AuthorizationType
|
||||
API_KEY_HEADER,
|
||||
BASIC_AUTH_API_KEY,
|
||||
BASIC_AUTH_USERNAME_PASSWORD,
|
||||
OAUTH2,
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user