Merged feature/meta-data-producing-annotations into dev

This commit is contained in:
2024-12-19 16:04:21 -06:00
26 changed files with 1432 additions and 28 deletions

View File

@ -70,6 +70,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
@ -780,14 +781,37 @@ public class QInstanceValidator
if(assertCondition(StringUtils.hasContent(association.getName()), "missing a name for an Association on table " + table.getName()))
{
String messageSuffix = " for Association " + association.getName() + " on table " + table.getName();
boolean recognizedTable = false;
if(assertCondition(StringUtils.hasContent(association.getAssociatedTableName()), "missing associatedTableName" + messageSuffix))
{
assertCondition(qInstance.getTable(association.getAssociatedTableName()) != null, "unrecognized associatedTableName " + association.getAssociatedTableName() + messageSuffix);
if(assertCondition(qInstance.getTable(association.getAssociatedTableName()) != null, "unrecognized associatedTableName " + association.getAssociatedTableName() + messageSuffix))
{
recognizedTable = true;
}
}
if(assertCondition(StringUtils.hasContent(association.getJoinName()), "missing joinName" + messageSuffix))
{
assertCondition(qInstance.getJoin(association.getJoinName()) != null, "unrecognized joinName " + association.getJoinName() + messageSuffix);
QJoinMetaData join = qInstance.getJoin(association.getJoinName());
if(assertCondition(join != null, "unrecognized joinName " + association.getJoinName() + messageSuffix))
{
assert join != null; // covered by the assertCondition
if(recognizedTable)
{
boolean isLeftToRight = join.getLeftTable().equals(table.getName()) && join.getRightTable().equals(association.getAssociatedTableName());
boolean isRightToLeft = join.getRightTable().equals(table.getName()) && join.getLeftTable().equals(association.getAssociatedTableName());
assertCondition(isLeftToRight || isRightToLeft, "join [" + association.getJoinName() + "] does not connect tables [" + table.getName() + "] and [" + association.getAssociatedTableName() + "]" + messageSuffix);
if(isLeftToRight)
{
assertCondition(join.getType().equals(JoinType.ONE_TO_MANY) || join.getType().equals(JoinType.ONE_TO_ONE), "Join type does not have 'one' on this table's side side (left)" + messageSuffix);
}
else if(isRightToLeft)
{
assertCondition(join.getType().equals(JoinType.MANY_TO_ONE) || join.getType().equals(JoinType.ONE_TO_ONE), "Join type does not have 'one' on this table's side (right)" + messageSuffix);
}
}
}
}
}
}

View File

