mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
Initial version of QProcessPayload - like QRecordEntity, but for process values. refactoring of QRecordEntity to share logic
This commit is contained in:
@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. 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.processes;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||||
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ReflectiveBeanLikeClassUtils;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** base-class for bean-like classes to represent the fields of a process.
|
||||||
|
** similar in spirit to QRecordEntity, but for processes.
|
||||||
|
*******************************************************************************/
|
||||||
|
public class QProcessPayload
|
||||||
|
{
|
||||||
|
private static final QLogger LOG = QLogger.getLogger(QProcessPayload.class);
|
||||||
|
|
||||||
|
private static final ListingHash<Class<? extends QProcessPayload>, QRecordEntityField> fieldMapping = new ListingHash<>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Build an entity of this QRecord type from a QRecord
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static <T extends QProcessPayload> T fromProcessState(Class<T> c, ProcessState processState) throws QException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T entity = c.getConstructor().newInstance();
|
||||||
|
entity.populateFromProcessState(processState);
|
||||||
|
return (entity);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
throw (new QException("Error building process payload from state.", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
protected void populateFromProcessState(ProcessState processState)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<QRecordEntityField> fieldList = getFieldList(this.getClass());
|
||||||
|
// originalRecordValues = new HashMap<>();
|
||||||
|
|
||||||
|
for(QRecordEntityField qRecordEntityField : fieldList)
|
||||||
|
{
|
||||||
|
Serializable value = processState.getValues().get(qRecordEntityField.getFieldName());
|
||||||
|
Object typedValue = qRecordEntityField.convertValueType(value);
|
||||||
|
qRecordEntityField.getSetter().invoke(this, typedValue);
|
||||||
|
// originalRecordValues.put(qRecordEntityField.getFieldName(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
|
||||||
|
// {
|
||||||
|
// List<QRecord> associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name());
|
||||||
|
// if(associatedRecords == null)
|
||||||
|
// {
|
||||||
|
// qRecordEntityAssociation.getSetter().invoke(this, (Object) null);
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// List<QRecordEntity> associatedEntityList = new ArrayList<>();
|
||||||
|
// for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords))
|
||||||
|
// {
|
||||||
|
// associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord));
|
||||||
|
// }
|
||||||
|
// qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
|
||||||
|
// {
|
||||||
|
// List<QRecord> associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name());
|
||||||
|
// if(associatedRecords == null)
|
||||||
|
// {
|
||||||
|
// qRecordEntityAssociation.getSetter().invoke(this, (Object) null);
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// List<QRecordEntity> associatedEntityList = new ArrayList<>();
|
||||||
|
// for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords))
|
||||||
|
// {
|
||||||
|
// associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord));
|
||||||
|
// }
|
||||||
|
// qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
throw (new QRuntimeException("Error building process payload from process state.", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Copy the values from this payload into the given process state.
|
||||||
|
** ALL fields in the entity will be set in the process state.
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void toProcessState(ProcessState processState) throws QRuntimeException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
|
||||||
|
{
|
||||||
|
processState.getValues().put(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
throw (new QRuntimeException("Error populating process state from process payload.", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
public static Set<Class<?>> allowedFieldTypes()
|
||||||
|
{
|
||||||
|
HashSet<Class<?>> classes = new HashSet<>(ReflectiveBeanLikeClassUtils.defaultAllowedTypes());
|
||||||
|
classes.add(Map.class);
|
||||||
|
classes.add(List.class);
|
||||||
|
return (classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static List<QRecordEntityField> getFieldList(Class<? extends QProcessPayload> c)
|
||||||
|
{
|
||||||
|
if(!fieldMapping.containsKey(c))
|
||||||
|
{
|
||||||
|
List<QRecordEntityField> fieldList = new ArrayList<>();
|
||||||
|
for(Method possibleGetter : c.getMethods())
|
||||||
|
{
|
||||||
|
if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, false, allowedFieldTypes()))
|
||||||
|
{
|
||||||
|
Optional<Method> setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter);
|
||||||
|
|
||||||
|
if(setter.isPresent())
|
||||||
|
{
|
||||||
|
String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter);
|
||||||
|
fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), null));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG.debug("Getter method [" + possibleGetter.getName() + "] does not have a corresponding setter.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldMapping.put(c, fieldList);
|
||||||
|
}
|
||||||
|
return (fieldMapping.get(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -628,4 +628,15 @@ public class RunBackendStepInput extends AbstractActionInput
|
|||||||
{
|
{
|
||||||
return (QContext.getQInstance().getProcess(getProcessName()));
|
return (QContext.getQInstance().getProcess(getProcessName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** return a QProcessPayload subclass instance, with values populated from
|
||||||
|
** the current process state.
|
||||||
|
***************************************************************************/
|
||||||
|
public <T extends QProcessPayload> T getProcessPayload(Class<T> payloadClass) throws QException
|
||||||
|
{
|
||||||
|
return QProcessPayload.fromProcessState(payloadClass, getProcessState());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -445,4 +445,14 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
|
|||||||
this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment);
|
this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** Update the process state with values from the input processPayload
|
||||||
|
** subclass instance.
|
||||||
|
***************************************************************************/
|
||||||
|
public void setProcessPayload(QProcessPayload processPayload)
|
||||||
|
{
|
||||||
|
processPayload.toProcessState(getProcessState());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,20 +24,11 @@ package com.kingsrook.qqq.backend.core.model.data;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.AnnotatedParameterizedType;
|
|
||||||
import java.lang.reflect.AnnotatedType;
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalTime;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -49,6 +40,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
|||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||||
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
|
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ReflectiveBeanLikeClassUtils;
|
||||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||||
|
|
||||||
|
|
||||||
@ -325,13 +317,13 @@ public abstract class QRecordEntity
|
|||||||
List<QRecordEntityField> fieldList = new ArrayList<>();
|
List<QRecordEntityField> fieldList = new ArrayList<>();
|
||||||
for(Method possibleGetter : c.getMethods())
|
for(Method possibleGetter : c.getMethods())
|
||||||
{
|
{
|
||||||
if(isGetter(possibleGetter))
|
if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, true))
|
||||||
{
|
{
|
||||||
Optional<Method> setter = getSetterForGetter(c, possibleGetter);
|
Optional<Method> setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter);
|
||||||
|
|
||||||
if(setter.isPresent())
|
if(setter.isPresent())
|
||||||
{
|
{
|
||||||
String fieldName = getFieldNameFromGetter(possibleGetter);
|
String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter);
|
||||||
Optional<QField> fieldAnnotation = getQFieldAnnotation(c, fieldName);
|
Optional<QField> fieldAnnotation = getQFieldAnnotation(c, fieldName);
|
||||||
|
|
||||||
if(fieldAnnotation.isPresent())
|
if(fieldAnnotation.isPresent())
|
||||||
@ -378,19 +370,19 @@ public abstract class QRecordEntity
|
|||||||
List<QRecordEntityAssociation> associationList = new ArrayList<>();
|
List<QRecordEntityAssociation> associationList = new ArrayList<>();
|
||||||
for(Method possibleGetter : c.getMethods())
|
for(Method possibleGetter : c.getMethods())
|
||||||
{
|
{
|
||||||
if(isGetter(possibleGetter))
|
if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, true))
|
||||||
{
|
{
|
||||||
Optional<Method> setter = getSetterForGetter(c, possibleGetter);
|
Optional<Method> setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter);
|
||||||
|
|
||||||
if(setter.isPresent())
|
if(setter.isPresent())
|
||||||
{
|
{
|
||||||
String fieldName = getFieldNameFromGetter(possibleGetter);
|
String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter);
|
||||||
Optional<QAssociation> associationAnnotation = getQAssociationAnnotation(c, fieldName);
|
Optional<QAssociation> associationAnnotation = getQAssociationAnnotation(c, fieldName);
|
||||||
|
|
||||||
if(associationAnnotation.isPresent())
|
if(associationAnnotation.isPresent())
|
||||||
{
|
{
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Class<? extends QRecordEntity> listTypeParam = (Class<? extends QRecordEntity>) getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType());
|
Class<? extends QRecordEntity> listTypeParam = (Class<? extends QRecordEntity>) ReflectiveBeanLikeClassUtils.getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType());
|
||||||
associationList.add(new QRecordEntityAssociation(fieldName, possibleGetter, setter.get(), listTypeParam, associationAnnotation.orElse(null)));
|
associationList.add(new QRecordEntityAssociation(fieldName, possibleGetter, setter.get(), listTypeParam, associationAnnotation.orElse(null)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -457,130 +449,4 @@ public abstract class QRecordEntity
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public static String getFieldNameFromGetter(Method getter)
|
|
||||||
{
|
|
||||||
String nameWithoutGet = getter.getName().replaceFirst("^get", "");
|
|
||||||
if(nameWithoutGet.length() == 1)
|
|
||||||
{
|
|
||||||
return (nameWithoutGet.toLowerCase(Locale.ROOT));
|
|
||||||
}
|
|
||||||
return (nameWithoutGet.substring(0, 1).toLowerCase(Locale.ROOT) + nameWithoutGet.substring(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private static boolean isGetter(Method method)
|
|
||||||
{
|
|
||||||
if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*"))
|
|
||||||
{
|
|
||||||
if(isSupportedFieldType(method.getReturnType()) || isSupportedAssociation(method.getReturnType(), method.getAnnotatedReturnType()))
|
|
||||||
{
|
|
||||||
return (true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if(!method.getName().equals("getClass") && method.getAnnotation(QIgnore.class) == null)
|
|
||||||
{
|
|
||||||
LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private static Optional<Method> getSetterForGetter(Class<? extends QRecordEntity> c, Method getter)
|
|
||||||
{
|
|
||||||
String setterName = getter.getName().replaceFirst("^get", "set");
|
|
||||||
for(Method method : c.getMethods())
|
|
||||||
{
|
|
||||||
if(method.getName().equals(setterName))
|
|
||||||
{
|
|
||||||
if(method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(getter.getReturnType()))
|
|
||||||
{
|
|
||||||
return (Optional.of(method));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG.info("Method [" + method.getName() + "] looks like a setter for [" + getter.getName() + "], but its parameters, [" + Arrays.toString(method.getParameterTypes()) + "], don't match the getter's return type [" + getter.getReturnType() + "]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private static boolean isSupportedFieldType(Class<?> returnType)
|
|
||||||
{
|
|
||||||
// todo - more types!!
|
|
||||||
return (returnType.equals(String.class)
|
|
||||||
|| returnType.equals(Integer.class)
|
|
||||||
|| returnType.equals(Long.class)
|
|
||||||
|| returnType.equals(int.class)
|
|
||||||
|| returnType.equals(Boolean.class)
|
|
||||||
|| returnType.equals(boolean.class)
|
|
||||||
|| returnType.equals(BigDecimal.class)
|
|
||||||
|| returnType.equals(Instant.class)
|
|
||||||
|| returnType.equals(LocalDate.class)
|
|
||||||
|| returnType.equals(LocalTime.class)
|
|
||||||
|| returnType.equals(byte[].class));
|
|
||||||
/////////////////////////////////////////////
|
|
||||||
// note - this list has implications upon: //
|
|
||||||
// - QFieldType.fromClass //
|
|
||||||
// - QRecordEntityField.convertValueType //
|
|
||||||
/////////////////////////////////////////////
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private static boolean isSupportedAssociation(Class<?> returnType, AnnotatedType annotatedType)
|
|
||||||
{
|
|
||||||
Class<?> listTypeParam = getListTypeParam(returnType, annotatedType);
|
|
||||||
return (listTypeParam != null && QRecordEntity.class.isAssignableFrom(listTypeParam));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private static Class<?> getListTypeParam(Class<?> listType, AnnotatedType annotatedType)
|
|
||||||
{
|
|
||||||
if(listType.equals(List.class))
|
|
||||||
{
|
|
||||||
if(annotatedType instanceof AnnotatedParameterizedType apt)
|
|
||||||
{
|
|
||||||
AnnotatedType[] annotatedActualTypeArguments = apt.getAnnotatedActualTypeArguments();
|
|
||||||
for(AnnotatedType annotatedActualTypeArgument : annotatedActualTypeArguments)
|
|
||||||
{
|
|
||||||
Type type = annotatedActualTypeArgument.getType();
|
|
||||||
if(type instanceof Class<?> c)
|
|
||||||
{
|
|
||||||
return (c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (null);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import java.math.BigDecimal;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
|
import java.util.Map;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
|
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
|
||||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
|
|
||||||
@ -170,6 +171,11 @@ public class QRecordEntityField
|
|||||||
{
|
{
|
||||||
return (ValueUtils.getValueAsByteArray(value));
|
return (ValueUtils.getValueAsByteArray(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(type.equals(Map.class))
|
||||||
|
{
|
||||||
|
return (ValueUtils.getValueAsMap(value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
|
@ -46,6 +46,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
|
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ReflectiveBeanLikeClassUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||||
@ -188,7 +189,7 @@ public class QFieldMetaData implements Cloneable, QMetaDataObject
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.name = QRecordEntity.getFieldNameFromGetter(getter);
|
this.name = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(getter);
|
||||||
this.type = QFieldType.fromClass(getter.getReturnType());
|
this.type = QFieldType.fromClass(getter.getReturnType());
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. 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.utils;
|
||||||
|
|
||||||
|
|
||||||
|
import java.lang.reflect.AnnotatedParameterizedType;
|
||||||
|
import java.lang.reflect.AnnotatedType;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QIgnore;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Utilities for bean-like classes (e.g., QRecordEntity, QProcessPayload) that
|
||||||
|
** use reflection to understand their bean-fields
|
||||||
|
*******************************************************************************/
|
||||||
|
public class ReflectiveBeanLikeClassUtils
|
||||||
|
{
|
||||||
|
private static final QLogger LOG = QLogger.getLogger(ReflectiveBeanLikeClassUtils.class);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static String getFieldNameFromGetter(Method getter)
|
||||||
|
{
|
||||||
|
String nameWithoutGet = getter.getName().replaceFirst("^get", "");
|
||||||
|
if(nameWithoutGet.length() == 1)
|
||||||
|
{
|
||||||
|
return (nameWithoutGet.toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
return (nameWithoutGet.substring(0, 1).toLowerCase(Locale.ROOT) + nameWithoutGet.substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static boolean isGetter(Method method, boolean allowAssociations)
|
||||||
|
{
|
||||||
|
return isGetter(method, allowAssociations, defaultAllowedTypes());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static boolean isGetter(Method method, boolean allowAssociations, Collection<Class<?>> allowedTypes)
|
||||||
|
{
|
||||||
|
if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*"))
|
||||||
|
{
|
||||||
|
if(allowedTypes.contains(method.getReturnType()) || (allowAssociations && isSupportedAssociation(method.getReturnType(), method.getAnnotatedReturnType())))
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(!method.getName().equals("getClass") && method.getAnnotation(QIgnore.class) == null)
|
||||||
|
{
|
||||||
|
LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static Optional<Method> getSetterForGetter(Class<?> c, Method getter)
|
||||||
|
{
|
||||||
|
String setterName = getter.getName().replaceFirst("^get", "set");
|
||||||
|
for(Method method : c.getMethods())
|
||||||
|
{
|
||||||
|
if(method.getName().equals(setterName))
|
||||||
|
{
|
||||||
|
if(method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(getter.getReturnType()))
|
||||||
|
{
|
||||||
|
return (Optional.of(method));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG.info("Method [" + method.getName() + "] looks like a setter for [" + getter.getName() + "], but its parameters, [" + Arrays.toString(method.getParameterTypes()) + "], don't match the getter's return type [" + getter.getReturnType() + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
public static Collection<Class<?>> defaultAllowedTypes()
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// note - this list has implications upon: //
|
||||||
|
// - QFieldType.fromClass //
|
||||||
|
// - QRecordEntityField.convertValueType //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
return (Set.of(String.class,
|
||||||
|
Integer.class,
|
||||||
|
Long.class,
|
||||||
|
int.class,
|
||||||
|
Boolean.class,
|
||||||
|
boolean.class,
|
||||||
|
BigDecimal.class,
|
||||||
|
Instant.class,
|
||||||
|
LocalDate.class,
|
||||||
|
LocalTime.class,
|
||||||
|
byte[].class));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static boolean isSupportedAssociation(Class<?> returnType, AnnotatedType annotatedType)
|
||||||
|
{
|
||||||
|
Class<?> listTypeParam = getListTypeParam(returnType, annotatedType);
|
||||||
|
return (listTypeParam != null && QRecordEntity.class.isAssignableFrom(listTypeParam));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static Class<?> getListTypeParam(Class<?> listType, AnnotatedType annotatedType)
|
||||||
|
{
|
||||||
|
if(listType.equals(List.class))
|
||||||
|
{
|
||||||
|
if(annotatedType instanceof AnnotatedParameterizedType apt)
|
||||||
|
{
|
||||||
|
AnnotatedType[] annotatedActualTypeArguments = apt.getAnnotatedActualTypeArguments();
|
||||||
|
for(AnnotatedType annotatedActualTypeArgument : annotatedActualTypeArguments)
|
||||||
|
{
|
||||||
|
Type type = annotatedActualTypeArgument.getType();
|
||||||
|
if(type instanceof Class<?> c)
|
||||||
|
{
|
||||||
|
return (c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -22,6 +22,7 @@
|
|||||||
package com.kingsrook.qqq.backend.core.utils;
|
package com.kingsrook.qqq.backend.core.utils;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@ -38,6 +39,7 @@ import java.time.temporal.ChronoField;
|
|||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
|
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
|
||||||
@ -1012,4 +1014,37 @@ public class ValueUtils
|
|||||||
|
|
||||||
return defaultIfCannotInfer;
|
return defaultIfCannotInfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
public static Map getValueAsMap(Serializable value)
|
||||||
|
{
|
||||||
|
if(value == null)
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
else if(value instanceof Map<?, ?> map)
|
||||||
|
{
|
||||||
|
return (map);
|
||||||
|
}
|
||||||
|
else if(value instanceof String string && string.startsWith("{") && string.endsWith("}"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Map map = JsonUtils.toObject(string, Map.class);
|
||||||
|
return (map);
|
||||||
|
}
|
||||||
|
catch(IOException e)
|
||||||
|
{
|
||||||
|
throw new QValueException("Error parsing string to map", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new QValueException("Unrecognized object type in getValueAsMap: " + value.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user