Merge branch 'feature/sprint-21' into dev

This commit is contained in:
Tim Chamberlain
2023-03-02 15:17:27 -06:00
8 changed files with 442 additions and 9 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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));
}
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode;
import java.io.Serializable;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.actions.widgets.RenderWidgetInput;
@ -35,6 +36,8 @@ public abstract class AbstractWidgetValueSource
protected String name; protected String name;
protected String type; protected String type;
protected Map<String, Serializable> inputValues;
/******************************************************************************* /*******************************************************************************
@ -116,4 +119,35 @@ public abstract class AbstractWidgetValueSource
//////////////////////// ////////////////////////
} }
/*******************************************************************************
** Getter for inputValues
*******************************************************************************/
public Map<String, Serializable> getInputValues()
{
return (this.inputValues);
}
/*******************************************************************************
** Setter for inputValues
*******************************************************************************/
public void setInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
}
/*******************************************************************************
** Fluent setter for inputValues
*******************************************************************************/
public AbstractWidgetValueSource withInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
return (this);
}
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Object> 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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Object> context, RenderWidgetInput input) throws QException
{
if(inputValues != null)
{
context.putAll(inputValues);
}
Function<Object, Object> 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<String, Serializable> inputValues)
{
this.inputValues = inputValues;
return (this);
}
}

View File

@ -74,14 +74,14 @@ public class WidgetCalculation extends AbstractWidgetValueSource
{ {
Instant now = Instant.now(); Instant now = Instant.now();
Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0))); 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<String> valueNames, Map<String, Object> context) -> AGE_SECONDS((List<String> valueNames, Map<String, Object> context) ->
{ {
Instant now = Instant.now(); Instant now = Instant.now();
Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0))); 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<String> valueNames, Map<String, Object> context) -> PERCENT_CHANGE((List<String> valueNames, Map<String, Object> context) ->
@ -92,8 +92,8 @@ public class WidgetCalculation extends AbstractWidgetValueSource
/////////////////////////////////////////////// ///////////////////////////////////////////////
// 100 * ( (current - previous) / previous ) // // 100 * ( (current - previous) / previous ) //
/////////////////////////////////////////////// ///////////////////////////////////////////////
BigDecimal difference = current.subtract(previous); BigDecimal difference = current == null ? null : current.subtract(previous);
if(BigDecimal.ZERO.equals(previous)) if(BigDecimal.ZERO.equals(previous) || difference == null)
{ {
return (null); return (null);
} }

View File

@ -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 ** 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" ** it is 1 (the singular) or not-1 (0 or 2+, the plural). Get back "" or "s"

View File

@ -285,4 +285,23 @@ class StringUtilsTest extends BaseTest
assertEquals("Abc", StringUtils.ucFirst("abc")); 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"));
}
} }

View File

@ -41,6 +41,7 @@ import java.time.LocalTime;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
@ -49,6 +50,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.NotImplementedException; import org.apache.commons.lang.NotImplementedException;
@ -59,6 +61,8 @@ import org.apache.commons.lang.NotImplementedException;
*******************************************************************************/ *******************************************************************************/
public class QueryManager public class QueryManager
{ {
private static final QLogger LOG = QLogger.getLogger(QueryManager.class);
public static final int DEFAULT_PAGE_SIZE = 2000; public static final int DEFAULT_PAGE_SIZE = 2000;
public static int PAGE_SIZE = DEFAULT_PAGE_SIZE; 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 public static Instant getInstant(ResultSet resultSet, int column) throws SQLException
{ {
Timestamp value = resultSet.getTimestamp(column); try
if(resultSet.wasNull())
{ {
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);
}
} }