diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java index b1662c6a..604a9c98 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.actions; +import java.io.Serializable; +import java.util.List; +import java.util.function.Function; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; @@ -41,11 +44,23 @@ public class ActionHelper public static void validateSession(AbstractActionInput request) throws QException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); - QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); if(!authenticationModule.isSessionValid(request.getInstance(), request.getSession())) { throw new QAuthenticationException("Invalid session in request"); } } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void editFirstValue(List values, Function editFunction) + { + if(values.size() > 0) + { + values.set(0, editFunction.apply(String.valueOf(values.get(0)))); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java index 7763cf23..88f01258 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java @@ -23,6 +23,9 @@ package com.kingsrook.qqq.backend.core.utils; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; @@ -31,6 +34,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.json.JSONArray; @@ -219,4 +224,44 @@ public class JsonUtils return (json != null && json.matches("(?s)\\s*\\[.*")); } + + + /******************************************************************************* + ** Convert a json object into a QRecord + ** + *******************************************************************************/ + public static QRecord parseQRecord(JSONObject jsonObject, Map fields) + { + QRecord record = new QRecord(); + for(String fieldName : fields.keySet()) + { + QFieldMetaData metaData = fields.get(fieldName); + String backendName = metaData.getBackendName() != null ? metaData.getBackendName() : fieldName; + switch(metaData.getType()) + { + case INTEGER -> record.setValue(fieldName, jsonObject.optInt(backendName)); + case DECIMAL -> record.setValue(fieldName, jsonObject.optBigDecimal(backendName, null)); + case BOOLEAN -> record.setValue(fieldName, jsonObject.optBoolean(backendName)); + case DATE_TIME -> + { + String dateTimeString = jsonObject.optString(backendName); + if(StringUtils.hasContent(dateTimeString)) + { + try + { + record.setValue(fieldName, LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_ZONED_DATE_TIME)); + } + catch(DateTimeParseException dtpe1) + { + record.setValue(fieldName, LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_DATE_TIME)); + } + } + } + default -> record.setValue(fieldName, jsonObject.optString(backendName)); + } + } + + return (record); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index 1970e360..12a6fbff 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -228,6 +228,10 @@ public class ValueUtils ZoneId zid = (tz == null) ? ZoneId.systemDefault() : tz.toZoneId(); return LocalDateTime.ofInstant(c.toInstant(), zid); } + else if(value instanceof String s) + { + return LocalDateTime.parse(s); + } else { throw (new QValueException("Unsupported class " + value.getClass().getName() + " for converting to LocalDateTime.")); 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 d95ac1f7..f922fcc9 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 @@ -30,15 +30,9 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; +import com.kingsrook.qqq.backend.module.api.actions.APICountAction; import com.kingsrook.qqq.backend.module.api.actions.APIInsertAction; -// import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSCountAction; -// import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSDeleteAction; -// import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSInsertAction; -// import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSQueryAction; -// import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSUpdateAction; -// import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; -// import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; - +import com.kingsrook.qqq.backend.module.api.actions.APIQueryAction; /******************************************************************************* @@ -84,7 +78,7 @@ public class APIBackendModule implements QBackendModuleInterface @Override public CountInterface getCountInterface() { - return (null); //return (new RDBMSCountAction()); + return (new APICountAction()); } @@ -95,7 +89,7 @@ public class APIBackendModule implements QBackendModuleInterface @Override public QueryInterface getQueryInterface() { - return (null); //return (new RDBMSQueryAction()); + return (new APIQueryAction()); } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java new file mode 100644 index 00000000..81240b42 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java @@ -0,0 +1,86 @@ +/* + * 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.List; +import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class APICountAction extends AbstractAPIAction implements CountInterface +{ + private static final Logger LOG = LogManager.getLogger(APICountAction.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public CountOutput execute(CountInput countInput) throws QException + { + QTableMetaData table = countInput.getTable(); + preAction(countInput); + + try + { + QQueryFilter filter = countInput.getFilter(); + String paramString = apiActionUtil.buildQueryString(filter, null, null, table.getFields()); + + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); + HttpClient client = httpClientBuilder.build(); + + String url = apiActionUtil.buildTableUrl(table); + HttpGet request = new HttpGet(url + paramString); + + apiActionUtil.setupAuthorizationInRequest(request); + apiActionUtil.setupContentTypeInRequest(request); + apiActionUtil.setupAdditionalHeaders(request); + + HttpResponse response = client.execute(request); + List queryResults = apiActionUtil.processGetResponse(table, response); + + CountOutput rs = new CountOutput(); + rs.setCount(queryResults.size()); + return rs; + } + catch(Exception e) + { + LOG.warn("Error in API count", e); + throw new QException("Error executing count: " + e.getMessage(), e); + } + } +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java new file mode 100644 index 00000000..d7a9aaa2 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java @@ -0,0 +1,89 @@ +/* + * 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.List; +import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class APIQueryAction extends AbstractAPIAction implements QueryInterface +{ + private static final Logger LOG = LogManager.getLogger(APIQueryAction.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QueryOutput execute(QueryInput queryInput) throws QException + { + QTableMetaData table = queryInput.getTable(); + preAction(queryInput); + + try + { + QQueryFilter filter = queryInput.getFilter(); + String paramString = apiActionUtil.buildQueryString(filter, queryInput.getLimit(), queryInput.getSkip(), table.getFields()); + + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); + HttpClient client = httpClientBuilder.build(); + + String url = apiActionUtil.buildTableUrl(table) + paramString; + HttpGet request = new HttpGet(url); + + LOG.info("API URL: " + url); + + apiActionUtil.setupAuthorizationInRequest(request); + apiActionUtil.setupContentTypeInRequest(request); + apiActionUtil.setupAdditionalHeaders(request); + + HttpResponse response = client.execute(request); + List queryResults = apiActionUtil.processGetResponse(table, response); + + QueryOutput queryOutput = new QueryOutput(queryInput); + queryOutput.addRecords(queryResults); + return (queryOutput); + } + catch(Exception e) + { + LOG.warn("Error in API Query", e); + throw new QException("Error executing query: " + e.getMessage(), e); + } + } + +} 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 d39a3bfc..ce277999 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 @@ -24,10 +24,17 @@ package com.kingsrook.qqq.backend.module.api.actions; import java.io.IOException; import java.io.Serializable; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.Map; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -43,6 +50,7 @@ import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.json.JSONArray; import org.json.JSONObject; @@ -89,6 +97,18 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** method to build up a query string based on a given QFilter object + ** + *******************************************************************************/ + protected String buildQueryString(QQueryFilter filter, Integer limit, Integer skip, Map fields) throws QException + { + // todo: reasonable default action + return (null); + } + + + /******************************************************************************* ** As part of making a request - set up its authorization header (not just ** strictly "Authorization", but whatever is needed for auth). @@ -107,6 +127,10 @@ public class BaseAPIActionUtil request.addHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getUsername(), backendMetaData.getPassword())); break; + case API_KEY_HEADER: + request.addHeader("API-Key", backendMetaData.getApiKey()); + break; + default: throw new IllegalArgumentException("Unexpected authorization type: " + backendMetaData.getAuthorizationType()); } @@ -141,7 +165,7 @@ public class BaseAPIActionUtil *******************************************************************************/ public void setupAdditionalHeaders(HttpRequestBase request) { - + request.addHeader("Accept", "application/json"); } @@ -219,6 +243,66 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** + *******************************************************************************/ + protected QRecord jsonObjectToRecord(JSONObject jsonObject, Map fields) throws IOException + { + QRecord record = JsonUtils.parseQRecord(jsonObject, fields); + return (record); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected List processGetResponse(QTableMetaData table, HttpResponse response) throws IOException + { + int statusCode = response.getStatusLine().getStatusCode(); + System.out.println(statusCode); + + HttpEntity entity = response.getEntity(); + String resultString = EntityUtils.toString(entity); + + List recordList = new ArrayList<>(); + if(StringUtils.hasContent(resultString)) + { + JSONArray resultList = null; + JSONObject jsonObject = null; + + if(resultString.startsWith("[")) + { + resultList = JsonUtils.toJSONArray(resultString); + } + else + { + String tablePath = getBackendDetails(table).getTablePath(); + jsonObject = JsonUtils.toJSONObject(resultString); + if(jsonObject.has(tablePath)) + { + resultList = jsonObject.getJSONArray(getBackendDetails(table).getTablePath()); + } + } + + if(resultList != null) + { + for(int i = 0; i < resultList.length(); i++) + { + recordList.add(jsonObjectToRecord(resultList.getJSONObject(i), table.getFields())); + } + } + else + { + recordList.add(jsonObjectToRecord(jsonObject, table.getFields())); + } + } + + return (recordList); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -328,4 +412,14 @@ public class BaseAPIActionUtil { this.actionInput = actionInput; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected String urlEncode(String s) + { + return (URLEncoder.encode(s, StandardCharsets.UTF_8)); + } } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java index eb357c02..fbfde3b9 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java @@ -27,7 +27,8 @@ package com.kingsrook.qqq.backend.module.api.model; *******************************************************************************/ public enum AuthorizationType { + API_KEY_HEADER, BASIC_AUTH_API_KEY, - BASIC_AUTH_USERNAME_PASSWORD + BASIC_AUTH_USERNAME_PASSWORD, } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index 346f0805..4f6d6331 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -27,8 +27,8 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.interfaces.QActionInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -255,42 +255,42 @@ public abstract class AbstractRDBMSAction implements QActionInterface case STARTS_WITH: { clause += " LIKE ?"; - editFirstValue(values, (s -> s + "%")); + ActionHelper.editFirstValue(values, (s -> s + "%")); expectedNoOfParams = 1; break; } case ENDS_WITH: { clause += " LIKE ?"; - editFirstValue(values, (s -> "%" + s)); + ActionHelper.editFirstValue(values, (s -> "%" + s)); expectedNoOfParams = 1; break; } case CONTAINS: { clause += " LIKE ?"; - editFirstValue(values, (s -> "%" + s + "%")); + ActionHelper.editFirstValue(values, (s -> "%" + s + "%")); expectedNoOfParams = 1; break; } case NOT_STARTS_WITH: { clause += " NOT LIKE ?"; - editFirstValue(values, (s -> s + "%")); + ActionHelper.editFirstValue(values, (s -> s + "%")); expectedNoOfParams = 1; break; } case NOT_ENDS_WITH: { clause += " NOT LIKE ?"; - editFirstValue(values, (s -> "%" + s)); + ActionHelper.editFirstValue(values, (s -> "%" + s)); expectedNoOfParams = 1; break; } case NOT_CONTAINS: { clause += " NOT LIKE ?"; - editFirstValue(values, (s -> "%" + s + "%")); + ActionHelper.editFirstValue(values, (s -> "%" + s + "%")); expectedNoOfParams = 1; break; } @@ -372,19 +372,6 @@ public abstract class AbstractRDBMSAction implements QActionInterface - /******************************************************************************* - ** - *******************************************************************************/ - private static void editFirstValue(List values, Function editFunction) - { - if(values.size() > 0) - { - values.set(0, editFunction.apply(String.valueOf(values.get(0)))); - } - } - - - /******************************************************************************* ** *******************************************************************************/