diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/queues/GetQueueSize.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/queues/GetQueueSize.java new file mode 100644 index 00000000..e20bdafc --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/queues/GetQueueSize.java @@ -0,0 +1,84 @@ +/* + * 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.backend.core.actions.queues; + + +import java.util.List; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.AmazonSQSClientBuilder; +import com.amazonaws.services.sqs.model.GetQueueAttributesResult; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class GetQueueSize +{ + private static final QLogger LOG = QLogger.getLogger(GetQueueSize.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Integer getQueueSize(QQueueProviderMetaData queueProviderMetaData, QQueueMetaData queueMetaData) throws QException + { + try + { + ////////////////////////////////////////////////////////////////// + // todo - handle other queue provider types, somewhere, somehow // + ////////////////////////////////////////////////////////////////// + SQSQueueProviderMetaData queueProvider = (SQSQueueProviderMetaData) queueProviderMetaData; + + BasicAWSCredentials credentials = new BasicAWSCredentials(queueProvider.getAccessKey(), queueProvider.getSecretKey()); + final AmazonSQS sqs = AmazonSQSClientBuilder.standard() + .withRegion(queueProvider.getRegion()) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + + String queueUrl = queueProvider.getBaseURL(); + if(!queueUrl.endsWith("/")) + { + queueUrl += "/"; + } + queueUrl += queueMetaData.getQueueName(); + + GetQueueAttributesResult queueAttributes = sqs.getQueueAttributes(queueUrl, List.of("ApproximateNumberOfMessages")); + String approximateNumberOfMessages = queueAttributes.getAttributes().get("ApproximateNumberOfMessages"); + return (Integer.parseInt(approximateNumberOfMessages)); + } + catch(Exception e) + { + LOG.warn("Error getting queue size", e, logPair("queueName", queueMetaData == null ? "null" : queueMetaData.getName())); + throw (new QException("Error getting queue size", e)); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java index f3ab6ab2..f6b065fc 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; +import java.io.Serializable; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; @@ -35,6 +36,8 @@ public abstract class AbstractWidgetValueSource protected String name; protected String type; + protected Map inputValues; + /******************************************************************************* @@ -116,4 +119,35 @@ public abstract class AbstractWidgetValueSource //////////////////////// } + + + /******************************************************************************* + ** Getter for inputValues + *******************************************************************************/ + public Map getInputValues() + { + return (this.inputValues); + } + + + + /******************************************************************************* + ** Setter for inputValues + *******************************************************************************/ + public void setInputValues(Map inputValues) + { + this.inputValues = inputValues; + } + + + + /******************************************************************************* + ** Fluent setter for inputValues + *******************************************************************************/ + public AbstractWidgetValueSource withInputValues(Map inputValues) + { + this.inputValues = inputValues; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/QueueSizeWidgetValue.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/QueueSizeWidgetValue.java new file mode 100644 index 00000000..3e354936 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/QueueSizeWidgetValue.java @@ -0,0 +1,100 @@ +/* + * 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.backend.core.model.metadata.dashboard.nocode; + + +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.queues.GetQueueSize; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; +import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QueueSizeWidgetValue extends AbstractWidgetValueSource +{ + private static final QLogger LOG = QLogger.getLogger(QueueSizeWidgetValue.class); + + private String queueName; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object evaluate(Map context, RenderWidgetInput input) throws QException + { + QQueueMetaData queue = QContext.getQInstance().getQueue(queueName); + QQueueProviderMetaData queueProvider = QContext.getQInstance().getQueueProvider(queue.getProviderName()); + return (new GetQueueSize().getQueueSize(queueProvider, queue)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QueueSizeWidgetValue withName(String name) + { + setName(name); + return (this); + } + + + + /******************************************************************************* + ** Getter for queueName + *******************************************************************************/ + public String getQueueName() + { + return (this.queueName); + } + + + + /******************************************************************************* + ** Setter for queueName + *******************************************************************************/ + public void setQueueName(String queueName) + { + this.queueName = queueName; + } + + + + /******************************************************************************* + ** Fluent setter for queueName + *******************************************************************************/ + public QueueSizeWidgetValue withQueueName(String queueName) + { + this.queueName = queueName; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetAdHocValue.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetAdHocValue.java new file mode 100644 index 00000000..384dd684 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetAdHocValue.java @@ -0,0 +1,125 @@ +/* + * 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.backend.core.model.metadata.dashboard.nocode; + + +import java.io.Serializable; +import java.util.Map; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class WidgetAdHocValue extends AbstractWidgetValueSource +{ + private QCodeReference codeReference; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetAdHocValue() + { + setType(getClass().getSimpleName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object evaluate(Map context, RenderWidgetInput input) throws QException + { + if(inputValues != null) + { + context.putAll(inputValues); + } + + Function function = QCodeLoader.getFunction(codeReference); + Object result = function.apply(context); + return (result); + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + @Override + public WidgetAdHocValue withName(String name) + { + setName(name); + return (this); + } + + + + /******************************************************************************* + ** Getter for codeReference + *******************************************************************************/ + public QCodeReference getCodeReference() + { + return (this.codeReference); + } + + + + /******************************************************************************* + ** Setter for codeReference + *******************************************************************************/ + public void setCodeReference(QCodeReference codeReference) + { + this.codeReference = codeReference; + } + + + + /******************************************************************************* + ** Fluent setter for codeReference + *******************************************************************************/ + public WidgetAdHocValue withCodeReference(QCodeReference codeReference) + { + this.codeReference = codeReference; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for inputValues + *******************************************************************************/ + @Override + public WidgetAdHocValue withInputValues(Map inputValues) + { + this.inputValues = inputValues; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java index 56df2403..66650057 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java @@ -74,14 +74,14 @@ public class WidgetCalculation extends AbstractWidgetValueSource { Instant now = Instant.now(); Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0))); - return (then.until(now, ChronoUnit.MINUTES)); + return (then == null ? null : then.until(now, ChronoUnit.MINUTES)); }), AGE_SECONDS((List valueNames, Map context) -> { Instant now = Instant.now(); Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0))); - return (then.until(now, ChronoUnit.SECONDS)); + return (then == null ? null : then.until(now, ChronoUnit.SECONDS)); }), PERCENT_CHANGE((List valueNames, Map context) -> @@ -92,8 +92,8 @@ public class WidgetCalculation extends AbstractWidgetValueSource /////////////////////////////////////////////// // 100 * ( (current - previous) / previous ) // /////////////////////////////////////////////// - BigDecimal difference = current.subtract(previous); - if(BigDecimal.ZERO.equals(previous)) + BigDecimal difference = current == null ? null : current.subtract(previous); + if(BigDecimal.ZERO.equals(previous) || difference == null) { return (null); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java index 0fc8115d..fa62e836 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java @@ -313,6 +313,42 @@ public class StringUtils + /******************************************************************************* + ** Given a "formatString" containing any number of {singular,plural} style "tokens", + ** replace the "tokens" with the "singular" options if the 'size' parameter is 1 + ** or the "plural" options if not-1 (e.g., 0 or 2+) + ** + ** e.g.: StringUtils.pluralFormat(n, "Apple{,s} {was,were} eaten")) // seems easier. + ** e.g.: StringUtils.pluralFormat(n, "Apple{ was,s were} eaten")) // also works... + *******************************************************************************/ + public static String pluralFormat(Integer size, String formatString) + { + int lastIndex = 0; + StringBuilder output = new StringBuilder(); + + Pattern pattern = Pattern.compile("\\{.*?,.*?}"); + Matcher matcher = pattern.matcher(formatString); + while(matcher.find()) + { + String group = matcher.group(); + String groupBody = group.substring(1, group.length() - 1); + String[] groupParts = groupBody.split(",", 2); + String replacement = (size == 1) ? groupParts[0] : groupParts[1]; + output.append(formatString, lastIndex, matcher.start()).append(replacement); + + lastIndex = matcher.end(); + } + + if(lastIndex < formatString.length()) + { + output.append(formatString, lastIndex, formatString.length()); + } + + return (output.toString()); + } + + + /******************************************************************************* ** Switch between strings based on if the size of the parameter collection. If ** it is 1 (the singular) or not-1 (0 or 2+, the plural). Get back "" or "s" diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java index 9b3fce82..26999d62 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java @@ -285,4 +285,23 @@ class StringUtilsTest extends BaseTest assertEquals("Abc", StringUtils.ucFirst("abc")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPluralFormat() + { + assertEquals("Apple", StringUtils.pluralFormat(1, "Apple{,s}")); + assertEquals("Apples", StringUtils.pluralFormat(0, "Apple{,s}")); + assertEquals("Apples", StringUtils.pluralFormat(2, "Apple{,s}")); + + assertEquals("Apple and Orange", StringUtils.pluralFormat(1, "Apple{,s} and Orange{,s}")); + assertEquals("Apples and Oranges", StringUtils.pluralFormat(2, "Apple{,s} and Orange{,s}")); + + assertEquals("Apple was eaten", StringUtils.pluralFormat(1, "Apple{,s} {was,were} eaten")); + assertEquals("Apples were eaten", StringUtils.pluralFormat(2, "Apple{,s} {was,were} eaten")); + } + } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java index bfae6eb9..e1fe3575 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java @@ -41,6 +41,7 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Calendar; @@ -49,6 +50,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.commons.lang.NotImplementedException; @@ -59,6 +61,8 @@ import org.apache.commons.lang.NotImplementedException; *******************************************************************************/ public class QueryManager { + private static final QLogger LOG = QLogger.getLogger(QueryManager.class); + public static final int DEFAULT_PAGE_SIZE = 2000; public static int PAGE_SIZE = DEFAULT_PAGE_SIZE; @@ -1409,13 +1413,44 @@ public class QueryManager *******************************************************************************/ public static Instant getInstant(ResultSet resultSet, int column) throws SQLException { - Timestamp value = resultSet.getTimestamp(column); - if(resultSet.wasNull()) + try { - return (null); - } + ///////////////////////////////////////////////////////////////////////////////////////////// + // this will be a zone-less date-time string, in the database server's configured timezone // + ///////////////////////////////////////////////////////////////////////////////////////////// + String string = resultSet.getString(column); + if(resultSet.wasNull()) + { + return (null); + } - return (value.toInstant()); + ////////////////////////////////////////////////////////////////////////////////////////////// + // make an Instant (which means UTC) from that zone-less date-time string. // + // if the database server was giving back non-utc times, we'd need a different ZoneId here? // + // e.g., as configured via ... a system property or database metadata setting // + ////////////////////////////////////////////////////////////////////////////////////////////// + LocalDateTime localDateTime = LocalDateTime.parse(string.replace(' ', 'T')); + ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("UTC")); + Instant instant = zonedDateTime.toInstant(); + return (instant); + } + catch(Exception e) + { + LOG.error("Error getting an instant value from a database result - proceeding with potentially wrong-timezone implementation...", e); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if for some reason the parsing and stuff above fails, well, this will give us back "some" date, maybe // + // this was our old logic, which probably had timezones wrong if server wasn't in UTC // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + Timestamp value = resultSet.getTimestamp(column); + if(resultSet.wasNull()) + { + return (null); + } + + Instant instant = value.toInstant(); + return (instant); + } }