This is an automated email from the ASF dual-hosted git repository.
mridulpathak pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new bd4bff1b87 Implemented: real POI-based .xlsx export via a new
screenxlsx view-handler type (OFBIZ-13567)
bd4bff1b87 is described below
commit bd4bff1b8749151b473b361d0814cfaca610b99b
Author: Mridul Pathak <[email protected]>
AuthorDate: Tue Sep 8 16:55:10 2026 +0530
Implemented: real POI-based .xlsx export via a new screenxlsx view-handler
type (OFBIZ-13567)
Adds a new screenxlsx view-handler type that renders screens as genuine
binary .xlsx workbooks through Apache POI, additive alongside the existing
screenxls/screencsv types.
- ScreenXlsxViewHandler drives the existing, unmodified
ScreenRenderer/FormRenderer widget-tree walk through two new renderer
implementations
- XlsxFormRenderer writes typed POI cells for form/grid data: NUMERIC for
currency/accounting-number fields, real Excel dates for date/date-time fields,
STRING otherwise, always falling back to STRING on any unparseable value
- XlsxScreenStringRenderer is a no-op renderer that lets the screen tree
walk complete, since only form/grid content is in scope
- New poi-ooxml Gradle dependency supplies the XSSF/SXSSF classes core POI
lacks
- New WebtoolsLayoutDemoXlsx demo view-map proves the mechanism end-to-end
alongside its existing screenxls counterpart, producing a real, correctly-typed
multi-sheet workbook
- New CommonViewAsXlsx UI label for the matching entry points added on the
plugins/example side
- Includes unit test coverage for the renderer's row/column/cell-typing
logic
---
dependencies.gradle | 1 +
framework/common/config/CommonUiLabels.xml | 3 +
.../webcommon/WEB-INF/handlers-controller.xml | 1 +
.../webapp/webtools/WEB-INF/controller.xml | 2 +
.../renderer/xlsx/ScreenXlsxViewHandler.java | 128 ++++++
.../widget/renderer/xlsx/XlsxFormRenderer.java | 433 +++++++++++++++++++++
.../renderer/xlsx/XlsxScreenStringRenderer.java | 173 ++++++++
.../widget/renderer/xlsx/XlsxFormRendererTest.java | 232 +++++++++++
gradle/libs.versions.toml | 1 +
9 files changed, 974 insertions(+)
diff --git a/dependencies.gradle b/dependencies.gradle
index 20a7f9f510..8ae7ab70b0 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -53,6 +53,7 @@ dependencies {
implementation libs.log4j.core // Somehow needed by Buildbot to compile
OFBizDynamicThresholdFilter.java
implementation libs.log4j.layout.template.json // JSON/ECS structured
logging profile (log4j2-json.xml)
implementation libs.poi
+ implementation libs.poi.ooxml
implementation libs.bundles.pdfbox
implementation libs.shiro.core
implementation libs.shiro.crypto.cipher
diff --git a/framework/common/config/CommonUiLabels.xml
b/framework/common/config/CommonUiLabels.xml
index da56b33e87..f0847bdae7 100644
--- a/framework/common/config/CommonUiLabels.xml
+++ b/framework/common/config/CommonUiLabels.xml
@@ -13150,6 +13150,9 @@
<value xml:lang="es-MX">Ver como hoja de cálculo</value>
<value xml:lang="fr">Voir en tableur</value>
</property>
+ <property key="CommonViewAsXlsx">
+ <value xml:lang="en">View as spreadsheet (.xlsx)</value>
+ </property>
<property key="CommonViewAsXml">
<value xml:lang="en">View as xml</value>
<value xml:lang="es">Ver como XML</value>
diff --git a/framework/common/webcommon/WEB-INF/handlers-controller.xml
b/framework/common/webcommon/WEB-INF/handlers-controller.xml
index b1de710c83..f0080c20f6 100644
--- a/framework/common/webcommon/WEB-INF/handlers-controller.xml
+++ b/framework/common/webcommon/WEB-INF/handlers-controller.xml
@@ -41,6 +41,7 @@ under the License.
<handler name="screentext" type="view"
class="org.apache.ofbiz.widget.renderer.macro.MacroScreenViewHandler"/>
<handler name="screencsv" type="view"
class="org.apache.ofbiz.widget.renderer.macro.MacroScreenViewHandler"/>
<handler name="screenxls" type="view"
class="org.apache.ofbiz.widget.renderer.macro.MacroScreenViewHandler"/>
+ <handler name="screenxlsx" type="view"
class="org.apache.ofbiz.widget.renderer.xlsx.ScreenXlsxViewHandler"/>
<handler name="screenfop" type="view"
class="org.apache.ofbiz.widget.renderer.fo.ScreenFopViewHandler"/>
<handler name="jsp" type="view"
class="org.apache.ofbiz.webapp.view.JspViewHandler"/>
<handler name="http" type="view"
class="org.apache.ofbiz.webapp.view.HttpViewHandler"/>
diff --git a/framework/webtools/webapp/webtools/WEB-INF/controller.xml
b/framework/webtools/webapp/webtools/WEB-INF/controller.xml
index c579a9206a..8f2990fbd2 100644
--- a/framework/webtools/webapp/webtools/WEB-INF/controller.xml
+++ b/framework/webtools/webapp/webtools/WEB-INF/controller.xml
@@ -741,6 +741,7 @@ under the License.
<request-map uri="WebtoolsLayoutDemoXml"><security https="true"
auth="true"/><response name="success" type="view"
value="WebtoolsLayoutDemoXml"/></request-map>
<request-map uri="WebtoolsLayoutDemoCsv"><security https="true"
auth="true"/><response name="success" type="view"
value="WebtoolsLayoutDemoCsv"/></request-map>
<request-map uri="WebtoolsLayoutDemoXls"><security https="true"
auth="true"/><response name="success" type="view"
value="WebtoolsLayoutDemoXls"/></request-map>
+ <request-map uri="WebtoolsLayoutDemoXlsx"><security https="true"
auth="true"/><response name="success" type="view"
value="WebtoolsLayoutDemoXlsx"/></request-map>
<request-map uri="AvailableUelList"><security https="true"
auth="true"/><response name="success" type="view"
value="AvailableUelList"/></request-map>
<!-- end of request mappings -->
@@ -857,6 +858,7 @@ under the License.
<view-map name="WebtoolsLayoutDemoXml" type="screenxml"
page="component://webtools/widget/MiscScreens.xml#WebtoolsLayoutDemoText"
content-type="text/xml"/>
<view-map name="WebtoolsLayoutDemoCsv" type="screencsv"
page="component://webtools/widget/MiscScreens.xml#WebtoolsLayoutDemoText"
content-type="text/csv"/>
<view-map name="WebtoolsLayoutDemoXls" type="screenxls"
page="component://webtools/widget/MiscScreens.xml#WebtoolsLayoutDemoText"
content-type="application/vnd.ms-excel"/>
+ <view-map name="WebtoolsLayoutDemoXlsx" type="screenxlsx"
page="component://webtools/widget/MiscScreens.xml#WebtoolsLayoutDemoText"
content-type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"/>
<view-map name="AvailableUelList" type="screen"
page="component://webtools/widget/MiscScreens.xml#AvailableUelList"/>
<!-- end of view mappings -->
diff --git
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/xlsx/ScreenXlsxViewHandler.java
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/xlsx/ScreenXlsxViewHandler.java
new file mode 100644
index 0000000000..a0f6936bbf
--- /dev/null
+++
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/xlsx/ScreenXlsxViewHandler.java
@@ -0,0 +1,128 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.widget.renderer.xlsx;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.GeneralException;
+import org.apache.ofbiz.base.util.UtilGenerics;
+import org.apache.ofbiz.base.util.UtilHttp;
+import org.apache.ofbiz.base.util.UtilValidate;
+import org.apache.ofbiz.base.util.collections.MapStack;
+import org.apache.ofbiz.webapp.control.ConfigXMLReader;
+import org.apache.ofbiz.webapp.view.AbstractViewHandler;
+import org.apache.ofbiz.webapp.view.ViewHandlerException;
+import org.apache.ofbiz.widget.model.ModelTheme;
+import org.apache.ofbiz.widget.renderer.ScreenRenderer;
+import org.apache.ofbiz.widget.renderer.ScreenStringRenderer;
+import org.apache.ofbiz.widget.renderer.VisualTheme;
+import org.apache.ofbiz.widget.renderer.macro.MacroScreenRenderer;
+import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+import org.xml.sax.SAXException;
+
+import freemarker.template.TemplateException;
+
+/**
+ * Renders a screen's form/grid content as a genuine binary .xlsx workbook via
Apache POI,
+ * bypassing the FreeMarker macro pipeline entirely (unlike {@code
ScreenFopViewHandler},
+ * which still renders XSL-FO text via macros and only converts to binary as a
final step).
+ */
+public class ScreenXlsxViewHandler extends AbstractViewHandler {
+ private static final String MODULE = ScreenXlsxViewHandler.class.getName();
+ private static final String DEFAULT_ERROR_TEMPLATE =
"component://common/widget/CommonScreens.xml#FoError";
+ public static final String CONTENT_TYPE =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+
+ private ServletContext servletContext;
+
+ @Override
+ public void init(ServletContext context) throws ViewHandlerException {
+ this.servletContext = context;
+ }
+
+ @Override
+ public Map<String, Object> prepareViewContext(HttpServletRequest request,
HttpServletResponse response,
+ ConfigXMLReader.ViewMap viewMap) {
+ MapStack<String> context = MapStack.create();
+ ScreenRenderer.populateContextForRequest(context, null, request,
response, servletContext, viewMap.isSecureContext());
+ return context;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void render(String name, String page, String info, String
contentType, String encoding, HttpServletRequest request,
+ HttpServletResponse response, Map<String, Object> context) throws
ViewHandlerException {
+ try (SXSSFWorkbook workbook = new SXSSFWorkbook()) {
+ Writer writer = new StringWriter();
+ XlsxFormRenderer xlsxFormRenderer = new XlsxFormRenderer(workbook);
+ ScreenStringRenderer screenStringRenderer = new
XlsxScreenStringRenderer();
+ ScreenRenderer screens = new ScreenRenderer(writer,
UtilGenerics.cast(context), screenStringRenderer);
+ screens.getContext().put("formStringRenderer", xlsxFormRenderer);
+ screens.getContext().put("screens", screens);
+ screens.render(page);
+
+ String responseContentType = (UtilValidate.isEmpty(contentType) ||
"text/html".equals(contentType)) ? CONTENT_TYPE : contentType;
+ response.setContentType(responseContentType);
+ response.setHeader("Content-Disposition", "attachment;
filename=\"" + name + ".xlsx\"");
+ workbook.write(response.getOutputStream());
+ response.getOutputStream().flush();
+ } catch (IOException | GeneralException | SAXException |
ParserConfigurationException e) {
+ renderError("Problems rendering the xlsx export", e, request,
response, context);
+ }
+ }
+
+ /**
+ * Render error.
+ * @param msg the msg
+ * @param e the e
+ * @param request the request
+ * @param response the response
+ * @param context the context
+ * @throws ViewHandlerException the view handler exception
+ */
+ protected void renderError(String msg, Exception e, HttpServletRequest
request, HttpServletResponse response,
+ Map<String, Object> context) throws ViewHandlerException {
+ Debug.logError(msg + ": " + e, MODULE);
+ try {
+ Writer writer = new StringWriter();
+ VisualTheme visualTheme = UtilHttp.getVisualTheme(request);
+ ModelTheme modelTheme = visualTheme.getModelTheme();
+ ScreenStringRenderer screenStringRenderer = new
MacroScreenRenderer(modelTheme.getType("screen"),
+ modelTheme.getScreenRendererLocation("screen"));
+ ScreenRenderer screens = new ScreenRenderer(writer,
UtilGenerics.cast(context), screenStringRenderer);
+ screens.getContext().put("errorMessage", msg + ": " + e);
+ screens.render(DEFAULT_ERROR_TEMPLATE);
+ response.setContentType("text/html");
+ response.getWriter().write(writer.toString());
+ writer.close();
+ } catch (IOException | GeneralException | SAXException |
ParserConfigurationException | TemplateException x) {
+ Debug.logError("Multiple errors rendering xlsx export", MODULE);
+ throw new ViewHandlerException("Multiple errors rendering xlsx
export", x);
+ }
+ }
+}
diff --git
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/xlsx/XlsxFormRenderer.java
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/xlsx/XlsxFormRenderer.java
new file mode 100644
index 0000000000..672f19b708
--- /dev/null
+++
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/xlsx/XlsxFormRenderer.java
@@ -0,0 +1,433 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.widget.renderer.xlsx;
+
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.util.Map;
+
+import org.apache.ofbiz.widget.model.ModelForm;
+import org.apache.ofbiz.widget.model.ModelFormField;
+import org.apache.ofbiz.widget.renderer.FormStringRenderer;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.util.WorkbookUtil;
+
+/**
+ * Writes POI spreadsheet cells directly from widget form/grid rendering
callbacks,
+ * in place of the macro-call text a {@code FormStringRenderer} normally
produces.
+ * Used by {@code ScreenXlsxViewHandler} to build a real binary .xlsx workbook.
+ */
+public final class XlsxFormRenderer implements FormStringRenderer {
+
+ private final Workbook workbook;
+ private final CellStyle numericStyle;
+ private final CellStyle dateStyle;
+ private final CellStyle dateTimeStyle;
+
+ private Sheet sheet;
+ private Row currentRow;
+ private Cell currentCell;
+ private int rowIndex;
+ private int colIndex;
+
+ public XlsxFormRenderer(Workbook workbook) {
+ this.workbook = workbook;
+ CreationHelper creationHelper = workbook.getCreationHelper();
+
+ this.numericStyle = workbook.createCellStyle();
+
this.numericStyle.setDataFormat(creationHelper.createDataFormat().getFormat("#,##0.00"));
+
+ this.dateStyle = workbook.createCellStyle();
+
this.dateStyle.setDataFormat(creationHelper.createDataFormat().getFormat("m/d/yyyy"));
+
+ this.dateTimeStyle = workbook.createCellStyle();
+
this.dateTimeStyle.setDataFormat(creationHelper.createDataFormat().getFormat("m/d/yyyy
h:mm"));
+ }
+
+ Sheet getSheet() {
+ return sheet;
+ }
+
+ @Override
+ public void renderFormatListWrapperOpen(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ this.sheet =
workbook.createSheet(WorkbookUtil.createSafeSheetName(modelForm.getName()));
+ this.rowIndex = 0;
+ this.colIndex = 0;
+ }
+
+ @Override
+ public void renderFormatListWrapperClose(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatHeaderOpen(Appendable writer, Map<String, Object>
context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatHeaderClose(Appendable writer, Map<String, Object>
context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatHeaderRowOpen(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ this.currentRow = sheet.createRow(rowIndex++);
+ this.colIndex = 0;
+ }
+
+ @Override
+ public void renderFormatHeaderRowClose(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatHeaderRowCellOpen(Appendable writer, Map<String,
Object> context, ModelForm modelForm,
+ ModelFormField modelFormField, int positionSpan) {
+ this.currentCell = currentRow.createCell(colIndex);
+ }
+
+ @Override
+ public void renderFormatHeaderRowCellClose(Appendable writer, Map<String,
Object> context, ModelForm modelForm,
+ ModelFormField modelFormField) {
+ this.currentCell = null;
+ this.colIndex++;
+ }
+
+ @Override
+ public void renderFormatHeaderRowFormCellOpen(Appendable writer,
Map<String, Object> context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatHeaderRowFormCellClose(Appendable writer,
Map<String, Object> context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatHeaderRowFormCellTitleSeparator(Appendable writer,
Map<String, Object> context, ModelForm modelForm,
+ ModelFormField modelFormField, boolean isLast) {
+ }
+
+ @Override
+ public void renderFieldTitle(Appendable writer, Map<String, Object>
context, ModelFormField modelFormField) {
+ Cell cell = obtainCell();
+ cell.setCellValue(modelFormField.getTitle(context));
+ }
+
+ @Override
+ public void renderSingleFormFieldTitle(Appendable writer, Map<String,
Object> context, ModelFormField modelFormField) {
+ renderFieldTitle(writer, context, modelFormField);
+ }
+
+ @Override
+ public void renderFormatItemRowOpen(Appendable writer, Map<String, Object>
context, ModelForm modelForm) {
+ this.currentRow = sheet.createRow(rowIndex++);
+ this.colIndex = 0;
+ }
+
+ @Override
+ public void renderFormatItemRowClose(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatItemRowCellOpen(Appendable writer, Map<String,
Object> context, ModelForm modelForm,
+ ModelFormField modelFormField, int positionSpan) {
+ this.currentCell = currentRow.createCell(colIndex);
+ }
+
+ @Override
+ public void renderFormatItemRowCellClose(Appendable writer, Map<String,
Object> context, ModelForm modelForm,
+ ModelFormField modelFormField) {
+ this.currentCell = null;
+ this.colIndex++;
+ }
+
+ @Override
+ public void renderFormatItemRowFormCellOpen(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatItemRowFormCellClose(Appendable writer,
Map<String, Object> context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatSingleWrapperOpen(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ this.sheet =
workbook.createSheet(WorkbookUtil.createSafeSheetName(modelForm.getName()));
+ this.rowIndex = 0;
+ this.colIndex = 0;
+ }
+
+ @Override
+ public void renderFormatSingleWrapperClose(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatFieldRowOpen(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ this.currentRow = sheet.createRow(rowIndex++);
+ this.colIndex = 0;
+ }
+
+ @Override
+ public void renderFormatFieldRowClose(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormatFieldRowTitleCellOpen(Appendable writer,
Map<String, Object> context, ModelFormField modelFormField) {
+ this.currentCell = currentRow.createCell(colIndex);
+ }
+
+ @Override
+ public void renderFormatFieldRowTitleCellClose(Appendable writer,
Map<String, Object> context, ModelFormField modelFormField) {
+ this.currentCell = null;
+ this.colIndex++;
+ }
+
+ @Override
+ public void renderFormatFieldRowSpacerCell(Appendable writer, Map<String,
Object> context, ModelFormField modelFormField) {
+ }
+
+ @Override
+ public void renderFormatFieldRowWidgetCellOpen(Appendable writer,
Map<String, Object> context, ModelFormField modelFormField,
+ int positions, int positionSpan, Integer nextPositionInRow) {
+ this.currentCell = currentRow.createCell(colIndex);
+ }
+
+ @Override
+ public void renderFormatFieldRowWidgetCellClose(Appendable writer,
Map<String, Object> context, ModelFormField modelFormField,
+ int positions, int positionSpan, Integer nextPositionInRow) {
+ this.currentCell = null;
+ this.colIndex++;
+ }
+
+ @Override
+ public void renderFormatEmptySpace(Appendable writer, Map<String, Object>
context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderDisplayField(Appendable writer, Map<String, Object>
context, ModelFormField.DisplayField displayField) {
+ String type = displayField.getType();
+ String rawValue = displayField.getModelFormField().getEntry(context);
+ String displayValue = displayField.getDescription(context);
+ if ("currency".equals(type) || "accounting-number".equals(type)) {
+ writeNumericOrFallback(rawValue, displayValue);
+ } else if ("date".equals(type)) {
+ writeDateOrFallback(rawValue, displayValue, true);
+ } else if ("date-time".equals(type)) {
+ writeDateOrFallback(rawValue, displayValue, false);
+ } else {
+ writeString(displayValue);
+ }
+ }
+
+ @Override
+ public void renderTextField(Appendable writer, Map<String, Object>
context, ModelFormField.TextField textField) {
+ String type = textField.getType();
+ String rawValue = textField.getModelFormField().getEntry(context);
+ if ("currency".equals(type) || "accounting-number".equals(type)) {
+ writeNumericOrFallback(rawValue, rawValue);
+ } else {
+ writeString(rawValue);
+ }
+ }
+
+ @Override
+ public void renderDateTimeField(Appendable writer, Map<String, Object>
context, ModelFormField.DateTimeField dateTimeField) {
+ String rawValue = dateTimeField.getModelFormField().getEntry(context);
+ writeDateOrFallback(rawValue, rawValue, dateTimeField.isDateType());
+ }
+
+ private void writeString(String value) {
+ Cell cell = obtainCell();
+ cell.setCellValue(value == null ? "" : value);
+ }
+
+ private void writeNumericOrFallback(String rawValue, String displayValue) {
+ BigDecimal numeric = parseNumeric(rawValue);
+ if (numeric != null) {
+ Cell cell = obtainCell();
+ cell.setCellValue(numeric.doubleValue());
+ cell.setCellStyle(numericStyle);
+ } else {
+ writeString(displayValue);
+ }
+ }
+
+ private void writeDateOrFallback(String rawValue, String displayValue,
boolean isDateType) {
+ java.util.Date parsed = parseDateTime(rawValue, isDateType);
+ if (parsed != null) {
+ Cell cell = obtainCell();
+ cell.setCellValue(parsed);
+ cell.setCellStyle(isDateType ? dateStyle : dateTimeStyle);
+ } else {
+ writeString(displayValue);
+ }
+ }
+
+ /**
+ * Returns the cell that a title/value write should land in. When an
explicit
+ * {@code renderFormat*CellOpen} call already created {@link
#currentCell}, that same cell is
+ * returned unchanged. Otherwise (a field rendered without a separate
column, the default when
+ * {@code separate-columns}/{@code separate-column} is not set) a cell is
created at the current
+ * column and the column index is advanced, so subsequent bare fields land
in the next column.
+ */
+ private Cell obtainCell() {
+ if (currentCell == null) {
+ return currentRow.createCell(colIndex++);
+ }
+ return currentCell;
+ }
+
+ private static BigDecimal parseNumeric(String rawValue) {
+ if (rawValue == null || rawValue.isEmpty()) {
+ return null;
+ }
+ try {
+ return new BigDecimal(rawValue.trim());
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ private static java.util.Date parseDateTime(String rawValue, boolean
isDateType) {
+ if (rawValue == null || rawValue.isEmpty()) {
+ return null;
+ }
+ try {
+ return isDateType ? java.sql.Date.valueOf(rawValue.trim()) :
Timestamp.valueOf(rawValue.trim());
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void renderDropDownField(Appendable writer, Map<String, Object>
context, ModelFormField.DropDownField dropDownField) {
+ writeString(dropDownField.getModelFormField().getEntry(context));
+ }
+
+ @Override
+ public void renderCheckField(Appendable writer, Map<String, Object>
context, ModelFormField.CheckField checkField) {
+ writeString(checkField.getModelFormField().getEntry(context));
+ }
+
+ @Override
+ public void renderRadioField(Appendable writer, Map<String, Object>
context, ModelFormField.RadioField radioField) {
+ writeString(radioField.getModelFormField().getEntry(context));
+ }
+
+ @Override
+ public void renderHiddenField(Appendable writer, Map<String, Object>
context, ModelFormField modelFormField, String value) {
+ }
+
+ @Override
+ public void renderHiddenField(Appendable writer, Map<String, Object>
context, ModelFormField.HiddenField hiddenField) {
+ }
+
+ @Override
+ public void renderIgnoredField(Appendable writer, Map<String, Object>
context, ModelFormField.IgnoredField ignoredField) {
+ }
+
+ @Override
+ public void renderSubmitField(Appendable writer, Map<String, Object>
context, ModelFormField.SubmitField submitField) {
+ }
+
+ @Override
+ public void renderResetField(Appendable writer, Map<String, Object>
context, ModelFormField.ResetField resetField) {
+ }
+
+ @Override
+ public void renderFormOpen(Appendable writer, Map<String, Object> context,
ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderFormClose(Appendable writer, Map<String, Object>
context, ModelForm modelForm) {
+ }
+
+ @Override
+ public void renderMultiFormClose(Appendable writer, Map<String, Object>
context, ModelForm modelForm) {
+ }
+
+ // Everything below has no place in a tabular data export (styling,
hyperlinks, lookups,
+ // find-fields, banners, tooltips, field groups, images) - matches the
CSV/XLS FTL macro
+ // libraries' own no-op behavior for the same methods.
+
+ @Override
+ public void renderHyperlinkField(Appendable writer, Map<String, Object>
context, ModelFormField.HyperlinkField hyperlinkField) {
+ }
+
+ @Override
+ public void renderMenuField(Appendable writer, Map<String, Object>
context, ModelFormField.MenuField menuField) {
+ }
+
+ @Override
+ public void renderTextareaField(Appendable writer, Map<String, Object>
context, ModelFormField.TextareaField textareaField) {
+ }
+
+ @Override
+ public void renderTextFindField(Appendable writer, Map<String, Object>
context, ModelFormField.TextFindField textField) {
+ }
+
+ @Override
+ public void renderDateFindField(Appendable writer, Map<String, Object>
context, ModelFormField.DateFindField textField) {
+ }
+
+ @Override
+ public void renderRangeFindField(Appendable writer, Map<String, Object>
context, ModelFormField.RangeFindField textField) {
+ }
+
+ @Override
+ public void renderDateRangePickerField(Appendable writer, Map<String,
Object> context,
+ ModelFormField.DateRangePickerField textField) {
+ }
+
+ @Override
+ public void renderLookupField(Appendable writer, Map<String, Object>
context, ModelFormField.LookupField textField) {
+ }
+
+ @Override
+ public void renderFileField(Appendable writer, Map<String, Object>
context, ModelFormField.FileField textField) {
+ }
+
+ @Override
+ public void renderPasswordField(Appendable writer, Map<String, Object>
context, ModelFormField.PasswordField textField) {
+ }
+
+ @Override
+ public void renderImageField(Appendable writer, Map<String, Object>
context, ModelFormField.ImageField textField) {
+ }
+
+ @Override
+ public void renderBanner(Appendable writer, Map<String, Object> context,
ModelForm.Banner banner) {
+ }
+
+ @Override
+ public void renderContainerFindField(Appendable writer, Map<String,
Object> context, ModelFormField.ContainerField containerField) {
+ }
+
+ @Override
+ public void renderFieldGroupOpen(Appendable writer, Map<String, Object>
context, ModelForm.FieldGroup fieldGroup) {
+ }
+
+ @Override
+ public void renderFieldGroupClose(Appendable writer, Map<String, Object>
context, ModelForm.FieldGroup fieldGroup) {
+ }
+
+ @Override
+ public void renderEmptyFormDataMessage(Appendable writer, Map<String,
Object> context, ModelForm modelForm) {
+ }
+}
diff --git
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/xlsx/XlsxScreenStringRenderer.java
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/xlsx/XlsxScreenStringRenderer.java
new file mode 100644
index 0000000000..173c115ba6
--- /dev/null
+++
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/xlsx/XlsxScreenStringRenderer.java
@@ -0,0 +1,173 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.widget.renderer.xlsx;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.ofbiz.base.util.GeneralException;
+import org.apache.ofbiz.entity.GenericValue;
+import org.apache.ofbiz.widget.model.ModelScreen;
+import org.apache.ofbiz.widget.model.ModelScreenWidget;
+import org.apache.ofbiz.widget.renderer.ScreenStringRenderer;
+
+/**
+ * No-op {@code ScreenStringRenderer}: lets {@code ScreenRenderer} walk a
screen's
+ * container/section/label tree without error while an xlsx export is in
progress.
+ * None of this screen-level chrome is part of the tabular data export - only
the
+ * form/grid content reached via {@code XlsxFormRenderer} matters.
+ */
+public class XlsxScreenStringRenderer implements ScreenStringRenderer {
+
+ @Override
+ public String getRendererName() {
+ return "XlsxScreenStringRenderer";
+ }
+
+ @Override
+ public void renderBegin(Appendable writer, Map<String, Object> context)
throws IOException {
+ }
+
+ @Override
+ public void renderEnd(Appendable writer, Map<String, Object> context)
throws IOException {
+ }
+
+ @Override
+ public void renderScreenBegin(Appendable writer, Map<String, Object>
context, ModelScreen modelScreen) throws IOException {
+ }
+
+ @Override
+ public void renderScreenEnd(Appendable writer, Map<String, Object>
context, ModelScreen modelScreen) throws IOException {
+ }
+
+ @Override
+ public void renderSectionBegin(Appendable writer, Map<String, Object>
context, ModelScreenWidget.Section section) throws IOException {
+ }
+
+ @Override
+ public void renderSectionEnd(Appendable writer, Map<String, Object>
context, ModelScreenWidget.Section section) throws IOException {
+ }
+
+ @Override
+ public void renderColumnContainer(Appendable writer, Map<String, Object>
context, ModelScreenWidget.ColumnContainer columnContainer)
+ throws IOException {
+ }
+
+ @Override
+ public void renderContainerBegin(Appendable writer, Map<String, Object>
context, ModelScreenWidget.Container container) throws IOException {
+ }
+
+ @Override
+ public void renderContainerEnd(Appendable writer, Map<String, Object>
context, ModelScreenWidget.Container container) throws IOException {
+ }
+
+ @Override
+ public void renderContentBegin(Appendable writer, Map<String, Object>
context, ModelScreenWidget.Content content) throws IOException {
+ }
+
+ @Override
+ public void renderContentBody(Appendable writer, Map<String, Object>
context, ModelScreenWidget.Content content) throws IOException {
+ }
+
+ @Override
+ public void renderContentEnd(Appendable writer, Map<String, Object>
context, ModelScreenWidget.Content content) throws IOException {
+ }
+
+ @Override
+ public void renderSubContentBegin(Appendable writer, Map<String, Object>
context, ModelScreenWidget.SubContent content) throws IOException {
+ }
+
+ @Override
+ public void renderSubContentBody(Appendable writer, Map<String, Object>
context, ModelScreenWidget.SubContent content) throws IOException {
+ }
+
+ @Override
+ public void renderSubContentEnd(Appendable writer, Map<String, Object>
context, ModelScreenWidget.SubContent content) throws IOException {
+ }
+
+ @Override
+ public void renderHorizontalSeparator(Appendable writer, Map<String,
Object> context, ModelScreenWidget.HorizontalSeparator separator)
+ throws IOException {
+ }
+
+ @Override
+ public void renderLabel(Appendable writer, Map<String, Object> context,
ModelScreenWidget.Label label) throws IOException {
+ }
+
+ @Override
+ public void renderLink(Appendable writer, Map<String, Object> context,
ModelScreenWidget.ScreenLink link) throws IOException {
+ }
+
+ @Override
+ public void renderImage(Appendable writer, Map<String, Object> context,
ModelScreenWidget.ScreenImage image) throws IOException {
+ }
+
+ @Override
+ public void renderContentFrame(Appendable writer, Map<String, Object>
context, ModelScreenWidget.Content content) throws IOException {
+ }
+
+ @Override
+ public void renderScreenletBegin(Appendable writer, Map<String, Object>
context, boolean collapsed, ModelScreenWidget.Screenlet screenlet)
+ throws IOException {
+ }
+
+ @Override
+ public void renderScreenletSubWidget(Appendable writer, Map<String,
Object> context, ModelScreenWidget subWidget,
+ ModelScreenWidget.Screenlet screenlet) throws GeneralException,
IOException {
+ }
+
+ @Override
+ public void renderScreenletEnd(Appendable writer, Map<String, Object>
context, ModelScreenWidget.Screenlet screenlet) throws IOException {
+ }
+
+ @Override
+ public void renderPortalPageBegin(Appendable writer, Map<String, Object>
context, ModelScreenWidget.PortalPage portalPage)
+ throws GeneralException, IOException {
+ }
+
+ @Override
+ public void renderPortalPageEnd(Appendable writer, Map<String, Object>
context, ModelScreenWidget.PortalPage portalPage)
+ throws GeneralException, IOException {
+ }
+
+ @Override
+ public void renderPortalPageColumnBegin(Appendable writer, Map<String,
Object> context, ModelScreenWidget.PortalPage portalPage,
+ GenericValue portalPageColumn) throws
GeneralException, IOException {
+ }
+
+ @Override
+ public void renderPortalPageColumnEnd(Appendable writer, Map<String,
Object> context, ModelScreenWidget.PortalPage portalPage,
+ GenericValue portalPageColumn) throws
GeneralException, IOException {
+ }
+
+ @Override
+ public void renderPortalPagePortletBegin(Appendable writer, Map<String,
Object> context, ModelScreenWidget.PortalPage portalPage,
+ GenericValue portalPortlet) throws
GeneralException, IOException {
+ }
+
+ @Override
+ public void renderPortalPagePortletBody(Appendable writer, Map<String,
Object> context, ModelScreenWidget.PortalPage portalPage,
+ GenericValue portalPortlet) throws
GeneralException, IOException {
+ }
+
+ @Override
+ public void renderPortalPagePortletEnd(Appendable writer, Map<String,
Object> context, ModelScreenWidget.PortalPage portalPage,
+ GenericValue portalPortlet) throws
GeneralException, IOException {
+ }
+}
diff --git
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/xlsx/XlsxFormRendererTest.java
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/xlsx/XlsxFormRendererTest.java
new file mode 100644
index 0000000000..7427caf93c
--- /dev/null
+++
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/xlsx/XlsxFormRendererTest.java
@@ -0,0 +1,232 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.widget.renderer.xlsx;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.sql.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.ofbiz.widget.model.ModelForm;
+import org.apache.ofbiz.widget.model.ModelFormField;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.junit.jupiter.api.Test;
+
+final class XlsxFormRendererTest {
+
+ private final Map<String, Object> context = new HashMap<>();
+ private final XSSFWorkbook workbook = new XSSFWorkbook();
+ private final XlsxFormRenderer renderer = new XlsxFormRenderer(workbook);
+
+ @Test
+ void writesHeaderRowThenPlainTextItemRow() {
+ ModelForm modelForm = mock(ModelForm.class);
+ when(modelForm.getName()).thenReturn("TestForm");
+
+ ModelFormField titleField = mock(ModelFormField.class,
RETURNS_DEEP_STUBS);
+ when(titleField.getTitle(context)).thenReturn("Product Name");
+
+ ModelFormField.TextField textField =
mock(ModelFormField.TextField.class, RETURNS_DEEP_STUBS);
+ when(textField.getType()).thenReturn("");
+
when(textField.getModelFormField().getEntry(context)).thenReturn("Widget");
+
+ renderer.renderFormatListWrapperOpen(null, context, modelForm);
+ renderer.renderFormatHeaderRowOpen(null, context, modelForm);
+ renderer.renderFormatHeaderRowCellOpen(null, context, modelForm,
titleField, 1);
+ renderer.renderFieldTitle(null, context, titleField);
+ renderer.renderFormatHeaderRowCellClose(null, context, modelForm,
titleField);
+ renderer.renderFormatHeaderRowClose(null, context, modelForm);
+
+ renderer.renderFormatItemRowOpen(null, context, modelForm);
+ renderer.renderFormatItemRowCellOpen(null, context, modelForm,
titleField, 1);
+ renderer.renderTextField(null, context, textField);
+ renderer.renderFormatItemRowCellClose(null, context, modelForm,
titleField);
+ renderer.renderFormatItemRowClose(null, context, modelForm);
+ renderer.renderFormatListWrapperClose(null, context, modelForm);
+
+ Sheet sheet = renderer.getSheet();
+ assertThat(sheet.getSheetName(), equalTo("TestForm"));
+ assertThat(sheet.getRow(0).getCell(0).getStringCellValue(),
equalTo("Product Name"));
+ assertThat(sheet.getRow(1).getCell(0).getStringCellValue(),
equalTo("Widget"));
+ }
+
+ @Test
+ void writesCurrencyDisplayFieldAsNumericCell() {
+ ModelForm modelForm = mock(ModelForm.class);
+ when(modelForm.getName()).thenReturn("TestForm");
+
+ ModelFormField.DisplayField priceField =
mock(ModelFormField.DisplayField.class, RETURNS_DEEP_STUBS);
+ when(priceField.getType()).thenReturn("currency");
+
when(priceField.getModelFormField().getEntry(context)).thenReturn("19.99");
+ when(priceField.getDescription(context)).thenReturn("$19.99");
+
+ renderer.renderFormatListWrapperOpen(null, context, modelForm);
+ renderer.renderFormatItemRowOpen(null, context, modelForm);
+ renderer.renderFormatItemRowCellOpen(null, context, modelForm,
priceField.getModelFormField(), 1);
+ renderer.renderDisplayField(null, context, priceField);
+ renderer.renderFormatItemRowCellClose(null, context, modelForm,
priceField.getModelFormField());
+
+ Cell cell = renderer.getSheet().getRow(0).getCell(0);
+ assertThat(cell.getCellType(), equalTo(CellType.NUMERIC));
+ assertThat(cell.getNumericCellValue(), equalTo(19.99));
+ }
+
+ @Test
+ void fallsBackToStringWhenCurrencyValueIsUnparseable() {
+ ModelForm modelForm = mock(ModelForm.class);
+ when(modelForm.getName()).thenReturn("TestForm");
+
+ ModelFormField.DisplayField priceField =
mock(ModelFormField.DisplayField.class, RETURNS_DEEP_STUBS);
+ when(priceField.getType()).thenReturn("currency");
+ when(priceField.getModelFormField().getEntry(context)).thenReturn("");
+ when(priceField.getDescription(context)).thenReturn("N/A");
+
+ renderer.renderFormatListWrapperOpen(null, context, modelForm);
+ renderer.renderFormatItemRowOpen(null, context, modelForm);
+ renderer.renderFormatItemRowCellOpen(null, context, modelForm,
priceField.getModelFormField(), 1);
+ renderer.renderDisplayField(null, context, priceField);
+ renderer.renderFormatItemRowCellClose(null, context, modelForm,
priceField.getModelFormField());
+
+ Cell cell = renderer.getSheet().getRow(0).getCell(0);
+ assertThat(cell.getCellType(), equalTo(CellType.STRING));
+ assertThat(cell.getStringCellValue(), equalTo("N/A"));
+ }
+
+ @Test
+ void writesDateDisplayFieldAsRealDateCell() {
+ ModelForm modelForm = mock(ModelForm.class);
+ when(modelForm.getName()).thenReturn("TestForm");
+
+ ModelFormField.DisplayField shipDateField =
mock(ModelFormField.DisplayField.class, RETURNS_DEEP_STUBS);
+ when(shipDateField.getType()).thenReturn("date");
+
when(shipDateField.getModelFormField().getEntry(context)).thenReturn("2026-01-15");
+ when(shipDateField.getDescription(context)).thenReturn("01/15/2026");
+
+ renderer.renderFormatListWrapperOpen(null, context, modelForm);
+ renderer.renderFormatItemRowOpen(null, context, modelForm);
+ renderer.renderFormatItemRowCellOpen(null, context, modelForm,
shipDateField.getModelFormField(), 1);
+ renderer.renderDisplayField(null, context, shipDateField);
+ renderer.renderFormatItemRowCellClose(null, context, modelForm,
shipDateField.getModelFormField());
+
+ Cell cell = renderer.getSheet().getRow(0).getCell(0);
+ assertThat(DateUtil.isCellDateFormatted(cell), equalTo(true));
+ assertThat(cell.getDateCellValue(),
equalTo(Date.valueOf("2026-01-15")));
+ }
+
+ @Test
+ void writesDateTimeFieldAsRealDateCell() {
+ ModelForm modelForm = mock(ModelForm.class);
+ when(modelForm.getName()).thenReturn("TestForm");
+
+ ModelFormField.DateTimeField orderDateField =
mock(ModelFormField.DateTimeField.class, RETURNS_DEEP_STUBS);
+ when(orderDateField.isDateType()).thenReturn(false);
+ when(orderDateField.isTimeType()).thenReturn(false);
+
when(orderDateField.getModelFormField().getEntry(context)).thenReturn("2026-01-15
10:30:00.0");
+
+ renderer.renderFormatListWrapperOpen(null, context, modelForm);
+ renderer.renderFormatItemRowOpen(null, context, modelForm);
+ renderer.renderFormatItemRowCellOpen(null, context, modelForm,
orderDateField.getModelFormField(), 1);
+ renderer.renderDateTimeField(null, context, orderDateField);
+ renderer.renderFormatItemRowCellClose(null, context, modelForm,
orderDateField.getModelFormField());
+
+ Cell cell = renderer.getSheet().getRow(0).getCell(0);
+ assertThat(DateUtil.isCellDateFormatted(cell), equalTo(true));
+ assertThat(cell.getDateCellValue(),
equalTo(java.sql.Timestamp.valueOf("2026-01-15 10:30:00.0")));
+ }
+
+ @Test
+ void writesDropDownFieldRawValueAsString() {
+ ModelForm modelForm = mock(ModelForm.class);
+ when(modelForm.getName()).thenReturn("TestForm");
+
+ ModelFormField.DropDownField statusField =
mock(ModelFormField.DropDownField.class, RETURNS_DEEP_STUBS);
+
when(statusField.getModelFormField().getEntry(context)).thenReturn("ORDER_APPROVED");
+
+ renderer.renderFormatListWrapperOpen(null, context, modelForm);
+ renderer.renderFormatItemRowOpen(null, context, modelForm);
+ renderer.renderFormatItemRowCellOpen(null, context, modelForm,
statusField.getModelFormField(), 1);
+ renderer.renderDropDownField(null, context, statusField);
+ renderer.renderFormatItemRowCellClose(null, context, modelForm,
statusField.getModelFormField());
+
+ Cell cell = renderer.getSheet().getRow(0).getCell(0);
+ assertThat(cell.getStringCellValue(), equalTo("ORDER_APPROVED"));
+ }
+
+ @Test
+ void writesSingleFormAsTitleValueRows() {
+ ModelForm modelForm = mock(ModelForm.class);
+ when(modelForm.getName()).thenReturn("DetailForm");
+
+ ModelFormField nameField = mock(ModelFormField.class,
RETURNS_DEEP_STUBS);
+ when(nameField.getTitle(context)).thenReturn("Order Id");
+
+ ModelFormField.TextField valueField =
mock(ModelFormField.TextField.class, RETURNS_DEEP_STUBS);
+ when(valueField.getType()).thenReturn("");
+
when(valueField.getModelFormField().getEntry(context)).thenReturn("WS10000");
+
+ renderer.renderFormatSingleWrapperOpen(null, context, modelForm);
+ renderer.renderFormatFieldRowOpen(null, context, modelForm);
+ renderer.renderFormatFieldRowTitleCellOpen(null, context, nameField);
+ renderer.renderSingleFormFieldTitle(null, context, nameField);
+ renderer.renderFormatFieldRowTitleCellClose(null, context, nameField);
+ renderer.renderFormatFieldRowWidgetCellOpen(null, context, nameField,
1, 1, null);
+ renderer.renderTextField(null, context, valueField);
+ renderer.renderFormatFieldRowWidgetCellClose(null, context, nameField,
1, 1, null);
+ renderer.renderFormatFieldRowClose(null, context, modelForm);
+
+ Sheet sheet = renderer.getSheet();
+ assertThat(sheet.getSheetName(), equalTo("DetailForm"));
+ assertThat(sheet.getRow(0).getCell(0).getStringCellValue(),
equalTo("Order Id"));
+ assertThat(sheet.getRow(0).getCell(1).getStringCellValue(),
equalTo("WS10000"));
+ }
+
+ @Test
+ void writesBareHeaderTitlesWithoutExplicitCellOpenIntoDistinctColumns() {
+ ModelForm modelForm = mock(ModelForm.class);
+ when(modelForm.getName()).thenReturn("TestForm");
+
+ ModelFormField firstField = mock(ModelFormField.class,
RETURNS_DEEP_STUBS);
+ when(firstField.getTitle(context)).thenReturn("Product Name");
+
+ ModelFormField secondField = mock(ModelFormField.class,
RETURNS_DEEP_STUBS);
+ when(secondField.getTitle(context)).thenReturn("Quantity");
+
+ renderer.renderFormatListWrapperOpen(null, context, modelForm);
+ renderer.renderFormatHeaderRowOpen(null, context, modelForm);
+ // No renderFormatHeaderRowCellOpen calls:
modelForm.getSeparateColumns() and
+ // modelFormField.getSeparateColumn() are both false, the default
case, so FormRenderer
+ // invokes renderFieldTitle directly for each field with no preceding
cell-open.
+ renderer.renderFieldTitle(null, context, firstField);
+ renderer.renderFieldTitle(null, context, secondField);
+ renderer.renderFormatHeaderRowClose(null, context, modelForm);
+
+ Sheet sheet = renderer.getSheet();
+ assertThat(sheet.getRow(0).getCell(0).getStringCellValue(),
equalTo("Product Name"));
+ assertThat(sheet.getRow(0).getCell(1).getStringCellValue(),
equalTo("Quantity"));
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f390c46dde..7a4a31963d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -148,6 +148,7 @@ log4j-api = { module =
"org.apache.logging.log4j:log4j-api", version.ref = "log4
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref =
"log4j" }
log4j-layout-template-json = { module =
"org.apache.logging.log4j:log4j-layout-template-json", version.ref = "log4j" }
poi = { module = "org.apache.poi:poi", version.ref = "poi" }
+poi-ooxml = { module = "org.apache.poi:poi-ooxml", version.ref = "poi" }
pdfbox-core = { module = "org.apache.pdfbox:pdfbox", version.ref = "pdfbox" }
pdfbox-io = { module = "org.apache.pdfbox:pdfbox-io", version.ref = "pdfbox" }
shiro-core = { module = "org.apache.shiro:shiro-core", version.ref =
"shiro-core" }