Merged feature/dk-misc-20250318 into dev

This commit is contained in:
2025-03-27 12:04:21 -05:00
39 changed files with 1016 additions and 203 deletions

View File

@ -649,7 +649,7 @@ public class PollingAutomationPerTableRunner implements Runnable
input.setRecordList(records); input.setRecordList(records);
input.setAction(action); input.setAction(action);
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getRecordAutomationHandler(action); RecordAutomationHandler recordAutomationHandler = QCodeLoader.getAdHoc(RecordAutomationHandler.class, action.getCodeReference());
recordAutomationHandler.execute(input); recordAutomationHandler.execute(input);
} }
} }

View File

@ -24,17 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction; import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization; import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -43,9 +37,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/******************************************************************************* /*******************************************************************************
** Utility to load code for running QQQ customizers. ** Utility to load code for running QQQ customizers.
** **
** TODO - redo all to go through method that memoizes class & constructor ** That memoization causes 1,000,000 such calls to go from ~500ms to ~100ms.
** lookup. That memoziation causes 1,000,000 such calls to go from ~500ms
** to ~100ms.
*******************************************************************************/ *******************************************************************************/
public class QCodeLoader public class QCodeLoader
{ {
@ -70,84 +62,6 @@ public class QCodeLoader
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static <T, R> Function<T, R> getFunction(QCodeReference codeReference)
{
if(codeReference == null)
{
return (null);
}
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA customizers are supported at this time."));
}
try
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return ((Function<T, R>) customizerClass.getConstructor().newInstance());
}
catch(Exception e)
{
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
// if it finds an invalid code reference //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
return (null);
}
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static <T extends BackendStep> T getBackendStep(Class<T> expectedType, QCodeReference codeReference)
{
if(codeReference == null)
{
return (null);
}
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA BackendSteps are supported at this time."));
}
try
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return ((T) customizerClass.getConstructor().newInstance());
}
catch(Exception e)
{
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
// if it finds an invalid code reference //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
return (null);
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -177,7 +91,17 @@ public class QCodeLoader
if(constructor.isPresent()) if(constructor.isPresent())
{ {
return ((T) constructor.get().newInstance()); T t = (T) constructor.get().newInstance();
////////////////////////////////////////////////////////////////
// if the object is initializable, then, well, initialize it! //
////////////////////////////////////////////////////////////////
if(t instanceof InitializableViaCodeReference initializableViaCodeReference)
{
initializableViaCodeReference.initialize(codeReference);
}
return t;
} }
else else
{ {
@ -187,7 +111,7 @@ public class QCodeLoader
} }
catch(Exception e) catch(Exception e)
{ {
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference)); LOG.error("Error initializing codeReference", e, logPair("codeReference", codeReference));
////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here // // return null here - under the assumption that during normal run-time operations, we'll never hit here //
@ -198,67 +122,4 @@ public class QCodeLoader
} }
} }
/*******************************************************************************
**
*******************************************************************************/
public static RecordAutomationHandler getRecordAutomationHandler(TableAutomationAction action) throws QException
{
try
{
QCodeReference codeReference = action.getCodeReference();
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA customizers are supported at this time."));
}
Class<?> codeClass = Class.forName(codeReference.getName());
Object codeObject = codeClass.getConstructor().newInstance();
if(!(codeObject instanceof RecordAutomationHandler recordAutomationHandler))
{
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of RecordAutomationHandler"));
}
return (recordAutomationHandler);
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error getting record automation handler for action [" + action.getName() + "]", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
public static QCustomPossibleValueProvider getCustomPossibleValueProvider(QPossibleValueSource possibleValueSource) throws QException
{
try
{
Class<?> codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName());
Object codeObject = codeClass.getConstructor().newInstance();
if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider))
{
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider"));
}
return (customPossibleValueProvider);
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error getting custom possible value provider for PVS [" + possibleValueSource.getName() + "]", e));
}
}
} }

View File

