mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 21:20:45 +00:00
Merge branch 'feature/datetime-query-expressions' into integration/sprint-29
This commit is contained in:
@ -26,8 +26,9 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
@ -36,6 +37,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
* A single criteria Component of a Query
|
||||
*
|
||||
*******************************************************************************/
|
||||
@JsonDeserialize(using = QFilterCriteriaDeserializer.class)
|
||||
public class QFilterCriteria implements Serializable, Cloneable
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QFilterCriteria.class);
|
||||
@ -44,8 +46,10 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
private QCriteriaOperator operator;
|
||||
private List<Serializable> values;
|
||||
|
||||
private String otherFieldName;
|
||||
private AbstractFilterExpression<?> expression;
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - probably implement this as a type of expression - though would require a little special handling i think when evaluating... //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private String otherFieldName;
|
||||
|
||||
|
||||
|
||||
@ -98,23 +102,6 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QFilterCriteria(String fieldName, QCriteriaOperator operator, AbstractFilterExpression<?> expression)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
this.operator = operator;
|
||||
this.expression = expression;
|
||||
|
||||
///////////////////////////////////////
|
||||
// this guy doesn't like to be null? //
|
||||
///////////////////////////////////////
|
||||
this.values = new ArrayList<>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -332,35 +319,4 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for expression
|
||||
*******************************************************************************/
|
||||
public AbstractFilterExpression<?> getExpression()
|
||||
{
|
||||
return (this.expression);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for expression
|
||||
*******************************************************************************/
|
||||
public void setExpression(AbstractFilterExpression<?> expression)
|
||||
{
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for expression
|
||||
*******************************************************************************/
|
||||
public QFilterCriteria withExpression(AbstractFilterExpression<?> expression)
|
||||
{
|
||||
this.expression = expression;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,10 +28,31 @@ import java.io.Serializable;
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractFilterExpression<T extends Serializable>
|
||||
public abstract class AbstractFilterExpression<T extends Serializable> implements Serializable
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract T evaluate();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** To help with serialization, define a "type" in all subclasses
|
||||
*******************************************************************************/
|
||||
public String getType()
|
||||
{
|
||||
return (getClass().getSimpleName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** noop - but here so serialization won't be upset about there being a type
|
||||
** in a json object.
|
||||
*******************************************************************************/
|
||||
public void setType(String type)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
@ -31,9 +35,9 @@ import java.util.concurrent.TimeUnit;
|
||||
*******************************************************************************/
|
||||
public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
{
|
||||
private final Operator operator;
|
||||
private final int amount;
|
||||
private final TimeUnit timeUnit;
|
||||
private Operator operator;
|
||||
private int amount;
|
||||
private ChronoUnit timeUnit;
|
||||
|
||||
|
||||
|
||||
@ -46,7 +50,17 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
private NowWithOffset(Operator operator, int amount, TimeUnit timeUnit)
|
||||
public NowWithOffset()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
private NowWithOffset(Operator operator, int amount, ChronoUnit timeUnit)
|
||||
{
|
||||
this.operator = operator;
|
||||
this.amount = amount;
|
||||
@ -59,7 +73,19 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Deprecated
|
||||
public static NowWithOffset minus(int amount, TimeUnit timeUnit)
|
||||
{
|
||||
return (minus(amount, timeUnit.toChronoUnit()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static NowWithOffset minus(int amount, ChronoUnit timeUnit)
|
||||
{
|
||||
return (new NowWithOffset(Operator.MINUS, amount, timeUnit));
|
||||
}
|
||||
@ -70,7 +96,19 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Deprecated
|
||||
public static NowWithOffset plus(int amount, TimeUnit timeUnit)
|
||||
{
|
||||
return (plus(amount, timeUnit.toChronoUnit()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static NowWithOffset plus(int amount, ChronoUnit timeUnit)
|
||||
{
|
||||
return (new NowWithOffset(Operator.PLUS, amount, timeUnit));
|
||||
}
|
||||
@ -83,14 +121,24 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
@Override
|
||||
public Instant evaluate()
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Instant doesn't let us plus/minus WEEK, MONTH, or YEAR... //
|
||||
// but LocalDateTime does. So, make a LDT in UTC, do the plus/minus, then //
|
||||
// convert back to Instant @ UTC //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
|
||||
|
||||
LocalDateTime then;
|
||||
if(operator.equals(Operator.PLUS))
|
||||
{
|
||||
return (Instant.now().plus(amount, timeUnit.toChronoUnit()));
|
||||
then = now.plus(amount, timeUnit);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (Instant.now().minus(amount, timeUnit.toChronoUnit()));
|
||||
then = now.minus(amount, timeUnit);
|
||||
}
|
||||
|
||||
return (then.toInstant(ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
|
||||
@ -121,7 +169,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
** Getter for timeUnit
|
||||
**
|
||||
*******************************************************************************/
|
||||
public TimeUnit getTimeUnit()
|
||||
public ChronoUnit getTimeUnit()
|
||||
{
|
||||
return timeUnit;
|
||||
}
|
||||
|
@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
|
||||
{
|
||||
private Operator operator;
|
||||
private ChronoUnit timeUnit;
|
||||
|
||||
|
||||
|
||||
public enum Operator
|
||||
{THIS, LAST}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ThisOrLastPeriod()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
private ThisOrLastPeriod(Operator operator, ChronoUnit timeUnit)
|
||||
{
|
||||
this.operator = operator;
|
||||
this.timeUnit = timeUnit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static ThisOrLastPeriod this_(ChronoUnit timeUnit)
|
||||
{
|
||||
return (new ThisOrLastPeriod(Operator.THIS, timeUnit));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static ThisOrLastPeriod last(int amount, ChronoUnit timeUnit)
|
||||
{
|
||||
return (new ThisOrLastPeriod(Operator.LAST, timeUnit));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Instant evaluate()
|
||||
{
|
||||
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
|
||||
|
||||
switch(timeUnit)
|
||||
{
|
||||
case HOURS ->
|
||||
{
|
||||
if(operator.equals(Operator.THIS))
|
||||
{
|
||||
return Instant.now().truncatedTo(ChronoUnit.HOURS);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Instant.now().minus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS);
|
||||
}
|
||||
}
|
||||
case DAYS ->
|
||||
{
|
||||
Instant startOfToday = ValueUtils.getStartOfTodayInZoneId(zoneId.getId());
|
||||
return operator.equals(Operator.THIS) ? startOfToday : startOfToday.minus(1, ChronoUnit.DAYS);
|
||||
}
|
||||
case WEEKS ->
|
||||
{
|
||||
Instant startOfToday = ValueUtils.getStartOfTodayInZoneId(zoneId.getId());
|
||||
LocalDateTime startOfThisWeekLDT = LocalDateTime.ofInstant(startOfToday, zoneId);
|
||||
while(startOfThisWeekLDT.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue())
|
||||
{
|
||||
////////////////////////////////////////
|
||||
// go backwards until sunday is found //
|
||||
////////////////////////////////////////
|
||||
startOfThisWeekLDT = startOfThisWeekLDT.minus(1, ChronoUnit.DAYS);
|
||||
}
|
||||
Instant startOfThisWeek = startOfThisWeekLDT.toInstant(zoneId.getRules().getOffset(startOfThisWeekLDT));
|
||||
|
||||
return operator.equals(Operator.THIS) ? startOfThisWeek : startOfThisWeek.minus(7, ChronoUnit.DAYS);
|
||||
}
|
||||
case MONTHS ->
|
||||
{
|
||||
Instant startOfThisMonth = ValueUtils.getStartOfMonthInZoneId(zoneId.getId());
|
||||
LocalDateTime startOfThisMonthLDT = LocalDateTime.ofInstant(startOfThisMonth, ZoneId.of(zoneId.getId()));
|
||||
LocalDateTime startOfLastMonthLDT = startOfThisMonthLDT.minus(1, ChronoUnit.MONTHS);
|
||||
Instant startOfLastMonth = startOfLastMonthLDT.toInstant(ZoneId.of(zoneId.getId()).getRules().getOffset(Instant.now()));
|
||||
|
||||
return operator.equals(Operator.THIS) ? startOfThisMonth : startOfLastMonth;
|
||||
}
|
||||
case YEARS ->
|
||||
{
|
||||
Instant startOfThisYear = ValueUtils.getStartOfYearInZoneId(zoneId.getId());
|
||||
LocalDateTime startOfThisYearLDT = LocalDateTime.ofInstant(startOfThisYear, zoneId);
|
||||
LocalDateTime startOfLastYearLDT = startOfThisYearLDT.minus(1, ChronoUnit.YEARS);
|
||||
Instant startOfLastYear = startOfLastYearLDT.toInstant(zoneId.getRules().getOffset(Instant.now()));
|
||||
|
||||
return operator.equals(Operator.THIS) ? startOfThisYear : startOfLastYear;
|
||||
}
|
||||
default -> throw (new QRuntimeException("Unsupported timeUnit: " + timeUnit));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for operator
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Operator getOperator()
|
||||
{
|
||||
return operator;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for timeUnit
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ChronoUnit getTimeUnit()
|
||||
{
|
||||
return timeUnit;
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.actions.tables.query.serialization;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Custom jackson deserializer, to deal w/ abstract expression field
|
||||
*******************************************************************************/
|
||||
public class QFilterCriteriaDeserializer extends StdDeserializer<QFilterCriteria>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QFilterCriteriaDeserializer()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QFilterCriteriaDeserializer(Class<?> vc)
|
||||
{
|
||||
super(vc);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QFilterCriteria deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException
|
||||
{
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/////////////////////////////////
|
||||
// get values out of json node //
|
||||
/////////////////////////////////
|
||||
List<Serializable> values = objectMapper.treeToValue(node.get("values"), List.class);
|
||||
String fieldName = objectMapper.treeToValue(node.get("fieldName"), String.class);
|
||||
QCriteriaOperator operator = objectMapper.treeToValue(node.get("operator"), QCriteriaOperator.class);
|
||||
String otherFieldName = objectMapper.treeToValue(node.get("otherFieldName"), String.class);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// look at all the values - if any of them are actually meant to be an Expression (instance of subclass of AbstractFilterExpression) //
|
||||
// they'll have deserialized as a Map, with a "type" key. If that's the case, then re/de serialize them into the proper expression type //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ListIterator<Serializable> valuesIterator = CollectionUtils.nonNullList(values).listIterator();
|
||||
while(valuesIterator.hasNext())
|
||||
{
|
||||
Object value = valuesIterator.next();
|
||||
if(value instanceof Map<?, ?> map && map.containsKey("type"))
|
||||
{
|
||||
String expressionType = ValueUtils.getValueAsString(map.get("type"));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// right now, we'll assume that all expression subclasses are in the same package as AbstractFilterExpression //
|
||||
// so, we can just do a Class.forName on that name, and use JsonUtils.toObject requesting that class. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
String assumedExpressionJSON = JsonUtils.toJson(map);
|
||||
|
||||
String className = AbstractFilterExpression.class.getName().replace(AbstractFilterExpression.class.getSimpleName(), expressionType);
|
||||
Serializable replacementValue = (Serializable) JsonUtils.toObject(assumedExpressionJSON, Class.forName(className));
|
||||
valuesIterator.set(replacementValue);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new IOException("Error deserializing criteria value which appeared to be an expression of type [" + expressionType + "] inside QFilterCriteria", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// put fields into return object //
|
||||
///////////////////////////////////
|
||||
QFilterCriteria criteria = new QFilterCriteria();
|
||||
criteria.setFieldName(fieldName);
|
||||
criteria.setOperator(operator);
|
||||
criteria.setValues(values);
|
||||
criteria.setOtherFieldName(otherFieldName);
|
||||
|
||||
return (criteria);
|
||||
}
|
||||
}
|
@ -151,8 +151,27 @@ public class JsonUtils
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T> T toObject(String json, Class<T> targetClass) throws IOException
|
||||
{
|
||||
return (toObject(json, targetClass, null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** De-serialize a json string into an object of the specified class - with
|
||||
** customizations on the Jackson ObjectMapper.
|
||||
**.
|
||||
**
|
||||
** Internally using jackson - so jackson annotations apply!
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T> T toObject(String json, Class<T> targetClass, Consumer<ObjectMapper> objectMapperCustomizer) throws IOException
|
||||
{
|
||||
ObjectMapper objectMapper = newObjectMapper();
|
||||
if(objectMapperCustomizer != null)
|
||||
{
|
||||
objectMapperCustomizer.accept(objectMapper);
|
||||
}
|
||||
return objectMapper.reader().readValue(json, targetClass);
|
||||
}
|
||||
|
||||
@ -172,6 +191,25 @@ public class JsonUtils
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** De-serialize a json string into an object of the specified class - with
|
||||
** customizations on the Jackson ObjectMapper.
|
||||
**
|
||||
** Internally using jackson - so jackson annotations apply!
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T> T toObject(String json, TypeReference<T> typeReference, Consumer<ObjectMapper> objectMapperCustomizer) throws IOException
|
||||
{
|
||||
ObjectMapper objectMapper = newObjectMapper();
|
||||
if(objectMapperCustomizer != null)
|
||||
{
|
||||
objectMapperCustomizer.accept(objectMapper);
|
||||
}
|
||||
return objectMapper.readValue(json, typeReference);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** De-serialize a json string into a JSONObject (string must start with "{")
|
||||
**
|
||||
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import org.assertj.core.data.Offset;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for NowWithOffset
|
||||
*******************************************************************************/
|
||||
class NowWithOffsetTest extends BaseTest
|
||||
{
|
||||
private static final long DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test()
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
long oneWeekAgoMillis = NowWithOffset.minus(1, ChronoUnit.WEEKS).evaluate().toEpochMilli();
|
||||
assertThat(oneWeekAgoMillis).isCloseTo(now - (7 * DAY_IN_MILLIS), Offset.offset(10_000L));
|
||||
|
||||
long oneWeekFromNowMillis = NowWithOffset.plus(2, ChronoUnit.WEEKS).evaluate().toEpochMilli();
|
||||
assertThat(oneWeekFromNowMillis).isCloseTo(now + (14 * DAY_IN_MILLIS), Offset.offset(10_000L));
|
||||
|
||||
long oneMonthAgoMillis = NowWithOffset.minus(1, ChronoUnit.MONTHS).evaluate().toEpochMilli();
|
||||
assertThat(oneMonthAgoMillis).isCloseTo(now - (30 * DAY_IN_MILLIS), Offset.offset(10_000L + 2 * DAY_IN_MILLIS));
|
||||
|
||||
long oneMonthFromNowMillis = NowWithOffset.plus(2, ChronoUnit.MONTHS).evaluate().toEpochMilli();
|
||||
assertThat(oneMonthFromNowMillis).isCloseTo(now + (60 * DAY_IN_MILLIS), Offset.offset(10_000L + 3 * DAY_IN_MILLIS));
|
||||
|
||||
long oneYearAgoMillis = NowWithOffset.minus(1, ChronoUnit.YEARS).evaluate().toEpochMilli();
|
||||
assertThat(oneYearAgoMillis).isCloseTo(now - (365 * DAY_IN_MILLIS), Offset.offset(10_000L + 2 * DAY_IN_MILLIS));
|
||||
|
||||
long oneYearFromNowMillis = NowWithOffset.plus(2, ChronoUnit.YEARS).evaluate().toEpochMilli();
|
||||
assertThat(oneYearFromNowMillis).isCloseTo(now + (730 * DAY_IN_MILLIS), Offset.offset(10_000L + 3 * DAY_IN_MILLIS));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.actions.tables.query.serialization;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.Now;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.ThisOrLastPeriod;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.BETWEEN;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.GREATER_THAN;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for QFilterCriteriaDeserializer
|
||||
*******************************************************************************/
|
||||
class QFilterCriteriaDeserializerTest extends BaseTest
|
||||
{
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testDeserialize() throws IOException
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// just put a reference to this class here, so it's a tad easier to find this class via navigation in IDE... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
new QFilterCriteriaDeserializer();
|
||||
|
||||
{
|
||||
QFilterCriteria criteria = JsonUtils.toObject("""
|
||||
{"fieldName": "id", "operator": "EQUALS", "values": [1]}
|
||||
""", QFilterCriteria.class);
|
||||
assertEquals("id", criteria.getFieldName());
|
||||
assertEquals(EQUALS, criteria.getOperator());
|
||||
assertEquals(List.of(1), criteria.getValues());
|
||||
}
|
||||
|
||||
{
|
||||
QFilterCriteria criteria = JsonUtils.toObject("""
|
||||
{"fieldName": "createDate", "operator": "GREATER_THAN", "values":
|
||||
[{"type": "NowWithOffset", "operator": "PLUS", "amount": 5, "timeUnit": "MINUTES"}]
|
||||
}
|
||||
""", QFilterCriteria.class);
|
||||
assertEquals("createDate", criteria.getFieldName());
|
||||
assertEquals(GREATER_THAN, criteria.getOperator());
|
||||
AbstractFilterExpression<?> expression = (AbstractFilterExpression<?>) criteria.getValues().get(0);
|
||||
assertThat(expression).isInstanceOf(NowWithOffset.class);
|
||||
NowWithOffset nowWithOffset = (NowWithOffset) expression;
|
||||
assertEquals(5, nowWithOffset.getAmount());
|
||||
assertEquals(NowWithOffset.Operator.PLUS, nowWithOffset.getOperator());
|
||||
assertEquals(ChronoUnit.MINUTES, nowWithOffset.getTimeUnit());
|
||||
}
|
||||
|
||||
{
|
||||
QFilterCriteria criteria = JsonUtils.toObject("""
|
||||
{"fieldName": "orderDate", "operator": "EQUALS", "values": [{"type": "Now"}] }
|
||||
""", QFilterCriteria.class);
|
||||
assertEquals("orderDate", criteria.getFieldName());
|
||||
assertEquals(EQUALS, criteria.getOperator());
|
||||
AbstractFilterExpression<?> expression = (AbstractFilterExpression<?>) criteria.getValues().get(0);
|
||||
assertThat(expression).isInstanceOf(Now.class);
|
||||
}
|
||||
|
||||
{
|
||||
QFilterCriteria criteria = JsonUtils.toObject("""
|
||||
{"fieldName": "orderDate", "operator": "BETWEEN", "values": [{"type": "Now"}, {"type": "ThisOrLastPeriod"}] }
|
||||
""", QFilterCriteria.class);
|
||||
assertEquals("orderDate", criteria.getFieldName());
|
||||
assertEquals(BETWEEN, criteria.getOperator());
|
||||
AbstractFilterExpression<?> expression0 = (AbstractFilterExpression<?>) criteria.getValues().get(0);
|
||||
assertThat(expression0).isInstanceOf(Now.class);
|
||||
AbstractFilterExpression<?> expression1 = (AbstractFilterExpression<?>) criteria.getValues().get(1);
|
||||
assertThat(expression1).isInstanceOf(ThisOrLastPeriod.class);
|
||||
}
|
||||
|
||||
{
|
||||
assertThatThrownBy(() -> JsonUtils.toObject("""
|
||||
{"fieldName": "orderDate", "operator": "BETWEEN", "values": [{"type": "NotAnExpressionType"}] }
|
||||
""", QFilterCriteria.class)).hasMessageContaining("Error deserializing criteria value which appeared to be an expression");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user