@ -24,7 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata;
/*******************************************************************************
** Abstract class that knows how to produce meta data objects. Useful with
** MetaDataProducerHelper, to put point at a package full of these, and populate
** MetaDataProducerHelper, to point at a package full of these, and populate
** your whole QInstance.
*******************************************************************************/
public abstract class MetaDataProducer<T extends MetaDataProducerOutput> implements MetaDataProducerInterface<T>

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Comparator;
@ -31,10 +32,23 @@ import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.metadata.producers.ChildJoinFromRecordEntityGenericMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.PossibleValueSourceOfEnumGenericMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.PossibleValueSourceOfTableGenericMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingPossibleValueEnum;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -90,6 +104,9 @@ public class MetaDataProducerHelper
}
List<MetaDataProducerInterface<?>> producers = new ArrayList<>();
////////////////////////////////////////////////////////////////////////////////////////
// loop over classes, processing them based on either their type or their annotations //
////////////////////////////////////////////////////////////////////////////////////////
for(Class<?> aClass : classesInPackage)
{
try
@ -101,23 +118,27 @@ public class MetaDataProducerHelper
if(MetaDataProducerInterface.class.isAssignableFrom(aClass))
{
boolean foundValidConstructor = false;
for(Constructor<?> constructor : aClass.getConstructors())
{
if(constructor.getParameterCount() == 0)
{
Object o = constructor.newInstance();
producers.add((MetaDataProducerInterface<?>) o);
foundValidConstructor = true;
break;
}
}
CollectionUtils.addIfNotNull(producers, processMetaDataProducer(aClass));
}
if(!foundValidConstructor)
if(aClass.isAnnotationPresent(QMetaDataProducingEntity.class))
{
QMetaDataProducingEntity qMetaDataProducingEntity = aClass.getAnnotation(QMetaDataProducingEntity.class);
if(qMetaDataProducingEntity.producePossibleValueSource())
{
LOG.warn("Found a class which implements MetaDataProducerInterface, but it does not have a no-arg constructor, so it cannot be used.", logPair("class", aClass.getSimpleName()));
producers.addAll(processMetaDataProducingEntity(aClass));
}
}
if(aClass.isAnnotationPresent(QMetaDataProducingPossibleValueEnum.class))
{
QMetaDataProducingPossibleValueEnum qMetaDataProducingPossibleValueEnum = aClass.getAnnotation(QMetaDataProducingPossibleValueEnum.class);
if(qMetaDataProducingPossibleValueEnum.producePossibleValueSource())
{
CollectionUtils.addIfNotNull(producers, processMetaDataProducingPossibleValueEnum(aClass));
}
}
}
catch(Exception e)
{
@ -168,7 +189,176 @@ public class MetaDataProducerHelper
LOG.debug("Not using producer which is not enabled", logPair("producer", producer.getClass().getSimpleName()));
}
}
}
/***************************************************************************
**
***************************************************************************/
@SuppressWarnings("unchecked")
private static <T extends PossibleValueEnum<T>> MetaDataProducerInterface<?> processMetaDataProducingPossibleValueEnum(Class<?> aClass)
{
String warningPrefix = "Found a class annotated as @" + QMetaDataProducingPossibleValueEnum.class.getSimpleName();
if(!PossibleValueEnum.class.isAssignableFrom(aClass))
{
LOG.warn(warningPrefix + ", but which is not a " + PossibleValueEnum.class.getSimpleName() + ", so it will not be used.", logPair("class", aClass.getSimpleName()));
return null;
}
PossibleValueEnum<?>[] values = (PossibleValueEnum<?>[]) aClass.getEnumConstants();
return (new PossibleValueSourceOfEnumGenericMetaDataProducer<T>(aClass.getSimpleName(), (PossibleValueEnum<T>[]) values));
}
/***************************************************************************
**
***************************************************************************/
private static List<MetaDataProducerInterface<?>> processMetaDataProducingEntity(Class<?> aClass) throws Exception
{
List<MetaDataProducerInterface<?>> rs = new ArrayList<>();
String warningPrefix = "Found a class annotated as @" + QMetaDataProducingEntity.class.getSimpleName();
if(!QRecordEntity.class.isAssignableFrom(aClass))
{
LOG.warn(warningPrefix + ", but which is not a " + QRecordEntity.class.getSimpleName() + ", so it will not be used.", logPair("class", aClass.getSimpleName()));
return (rs);
}
Field tableNameField = aClass.getDeclaredField("TABLE_NAME");
if(!tableNameField.getType().equals(String.class))
{
LOG.warn(warningPrefix + ", but whose TABLE_NAME field is not a String, so it will not be used.", logPair("class", aClass.getSimpleName()));
return (rs);
}
String tableNameValue = (String) tableNameField.get(null);
rs.add(new PossibleValueSourceOfTableGenericMetaDataProducer(tableNameValue));
//////////////////////////
// process child tables //
//////////////////////////
QMetaDataProducingEntity qMetaDataProducingEntity = aClass.getAnnotation(QMetaDataProducingEntity.class);
for(ChildTable childTable : qMetaDataProducingEntity.childTables())
{
Class<? extends QRecordEntity> childEntityClass = childTable.childTableEntityClass();
if(childTable.childJoin().enabled())
{
CollectionUtils.addIfNotNull(rs, processChildJoin(aClass, childTable));
if(childTable.childRecordListWidget().enabled())
{
CollectionUtils.addIfNotNull(rs, processChildRecordListWidget(aClass, childTable));
}
}
else
{
if(childTable.childRecordListWidget().enabled())
{
//////////////////////////////////////////////////////////////////////////
// if not doing the join, can't do the child-widget, so warn about that //
//////////////////////////////////////////////////////////////////////////
LOG.warn(warningPrefix + " requested to produce a ChildRecordListWidget, but not produce a Join - which is not allowed (must do join to do widget). ", logPair("class", aClass.getSimpleName()), logPair("childEntityClass", childEntityClass.getSimpleName()));
}
}
}
return (rs);
}
/***************************************************************************
**
***************************************************************************/
private static MetaDataProducerInterface<?> processChildRecordListWidget(Class<?> aClass, ChildTable childTable) throws Exception
{
Class<? extends QRecordEntity> childEntityClass = childTable.childTableEntityClass();
String parentTableName = getTableNameStaticFieldValue(aClass);
String childTableName = getTableNameStaticFieldValue(childEntityClass);
ChildRecordListWidget childRecordListWidget = childTable.childRecordListWidget();
return (new ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, childRecordListWidget));
}
/***************************************************************************
**
***************************************************************************/
private static String findPossibleValueField(Class<? extends QRecordEntity> entityClass, String possibleValueSourceName)
{
for(Field field : entityClass.getDeclaredFields())
{
if(field.isAnnotationPresent(QField.class))
{
QField qField = field.getAnnotation(QField.class);
if(qField.possibleValueSourceName().equals(possibleValueSourceName))
{
return field.getName();
}
}
}
return (null);
}
/***************************************************************************
**
***************************************************************************/
private static MetaDataProducerInterface<?> processChildJoin(Class<?> aClass, ChildTable childTable) throws Exception
{
Class<? extends QRecordEntity> childEntityClass = childTable.childTableEntityClass();
String parentTableName = getTableNameStaticFieldValue(aClass);
String childTableName = getTableNameStaticFieldValue(childEntityClass);
String possibleValueFieldName = findPossibleValueField(childEntityClass, parentTableName);
if(!StringUtils.hasContent(possibleValueFieldName))
{
LOG.warn("Could not find field in [" + childEntityClass.getSimpleName() + "] with possibleValueSource referencing table [" + aClass.getSimpleName() + "]");
return (null);
}
return (new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName));
}
/***************************************************************************
**
***************************************************************************/
private static MetaDataProducerInterface<?> processMetaDataProducer(Class<?> aClass) throws Exception
{
for(Constructor<?> constructor : aClass.getConstructors())
{
if(constructor.getParameterCount() == 0)
{
Object o = constructor.newInstance();
return (MetaDataProducerInterface<?>) o;
}
}
LOG.warn("Found a class which implements MetaDataProducerInterface, but it does not have a no-arg constructor, so it cannot be used.", logPair("class", aClass.getSimpleName()));
return null;
}
/***************************************************************************
**
***************************************************************************/
private static String getTableNameStaticFieldValue(Class<?> aClass) throws NoSuchFieldException, IllegalAccessException
{
Field tableNameField = aClass.getDeclaredField("TABLE_NAME");
if(!tableNameField.getType().equals(String.class))
{
return (null);
}
String tableNameValue = (String) tableNameField.get(null);
return (tableNameValue);
}
}

