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

commit cc6d9d5d05c02f1b7e530121b76e22def767e0da
Author: Mridul Pathak <[email protected]>
AuthorDate: Fri Oct 16 17:02:48 2026 +0530

    Improved: MacroFormRenderer builds the find, lookup, pagination, file, 
password, image and banner macro calls with RenderableFtlFormElementsBuilder 
(OFBIZ-13681)
    
    MacroFormRenderer built the macro calls of the text find, range find, 
lookup, file, password and image fields, of the pagination and of the banner as 
text and compiled them. RenderableFtlFormElementsBuilder now creates these 
macro calls, so that FtlWriter passes their values as data: the target 
parameters of the lookup field are a list instead of a hash literal of quoted 
strings. The macro signatures and the output are unchanged, except that values 
containing a quote, a backslash or a [...]
---
 .../widget/renderer/macro/MacroFormRenderer.java   | 820 +--------------------
 .../macro/RenderableFtlFormElementsBuilder.java    | 469 ++++++++++++
 .../renderer/macro/MacroFormRendererTest.java      | 196 ++---
 .../RenderableFtlFormElementsBuilderTest.java      | 318 ++++++++
 4 files changed, 853 insertions(+), 950 deletions(-)

diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
index c3f8144f3e..94ee7be6e6 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
@@ -21,28 +21,22 @@ package org.apache.ofbiz.widget.renderer.macro;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.io.UnsupportedEncodingException;
-import java.util.HashSet;
 import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Set;
 
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import jakarta.servlet.http.HttpSession;
 
 import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.UtilCodec;
 import org.apache.ofbiz.base.util.UtilGenerics;
 import org.apache.ofbiz.base.util.UtilHttp;
-import org.apache.ofbiz.base.util.UtilMisc;
 import org.apache.ofbiz.base.util.UtilProperties;
 import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.base.util.string.FlexibleStringExpander;
 import org.apache.ofbiz.entity.Delegator;
-import org.apache.ofbiz.security.CsrfUtil;
 import org.apache.ofbiz.webapp.control.RequestHandler;
 import org.apache.ofbiz.webapp.taglib.ContentUrlTag;
 import org.apache.ofbiz.widget.WidgetWorker;
@@ -76,9 +70,7 @@ import org.apache.ofbiz.widget.model.ModelScreenWidget;
 import org.apache.ofbiz.widget.model.ModelSingleForm;
 import org.apache.ofbiz.widget.model.ModelWidget;
 import org.apache.ofbiz.widget.model.ThemeFactory;
-import org.apache.ofbiz.widget.renderer.FormRenderer;
 import org.apache.ofbiz.widget.renderer.FormStringRenderer;
-import org.apache.ofbiz.widget.renderer.Paginator;
 import org.apache.ofbiz.widget.renderer.UtilHelpText;
 import org.apache.ofbiz.widget.renderer.VisualTheme;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
@@ -209,8 +201,8 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
             throws IOException {
         this.request.setAttribute("image", 
hyperlinkField.getImageLocation(context));
         ModelFormField modelFormField = hyperlinkField.getModelFormField();
-        String encodedAlternate = encode(hyperlinkField.getAlternate(context), 
modelFormField, context);
-        String encodedImageTitle = 
encode(hyperlinkField.getImageTitle(context), modelFormField, context);
+        String encodedAlternate = 
encodeForMacro(hyperlinkField.getAlternate(context), modelFormField, context);
+        String encodedImageTitle = 
encodeForMacro(hyperlinkField.getImageTitle(context), modelFormField, context);
         this.request.setAttribute("alternate", encodedAlternate);
         this.request.setAttribute("imageTitle", encodedImageTitle);
         this.request.setAttribute("descriptionSize", hyperlinkField.getSize());
@@ -617,188 +609,14 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
 
     @Override
     public void renderTextFindField(Appendable writer, Map<String, Object> 
context, TextFindField textFindField) throws IOException {
-        ModelFormField modelFormField = textFindField.getModelFormField();
-        String defaultOption = textFindField.getDefaultOption(context);
-        String conditionGroup = modelFormField.getConditionGroup();
-        String className = "";
-        String alert = "false";
-        String opEquals = "";
-        String opBeginsWith = "";
-        String opContains = "";
-        String opIsEmpty = "";
-        String opNotEqual = "";
-        String name = modelFormField.getParameterName(context);
-        String size = Integer.toString(textFindField.getSize());
-        String maxlength = "";
-        String autocomplete = "";
-        if (UtilValidate.isNotEmpty(modelFormField.getWidgetStyle())) {
-            className = modelFormField.getWidgetStyle();
-            if (modelFormField.shouldBeRed(context)) {
-                alert = "true";
-            }
-        }
-        Locale locale = (Locale) context.get("locale");
-        if (!textFindField.getHideOptions()) {
-            opEquals = UtilProperties.getMessage("conditionalUiLabels", 
"equals", locale);
-            opBeginsWith = UtilProperties.getMessage("conditionalUiLabels", 
"begins_with", locale);
-            opContains = UtilProperties.getMessage("conditionalUiLabels", 
"contains", locale);
-            opIsEmpty = UtilProperties.getMessage("conditionalUiLabels", 
"is_empty", locale);
-            opNotEqual = UtilProperties.getMessage("conditionalUiLabels", 
"not_equal", locale);
-        }
-        String value = modelFormField.getEntry(context, 
textFindField.getDefaultValue(context));
-        if (value == null) {
-            value = "";
-        }
-        if (textFindField.getMaxlength() != null) {
-            maxlength = textFindField.getMaxlength().toString();
-        }
-        if (!textFindField.getClientAutocompleteField()) {
-            autocomplete = "off";
-        }
-        String titleStyle = "";
-        if (UtilValidate.isNotEmpty(modelFormField.getTitleStyle())) {
-            titleStyle = modelFormField.getTitleStyle();
-        }
-        String ignoreCase = UtilProperties.getMessage("conditionalUiLabels", 
"ignore_case", locale);
-        boolean ignCase = textFindField.getIgnoreCase(context);
-        boolean hideIgnoreCase = textFindField.getHideIgnoreCase();
-        String tabindex = modelFormField.getTabindex();
-        boolean disabled = modelFormField.getDisabled(context);
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderTextFindField ");
-        sr.append(" name=\"");
-        sr.append(name);
-        sr.append("\" value=\"");
-        sr.append(value);
-        sr.append("\" defaultOption=\"");
-        sr.append(defaultOption);
-        sr.append("\" opEquals=\"");
-        sr.append(opEquals);
-        sr.append("\" opBeginsWith=\"");
-        sr.append(opBeginsWith);
-        sr.append("\" opContains=\"");
-        sr.append(opContains);
-        sr.append("\" opIsEmpty=\"");
-        sr.append(opIsEmpty);
-        sr.append("\" opNotEqual=\"");
-        sr.append(opNotEqual);
-        sr.append("\" className=\"");
-        sr.append(className);
-        sr.append("\" alert=\"");
-        sr.append(alert);
-        sr.append("\" size=\"");
-        sr.append(size);
-        sr.append("\" maxlength=\"");
-        sr.append(maxlength);
-        sr.append("\" autocomplete=\"");
-        sr.append(autocomplete);
-        sr.append("\" titleStyle=\"");
-        sr.append(titleStyle);
-        sr.append("\" hideIgnoreCase=");
-        sr.append(Boolean.toString(hideIgnoreCase));
-        sr.append(" ignCase=");
-        sr.append(Boolean.toString(ignCase));
-        sr.append(" ignoreCase=\"");
-        sr.append(ignoreCase);
-        sr.append("\" tabindex=\"");
-        sr.append(tabindex);
-        sr.append("\" conditionGroup=\"");
-        sr.append(conditionGroup);
-        sr.append("\" disabled=");
-        sr.append(Boolean.toString(disabled));
-        sr.append(" />");
-        executeMacro(writer, sr.toString());
-        this.appendTooltip(writer, context, modelFormField);
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.textFindField(context, textFindField));
+        this.appendTooltip(writer, context, textFindField.getModelFormField());
     }
 
     @Override
     public void renderRangeFindField(Appendable writer, Map<String, Object> 
context, RangeFindField rangeFindField) throws IOException {
-        ModelFormField modelFormField = rangeFindField.getModelFormField();
-        Locale locale = (Locale) context.get("locale");
-        String opEquals = UtilProperties.getMessage("conditionalUiLabels", 
"equals", locale);
-        String opGreaterThan = 
UtilProperties.getMessage("conditionalUiLabels", "greater_than", locale);
-        String opGreaterThanEquals = 
UtilProperties.getMessage("conditionalUiLabels", "greater_than_equals", locale);
-        String opLessThan = UtilProperties.getMessage("conditionalUiLabels", 
"less_than", locale);
-        String opLessThanEquals = 
UtilProperties.getMessage("conditionalUiLabels", "less_than_equals", locale);
-        String conditionGroup = modelFormField.getConditionGroup();
-        String className = "";
-        String alert = "false";
-        if (UtilValidate.isNotEmpty(modelFormField.getWidgetStyle())) {
-            className = modelFormField.getWidgetStyle();
-            if (modelFormField.shouldBeRed(context)) {
-                alert = "true";
-            }
-        }
-        String name = modelFormField.getParameterName(context);
-        String size = Integer.toString(rangeFindField.getSize());
-        String value = modelFormField.getEntry(context, 
rangeFindField.getDefaultValue(context));
-        if (value == null) {
-            value = "";
-        }
-        Integer maxlength = rangeFindField.getMaxlength();
-        String autocomplete = "";
-
-        if (!rangeFindField.getClientAutocompleteField()) {
-            autocomplete = "off";
-        }
-        String titleStyle = modelFormField.getTitleStyle();
-
-        if (titleStyle == null) {
-            titleStyle = "";
-        }
-        String defaultOptionFrom = rangeFindField.getDefaultOptionFrom();
-        String value2 = modelFormField.getEntry(context);
-        if (value2 == null) {
-            value2 = "";
-        }
-        String defaultOptionThru = rangeFindField.getDefaultOptionThru();
-        String tabindex = modelFormField.getTabindex();
-        boolean disabled = modelFormField.getDisabled(context);
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderRangeFindField ");
-        sr.append(" className=\"");
-        sr.append(className);
-        sr.append("\" alert=\"");
-        sr.append(alert);
-        sr.append("\" name=\"");
-        sr.append(name);
-        sr.append("\" value=\"");
-        sr.append(value);
-        sr.append("\" size=\"");
-        sr.append(size);
-        sr.append("\" maxlength=\"");
-        if (maxlength != null) {
-            sr.append(Integer.toString(maxlength));
-        }
-        sr.append("\" autocomplete=\"");
-        sr.append(autocomplete);
-        sr.append("\" titleStyle=\"");
-        sr.append(titleStyle);
-        sr.append("\" defaultOptionFrom=\"");
-        sr.append(defaultOptionFrom);
-        sr.append("\" opEquals=\"");
-        sr.append(opEquals);
-        sr.append("\" opGreaterThan=\"");
-        sr.append(opGreaterThan);
-        sr.append("\" opGreaterThanEquals=\"");
-        sr.append(opGreaterThanEquals);
-        sr.append("\" opLessThan=\"");
-        sr.append(opLessThan);
-        sr.append("\" opLessThanEquals=\"");
-        sr.append(opLessThanEquals);
-        sr.append("\" value2=\"");
-        sr.append(value2);
-        sr.append("\" defaultOptionThru=\"");
-        sr.append(defaultOptionThru);
-        sr.append("\" conditionGroup=\"");
-        sr.append(conditionGroup);
-        sr.append("\" tabindex=\"");
-        sr.append(tabindex);
-        sr.append("\" disabled=");
-        sr.append(Boolean.toString(disabled));
-        sr.append(" />");
-        executeMacro(writer, sr.toString());
-        this.appendTooltip(writer, context, modelFormField);
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.rangeFindField(context, rangeFindField));
+        this.appendTooltip(writer, context, 
rangeFindField.getModelFormField());
     }
 
     @Override
