Adding QRecordEntity

This commit is contained in:
2022-07-14 09:32:38 -05:00
parent 7d7b0297cd
commit 2da998e0d8
9 changed files with 920 additions and 7 deletions

View File

@ -27,6 +27,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -255,7 +256,6 @@ public class QRecord implements Serializable
/*******************************************************************************
**
*******************************************************************************/
@ -343,4 +343,13 @@ public class QRecord implements Serializable
return (String) this.backendDetails.get(key);
}
/*******************************************************************************
** Convert this record to an QRecordEntity
*******************************************************************************/
public <T extends QRecordEntity> T toEntity(Class<T> c) throws QException
{
return (QRecordEntity.fromQRecord(c, this));
}
}

View File

@ -0,0 +1,213 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Base class for entity beans that are interoperable with QRecords.
*******************************************************************************/
public abstract class QRecordEntity
{
private static final Logger LOG = LogManager.getLogger(QRecordEntity.class);
private static final ListingHash<Class<? extends QRecordEntity>, QRecordEntityField> fieldMapping = new ListingHash<>();
/*******************************************************************************
** Build an entity of this QRecord type from a QRecord
**
*******************************************************************************/
public static <T extends QRecordEntity> T fromQRecord(Class<T> c, QRecord qRecord) throws QException
{
try
{
T entity = c.getConstructor().newInstance();
List<QRecordEntityField> fieldList = getFieldList(c);
for(QRecordEntityField qRecordEntityField : fieldList)
{
Serializable value = qRecord.getValue(qRecordEntityField.getFieldName());
Object typedValue = qRecordEntityField.convertValueType(value);
qRecordEntityField.getSetter().invoke(entity, typedValue);
}
return (entity);
}
catch(Exception e)
{
throw (new QException("Error building entity from qRecord.", e));
}
}
/*******************************************************************************
** Convert this entity to a QRecord.
**
*******************************************************************************/
public QRecord toQRecord() throws QException
{
try
{
QRecord qRecord = new QRecord();
List<QRecordEntityField> fieldList = getFieldList(this.getClass());
for(QRecordEntityField qRecordEntityField : fieldList)
{
qRecord.setValue(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this));
}
return (qRecord);
}
catch(Exception e)
{
throw (new QException("Error building qRecord from entity.", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private static List<QRecordEntityField> getFieldList(Class<? extends QRecordEntity> c)
{
if(!fieldMapping.containsKey(c))
{
List<QRecordEntityField> fieldList = new ArrayList<>();
for(Method possibleGetter : c.getMethods())
{
if(isGetter(possibleGetter))
{
Optional<Method> setter = getSetterForGetter(c, possibleGetter);
if(setter.isPresent())
{
String name = getFieldNameFromGetter(possibleGetter);
fieldList.add(new QRecordEntityField(name, possibleGetter, setter.get(), possibleGetter.getReturnType()));
}
else
{
LOG.info("Getter method [" + possibleGetter.getName() + "] does not have a corresponding setter.");
}
}
}
fieldMapping.put(c, fieldList);
}
return (fieldMapping.get(c));
}
/*******************************************************************************
**
*******************************************************************************/
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()))
{
return (true);
}
else
{
if(!method.getName().equals("getClass"))
{
LOG.info("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], 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(int.class)
|| returnType.equals(Boolean.class)
|| returnType.equals(boolean.class)
|| returnType.equals(BigDecimal.class));
}
}

View File

@ -0,0 +1,138 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Reflective information about a field in a QRecordEntity
*******************************************************************************/
public class QRecordEntityField
{
private final String fieldName;
private final Method getter;
private final Method setter;
private final Class<?> type;
/*******************************************************************************
** Constructor.
*******************************************************************************/
public QRecordEntityField(String fieldName, Method getter, Method setter, Class<?> type)
{
this.fieldName = fieldName;
this.getter = getter;
this.setter = setter;
this.type = type;
}
/*******************************************************************************
** Getter for fieldName
**
*******************************************************************************/
public String getFieldName()
{
return fieldName;
}
/*******************************************************************************
** Getter for getter
**
*******************************************************************************/
public Method getGetter()
{
return getter;
}
/*******************************************************************************
** Getter for setter
**
*******************************************************************************/
public Method getSetter()
{
return setter;
}
/*******************************************************************************
** Getter for type
**
*******************************************************************************/
public Class<?> getType()
{
return type;
}
/*******************************************************************************
**
*******************************************************************************/
public Object convertValueType(Serializable value)
{
if(value == null)
{
return (null);
}
if(value.getClass().equals(type))
{
return (value);
}
if(type.equals(String.class))
{
return (ValueUtils.getValueAsString(value));
}
if(type.equals(Integer.class) || type.equals(int.class))
{
return (ValueUtils.getValueAsInteger(value));
}
if(type.equals(Boolean.class) || type.equals(boolean.class))
{
return (ValueUtils.getValueAsBoolean(value));
}
if(type.equals(BigDecimal.class))
{
return (ValueUtils.getValueAsBigDecimal(value));
}
throw (new QValueException("Unhandled value type [" + type + "] for field [" + fieldName + "]"));
}
}

View File

@ -23,6 +23,10 @@ package com.kingsrook.qqq.backend.core.model.metadata;
import java.io.Serializable;
import java.lang.reflect.Method;
import com.github.hervian.reflection.Fun;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
@ -31,14 +35,14 @@ import java.io.Serializable;
*******************************************************************************/
public class QFieldMetaData
{
private String name;
private String label;
private String backendName;
private String name;
private String label;
private String backendName;
private QFieldType type;
private boolean isRequired = false;
private boolean isRequired = false;
private Serializable defaultValue;
private String possibleValueSourceName;
private String possibleValueSourceName;
@ -62,6 +66,30 @@ public class QFieldMetaData
/*******************************************************************************
** Initialize a fieldMetaData from a reference to a getter on an entity.
** e.g., new QFieldMetaData(Order::getOrderNo).
*******************************************************************************/
public <T> QFieldMetaData(Fun.With1ParamAndVoid<T> getterRef) throws QException
{
try
{
Method getter = Fun.toMethod(getterRef);
this.name = QRecordEntity.getFieldNameFromGetter(getter);
this.type = QFieldType.fromClass(getter.getReturnType());
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error constructing field from getterRef: " + getterRef, e));
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -22,6 +22,10 @@
package com.kingsrook.qqq.backend.core.model.metadata;
import java.math.BigDecimal;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
** Possible data types for Q-fields.
**
@ -36,5 +40,28 @@ public enum QFieldType
DATE_TIME,
TEXT,
HTML,
PASSWORD
PASSWORD;
/*******************************************************************************
** Get a field type enum constant for a java class.
*******************************************************************************/
public static QFieldType fromClass(Class<?> c) throws QException
{
if(c.equals(String.class))
{
return (STRING);
}
if(c.equals(Integer.class) || c.equals(int.class))
{
return (INTEGER);
}
if(c.equals(BigDecimal.class))
{
return (DECIMAL);
}
throw (new QException("Unrecognized class [" + c + "]"));
}
}

View File

@ -0,0 +1,195 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data;
import java.math.BigDecimal;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.testentities.Item;
import com.kingsrook.qqq.backend.core.model.data.testentities.ItemWithPrimitives;
import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
**
*******************************************************************************/
class QRecordEntityTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testItemToQRecord() throws QException
{
Item item = new Item();
item.setSku("ABC-123");
item.setDescription("My Item");
item.setQuantity(47);
item.setPrice(new BigDecimal("3.50"));
item.setFeatured(true);
QRecord qRecord = item.toQRecord();
assertEquals("ABC-123", qRecord.getValueString("sku"));
assertEquals("My Item", qRecord.getValueString("description"));
assertEquals(47, qRecord.getValueInteger("quantity"));
assertEquals(new BigDecimal("3.50"), qRecord.getValueBigDecimal("price"));
assertTrue(qRecord.getValueBoolean("featured"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQRecordToItem() throws QException
{
QRecord qRecord = new QRecord()
.withValue("sku", "WXYZ-9876")
.withValue("description", "Items are cool")
.withValue("quantity", 42)
.withValue("price", new BigDecimal("3.50"))
.withValue("featured", false);
Item item = qRecord.toEntity(Item.class);
assertEquals("WXYZ-9876", item.getSku());
assertEquals("Items are cool", item.getDescription());
assertEquals(42, item.getQuantity());
assertEquals(new BigDecimal("3.50"), item.getPrice());
assertFalse(item.getFeatured());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testItemWithPrimitivesToQRecord() throws QException
{
ItemWithPrimitives item = new ItemWithPrimitives();
item.setSku("ABC-123");
item.setDescription(null);
item.setQuantity(47);
item.setPrice(new BigDecimal("3.50"));
item.setFeatured(true);
QRecord qRecord = item.toQRecord();
assertEquals("ABC-123", qRecord.getValueString("sku"));
assertNull(qRecord.getValueString("description"));
assertEquals(47, qRecord.getValueInteger("quantity"));
assertEquals(new BigDecimal("3.50"), qRecord.getValueBigDecimal("price"));
assertTrue(qRecord.getValueBoolean("featured"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQRecordToItemWithPrimitives() throws QException
{
QRecord qRecord = new QRecord()
.withValue("sku", "WXYZ-9876")
.withValue("description", null)
.withValue("quantity", 42)
.withValue("price", new BigDecimal("3.50"))
.withValue("featured", false);
ItemWithPrimitives item = qRecord.toEntity(ItemWithPrimitives.class);
assertEquals("WXYZ-9876", item.getSku());
assertNull(item.getDescription());
assertEquals(42, item.getQuantity());
assertEquals(new BigDecimal("3.50"), item.getPrice());
assertFalse(item.getFeatured());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQRecordWithAllStringValuesToItem() throws QException
{
QRecord qRecord = new QRecord()
.withValue("sku", "WXYZ-9876")
.withValue("description", "Items are cool")
.withValue("quantity", "42")
.withValue("price", "3.50")
.withValue("featured", "false");
Item item = qRecord.toEntity(Item.class);
assertEquals("WXYZ-9876", item.getSku());
assertEquals("Items are cool", item.getDescription());
assertEquals(42, item.getQuantity());
assertEquals(new BigDecimal("3.50"), item.getPrice());
assertFalse(item.getFeatured());
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
void testQTableConstructionFromEntity() throws QException
{
QTableMetaData qTableMetaData = new QTableMetaData()
.withField(new QFieldMetaData(Item::getSku))
.withField(new QFieldMetaData(Item::getDescription))
.withField(new QFieldMetaData(Item::getQuantity));
assertEquals(QFieldType.STRING, qTableMetaData.getField("sku").getType());
assertEquals(QFieldType.INTEGER, qTableMetaData.getField("quantity").getType());
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
void testQTableConstructionWithPrimitives() throws QException
{
QTableMetaData qTableMetaData = new QTableMetaData()
.withField(new QFieldMetaData(ItemWithPrimitives::getSku))
.withField(new QFieldMetaData(ItemWithPrimitives::getDescription))
.withField(new QFieldMetaData(ItemWithPrimitives::getQuantity));
assertEquals(QFieldType.STRING, qTableMetaData.getField("sku").getType());
assertEquals(QFieldType.INTEGER, qTableMetaData.getField("quantity").getType());
}
}

View File

@ -0,0 +1,149 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data.testentities;
import java.math.BigDecimal;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
** Sample of an entity that can be converted to & from a QRecord
*******************************************************************************/
public class Item extends QRecordEntity
{
private String sku;
private String description;
private Integer quantity;
private BigDecimal price;
private Boolean featured;
/*******************************************************************************
** Getter for sku
**
*******************************************************************************/
public String getSku()
{
return sku;
}
/*******************************************************************************
** Setter for sku
**
*******************************************************************************/
public void setSku(String sku)
{
this.sku = sku;
}
/*******************************************************************************
** Getter for description
**
*******************************************************************************/
public String getDescription()
{
return description;
}
/*******************************************************************************
** Setter for description
**
*******************************************************************************/
public void setDescription(String description)
{
this.description = description;
}
/*******************************************************************************
** Getter for quantity
**
*******************************************************************************/
public Integer getQuantity()
{
return quantity;
}
/*******************************************************************************
** Setter for quantity
**
*******************************************************************************/
public void setQuantity(Integer quantity)
{
this.quantity = quantity;
}
/*******************************************************************************
** Getter for price
**
*******************************************************************************/
public BigDecimal getPrice()
{
return price;
}
/*******************************************************************************
** Setter for price
**
*******************************************************************************/
public void setPrice(BigDecimal price)
{
this.price = price;
}
/*******************************************************************************
** Getter for featured
**
*******************************************************************************/
public Boolean getFeatured()
{
return featured;
}
/*******************************************************************************
** Setter for featured
**
*******************************************************************************/
public void setFeatured(Boolean featured)
{
this.featured = featured;
}
}

View File

@ -0,0 +1,149 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data.testentities;
import java.math.BigDecimal;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
** Sample of an entity that can be converted to & from a QRecord
*******************************************************************************/
public class ItemWithPrimitives extends QRecordEntity
{
private String sku;
private String description;
private int quantity;
private BigDecimal price;
private boolean featured;
/*******************************************************************************
** Getter for sku
**
*******************************************************************************/
public String getSku()
{
return sku;
}
/*******************************************************************************
** Setter for sku
**
*******************************************************************************/
public void setSku(String sku)
{
this.sku = sku;
}
/*******************************************************************************
** Getter for description
**
*******************************************************************************/
public String getDescription()
{
return description;
}
/*******************************************************************************
** Setter for description
**
*******************************************************************************/
public void setDescription(String description)
{
this.description = description;
}
/*******************************************************************************
** Getter for quantity
**
*******************************************************************************/
public int getQuantity()
{
return quantity;
}
/*******************************************************************************
** Setter for quantity
**
*******************************************************************************/
public void setQuantity(int quantity)
{
this.quantity = quantity;
}
/*******************************************************************************
** Getter for price
**
*******************************************************************************/
public BigDecimal getPrice()
{
return price;
}
/*******************************************************************************
** Setter for price
**
*******************************************************************************/
public void setPrice(BigDecimal price)
{
this.price = price;
}
/*******************************************************************************
** Getter for featured
**
*******************************************************************************/
public boolean getFeatured()
{
return featured;
}
/*******************************************************************************
** Setter for featured
**
*******************************************************************************/
public void setFeatured(boolean featured)
{
this.featured = featured;
}
}