View File

@ -27,12 +27,12 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
** Interface for classes that know how to produce meta data objects. Useful with
** MetaDataProducerHelper, to put point at a package full of these, and populate
** MetaDataProducerHelper, to point at a package full of these, and populate
** your whole QInstance.
**
** See also MetaDataProducer - an implementer of this interface, which actually
** came first, and is fine to extend if producing a meta-data class is all your
** clas means to do (nice and "Single-responsibility principle").
** class means to do (nice and "Single-responsibility principle").
**
** But, in some applications you may want to, for example, have one class that
** defines a process step, and also produces the meta-data for that process, so

View File

@ -22,6 +22,9 @@
package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
import java.util.Objects;
/*******************************************************************************
** An actual possible value - an id and label.
**
@ -76,4 +79,37 @@ public class QPossibleValue<T>
{
return label;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean equals(Object o)
{
if(this == o)
{
return true;
}
if(o == null || getClass() != o.getClass())
{
return false;
}
QPossibleValue<?> that = (QPossibleValue<?>) o;
return Objects.equals(id, that.id) && Objects.equals(label, that.label);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public int hashCode()
{
return Objects.hash(id, label);
}
}

View File

@ -23,7 +23,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
@ -97,7 +100,7 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
** Create a new possible value source, for an enum, with default settings.
** e.g., type=ENUM; name from param values from the param; LABEL_ONLY format
*******************************************************************************/
public static <T extends PossibleValueEnum<?>> QPossibleValueSource newForEnum(String name, T[] values)
public static <I, T extends PossibleValueEnum<I>> QPossibleValueSource newForEnum(String name, T[] values)
{
return new QPossibleValueSource()
.withName(name)
@ -553,11 +556,25 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
** myPossibleValueSource.withValuesFromEnum(MyEnum.values()));
**
*******************************************************************************/
public <T extends PossibleValueEnum<?>> QPossibleValueSource withValuesFromEnum(T[] values)
public <I, T extends PossibleValueEnum<I>> QPossibleValueSource withValuesFromEnum(T[] values)
{
Set<I> usedIds = new HashSet<>();
List<I> duplicatedIds = new ArrayList<>();
for(T t : values)
{
if(usedIds.contains(t.getPossibleValueId()))
{
duplicatedIds.add(t.getPossibleValueId());
}
addEnumValue(new QPossibleValue<>(t.getPossibleValueId(), t.getPossibleValueLabel()));
usedIds.add(t.getPossibleValueId());
}
if(!duplicatedIds.isEmpty())
{
throw (new QRuntimeException("Error: Duplicated id(s) found in enum values: " + duplicatedIds));
}
return (this);

View File

@ -0,0 +1,105 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Generic meta-data-producer, which should be instantiated (e.g., by
** MetaDataProducer Helper), to produce a QJoinMetaData, based on a
** QRecordEntity and a ChildTable sub-annotation.
**
** e.g., Orders & LineItems - on the Order entity
** <code>
@QMetaDataProducingEntity(
childTables = { @ChildTable(
childTableEntityClass = LineItem.class,
childJoin = @ChildJoin(enabled = true),
childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Order Lines"))
}
)
public class Order extends QRecordEntity
** </code>
**
** A Join will be made:
** - left: Order
** - right: LineItem
** - type: ONE_TO_MANY (one order (parent table) has mny lines (child table))
** - joinOn: order's primary key, lineItem's orderId field
** - name: inferred, based on the table names orderJoinLineItem)
*******************************************************************************/
public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDataProducerInterface<QJoinMetaData>
{
private String childTableName; // e.g., lineItem
private String parentTableName; // e.g., order
private String foreignKeyFieldName; // e.g., orderId
/***************************************************************************
**
***************************************************************************/
public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName)
{
Objects.requireNonNull(childTableName, "childTableName cannot be null");
Objects.requireNonNull(parentTableName, "parentTableName cannot be null");
Objects.requireNonNull(foreignKeyFieldName, "foreignKeyFieldName cannot be null");
this.childTableName = childTableName;
this.parentTableName = parentTableName;
this.foreignKeyFieldName = foreignKeyFieldName;
}
/***************************************************************************
**
***************************************************************************/
@Override
public QJoinMetaData produce(QInstance qInstance) throws QException
{
QTableMetaData possibleValueTable = qInstance.getTable(parentTableName);
if(possibleValueTable == null)
{
throw (new QException("Could not find tableMetaData " + parentTableName));
}
QJoinMetaData join = new QJoinMetaData()
.withLeftTable(parentTableName)
.withRightTable(childTableName)
.withInferredName()
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn(possibleValueTable.getPrimaryKeyField(), foreignKeyFieldName));
return (join);
}
}