@@ -819,560 +637,28 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
 
     @Override
     public void renderLookupField(Appendable writer, Map<String, Object> 
context, LookupField lookupField) throws IOException {
-        ModelFormField modelFormField = lookupField.getModelFormField();
-        String lookupFieldFormName = lookupField.getFormName(context);
-        String conditionGroup = modelFormField.getConditionGroup();
-        String className = "";
-        String alert = "false";
-        if (UtilValidate.isNotEmpty(modelFormField.getWidgetStyle())) {
-            className = modelFormField.getWidgetStyle();
-            if (modelFormField.shouldBeRed(context)) {
-                alert = "true";
-            }
-        }
-        //check for required field style on single forms
-        if (shouldApplyRequiredField(modelFormField)) {
-            String requiredStyle = modelFormField.getRequiredFieldStyle();
-            if (UtilValidate.isEmpty(requiredStyle)) {
-                requiredStyle = "required";
-            }
-            if (UtilValidate.isEmpty(className)) {
-                className = requiredStyle;
-            } else {
-                className = requiredStyle + " " + className;
-            }
-        }
-        String name = modelFormField.getParameterName(context);
-        String value = modelFormField.getEntry(context, 
lookupField.getDefaultValue(context));
-        if (value == null) {
-            value = "";
-        }
-        String size = Integer.toString(lookupField.getSize());
-        Integer maxlength = lookupField.getMaxlength();
-        String id = modelFormField.getCurrentContainerId(context);
-        List<ModelForm.UpdateArea> updateAreas = 
modelFormField.getOnChangeUpdateAreas();
-        //add default ajax auto completer to all lookup fields
-        if (UtilValidate.isEmpty(updateAreas) && 
UtilValidate.isNotEmpty(lookupFieldFormName)) {
-            String autoCompleterTarget = null;
-            if (lookupFieldFormName.indexOf('?') == -1) {
-                autoCompleterTarget = lookupFieldFormName + "?";
-            } else {
-                autoCompleterTarget = lookupFieldFormName + "&amp;amp;";
-            }
-            autoCompleterTarget = autoCompleterTarget + "ajaxLookup=Y";
-            updateAreas = new LinkedList<>();
-            updateAreas.add(ModelForm.UpdateArea.literal("change", id, 
autoCompleterTarget));
-        }
-        boolean ajaxEnabled = UtilValidate.isNotEmpty(updateAreas) && 
this.javaScriptEnabled;
-        String autocomplete = "";
-        if (!lookupField.getClientAutocompleteField() || !ajaxEnabled) {
-            autocomplete = "off";
-        }
-        String event = modelFormField.getEvent();
-        String action = modelFormField.getAction(context);
-        boolean readonly = lookupField.getReadonly();
-        // add lookup pop-up button
-        String descriptionFieldName = lookupField.getDescriptionFieldName();
-        ModelForm modelForm = modelFormField.getModelForm();
-        String formName = modelFormField.getParentFormName();
-        if (UtilValidate.isEmpty(formName)) {
-            formName = FormRenderer.getCurrentFormName(modelForm, context);
-        }
-        StringBuilder targetParameterIter = new StringBuilder();
-        StringBuilder imgSrc = new StringBuilder();
-        // FIXME: refactor using the StringUtils methods
-        List<String> targetParameterList = 
lookupField.getTargetParameterList(context);
-        targetParameterIter.append("[");
-        for (String targetParameter : targetParameterList) {
-            if (targetParameterIter.length() > 1) {
-                targetParameterIter.append(",");
-            }
-            targetParameterIter.append("'");
-            targetParameterIter.append(targetParameter);
-            targetParameterIter.append("'");
-        }
-        targetParameterIter.append("]");
-        this.appendContentUrl(imgSrc, "/images/fieldlookup.gif");
-        String ajaxUrl = "";
-        if (ajaxEnabled) {
-            ajaxUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null, 
modelForm, "", context);
-        }
-        String lookupPresentation = lookupField.getLookupPresentation();
-        if (UtilValidate.isEmpty(lookupPresentation)) {
-            lookupPresentation = "";
-        }
-        String lookupHeight = lookupField.getLookupHeight();
-        String lookupWidth = lookupField.getLookupWidth();
-        String lookupPosition = lookupField.getLookupPosition();
-        String fadeBackground = lookupField.getFadeBackground();
-        if (UtilValidate.isEmpty(fadeBackground)) {
-            fadeBackground = "false";
-        }
-        Boolean isInitiallyCollapsed = lookupField.getInitiallyCollapsed();
-        String clearText = "";
-        Map<String, Object> uiLabelMap = 
UtilGenerics.cast(context.get("uiLabelMap"));
-        if (uiLabelMap != null) {
-            clearText = (String) uiLabelMap.get("CommonClear");
-        } else {
-            Debug.logWarning("Could not find uiLabelMap in context", MODULE);
-        }
-        Boolean showDescription = lookupField.getShowDescription();
-        if (showDescription == null) {
-            showDescription = 
"Y".equals(visualTheme.getModelTheme().getLookupShowDescription());
-        }
-        // lastViewName, used by lookup to remember the real last view name; 
read only from session (set by RequestHandler) to prevent user input
-        String lastViewName = (String) 
request.getSession().getAttribute("_LAST_VIEW_NAME_");
-        if (UtilValidate.isEmpty(lastViewName)) {
-            lastViewName = "";
-        }
-        lastViewName = UtilHttp.getEncodedParameter(lastViewName);
-        String tabindex = modelFormField.getTabindex();
-        boolean disabled = modelFormField.getDisabled(context);
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderLookupField ");
-        sr.append(" className=\"");
-        sr.append(className);
-        sr.append("\" alert=\"");
-        sr.append(alert);
-        sr.append("\" name=\"");
-        sr.append(name);
-        sr.append("\" value=\"");
-        sr.append(value);
-        sr.append("\" size=\"");
-        sr.append(size);
-        sr.append("\" maxlength=\"");
-        sr.append((maxlength != null ? Integer.toString(maxlength) : ""));
-        sr.append("\" id=\"");
-        sr.append(id);
-        sr.append("\" event=\"");
-        if (event != null) {
-            sr.append(event);
-        }
-        sr.append("\" action=\"");
-        if (action != null) {
-            sr.append(action);
-        }
-        sr.append("\" readonly=");
-        sr.append(Boolean.toString(readonly));
-        sr.append(" autocomplete=\"");
-        sr.append(autocomplete);
-        sr.append("\" descriptionFieldName=\"");
-        sr.append(descriptionFieldName);
-        sr.append("\" formName=\"");
-        sr.append(formName);
-        sr.append("\" fieldFormName=\"");
-        sr.append(lookupFieldFormName);
-        sr.append("\" targetParameterIter=");
-        sr.append(targetParameterIter.toString());
-        sr.append(" imgSrc=\"");
-        sr.append(imgSrc.toString());
-        sr.append("\" ajaxUrl=\"");
-        sr.append(ajaxUrl);
-        sr.append("\" ajaxEnabled=");
-        sr.append(Boolean.toString(ajaxEnabled));
-        sr.append(" presentation=\"");
-        sr.append(lookupPresentation);
-        if (UtilValidate.isNotEmpty(lookupHeight)) {
-            sr.append("\" height=\"");
-            sr.append(lookupHeight);
-        }
-        if (UtilValidate.isNotEmpty(lookupWidth)) {
-            sr.append("\" width=\"");
-            sr.append(lookupWidth);
-        }
-        if (UtilValidate.isNotEmpty(lookupPosition)) {
-            sr.append("\" position=\"");
-            sr.append(lookupPosition);
-        }
-        sr.append("\" fadeBackground=\"");
-        sr.append(fadeBackground);
-        sr.append("\" clearText=\"");
-        sr.append(clearText);
-        sr.append("\" showDescription=\"");
-        sr.append(Boolean.toString(showDescription));
-        sr.append("\" initiallyCollapsed=\"");
-        sr.append(Boolean.toString(isInitiallyCollapsed));
-        sr.append("\" lastViewName=\"");
-        sr.append(lastViewName);
-        sr.append("\" conditionGroup=\"");
-        sr.append(conditionGroup);
-        sr.append("\" tabindex=\"");
-        sr.append(tabindex);
-        sr.append("\" disabled=");
-        sr.append(Boolean.toString(disabled));
-        sr.append(" delegatorName=\"");
-        sr.append(((HttpSession) 
context.get("session")).getAttribute("delegatorName").toString());
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        final ModelFormField modelFormField = lookupField.getModelFormField();
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.lookupField(context, lookupField, 
this.javaScriptEnabled));
         this.addAsterisks(writer, context, modelFormField);
         this.makeHyperlinkString(writer, lookupField.getSubHyperlink(), 
context);
         this.appendTooltip(writer, context, modelFormField);
     }
 
     public void renderNextPrev(Appendable writer, Map<String, Object> context, 
ModelForm modelForm) {
-        boolean ajaxEnabled = false;
-        List<ModelForm.UpdateArea> updateAreas = 
modelForm.getOnPaginateUpdateAreas();
-        String targetService = modelForm.getPaginateTarget(context);
-        if (this.javaScriptEnabled) {
-            if (UtilValidate.isNotEmpty(updateAreas)) {
-                ajaxEnabled = true;
-            }
-        }
-        if (targetService == null) {
-            targetService = "${targetService}";
-        }
-        if (UtilValidate.isEmpty(targetService) && updateAreas == null) {
-            Debug.logWarning("Cannot paginate because TargetService is empty 
for the form: " + modelForm.getName(), MODULE);
-            return;
-        }
-        // get the parameterized pagination index and size fields
-        int paginatorNumber = WidgetWorker.getPaginatorNumber(context);
-        String viewIndexParam = modelForm.getMultiPaginateIndexField(context);
-        String viewSizeParam = modelForm.getMultiPaginateSizeField(context);
-        int viewIndex = Paginator.getViewIndex(modelForm, context);
-        int viewSize = Paginator.getViewSize(modelForm, context);
-        int listSize = Paginator.getListSize(context);
-        int lowIndex = Paginator.getLowIndex(context);
-        int highIndex = Paginator.getHighIndex(context);
-        int actualPageSize = Paginator.getActualPageSize(context);
-        // needed for the "Page" and "rows" labels
-        Map<String, String> uiLabelMap = 
UtilGenerics.cast(context.get("uiLabelMap"));
-        String pageLabel = "";
-        String commonDisplaying = "";
-        if (uiLabelMap == null) {
-            Debug.logWarning("Could not find uiLabelMap in context", MODULE);
-        } else {
-            pageLabel = uiLabelMap.get("CommonPage");
-            Map<String, Integer> messageMap = UtilMisc.toMap("lowCount", 
lowIndex + 1, "highCount", lowIndex + actualPageSize, "total", listSize);
-            commonDisplaying = UtilProperties.getMessage("CommonUiLabels", 
"CommonDisplaying", messageMap, (Locale) context.get("locale"));
-        }
-        // for legacy support, the viewSizeParam is VIEW_SIZE and 
viewIndexParam is VIEW_INDEX when the fields are "viewSize" and "viewIndex"
-        if (("viewIndex" + "_" + paginatorNumber).equals(viewIndexParam)) {
-            viewIndexParam = "VIEW_INDEX" + "_" + paginatorNumber;
-        }
-        if (("viewSize" + "_" + paginatorNumber).equals(viewSizeParam)) {
-            viewSizeParam = "VIEW_SIZE" + "_" + paginatorNumber;
-        }
-        String str = (String) context.get("_QBESTRING_");
-
-        // refresh any csrf token in the query string for pagination
-        String tokenValue = CsrfUtil.generateTokenForNonAjax(request, 
targetService);
-        str = CsrfUtil.addOrUpdateTokenInQueryString(str, tokenValue);
-
-        // strip legacy viewIndex/viewSize params from the query string
-        String queryString = UtilHttp.stripViewParamsFromQueryString(str, "" + 
paginatorNumber);
-        // strip parameterized index/size params from the query string
-        Set<String> paramNames = new HashSet<>();
-        paramNames.add(viewIndexParam);
-        paramNames.add(viewSizeParam);
-        queryString = UtilHttp.stripNamedParamsFromQueryString(queryString, 
paramNames);
-        String anchor = "";
-        String paginateAnchor = modelForm.getPaginateTargetAnchor();
-        if (UtilValidate.isNotEmpty(paginateAnchor)) {
-            anchor = "#" + paginateAnchor;
-        }
-        // Create separate url path String and request parameters String,
-        // add viewIndex/viewSize parameters to request parameter String
-        String urlPath = UtilHttp.removeQueryStringFromTarget(targetService);
-        String prepLinkText = UtilHttp.getQueryStringFromTarget(targetService);
-        String prepLinkSizeText;
-        if (UtilValidate.isNotEmpty(queryString)) {
-            queryString = UtilHttp.encodeAmpersands(queryString);
-        }
-        if (prepLinkText.indexOf('?') < 0) {
-            prepLinkText += "?";
-        } else if (!prepLinkText.endsWith("?")) {
-            prepLinkText += "&amp;";
-        }
-        if (UtilValidate.isNotEmpty(queryString) && 
!"null".equals(queryString)) {
-            prepLinkText += queryString + "&amp;";
-        }
-        prepLinkSizeText = prepLinkText + viewSizeParam + "='+this.value+'" + 
"&amp;" + viewIndexParam + "=0";
-        prepLinkText += viewSizeParam + "=" + viewSize + "&amp;" + 
viewIndexParam + "=";
-        if (ajaxEnabled) {
-            // Prepare params for prototype.js
-            prepLinkText = prepLinkText.replace("?", "");
-            prepLinkText = prepLinkText.replace("&amp;", "&");
-        }
-        String linkText;
-        String paginateStyle = modelForm.getPaginateStyle();
-        String paginateFirstStyle = modelForm.getPaginateFirstStyle();
-        String paginateFirstLabel = modelForm.getPaginateFirstLabel(context);
-        String firstUrl = "";
-        String ajaxFirstUrl = "";
-        String paginatePreviousStyle = modelForm.getPaginatePreviousStyle();
-        String paginatePreviousLabel = 
modelForm.getPaginatePreviousLabel(context);
-        String previousUrl = "";
-        String ajaxPreviousUrl = "";
-        String selectUrl = "";
-        String ajaxSelectUrl = "";
-        String paginateViewSizeLabel = 
modelForm.getPaginateViewSizeLabel(context);
-        String selectSizeUrl = "";
-        String ajaxSelectSizeUrl = "";
-        String paginateNextStyle = modelForm.getPaginateNextStyle();
-        String paginateNextLabel = modelForm.getPaginateNextLabel(context);
-        String nextUrl = "";
-        String ajaxNextUrl = "";
-        String paginateLastStyle = modelForm.getPaginateLastStyle();
-        String paginateLastLabel = modelForm.getPaginateLastLabel(context);
-        String lastUrl = "";
-        String ajaxLastUrl = "";
-        if (viewIndex > 0) {
-            if (ajaxEnabled) {
-                ajaxFirstUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null, 
modelForm, prepLinkText + 0 + anchor, context);
-            } else {
-                linkText = prepLinkText + 0 + anchor;
-                firstUrl = rh.makeLink(this.request, this.response, urlPath + 
linkText);
-            }
-        }
-        if (viewIndex > 0) {
-            if (ajaxEnabled) {
-                ajaxPreviousUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null, 
modelForm,
-                        prepLinkText + (viewIndex - 1) + anchor, context);
-            } else {
-                linkText = prepLinkText + (viewIndex - 1) + anchor;
-                previousUrl = rh.makeLink(this.request, this.response, urlPath 
+ linkText);
-            }
-        }
-        // Page select dropdown
-        if (listSize > 0 && this.javaScriptEnabled) {
-            if (ajaxEnabled) {
-                ajaxSelectUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null, 
modelForm,
-                        prepLinkText + "' + (this.value - 1) + '", context);
-            } else {
-                linkText = prepLinkText;
-                if (linkText.startsWith("/")) {
-                    linkText = linkText.substring(1);
-                }
-                selectUrl = rh.makeLink(this.request, this.response, urlPath + 
linkText);
-            }
-        }
-        // Next button
-        if (highIndex < listSize) {
-            if (ajaxEnabled) {
-                ajaxNextUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null,
-                        modelForm, prepLinkText + (viewIndex + 1) + anchor, 
context);
-            } else {
-                linkText = prepLinkText + (viewIndex + 1) + anchor;
-                nextUrl = rh.makeLink(this.request, this.response, urlPath + 
linkText);
-            }
-        }
-        // Last button
-        if (highIndex < listSize) {
-            int lastIndex = UtilMisc.getViewLastIndex(listSize, viewSize);
-            if (ajaxEnabled) {
-                ajaxLastUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null,
-                        modelForm, prepLinkText + lastIndex + anchor, context);
-            } else {
-                linkText = prepLinkText + lastIndex + anchor;
-                lastUrl = rh.makeLink(this.request, this.response, urlPath + 
linkText);
-            }
-        }
-        // Page size select dropdown
-        if (listSize > 0 && this.javaScriptEnabled) {
-            if (ajaxEnabled) {
-                ajaxSelectSizeUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null,
-                        modelForm, prepLinkSizeText + anchor, context);
-            } else {
-                linkText = prepLinkSizeText;
-                if (linkText.startsWith("/")) {
-                    linkText = linkText.substring(1);
-                }
-                selectSizeUrl = rh.makeLink(this.request, this.response, 
urlPath + linkText);
-            }
-        }
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderNextPrev ");
-        sr.append(" paginateStyle=\"");
-        sr.append(paginateStyle);
-        sr.append("\" paginateFirstStyle=\"");
-        sr.append(paginateFirstStyle);
-        sr.append("\" viewIndex=");
-        sr.append(Integer.toString(viewIndex));
-        sr.append(" highIndex=");
-        sr.append(Integer.toString(highIndex));
-        sr.append(" listSize=");
-        sr.append(Integer.toString(listSize));
-        sr.append(" viewSize=");
-        sr.append(Integer.toString(viewSize));
-        sr.append(" ajaxEnabled=");
-        sr.append(Boolean.toString(ajaxEnabled));
-        sr.append(" javaScriptEnabled=");
-        sr.append(Boolean.toString(javaScriptEnabled));
-        sr.append(" ajaxFirstUrl=\"");
-        sr.append(ajaxFirstUrl);
-        sr.append("\" ajaxFirstUrl=\"");
-        sr.append(ajaxFirstUrl);
-        sr.append("\" ajaxFirstUrl=\"");
-        sr.append(ajaxFirstUrl);
-        sr.append("\" firstUrl=\"");
-        sr.append(firstUrl);
-        sr.append("\" paginateFirstLabel=\"");
-        sr.append(paginateFirstLabel);
-        sr.append("\" paginatePreviousStyle=\"");
-        sr.append(paginatePreviousStyle);
-        sr.append("\" ajaxPreviousUrl=\"");
-        sr.append(ajaxPreviousUrl);
-        sr.append("\" previousUrl=\"");
-        sr.append(previousUrl);
-        sr.append("\" paginatePreviousLabel=\"");
-        sr.append(paginatePreviousLabel);
-        sr.append("\" pageLabel=\"");
-        sr.append(pageLabel);
-        sr.append("\" ajaxSelectUrl=\"");
-        sr.append(ajaxSelectUrl);
-        sr.append("\" selectUrl=\"");
-        sr.append(selectUrl);
-        sr.append("\" ajaxSelectSizeUrl=\"");
-        sr.append(ajaxSelectSizeUrl);
-        sr.append("\" selectSizeUrl=\"");
-        sr.append(selectSizeUrl);
-        sr.append("\" commonDisplaying=\"");
-        sr.append(commonDisplaying);
-        sr.append("\" paginateNextStyle=\"");
-        sr.append(paginateNextStyle);
-        sr.append("\" ajaxNextUrl=\"");
-        sr.append(ajaxNextUrl);
-        sr.append("\" nextUrl=\"");
-        sr.append(nextUrl);
-        sr.append("\" paginateNextLabel=\"");
-        sr.append(paginateNextLabel);
-        sr.append("\" paginateLastStyle=\"");
-        sr.append(paginateLastStyle);
-        sr.append("\" ajaxLastUrl=\"");
-        sr.append(ajaxLastUrl);
-        sr.append("\" lastUrl=\"");
-        sr.append(lastUrl);
-        sr.append("\" paginateLastLabel=\"");
-        sr.append(paginateLastLabel);
-        sr.append("\" paginateViewSizeLabel=\"");
-        sr.append(paginateViewSizeLabel);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.nextPrev(context, modelForm, 
this.javaScriptEnabled));
     }
 
     @Override
     public void renderFileField(Appendable writer, Map<String, Object> 
context, FileField textField) throws IOException {
-        ModelFormField modelFormField = textField.getModelFormField();
-        String className = "";
-        String alert = "false";
-        String name = modelFormField.getParameterName(context);
-        String value = modelFormField.getEntry(context, 
textField.getDefaultValue(context));
-        String size = Integer.toString(textField.getSize());
-        String maxlength = "";
-        String autocomplete = "";
-        if (UtilValidate.isNotEmpty(modelFormField.getWidgetStyle())) {
-            className = modelFormField.getWidgetStyle();
-            if (modelFormField.shouldBeRed(context)) {
-                alert = "true";
-            }
-        }
-        if (UtilValidate.isEmpty(value)) {
-            value = "";
-        }
-        if (textField.getMaxlength() != null) {
-            maxlength = textField.getMaxlength().toString();
-        }
-        if (!textField.getClientAutocompleteField()) {
-            autocomplete = "off";
-        }
-        String tabindex = modelFormField.getTabindex();
-        boolean disabled = modelFormField.getDisabled(context);
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFileField ");
-        sr.append(" className=\"");
-        sr.append(className);
-        sr.append("\" alert=\"");
-        sr.append(alert);
-        sr.append("\" name=\"");
-        sr.append(name);
-        sr.append("\" value=\"");
-        sr.append(value);
-        sr.append("\" size=\"");
-        sr.append(size);
-        sr.append("\" maxlength=\"");
-        sr.append(maxlength);
-        sr.append("\" autocomplete=\"");
-        sr.append(autocomplete);
-        sr.append("\" tabindex=\"");
-        sr.append(tabindex);
-        sr.append("\" disabled=");
-        sr.append(Boolean.toString(disabled));
-        sr.append(" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.fileField(context, textField));
         this.makeHyperlinkString(writer, textField.getSubHyperlink(), context);
-        this.appendTooltip(writer, context, modelFormField);
+        this.appendTooltip(writer, context, textField.getModelFormField());
     }
 
     @Override
     public void renderPasswordField(Appendable writer, Map<String, Object> 
context, PasswordField passwordField) throws IOException {
-        ModelFormField modelFormField = passwordField.getModelFormField();
-        String className = "";
-        String alert = "false";
-        String name = modelFormField.getParameterName(context);
-        String size = Integer.toString(passwordField.getSize());
-        String maxlength = "";
-        String id = modelFormField.getCurrentContainerId(context);
-        boolean disabled = modelFormField.getDisabled(context);
-        String autocomplete = "";
-        if (UtilValidate.isNotEmpty(modelFormField.getWidgetStyle())) {
-            className = modelFormField.getWidgetStyle();
-            if (modelFormField.shouldBeRed(context)) {
-                alert = "true";
-            }
-        }
-        String value = modelFormField.getEntry(context, 
passwordField.getDefaultValue(context));
-        if (value == null) {
-            value = "";
-        }
-        if (passwordField.getMaxlength() != null) {
-            maxlength = passwordField.getMaxlength().toString();
-        }
-        if (id == null) {
-            id = "";
-        }
-        if (!passwordField.getClientAutocompleteField()) {
-            autocomplete = "off";
-        }
-
-        //check for required field style on single forms
-        if (shouldApplyRequiredField(modelFormField)) {
-            String requiredStyle = modelFormField.getRequiredFieldStyle();
-            if (UtilValidate.isEmpty(requiredStyle)) {
-                requiredStyle = "required";
-            }
-            if (UtilValidate.isEmpty(className)) {
-                className = requiredStyle;
-            } else {
-                className = requiredStyle + " " + className;
-            }
-        }
-
-        String tabindex = modelFormField.getTabindex();
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderPasswordField ");
-        sr.append(" className=\"");
-        sr.append(className);
-        sr.append("\" alert=\"");
-        sr.append(alert);
-        sr.append("\" name=\"");
-        sr.append(name);
-        sr.append("\" value=\"");
-        sr.append(value);
-        sr.append("\" size=\"");
-        sr.append(size);
-        sr.append("\" maxlength=\"");
-        sr.append(maxlength);
-        sr.append("\" id=\"");
-        sr.append(id);
-        sr.append("\" autocomplete=\"");
-        sr.append(autocomplete);
-        sr.append("\" tabindex=\"");
-        sr.append(tabindex);
-        sr.append("\" disabled=");
-        sr.append(Boolean.toString(disabled));
-        sr.append(" />");
-        executeMacro(writer, sr.toString());
+        final ModelFormField modelFormField = 
passwordField.getModelFormField();
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.passwordField(context, passwordField));
         this.addAsterisks(writer, context, modelFormField);
         this.makeHyperlinkString(writer, passwordField.getSubHyperlink(), 
context);
         this.appendTooltip(writer, context, modelFormField);
@@ -1380,47 +666,9 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
 
     @Override
     public void renderImageField(Appendable writer, Map<String, Object> 
context, ImageField imageField) throws IOException {
-        ModelFormField modelFormField = imageField.getModelFormField();
-        String value = modelFormField.getEntry(context, 
imageField.getValue(context));
-        String description = imageField.getDescription(context);
-        String alternate = imageField.getAlternate(context);
-        String style = imageField.getStyle(context);
-        if (UtilValidate.isEmpty(description)) {
-            description = imageField.getModelFormField().getTitle(context);
-        }
-        if (UtilValidate.isEmpty(alternate)) {
-            alternate = description;
-        }
-        if (UtilValidate.isNotEmpty(value)) {
-            if (!value.startsWith("http")) {
-                StringBuilder buffer = new StringBuilder();
-                ContentUrlTag.appendContentPrefix(request, buffer);
-                buffer.append(value);
-                value = buffer.toString();
-            }
-        } else if (value == null) {
-            value = "";
-        }
-        String event = modelFormField.getEvent();
-        String action = modelFormField.getAction(context);
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderImageField ");
-        sr.append(" value=\"");
-        sr.append(value);
-        sr.append("\" description=\"");
-        sr.append(encode(description, modelFormField, context));
-        sr.append("\" alternate=\"");
-        sr.append(encode(alternate, modelFormField, context));
-        sr.append("\" style=\"");
-        sr.append(style);
-        sr.append("\" event=\"");
-        sr.append(event == null ? "" : event);
-        sr.append("\" action=\"");
-        sr.append(action == null ? "" : action);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.imageField(context, imageField));
         this.makeHyperlinkString(writer, imageField.getSubHyperlink(), 
context);
-        this.appendTooltip(writer, context, modelFormField);
+        this.appendTooltip(writer, context, imageField.getModelFormField());
     }
 
     @Override