@ -290,7 +290,18 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@Deprecated(since = "call one that doesn't take input param")
public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException
{
return linkRecordEdit(tableName, recordId);
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkRecordEdit(String tableName, Serializable recordId) throws QException
{ {
String tablePath = QContext.getQInstance().getTablePath(tableName); String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + recordId + "/edit"); return (tablePath + "/" + recordId + "/edit");
@ -317,7 +328,17 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@Deprecated(since = "call one that doesn't take input param")
public static String linkProcessForFilter(AbstractActionInput input, String processName, QQueryFilter filter) throws QException public static String linkProcessForFilter(AbstractActionInput input, String processName, QQueryFilter filter) throws QException
{
return linkProcessForFilter(processName, filter);
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkProcessForFilter(String processName, QQueryFilter filter) throws QException
{ {
QProcessMetaData process = QContext.getQInstance().getProcess(processName); QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process == null) if(process == null)
@ -337,10 +358,21 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@Deprecated(since = "call one that doesn't take input param")
public static String linkProcessForRecord(AbstractActionInput input, String processName, Serializable recordId) throws QException public static String linkProcessForRecord(AbstractActionInput input, String processName, Serializable recordId) throws QException
{
return linkProcessForRecord(processName, recordId);
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkProcessForRecord(String processName, Serializable recordId) throws QException
{ {
QProcessMetaData process = QContext.getQInstance().getProcess(processName); QProcessMetaData process = QContext.getQInstance().getProcess(processName);
String tableName = process.getTableName(); String tableName = process.getTableName();

View File

@ -54,6 +54,14 @@ public interface ExportStreamerInterface
// noop in base class // noop in base class
} }
/***************************************************************************
**
***************************************************************************/
default void setExportStyleCustomizer(ExportStyleCustomizerInterface exportStyleCustomizer)
{
// noop in base class
}
/******************************************************************************* /*******************************************************************************
** Called once per sheet, before any rows are available. Meant to write a ** Called once per sheet, before any rows are available. Meant to write a
** header, for example. ** header, for example.

View File

@ -0,0 +1,35 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting;
/*******************************************************************************
** interface for classes that can be used to customize visual style aspects of
** exports/reports.
**
** Anticipates very different sub-interfaces based on the file type being generated,
** and the capabilities of each. e.g., excel (bolds, fonts, cell merging) vs
** json (different structure of objects).
*******************************************************************************/
public interface ExportStyleCustomizerInterface
{
}

View File

@ -163,6 +163,17 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
reportStreamer = reportFormat.newReportStreamer(); reportStreamer = reportFormat.newReportStreamer();
} }
if(reportInput.getExportStyleCustomizer() != null)
{
ExportStyleCustomizerInterface styleCustomizer = QCodeLoader.getAdHoc(ExportStyleCustomizerInterface.class, reportInput.getExportStyleCustomizer());
reportStreamer.setExportStyleCustomizer(styleCustomizer);
}
else if(report.getExportStyleCustomizer() != null)
{
ExportStyleCustomizerInterface styleCustomizer = QCodeLoader.getAdHoc(ExportStyleCustomizerInterface.class, report.getExportStyleCustomizer());
reportStreamer.setExportStyleCustomizer(styleCustomizer);
}
reportStreamer.preRun(reportInput.getReportDestination(), views); reportStreamer.preRun(reportInput.getReportDestination(), views);
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
@ -211,7 +222,8 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
if(dataSourceTableView.getViewCustomizer() != null) if(dataSourceTableView.getViewCustomizer() != null)
{ {
Function<QReportView, QReportView> viewCustomizerFunction = QCodeLoader.getFunction(dataSourceTableView.getViewCustomizer()); @SuppressWarnings("unchecked")
Function<QReportView, QReportView> viewCustomizerFunction = QCodeLoader.getAdHoc(Function.class, dataSourceTableView.getViewCustomizer());
if(viewCustomizerFunction instanceof ReportViewCustomizer reportViewCustomizer) if(viewCustomizerFunction instanceof ReportViewCustomizer reportViewCustomizer)
{ {
reportViewCustomizer.setReportInput(reportInput); reportViewCustomizer.setReportInput(reportInput);
@ -660,7 +672,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
//////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if any fields are 'showPossibleValueLabel', then move display values for them into the record's values map // // if any fields are 'showPossibleValueLabel', then move display values for them into the record's values map //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QReportField column : tableView.getColumns()) for(QReportField column : CollectionUtils.nonNullList(tableView.getColumns()))
{ {
if(column.getShowPossibleValueLabel()) if(column.getShowPossibleValueLabel())
{ {

View File

@ -46,6 +46,7 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface; import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStyleCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.ReportUtils; import com.kingsrook.qqq.backend.core.actions.reporting.ReportUtils;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException; import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
@ -77,6 +78,7 @@ import org.apache.poi.xssf.usermodel.XSSFPivotTable;
import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/******************************************************************************* /*******************************************************************************
@ -112,7 +114,8 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
public static final String EXCEL_DATE_FORMAT = "yyyy-MM-dd"; public static final String EXCEL_DATE_FORMAT = "yyyy-MM-dd";
public static final String EXCEL_DATE_TIME_FORMAT = "yyyy-MM-dd H:mm:ss"; public static final String EXCEL_DATE_TIME_FORMAT = "yyyy-MM-dd H:mm:ss";
private PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface(); private ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface;
private Map<String, String> excelCellFormats; private Map<String, String> excelCellFormats;
private Map<String, XSSFCellStyle> styles = new HashMap<>(); private Map<String, XSSFCellStyle> styles = new HashMap<>();
@ -402,6 +405,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
dateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT)); dateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT));
styles.put("datetime", dateTimeStyle); styles.put("datetime", dateTimeStyle);
PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface();
styles.put("title", poiExcelStylerInterface.createStyleForTitle(workbook, createHelper)); styles.put("title", poiExcelStylerInterface.createStyleForTitle(workbook, createHelper));
styles.put("header", poiExcelStylerInterface.createStyleForHeader(workbook, createHelper)); styles.put("header", poiExcelStylerInterface.createStyleForHeader(workbook, createHelper));
styles.put("footer", poiExcelStylerInterface.createStyleForFooter(workbook, createHelper)); styles.put("footer", poiExcelStylerInterface.createStyleForFooter(workbook, createHelper));
@ -413,6 +417,11 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
XSSFCellStyle footerDateTimeStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper); XSSFCellStyle footerDateTimeStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper);
footerDateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT)); footerDateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT));
styles.put("footer-datetime", footerDateTimeStyle); styles.put("footer-datetime", footerDateTimeStyle);
if(styleCustomizerInterface != null)
{
styleCustomizerInterface.customizeStyles(styles, workbook, createHelper);
}
} }
@ -458,7 +467,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
} }
else else
{ {
sheetWriter.beginSheet(); sheetWriter.beginSheet(view, styleCustomizerInterface);
//////////////////////////////////////////////// ////////////////////////////////////////////////
// put the title and header rows in the sheet // // put the title and header rows in the sheet //
@ -560,6 +569,16 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
/***************************************************************************
**
***************************************************************************/
public static void setStyleForField(QRecord record, String fieldName, String styleName)
{
record.setDisplayValue(fieldName + ":excelStyle", styleName);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -567,12 +586,12 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
{ {
sheetWriter.insertRow(rowNo++); sheetWriter.insertRow(rowNo++);
int styleIndex = -1; int baseStyleIndex = -1;
int dateStyleIndex = styles.get("date").getIndex(); int dateStyleIndex = styles.get("date").getIndex();
int dateTimeStyleIndex = styles.get("datetime").getIndex(); int dateTimeStyleIndex = styles.get("datetime").getIndex();
if(isFooter) if(isFooter)
{ {
styleIndex = styles.get("footer").getIndex(); baseStyleIndex = styles.get("footer").getIndex();
dateStyleIndex = styles.get("footer-date").getIndex(); dateStyleIndex = styles.get("footer-date").getIndex();
dateTimeStyleIndex = styles.get("footer-datetime").getIndex(); dateTimeStyleIndex = styles.get("footer-datetime").getIndex();
} }
@ -582,6 +601,13 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
{ {
Serializable value = qRecord.getValue(field.getName()); Serializable value = qRecord.getValue(field.getName());
String overrideStyleName = qRecord.getDisplayValue(field.getName() + ":excelStyle");
int styleIndex = baseStyleIndex;
if(overrideStyleName != null)
{
styleIndex = styles.get(overrideStyleName).getIndex();
}
if(value != null) if(value != null)
{ {
if(value instanceof String s) if(value instanceof String s)
@ -706,7 +732,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
{ {
if(!ReportType.PIVOT.equals(currentView.getType())) if(!ReportType.PIVOT.equals(currentView.getType()))
{ {
sheetWriter.endSheet(); sheetWriter.endSheet(currentView, styleCustomizerInterface);
} }
activeSheetWriter.flush(); activeSheetWriter.flush();
@ -815,7 +841,29 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
*******************************************************************************/ *******************************************************************************/
protected PoiExcelStylerInterface getStylerInterface() protected PoiExcelStylerInterface getStylerInterface()
{ {
if(styleCustomizerInterface != null)
{
return styleCustomizerInterface.getExcelStyler();
}
return (new PlainPoiExcelStyler()); return (new PlainPoiExcelStyler());
} }
/***************************************************************************
**
***************************************************************************/
@Override
public void setExportStyleCustomizer(ExportStyleCustomizerInterface exportStyleCustomizer)
{
if(exportStyleCustomizer instanceof ExcelPoiBasedStreamingStyleCustomizerInterface poiExcelStylerInterface)
{
this.styleCustomizerInterface = poiExcelStylerInterface;
}
else
{
LOG.debug("Supplied export style customizer is not an instance of ExcelPoiStyleCustomizerInterface, so will not be used for an excel export", logPair("exportStyleCustomizerClass", exportStyleCustomizer.getClass().getSimpleName()));
}
}
} }

View File

@ -0,0 +1,81 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStyleCustomizerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/*******************************************************************************
** style customization points for Excel files generated via our streaming POI.
*******************************************************************************/
public interface ExcelPoiBasedStreamingStyleCustomizerInterface extends ExportStyleCustomizerInterface
{
/***************************************************************************
** slightly legacy way we did excel styles - but get an instance of object
** that defaults "default" styles (header, footer, etc).
***************************************************************************/
default PoiExcelStylerInterface getExcelStyler()
{
return (new PlainPoiExcelStyler());
}
/***************************************************************************
** either change "default" styles put in the styles map, or create new ones
** which can then be applied to row/field values (cells) via:
** ExcelPoiBasedStreamingExportStreamer.setStyleForField(row, fieldName, styleName);
***************************************************************************/
default void customizeStyles(Map<String, XSSFCellStyle> styles, XSSFWorkbook workbook, CreationHelper createHelper)
{
//////////////////
// noop default //
//////////////////
}
/***************************************************************************
** for a given view (sheet), return a list of custom column widths.
** any nulls in the list are ignored (so default width is used).
***************************************************************************/
default List<Integer> getColumnWidthsForView(QReportView view)
{
return (null);
}
/***************************************************************************
** for a given view (sheet), return a list of any ranges which should be
** merged, as in "A1:C1" (first three cells in first row).
***************************************************************************/
default List<String> getMergedRangesForView(QReportView view)
{
return (null);
}
}

View File

@ -25,7 +25,10 @@ package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.poi.ss.util.CellReference; import org.apache.poi.ss.util.CellReference;
@ -53,13 +56,33 @@ public class StreamedSheetWriter
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public void beginSheet() throws IOException public void beginSheet(QReportView view, ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface) throws IOException
{ {
writer.write(""" writer.write("""
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">""");
<sheetData>""");
if(styleCustomizerInterface != null && view != null)
{
List<Integer> columnWidths = styleCustomizerInterface.getColumnWidthsForView(view);
if(CollectionUtils.nullSafeHasContents(columnWidths))
{
writer.write("<cols>");
for(int i = 0; i < columnWidths.size(); i++)
{
Integer width = columnWidths.get(i);
if(width != null)
{
writer.write("""
<col min="%d" max="%d" width="%d" customWidth="1"/>
""".formatted(i + 1, i + 1, width));
}
}
writer.write("</cols>");
}
}
writer.write("<sheetData>");
} }
@ -67,11 +90,25 @@ public class StreamedSheetWriter
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public void endSheet() throws IOException public void endSheet(QReportView view, ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface) throws IOException
{ {
writer.write(""" writer.write("</sheetData>");
</sheetData>
</worksheet>"""); if(styleCustomizerInterface != null && view != null)
{
List<String> mergedRanges = styleCustomizerInterface.getMergedRangesForView(view);
if(CollectionUtils.nullSafeHasContents(mergedRanges))
{
writer.write(String.format("<mergeCells count=\"%d\">", mergedRanges.size()));
for(String range : mergedRanges)
{
writer.write(String.format("<mergeCell ref=\"%s\"/>", range));
}
writer.write("</mergeCells>");
}
}
writer.write("</worksheet>");
} }

View File

@ -53,7 +53,8 @@ public class QJavaExecutor implements QCodeExecutor
Serializable output; Serializable output;
try try
{ {
Function<Map<String, Object>, Serializable> function = QCodeLoader.getFunction(codeReference); @SuppressWarnings("unchecked")
Function<Map<String, Object>, Serializable> function = QCodeLoader.getAdHoc(Function.class, codeReference);
output = function.apply(context); output = function.apply(context);
} }
catch(Exception e) catch(Exception e)

View File

@ -341,7 +341,7 @@ public class QPossibleValueTranslator
try try
{ {
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource); QCustomPossibleValueProvider<?> customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference());
return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value))); return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value)));
} }
catch(Exception e) catch(Exception e)

View File

@ -424,7 +424,7 @@ public class SearchPossibleValueSourceAction
{ {
try try
{ {
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource); QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference());
List<QPossibleValue<?>> possibleValues = customPossibleValueProvider.search(input); List<QPossibleValue<?>> possibleValues = customPossibleValueProvider.search(input);
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput(); SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();

View File

@ -1425,7 +1425,7 @@ public class QInstanceEnricher
{ {
try try
{ {
QCustomPossibleValueProvider<?> customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource); QCustomPossibleValueProvider<?> customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference());
Method getPossibleValueMethod = customPossibleValueProvider.getClass().getDeclaredMethod("getPossibleValue", Serializable.class); Method getPossibleValueMethod = customPossibleValueProvider.getClass().getDeclaredMethod("getPossibleValue", Serializable.class);
Type returnType = getPossibleValueMethod.getGenericReturnType(); Type returnType = getPossibleValueMethod.getGenericReturnType();

View File

@ -28,6 +28,7 @@ import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface; import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
@ -44,6 +45,7 @@ public class ReportInput extends AbstractTableActionInput
private ReportDestination reportDestination; private ReportDestination reportDestination;
private Supplier<? extends ExportStreamerInterface> overrideExportStreamerSupplier; private Supplier<? extends ExportStreamerInterface> overrideExportStreamerSupplier;
private QCodeReference exportStyleCustomizer;
@ -208,4 +210,35 @@ public class ReportInput extends AbstractTableActionInput
return (this); return (this);
} }
/*******************************************************************************
** Getter for exportStyleCustomizer
*******************************************************************************/
public QCodeReference getExportStyleCustomizer()
{
return (this.exportStyleCustomizer);
}
/*******************************************************************************
** Setter for exportStyleCustomizer
*******************************************************************************/
public void setExportStyleCustomizer(QCodeReference exportStyleCustomizer)
{
this.exportStyleCustomizer = exportStyleCustomizer;
}
/*******************************************************************************
** Fluent setter for exportStyleCustomizer
*******************************************************************************/
public ReportInput withExportStyleCustomizer(QCodeReference exportStyleCustomizer)
{
this.exportStyleCustomizer = exportStyleCustomizer;
return (this);
}
} }

View File

@ -583,4 +583,31 @@ public abstract class QRecordEntity
return (null); return (null);
} }
/***************************************************************************
**
***************************************************************************/
public static String getTableName(Class<? extends QRecordEntity> entityClass) throws QException
{
try
{
Field tableNameField = entityClass.getDeclaredField("TABLE_NAME");
String tableNameValue = (String) tableNameField.get(null);
return (tableNameValue);
}
catch(Exception e)
{
throw (new QException("Could not get TABLE_NAME from entity class: " + entityClass.getSimpleName(), e));
}
}
/***************************************************************************
** named without the 'get' to avoid conflict w/ entity fields named that...
***************************************************************************/
public String tableName() throws QException
{
return (getTableName(this.getClass()));
}
} }

View File

@ -417,7 +417,7 @@ public class MetaDataProducerHelper
return (null); return (null);
} }
ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName); ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName, childTable.childJoin().orderBy());
producer.setSourceClass(entityClass); producer.setSourceClass(entityClass);
return producer; return producer;
} }

View File

@ -0,0 +1,38 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.code;
/*******************************************************************************
** an object which is intended to be constructed via a CodeReference, and,
** moreso, after it is created, then the initialize method here gets called,
** passing the codeRefernce in - e.g., to do additional initalization of the
** object, e.g., properties in a QCodeReferenceWithProperties
*******************************************************************************/
public interface InitializableViaCodeReference
{
/***************************************************************************
**
***************************************************************************/
void initialize(QCodeReference codeReference);
}

View File

@ -29,7 +29,7 @@ import java.io.Serializable;
** Pointer to code to be ran by the qqq framework, e.g., for custom behavior - ** Pointer to code to be ran by the qqq framework, e.g., for custom behavior -
** maybe process steps, maybe customization to a table, etc. ** maybe process steps, maybe customization to a table, etc.
*******************************************************************************/ *******************************************************************************/
public class QCodeReference implements Serializable public class QCodeReference implements Serializable, Cloneable
{ {
private String name; private String name;
private QCodeType codeType; private QCodeType codeType;
@ -58,6 +58,25 @@ public class QCodeReference implements Serializable
/***************************************************************************
**
***************************************************************************/
@Override
public QCodeReference clone()
{
try
{
QCodeReference clone = (QCodeReference) super.clone();
return clone;
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -179,5 +198,4 @@ public class QCodeReference implements Serializable
this.inlineCode = inlineCode; this.inlineCode = inlineCode;
return (this); return (this);
} }
} }

View File

@ -0,0 +1,59 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.code;
import java.io.Serializable;
import java.util.Map;
/*******************************************************************************
** a code reference that also has a map of properties. This object (with the
** properties) will be passed in to the referenced object, if it implements
** InitializableViaCodeReference.
*******************************************************************************/
public class QCodeReferenceWithProperties extends QCodeReference
{
private final Map<String, Serializable> properties;
/***************************************************************************
**
***************************************************************************/
public QCodeReferenceWithProperties(Class<?> javaClass, Map<String, Serializable> properties)
{
super(javaClass);
this.properties = properties;
}
/*******************************************************************************
** Getter for properties
**
*******************************************************************************/
public Map<String, Serializable> getProperties()
{
return properties;
}
}

View File

@ -62,7 +62,8 @@ public class WidgetAdHocValue extends AbstractWidgetValueSource
context.putAll(inputValues); context.putAll(inputValues);
} }
Function<Object, Object> function = QCodeLoader.getFunction(codeReference); @SuppressWarnings("unchecked")
Function<Object, Object> function = QCodeLoader.getAdHoc(Function.class, codeReference);
Object result = function.apply(context); Object result = function.apply(context);
return (result); return (result);
} }

View File

@ -238,7 +238,7 @@ public class QFieldMetaData implements Cloneable
if(StringUtils.hasContent(fieldAnnotation.defaultValue())) if(StringUtils.hasContent(fieldAnnotation.defaultValue()))
{ {
ValueUtils.getValueAsFieldType(this.type, fieldAnnotation.defaultValue()); withDefaultValue(ValueUtils.getValueAsFieldType(this.type, fieldAnnotation.defaultValue()));
} }
} }
} }

View File

@ -55,6 +55,7 @@ public class QFrontendFieldMetaData implements Serializable
private String possibleValueSourceName; private String possibleValueSourceName;
private String displayFormat; private String displayFormat;
private Serializable defaultValue; private Serializable defaultValue;
private Integer maxLength;
private List<FieldAdornment> adornments; private List<FieldAdornment> adornments;
private List<QHelpContent> helpContents; private List<QHelpContent> helpContents;
@ -85,6 +86,7 @@ public class QFrontendFieldMetaData implements Serializable
this.defaultValue = fieldMetaData.getDefaultValue(); this.defaultValue = fieldMetaData.getDefaultValue();
this.helpContents = fieldMetaData.getHelpContents(); this.helpContents = fieldMetaData.getHelpContents();
this.inlinePossibleValueSource = fieldMetaData.getInlinePossibleValueSource(); this.inlinePossibleValueSource = fieldMetaData.getInlinePossibleValueSource();
this.maxLength = fieldMetaData.getMaxLength();
for(FieldBehavior<?> behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors())) for(FieldBehavior<?> behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors()))
{ {

View File

@ -48,6 +48,8 @@ public class QFrontendProcessMetaData
private String label; private String label;
private String tableName; private String tableName;
private boolean isHidden; private boolean isHidden;
private Integer minInputRecords;
private Integer maxInputRecords;
private QIcon icon; private QIcon icon;
@ -72,6 +74,8 @@ public class QFrontendProcessMetaData
this.tableName = processMetaData.getTableName(); this.tableName = processMetaData.getTableName();
this.isHidden = processMetaData.getIsHidden(); this.isHidden = processMetaData.getIsHidden();
this.stepFlow = processMetaData.getStepFlow().toString(); this.stepFlow = processMetaData.getStepFlow().toString();
this.minInputRecords = processMetaData.getMinInputRecords();
this.maxInputRecords = processMetaData.getMaxInputRecords();
if(includeSteps) if(includeSteps)
{ {
@ -213,4 +217,27 @@ public class QFrontendProcessMetaData
{ {
return icon; return icon;
} }
/*******************************************************************************
** Getter for minInputRecords
**
*******************************************************************************/
public Integer getMinInputRecords()
{
return minInputRecords;
}
/*******************************************************************************
** Getter for maxInputRecords
**
*******************************************************************************/
public Integer getMaxInputRecords()
{
return maxInputRecords;
}
} }

View File

@ -31,7 +31,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout;
** Future may allow something like a "namespace", and/or multiple icons for ** Future may allow something like a "namespace", and/or multiple icons for
** use in different frontends, etc. ** use in different frontends, etc.
*******************************************************************************/ *******************************************************************************/
public class QIcon public class QIcon implements Cloneable
{ {
private String name; private String name;
private String path; private String path;
@ -58,6 +58,25 @@ public class QIcon
/***************************************************************************
**
***************************************************************************/
@Override
public QIcon clone()
{
try
{
QIcon clone = (QIcon) super.clone();
return clone;
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
/******************************************************************************* /*******************************************************************************
** Getter for name ** Getter for name
** **
@ -154,6 +173,4 @@ public class QIcon
this.color = color; this.color = color;
return (this); return (this);
} }
} }

View File

@ -24,11 +24,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.producers;
import java.util.Objects; import java.util.Objects;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; 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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; 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.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -39,8 +41,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
** **
** e.g., Orders & LineItems - on the Order entity ** e.g., Orders & LineItems - on the Order entity
** <code> ** <code>
@QMetaDataProducingEntity( @QMetaDataProducingEntity( childTables = { @ChildTable(
childTables = { @ChildTable(
childTableEntityClass = LineItem.class, childTableEntityClass = LineItem.class,
childJoin = @ChildJoin(enabled = true), childJoin = @ChildJoin(enabled = true),
childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Order Lines")) childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Order Lines"))
@ -62,13 +63,16 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
private String parentTableName; // e.g., order private String parentTableName; // e.g., order
private String foreignKeyFieldName; // e.g., orderId private String foreignKeyFieldName; // e.g., orderId
private ChildJoin.OrderBy[] orderBys;
private Class<?> sourceClass; private Class<?> sourceClass;
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName) public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName, ChildJoin.OrderBy[] orderBys)
{ {
Objects.requireNonNull(childTableName, "childTableName cannot be null"); Objects.requireNonNull(childTableName, "childTableName cannot be null");
Objects.requireNonNull(parentTableName, "parentTableName cannot be null"); Objects.requireNonNull(parentTableName, "parentTableName cannot be null");
@ -77,6 +81,7 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
this.childTableName = childTableName; this.childTableName = childTableName;
this.parentTableName = parentTableName; this.parentTableName = parentTableName;
this.foreignKeyFieldName = foreignKeyFieldName; this.foreignKeyFieldName = foreignKeyFieldName;
this.orderBys = orderBys;
} }
@ -87,18 +92,39 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
@Override @Override
public QJoinMetaData produce(QInstance qInstance) throws QException public QJoinMetaData produce(QInstance qInstance) throws QException
{ {
QTableMetaData possibleValueTable = qInstance.getTable(parentTableName); QTableMetaData parentTable = qInstance.getTable(parentTableName);
if(possibleValueTable == null) if(parentTable == null)
{ {
throw (new QException("Could not find tableMetaData " + parentTableName)); throw (new QException("Could not find tableMetaData " + parentTableName));
} }
QTableMetaData childTable = qInstance.getTable(childTableName);
if(childTable == null)
{
throw (new QException("Could not find tableMetaData " + childTable));
}
QJoinMetaData join = new QJoinMetaData() QJoinMetaData join = new QJoinMetaData()
.withLeftTable(parentTableName) .withLeftTable(parentTableName)
.withRightTable(childTableName) .withRightTable(childTableName)
.withInferredName() .withInferredName()
.withType(JoinType.ONE_TO_MANY) .withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn(possibleValueTable.getPrimaryKeyField(), foreignKeyFieldName)); .withJoinOn(new JoinOn(parentTable.getPrimaryKeyField(), foreignKeyFieldName));
if(orderBys != null && orderBys.length > 0)
{
for(ChildJoin.OrderBy orderBy : orderBys)
{
join.withOrderBy(new QFilterOrderBy(orderBy.fieldName(), orderBy.isAscending()));
}
}
else
{
//////////////////////////////////////////////////////////
// by default, sort by the id of the child table... mmm //
//////////////////////////////////////////////////////////
join.withOrderBy(new QFilterOrderBy(childTable.getPrimaryKeyField()));
}
return (join); return (join);
} }
@ -126,6 +152,7 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
} }
/******************************************************************************* /*******************************************************************************
** Fluent setter for sourceClass ** Fluent setter for sourceClass
** **

View File

@ -35,4 +35,16 @@ import java.lang.annotation.RetentionPolicy;
public @interface ChildJoin public @interface ChildJoin
{ {
boolean enabled(); boolean enabled();
OrderBy[] orderBy() default { };
/***************************************************************************
**
***************************************************************************/
@interface OrderBy
{
String fieldName();
boolean isAscending() default true;
}
} }

View File

@ -40,7 +40,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
** (optionally along with queryJoins and queryInputCustomizer) is used. ** (optionally along with queryJoins and queryInputCustomizer) is used.
** - else a staticDataSupplier is used. ** - else a staticDataSupplier is used.
*******************************************************************************/ *******************************************************************************/
public class QReportDataSource public class QReportDataSource implements Cloneable
{ {
private String name; private String name;
@ -55,6 +55,39 @@ public class QReportDataSource
/***************************************************************************
**
***************************************************************************/
@Override
public QReportDataSource clone()
{
try
{
QReportDataSource clone = (QReportDataSource) super.clone();
if(queryFilter != null)
{
clone.queryFilter = queryFilter.clone();
}
if(queryJoins != null)
{
clone.queryJoins = new ArrayList<>();
for(QueryJoin join : queryJoins)
{
queryJoins.add(join.clone());
}
}
return clone;
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
/******************************************************************************* /*******************************************************************************
** Getter for name ** Getter for name
** **
@ -274,6 +307,7 @@ public class QReportDataSource
} }
/******************************************************************************* /*******************************************************************************
** Getter for customRecordSource ** Getter for customRecordSource
*******************************************************************************/ *******************************************************************************/
@ -303,5 +337,4 @@ public class QReportDataSource
return (this); return (this);
} }
} }

View File

@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
@ -37,7 +38,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/******************************************************************************* /*******************************************************************************
** Meta-data definition of a report generated by QQQ ** Meta-data definition of a report generated by QQQ
*******************************************************************************/ *******************************************************************************/
public class QReportMetaData implements QAppChildMetaData, MetaDataWithPermissionRules, TopLevelMetaDataInterface public class QReportMetaData implements QAppChildMetaData, MetaDataWithPermissionRules, TopLevelMetaDataInterface, Cloneable
{ {
private String name; private String name;
private String label; private String label;
@ -52,6 +53,72 @@ public class QReportMetaData implements QAppChildMetaData, MetaDataWithPermissio
private QIcon icon; private QIcon icon;
private QCodeReference exportStyleCustomizer;
/***************************************************************************
**
***************************************************************************/
@Override
public QReportMetaData clone()
{
try
{
QReportMetaData clone = (QReportMetaData) super.clone();
//////////////////////////////
// Deep copy mutable fields //
//////////////////////////////
if(this.inputFields != null)
{
clone.inputFields = new ArrayList<>();
for(QFieldMetaData inputField : this.inputFields)
{
clone.inputFields.add(inputField.clone());
}
}
if(this.dataSources != null)
{
clone.dataSources = new ArrayList<>();
for(QReportDataSource dataSource : this.dataSources)
{
clone.dataSources.add(dataSource.clone());
}
}
if(this.views != null)
{
clone.views = new ArrayList<>();
for(QReportView view : this.views)
{
clone.views.add(view.clone());
}
}
if(this.permissionRules != null)
{
clone.permissionRules = this.permissionRules.clone();
}
if(this.icon != null)
{
clone.icon = this.icon.clone();
}
if(this.exportStyleCustomizer != null)
{
clone.exportStyleCustomizer = this.exportStyleCustomizer.clone();
}
return clone;
}
catch(CloneNotSupportedException e)
{
throw new AssertionError("Cloning not supported", e);
}
}
/******************************************************************************* /*******************************************************************************
@ -397,4 +464,35 @@ public class QReportMetaData implements QAppChildMetaData, MetaDataWithPermissio
qInstance.addReport(this); qInstance.addReport(this);
} }
/*******************************************************************************
** Getter for exportStyleCustomizer
*******************************************************************************/
public QCodeReference getExportStyleCustomizer()
{
return (this.exportStyleCustomizer);
}
/*******************************************************************************
** Setter for exportStyleCustomizer
*******************************************************************************/
public void setExportStyleCustomizer(QCodeReference exportStyleCustomizer)
{
this.exportStyleCustomizer = exportStyleCustomizer;
}
/*******************************************************************************
** Fluent setter for exportStyleCustomizer
*******************************************************************************/
public QReportMetaData withExportStyleCustomizer(QCodeReference exportStyleCustomizer)
{
this.exportStyleCustomizer = exportStyleCustomizer;
return (this);
}
} }

View File

@ -24,8 +24,12 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction; import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -37,9 +41,14 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.mapp
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.mapping.BulkLoadTableStructureBuilder; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.mapping.BulkLoadTableStructureBuilder;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadFileRow; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadFileRow;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfileField;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadTableStructure; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadTableStructure;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.json.JSONObject;
/******************************************************************************* /*******************************************************************************
@ -72,7 +81,7 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
{ {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<String> headerValues = (List<String>) runBackendStepOutput.getValue("headerValues"); List<String> headerValues = (List<String>) runBackendStepOutput.getValue("headerValues");
buildSuggestedMapping(headerValues, tableStructure, runBackendStepOutput); buildSuggestedMapping(headerValues, getPrepopulatedValues(runBackendStepInput), tableStructure, runBackendStepOutput);
} }
} }
@ -81,10 +90,62 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
private void buildSuggestedMapping(List<String> headerValues, BulkLoadTableStructure tableStructure, RunBackendStepOutput runBackendStepOutput) private Map<String, Serializable> getPrepopulatedValues(RunBackendStepInput runBackendStepInput)
{
String prepopulatedValuesJson = runBackendStepInput.getValueString("prepopulatedValues");
if(StringUtils.hasContent(prepopulatedValuesJson))
{
Map<String, Serializable> rs = new LinkedHashMap<>();
JSONObject jsonObject = JsonUtils.toJSONObject(prepopulatedValuesJson);
for(String key : jsonObject.keySet())
{
rs.put(key, jsonObject.optString(key, null));
}
return (rs);
}
return (Collections.emptyMap());
}
/***************************************************************************
**
***************************************************************************/
private void buildSuggestedMapping(List<String> headerValues, Map<String, Serializable> prepopulatedValues, BulkLoadTableStructure tableStructure, RunBackendStepOutput runBackendStepOutput)
{ {
BulkLoadMappingSuggester bulkLoadMappingSuggester = new BulkLoadMappingSuggester(); BulkLoadMappingSuggester bulkLoadMappingSuggester = new BulkLoadMappingSuggester();
BulkLoadProfile bulkLoadProfile = bulkLoadMappingSuggester.suggestBulkLoadMappingProfile(tableStructure, headerValues); BulkLoadProfile bulkLoadProfile = bulkLoadMappingSuggester.suggestBulkLoadMappingProfile(tableStructure, headerValues);
if(CollectionUtils.nullSafeHasContents(prepopulatedValues))
{
for(Map.Entry<String, Serializable> entry : prepopulatedValues.entrySet())
{
String fieldName = entry.getKey();
boolean foundFieldInProfile = false;
for(BulkLoadProfileField bulkLoadProfileField : bulkLoadProfile.getFieldList())
{
if(bulkLoadProfileField.getFieldName().equals(fieldName))
{
foundFieldInProfile = true;
bulkLoadProfileField.setColumnIndex(null);
bulkLoadProfileField.setHeaderName(null);
bulkLoadProfileField.setDefaultValue(entry.getValue());
break;
}
}
if(!foundFieldInProfile)
{
BulkLoadProfileField bulkLoadProfileField = new BulkLoadProfileField();
bulkLoadProfileField.setFieldName(fieldName);
bulkLoadProfileField.setDefaultValue(entry.getValue());
bulkLoadProfile.getFieldList().add(bulkLoadProfileField);
}
}
}
runBackendStepOutput.addValue("bulkLoadProfile", bulkLoadProfile); runBackendStepOutput.addValue("bulkLoadProfile", bulkLoadProfile);
runBackendStepOutput.addValue("suggestedBulkLoadProfile", bulkLoadProfile); runBackendStepOutput.addValue("suggestedBulkLoadProfile", bulkLoadProfile);
} }

View File

@ -76,6 +76,23 @@ public class XlsxFileToRows extends AbstractIteratorBasedFileToRows<org.dhatim.f
} }
/***************************************************************************
** open/go-to a specific sheet (by 0-based index). resets rows & iterator.
***************************************************************************/
public void openSheet(int index) throws IOException
{
Optional<Sheet> sheet = workbook.getSheet(index);
if(sheet.isEmpty())
{
throw (new IOException("No sheet found for index: " + index));
}
rows = sheet.get().openStream();
setIterator(rows.iterator());
}
/*************************************************************************** /***************************************************************************
** **

View File

@ -53,7 +53,7 @@ public class BaseStreamedETLStep
protected AbstractExtractStep getExtractStep(RunBackendStepInput runBackendStepInput) protected AbstractExtractStep getExtractStep(RunBackendStepInput runBackendStepInput)
{ {
QCodeReference codeReference = (QCodeReference) runBackendStepInput.getValue(StreamedETLWithFrontendProcess.FIELD_EXTRACT_CODE); QCodeReference codeReference = (QCodeReference) runBackendStepInput.getValue(StreamedETLWithFrontendProcess.FIELD_EXTRACT_CODE);
return (QCodeLoader.getBackendStep(AbstractExtractStep.class, codeReference)); return (QCodeLoader.getAdHoc(AbstractExtractStep.class, codeReference));
} }

View File

@ -22,13 +22,13 @@
package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend; package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public class CouldNotFindQueryFilterForExtractStepException extends QException public class CouldNotFindQueryFilterForExtractStepException extends QUserFacingException
{ {
/******************************************************************************* /*******************************************************************************
** **

View File

@ -279,7 +279,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep
return (new QQueryFilter().withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, idStrings))); return (new QQueryFilter().withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, idStrings)));
} }
throw (new CouldNotFindQueryFilterForExtractStepException("Could not find query filter for Extract step.")); throw (new CouldNotFindQueryFilterForExtractStepException("No records were selected for running this process."));
} }

View File

@ -318,7 +318,7 @@ public class SavedReportToReportMetaDataAdapter
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private static QReportField makeQReportField(String fieldName, FieldAndJoinTable fieldAndJoinTable) public static QReportField makeQReportField(String fieldName, FieldAndJoinTable fieldAndJoinTable)
{ {
QReportField reportField = new QReportField(); QReportField reportField = new QReportField();
@ -404,5 +404,5 @@ public class SavedReportToReportMetaDataAdapter
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable) {} public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable) {}
} }

View File

@ -22,12 +22,19 @@
package com.kingsrook.qqq.backend.core.actions.customizers; package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.Timer; import com.kingsrook.qqq.backend.core.utils.Timer;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/******************************************************************************* /*******************************************************************************
@ -80,6 +87,7 @@ class QCodeLoaderTest extends BaseTest
} }
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -91,4 +99,50 @@ class QCodeLoaderTest extends BaseTest
} }
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testCodeReferenceWithProperties()
{
assertNull(QCodeLoader.getAdHoc(SomeClass.class, new QCodeReference(SomeClass.class)));
SomeClass someObject = QCodeLoader.getAdHoc(SomeClass.class, new QCodeReferenceWithProperties(SomeClass.class, Map.of("property", "someValue")));
assertEquals("someValue", someObject.someProperty);
SomeClass someOtherObject = QCodeLoader.getAdHoc(SomeClass.class, new QCodeReferenceWithProperties(SomeClass.class, Map.of("property", "someOtherValue")));
assertEquals("someOtherValue", someOtherObject.someProperty);
}
/***************************************************************************
**
***************************************************************************/
public static class SomeClass implements InitializableViaCodeReference
{
private String someProperty;
/***************************************************************************
**
***************************************************************************/
@Override
public void initialize(QCodeReference codeReference)
{
if(codeReference instanceof QCodeReferenceWithProperties codeReferenceWithProperties)
{
someProperty = ValueUtils.getValueAsString(codeReferenceWithProperties.getProperties().get("property"));
}
if(!StringUtils.hasContent(someProperty))
{
throw new IllegalStateException("Missing property");
}
}
}
} }

View File

@ -38,6 +38,7 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.reporting.excel.TestExcelStyler;
import com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel.ExcelFastexcelExportStreamer; import com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel.ExcelFastexcelExportStreamer;
import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.BoldHeaderAndFooterPoiExcelStyler; import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.BoldHeaderAndFooterPoiExcelStyler;
import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.ExcelPoiBasedStreamingExportStreamer; import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.ExcelPoiBasedStreamingExportStreamer;
@ -56,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
@ -490,6 +492,34 @@ public class GenerateReportActionTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void runXlsxWithStyleCustomizer() throws Exception
{
ReportFormat format = ReportFormat.XLSX;
String name = "/tmp/report-customized.xlsx";
try(FileOutputStream fileOutputStream = new FileOutputStream(name))
{
QInstance qInstance = QContext.getQInstance();
qInstance.addReport(defineTableOnlyReport());
insertPersonRecords(qInstance);
ReportInput reportInput = new ReportInput();
reportInput.setReportName(REPORT_NAME);
reportInput.setReportDestination(new ReportDestination().withReportFormat(format).withReportOutputStream(fileOutputStream));
reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()));
reportInput.setExportStyleCustomizer(new QCodeReference(TestExcelStyler.class));
new GenerateReportAction().execute(reportInput);
System.out.println("Wrote File: " + name);
LocalMacDevUtils.openFile(name);
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -0,0 +1,76 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excel;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.ExcelPoiBasedStreamingStyleCustomizerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/*******************************************************************************
**
*******************************************************************************/
public class TestExcelStyler implements ExcelPoiBasedStreamingStyleCustomizerInterface
{
/***************************************************************************
**
***************************************************************************/
@Override
public List<Integer> getColumnWidthsForView(QReportView view)
{
return List.of(60, 50, 40);
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<String> getMergedRangesForView(QReportView view)
{
return List.of("A1:B1");
}
/***************************************************************************
**
***************************************************************************/
@Override
public void customizeStyles(Map<String, XSSFCellStyle> styles, XSSFWorkbook workbook, CreationHelper createHelper)
{
Font font = workbook.createFont();
font.setFontHeightInPoints((short) 16);
font.setBold(true);
XSSFCellStyle cellStyle = workbook.createCellStyle();
cellStyle.setFont(font);
styles.put("header", cellStyle);
}
}

View File

@ -41,6 +41,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -566,4 +567,22 @@ class QRecordEntityTest extends BaseTest
assertEquals(0, order.getLineItems().size()); assertEquals(0, order.getLineItems().size());
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableName() throws QException
{
assertEquals(Item.TABLE_NAME, QRecordEntity.getTableName(Item.class));
assertEquals(Item.TABLE_NAME, Item.getTableName(Item.class));
assertEquals(Item.TABLE_NAME, new Item().tableName());
//////////////////////////////////
// no TABLE_NAME in Order class //
//////////////////////////////////
assertThatThrownBy(() -> Order.getTableName(Order.class));
}
} }

View File

@ -22,8 +22,22 @@
package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert; package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfileField;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -66,4 +80,39 @@ class BulkInsertPrepareFileMappingStepTest extends BaseTest
assertEquals("BAA", BulkInsertPrepareFileMappingStep.toHeaderLetter(2 * 26 * 26 + 26 + 0)); assertEquals("BAA", BulkInsertPrepareFileMappingStep.toHeaderLetter(2 * 26 * 26 + 26 + 0));
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws Exception
{
String fileName = "personFile.csv";
StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(fileName);
OutputStream outputStream = new StorageAction().createOutputStream(storageInput);
outputStream.write("""
name,noOfShoes
John,2
Jane,4
""".getBytes(StandardCharsets.UTF_8));
outputStream.close();
RunProcessInput runProcessInput = new RunProcessInput();
BulkInsertStepUtils.setStorageInputForTheFile(runProcessInput, storageInput);
runProcessInput.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY);
runProcessInput.addValue("prepopulatedValues", JsonUtils.toJson(Map.of("homeStateId", 1)));
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(runProcessInput.getProcessState());
RunBackendStepOutput runBackendStepOutput = new RunBackendStepOutput();
new BulkInsertPrepareFileMappingStep().run(runBackendStepInput, runBackendStepOutput);
BulkLoadProfile bulkLoadProfile = (BulkLoadProfile) runBackendStepOutput.getValue("suggestedBulkLoadProfile");
Optional<BulkLoadProfileField> homeStateId = bulkLoadProfile.getFieldList().stream().filter(f -> f.getFieldName().equals("homeStateId")).findFirst();
assertThat(homeStateId).isPresent();
assertEquals("1", homeStateId.get().getDefaultValue());
}
} }