View File

@ -0,0 +1,100 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Generic meta-data-producer, which should be instantiated (e.g., by
** MetaDataProducer Helper), to produce a ChildRecordList QWidgetMetaData, to
** produce a QJoinMetaData, based on a QRecordEntity and a ChildTable sub-annotation.
**
** e.g., Orders & LineItems - on the Order entity
** <code>
@QMetaDataProducingEntity( childTables = { @ChildTable(
childTableEntityClass = LineItem.class,
childJoin = @ChildJoin(enabled = true),
childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Order Lines"))
})
public class Order extends QRecordEntity
** </code>
**
*******************************************************************************/
public class ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer implements MetaDataProducerInterface<QWidgetMetaData>
{
private String childTableName; // e.g., lineItem
private String parentTableName; // e.g., order
private ChildRecordListWidget childRecordListWidget;
/***************************************************************************
**
***************************************************************************/
public ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, ChildRecordListWidget childRecordListWidget)
{
this.childTableName = childTableName;
this.parentTableName = parentTableName;
this.childRecordListWidget = childRecordListWidget;
}
/***************************************************************************
**
***************************************************************************/
@Override
public QWidgetMetaData produce(QInstance qInstance) throws QException
{
String name = QJoinMetaData.makeInferredJoinName(parentTableName, childTableName);
QJoinMetaData join = qInstance.getJoin(name);
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(join)
.withName(name)
.withLabel(childRecordListWidget.label())
.withCanAddChildRecord(childRecordListWidget.canAddChildRecords())
.getWidgetMetaData();
if(StringUtils.hasContent(childRecordListWidget.manageAssociationName()))
{
widget.withDefaultValue("manageAssociationName", childRecordListWidget.manageAssociationName());
}
if(childRecordListWidget.maxRows() > 0)
{
widget.withDefaultValue("maxRows", childRecordListWidget.maxRows());
}
return (widget);
}
}

View File