@@ -1437,43 +685,7 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
 
     @Override
     public void renderBanner(Appendable writer, Map<String, Object> context, 
ModelForm.Banner banner) throws IOException {
-        String style = banner.getStyle(context);
-        String leftStyle = banner.getLeftTextStyle(context);
-        if (UtilValidate.isEmpty(leftStyle)) {
-            leftStyle = style;
-        }
-        String rightStyle = banner.getRightTextStyle(context);
-        if (UtilValidate.isEmpty(rightStyle)) {
-            rightStyle = style;
-        }
-        String leftText = banner.getLeftText(context);
-        if (leftText == null) {
-            leftText = "";
-        }
-        String text = banner.getText(context);
-        if (text == null) {
-            text = "";
-        }
-        String rightText = banner.getRightText(context);
-        if (rightText == null) {
-            rightText = "";
-        }
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderBanner ");
-        sr.append(" style=\"");
-        sr.append(style);
-        sr.append("\" leftStyle=\"");
-        sr.append(leftStyle);
-        sr.append("\" rightStyle=\"");
-        sr.append(rightStyle);
-        sr.append("\" leftText=\"");
-        sr.append(leftText);
-        sr.append("\" text=\"");
-        sr.append(text);
-        sr.append("\" rightText=\"");
-        sr.append(rightText);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.banner(context, banner));
     }
 
     /**
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
index 539ad1d401..b83a0766e0 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
@@ -19,10 +19,12 @@
 package org.apache.ofbiz.widget.renderer.macro;
 
 import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
@@ -1876,6 +1878,473 @@ public final class RenderableFtlFormElementsBuilder {
                 .build();
     }
 
+    /**
+     * Creates the macro call that renders a text find field.
+     * @param context the context
+     * @param textFindField the text find field
+     * @return the RenderableFtl
+     */
+    public RenderableFtl textFindField(final Map<String, Object> context, 
final ModelFormField.TextFindField textFindField) {
+        final ModelFormField modelFormField = 
textFindField.getModelFormField();
+        final Locale locale = (Locale) context.get("locale");
+        String opEquals = "";
+        String opBeginsWith = "";
+        String opContains = "";
+        String opIsEmpty = "";
+        String opNotEqual = "";
+        if (!textFindField.getHideOptions()) {
+            opEquals = UtilProperties.getMessage("conditionalUiLabels", 
"equals", locale);
+            opBeginsWith = UtilProperties.getMessage("conditionalUiLabels", 
"begins_with", locale);
+            opContains = UtilProperties.getMessage("conditionalUiLabels", 
"contains", locale);
+            opIsEmpty = UtilProperties.getMessage("conditionalUiLabels", 
"is_empty", locale);
+            opNotEqual = UtilProperties.getMessage("conditionalUiLabels", 
"not_equal", locale);
+        }
+        final String value = modelFormField.getEntry(context, 
textFindField.getDefaultValue(context));
+        final var builder = 
RenderableFtlMacroCall.builder().name("renderTextFindField");
+        applyWidgetStyleAndAlert(modelFormField, context, builder);
+        return builder
+                .stringParameter("name", 
nonNull(modelFormField.getParameterName(context)))
+                .stringParameter("value", nonNull(value))
+                .stringParameter("defaultOption", 
nonNull(textFindField.getDefaultOption(context)))
+                .stringParameter("opEquals", nonNull(opEquals))
+                .stringParameter("opBeginsWith", nonNull(opBeginsWith))
+                .stringParameter("opContains", nonNull(opContains))
+                .stringParameter("opIsEmpty", nonNull(opIsEmpty))
+                .stringParameter("opNotEqual", nonNull(opNotEqual))
+                .stringParameter("size", 
Integer.toString(textFindField.getSize()))
+                .stringParameter("maxlength", textFindField.getMaxlength() == 
null ? "" : textFindField.getMaxlength().toString())
+                .stringParameter("autocomplete", 
textFindField.getClientAutocompleteField() ? "" : "off")
+                .stringParameter("titleStyle", 
nonNull(modelFormField.getTitleStyle()))
+                .booleanParameter("hideIgnoreCase", 
textFindField.getHideIgnoreCase())
+                .booleanParameter("ignCase", 
textFindField.getIgnoreCase(context))
+                .stringParameter("ignoreCase", 
nonNull(UtilProperties.getMessage("conditionalUiLabels", "ignore_case", 
locale)))
+                .stringParameter("tabindex", 
nonNull(modelFormField.getTabindex()))
+                .stringParameter("conditionGroup", 
nonNull(modelFormField.getConditionGroup()))
+                .booleanParameter("disabled", 
modelFormField.getDisabled(context))
+                .build();
+    }
+
+    /**
+     * Creates the macro call that renders a range find field.
+     * @param context the context
+     * @param rangeFindField the range find field
+     * @return the RenderableFtl
+     */
+    public RenderableFtl rangeFindField(final Map<String, Object> context, 
final ModelFormField.RangeFindField rangeFindField) {
+        final ModelFormField modelFormField = 
rangeFindField.getModelFormField();
+        final Locale locale = (Locale) context.get("locale");
+        final String value = modelFormField.getEntry(context, 
rangeFindField.getDefaultValue(context));
+        final String value2 = modelFormField.getEntry(context);
+        final Integer maxlength = rangeFindField.getMaxlength();
+        final var builder = 
RenderableFtlMacroCall.builder().name("renderRangeFindField");
+        applyWidgetStyleAndAlert(modelFormField, context, builder);
+        return builder
+                .stringParameter("name", 
nonNull(modelFormField.getParameterName(context)))
+                .stringParameter("value", nonNull(value))
+                .stringParameter("size", 
Integer.toString(rangeFindField.getSize()))
+                .stringParameter("maxlength", maxlength == null ? "" : 
Integer.toString(maxlength))
+                .stringParameter("autocomplete", 
rangeFindField.getClientAutocompleteField() ? "" : "off")
+                .stringParameter("titleStyle", 
nonNull(modelFormField.getTitleStyle()))
+                .stringParameter("defaultOptionFrom", 
nonNull(rangeFindField.getDefaultOptionFrom()))
+                .stringParameter("opEquals", 
nonNull(UtilProperties.getMessage("conditionalUiLabels", "equals", locale)))
+                .stringParameter("opGreaterThan", 
nonNull(UtilProperties.getMessage("conditionalUiLabels", "greater_than", 
locale)))
+                .stringParameter("opGreaterThanEquals",
+                        
nonNull(UtilProperties.getMessage("conditionalUiLabels", "greater_than_equals", 
locale)))
+                .stringParameter("opLessThan", 
nonNull(UtilProperties.getMessage("conditionalUiLabels", "less_than", locale)))
+                .stringParameter("opLessThanEquals", 
nonNull(UtilProperties.getMessage("conditionalUiLabels", "less_than_equals", 
locale)))
+                .stringParameter("value2", nonNull(value2))
+                .stringParameter("defaultOptionThru", 
nonNull(rangeFindField.getDefaultOptionThru()))
+                .stringParameter("conditionGroup", 
nonNull(modelFormField.getConditionGroup()))
+                .stringParameter("tabindex", 
nonNull(modelFormField.getTabindex()))
+                .booleanParameter("disabled", 
modelFormField.getDisabled(context))
+                .build();
+    }
+
+    /**
+     * Creates the macro call that renders a lookup field.
+     * @param context the context
+     * @param lookupField the lookup field
+     * @param javaScriptEnabled whether JavaScript is enabled in the browser 
of the user
+     * @return the RenderableFtl
+     * @throws UnsupportedEncodingException if the last view name cannot be 
encoded
+     */
+    public RenderableFtl lookupField(final Map<String, Object> context, final 
ModelFormField.LookupField lookupField,
+                                     final boolean javaScriptEnabled) throws 
UnsupportedEncodingException {
+        final ModelFormField modelFormField = lookupField.getModelFormField();
+        final String lookupFieldFormName = lookupField.getFormName(context);
+        final String id = modelFormField.getCurrentContainerId(context);
+        List<ModelForm.UpdateArea> updateAreas = 
modelFormField.getOnChangeUpdateAreas();
+        //add default ajax auto completer to all lookup fields
+        if (UtilValidate.isEmpty(updateAreas) && 
UtilValidate.isNotEmpty(lookupFieldFormName)) {
+            final String autoCompleterTarget = 
(lookupFieldFormName.indexOf('?') == -1 ? lookupFieldFormName + "?"
+                    : lookupFieldFormName + "&amp;amp;") + "ajaxLookup=Y";
+            updateAreas = new ArrayList<>();
+            updateAreas.add(ModelForm.UpdateArea.literal("change", id, 
autoCompleterTarget));
+        }
+        final boolean ajaxEnabled = UtilValidate.isNotEmpty(updateAreas) && 
javaScriptEnabled;
+        final ModelForm modelForm = modelFormField.getModelForm();
+        String formName = modelFormField.getParentFormName();
+        if (UtilValidate.isEmpty(formName)) {
+            formName = FormRenderer.getCurrentFormName(modelForm, context);
+        }
+        final String ajaxUrl = ajaxEnabled ? 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null, 
modelForm, "", context) : "";
+        String clearText = "";
+        final Map<String, Object> uiLabelMap = 
UtilGenerics.cast(context.get("uiLabelMap"));
+        if (uiLabelMap != null) {
+            clearText = (String) uiLabelMap.get("CommonClear");
+        } else {
+            Debug.logWarning("Could not find uiLabelMap in context", MODULE);
+        }
+        Boolean showDescription = lookupField.getShowDescription();
+        if (showDescription == null) {
+            showDescription = 
"Y".equals(visualTheme.getModelTheme().getLookupShowDescription());
+        }
+        // lastViewName, used by lookup to remember the real last view name; 
read only from session (set by RequestHandler) to prevent user input
+        String lastViewName = (String) 
request.getSession().getAttribute("_LAST_VIEW_NAME_");
+        if (UtilValidate.isEmpty(lastViewName)) {
+            lastViewName = "";
+        }
+        lastViewName = UtilHttp.getEncodedParameter(lastViewName);
+
+        String className = nonNull(modelFormField.getWidgetStyle());
+        final String alert = UtilValidate.isNotEmpty(className) && 
modelFormField.shouldBeRed(context) ? "true" : "false";
+        className = withRequiredStyle(modelFormField, className);
+        final Integer maxlength = lookupField.getMaxlength();
+        final String fadeBackground = lookupField.getFadeBackground();
+        final var builder = RenderableFtlMacroCall.builder()
+                .name("renderLookupField")
+                .stringParameter("className", className)
+                .stringParameter("alert", alert)
+                .stringParameter("name", 
nonNull(modelFormField.getParameterName(context)))
+                .stringParameter("value", 
nonNull(modelFormField.getEntry(context, lookupField.getDefaultValue(context))))
+                .stringParameter("size", 
Integer.toString(lookupField.getSize()))
+                .stringParameter("maxlength", maxlength == null ? "" : 
Integer.toString(maxlength))
+                .stringParameter("id", nonNull(id))
+                .stringParameter("event", nonNull(modelFormField.getEvent()))
+                .stringParameter("action", 
nonNull(modelFormField.getAction(context)))
+                .booleanParameter("readonly", lookupField.getReadonly())
+                .stringParameter("autocomplete", 
!lookupField.getClientAutocompleteField() || !ajaxEnabled ? "off" : "")
+                .stringParameter("descriptionFieldName", 
nonNull(lookupField.getDescriptionFieldName()))
+                .stringParameter("formName", nonNull(formName))
+                .stringParameter("fieldFormName", nonNull(lookupFieldFormName))
+                .objectParameter("targetParameterIter", new 
ArrayList<>(lookupField.getTargetParameterList(context)))
+                .stringParameter("imgSrc", 
pathAsContentUrl("/images/fieldlookup.gif"))
+                .stringParameter("ajaxUrl", ajaxUrl)
+                .booleanParameter("ajaxEnabled", ajaxEnabled)
+                .stringParameter("presentation", 
nonNull(lookupField.getLookupPresentation()));
+        if (UtilValidate.isNotEmpty(lookupField.getLookupHeight())) {
+            builder.stringParameter("height", lookupField.getLookupHeight());
+        }
+        if (UtilValidate.isNotEmpty(lookupField.getLookupWidth())) {
+            builder.stringParameter("width", lookupField.getLookupWidth());
+        }
+        if (UtilValidate.isNotEmpty(lookupField.getLookupPosition())) {
+            builder.stringParameter("position", 
lookupField.getLookupPosition());
+        }
+        return builder
+                .stringParameter("fadeBackground", 
UtilValidate.isEmpty(fadeBackground) ? "false" : fadeBackground)
+                .stringParameter("clearText", nonNull(clearText))
+                .stringParameter("showDescription", 
Boolean.toString(showDescription))
+                .stringParameter("initiallyCollapsed", 
Boolean.toString(lookupField.getInitiallyCollapsed()))
+                .stringParameter("lastViewName", lastViewName)
+                .stringParameter("conditionGroup", 
nonNull(modelFormField.getConditionGroup()))
+                .stringParameter("tabindex", 
nonNull(modelFormField.getTabindex()))
+                .booleanParameter("disabled", 
modelFormField.getDisabled(context))
+                .stringParameter("delegatorName", ((HttpSession) 
context.get("session")).getAttribute("delegatorName").toString())
+                .build();
+    }
+
+    /**
+     * Creates the macro call that renders the pagination of a list form.
+     * @param context the context
+     * @param modelForm the form
+     * @param javaScriptEnabled whether JavaScript is enabled in the browser 
of the user
+     * @return the RenderableFtl, which renders nothing if the form cannot 
paginate
+     */
+    public RenderableFtl nextPrev(final Map<String, Object> context, final 
ModelForm modelForm, final boolean javaScriptEnabled) {
+        boolean ajaxEnabled = false;
+        final List<ModelForm.UpdateArea> updateAreas = 
modelForm.getOnPaginateUpdateAreas();
+        String targetService = modelForm.getPaginateTarget(context);
+        if (javaScriptEnabled && UtilValidate.isNotEmpty(updateAreas)) {
+            ajaxEnabled = true;
+        }
+        if (targetService == null) {
+            targetService = "${targetService}";
+        }
+        if (UtilValidate.isEmpty(targetService) && updateAreas == null) {
+            Debug.logWarning("Cannot paginate because TargetService is empty 
for the form: " + modelForm.getName(), MODULE);
+            return RenderableFtlNoop.INSTANCE;
+        }
+        // get the parameterized pagination index and size fields
+        final int paginatorNumber = WidgetWorker.getPaginatorNumber(context);
+        String viewIndexParam = modelForm.getMultiPaginateIndexField(context);
+        String viewSizeParam = modelForm.getMultiPaginateSizeField(context);
+        final int viewIndex = Paginator.getViewIndex(modelForm, context);
+        final int viewSize = Paginator.getViewSize(modelForm, context);
+        final int listSize = Paginator.getListSize(context);
+        final int lowIndex = Paginator.getLowIndex(context);
+        final int highIndex = Paginator.getHighIndex(context);
+        final int actualPageSize = Paginator.getActualPageSize(context);
+        // needed for the "Page" and "rows" labels
+        final Map<String, String> uiLabelMap = 
UtilGenerics.cast(context.get("uiLabelMap"));
+        String pageLabel = "";
+        String commonDisplaying = "";
+        if (uiLabelMap == null) {
+            Debug.logWarning("Could not find uiLabelMap in context", MODULE);
+        } else {
+            pageLabel = uiLabelMap.get("CommonPage");
+            final Map<String, Integer> messageMap = UtilMisc.toMap("lowCount", 
lowIndex + 1, "highCount", lowIndex + actualPageSize,
+                    "total", listSize);
+            commonDisplaying = UtilProperties.getMessage("CommonUiLabels", 
"CommonDisplaying", messageMap, (Locale) context.get("locale"));
+        }
+        // for legacy support, the viewSizeParam is VIEW_SIZE and 
viewIndexParam is VIEW_INDEX when the fields are "viewSize" and "viewIndex"
+        if (("viewIndex" + "_" + paginatorNumber).equals(viewIndexParam)) {
+            viewIndexParam = "VIEW_INDEX" + "_" + paginatorNumber;
+        }
+        if (("viewSize" + "_" + paginatorNumber).equals(viewSizeParam)) {
+            viewSizeParam = "VIEW_SIZE" + "_" + paginatorNumber;
+        }
+        String str = (String) context.get("_QBESTRING_");
+
+        // refresh any csrf token in the query string for pagination
+        final String tokenValue = CsrfUtil.generateTokenForNonAjax(request, 
targetService);
+        str = CsrfUtil.addOrUpdateTokenInQueryString(str, tokenValue);
+
+        // strip legacy viewIndex/viewSize params from the query string
+        String queryString = UtilHttp.stripViewParamsFromQueryString(str, "" + 
paginatorNumber);
+        // strip parameterized index/size params from the query string
+        final Set<String> paramNames = new HashSet<>();
+        paramNames.add(viewIndexParam);
+        paramNames.add(viewSizeParam);
+        queryString = UtilHttp.stripNamedParamsFromQueryString(queryString, 
paramNames);
+        String anchor = "";
+        final String paginateAnchor = modelForm.getPaginateTargetAnchor();
+        if (UtilValidate.isNotEmpty(paginateAnchor)) {
+            anchor = "#" + paginateAnchor;
+        }
+        // Create separate url path String and request parameters String,
+        // add viewIndex/viewSize parameters to request parameter String
+        final String urlPath = 
UtilHttp.removeQueryStringFromTarget(targetService);
+        String prepLinkText = UtilHttp.getQueryStringFromTarget(targetService);
+        if (UtilValidate.isNotEmpty(queryString)) {
+            queryString = UtilHttp.encodeAmpersands(queryString);
+        }
+        if (prepLinkText.indexOf('?') < 0) {
+            prepLinkText += "?";
+        } else if (!prepLinkText.endsWith("?")) {
+            prepLinkText += "&amp;";
+        }
+        if (UtilValidate.isNotEmpty(queryString) && 
!"null".equals(queryString)) {
+            prepLinkText += queryString + "&amp;";
+        }
+        final String prepLinkSizeText = prepLinkText + viewSizeParam + 
"='+this.value+'" + "&amp;" + viewIndexParam + "=0";
+        prepLinkText += viewSizeParam + "=" + viewSize + "&amp;" + 
viewIndexParam + "=";
+        if (ajaxEnabled) {
+            // Prepare params for prototype.js
+            prepLinkText = prepLinkText.replace("?", "");
+            prepLinkText = prepLinkText.replace("&amp;", "&");
+        }
+        String firstUrl = "";
+        String ajaxFirstUrl = "";
+        String previousUrl = "";
+        String ajaxPreviousUrl = "";
+        String selectUrl = "";
+        String ajaxSelectUrl = "";
+        String selectSizeUrl = "";
+        String ajaxSelectSizeUrl = "";
+        String nextUrl = "";
+        String ajaxNextUrl = "";
+        String lastUrl = "";
+        String ajaxLastUrl = "";
+        if (viewIndex > 0) {
+            if (ajaxEnabled) {
+                ajaxFirstUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null, 
modelForm, prepLinkText + 0 + anchor, context);
+                ajaxPreviousUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null, 
modelForm,
+                        prepLinkText + (viewIndex - 1) + anchor, context);
+            } else {
+                firstUrl = requestHandler.makeLink(request, response, urlPath 
+ prepLinkText + 0 + anchor);
+                previousUrl = requestHandler.makeLink(request, response, 
urlPath + prepLinkText + (viewIndex - 1) + anchor);
+            }
+        }
+        // Page select dropdown
+        if (listSize > 0 && javaScriptEnabled) {
+            if (ajaxEnabled) {
+                ajaxSelectUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null, 
modelForm,
+                        prepLinkText + "' + (this.value - 1) + '", context);
+            } else {
+                final String linkText = prepLinkText.startsWith("/") ? 
prepLinkText.substring(1) : prepLinkText;
+                selectUrl = requestHandler.makeLink(request, response, urlPath 
+ linkText);
+            }
+        }
+        if (highIndex < listSize) {
+            // Next button and last button
+            final int lastIndex = UtilMisc.getViewLastIndex(listSize, 
viewSize);
+            if (ajaxEnabled) {
+                ajaxNextUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null,
+                        modelForm, prepLinkText + (viewIndex + 1) + anchor, 
context);
+                ajaxLastUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null,
+                        modelForm, prepLinkText + lastIndex + anchor, context);
+            } else {
+                nextUrl = requestHandler.makeLink(request, response, urlPath + 
prepLinkText + (viewIndex + 1) + anchor);
+                lastUrl = requestHandler.makeLink(request, response, urlPath + 
prepLinkText + lastIndex + anchor);
+            }
+        }
+        // Page size select dropdown
+        if (listSize > 0 && javaScriptEnabled) {
+            if (ajaxEnabled) {
+                ajaxSelectSizeUrl = 
MacroCommonRenderer.createAjaxParamsFromUpdateAreas(updateAreas, null,
+                        modelForm, prepLinkSizeText + anchor, context);
+            } else {
+                final String linkText = prepLinkSizeText.startsWith("/") ? 
prepLinkSizeText.substring(1) : prepLinkSizeText;
+                selectSizeUrl = requestHandler.makeLink(request, response, 
urlPath + linkText);
+            }
+        }
+        return RenderableFtlMacroCall.builder()
+                .name("renderNextPrev")
+                .stringParameter("paginateStyle", 
nonNull(modelForm.getPaginateStyle()))
+                .stringParameter("paginateFirstStyle", 
nonNull(modelForm.getPaginateFirstStyle()))
+                .intParameter("viewIndex", viewIndex)
+                .intParameter("highIndex", highIndex)
+                .intParameter("listSize", listSize)
+                .intParameter("viewSize", viewSize)
+                .booleanParameter("ajaxEnabled", ajaxEnabled)
+                .booleanParameter("javaScriptEnabled", javaScriptEnabled)
+                .stringParameter("ajaxFirstUrl", ajaxFirstUrl)
+                .stringParameter("firstUrl", nonNull(firstUrl))
+                .stringParameter("paginateFirstLabel", 
nonNull(modelForm.getPaginateFirstLabel(context)))
+                .stringParameter("paginatePreviousStyle", 
nonNull(modelForm.getPaginatePreviousStyle()))
+                .stringParameter("ajaxPreviousUrl", ajaxPreviousUrl)
+                .stringParameter("previousUrl", nonNull(previousUrl))
+                .stringParameter("paginatePreviousLabel", 
nonNull(modelForm.getPaginatePreviousLabel(context)))
+                .stringParameter("pageLabel", nonNull(pageLabel))
+                .stringParameter("ajaxSelectUrl", ajaxSelectUrl)
+                .stringParameter("selectUrl", nonNull(selectUrl))
+                .stringParameter("ajaxSelectSizeUrl", ajaxSelectSizeUrl)
+                .stringParameter("selectSizeUrl", nonNull(selectSizeUrl))
+                .stringParameter("commonDisplaying", nonNull(commonDisplaying))
+                .stringParameter("paginateNextStyle", 
nonNull(modelForm.getPaginateNextStyle()))
+                .stringParameter("ajaxNextUrl", ajaxNextUrl)
+                .stringParameter("nextUrl", nonNull(nextUrl))
+                .stringParameter("paginateNextLabel", 
nonNull(modelForm.getPaginateNextLabel(context)))
+                .stringParameter("paginateLastStyle", 
nonNull(modelForm.getPaginateLastStyle()))
+                .stringParameter("ajaxLastUrl", ajaxLastUrl)
+                .stringParameter("lastUrl", nonNull(lastUrl))
+                .stringParameter("paginateLastLabel", 
nonNull(modelForm.getPaginateLastLabel(context)))
+                .stringParameter("paginateViewSizeLabel", 
nonNull(modelForm.getPaginateViewSizeLabel(context)))
+                .build();
+    }
+
+    /**
+     * Creates the macro call that renders a file field.
+     * @param context the context
+     * @param fileField the file field
+     * @return the RenderableFtl
+     */
+    public RenderableFtl fileField(final Map<String, Object> context, final 
ModelFormField.FileField fileField) {
+        final ModelFormField modelFormField = fileField.getModelFormField();
+        final var builder = 
RenderableFtlMacroCall.builder().name("renderFileField");
+        applyWidgetStyleAndAlert(modelFormField, context, builder);
+        return builder
+                .stringParameter("name", 
nonNull(modelFormField.getParameterName(context)))
+                .stringParameter("value", 
nonNull(modelFormField.getEntry(context, fileField.getDefaultValue(context))))
+                .stringParameter("size", Integer.toString(fileField.getSize()))
+                .stringParameter("maxlength", fileField.getMaxlength() == null 
? "" : fileField.getMaxlength().toString())
+                .stringParameter("autocomplete", 
fileField.getClientAutocompleteField() ? "" : "off")
+                .stringParameter("tabindex", 
nonNull(modelFormField.getTabindex()))
+                .booleanParameter("disabled", 
modelFormField.getDisabled(context))
+                .build();
+    }
+
+    /**
+     * Creates the macro call that renders a password field.
+     * @param context the context
+     * @param passwordField the password field
+     * @return the RenderableFtl
+     */
+    public RenderableFtl passwordField(final Map<String, Object> context, 
final ModelFormField.PasswordField passwordField) {
+        final ModelFormField modelFormField = 
passwordField.getModelFormField();
+        String className = nonNull(modelFormField.getWidgetStyle());
+        final String alert = UtilValidate.isNotEmpty(className) && 
modelFormField.shouldBeRed(context) ? "true" : "false";
+        className = withRequiredStyle(modelFormField, className);
+        return RenderableFtlMacroCall.builder()
+                .name("renderPasswordField")
+                .stringParameter("className", className)
+                .stringParameter("alert", alert)
+                .stringParameter("name", 
nonNull(modelFormField.getParameterName(context)))
+                .stringParameter("value", 
nonNull(modelFormField.getEntry(context, 
passwordField.getDefaultValue(context))))
+                .stringParameter("size", 
Integer.toString(passwordField.getSize()))
+                .stringParameter("maxlength", passwordField.getMaxlength() == 
null ? "" : passwordField.getMaxlength().toString())
+                .stringParameter("id", 
nonNull(modelFormField.getCurrentContainerId(context)))
+                .stringParameter("autocomplete", 
passwordField.getClientAutocompleteField() ? "" : "off")
+                .stringParameter("tabindex", 
nonNull(modelFormField.getTabindex()))
+                .booleanParameter("disabled", 
modelFormField.getDisabled(context))
+                .build();
+    }
+
+    /**
+     * Creates the macro call that renders an image field.
+     * @param context the context
+     * @param imageField the image field
+     * @return the RenderableFtl
+     */
+    public RenderableFtl imageField(final Map<String, Object> context, final 
ModelFormField.ImageField imageField) {
+        final ModelFormField modelFormField = imageField.getModelFormField();
+        String value = modelFormField.getEntry(context, 
imageField.getValue(context));
+        String description = imageField.getDescription(context);
+        String alternate = imageField.getAlternate(context);
+        if (UtilValidate.isEmpty(description)) {
+            description = modelFormField.getTitle(context);
+        }
+        if (UtilValidate.isEmpty(alternate)) {
+            alternate = description;
+        }
+        if (UtilValidate.isNotEmpty(value) && !value.startsWith("http")) {
+            value = pathAsContentUrl(value);
+        }
+        return RenderableFtlMacroCall.builder()
+                .name("renderImageField")
+                .stringParameter("value", nonNull(value))
+                .stringParameter("description", nonNull(encode(description, 
modelFormField, context)))
+                .stringParameter("alternate", nonNull(encode(alternate, 
modelFormField, context)))
+                .stringParameter("style", 
nonNull(imageField.getStyle(context)))
+                .stringParameter("event", nonNull(modelFormField.getEvent()))
+                .stringParameter("action", 
nonNull(modelFormField.getAction(context)))
+                .build();
+    }
+
+    /**
+     * Creates the macro call that renders a banner.
+     * @param context the context
+     * @param banner the banner
+     * @return the RenderableFtl
+     */
+    public RenderableFtl banner(final Map<String, Object> context, final 
ModelForm.Banner banner) {
+        final String style = banner.getStyle(context);
+        final String leftStyle = banner.getLeftTextStyle(context);
+        final String rightStyle = banner.getRightTextStyle(context);
+        return RenderableFtlMacroCall.builder()
+                .name("renderBanner")
+                .stringParameter("style", nonNull(style))
+                .stringParameter("leftStyle", 
nonNull(UtilValidate.isEmpty(leftStyle) ? style : leftStyle))
+                .stringParameter("rightStyle", 
nonNull(UtilValidate.isEmpty(rightStyle) ? style : rightStyle))
+                .stringParameter("leftText", 
nonNull(banner.getLeftText(context)))
+                .stringParameter("text", nonNull(banner.getText(context)))
+                .stringParameter("rightText", 
nonNull(banner.getRightText(context)))
+                .build();
+    }
+
+    // the class name of a field that is required in a single form gets the 
required style first
+    private static String withRequiredStyle(final ModelFormField 
modelFormField, final String className) {
+        if (!shouldApplyRequiredField(modelFormField)) {
+            return className;
+        }
+        String requiredStyle = modelFormField.getRequiredFieldStyle();
+        if (UtilValidate.isEmpty(requiredStyle)) {
+            requiredStyle = "required";
+        }
+        return UtilValidate.isEmpty(className) ? requiredStyle : requiredStyle 
+ " " + className;
+    }
+
     private static RenderableFtl macroWithoutParameters(final String 
macroName) {
         return RenderableFtlMacroCall.builder().name(macroName).build();
     }
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
index fe7e566970..911992ff25 100644
--- 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
@@ -19,11 +19,7 @@
 package org.apache.ofbiz.widget.renderer.macro;
 
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.empty;
-import static org.hamcrest.Matchers.not;
-import static org.hamcrest.Matchers.startsWith;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyMap;
@@ -35,7 +31,6 @@ import static org.mockito.ArgumentMatchers.notNull;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.CALLS_REAL_METHODS;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
@@ -46,10 +41,8 @@ import static org.mockito.Mockito.when;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
@@ -72,7 +65,6 @@ import org.hamcrest.Matchers;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.MockedStatic;
 
