001 /*---
002    Copyright 2006-2007 Visual Systems Corporation.
003    http://www.vscorp.com
004 
005    Licensed under the Apache License, Version 2.0 (the "License");
006    you may not use this file except in compliance with the License.
007    You may obtain a copy of the License at
008    
009         http://www.apache.org/licenses/LICENSE-2.0
010    
011    Unless required by applicable law or agreed to in writing, software
012    distributed under the License is distributed on an "AS IS" BASIS,
013    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014    See the License for the specific language governing permissions and
015    limitations under the License.
016 ---*/
017 package net.sourceforge.wicketwebbeans.containers;
018 
019 import java.beans.PropertyChangeEvent;
020 import java.beans.PropertyChangeListener;
021 import java.io.Serializable;
022 import java.lang.reflect.Constructor;
023 import java.util.ArrayList;
024 import java.util.HashSet;
025 import java.util.List;
026 import java.util.Set;
027 import java.util.logging.Logger;
028 
029 import net.sourceforge.wicketwebbeans.actions.BeanActionButton;
030 import net.sourceforge.wicketwebbeans.fields.AbstractField;
031 import net.sourceforge.wicketwebbeans.fields.Field;
032 import net.sourceforge.wicketwebbeans.model.BeanMetaData;
033 import net.sourceforge.wicketwebbeans.model.BeanPropertyModel;
034 import net.sourceforge.wicketwebbeans.model.ElementMetaData;
035 import net.sourceforge.wicketwebbeans.model.TabMetaData;
036 
037 import org.apache.wicket.Component;
038 import org.apache.wicket.MarkupContainer;
039 import org.apache.wicket.ajax.AjaxRequestTarget;
040 import org.apache.wicket.ajax.IAjaxCallDecorator;
041 import org.apache.wicket.ajax.form.AjaxFormValidatingBehavior;
042 import org.apache.wicket.behavior.AbstractBehavior;
043 import org.apache.wicket.behavior.IBehavior;
044 import org.apache.wicket.behavior.SimpleAttributeModifier;
045 import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
046 import org.apache.wicket.extensions.markup.html.tabs.ITab;
047 import org.apache.wicket.extensions.markup.html.tabs.TabbedPanel;
048 import org.apache.wicket.markup.ComponentTag;
049 import org.apache.wicket.markup.html.WebMarkupContainer;
050 import org.apache.wicket.markup.html.basic.Label;
051 import org.apache.wicket.markup.html.form.Form;
052 import org.apache.wicket.markup.html.form.FormComponent;
053 import org.apache.wicket.markup.html.form.HiddenField;
054 import org.apache.wicket.markup.html.form.SubmitLink;
055 import org.apache.wicket.markup.html.list.ListItem;
056 import org.apache.wicket.markup.html.list.ListView;
057 import org.apache.wicket.markup.html.panel.FeedbackPanel;
058 import org.apache.wicket.markup.html.panel.Panel;
059 import org.apache.wicket.model.IModel;
060 import org.apache.wicket.model.Model;
061 import org.apache.wicket.model.PropertyModel;
062 import org.apache.wicket.model.StringResourceModel;
063 import org.apache.wicket.util.string.Strings;
064 
065 
066 /**
067  * Generic component for presenting a bean form. Supports the following parameters: <p>
068  <ul>
069  <li>label - the form's label.</li>
070  <li>rows - if the bean is a List, this is the number of rows to be displayed. Defaults to 10.</li>
071  <li>container - a container to use in place of the default BeanGridPanel or BeanTablePanel. This container must must be a Panel and
072  *   implement a constructor of the form: <p>
073  *   <code>public Constructor(String id, final Object bean, BeanMetaData beanMetaData, TabMetaData tabMetaData)</code>
074  *   <p>
075  *   where id = Wicket component ID<br>
076  *   bean = the bean, or IModel containing the bean<br>
077  *   beanMetaData = the BeanMetaData for bean<br>
078  *   tabMetaData = the tab metadata
079  *   </li>
080  </ul>
081  
082  * You can override default error message for required field.<br>
083  * Property key of the message: <em>wicketwebbeans.BeanForm.fieldIsRequired</em><br>
084  * Variable in the message which will be substituted for field label: <em>${fieldLabel}</em>
085  *
086  @author Dan Syrstad
087  */
088 public class BeanForm extends Panel
089 {
090     public static final String PARAM_ROWS = "rows";
091     
092     private static Logger logger = Logger.getLogger(BeanForm.class.getName());
093     private static final long serialVersionUID = -7287729257178283645L;
094     private static Class<?>[] CONTAINER_CONSTRUCTOR_PARAMS = String.class, Object.class, BeanMetaData.class, TabMetaData.class };
095 
096     private Form form;
097     private FormVisitor formVisitor;
098     private FeedbackPanel feedback;
099     // Wicket ID/HTML ID of field with focus.
100     private String focusField = null;
101     private BeanPropertyChangeListener listener = new BeanPropertyChangeListener();
102 
103     /** Maps components in this form to their properties. */
104     private Set<ComponentPropertyMapping> componentPropertyMappings = new HashSet<ComponentPropertyMapping>(200);
105     /** Components that should be refreshed on the new Ajax Component update. */ 
106     private Set<ComponentPropertyMapping> refreshComponents = new HashSet<ComponentPropertyMapping>(200);
107     /** Form submit recursion counter. Zero means we're not validating currently. */
108     private int submitCnt = 0;
109     private TabbedPanel tabbedPanel = null;
110     
111     /**
112      * Construct a new BeanForm.
113      *
114      @param id the Wicket id for the panel.
115      @param bean the bean to be displayed. This may be an IModel or regular bean object.
116      *  The bean may be a List or, if an IModel, a model that returns a List. If so, the bean is display is
117      *  displayed using BeanTablePanel. Otherwise BeanGridPanel is used.
118      @param beanMetaData the meta data for the bean. If bean is a List or model of a List, then this must be
119      *  the BeanMetaData for a single element (row) of the List. 
120      */
121     public BeanForm(String id, final Object bean, final BeanMetaData beanMetaData)
122     {
123         this(id, bean, beanMetaData, null);
124     }
125     
126     /**
127      * Construct a new BeanForm.
128      *
129      @param id the Wicket id for the panel.
130      @param bean the bean to be displayed. This may be an IModel or regular bean object.
131      *  The bean may be a List or, if an IModel, a model that returns a List. If so, the bean is display is
132      *  displayed using BeanTablePanel. Otherwise BeanGridPanel is used.
133      @param beanMetaData the meta data for the bean. If bean is a List or model of a List, then this must be
134      *  the BeanMetaData for a single element (row) of the List.
135      @param container an optional container to use in place of the default BeanGridPanel or BeanTablePanel. This container must must be a Panel and
136      *   implement a constructor of the form: <p>
137      *   <code>public Constructor(String id, final Object bean, BeanMetaData beanMetaData, TabMetaData tabMetaData)</code>
138      *   <p>
139      *   where id = Wicket component ID<br>
140      *   bean = the bean, or IModel containing the bean<br>
141      *   beanMetaData = the BeanMetaData for bean<br>
142      *   tabMetaData = the tab metadata<p>
143      *   May be null.
144      */
145     public BeanForm(String id, final Object bean, final BeanMetaData beanMetaData, final Class<? extends Panel> container)
146     {
147         super(id);
148         
149         form = new Form("f") {
150             // Track whether the form is in submit processing.
151             public boolean process()
152             {
153                 ++submitCnt;
154                 try {
155                     return super.process();
156                 }
157                 finally {
158                     --submitCnt;
159                 }
160             }
161         };
162         
163         form.setOutputMarkupId(true);
164         add(form);
165         
166         String title = beanMetaData.getLabel();
167         form.addnew Label("title", title) );
168         
169         String serverErrorMsg = getLocalizer().getString("beanFormError.msg", this, "An error occurred on the server. Your session may have timed out.");
170         form.addnew Label("beanFormIndicatorErrorLabel", serverErrorMsg) );
171         
172         beanMetaData.consumeParameter(PARAM_ROWS);
173         
174         final HiddenField focusField = new HiddenField("focusField"new PropertyModel(this, "focusField"));
175         focusField.addnew AbstractBehavior() {
176             public void onComponentTag(Component component, ComponentTag tag)
177             {
178                 tag.put("id""bfFocusField");
179                 super.onComponentTag(component, tag);
180             }
181         });
182         
183         form.add(focusField);
184         
185         formVisitor = new FormVisitor();
186         
187         List<TabMetaData> tabMetaDataList = beanMetaData.getTabs();
188         if (tabMetaDataList.get(0).getId().equals(BeanMetaData.DEFAULT_TAB_ID)) {
189             // Single default tab - none explicitly specified. Don't add a tab panel.
190             form.addcreatePanel("tabs", bean, beanMetaData, tabMetaDataList.get(0), container) );
191         }
192         else {
193             List<AbstractTab> tabs = new ArrayList<AbstractTab>();
194             for (final TabMetaData tabMetaData : tabMetaDataList) {
195                 tabs.addnew AbstractTabnew Model(tabMetaData.getLabel()) ) {
196                     public Panel getPanel(String panelId)
197                     {
198                         return createPanel(panelId, bean, beanMetaData, tabMetaData, container);
199                     }
200                 } );
201             }
202     
203             // This is a tabbed panel that submits the form and doesn't switch if there are errors. 
204             tabbedPanel = new TabbedPanel("tabs", tabs) {
205                 protected WebMarkupContainer newLink(String linkId, final int index)
206                 {
207                     return new TabbedPanelSubmitLink(linkId, index);
208                 }
209             };
210             
211             form.add(tabbedPanel);
212         }
213         
214         feedback = new FeedbackPanel("feedback");
215         feedback.setOutputMarkupId(true);
216         form.add(feedback);        
217 
218         // Add bean actions.
219         List<ElementMetaData> globalActions = beanMetaData.getGlobalActions();
220         form.add(new ListView("actions", globalActions) {
221             protected void populateItem(ListItem item)
222             {
223                 ElementMetaData element = (ElementMetaData)item.getModelObject();
224                 item.addnew BeanActionButton("action", element, form, bean) );
225             }
226         });
227     }
228     
229     /**
230      * Creates the panel for the given tab.
231      *
232      @param panelId the Wicket id for the panel component.
233      @param bean may be a bean or an IModel containing a bean.
234      @param beanMetaData the BeanMetaData.
235      @param tabMetaData the TabMetaData.
236      @param container the container class to use. May be null.
237      
238      @return a Panel.
239      */
240     protected Panel createPanel(String panelId, Object bean, BeanMetaData beanMetaData, TabMetaData tabMetaData, Class<? extends Panel> containerClass)
241     {
242         if (containerClass == null) {
243             containerClass = beanMetaData.getContainerClass();
244         }
245         
246         if (containerClass != null) {
247             try {
248                 Constructor<? extends Panel> constructor = containerClass.getConstructor(CONTAINER_CONSTRUCTOR_PARAMS);
249                 return constructor.newInstance(panelId, bean, beanMetaData, tabMetaData);
250             }
251             catch (Exception e) {
252                 throw new RuntimeException("Error instantiating container", e);
253             }
254         }
255         
256         boolean isList = (bean instanceof List);
257         if (bean instanceof IModel) {
258             Object modelBean = ((IModel)bean).getObject();
259             isList = (modelBean instanceof List);
260         }
261         
262         if (isList) {
263             // BeanTablePanel expects a model. Wrap bean if necessary.
264             IModel model;
265             if (bean instanceof IModel) {
266                 model = (IModel)bean;
267             }
268             else {
269                 model = new Model((Serializable)bean);
270             }
271             
272             // Get Number of rows from parameters
273             int rows = beanMetaData.getIntParameter(PARAM_ROWS, 10);
274             return new BeanTablePanel(panelId, model, beanMetaData, rows);
275         }
276 
277         return new BeanGridPanel(panelId, bean, beanMetaData, tabMetaData);
278     }
279     
280     /**
281      * Finds the BeanForm that is the parent of the given childComponent.
282      *
283      @param childComponent the child, may be null.
284      
285      @return the parent BeanForm, or null if childComponent is not part of a BeanForm.
286      */
287     public static BeanForm findBeanFormParent(Component childComponent)
288     {
289         if (childComponent == null) {
290             return null;
291         }
292         
293         return (BeanForm)childComponent.visitParents(BeanForm.class, new IVisitor() {
294             public Object component(Component visited)
295             {
296                 return (BeanForm)visited;
297             }
298         });
299     }
300     
301     /**
302      * Determines if the BeanForm associated with childComponent is currently in a form
303      * submit phase.
304      
305      @param childComponent the child, may be null.
306      
307      @return true if the BeanForm is validating, or false if not.
308      */
309     public static boolean isInSubmit(Component childComponent)
310     {
311         BeanForm beanForm = findBeanFormParent(childComponent);
312         if (beanForm != null) {
313             return beanForm.submitCnt > 0;
314         }
315         
316         return false;
317     }
318     
319     /**
320      * Rather than using Wicket's required field validation, which doesn't play well with Ajax and forms,
321      * allow validation of fields on actions. User must call this from the action method.
322      * Adds errors to the page if empty required fields are found. 
323      *
324      @return true if validation was successful, else false if errors were found.
325      */
326     public boolean validateRequired()
327     {
328         RequiredFieldValidator validator = new RequiredFieldValidator();
329         
330         // If we have a tabbed panel, we have to go thru each tab and validate it because the components for a tab are only created
331         // when the tab is open.
332         if (tabbedPanel == null) {
333             visitChildren(AbstractField.class, validator);
334         }
335         else {
336             for (ITab tab : (List<ITab>)tabbedPanel.getTabs()) {
337                 Panel panel = tab.getPanel("x");
338                 // Needs to be part of the page for errors.
339                 getPage().add(panel);
340                 // Cause ListViews to be populated.
341                 panel.beforeRender();
342                 panel.visitChildren(AbstractField.class, validator);
343                 getPage().remove(panel);
344             }
345         }
346         
347         return !validator.errorsFound;
348     }
349     
350     /**
351      * Registers the given component with this form. This is usually called by Fields
352      * (for example, see {@link AbstractField}) to add the form behavior to their
353      * components.
354      
355      @param component
356      */
357     public void registerComponent(Component component, BeanPropertyModel beanModel, ElementMetaData element)
358     {
359         ComponentPropertyMapping mapping = new ComponentPropertyMapping(beanModel, element);
360         componentPropertyMappings.add(mapping);
361             
362         // Make sure we don't register ourself twice.
363         if (beanModel != null && beanModel.getBeanForm() == null) {
364             // Listen for PropertyChangeEvents on this bean, if necessary.
365             // TODO When do we unregister?? Maybe a WeakRef to ourself in the listener? Then listener unregisters
366             // TODO if we don't exist anymore.
367             element.getBeanMetaData().addPropertyChangeListener(beanModel, listener);
368             beanModel.setBeanForm(this);
369         }
370         
371         if (component instanceof MarkupContainer) {
372             ((MarkupContainer)component).visitChildren(formVisitor);
373         }
374         else {
375             component.add(new FormSubmitter("onchange"));
376         }
377     }
378     
379     
380     /**
381      * Gets the listener.
382      *
383      @return a BeanPropertyChangeListener.
384      */
385     public BeanPropertyChangeListener getListener()
386     {
387         return listener;
388     }
389 
390     /**
391      * Allows external app to set the field to receive focus.
392      
393      @param component the component, may be null to unset the field.
394      */
395     public void setFocusComponent(Component component)
396     {
397         setFocusFieldcomponent == null null : component.getId() );
398     }
399     
400     /**
401      * Gets the focusField.
402      *
403      @return the focusField.
404      */
405     public String getFocusField()
406     {
407         return focusField;
408     }
409 
410     /**
411      * Sets the focusField.
412      *
413      @param focusField the focusField to set.
414      */
415     public void setFocusField(String focusField)
416     {
417         this.focusField = focusField;
418     }
419 
420     /**
421      @return true if {@link #refreshComponents(AjaxRequestTarget, Component)} needs to be called.
422      */
423     public boolean isComponentRefreshNeeded()
424     {
425         return !refreshComponents.isEmpty();
426     }
427     
428     /**
429      * Clears the components that would be refreshed if {@link #refreshComponents(AjaxRequestTarget, Component)} were called.
430      */
431     public void clearRefreshComponents()
432     {
433         refreshComponents.clear();
434     }
435     
436     /**
437      * Refresh the targetComponent, in addition to any components that need to be updated
438      * due to property change events.
439      *
440      @param target
441      @param targetComponent the targetComponent, may be null.
442      */
443     public void refreshComponents(final AjaxRequestTarget target, Component targetComponent)
444     {
445         if (targetComponent != null) {
446             refreshComponent(target, targetComponent);
447         }
448         
449         if (!refreshComponents.isEmpty()) {
450             // Refresh components fired from our PropertyChangeListener.
451             
452             // Visit all children and see if they match the fired events. 
453             form.visitChildrennew IVisitor() {
454                 public Object component(Component component)
455                 {
456                     Object model = component.getModel();
457                     if (model instanceof BeanPropertyModel) {
458                         BeanPropertyModel propModel = (BeanPropertyModel)model;
459                         ElementMetaData componentMetaData = propModel.getElementMetaData();
460                         for (ComponentPropertyMapping mapping : refreshComponents) {
461                             if (mapping.elementMetaData == componentMetaData) {
462                                 refreshComponent(target, component);
463                                 break;
464                             }
465                         }
466                     }
467 
468                     return IVisitor.CONTINUE_TRAVERSAL;
469                 }
470             });
471 
472             refreshComponents.clear();
473         }
474     }
475 
476     private void refreshComponent(final AjaxRequestTarget target, Component targetComponent)
477     {
478         // Refresh this field. We have to find the parent Field to do this.
479         MarkupContainer field;
480         if (targetComponent instanceof Field) {
481             field = (MarkupContainer)targetComponent;
482         }
483         else {
484             field = targetComponent.findParent(AbstractField.class);
485         }
486         
487         if (field != null) {
488             if (!field.getRenderBodyOnly()) {
489                 target.addComponent(field);
490             }
491             else {
492                 // Field is RenderBodyOnly, have to add children individually
493                 field.visitChildrennew IVisitor() {
494                     public Object component(Component component)
495                     {
496                         if (!component.getRenderBodyOnly()) {
497                             target.addComponent(component);
498                         }
499                         
500                         return IVisitor.CONTINUE_TRAVERSAL;
501                     }
502                     
503                 });
504             }
505         }
506         else {
507             target.addComponent(targetComponent);
508         }
509     }
510 
511     /**
512      * Monitors tab panel submits. 
513      */
514     private final class TabbedPanelSubmitLink extends SubmitLink
515     {
516         private final int index;
517 
518         private TabbedPanelSubmitLink(String id, int index)
519         {
520             super(id, form);
521             this.index = index;
522         }
523 
524         @Override
525         public void onSubmit()
526         {
527             if (tabbedPanel.getSelectedTab() != index) {
528                 tabbedPanel.setSelectedTab(index);
529                 // TODO this could remember last focus field on the tab and refocus when switching back to the tab
530                 // TODO Keep separate tab array of focus fields?
531                 setFocusField(null);
532             }
533 
534             refreshComponents.clear();
535         }
536     }
537 
538     private final class FormVisitor implements IVisitor, Serializable
539     {
540         public Object component(Component component
541         {
542             if (component instanceof FormComponent) {
543                 boolean addBehavior = true;
544                 for (IBehavior behavior : (List<IBehavior>)component.getBehaviors()) {
545                     if (behavior instanceof FormSubmitter) {
546                         addBehavior = false;
547                         break;
548                     }
549                 }
550                 
551                 if (addBehavior) {
552                     FormSubmitter behavior = new FormSubmitter("onchange");
553                     // Note: Do NOT set a delay. The delay can cause an onchange to be sent AFTER a button submit
554                     // which causes the submit button's messages to be erased. <- That was true when we used AjaxSubmitButtons, we don't anymore.
555                     //behavior.setThrottleDelay(Duration.milliseconds(250));
556                     component.add(behavior);
557                     component.addnew SimpleAttributeModifier("onfocus""bfOnFocus(this)") );
558                 }
559             }
560             
561             return IVisitor.CONTINUE_TRAVERSAL;
562         }
563     }
564 
565     private final class FormSubmitter extends AjaxFormValidatingBehavior implements Serializable
566     {
567         private FormSubmitter(String event)
568         {
569             super(form, event);
570         }
571 
572         @Override
573         protected void onSubmit(final AjaxRequestTarget target)
574         {
575             /*
576             // NOTE: The following code fails to clear off field errors that have been corrected.
577              
578             // Only refresh messages if we have one. Otherwise previous error messages go away on the
579             // first field change.
580             if (form.getPage().hasFeedbackMessage()) {
581                 super.onSubmit(target);
582             }
583             */
584             super.onSubmit(target);
585             refreshComponents(target, getComponent() );
586         }
587 
588         @Override
589         protected void onError(AjaxRequestTarget target)
590         {
591             super.onError(target);
592             refreshComponents(target, getComponent() );
593         }
594         
595         @Override
596         protected IAjaxCallDecorator getAjaxCallDecorator()
597         {
598             return AjaxBusyDecorator.INSTANCE;
599         }
600     }
601     
602     public static final class AjaxBusyDecorator implements IAjaxCallDecorator
603     {
604         public static final AjaxBusyDecorator INSTANCE = new AjaxBusyDecorator();
605 
606         public CharSequence decorateOnFailureScript(CharSequence script)
607         {
608             return "bfIndicatorError();" + script;
609         }
610 
611         public CharSequence decorateOnSuccessScript(CharSequence script)
612         {
613             return "bfIndicatorOff();" + script;
614         }
615 
616         public CharSequence decorateScript(CharSequence script)
617         {
618             return "bfIndicatorOn(); " + script;
619         }
620     }
621     
622     /**
623      * Simple data structure for mapping components and properties. <p>
624      */
625     private static final class ComponentPropertyMapping implements Serializable
626     {
627         /** IModel holding the bean. */
628         private BeanPropertyModel beanModel;
629         private ElementMetaData elementMetaData;
630         
631         ComponentPropertyMapping(BeanPropertyModel beanModel, ElementMetaData elementMetaData)
632         {
633             this.beanModel = beanModel;
634             this.elementMetaData = elementMetaData;
635         }
636 
637         /** 
638          * {@inheritDoc}
639          @see java.lang.Object#hashCode()
640          */
641         @Override
642         public int hashCode()
643         {
644             int result = 31 ((beanModel == null: beanModel.hashCode());
645             result = 31 * result + ((elementMetaData == null: elementMetaData.hashCode());
646             return result;
647         }
648         
649         private Object getBean()
650         {
651             return beanModel.getBean();
652         }
653 
654         /** 
655          * {@inheritDoc}
656          @see java.lang.Object#equals(java.lang.Object)
657          */
658         @Override
659         public boolean equals(Object obj)
660         {
661             if (!(obj instanceof ComponentPropertyMapping)) {
662                 return false;
663             }
664 
665             final ComponentPropertyMapping other = (ComponentPropertyMapping)obj;
666             return beanModel == other.beanModel && 
667                     (elementMetaData == other.elementMetaData || 
668                      (elementMetaData != null && elementMetaData.equals(other.elementMetaData)));
669         }
670     }
671     
672     /**
673      * Listens to property change events on a bean and adds them to the queue of
674      * components to be refreshed. <p>
675      */
676     public final class BeanPropertyChangeListener implements PropertyChangeListener, Serializable
677     {
678         public void propertyChange(PropertyChangeEvent evt)
679         {
680             // Find matching component
681             Object bean = evt.getSource();
682             String propName = evt.getPropertyName();
683             for (ComponentPropertyMapping mapping : componentPropertyMappings) {
684                 if (bean == mapping.getBean() && propName.equals(mapping.elementMetaData.getPropertyName())) {
685                     BeanForm.this.refreshComponents.add(mapping);
686                 }
687             }
688         }
689     }
690     
691     /**
692      * Validates required fields on the form and sets an error message on the component if necessary.
693      */
694     private static final class RequiredFieldValidator implements IVisitor 
695     {
696       private class FieldLabel implements Serializable{
697         String fieldLabel;
698         public FieldLabel(String fieldLabel){this.fieldLabel = fieldLabel;}
699       public String getFieldLabel() {return fieldLabel;}
700       public void setFieldLabel(String fieldLabel) {this.fieldLabel = fieldLabel;}
701       }
702         boolean errorsFound = false;
703         
704         public Object component(Component component)
705         {
706             AbstractField field = (AbstractField)component;
707             if (field.isRequiredField() && Strings.isEmpty(field.getModelObjectAsString())) {
708               FieldLabel fieldName = new FieldLabel(field.getElementMetaData().getLabel());
709               StringResourceModel labelModel = new StringResourceModel("wicketwebbeans.BeanForm.fieldIsRequired", field.getElementMetaData().getBeanMetaData().getComponent()new Model(fieldName)"${fieldLabel} is required");
710               field.error(labelModel.getObject().toString());
711                 errorsFound = true;
712             }
713             
714             return CONTINUE_TRAVERSAL;
715         }
716     
717 }
Java2html