@ -0,0 +1,64 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
/***************************************************************************
** Generic meta-data-producer, which should be instantiated (e.g., by
** MetaDataProducer Helper), to produce a QPossibleValueSource meta-data
** based on a PossibleValueEnum
**
***************************************************************************/
public class PossibleValueSourceOfEnumGenericMetaDataProducer<T extends PossibleValueEnum<T>> implements MetaDataProducerInterface<QPossibleValueSource>
{
private final String name;
private final PossibleValueEnum<T>[] values;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public PossibleValueSourceOfEnumGenericMetaDataProducer(String name, PossibleValueEnum<T>[] values)
{
this.name = name;
this.values = values;
}
/***************************************************************************
**
***************************************************************************/
@Override
public QPossibleValueSource produce(QInstance qInstance)
{
return (QPossibleValueSource.newForEnum(name, values));
}
}

View File

@ -0,0 +1,61 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
/***************************************************************************
** Generic meta-data-producer, which should be instantiated (e.g., by
** MetaDataProducer Helper), to produce a QPossibleValueSource meta-data
** based on a QRecordEntity class (which has corresponding QTableMetaData).
**
***************************************************************************/
public class PossibleValueSourceOfTableGenericMetaDataProducer implements MetaDataProducerInterface<QPossibleValueSource>
{
private final String tableName;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public PossibleValueSourceOfTableGenericMetaDataProducer(String tableName)
{
this.tableName = tableName;
}
/***************************************************************************
**
***************************************************************************/
@Override
public QPossibleValueSource produce(QInstance qInstance)
{
return (QPossibleValueSource.newForTable(tableName));
}
}

View File

@ -0,0 +1,38 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/***************************************************************************
** value that goes inside a QMetadataProducingEntity annotation, to control
** the generation of a QJoinMetaData
***************************************************************************/
@Retention(RetentionPolicy.RUNTIME)
@SuppressWarnings("checkstyle:MissingJavadocMethod")
public @interface ChildJoin
{
boolean enabled();
}

View File

@ -0,0 +1,46 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/***************************************************************************
** value that goes inside a QMetadataProducingEntity annotation, to control
** the generation of a QWidgetMetaData - for a ChildRecordList widget.
***************************************************************************/
@Retention(RetentionPolicy.RUNTIME)
@SuppressWarnings("checkstyle:MissingJavadocMethod")
public @interface ChildRecordListWidget
{
boolean enabled();
String label() default "";
int maxRows() default 20;
boolean canAddChildRecords() default false;
String manageAssociationName() default "";
}

View File

@ -0,0 +1,45 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/***************************************************************************
** value that goes inside a QMetadataProducingEntity annotation, to define
** child-tables, e.g., for producing joins and childRecordList widgets
***************************************************************************/
@Retention(RetentionPolicy.RUNTIME)
@SuppressWarnings("checkstyle:MissingJavadocMethod")
public @interface ChildTable
{
Class<? extends QRecordEntity> childTableEntityClass();
String joinFieldName() default "";
ChildJoin childJoin() default @ChildJoin(enabled = false);
ChildRecordListWidget childRecordListWidget() default @ChildRecordListWidget(enabled = false);
}

View File

@ -0,0 +1,48 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*******************************************************************************
** annotation to go on a QRecordEntity class, which you would like to be
** processed by MetaDataProducerHelper, to automatically produce some meta-data
** objects. Specifically supports:
**
** - Making a possible-value-source out of the table.
** - Processing child tables to create joins and childRecordList widgets
*******************************************************************************/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@SuppressWarnings("checkstyle:MissingJavadocMethod")
public @interface QMetaDataProducingEntity
{
boolean producePossibleValueSource() default true;
ChildTable[] childTables() default { };
}

View File

@ -0,0 +1,42 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*******************************************************************************
** annotation to go on a PossibleValueEnum class, which you would like to be
** processed by MetaDataProducerHelper, to automatically produce possible-value-
** source meta-data based on the enum.
*******************************************************************************/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@SuppressWarnings("checkstyle:MissingJavadocMethod")
public @interface QMetaDataProducingPossibleValueEnum
{
boolean producePossibleValueSource() default true;
}

View File

@ -1329,6 +1329,21 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
/*******************************************************************************
** Getter for an association by name
*******************************************************************************/
public Optional<Association> getAssociationByName(String name)
{
if(associations == null)
{
return (Optional.empty());
}
return (getAssociations().stream().filter(a -> a.getName().equals(name)).findFirst());
}
/*******************************************************************************
** Setter for associations
*******************************************************************************/

View File

