You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ofbiz.apache.org by ad...@apache.org on 2015/01/18 22:03:42 UTC

svn commit: r1652852 [7/22] - in /ofbiz/trunk: applications/content/src/org/ofbiz/content/cms/ applications/content/src/org/ofbiz/content/content/ applications/content/src/org/ofbiz/content/data/ applications/content/src/org/ofbiz/content/output/ appli...

Added: ofbiz/trunk/framework/widget/src/org/ofbiz/widget/model/ModelFormField.java
URL: http://svn.apache.org/viewvc/ofbiz/trunk/framework/widget/src/org/ofbiz/widget/model/ModelFormField.java?rev=1652852&view=auto
==============================================================================
--- ofbiz/trunk/framework/widget/src/org/ofbiz/widget/model/ModelFormField.java (added)
+++ ofbiz/trunk/framework/widget/src/org/ofbiz/widget/model/ModelFormField.java Sun Jan 18 21:03:40 2015
@@ -0,0 +1,3806 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.ofbiz.widget.model;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.text.DateFormat;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+
+import org.ofbiz.base.conversion.ConversionException;
+import org.ofbiz.base.conversion.DateTimeConverters;
+import org.ofbiz.base.conversion.DateTimeConverters.StringToTimestamp;
+import org.ofbiz.base.util.BshUtil;
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.base.util.GeneralException;
+import org.ofbiz.base.util.ObjectType;
+import org.ofbiz.base.util.StringUtil;
+import org.ofbiz.base.util.UtilCodec;
+import org.ofbiz.base.util.UtilDateTime;
+import org.ofbiz.base.util.UtilFormatOut;
+import org.ofbiz.base.util.UtilGenerics;
+import org.ofbiz.base.util.UtilMisc;
+import org.ofbiz.base.util.UtilProperties;
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.base.util.UtilXml;
+import org.ofbiz.base.util.collections.FlexibleMapAccessor;
+import org.ofbiz.base.util.collections.MapStack;
+import org.ofbiz.base.util.string.FlexibleStringExpander;
+import org.ofbiz.entity.Delegator;
+import org.ofbiz.entity.GenericEntity;
+import org.ofbiz.entity.GenericEntityException;
+import org.ofbiz.entity.GenericValue;
+import org.ofbiz.entity.condition.EntityCondition;
+import org.ofbiz.entity.finder.EntityFinderUtil;
+import org.ofbiz.entity.model.ModelEntity;
+import org.ofbiz.entity.util.EntityUtil;
+import org.ofbiz.widget.WidgetWorker;
+import org.ofbiz.widget.model.CommonWidgetModels.AutoEntityParameters;
+import org.ofbiz.widget.model.CommonWidgetModels.AutoServiceParameters;
+import org.ofbiz.widget.model.CommonWidgetModels.Image;
+import org.ofbiz.widget.model.CommonWidgetModels.Link;
+import org.ofbiz.widget.model.CommonWidgetModels.Parameter;
+import org.ofbiz.widget.model.ModelForm.UpdateArea;
+import org.ofbiz.widget.renderer.FormStringRenderer;
+import org.w3c.dom.Element;
+
+import bsh.EvalError;
+import bsh.Interpreter;
+
+/**
+ * Models the <field> element.
+ * 
+ * @see <code>widget-form.xsd</code>
+ */
+public class ModelFormField {
+
+    /*
+     * ----------------------------------------------------------------------- *
+     *                     DEVELOPERS PLEASE READ
+     * ----------------------------------------------------------------------- *
+     * 
+     * This model is intended to be a read-only data structure that represents
+     * an XML element. Outside of object construction, the class should not
+     * have any behaviors. All behavior should be contained in model visitors.
+     * 
+     * Instances of this class will be shared by multiple threads - therefore
+     * it is immutable. DO NOT CHANGE THE OBJECT'S STATE AT RUN TIME!
+     * 
+     */
+
+    public static final String module = ModelFormField.class.getName();
+
+    public static ModelFormField from(ModelFormFieldBuilder builder) {
+        return new ModelFormField(builder);
+    }
+
+    private final FlexibleStringExpander action;
+    private final String attributeName;
+    private final boolean encodeOutput;
+    private final String entityName;
+    private final FlexibleMapAccessor<Object> entryAcsr;
+    private final String event;
+    private final FieldInfo fieldInfo;
+    private final String fieldName;
+    private final String headerLink;
+    private final String headerLinkStyle;
+    private final String idName;
+    private final FlexibleMapAccessor<Map<String, ? extends Object>> mapAcsr;
+    private final ModelForm modelForm;
+    private final String name;
+    private final List<UpdateArea> onChangeUpdateAreas;
+    private final List<UpdateArea> onClickUpdateAreas;
+    private final String parameterName;
+    private final Integer position;
+    private final String redWhen;
+    private final Boolean requiredField;
+    private final String requiredFieldStyle;
+    private final boolean separateColumn;
+    private final String serviceName;
+    private final Boolean sortField;
+    private final String sortFieldAscStyle;
+    private final String sortFieldDescStyle;
+    private final String sortFieldHelpText;
+    private final String sortFieldStyle;
+    private final FlexibleStringExpander title;
+    private final String titleAreaStyle;
+    private final String titleStyle;
+    private final FlexibleStringExpander tooltip;
+    private final String tooltipStyle;
+    private final FlexibleStringExpander useWhen;
+    private final String widgetAreaStyle;
+    private final String widgetStyle;
+
+    private ModelFormField(ModelFormFieldBuilder builder) {
+        this.action = builder.getAction();
+        this.attributeName = builder.getAttributeName();
+        this.encodeOutput = builder.getEncodeOutput();
+        this.entityName = builder.getEntityName();
+        this.entryAcsr = builder.getEntryAcsr();
+        this.event = builder.getEvent();
+        if (builder.getFieldInfo() != null) {
+            this.fieldInfo = builder.getFieldInfo().copy(this);
+        } else {
+            this.fieldInfo = null;
+        }
+        this.fieldName = builder.getFieldName();
+        this.headerLink = builder.getHeaderLink();
+        this.headerLinkStyle = builder.getHeaderLinkStyle();
+        this.idName = builder.getIdName();
+        this.mapAcsr = builder.getMapAcsr();
+        this.modelForm = builder.getModelForm();
+        this.name = builder.getName();
+        if (builder.getOnChangeUpdateAreas().isEmpty()) {
+            this.onChangeUpdateAreas = Collections.emptyList();
+        } else {
+            this.onChangeUpdateAreas = Collections.unmodifiableList(new ArrayList<UpdateArea>(builder.getOnChangeUpdateAreas()));
+        }
+        if (builder.getOnClickUpdateAreas().isEmpty()) {
+            this.onClickUpdateAreas = Collections.emptyList();
+        } else {
+            this.onClickUpdateAreas = Collections.unmodifiableList(new ArrayList<UpdateArea>(builder.getOnClickUpdateAreas()));
+        }
+        this.parameterName = builder.getParameterName();
+        this.position = builder.getPosition();
+        this.redWhen = builder.getRedWhen();
+        this.requiredField = builder.getRequiredField();
+        this.requiredFieldStyle = builder.getRequiredFieldStyle();
+        this.separateColumn = builder.getSeparateColumn();
+        this.serviceName = builder.getServiceName();
+        this.sortField = builder.getSortField();
+        this.sortFieldAscStyle = builder.getSortFieldAscStyle();
+        this.sortFieldDescStyle = builder.getSortFieldDescStyle();
+        this.sortFieldHelpText = builder.getSortFieldHelpText();
+        this.sortFieldStyle = builder.getSortFieldStyle();
+        this.title = builder.getTitle();
+        this.titleAreaStyle = builder.getTitleAreaStyle();
+        this.titleStyle = builder.getTitleStyle();
+        this.tooltip = builder.getTooltip();
+        this.tooltipStyle = builder.getTooltipStyle();
+        this.useWhen = builder.getUseWhen();
+        this.widgetAreaStyle = builder.getWidgetAreaStyle();
+        this.widgetStyle = builder.getWidgetStyle();
+    }
+
+    public FlexibleStringExpander getAction() {
+        return action;
+    }
+
+    public String getAction(Map<String, ? extends Object> context) {
+        if (UtilValidate.isNotEmpty(this.action))
+            return action.expandString(context);
+        return null;
+    }
+
+    /**
+     * Gets the name of the Service Attribute (aka Parameter) that corresponds
+     * with this field. This can be used to get additional information about the field.
+     * Use the getServiceName() method to get the Entity name that the field is in.
+     *
+     * @return returns the name of the Service Attribute 
+     */
+    public String getAttributeName() {
+        if (UtilValidate.isNotEmpty(this.attributeName))
+            return this.attributeName;
+        return this.name;
+    }
+
+    public String getCurrentContainerId(Map<String, Object> context) {
+        ModelForm modelForm = this.getModelForm();
+        String idName = FlexibleStringExpander.expandString(this.getIdName(), context);
+
+        if (modelForm != null) {
+            Integer itemIndex = (Integer) context.get("itemIndex");
+            if ("list".equals(modelForm.getType()) || "multi".equals(modelForm.getType())) {
+                if (itemIndex != null) {
+                    return idName + modelForm.getItemIndexSeparator() + itemIndex.intValue();
+                }
+            }
+        }
+        return idName;
+    }
+
+    public boolean getEncodeOutput() {
+        return this.encodeOutput;
+    }
+
+    public String getEntityName() {
+        if (UtilValidate.isNotEmpty(this.entityName))
+            return this.entityName;
+        return this.modelForm.getDefaultEntityName();
+    }
+
+    /**
+     * Gets the entry from the context that corresponds to this field; if this
+     * form is being rendered in an error condition (ie isError in the context
+     * is true) then the value will be retrieved from the parameters Map in
+     * the context.
+     *
+     * @param context the context
+     * @return returns the entry from the context that corresponds to this field
+     */
+    public String getEntry(Map<String, ? extends Object> context) {
+        return this.getEntry(context, "");
+    }
+
+    public String getEntry(Map<String, ? extends Object> context, String defaultValue) {
+        Boolean isError = (Boolean) context.get("isError");
+        Boolean useRequestParameters = (Boolean) context.get("useRequestParameters");
+
+        Locale locale = (Locale) context.get("locale");
+        if (locale == null)
+            locale = Locale.getDefault();
+        TimeZone timeZone = (TimeZone) context.get("timeZone");
+        if (timeZone == null)
+            timeZone = TimeZone.getDefault();
+
+        String returnValue;
+
+        // if useRequestParameters is TRUE then parameters will always be used, if FALSE then parameters will never be used
+        // if isError is TRUE and useRequestParameters is not FALSE (ie is null or TRUE) then parameters will be used
+        if ((Boolean.TRUE.equals(isError) && !Boolean.FALSE.equals(useRequestParameters))
+                || (Boolean.TRUE.equals(useRequestParameters))) {
+            //Debug.logInfo("Getting entry, isError true so getting from parameters for field " + this.getName() + " of form " + this.modelForm.getName(), module);
+            Map<String, Object> parameters = UtilGenerics.checkMap(context.get("parameters"), String.class, Object.class);
+            String parameterName = this.getParameterName(context);
+            if (parameters != null && parameters.get(parameterName) != null) {
+                Object parameterValue = parameters.get(parameterName);
+                if (parameterValue instanceof String) {
+                    returnValue = (String) parameterValue;
+                } else {
+                    // we might want to do something else here in the future, but for now this is probably best
+                    Debug.logWarning("Found a non-String parameter value for field [" + this.getModelForm().getName() + "."
+                            + this.getFieldName() + "]", module);
+                    returnValue = defaultValue;
+                }
+            } else {
+                returnValue = defaultValue;
+            }
+        } else {
+            //Debug.logInfo("Getting entry, isError false so getting from Map in context for field " + this.getName() + " of form " + this.modelForm.getName(), module);
+            Map<String, ? extends Object> dataMap = this.getMap(context);
+            boolean dataMapIsContext = false;
+            if (dataMap == null) {
+                //Debug.logInfo("Getting entry, no Map found with name " + this.getMapName() + ", using context for field " + this.getName() + " of form " + this.modelForm.getName(), module);
+                dataMap = context;
+                dataMapIsContext = true;
+            }
+            Object retVal = null;
+            if (UtilValidate.isNotEmpty(this.entryAcsr)) {
+                if (dataMap instanceof GenericEntity) {
+                    GenericEntity genEnt = (GenericEntity) dataMap;
+                    if (genEnt.getModelEntity().isField(this.entryAcsr.getOriginalName())) {
+                        retVal = genEnt.get(this.entryAcsr.getOriginalName(), locale);
+                    } else {
+                        //TODO: this may never come up, but if necessary use the FlexibleStringExander to eval the name first: String evaled = this.entryAcsr
+                    }
+                } else {
+                    retVal = this.entryAcsr.get(dataMap, locale);
+                }
+            } else {
+                // if no entry name was specified, use the field's name
+                if (dataMap.containsKey(this.name)) {
+                    retVal = dataMap.get(this.name);
+                }
+            }
+
+            // this is a special case to fill in fields during a create by default from parameters passed in
+            if (dataMapIsContext && retVal == null && !Boolean.FALSE.equals(useRequestParameters)) {
+                Map<String, ? extends Object> parameters = UtilGenerics.checkMap(context.get("parameters"));
+                if (parameters != null) {
+                    if (UtilValidate.isNotEmpty(this.entryAcsr))
+                        retVal = this.entryAcsr.get(parameters);
+                    else
+                        retVal = parameters.get(this.name);
+                }
+            }
+
+            if (retVal != null) {
+                // format string based on the user's locale and time zone
+                if (retVal instanceof Double || retVal instanceof Float || retVal instanceof BigDecimal) {
+                    NumberFormat nf = NumberFormat.getInstance(locale);
+                    nf.setMaximumFractionDigits(10);
+                    return nf.format(retVal);
+                } else if (retVal instanceof java.sql.Date) {
+                    DateFormat df = UtilDateTime.toDateFormat(UtilDateTime.DATE_FORMAT, timeZone, null);
+                    return df.format((java.util.Date) retVal);
+                } else if (retVal instanceof java.sql.Time) {
+                    DateFormat df = UtilDateTime.toTimeFormat(UtilDateTime.TIME_FORMAT, timeZone, null);
+                    return df.format((java.util.Date) retVal);
+                } else if (retVal instanceof java.sql.Timestamp) {
+                    DateFormat df = UtilDateTime.toDateTimeFormat(UtilDateTime.DATE_TIME_FORMAT, timeZone, null);
+                    return df.format((java.util.Date) retVal);
+                } else if (retVal instanceof java.util.Date) {
+                    DateFormat df = UtilDateTime.toDateTimeFormat("EEE MMM dd hh:mm:ss z yyyy", timeZone, null);
+                    return df.format((java.util.Date) retVal);
+                } else {
+                    returnValue = retVal.toString();
+                }
+            } else {
+                returnValue = defaultValue;
+            }
+        }
+
+        if (this.getEncodeOutput() && returnValue != null) {
+            UtilCodec.SimpleEncoder simpleEncoder = (UtilCodec.SimpleEncoder) context.get("simpleEncoder");
+            if (simpleEncoder != null)
+                returnValue = simpleEncoder.encode(returnValue);
+        }
+        return returnValue;
+    }
+
+    public FlexibleMapAccessor<Object> getEntryAcsr() {
+        return entryAcsr;
+    }
+
+    public String getEntryName() {
+        if (UtilValidate.isNotEmpty(this.entryAcsr))
+            return this.entryAcsr.getOriginalName();
+        return this.name;
+    }
+
+    public String getEvent() {
+        return event;
+    }
+
+    public FieldInfo getFieldInfo() {
+        return fieldInfo;
+    }
+
+    /**
+     * Gets the name of the Entity Field that corresponds
+     * with this field. This can be used to get additional information about the field.
+     * Use the getEntityName() method to get the Entity name that the field is in.
+     *
+     * @return return the name of the Entity Field that corresponds with this field
+     */
+    public String getFieldName() {
+        if (UtilValidate.isNotEmpty(this.fieldName))
+            return this.fieldName;
+        return this.name;
+    }
+
+    public String getHeaderLink() {
+        return headerLink;
+    }
+
+    public String getHeaderLinkStyle() {
+        return headerLinkStyle;
+    }
+
+    public String getIdName() {
+        if (UtilValidate.isNotEmpty(idName))
+            return idName;
+        return this.modelForm.getName() + "_" + this.getFieldName();
+    }
+
+    public Map<String, ? extends Object> getMap(Map<String, ? extends Object> context) {
+        if (UtilValidate.isEmpty(this.mapAcsr))
+            return this.modelForm.getDefaultMap(context); //Debug.logInfo("Getting Map from default of the form because of no mapAcsr for field " + this.getName(), module);
+
+        // Debug.logInfo("Getting Map from mapAcsr for field " + this.getName() + ", map-name=" + mapAcsr.getOriginalName() + ", context type=" + context.getClass().toString(), module);
+        Map<String, ? extends Object> result = null;
+        try {
+            result = mapAcsr.get(context);
+        } catch (java.lang.ClassCastException e) {
+            String errMsg = "Got an unexpected object type (not a Map) for map-name [" + mapAcsr.getOriginalName()
+                    + "] in field with name [" + this.getName() + "]: " + e.getMessage();
+            Debug.logError(errMsg, module);
+            throw new ClassCastException(errMsg);
+        }
+        return result;
+    }
+
+    public FlexibleMapAccessor<Map<String, ? extends Object>> getMapAcsr() {
+        return mapAcsr;
+    }
+
+    /** Get the name of the Map in the form context that contains the entry,
+     * available from the getEntryName() method. This entry is used to
+     * pre-populate the field widget when not in an error condition. In an
+     * error condition the parameter name is used to get the value from the
+     * parameters Map.
+     *
+     * @return returns the name of the Map in the form context that contains the entry
+     */
+    public String getMapName() {
+        if (UtilValidate.isNotEmpty(this.mapAcsr))
+            return this.mapAcsr.getOriginalName();
+        return this.modelForm.getDefaultMapName();
+    }
+
+    public ModelForm getModelForm() {
+        return modelForm;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public List<UpdateArea> getOnChangeUpdateAreas() {
+        return onChangeUpdateAreas;
+    }
+
+    public List<UpdateArea> getOnClickUpdateAreas() {
+        return onClickUpdateAreas;
+    }
+
+    public String getParameterName() {
+        return parameterName;
+    }
+
+    /**
+     * Get the name to use for the parameter for this field in the form interpreter.
+     * For HTML forms this is the request parameter name.
+     *
+     * @return returns the name to use for the parameter for this field in the form interpreter
+     */
+    public String getParameterName(Map<String, ? extends Object> context) {
+        String baseName;
+        if (UtilValidate.isNotEmpty(this.parameterName))
+            baseName = this.parameterName;
+        else
+            baseName = this.name;
+
+        Integer itemIndex = (Integer) context.get("itemIndex");
+        if (itemIndex != null && "multi".equals(this.modelForm.getType())) {
+            return baseName + this.modelForm.getItemIndexSeparator() + itemIndex.intValue();
+        } else {
+            return baseName;
+        }
+    }
+
+    public int getPosition() {
+        if (this.position == null)
+            return 1;
+        return position.intValue();
+    }
+
+    public String getRedWhen() {
+        return redWhen;
+    }
+
+    public boolean getRequiredField() {
+        return this.requiredField != null ? this.requiredField : false;
+    }
+
+    public String getRequiredFieldStyle() {
+        if (UtilValidate.isNotEmpty(this.requiredFieldStyle))
+            return this.requiredFieldStyle;
+        return this.modelForm.getDefaultRequiredFieldStyle();
+    }
+
+    public boolean getSeparateColumn() {
+        return this.separateColumn;
+    }
+
+    public String getServiceName() {
+        if (UtilValidate.isNotEmpty(this.serviceName))
+            return this.serviceName;
+        return this.modelForm.getDefaultServiceName();
+    }
+
+    public Boolean getSortField() {
+        return sortField;
+    }
+
+    public String getSortFieldAscStyle() {
+        return sortFieldAscStyle;
+    }
+
+    public String getSortFieldDescStyle() {
+        return sortFieldDescStyle;
+    }
+
+    public String getSortFieldHelpText() {
+        return sortFieldHelpText;
+    }
+
+    public String getSortFieldHelpText(Map<String, Object> context) {
+        return FlexibleStringExpander.expandString(this.sortFieldHelpText, context);
+    }
+
+    public String getSortFieldStyle() {
+        if (UtilValidate.isNotEmpty(this.sortFieldStyle))
+            return this.sortFieldStyle;
+        return this.modelForm.getDefaultSortFieldStyle();
+    }
+
+    public String getSortFieldStyleAsc() {
+        if (UtilValidate.isNotEmpty(this.sortFieldAscStyle))
+            return this.sortFieldAscStyle;
+        return this.modelForm.getDefaultSortFieldAscStyle();
+    }
+
+    public String getSortFieldStyleDesc() {
+        if (UtilValidate.isNotEmpty(this.sortFieldDescStyle))
+            return this.sortFieldDescStyle;
+        return this.modelForm.getDefaultSortFieldDescStyle();
+    }
+
+    public FlexibleStringExpander getTitle() {
+        return title;
+    }
+
+    public String getTitle(Map<String, Object> context) {
+        if (UtilValidate.isNotEmpty(this.title))
+            return title.expandString(context);
+
+        // create a title from the name of this field; expecting a Java method/field style name, ie productName or productCategoryId
+        if (UtilValidate.isEmpty(this.name))
+            return ""; // this should never happen, ie name is required
+
+        // search for a localized label for the field's name
+        Map<String, String> uiLabelMap = UtilGenerics.checkMap(context.get("uiLabelMap"));
+        if (uiLabelMap != null) {
+            String titleFieldName = "FormFieldTitle_" + this.name;
+            String localizedName = uiLabelMap.get(titleFieldName);
+            if (!localizedName.equals(titleFieldName)) {
+                return localizedName;
+            }
+        } else {
+            Debug.logWarning("Could not find uiLabelMap in context while rendering form " + this.modelForm.getName(), module);
+        }
+
+        // create a title from the name of this field; expecting a Java method/field style name, ie productName or productCategoryId
+        StringBuilder autoTitlewriter = new StringBuilder();
+
+        // always use upper case first letter...
+        autoTitlewriter.append(Character.toUpperCase(this.name.charAt(0)));
+
+        // just put spaces before the upper case letters
+        for (int i = 1; i < this.name.length(); i++) {
+            char curChar = this.name.charAt(i);
+            if (Character.isUpperCase(curChar)) {
+                autoTitlewriter.append(' ');
+            }
+            autoTitlewriter.append(curChar);
+        }
+
+        return autoTitlewriter.toString();
+    }
+
+    public String getTitleAreaStyle() {
+        if (UtilValidate.isNotEmpty(this.titleAreaStyle))
+            return this.titleAreaStyle;
+        return this.modelForm.getDefaultTitleAreaStyle();
+    }
+
+    public String getTitleStyle() {
+        if (UtilValidate.isNotEmpty(this.titleStyle))
+            return this.titleStyle;
+        return this.modelForm.getDefaultTitleStyle();
+    }
+
+    public FlexibleStringExpander getTooltip() {
+        return tooltip;
+    }
+
+    public String getTooltip(Map<String, Object> context) {
+        String tooltipString = "";
+        if (UtilValidate.isNotEmpty(tooltip))
+            tooltipString = tooltip.expandString(context);
+        if (this.getEncodeOutput()) {
+            UtilCodec.SimpleEncoder simpleEncoder = (UtilCodec.SimpleEncoder) context.get("simpleEncoder");
+            if (simpleEncoder != null)
+                tooltipString = simpleEncoder.encode(tooltipString);
+        }
+        return tooltipString;
+    }
+
+    public String getTooltipStyle() {
+        if (UtilValidate.isNotEmpty(this.tooltipStyle))
+            return this.tooltipStyle;
+        return this.modelForm.getDefaultTooltipStyle();
+    }
+
+    public FlexibleStringExpander getUseWhen() {
+        return useWhen;
+    }
+
+    public String getUseWhen(Map<String, Object> context) {
+        if (UtilValidate.isNotEmpty(this.useWhen))
+            return this.useWhen.expandString(context);
+        return "";
+    }
+
+    public String getWidgetAreaStyle() {
+        if (UtilValidate.isNotEmpty(this.widgetAreaStyle))
+            return this.widgetAreaStyle;
+        return this.modelForm.getDefaultWidgetAreaStyle();
+    }
+
+    public String getWidgetStyle() {
+        if (UtilValidate.isNotEmpty(this.widgetStyle))
+            return this.widgetStyle;
+        return this.modelForm.getDefaultWidgetStyle();
+    }
+
+    /**
+     * Checks if field is a row submit field.
+     */
+    public boolean isRowSubmit() {
+        if (!"multi".equals(getModelForm().getType()))
+            return false;
+        if (getFieldInfo().getFieldType() != FieldInfo.CHECK)
+            return false;
+        if (!CheckField.ROW_SUBMIT_FIELD_NAME.equals(getName()))
+            return false;
+        return true;
+    }
+
+    public boolean isSortField() {
+        return this.sortField != null && this.sortField.booleanValue();
+    }
+
+    public boolean isUseWhenEmpty() {
+        if (this.useWhen == null) {
+            return true;
+        }
+
+        return this.useWhen.isEmpty();
+    }
+
+    public void renderFieldString(Appendable writer, Map<String, Object> context, FormStringRenderer formStringRenderer)
+            throws IOException {
+        this.fieldInfo.renderFieldString(writer, context, formStringRenderer);
+    }
+
+    /**
+     * the widget/interaction part will be red if the date value is
+     *  before-now (for ex. thruDate), after-now (for ex. fromDate), or by-name (if the
+     *  field's name or entry-name or fromDate or thruDate the corresponding
+     *  action will be done); only applicable when the field is a timestamp
+     *
+     * @param context the context
+     * @return true if the field should be read otherwise false
+     */
+    public boolean shouldBeRed(Map<String, Object> context) {
+        // red-when (never | before-now | after-now | by-name) "by-name"
+
+        String redCondition = this.redWhen;
+
+        if ("never".equals(redCondition))
+            return false;
+
+        // for performance resaons we check this first, most fields will be eliminated here and the valueOfs will not be necessary
+        if (UtilValidate.isEmpty(redCondition) || "by-name".equals(redCondition)) {
+            if ("fromDate".equals(this.name) || (this.entryAcsr != null && "fromDate".equals(this.entryAcsr.getOriginalName()))) {
+                redCondition = "after-now";
+            } else if ("thruDate".equals(this.name)
+                    || (this.entryAcsr != null && "thruDate".equals(this.entryAcsr.getOriginalName()))) {
+                redCondition = "before-now";
+            } else {
+                return false;
+            }
+        }
+
+        boolean isBeforeNow = false;
+        if ("before-now".equals(redCondition)) {
+            isBeforeNow = true;
+        } else if ("after-now".equals(redCondition)) {
+            isBeforeNow = false;
+        } else {
+            return false;
+        }
+
+        java.sql.Date dateVal = null;
+        java.sql.Time timeVal = null;
+        java.sql.Timestamp timestampVal = null;
+
+        //now before going on, check to see if the current entry is a valid date and/or time and get the value
+        String value = this.getEntry(context, null);
+        try {
+            timestampVal = java.sql.Timestamp.valueOf(value);
+        } catch (Exception e) {
+            // okay, not a timestamp...
+        }
+
+        if (timestampVal == null) {
+            try {
+                dateVal = java.sql.Date.valueOf(value);
+            } catch (Exception e) {
+                // okay, not a date...
+            }
+        }
+
+        if (timestampVal == null && dateVal == null) {
+            try {
+                timeVal = java.sql.Time.valueOf(value);
+            } catch (Exception e) {
+                // okay, not a time...
+            }
+        }
+
+        if (timestampVal == null && dateVal == null && timeVal == null) {
+            return false;
+        }
+
+        long nowMillis = System.currentTimeMillis();
+        if (timestampVal != null) {
+            java.sql.Timestamp nowStamp = new java.sql.Timestamp(nowMillis);
+            if (!timestampVal.equals(nowStamp)) {
+                if (isBeforeNow) {
+                    if (timestampVal.before(nowStamp)) {
+                        return true;
+                    }
+                } else {
+                    if (timestampVal.after(nowStamp)) {
+                        return true;
+                    }
+                }
+            }
+        } else if (dateVal != null) {
+            java.sql.Date nowDate = new java.sql.Date(nowMillis);
+            if (!dateVal.equals(nowDate)) {
+                if (isBeforeNow) {
+                    if (dateVal.before(nowDate)) {
+                        return true;
+                    }
+                } else {
+                    if (dateVal.after(nowDate)) {
+                        return true;
+                    }
+                }
+            }
+        } else if (timeVal != null) {
+            java.sql.Time nowTime = new java.sql.Time(nowMillis);
+            if (!timeVal.equals(nowTime)) {
+                if (isBeforeNow) {
+                    if (timeVal.before(nowTime)) {
+                        return true;
+                    }
+                } else {
+                    if (timeVal.after(nowTime)) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    public boolean shouldUse(Map<String, Object> context) {
+        String useWhenStr = this.getUseWhen(context);
+        if (UtilValidate.isEmpty(useWhenStr))
+            return true;
+
+        try {
+            Interpreter bsh = this.modelForm.getBshInterpreter(context);
+            Object retVal = bsh.eval(StringUtil.convertOperatorSubstitutions(useWhenStr));
+            boolean condTrue = false;
+            // retVal should be a Boolean, if not something weird is up...
+            if (retVal instanceof Boolean) {
+                Boolean boolVal = (Boolean) retVal;
+                condTrue = boolVal.booleanValue();
+            } else {
+                throw new IllegalArgumentException("Return value from use-when condition eval was not a Boolean: "
+                        + (retVal != null ? retVal.getClass().getName() : "null") + " [" + retVal + "] on the field " + this.name
+                        + " of form " + this.modelForm.getName());
+            }
+
+            return condTrue;
+        } catch (EvalError e) {
+            String errMsg = "Error evaluating BeanShell use-when condition [" + useWhenStr + "] on the field " + this.name
+                    + " of form " + this.modelForm.getName() + ": " + e.toString();
+            Debug.logError(e, errMsg, module);
+            //Debug.logError("For use-when eval error context is: " + context, module);
+            throw new IllegalArgumentException(errMsg);
+        }
+    }
+
+    /**
+     * Models the &lt;auto-complete&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class AutoComplete {
+        private final String autoSelect;
+        private final String choices;
+        private final String frequency;
+        private final String fullSearch;
+        private final String ignoreCase;
+        private final String minChars;
+        private final String partialChars;
+        private final String partialSearch;
+
+        public AutoComplete(Element element) {
+            this.autoSelect = element.getAttribute("auto-select");
+            this.frequency = element.getAttribute("frequency");
+            this.minChars = element.getAttribute("min-chars");
+            this.choices = element.getAttribute("choices");
+            this.partialSearch = element.getAttribute("partial-search");
+            this.partialChars = element.getAttribute("partial-chars");
+            this.ignoreCase = element.getAttribute("ignore-case");
+            this.fullSearch = element.getAttribute("full-search");
+        }
+
+        public String getAutoSelect() {
+            return this.autoSelect;
+        }
+
+        public String getChoices() {
+            return this.choices;
+        }
+
+        public String getFrequency() {
+            return this.frequency;
+        }
+
+        public String getFullSearch() {
+            return this.fullSearch;
+        }
+
+        public String getIgnoreCase() {
+            return this.ignoreCase;
+        }
+
+        public String getMinChars() {
+            return this.minChars;
+        }
+
+        public String getPartialChars() {
+            return this.partialChars;
+        }
+
+        public String getPartialSearch() {
+            return this.partialSearch;
+        }
+    }
+
+    /**
+     * Models the &lt;check&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class CheckField extends FieldInfoWithOptions {
+        public final static String ROW_SUBMIT_FIELD_NAME = "_rowSubmit";
+        private final FlexibleStringExpander allChecked;
+
+        private CheckField(CheckField original, ModelFormField modelFormField) {
+            super(original, modelFormField);
+            this.allChecked = original.allChecked;
+        }
+
+        public CheckField(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+            allChecked = FlexibleStringExpander.getInstance(element.getAttribute("all-checked"));
+        }
+
+        public CheckField(int fieldSource, ModelFormField modelFormField) {
+            super(fieldSource, FieldInfo.CHECK, modelFormField);
+            this.allChecked = FlexibleStringExpander.getInstance("");
+        }
+
+        public CheckField(ModelFormField modelFormField) {
+            super(FieldInfo.SOURCE_EXPLICIT, FieldInfo.CHECK, modelFormField);
+            this.allChecked = FlexibleStringExpander.getInstance("");
+        }
+
+        @Override
+        public void accept(ModelFieldVisitor visitor) throws Exception {
+            visitor.visit(this);
+        }
+
+        @Override
+        public FieldInfo copy(ModelFormField modelFormField) {
+            return new CheckField(this, modelFormField);
+        }
+
+        public FlexibleStringExpander getAllChecked() {
+            return allChecked;
+        }
+
+        public Boolean isAllChecked(Map<String, Object> context) {
+            String allCheckedStr = this.allChecked.expandString(context);
+            if (!allCheckedStr.isEmpty())
+                return Boolean.valueOf("true".equals(allCheckedStr));
+            else
+                return null;
+        }
+
+        @Override
+        public void renderFieldString(Appendable writer, Map<String, Object> context, FormStringRenderer formStringRenderer)
+                throws IOException {
+            formStringRenderer.renderCheckField(writer, context, this);
+        }
+    }
+
+    /**
+     * Models the &lt;container&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class ContainerField extends FieldInfo {
+
+        private ContainerField(ContainerField original, ModelFormField modelFormField) {
+            super(original.getFieldSource(), original.getFieldType(), modelFormField);
+        }
+
+        public ContainerField(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+        }
+
+        public ContainerField(int fieldSource, int fieldType, ModelFormField modelFormField) {
+            super(fieldSource, fieldType, modelFormField);
+        }
+
+        @Override
+        public void accept(ModelFieldVisitor visitor) throws Exception {
+            visitor.visit(this);
+        }
+
+        @Override
+        public FieldInfo copy(ModelFormField modelFormField) {
+            return new ContainerField(this, modelFormField);
+        }
+
+        @Override
+        public void renderFieldString(Appendable writer, Map<String, Object> context, FormStringRenderer formStringRenderer)
+                throws IOException {
+            formStringRenderer.renderContainerFindField(writer, context, this);
+        }
+    }
+
+    /**
+     * Models the &lt;date-find&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class DateFindField extends DateTimeField {
+        private final String defaultOptionFrom;
+        private final String defaultOptionThru;
+
+        private DateFindField(DateFindField original, ModelFormField modelFormField) {
+            super(original, modelFormField);
+            this.defaultOptionFrom = original.defaultOptionFrom;
+            this.defaultOptionThru = original.defaultOptionThru;
+        }
+
+        public DateFindField(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+            this.defaultOptionFrom = element.getAttribute("default-option-from");
+            this.defaultOptionThru = element.getAttribute("default-option-thru");
+        }
+
+        public DateFindField(int fieldSource, ModelFormField modelFormField) {
+            super(fieldSource, modelFormField);
+            this.defaultOptionFrom = "greaterThanEqualTo";
+            this.defaultOptionThru = "lessThanEqualTo";
+        }
+
+        public DateFindField(int fieldSource, String type) {
+            super(fieldSource, type);
+            this.defaultOptionFrom = "greaterThanEqualTo";
+            this.defaultOptionThru = "lessThanEqualTo";
+        }
+
+        @Override
+        public void accept(ModelFieldVisitor visitor) throws Exception {
+            visitor.visit(this);
+        }
+
+        @Override
+        public FieldInfo copy(ModelFormField modelFormField) {
+            return new DateFindField(this, modelFormField);
+        }
+
+        public String getDefaultOptionFrom() {
+            return this.defaultOptionFrom;
+        }
+
+        public String getDefaultOptionThru() {
+            return this.defaultOptionThru;
+        }
+
+        @Override
+        public void renderFieldString(Appendable writer, Map<String, Object> context, FormStringRenderer formStringRenderer)
+                throws IOException {
+            formStringRenderer.renderDateFindField(writer, context, this);
+        }
+    }
+
+    /**
+     * Models the &lt;date-time&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class DateTimeField extends FieldInfo {
+        private final String clock;
+        private final FlexibleStringExpander defaultValue;
+        private final String inputMethod;
+        private final String mask;
+        private final String step;
+        private final String type;
+
+        protected DateTimeField(DateTimeField original, ModelFormField modelFormField) {
+            super(original.getFieldSource(), original.getFieldType(), modelFormField);
+            this.defaultValue = original.defaultValue;
+            this.type = original.type;
+            this.inputMethod = original.inputMethod;
+            this.clock = original.clock;
+            this.mask = original.mask;
+            this.step = original.step;
+        }
+
+        public DateTimeField(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+            this.defaultValue = FlexibleStringExpander.getInstance(element.getAttribute("default-value"));
+            this.type = element.getAttribute("type");
+            this.inputMethod = element.getAttribute("input-method");
+            this.clock = element.getAttribute("clock");
+            this.mask = element.getAttribute("mask");
+            String step = element.getAttribute("step");
+            if (step.isEmpty()) {
+                step = "1";
+            }
+            this.step = step;
+        }
+
+        public DateTimeField(int fieldSource, ModelFormField modelFormField) {
+            super(fieldSource, FieldInfo.DATE_TIME, modelFormField);
+            this.defaultValue = FlexibleStringExpander.getInstance("");
+            this.type = "";
+            this.inputMethod = "";
+            this.clock = "";
+            this.mask = "";
+            this.step = "1";
+        }
+
+        public DateTimeField(int fieldSource, String type) {
+            super(fieldSource, FieldInfo.DATE_TIME, null);
+            this.defaultValue = FlexibleStringExpander.getInstance("");
+            this.type = type;
+            this.inputMethod = "";
+            this.clock = "";
+            this.mask = "";
+            this.step = "1";
+        }
+
+        public DateTimeField(ModelFormField modelFormField) {
+            super(FieldInfo.SOURCE_EXPLICIT, FieldInfo.DATE_TIME, modelFormField);
+            this.defaultValue = FlexibleStringExpander.getInstance("");
+            this.type = "";
+            this.inputMethod = "";
+            this.clock = "";
+            this.mask = "";
+            this.step = "1";
+        }
+
+        @Override
+        public void accept(ModelFieldVisitor visitor) throws Exception {
+            visitor.visit(this);
+        }
+
+        @Override
+        public FieldInfo copy(ModelFormField modelFormField) {
+            return new DateTimeField(this, modelFormField);
+        }
+
+        public String getClock() {
+            return this.clock;
+        }
+
+        /**
+         * Returns the default-value if specified, otherwise the current date, time or timestamp
+         *
+         * @param context Context Map
+         * @return Default value string for date-time
+         */
+        public String getDefaultDateTimeString(Map<String, Object> context) {
+            if (UtilValidate.isNotEmpty(this.defaultValue))
+                return this.getDefaultValue(context);
+
+            if ("date".equals(this.type))
+                return (new java.sql.Date(System.currentTimeMillis())).toString();
+            else if ("time".equals(this.type))
+                return (new java.sql.Time(System.currentTimeMillis())).toString();
+            else
+                return UtilDateTime.nowTimestamp().toString();
+        }
+
+        public FlexibleStringExpander getDefaultValue() {
+            return defaultValue;
+        }
+
+        public String getDefaultValue(Map<String, Object> context) {
+            if (this.defaultValue != null) {
+                return this.defaultValue.expandString(context);
+            } else {
+                return "";
+            }
+        }
+
+        public String getInputMethod() {
+            return this.inputMethod;
+        }
+
+        public String getMask() {
+            return this.mask;
+        }
+
+        public String getStep() {
+            return this.step;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        @Override
+        public void renderFieldString(Appendable writer, Map<String, Object> context, FormStringRenderer formStringRenderer)
+                throws IOException {
+            formStringRenderer.renderDateTimeField(writer, context, this);
+        }
+    }
+
+    /**
+     * Models the &lt;display-entity&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class DisplayEntityField extends DisplayField {
+        private final boolean cache;
+        private final String entityName;
+        private final String keyFieldName;
+        private final SubHyperlink subHyperlink;
+
+        private DisplayEntityField(DisplayEntityField original, ModelFormField modelFormField) {
+            super(original, modelFormField);
+            this.cache = original.cache;
+            this.entityName = original.entityName;
+            this.keyFieldName = original.keyFieldName;
+            if (original.subHyperlink != null) {
+                this.subHyperlink = new SubHyperlink(original.subHyperlink, modelFormField);
+            } else {
+                this.subHyperlink = null;
+            }
+        }
+
+        public DisplayEntityField(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+            this.cache = !"false".equals(element.getAttribute("cache"));
+            this.entityName = element.getAttribute("entity-name");
+            this.keyFieldName = element.getAttribute("key-field-name");
+            Element subHyperlinkElement = UtilXml.firstChildElement(element, "sub-hyperlink");
+            if (subHyperlinkElement != null) {
+                this.subHyperlink = new SubHyperlink(subHyperlinkElement, modelFormField);
+            } else {
+                this.subHyperlink = null;
+            }
+        }
+
+        public DisplayEntityField(int fieldSource, ModelFormField modelFormField) {
+            super(fieldSource, FieldInfo.DISPLAY_ENTITY, modelFormField);
+            this.cache = true;
+            this.entityName = "";
+            this.keyFieldName = "";
+            this.subHyperlink = null;
+        }
+
+        public DisplayEntityField(ModelFormField modelFormField) {
+            super(FieldInfo.SOURCE_EXPLICIT, FieldInfo.DISPLAY_ENTITY, modelFormField);
+            this.cache = true;
+            this.entityName = "";
+            this.keyFieldName = "";
+            this.subHyperlink = null;
+        }
+
+        @Override
+        public void accept(ModelFieldVisitor visitor) throws Exception {
+            visitor.visit(this);
+        }
+
+        @Override
+        public FieldInfo copy(ModelFormField modelFormField) {
+            return new DisplayEntityField(this, modelFormField);
+        }
+
+        public boolean getCache() {
+            return cache;
+        }
+
+        @Override
+        public String getDescription(Map<String, Object> context) {
+            Locale locale = UtilMisc.ensureLocale(context.get("locale"));
+
+            // rather than using the context to expand the string, lookup the given entity and use it to expand the string
+            GenericValue value = null;
+            String fieldKey = this.keyFieldName;
+            if (UtilValidate.isEmpty(fieldKey))
+                fieldKey = getModelFormField().fieldName;
+
+            Delegator delegator = WidgetWorker.getDelegator(context);
+            String fieldValue = getModelFormField().getEntry(context);
+            try {
+                value = delegator.findOne(this.entityName, this.cache, fieldKey, fieldValue);
+            } catch (GenericEntityException e) {
+                String errMsg = "Error getting value from the database for display of field [" + getModelFormField().getName()
+                        + "] on form [" + getModelFormField().modelForm.getName() + "]: " + e.toString();
+                Debug.logError(e, errMsg, module);
+                throw new IllegalArgumentException(errMsg);
+            }
+
+            String retVal = null;
+            if (value != null) {
+                // expanding ${} stuff, passing locale explicitly to expand value string because it won't be found in the Entity
+                MapStack<String> localContext = MapStack.create(context);
+                // Rendering code might try to modify the GenericEntity instance,
+                // so we make a copy of it.
+                Map<String, Object> genericEntityClone = UtilGenerics.cast(value.clone());
+                localContext.push(genericEntityClone);
+
+                // expand with the new localContext, which is locale aware
+                retVal = this.getDescription().expandString(localContext, locale);
+            }
+            // try to get the entry for the field if description doesn't expand to anything
+            if (UtilValidate.isEmpty(retVal))
+                retVal = fieldValue;
+            if (UtilValidate.isEmpty(retVal))
+                retVal = "";
+            return retVal;
+        }
+
+        public String getEntityName() {
+            return entityName;
+        }
+
+        public String getKeyFieldName() {
+            return keyFieldName;
+        }
+
+        public SubHyperlink getSubHyperlink() {
+            return this.subHyperlink;
+        }
+    }
+
+    /**
+     * Models the &lt;display&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class DisplayField extends FieldInfo {
+        private final boolean alsoHidden;
+        private final FlexibleStringExpander currency;
+        private final FlexibleStringExpander date;
+        private final FlexibleStringExpander defaultValue;
+        private final FlexibleStringExpander description;
+        private final FlexibleStringExpander imageLocation;
+        private final InPlaceEditor inPlaceEditor;
+        private final String size; // maximum number of characters to display
+        private final String type; // matches type of field, currently text or currency
+
+        protected DisplayField(DisplayField original, ModelFormField modelFormField) {
+            super(original.getFieldSource(), original.getFieldType(), modelFormField);
+            this.alsoHidden = original.alsoHidden;
+            this.currency = original.currency;
+            this.date = original.date;
+            this.defaultValue = original.defaultValue;
+            this.description = original.description;
+            this.imageLocation = original.imageLocation;
+            this.inPlaceEditor = original.inPlaceEditor;
+            this.size = original.size;
+            this.type = original.type;
+        }
+
+        public DisplayField(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+            this.alsoHidden = !"false".equals(element.getAttribute("also-hidden"));
+            this.currency = FlexibleStringExpander.getInstance(element.getAttribute("currency"));
+            this.date = FlexibleStringExpander.getInstance(element.getAttribute("date"));
+            this.defaultValue = FlexibleStringExpander.getInstance(element.getAttribute("default-value"));
+            this.description = FlexibleStringExpander.getInstance(element.getAttribute("description"));
+            this.imageLocation = FlexibleStringExpander.getInstance(element.getAttribute("image-location"));
+            Element inPlaceEditorElement = UtilXml.firstChildElement(element, "in-place-editor");
+            if (inPlaceEditorElement != null) {
+                this.inPlaceEditor = new InPlaceEditor(inPlaceEditorElement);
+            } else {
+                this.inPlaceEditor = null;
+            }
+            this.size = element.getAttribute("size");
+            this.type = element.getAttribute("type");
+        }
+
+        public DisplayField(int fieldSource, int fieldType, ModelFormField modelFormField) {
+            super(fieldSource, fieldType, modelFormField);
+            this.alsoHidden = true;
+            this.currency = FlexibleStringExpander.getInstance("");
+            this.date = FlexibleStringExpander.getInstance("");
+            this.defaultValue = FlexibleStringExpander.getInstance("");
+            this.description = FlexibleStringExpander.getInstance("");
+            this.imageLocation = FlexibleStringExpander.getInstance("");
+            this.inPlaceEditor = null;
+            this.size = "";
+            this.type = "";
+        }
+
+        public DisplayField(int fieldSource, ModelFormField modelFormField) {
+            super(fieldSource, FieldInfo.DISPLAY, modelFormField);
+            this.alsoHidden = true;
+            this.currency = FlexibleStringExpander.getInstance("");
+            this.date = FlexibleStringExpander.getInstance("");
+            this.defaultValue = FlexibleStringExpander.getInstance("");
+            this.description = FlexibleStringExpander.getInstance("");
+            this.imageLocation = FlexibleStringExpander.getInstance("");
+            this.inPlaceEditor = null;
+            this.size = "";
+            this.type = "";
+        }
+
+        public DisplayField(ModelFormField modelFormField) {
+            super(FieldInfo.SOURCE_EXPLICIT, FieldInfo.DISPLAY, modelFormField);
+            this.alsoHidden = true;
+            this.currency = FlexibleStringExpander.getInstance("");
+            this.date = FlexibleStringExpander.getInstance("");
+            this.defaultValue = FlexibleStringExpander.getInstance("");
+            this.description = FlexibleStringExpander.getInstance("");
+            this.imageLocation = FlexibleStringExpander.getInstance("");
+            this.inPlaceEditor = null;
+            this.size = "";
+            this.type = "";
+        }
+
+        @Override
+        public void accept(ModelFieldVisitor visitor) throws Exception {
+            visitor.visit(this);
+        }
+
+        @Override
+        public FieldInfo copy(ModelFormField modelFormField) {
+            return new DisplayField(this, modelFormField);
+        }
+
+        public boolean getAlsoHidden() {
+            return alsoHidden;
+        }
+
+        public FlexibleStringExpander getCurrency() {
+            return currency;
+        }
+
+        public FlexibleStringExpander getDate() {
+            return date;
+        }
+
+        public FlexibleStringExpander getDefaultValue() {
+            return defaultValue;
+        }
+
+        public String getDefaultValue(Map<String, Object> context) {
+            if (this.defaultValue != null) {
+                return this.defaultValue.expandString(context);
+            } else {
+                return "";
+            }
+        }
+
+        public FlexibleStringExpander getDescription() {
+            return description;
+        }
+
+        public String getDescription(Map<String, Object> context) {
+            String retVal = null;
+            if (UtilValidate.isNotEmpty(this.description))
+                retVal = this.description.expandString(context);
+            else
+                retVal = getModelFormField().getEntry(context);
+
+            if (UtilValidate.isEmpty(retVal)) {
+                retVal = this.getDefaultValue(context);
+            } else if ("currency".equals(type)) {
+                retVal = retVal.replaceAll("&nbsp;", " "); // FIXME : encoding currency is a problem for some locale, we should not have any &nbsp; in retVal other case may arise in future...
+                Locale locale = (Locale) context.get("locale");
+                if (locale == null)
+                    locale = Locale.getDefault();
+                String isoCode = null;
+                if (UtilValidate.isNotEmpty(this.currency))
+                    isoCode = this.currency.expandString(context);
+
+                try {
+                    BigDecimal parsedRetVal = (BigDecimal) ObjectType.simpleTypeConvert(retVal, "BigDecimal", null, null, locale,
+                            true);
+                    retVal = UtilFormatOut.formatCurrency(parsedRetVal, isoCode, locale, 10); // we set the max to 10 digits as an hack to not round numbers in the ui
+                } catch (GeneralException e) {
+                    String errMsg = "Error formatting currency value [" + retVal + "]: " + e.toString();
+                    Debug.logError(e, errMsg, module);
+                    throw new IllegalArgumentException(errMsg);
+                }
+            } else if ("date".equals(this.type) && retVal.length() > 10) {
+                Locale locale = (Locale) context.get("locale");
+                if (locale == null) {
+                    locale = Locale.getDefault();
+                }
+
+                StringToTimestamp stringToTimestamp = new DateTimeConverters.StringToTimestamp();
+                Timestamp timestamp = null;
+                try {
+                    timestamp = stringToTimestamp.convert(retVal);
+                    Date date = new Date(timestamp.getTime());
+
+                    DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.SHORT, locale);
+                    retVal = dateFormatter.format(date);
+                } catch (ConversionException e) {
+                    String errMsg = "Error formatting date using default instead [" + retVal + "]: " + e.toString();
+                    Debug.logError(e, errMsg, module);
+                    // create default date value from timestamp string
+                    retVal = retVal.substring(0, 10);
+                }
+
+            } else if ("date-time".equals(this.type) && retVal.length() > 16) {
+                Locale locale = (Locale) context.get("locale");
+                TimeZone timeZone = (TimeZone) context.get("timeZone");
+                if (locale == null) {
+                    locale = Locale.getDefault();
+                }
+                if (timeZone == null) {
+                    timeZone = TimeZone.getDefault();
+                }
+
+                StringToTimestamp stringToTimestamp = new DateTimeConverters.StringToTimestamp();
+                Timestamp timestamp = null;
+                try {
+                    timestamp = stringToTimestamp.convert(retVal);
+                    Date date = new Date(timestamp.getTime());
+
+                    DateFormat dateFormatter = UtilDateTime.toDateTimeFormat(null, timeZone, locale);
+                    retVal = dateFormatter.format(date);
+                } catch (ConversionException e) {
+                    String errMsg = "Error formatting date/time using default instead [" + retVal + "]: " + e.toString();
+                    Debug.logError(e, errMsg, module);
+                    // create default date/time value from timestamp string
+                    retVal = retVal.substring(0, 16);
+                }
+            } else if ("accounting-number".equals(this.type)) {
+                Locale locale = (Locale) context.get("locale");
+                if (locale == null) {
+                    locale = Locale.getDefault();
+                }
+                try {
+                    Double parsedRetVal = (Double) ObjectType.simpleTypeConvert(retVal, "Double", null, locale, false);
+                    String template = UtilProperties.getPropertyValue("arithmetic", "accounting-number.format",
+                            "#,##0.00;(#,##0.00)");
+                    retVal = UtilFormatOut.formatDecimalNumber(parsedRetVal.doubleValue(), template, locale);
+                } catch (GeneralException e) {
+                    String errMsg = "Error formatting number [" + retVal + "]: " + e.toString();
+                    Debug.logError(e, errMsg, module);
+                    throw new IllegalArgumentException(errMsg);
+                }
+            }
+            if (UtilValidate.isNotEmpty(this.description) && retVal != null && this.getModelFormField().getEncodeOutput()) {
+                UtilCodec.SimpleEncoder simpleEncoder = (UtilCodec.SimpleEncoder) context.get("simpleEncoder");
+                if (simpleEncoder != null) {
+                    retVal = simpleEncoder.encode(retVal);
+                }
+            }
+            return retVal;
+        }
+
+        public FlexibleStringExpander getImageLocation() {
+            return imageLocation;
+        }
+
+        public String getImageLocation(Map<String, Object> context) {
+            if (this.imageLocation != null)
+                return this.imageLocation.expandString(context);
+            return "";
+        }
+
+        public InPlaceEditor getInPlaceEditor() {
+            return this.inPlaceEditor;
+        }
+
+        public String getSize() {
+            return this.size;
+        }
+
+        public String getType() {
+            return this.type;
+        }
+
+        @Override
+        public void renderFieldString(Appendable writer, Map<String, Object> context, FormStringRenderer formStringRenderer)
+                throws IOException {
+            formStringRenderer.renderDisplayField(writer, context, this);
+        }
+    }
+
+    /**
+     * Models the &lt;drop-down&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class DropDownField extends FieldInfoWithOptions {
+        private final boolean allowEmpty;
+        private final boolean allowMulti;
+        private final AutoComplete autoComplete;
+        private final String current;
+        private final FlexibleStringExpander currentDescription;
+        private final int otherFieldSize;
+        private final String size;
+        private final SubHyperlink subHyperlink;
+        private final String textSize;
+
+        private DropDownField(DropDownField original, ModelFormField modelFormField) {
+            super(original, modelFormField);
+            this.allowEmpty = original.allowEmpty;
+            this.allowMulti = original.allowMulti;
+            this.autoComplete = original.autoComplete;
+            this.current = original.current;
+            this.currentDescription = original.currentDescription;
+            this.otherFieldSize = original.otherFieldSize;
+            this.size = original.size;
+            if (original.subHyperlink != null) {
+                this.subHyperlink = new SubHyperlink(original.subHyperlink, modelFormField);
+            } else {
+                this.subHyperlink = null;
+            }
+            this.textSize = original.textSize;
+        }
+
+        public DropDownField(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+            this.allowEmpty = "true".equals(element.getAttribute("allow-empty"));
+            this.allowMulti = "true".equals(element.getAttribute("allow-multiple"));
+            Element autoCompleteElement = UtilXml.firstChildElement(element, "auto-complete");
+            if (autoCompleteElement != null) {
+                this.autoComplete = new AutoComplete(autoCompleteElement);
+            } else {
+                this.autoComplete = null;
+            }
+            this.current = element.getAttribute("current");
+            this.currentDescription = FlexibleStringExpander.getInstance(element.getAttribute("current-description"));
+            int otherFieldSize = 0;
+            String sizeStr = element.getAttribute("other-field-size");
+            if (!sizeStr.isEmpty()) {
+                try {
+                    otherFieldSize = Integer.parseInt(sizeStr);
+                } catch (Exception e) {
+                    Debug.logError("Could not parse the size value of the text element: [" + sizeStr
+                            + "], setting to the default of 0", module);
+                }
+            }
+            this.otherFieldSize = otherFieldSize;
+            String size = element.getAttribute("size");
+            if (size.isEmpty()) {
+                size = "1";
+            }
+            this.size = size;
+            Element subHyperlinkElement = UtilXml.firstChildElement(element, "sub-hyperlink");
+            if (subHyperlinkElement != null) {
+                this.subHyperlink = new SubHyperlink(subHyperlinkElement, this.getModelFormField());
+            } else {
+                this.subHyperlink = null;
+            }
+            String textSize = element.getAttribute("text-size");
+            if (textSize.isEmpty()) {
+                textSize = "0";
+            }
+            this.textSize = textSize;
+        }
+
+        public DropDownField(int fieldSource, List<OptionSource> optionSources) {
+            super(fieldSource, FieldInfo.DROP_DOWN, optionSources);
+            this.allowEmpty = false;
+            this.allowMulti = false;
+            this.autoComplete = null;
+            this.current = "";
+            this.currentDescription = FlexibleStringExpander.getInstance("");
+            this.otherFieldSize = 0;
+            this.size = "1";
+            this.subHyperlink = null;
+            this.textSize = "0";
+        }
+
+        public DropDownField(int fieldSource, ModelFormField modelFormField) {
+            super(fieldSource, FieldInfo.DROP_DOWN, modelFormField);
+            this.allowEmpty = false;
+            this.allowMulti = false;
+            this.autoComplete = null;
+            this.current = "";
+            this.currentDescription = FlexibleStringExpander.getInstance("");
+            this.otherFieldSize = 0;
+            this.size = "1";
+            this.subHyperlink = null;
+            this.textSize = "0";
+        }
+
+        public DropDownField(ModelFormField modelFormField) {
+            super(FieldInfo.SOURCE_EXPLICIT, FieldInfo.DROP_DOWN, modelFormField);
+            this.allowEmpty = false;
+            this.allowMulti = false;
+            this.autoComplete = null;
+            this.current = "";
+            this.currentDescription = FlexibleStringExpander.getInstance("");
+            this.otherFieldSize = 0;
+            this.size = "1";
+            this.subHyperlink = null;
+            this.textSize = "0";
+        }
+
+        @Override
+        public void accept(ModelFieldVisitor visitor) throws Exception {
+            visitor.visit(this);
+        }
+
+        @Override
+        public FieldInfo copy(ModelFormField modelFormField) {
+            return new DropDownField(this, modelFormField);
+        }
+
+        public boolean getAllowMulti() {
+            return allowMulti;
+        }
+
+        public AutoComplete getAutoComplete() {
+            return this.autoComplete;
+        }
+
+        public String getCurrent() {
+            if (UtilValidate.isEmpty(this.current))
+                return "first-in-list";
+            return this.current;
+        }
+
+        public FlexibleStringExpander getCurrentDescription() {
+            return currentDescription;
+        }
+
+        public String getCurrentDescription(Map<String, Object> context) {
+            if (this.currentDescription == null)
+                return null;
+            return this.currentDescription.expandString(context);
+        }
+
+        public int getOtherFieldSize() {
+            return this.otherFieldSize;
+        }
+
+        /**
+         * Get the name to use for the parameter for this field in the form interpreter.
+         * For HTML forms this is the request parameter name.
+         * @param context the context
+         * @return returns the name to use for the parameter for this field in the form interpreter.
+         */
+        public String getParameterNameOther(Map<String, Object> context) {
+            String baseName;
+            if (UtilValidate.isNotEmpty(getModelFormField().parameterName))
+                baseName = getModelFormField().parameterName;
+            else
+                baseName = getModelFormField().name;
+
+            baseName += "_OTHER";
+            Integer itemIndex = (Integer) context.get("itemIndex");
+            if (itemIndex != null && "multi".equals(getModelFormField().modelForm.getType())) {
+                return baseName + getModelFormField().modelForm.getItemIndexSeparator() + itemIndex.intValue();
+            } else {
+                return baseName;
+            }
+        }
+
+        public String getSize() {
+            return this.size;
+        }
+
+        public SubHyperlink getSubHyperlink() {
+            return this.subHyperlink;
+        }
+
+        public String getTextSize() {
+            return this.textSize;
+        }
+
+        public boolean getAllowEmpty() {
+            return this.allowEmpty;
+        }
+
+        public boolean getAllowMultiple() {
+            return this.allowMulti;
+        }
+
+        @Override
+        public void renderFieldString(Appendable writer, Map<String, Object> context, FormStringRenderer formStringRenderer)
+                throws IOException {
+            formStringRenderer.renderDropDownField(writer, context, this);
+        }
+    }
+
+    /**
+     * Models the &lt;entity-options&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class EntityOptions extends OptionSource {
+        private final boolean cache;
+        private final List<EntityFinderUtil.ConditionExpr> constraintList;
+        private final FlexibleStringExpander description;
+        private final String entityName;
+        private final String filterByDate;
+        private final String keyFieldName;
+        private final List<String> orderByList;
+
+        public EntityOptions(Element entityOptionsElement, ModelFormField modelFormField) {
+            super(modelFormField);
+            this.cache = !"false".equals(entityOptionsElement.getAttribute("cache"));
+            List<? extends Element> constraintElements = UtilXml.childElementList(entityOptionsElement, "entity-constraint");
+            if (!constraintElements.isEmpty()) {
+                List<EntityFinderUtil.ConditionExpr> constraintList = new ArrayList<EntityFinderUtil.ConditionExpr>(
+                        constraintElements.size());
+                for (Element constraintElement : constraintElements) {
+                    constraintList.add(new EntityFinderUtil.ConditionExpr(constraintElement));
+                }
+                this.constraintList = Collections.unmodifiableList(constraintList);
+            } else {
+                this.constraintList = Collections.emptyList();
+            }
+            this.description = FlexibleStringExpander.getInstance(entityOptionsElement.getAttribute("description"));
+            this.entityName = entityOptionsElement.getAttribute("entity-name");
+            this.filterByDate = entityOptionsElement.getAttribute("filter-by-date");
+            this.keyFieldName = entityOptionsElement.getAttribute("key-field-name");
+            List<? extends Element> orderByElements = UtilXml.childElementList(entityOptionsElement, "entity-order-by");
+            if (!orderByElements.isEmpty()) {
+                List<String> orderByList = new ArrayList<String>(orderByElements.size());
+                for (Element orderByElement : orderByElements) {
+                    orderByList.add(orderByElement.getAttribute("field-name"));
+                }
+                this.orderByList = Collections.unmodifiableList(orderByList);
+            } else {
+                this.orderByList = Collections.emptyList();
+            }
+        }
+
+        private EntityOptions(EntityOptions original, ModelFormField modelFormField) {
+            super(modelFormField);
+            this.cache = original.cache;
+            this.constraintList = original.constraintList;
+            this.description = original.description;
+            this.entityName = original.entityName;
+            this.filterByDate = original.filterByDate;
+            this.keyFieldName = original.keyFieldName;
+            this.orderByList = original.orderByList;
+        }
+
+        public EntityOptions(ModelFormField modelFormField) {
+            super(modelFormField);
+            this.cache = true;
+            this.constraintList = Collections.emptyList();
+            this.description = FlexibleStringExpander.getInstance("");
+            this.entityName = "";
+            this.filterByDate = "";
+            this.keyFieldName = "";
+            this.orderByList = Collections.emptyList();
+        }
+
+        @Override
+        public void addOptionValues(List<OptionValue> optionValues, Map<String, Object> context, Delegator delegator) {
+            // first expand any conditions that need expanding based on the current context
+            EntityCondition findCondition = null;
+            if (UtilValidate.isNotEmpty(this.constraintList)) {
+                List<EntityCondition> expandedConditionList = new LinkedList<EntityCondition>();
+                for (EntityFinderUtil.Condition condition : constraintList) {
+                    ModelEntity modelEntity = delegator.getModelEntity(this.entityName);
+                    if (modelEntity == null) {
+                        throw new IllegalArgumentException("Error in entity-options: could not find entity [" + this.entityName
+                                + "]");
+                    }
+                    EntityCondition createdCondition = condition.createCondition(context, modelEntity,
+                            delegator.getModelFieldTypeReader(modelEntity));
+                    if (createdCondition != null) {
+                        expandedConditionList.add(createdCondition);
+                    }
+                }
+                findCondition = EntityCondition.makeCondition(expandedConditionList);
+            }
+
+            try {
+                Locale locale = UtilMisc.ensureLocale(context.get("locale"));
+
+                List<GenericValue> values = null;
+                values = delegator.findList(this.entityName, findCondition, null, this.orderByList, null, this.cache);
+
+                // filter-by-date if requested
+                if ("true".equals(this.filterByDate)) {
+                    values = EntityUtil.filterByDate(values, true);
+                } else if (!"false".equals(this.filterByDate)) {
+                    // not explicitly true or false, check to see if has fromDate and thruDate, if so do the filter
+                    ModelEntity modelEntity = delegator.getModelEntity(this.entityName);
+                    if (modelEntity != null && modelEntity.isField("fromDate") && modelEntity.isField("thruDate")) {
+                        values = EntityUtil.filterByDate(values, true);
+                    }
+                }
+
+                for (GenericValue value : values) {
+                    // add key and description with string expansion, ie expanding ${} stuff, passing locale explicitly to expand value string because it won't be found in the Entity
+                    MapStack<String> localContext = MapStack.create(context);
+                    // Rendering code might try to modify the GenericEntity instance,
+                    // so we make a copy of it.
+                    Map<String, Object> genericEntityClone = UtilGenerics.cast(value.clone());
+                    localContext.push(genericEntityClone);
+
+                    // expand with the new localContext, which is locale aware
+                    String optionDesc = this.description.expandString(localContext, locale);
+
+                    Object keyFieldObject = value.get(this.getKeyFieldName());
+                    if (keyFieldObject == null) {
+                        throw new IllegalArgumentException(
+                                "The entity-options identifier (from key-name attribute, or default to the field name) ["
+                                        + this.getKeyFieldName() + "], may not be a valid key field name for the entity ["
+                                        + this.entityName + "].");
+                    }
+                    String keyFieldValue = keyFieldObject.toString();
+                    optionValues.add(new OptionValue(keyFieldValue, optionDesc));
+                }
+            } catch (GenericEntityException e) {
+                Debug.logError(e, "Error getting entity options in form", module);
+            }
+        }
+
+        @Override
+        public OptionSource copy(ModelFormField modelFormField) {
+            return new EntityOptions(this, modelFormField);
+        }
+
+        public boolean getCache() {
+            return cache;
+        }
+
+        public List<EntityFinderUtil.ConditionExpr> getConstraintList() {
+            return constraintList;
+        }
+
+        public FlexibleStringExpander getDescription() {
+            return description;
+        }
+
+        public String getEntityName() {
+            return entityName;
+        }
+
+        public String getFilterByDate() {
+            return filterByDate;
+        }
+
+        public String getKeyFieldName() {
+            if (UtilValidate.isNotEmpty(this.keyFieldName))
+                return this.keyFieldName;
+            return getModelFormField().getFieldName(); // get the modelFormField fieldName
+        }
+
+        public List<String> getOrderByList() {
+            return orderByList;
+        }
+    }
+
+    public static abstract class FieldInfoWithOptions extends FieldInfo {
+
+        public static String getDescriptionForOptionKey(String key, List<OptionValue> allOptionValues) {
+            if (UtilValidate.isEmpty(key))
+                return "";
+
+            if (UtilValidate.isEmpty(allOptionValues))
+                return key;
+
+            for (OptionValue optionValue : allOptionValues) {
+                if (key.equals(optionValue.getKey())) {
+                    return optionValue.getDescription();
+                }
+            }
+
+            // if we get here we didn't find a match, just return the key
+            return key;
+        }
+
+        private final FlexibleStringExpander noCurrentSelectedKey;
+        private final List<OptionSource> optionSources;
+
+        public FieldInfoWithOptions(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+            this.noCurrentSelectedKey = FlexibleStringExpander.getInstance(element.getAttribute("no-current-selected-key"));
+            // read all option and entity-options sub-elements, maintaining order
+            ArrayList<OptionSource> optionSources = new ArrayList<OptionSource>();
+            List<? extends Element> childElements = UtilXml.childElementList(element);
+            if (childElements.size() > 0) {
+                for (Element childElement : childElements) {
+                    if ("option".equals(childElement.getTagName())) {
+                        optionSources.add(new SingleOption(childElement, modelFormField));
+                    } else if ("list-options".equals(childElement.getTagName())) {
+                        optionSources.add(new ListOptions(childElement, modelFormField));
+                    } else if ("entity-options".equals(childElement.getTagName())) {
+                        optionSources.add(new EntityOptions(childElement, modelFormField));
+                    }
+                }
+            } else {
+                // this must be added or the multi-form select box options would not show up
+                optionSources.add(new SingleOption("Y", " ", modelFormField));
+            }
+            optionSources.trimToSize();
+            this.optionSources = Collections.unmodifiableList(optionSources);
+        }
+
+        // Copy constructor.
+        protected FieldInfoWithOptions(FieldInfoWithOptions original, ModelFormField modelFormField) {
+            super(original.getFieldSource(), original.getFieldType(), modelFormField);
+            this.noCurrentSelectedKey = original.noCurrentSelectedKey;
+            if (original.optionSources.isEmpty()) {
+                this.optionSources = original.optionSources;
+            } else {
+                List<OptionSource> optionSources = new ArrayList<OptionSource>(original.optionSources.size());
+                for (OptionSource source : original.optionSources) {
+                    optionSources.add(source.copy(modelFormField));
+                }
+                this.optionSources = Collections.unmodifiableList(optionSources);
+            }
+        }
+
+        protected FieldInfoWithOptions(int fieldSource, int fieldType, List<OptionSource> optionSources) {
+            super(fieldSource, fieldType, null);
+            this.noCurrentSelectedKey = FlexibleStringExpander.getInstance("");
+            this.optionSources = Collections.unmodifiableList(new ArrayList<OptionSource>(optionSources));
+        }
+
+        public FieldInfoWithOptions(int fieldSource, int fieldType, ModelFormField modelFormField) {
+            super(fieldSource, fieldType, modelFormField);
+            this.noCurrentSelectedKey = FlexibleStringExpander.getInstance("");
+            this.optionSources = Collections.emptyList();
+        }
+
+        public List<OptionValue> getAllOptionValues(Map<String, Object> context, Delegator delegator) {
+            List<OptionValue> optionValues = new LinkedList<OptionValue>();
+            for (OptionSource optionSource : this.optionSources) {
+                optionSource.addOptionValues(optionValues, context, delegator);
+            }
+            return optionValues;
+        }
+
+        public FlexibleStringExpander getNoCurrentSelectedKey() {
+            return noCurrentSelectedKey;
+        }
+
+        public String getNoCurrentSelectedKey(Map<String, Object> context) {
+            return this.noCurrentSelectedKey.expandString(context);
+        }
+
+        public List<OptionSource> getOptionSources() {
+            return optionSources;
+        }
+    }
+
+    /**
+     * Models the &lt;file&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class FileField extends TextField {
+
+        public FileField(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+        }
+
+        private FileField(FileField original, ModelFormField modelFormField) {
+            super(original, modelFormField);
+        }
+
+        public FileField(int fieldSource, ModelFormField modelFormField) {
+            super(fieldSource, modelFormField);
+        }
+
+        @Override
+        public void accept(ModelFieldVisitor visitor) throws Exception {
+            visitor.visit(this);
+        }
+
+        @Override
+        public FieldInfo copy(ModelFormField modelFormField) {
+            return new FileField(this, modelFormField);
+        }
+
+        @Override
+        public void renderFieldString(Appendable writer, Map<String, Object> context, FormStringRenderer formStringRenderer)
+                throws IOException {
+            formStringRenderer.renderFileField(writer, context, this);
+        }
+    }
+
+    /**
+     * Models the &lt;hidden&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class HiddenField extends FieldInfo {
+        private final FlexibleStringExpander value;
+
+        public HiddenField(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+            this.value = FlexibleStringExpander.getInstance(element.getAttribute("value"));
+        }
+
+        private HiddenField(HiddenField original, ModelFormField modelFormField) {
+            super(original.getFieldSource(), original.getFieldType(), modelFormField);
+            this.value = original.value;
+        }
+
+        public HiddenField(int fieldSource, ModelFormField modelFormField) {
+            super(fieldSource, FieldInfo.HIDDEN, modelFormField);
+            this.value = FlexibleStringExpander.getInstance("");
+        }
+
+        public HiddenField(ModelFormField modelFormField) {
+            super(FieldInfo.SOURCE_EXPLICIT, FieldInfo.HIDDEN, modelFormField);
+            this.value = FlexibleStringExpander.getInstance("");
+        }
+
+        @Override
+        public void accept(ModelFieldVisitor visitor) throws Exception {
+            visitor.visit(this);
+        }
+
+        @Override
+        public FieldInfo copy(ModelFormField modelFormField) {
+            return new HiddenField(this, modelFormField);
+        }
+
+        public FlexibleStringExpander getValue() {
+            return value;
+        }
+
+        public String getValue(Map<String, Object> context) {
+            if (UtilValidate.isNotEmpty(this.value)) {
+                String valueEnc = this.value.expandString(context);
+                UtilCodec.SimpleEncoder simpleEncoder = (UtilCodec.SimpleEncoder) context.get("simpleEncoder");
+                if (simpleEncoder != null) {
+                    valueEnc = simpleEncoder.encode(valueEnc);
+                }
+                return valueEnc;
+            } else {
+                return getModelFormField().getEntry(context);
+            }
+        }
+
+        @Override
+        public void renderFieldString(Appendable writer, Map<String, Object> context, FormStringRenderer formStringRenderer)
+                throws IOException {
+            formStringRenderer.renderHiddenField(writer, context, this);
+        }
+    }
+
+    /**
+     * Models the &lt;hyperlink&gt; element.
+     * 
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class HyperlinkField extends FieldInfo {
+
+        private final boolean alsoHidden;
+        private final FlexibleStringExpander confirmationMsgExdr;
+        private final FlexibleStringExpander description;
+        private final boolean requestConfirmation;
+        private final Link link;
+        public HyperlinkField(Element element, ModelFormField modelFormField) {
+            super(element, modelFormField);
+            this.alsoHidden = !"false".equals(element.getAttribute("also-hidden"));
+            this.confirmationMsgExdr = FlexibleStringExpander.getInstance(element.getAttribute("confirmation-message"));

[... 1699 lines stripped ...]