@@ -491,125 +483,84 @@ public final class MacroFormRendererTest {
 
     @Test
     public void textFindFieldMacroRendered() throws IOException {
-        final ModelFormField localModelFormField = mock(ModelFormField.class);
         final ModelFormField.TextFindField textFindField = 
mock(ModelFormField.TextFindField.class);
-        
when(textFindField.getModelFormField()).thenReturn(localModelFormField);
-        when(textFindField.getHideOptions()).thenReturn(true);
-        when(localModelFormField.getWidgetStyle()).thenReturn("WIDGETSTYLE");
-        when(localModelFormField.shouldBeRed(notNull())).thenReturn(true);
-        
when(localModelFormField.getParameterName(notNull())).thenReturn("FIELDNAME");
+        when(renderableFtlFormElementsBuilder.textFindField(notNull(), 
eq(textFindField))).thenReturn(genericMacroCall);
+        genericTooltipRenderedExpectation(textFindField);
+
+        macroFormRenderer.renderTextFindField(appendable, ImmutableMap.of(), 
textFindField);
 
-        ImmutableMap<String, Object> context = ImmutableMap.of();
-        macroFormRenderer.renderTextFindField(appendable, context, 
textFindField);
-        assertAndGetMacroString("renderTextFindField", ImmutableMap.of(
-                "name", "FIELDNAME",
-                "className", "WIDGETSTYLE",
-                "alert", "true"));
+        genericSingleMacroRenderedVerification();
+        genericTooltipRenderedVerification();
     }
 
     @Test
     public void rangeFindFieldMacroRendered() throws IOException {
-        final ModelFormField localModelFormField = mock(ModelFormField.class);
         final ModelFormField.RangeFindField rangeFindField = 
mock(ModelFormField.RangeFindField.class);
-        
when(rangeFindField.getModelFormField()).thenReturn(localModelFormField);
-        when(localModelFormField.getWidgetStyle()).thenReturn("WIDGETSTYLE");
-        when(rangeFindField.getDefaultValue(notNull())).thenReturn("AAA");
-        when(localModelFormField.getEntry(notNull(), 
eq("AAA"))).thenReturn("AAA");
-        when(localModelFormField.getEntry(notNull())).thenReturn("BBB");
-        when(localModelFormField.shouldBeRed(notNull())).thenReturn(true);
-        
when(localModelFormField.getParameterName(notNull())).thenReturn("FIELDNAME");
+        when(renderableFtlFormElementsBuilder.rangeFindField(notNull(), 
eq(rangeFindField))).thenReturn(genericMacroCall);
+        genericTooltipRenderedExpectation(rangeFindField);
+
+        macroFormRenderer.renderRangeFindField(appendable, ImmutableMap.of(), 
rangeFindField);
 
-        ImmutableMap<String, Object> context = ImmutableMap.of();
-        macroFormRenderer.renderRangeFindField(appendable, context, 
rangeFindField);
-        assertAndGetMacroString("renderRangeFindField", ImmutableMap.of(
-                "name", "FIELDNAME",
-                "className", "WIDGETSTYLE",
-                "alert", "true",
-                "value", "AAA",
-                "value2", "BBB"));
+        genericSingleMacroRenderedVerification();
+        genericTooltipRenderedVerification();
     }
 
     @Test
     public void lookupFieldMacroRendered() throws IOException {
-        // Deep stubs: renderLookupField()'s shouldApplyRequiredField() call 
cascades through
-        // localModelFormField.getModelForm().getType() unconditionally.
-        final ModelFormField localModelFormField = mock(ModelFormField.class, 
RETURNS_DEEP_STUBS);
         final ModelFormField.LookupField lookupField = 
mock(ModelFormField.LookupField.class);
-        
when(httpSession.getAttribute("delegatorName")).thenReturn("delegator");
+        when(renderableFtlFormElementsBuilder.lookupField(notNull(), 
eq(lookupField), eq(true))).thenReturn(genericMacroCall);
+        genericTooltipRenderedExpectation(lookupField);
 
-        when(lookupField.getModelFormField()).thenReturn(localModelFormField);
-        when(localModelFormField.getEntry(notNull(), 
isNull())).thenReturn("VALUE");
-        
when(localModelFormField.getParameterName(notNull())).thenReturn("FIELDNAME");
-        
when(localModelFormField.getCurrentContainerId(notNull())).thenReturn("CONTAINERID");
+        macroFormRenderer.renderLookupField(appendable, ImmutableMap.of(), 
lookupField);
 
-        ImmutableMap<String, Object> context = ImmutableMap.of("session", 
httpSession);
-        macroFormRenderer.renderLookupField(appendable, context, lookupField);
-        assertAndGetMacroString("renderLookupField", ImmutableMap.of(
-                "name", "FIELDNAME",
-                "value", "VALUE",
-                "id", "CONTAINERID"));
+        genericSingleMacroRenderedVerification();
+        genericTooltipRenderedVerification();
     }
 
     @Test
     public void renderNextPrevMacroRendered() throws IOException {
         final ModelForm modelForm = mock(ModelForm.class);
-        final String targetService = ""; // Leave empty to avoid CSRF token 
generation.
-        final String paginateIndexField = "PAGINATE_INDEX";
-        final String paginateSizeField = "PAGINATE_SIZE";
-
-        when(modelForm.getPaginateTarget(notNull())).thenReturn(targetService);
-        
when(modelForm.getMultiPaginateIndexField(notNull())).thenReturn(paginateIndexField);
+        when(renderableFtlFormElementsBuilder.nextPrev(notNull(), 
eq(modelForm), eq(true))).thenReturn(genericMacroCall);
 
-        Map<String, Object> context = new HashMap<>();
-        context.put("session", httpSession);
-        context.put(paginateIndexField, 0);
-        context.put(paginateSizeField, 30);
-        macroFormRenderer.renderNextPrev(appendable, context, modelForm);
+        macroFormRenderer.renderNextPrev(appendable, new HashMap<>(), 
modelForm);
 
-        assertAndGetMacroString("renderNextPrev");
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
     public void fileFieldMacroRendered() throws IOException {
         final ModelFormField.FileField fileField = 
mock(ModelFormField.FileField.class);
-        when(fileField.getModelFormField()).thenReturn(modelFormField);
-        
when(modelFormField.getParameterName(notNull())).thenReturn("FIELDNAME");
-        when(modelFormField.getEntry(notNull(), isNull())).thenReturn("VALUE");
-        when(modelFormField.getWidgetStyle()).thenReturn("WIDGETSTYLE");
+        when(renderableFtlFormElementsBuilder.fileField(notNull(), 
eq(fileField))).thenReturn(genericMacroCall);
+        genericTooltipRenderedExpectation(fileField);
 
         macroFormRenderer.renderFileField(appendable, ImmutableMap.of(), 
fileField);
 
-        assertAndGetMacroString("renderFileField", ImmutableMap.of(
-                "name", "FIELDNAME",
-                "value", "VALUE",
-                "className", "WIDGETSTYLE"));
+        genericSingleMacroRenderedVerification();
+        genericTooltipRenderedVerification();
     }
 
     @Test
     public void passwordFieldMacroRendered() throws IOException {
         final ModelFormField.PasswordField passwordField = 
mock(ModelFormField.PasswordField.class);
-        when(passwordField.getModelFormField()).thenReturn(modelFormField);
-        
when(modelFormField.getParameterName(notNull())).thenReturn("FIELDNAME");
-        when(modelFormField.getEntry(notNull(), isNull())).thenReturn("VALUE");
-        when(modelFormField.getWidgetStyle()).thenReturn("WIDGETSTYLE");
+        when(renderableFtlFormElementsBuilder.passwordField(notNull(), 
eq(passwordField))).thenReturn(genericMacroCall);
+        genericTooltipRenderedExpectation(passwordField);
 
         macroFormRenderer.renderPasswordField(appendable, ImmutableMap.of(), 
passwordField);
 
-        assertAndGetMacroString("renderPasswordField", ImmutableMap.of(
-                "name", "FIELDNAME",
-                "value", "VALUE",
-                "className", "WIDGETSTYLE"));
+        genericSingleMacroRenderedVerification();
+        genericTooltipRenderedVerification();
     }
 
     @Test
     public void imageFieldMacroRendered() throws IOException {
         final ModelFormField.ImageField imageField = 
mock(ModelFormField.ImageField.class);
-        when(imageField.getModelFormField()).thenReturn(modelFormField);
-        when(modelFormField.getEntry(notNull(), isNull())).thenReturn("VALUE");
+        when(renderableFtlFormElementsBuilder.imageField(notNull(), 
eq(imageField))).thenReturn(genericMacroCall);
+        genericTooltipRenderedExpectation(imageField);
 
         macroFormRenderer.renderImageField(appendable, ImmutableMap.of(), 
imageField);
 
-        assertAndGetMacroString("renderImageField", ImmutableMap.of("value", 
"VALUE"));
+        genericSingleMacroRenderedVerification();
+        genericTooltipRenderedVerification();
     }
 
     @Test
@@ -681,21 +632,13 @@ public final class MacroFormRendererTest {
     }
 
     @Test
-    public void renderNextPrevUsesQueryString() throws IOException {
-        final ModelForm modelForm = mock(ModelForm.class);
-        final String targetService = ""; // Leave empty to avoid CSRF token 
generation.
-        final String qbeString = "field1=value1&amp;field2=value2+with+spaces";
-        final String linkFromQbeString = "LinkFromQBEString";
+    public void bannerMacroRendered() throws IOException {
+        final ModelForm.Banner banner = mock(ModelForm.Banner.class);
+        when(renderableFtlFormElementsBuilder.banner(notNull(), 
eq(banner))).thenReturn(genericMacroCall);
 
-        when(modelForm.getPaginateTarget(notNull())).thenReturn(targetService);
-        when(requestHandler.makeLink(notNull(), notNull(), 
contains(qbeString))).thenReturn(linkFromQbeString);
-
-        final Map<String, Object> context = new HashMap<>();
-        context.put("_QBESTRING_", qbeString);
-        context.put("listSize", 100);
-        macroFormRenderer.renderNextPrev(appendable, context, modelForm);
+        macroFormRenderer.renderBanner(appendable, ImmutableMap.of(), banner);
 
-        assertAndGetMacroString("renderNextPrev", ImmutableMap.of("nextUrl", 
linkFromQbeString));
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
@@ -735,6 +678,19 @@ public final class MacroFormRendererTest {
         genericTooltipRenderedVerification();
     }
 
+    @Test
+    public void 
hyperlinkFieldPassesTheAlternateAndTheImageTitleWithoutEscapedQuotes() throws 
IOException {
+        final ModelFormField.HyperlinkField hyperlinkField = 
mock(ModelFormField.HyperlinkField.class);
+        when(hyperlinkField.getModelFormField()).thenReturn(modelFormField);
+        when(hyperlinkField.getAlternate(notNull())).thenReturn("say \"hi\"");
+        when(hyperlinkField.getImageTitle(notNull())).thenReturn("a 
\"title\"");
+
+        macroFormRenderer.renderHyperlinkField(appendable, new HashMap<>(), 
hyperlinkField);
+
+        verify(request).setAttribute("alternate", "say \"hi\"");
+        verify(request).setAttribute("imageTitle", "a \"title\"");
+    }
+
     @Test
     public void fieldTitleWithHeaderLinkRendersHyperlinkTitle() throws 
IOException {
         when(modelFormField.getTitle(notNull())).thenReturn("FIELDTITLE");
@@ -767,46 +723,6 @@ public final class MacroFormRendererTest {
         genericTooltipRenderedVerification();
     }
 
-    private String assertAndGetMacroString(final String expectedName) {
-        return assertAndGetMacroString(expectedName, ImmutableMap.of());
-    }
-
-    private String assertAndGetMacroString(final String expectedName, final 
Map<String, Object> expectedAttributes) {
-        ArgumentCaptor<String> macroCaptor = 
ArgumentCaptor.forClass(String.class);
-        verify(ftlWriter, atLeastOnce()).processFtlString(notNull(), isNull(), 
macroCaptor.capture());
-
-        // The captor accumulates every processFtlString() call made on this 
mock since the test
-        // began, not just those since the last check - unlike JMockit's 
per-block capture, so a test
-        // that renders more than one macro (or calls a render method twice) 
must look at the most
-        // recent invocation, not the first, to see the one this specific call 
actually produced.
-        List<String> macros = macroCaptor.getAllValues();
-        assertThat(macros, not(empty()));
-        final String macro = macros.get(macros.size() - 1);
-        assertThat(macro, startsWith("<@" + expectedName));
-
-        expectedAttributes.forEach((name, value) -> 
assertMacroAttribute(macro, name, value));
-
-        return macro;
-    }
-
-    private void assertMacroAttribute(final String macro, final String 
attributeName, final Object attributeValue) {
-        if (attributeValue instanceof Number) {
-            assertThat(macro, containsString(attributeName + "=" + 
attributeValue));
-        } else if (attributeValue instanceof List<?>) {
-            final String valueString = ((List<?>) attributeValue).stream()
-                    .map(Object::toString)
-                    .collect(Collectors.joining(",", "[", "]"));
-            assertThat(macro, containsString(attributeName + "=" + 
valueString));
-        } else if (attributeValue instanceof Boolean) {
-            assertThat(macro, containsString(attributeName + "=" + 
attributeValue));
-        } else if (attributeValue instanceof FreemarkerRawString) {
-            final String valueString = ((FreemarkerRawString) 
attributeValue).getRawString();
-            assertThat(macro, containsString(attributeName + "=\"" + 
valueString + "\""));
-        } else {
-            assertThat(macro, containsString(attributeName + "=\"" + 
attributeValue + "\""));
-        }
-    }
-
     /**
      * Assert that the generic MacroCall instance is passed to the macro 
executor. This is used for simple renderings
      * where MacroFormRenderer has FormMacroCallBuilder to construct a 
MacroCall and then passes it straight to the
@@ -828,16 +744,4 @@ public final class MacroFormRendererTest {
     private void genericSubHyperlinkRenderedVerification() {
         verify(ftlWriter).processFtl(appendable, null, 
genericHyperlinkMacroCall);
     }
-
-    static class FreemarkerRawString {
-        private final String rawString;
-
-        FreemarkerRawString(final String rawString) {
-            this.rawString = rawString;
-        }
-
-        public String getRawString() {
-            return rawString;
-        }
-    }
 }
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java
 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java
index 8389332225..a5d0e296ca 100644
--- 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java
@@ -20,6 +20,8 @@ package org.apache.ofbiz.widget.renderer.macro;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import org.apache.ofbiz.base.util.UtilCodec;
+import org.apache.ofbiz.base.util.UtilProperties;
 import org.apache.ofbiz.webapp.control.ConfigXMLReader;
 import org.apache.ofbiz.webapp.control.RequestHandler;
 import org.apache.ofbiz.widget.content.StaticContentUrlProvider;
@@ -32,6 +34,7 @@ import 
org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlMacroCall;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlNoop;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlString;
+import org.mockito.MockedStatic;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -49,6 +52,11 @@ import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.hasKey;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mockStatic;
 import static org.mockito.ArgumentMatchers.notNull;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
@@ -1064,4 +1072,314 @@ public final class RenderableFtlFormElementsBuilderTest 
{
         assertThat(((RenderableFtlString) link).getFtlString().contains("A 
${description}"), equalTo(true));
         
assertThat(renderableFtlFormElementsBuilder.postMultiFormContent(context) 
instanceof RenderableFtlString, equalTo(true));
     }
+
+    private static MockedStatic<UtilProperties> labelsReturningTheirKey() {
+        final MockedStatic<UtilProperties> utilProperties = 
mockStatic(UtilProperties.class, CALLS_REAL_METHODS);
+        utilProperties.when(() -> UtilProperties.getMessage(anyString(), 
anyString(), nullable(Locale.class)))
+                .thenAnswer(invocation -> invocation.<String>getArgument(1) + 
"_MESSAGE");
+        return utilProperties;
+    }
+
+    @Test
+    public void textFindFieldPassesTheValuesAsTheyAre() {
+        final ModelFormField.TextFindField textFindField = 
mock(ModelFormField.TextFindField.class);
+        when(textFindField.getModelFormField()).thenReturn(modelFormField);
+        when(textFindField.getDefaultOption(notNull())).thenReturn("contains");
+        when(textFindField.getSize()).thenReturn(20);
+        when(textFindField.getMaxlength()).thenReturn(5);
+        when(textFindField.getClientAutocompleteField()).thenReturn(true);
+        when(modelFormField.getEntry(notNull(), 
nullable(String.class))).thenReturn("say \"hi\" ${1+1}");
+        
when(modelFormField.getParameterName(notNull())).thenReturn("FIELDNAME");
+
+        try (MockedStatic<UtilProperties> ignored = labelsReturningTheirKey()) 
{
+            assertThat(renderableFtlFormElementsBuilder.textFindField(new 
HashMap<>(), textFindField),
+                    
MacroCallMatcher.hasNameAndParameters("renderTextFindField",
+                            
MacroCallParameterMatcher.hasNameAndStringValue("name", "FIELDNAME"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("value", "say \"hi\" ${1+1}"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("defaultOption", "contains"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("opEquals", "equals_MESSAGE"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("size", "20"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("maxlength", "5"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("autocomplete", ""),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("ignoreCase", 
"ignore_case_MESSAGE")));
+        }
+    }
+
+    @Test
+    public void textFindFieldWithHiddenOptionsHasNoOperatorLabels() {
+        final ModelFormField.TextFindField textFindField = 
mock(ModelFormField.TextFindField.class);
+        when(textFindField.getModelFormField()).thenReturn(modelFormField);
+        when(textFindField.getHideOptions()).thenReturn(true);
+        when(textFindField.getMaxlength()).thenReturn(null);
+
+        try (MockedStatic<UtilProperties> ignored = labelsReturningTheirKey()) 
{
+            assertThat(renderableFtlFormElementsBuilder.textFindField(new 
HashMap<>(), textFindField),
+                    
MacroCallMatcher.hasNameAndParameters("renderTextFindField",
+                            
MacroCallParameterMatcher.hasNameAndStringValue("opEquals", ""),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("opNotEqual", ""),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("maxlength", ""),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("autocomplete", "off")));
+        }
+    }
+
+    @Test
+    public void rangeFindFieldSetsBothValues() {
+        final ModelFormField.RangeFindField rangeFindField = 
mock(ModelFormField.RangeFindField.class);
+        when(rangeFindField.getModelFormField()).thenReturn(modelFormField);
+        when(rangeFindField.getMaxlength()).thenReturn(null);
+        when(rangeFindField.getDefaultOptionFrom()).thenReturn("greaterThan");
+        when(rangeFindField.getDefaultOptionThru()).thenReturn("lessThan");
+        when(modelFormField.getEntry(notNull(), 
nullable(String.class))).thenReturn("AAA ${1+1}");
+        when(modelFormField.getEntry(notNull())).thenReturn("BBB");
+
+        try (MockedStatic<UtilProperties> ignored = labelsReturningTheirKey()) 
{
+            assertThat(renderableFtlFormElementsBuilder.rangeFindField(new 
HashMap<>(), rangeFindField),
+                    
MacroCallMatcher.hasNameAndParameters("renderRangeFindField",
+                            
MacroCallParameterMatcher.hasNameAndStringValue("value", "AAA ${1+1}"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("value2", "BBB"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("maxlength", ""),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("defaultOptionFrom", 
"greaterThan"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("defaultOptionThru", 
"lessThan"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("opGreaterThanEquals", 
"greater_than_equals_MESSAGE")));
+        }
+    }
+
+    @Test
+    public void lookupFieldPassesTheTargetParametersAsAList() throws Exception 
{
+        final ModelFormField.LookupField lookupField = 
mock(ModelFormField.LookupField.class);
+        when(lookupField.getModelFormField()).thenReturn(modelFormField);
+        when(lookupField.getFormName(notNull())).thenReturn("LookupForm");
+        
when(lookupField.getTargetParameterList(notNull())).thenReturn(List.of("it's", 
"a\"b"));
+        when(lookupField.getLookupPresentation()).thenReturn(null);
+        when(lookupField.getLookupHeight()).thenReturn("300");
+        when(modelFormField.getEntry(notNull(), 
nullable(String.class))).thenReturn("VALUE");
+        
when(modelFormField.getCurrentContainerId(notNull())).thenReturn("CONTAINERID");
+        
when(modelFormField.getParameterName(notNull())).thenReturn("FIELDNAME");
+        
when(httpSession.getAttribute("delegatorName")).thenReturn("delegator");
+        
when(staticContentUrlProvider.pathAsContentUrlString("/images/fieldlookup.gif")).thenReturn("PREFIX/images/fieldlookup.gif");
+        final Map<String, Object> context = new HashMap<>();
+        context.put("session", httpSession);
+
+        final RenderableFtlMacroCall macroCall = 
macroCall(renderableFtlFormElementsBuilder.lookupField(context, lookupField, 
false));
+        assertThat(macroCall, 
MacroCallMatcher.hasNameAndParameters("renderLookupField",
+                MacroCallParameterMatcher.hasNameAndStringValue("name", 
"FIELDNAME"),
+                MacroCallParameterMatcher.hasNameAndStringValue("value", 
"VALUE"),
+                MacroCallParameterMatcher.hasNameAndStringValue("id", 
"CONTAINERID"),
+                MacroCallParameterMatcher.hasNameAndStringValue("imgSrc", 
"PREFIX/images/fieldlookup.gif"),
+                
MacroCallParameterMatcher.hasNameAndStringValue("presentation", ""),
+                MacroCallParameterMatcher.hasNameAndStringValue("height", 
"300"),
+                MacroCallParameterMatcher.hasNameAndStringValue("ajaxUrl", ""),
+                
MacroCallParameterMatcher.hasNameAndBooleanValue("ajaxEnabled", false),
+                
MacroCallParameterMatcher.hasNameAndStringValue("delegatorName", "delegator")));
+        assertThat(macroCall.getParameters().get("targetParameterIter"), 
equalTo(List.of("it's", "a\"b")));
+        assertThat(macroCall.getParameters(), not(hasKey("width")));
+        assertThat(macroCall.getParameters(), not(hasKey("position")));
+    }
+
+    @Test
+    public void lookupFieldOfARequiredFieldGetsTheRequiredStyleFirst() throws 
Exception {
+        final ModelFormField.LookupField lookupField = 
mock(ModelFormField.LookupField.class);
+        when(lookupField.getModelFormField()).thenReturn(modelFormField);
+        when(modelFormField.getWidgetStyle()).thenReturn("WIDGETSTYLE");
+        when(modelFormField.shouldBeRed(notNull())).thenReturn(true);
+        when(modelFormField.getModelForm().getType()).thenReturn("single");
+        when(modelFormField.getRequiredField()).thenReturn(true);
+        when(modelFormField.getRequiredFieldStyle()).thenReturn("REQ");
+        
when(httpSession.getAttribute("delegatorName")).thenReturn("delegator");
+        final Map<String, Object> context = new HashMap<>();
+        context.put("session", httpSession);
+
+        assertThat(renderableFtlFormElementsBuilder.lookupField(context, 
lookupField, false),
+                MacroCallMatcher.hasNameAndParameters("renderLookupField",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("className", "REQ WIDGETSTYLE"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("alert", "true")));
+    }
+
+    private ModelForm paginatedForm() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getPaginateTarget(notNull())).thenReturn(""); // empty 
to avoid the generation of a CSRF token
+        
when(modelForm.getMultiPaginateIndexField(notNull())).thenReturn("PAGINATE_INDEX");
+        
when(modelForm.getMultiPaginateSizeField(notNull())).thenReturn("PAGINATE_SIZE");
+        return modelForm;
+    }
+
+    private static Map<String, Object> paginationContext(final String 
queryString) {
+        final Map<String, Object> context = new HashMap<>();
+        context.put("PAGINATE_INDEX", 0);
+        context.put("PAGINATE_SIZE", 30);
+        context.put("listSize", 100);
+        context.put("_QBESTRING_", queryString);
+        return context;
+    }
+
+    @Test
+    public void nextPrevUsesTheQueryStringOfTheListForTheLinks() {
+        final ModelForm modelForm = paginatedForm();
+        final String queryString = 
"field1=value1&amp;field2=value2+with+spaces";
+        when(requestHandler.makeLink(notNull(), notNull(), 
contains(queryString))).thenReturn("LinkFromQBEString");
+
+        final RenderableFtlMacroCall macroCall = macroCall(
+                
renderableFtlFormElementsBuilder.nextPrev(paginationContext(queryString), 
modelForm, true));
+        assertThat(macroCall, 
MacroCallMatcher.hasNameAndParameters("renderNextPrev",
+                MacroCallParameterMatcher.hasNameAndStringValue("nextUrl", 
"LinkFromQBEString"),
+                MacroCallParameterMatcher.hasNameAndStringValue("lastUrl", 
"LinkFromQBEString"),
+                MacroCallParameterMatcher.hasNameAndStringValue("selectUrl", 
"LinkFromQBEString"),
+                MacroCallParameterMatcher.hasNameAndStringValue("firstUrl", 
""),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("viewSize", 
30),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("listSize", 
100),
+                
MacroCallParameterMatcher.hasNameAndBooleanValue("ajaxEnabled", false),
+                
MacroCallParameterMatcher.hasNameAndBooleanValue("javaScriptEnabled", true)));
+    }
+
+    @Test
+    public void nextPrevWithoutJavaScriptHasNoSelectLinks() {
+        final ModelForm modelForm = paginatedForm();
+        when(requestHandler.makeLink(notNull(), notNull(), 
anyString())).thenReturn("LINK");
+
+        
assertThat(renderableFtlFormElementsBuilder.nextPrev(paginationContext(""), 
modelForm, false),
+                MacroCallMatcher.hasNameAndParameters("renderNextPrev",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("nextUrl", "LINK"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("selectUrl", ""),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("selectSizeUrl", ""),
+                        
MacroCallParameterMatcher.hasNameAndBooleanValue("javaScriptEnabled", false)));
+    }
+
+    @Test
+    public void nextPrevSetsTheLabels() {
+        final ModelForm modelForm = paginatedForm();
+        when(modelForm.getPaginateNextLabel(notNull())).thenReturn("Next 
${label}");
+        final Map<String, Object> context = paginationContext("");
+        context.put("uiLabelMap", Map.of("CommonPage", "PAGE"));
+
+        try (MockedStatic<UtilProperties> utilProperties = 
mockStatic(UtilProperties.class, CALLS_REAL_METHODS)) {
+            utilProperties.when(() -> UtilProperties.getMessage(anyString(), 
anyString(), anyMap(), nullable(Locale.class)))
+                    .thenReturn("DISPLAYING");
+            assertThat(renderableFtlFormElementsBuilder.nextPrev(context, 
modelForm, false),
+                    MacroCallMatcher.hasNameAndParameters("renderNextPrev",
+                            
MacroCallParameterMatcher.hasNameAndStringValue("pageLabel", "PAGE"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("commonDisplaying", 
"DISPLAYING"),
+                            
MacroCallParameterMatcher.hasNameAndStringValue("paginateNextLabel", "Next 
${label}")));
+        }
+    }
+
+    @Test
+    public void nextPrevRendersNothingWhenThereIsNothingToPaginate() {
+        final ModelForm modelForm = paginatedForm();
+        when(modelForm.getOnPaginateUpdateAreas()).thenReturn(null);
+
+        
assertThat(renderableFtlFormElementsBuilder.nextPrev(paginationContext(""), 
modelForm, true), equalTo(RenderableFtlNoop.INSTANCE));
+    }
+
+    @Test
+    public void fileFieldPassesTheValueAsItIs() {
+        final ModelFormField.FileField fileField = 
mock(ModelFormField.FileField.class);
+        when(fileField.getModelFormField()).thenReturn(modelFormField);
+        when(fileField.getSize()).thenReturn(30);
+        when(fileField.getMaxlength()).thenReturn(null);
+        
when(modelFormField.getParameterName(notNull())).thenReturn("FIELDNAME");
+        when(modelFormField.getEntry(notNull(), 
nullable(String.class))).thenReturn("it's \"VALUE\" ${1+1}");
+        when(modelFormField.getWidgetStyle()).thenReturn("WIDGETSTYLE");
+
+        assertThat(renderableFtlFormElementsBuilder.fileField(new HashMap<>(), 
fileField),
+                MacroCallMatcher.hasNameAndParameters("renderFileField",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("name", "FIELDNAME"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("value", "it's \"VALUE\" 
${1+1}"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("className", "WIDGETSTYLE"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("size", "30"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("maxlength", "")));
+    }
+
+    @Test
+    public void passwordFieldOfARequiredFieldGetsTheRequiredStyleFirst() {
+        final ModelFormField.PasswordField passwordField = 
mock(ModelFormField.PasswordField.class);
+        when(passwordField.getModelFormField()).thenReturn(modelFormField);
+        when(modelFormField.getWidgetStyle()).thenReturn("WIDGETSTYLE");
+        when(modelFormField.shouldBeRed(notNull())).thenReturn(true);
+        when(modelFormField.getModelForm().getType()).thenReturn("single");
+        when(modelFormField.getRequiredField()).thenReturn(true);
+        when(modelFormField.getRequiredFieldStyle()).thenReturn("REQ");
+        when(modelFormField.getCurrentContainerId(notNull())).thenReturn(null);
+
+        assertThat(renderableFtlFormElementsBuilder.passwordField(new 
HashMap<>(), passwordField),
+                MacroCallMatcher.hasNameAndParameters("renderPasswordField",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("className", "REQ WIDGETSTYLE"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("alert", "true"),
+                        MacroCallParameterMatcher.hasNameAndStringValue("id", 
"")));
+    }
+
+    @Test
+    public void passwordFieldWithoutRequiredStyleUsesTheDefaultRequiredStyle() 
{
+        final ModelFormField.PasswordField passwordField = 
mock(ModelFormField.PasswordField.class);
+        when(passwordField.getModelFormField()).thenReturn(modelFormField);
+        when(modelFormField.getModelForm().getType()).thenReturn("upload");
+        when(modelFormField.getRequiredField()).thenReturn(true);
+        when(modelFormField.getRequiredFieldStyle()).thenReturn("");
+
+        assertThat(renderableFtlFormElementsBuilder.passwordField(new 
HashMap<>(), passwordField),
+                MacroCallMatcher.hasNameAndParameters("renderPasswordField",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("className", "required"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("alert", "false")));
+    }
+
+    @Test
+    public void imageFieldPrefixesTheValueAndPassesTheTextsAsTheyAre() {
+        final ModelFormField.ImageField imageField = 
mock(ModelFormField.ImageField.class);
+        when(imageField.getModelFormField()).thenReturn(modelFormField);
+        when(imageField.getDescription(notNull())).thenReturn("say \"hi\" 
${1+1}");
+        when(imageField.getStyle(notNull())).thenReturn("STYLE");
+        when(modelFormField.getEntry(notNull(), 
nullable(String.class))).thenReturn("img.png");
+        
when(staticContentUrlProvider.pathAsContentUrlString("img.png")).thenReturn("PREFIX/img.png");
+
+        assertThat(renderableFtlFormElementsBuilder.imageField(new 
HashMap<>(), imageField),
+                MacroCallMatcher.hasNameAndParameters("renderImageField",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("value", "PREFIX/img.png"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("description", "say \"hi\" 
${1+1}"),
+                        // without an alternate the description is used
+                        
MacroCallParameterMatcher.hasNameAndStringValue("alternate", "say \"hi\" 
${1+1}"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", "STYLE")));
+    }
+
+    @Test
+    public void 
imageFieldKeepsAnAbsoluteValueAndUsesTheTitleWithoutDescription() {
+        final ModelFormField.ImageField imageField = 
mock(ModelFormField.ImageField.class);
+        when(imageField.getModelFormField()).thenReturn(modelFormField);
+        when(modelFormField.getEntry(notNull(), 
nullable(String.class))).thenReturn("http://host/img.png";);
+        when(modelFormField.getTitle(notNull())).thenReturn("TITLE");
+
+        assertThat(renderableFtlFormElementsBuilder.imageField(new 
HashMap<>(), imageField),
+                MacroCallMatcher.hasNameAndParameters("renderImageField",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("value", "http://host/img.png";),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("description", "TITLE")));
+    }
+
+    @Test
+    public void imageFieldEncodesTheTextsOnlyOnceWithTheEncoderOfTheContext() {
+        final ModelFormField.ImageField imageField = 
mock(ModelFormField.ImageField.class);
+        when(imageField.getModelFormField()).thenReturn(modelFormField);
+        when(imageField.getDescription(notNull())).thenReturn("<b>");
+        when(modelFormField.getEncodeOutput()).thenReturn(true);
+        final Map<String, Object> context = new HashMap<>();
+        context.put("simpleEncoder", UtilCodec.getEncoder("html"));
+
+        assertThat(renderableFtlFormElementsBuilder.imageField(context, 
imageField),
+                MacroCallMatcher.hasNameAndParameters("renderImageField",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("description", "&lt;b&gt;")));
+    }
+
+    @Test
+    public void bannerUsesTheStyleWhenThereIsNoStyleForASide() {
+        final ModelForm.Banner banner = mock(ModelForm.Banner.class);
+        when(banner.getStyle(notNull())).thenReturn("STYLE");
+        when(banner.getRightTextStyle(notNull())).thenReturn("RIGHTSTYLE");
+        when(banner.getText(notNull())).thenReturn("TEXT ${1+1}");
+
+        assertThat(renderableFtlFormElementsBuilder.banner(new HashMap<>(), 
banner),
+                MacroCallMatcher.hasNameAndParameters("renderBanner",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", "STYLE"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("leftStyle", "STYLE"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("rightStyle", "RIGHTSTYLE"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("leftText", ""),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("text", "TEXT ${1+1}"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("rightText", "")));
+    }
 }

Reply via email to