@ -700,4 +700,16 @@ public class CollectionUtils
return (map.containsKey(key) && map.get(key) != null);
}
/***************************************************************************
** add an element to a collection, but, only if the element isn't null
***************************************************************************/
public static <T, E extends T> void addIfNotNull(Collection<T> c, E element)
{
if(element != null)
{
c.add(element);
}
}
}

View File

@ -460,4 +460,19 @@ public class StringUtils
return (Pattern.matches("[a-f0-9]{8}(?:-[a-f0-9]{4}){4}[a-f0-9]{8}", s));
}
/***************************************************************************
**
***************************************************************************/
public static String emptyToNull(String s)
{
if(!hasContent(s))
{
return (null);
}
return (s);
}
}

View File

@ -2054,16 +2054,41 @@ public class QInstanceValidatorTest extends BaseTest
assertValidationFailureReasons((qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_ORDER).withAssociation(new Association().withName("myAssociation"))),
"missing joinName for Association myAssociation on table " + TestUtils.TABLE_NAME_ORDER,
"missing associatedTableName for Association myAssociation on table " + TestUtils.TABLE_NAME_ORDER
);
"missing associatedTableName for Association myAssociation on table " + TestUtils.TABLE_NAME_ORDER);
assertValidationFailureReasons((qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_ORDER).withAssociation(new Association().withName("myAssociation").withJoinName("notAJoin").withAssociatedTableName(TestUtils.TABLE_NAME_LINE_ITEM))),
"unrecognized joinName notAJoin for Association myAssociation on table " + TestUtils.TABLE_NAME_ORDER
);
"unrecognized joinName notAJoin for Association myAssociation on table " + TestUtils.TABLE_NAME_ORDER);
assertValidationFailureReasons((qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_ORDER).withAssociation(new Association().withName("myAssociation").withJoinName("orderLineItem").withAssociatedTableName("notATable"))),
"unrecognized associatedTableName notATable for Association myAssociation on table " + TestUtils.TABLE_NAME_ORDER
);
"unrecognized associatedTableName notATable for Association myAssociation on table " + TestUtils.TABLE_NAME_ORDER);
//////////////////////////////////
// wrong join on an association //
//////////////////////////////////
assertValidationFailureReasons((qInstance ->
{
Association association = qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getAssociationByName("orderLine").orElseThrow();
association.setJoinName("orderOrderExtrinsic");
}),
"join [orderOrderExtrinsic] does not connect tables [order] and [orderLine]");
//////////////////////////////////////////
// wrong table (doesn't match the join) //
//////////////////////////////////////////
assertValidationFailureReasons((qInstance ->
{
Association association = qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getAssociationByName("orderLine").orElseThrow();
association.setAssociatedTableName(TestUtils.TABLE_NAME_ORDER_EXTRINSIC);
}),
"join [orderLineItem] does not connect tables [order] and [orderExtrinsic]");
//////////////////////////////
// invalid type on the join //
//////////////////////////////
assertValidationFailureReasons((qInstance -> qInstance.getJoin("orderLineItem").setType(JoinType.MANY_TO_MANY)),
"Join type does not have 'one' on this table's side side (left)");
assertValidationFailureReasons((qInstance -> qInstance.getJoin("orderLineItem").setType(JoinType.MANY_TO_ONE)),
"Join type does not have 'one' on this table's side side (left)");
}
@ -2323,7 +2348,7 @@ public class QInstanceValidatorTest extends BaseTest
{
int noOfReasons = actualReasons == null ? 0 : actualReasons.size();
assertEquals(expectedReasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", expectedReasons)
+ "\nActual reasons: " + (noOfReasons > 0 ? String.join("\n", actualReasons) : "--"));
+ "\nActual reasons: " + (noOfReasons > 0 ? String.join("\n", actualReasons) : "--"));
}
for(String reason : expectedReasons)
@ -2451,6 +2476,7 @@ public class QInstanceValidatorTest extends BaseTest
public static class ValidAuthCustomizer implements QAuthenticationModuleCustomizerInterface {}
/***************************************************************************
**
***************************************************************************/
@ -2468,6 +2494,7 @@ public class QInstanceValidatorTest extends BaseTest
}
/***************************************************************************
**
***************************************************************************/

View File

@ -23,14 +23,26 @@ package com.kingsrook.qqq.backend.core.model.metadata;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.producers.TestAbstractMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.TestDisabledMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.TestImplementsMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.TestMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.producers.TestMetaDataProducingChildEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.TestMetaDataProducingEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.TestMetaDataProducingPossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.metadata.producers.TestNoInterfacesExtendsObject;
import com.kingsrook.qqq.backend.core.model.metadata.producers.TestNoValidConstructorMetaDataProducer;
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;
@ -54,6 +66,48 @@ class MetaDataProducerHelperTest
assertFalse(qInstance.getTables().containsKey(TestNoInterfacesExtendsObject.NAME));
assertFalse(qInstance.getTables().containsKey(TestAbstractMetaDataProducer.NAME));
assertFalse(qInstance.getTables().containsKey(TestDisabledMetaDataProducer.NAME));
/////////////////////////////////////////////
// annotation on PVS enum -> PVS meta data //
/////////////////////////////////////////////
assertTrue(qInstance.getPossibleValueSources().containsKey(TestMetaDataProducingPossibleValueEnum.class.getSimpleName()));
QPossibleValueSource enumPVS = qInstance.getPossibleValueSource(TestMetaDataProducingPossibleValueEnum.class.getSimpleName());
assertEquals(QPossibleValueSourceType.ENUM, enumPVS.getType());
assertEquals(2, enumPVS.getEnumValues().size());
assertEquals(new QPossibleValue<>(1, "One"), enumPVS.getEnumValues().get(0));
//////////////////////////////////////////////
// annotation on PVS table -> PVS meta data //
//////////////////////////////////////////////
assertTrue(qInstance.getPossibleValueSources().containsKey(TestMetaDataProducingEntity.TABLE_NAME));
QPossibleValueSource tablePVS = qInstance.getPossibleValueSource(TestMetaDataProducingEntity.TABLE_NAME);
assertEquals(QPossibleValueSourceType.TABLE, tablePVS.getType());
assertEquals(TestMetaDataProducingEntity.TABLE_NAME, tablePVS.getTableName());
//////////////////////////////////////////////////////////////////
// annotation on parent table w/ joined child -> join meta data //
//////////////////////////////////////////////////////////////////
String joinName = QJoinMetaData.makeInferredJoinName(TestMetaDataProducingEntity.TABLE_NAME, TestMetaDataProducingChildEntity.TABLE_NAME);
assertTrue(qInstance.getJoins().containsKey(joinName));
QJoinMetaData join = qInstance.getJoin(joinName);
assertEquals(TestMetaDataProducingEntity.TABLE_NAME, join.getLeftTable());
assertEquals(TestMetaDataProducingChildEntity.TABLE_NAME, join.getRightTable());
assertEquals(JoinType.ONE_TO_MANY, join.getType());
assertEquals("id", join.getJoinOns().get(0).getLeftField());
assertEquals("parentId", join.getJoinOns().get(0).getRightField());
//////////////////////////////////////////////////////////////////////////////////////
// annotation on parent table w/ joined child -> child record list widget meta data //
//////////////////////////////////////////////////////////////////////////////////////
assertTrue(qInstance.getWidgets().containsKey(joinName));
QWidgetMetaDataInterface widget = qInstance.getWidget(joinName);
assertEquals(WidgetType.CHILD_RECORD_LIST.getType(), widget.getType());
assertEquals("Test Children", widget.getLabel());
assertEquals(joinName, widget.getDefaultValues().get("joinName"));
assertEquals(false, widget.getDefaultValues().get("canAddChildRecord"));
assertNull(widget.getDefaultValues().get("manageAssociationName"));
assertEquals(15, widget.getDefaultValues().get("maxRows"));
}
}

View File

@ -0,0 +1,96 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/*******************************************************************************
** Unit test for QPossibleValueSource
*******************************************************************************/
class QPossibleValueSourceTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testWithValuesFromEnum()
{
assertThatThrownBy(() -> new QPossibleValueSource().withValuesFromEnum(DupeIds.values()))
.isInstanceOf(QRuntimeException.class)
.hasMessageContaining("Duplicated id(s)")
.hasMessageMatching(".*: \\[1]$");
}
/***************************************************************************
**
***************************************************************************/
private enum DupeIds implements PossibleValueEnum<Integer>
{
ONE_A(1, "A"),
TWO_B(2, "B"),
ONE_C(1, "C");
private final int id;
private final String label;
/***************************************************************************
**
***************************************************************************/
DupeIds(int id, String label)
{
this.id = id;
this.label = label;
}
/***************************************************************************
**
***************************************************************************/
@Override
public Integer getPossibleValueId()
{
return id;
}
/***************************************************************************
**
***************************************************************************/
@Override
public String getPossibleValueLabel()
{
return label;
}
}
}

View File

@ -0,0 +1,140 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** QRecord Entity for TestMetaDataProducingEntity table
*******************************************************************************/
public class TestMetaDataProducingChildEntity extends QRecordEntity implements MetaDataProducerInterface<QTableMetaData>
{
public static final String TABLE_NAME = "testMetaDataProducingChildEntity";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
@QField(possibleValueSourceName = TestMetaDataProducingEntity.TABLE_NAME)
private Integer parentId;
/***************************************************************************
**
***************************************************************************/
@Override
public QTableMetaData produce(QInstance qInstance) throws QException
{
return new QTableMetaData()
.withName(TABLE_NAME)
.withFieldsFromEntity(TestMetaDataProducingChildEntity.class);
}
/*******************************************************************************
** Default constructor
*******************************************************************************/
public TestMetaDataProducingChildEntity()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public TestMetaDataProducingChildEntity(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public TestMetaDataProducingChildEntity withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for parentId
*******************************************************************************/
public Integer getParentId()
{
return (this.parentId);
}
/*******************************************************************************
** Setter for parentId
*******************************************************************************/
public void setParentId(Integer parentId)
{
this.parentId = parentId;
}
/*******************************************************************************
** Fluent setter for parentId
*******************************************************************************/
public TestMetaDataProducingChildEntity withParentId(Integer parentId)
{
this.parentId = parentId;
return (this);
}
}

View File

@ -0,0 +1,119 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildJoin;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** QRecord Entity for TestMetaDataProducingEntity table
*******************************************************************************/
@QMetaDataProducingEntity(producePossibleValueSource = true,
childTables =
{
@ChildTable(childTableEntityClass = TestMetaDataProducingChildEntity.class,
childJoin = @ChildJoin(enabled = true),
childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Test Children", maxRows = 15))
}
)
public class TestMetaDataProducingEntity extends QRecordEntity implements MetaDataProducerInterface<QTableMetaData>
{
public static final String TABLE_NAME = "testMetaDataProducingEntity";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
/***************************************************************************
**
***************************************************************************/
@Override
public QTableMetaData produce(QInstance qInstance) throws QException
{
return new QTableMetaData()
.withName(TABLE_NAME)
.withFieldsFromEntity(TestMetaDataProducingEntity.class);
}
/*******************************************************************************
** Default constructor
*******************************************************************************/
public TestMetaDataProducingEntity()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public TestMetaDataProducingEntity(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public TestMetaDataProducingEntity withId(Integer id)
{
this.id = id;
return (this);
}
}

View File

@ -0,0 +1,74 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.producers;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingPossibleValueEnum;
/*******************************************************************************
**
*******************************************************************************/
@QMetaDataProducingPossibleValueEnum(producePossibleValueSource = true)
public enum TestMetaDataProducingPossibleValueEnum implements PossibleValueEnum<Integer>
{
ONE(1, "One"),
TWO(2, "Two");
private final int id;
private final String label;
/***************************************************************************
**
***************************************************************************/
TestMetaDataProducingPossibleValueEnum(int id, String label)
{
this.id = id;
this.label = label;
}
/***************************************************************************
**
***************************************************************************/
@Override
public String getPossibleValueLabel()
{
return label;
}
/***************************************************************************
**
***************************************************************************/
@Override
public Integer getPossibleValueId()
{
return id;
}
}

View File

@ -26,10 +26,12 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import com.google.gson.reflect.TypeToken;
@ -618,4 +620,23 @@ class CollectionUtilsTest extends BaseTest
4, Map.of("B", "B4")), output);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAddIfNotNull()
{
HashSet<String> s = new HashSet<>();
CollectionUtils.addIfNotNull(s, null);
assertEquals(Set.of(), s);
CollectionUtils.addIfNotNull(s, "");
assertEquals(Set.of(""), s);
CollectionUtils.addIfNotNull(s, "1");
assertEquals(Set.of("", "1"), s);
}
}

View File

@ -318,4 +318,19 @@ class StringUtilsTest extends BaseTest
assertEquals("Apples were eaten", StringUtils.pluralFormat(2, "Apple{,s} {was,were} eaten"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testEmptyToNull()
{
assertNull(StringUtils.emptyToNull(null));
assertNull(StringUtils.emptyToNull(""));
assertNull(StringUtils.emptyToNull(" "));
assertNull(StringUtils.emptyToNull(" "));
assertEquals("a", StringUtils.emptyToNull("a"));
}
}