View Javadoc

1   /*
2    * Copyright 2006-2016 The JGUIraffe Team.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License")
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.jguiraffe.gui.forms;
17  
18  import java.util.HashMap;
19  import java.util.Map;
20  import java.util.Set;
21  
22  import net.sf.jguiraffe.transform.DefaultValidationResult;
23  import net.sf.jguiraffe.transform.TransformerContext;
24  import net.sf.jguiraffe.transform.ValidationResult;
25  
26  /**
27   * <p>
28   * This class represents a form.
29   * </p>
30   * <p>
31   * Instances of this class can be used to deal with forms, e.g. initializing the
32   * form's widgets with data obtained from a model object or validating the
33   * user's input. A {@code Form} object must be initialized with objects
34   * representing the form elements or fields. These objects are of type
35   * {@link FieldHandler} and contain all information needed for correctly
36   * handling GUI widgets and the data they may contain.
37   * </p>
38   * <p>
39   * An important functionality of this class is to enable data transfer between
40   * the form's fields and the properties of a model object. By properly
41   * initializing the {@link FieldHandler} objects with transformers and
42   * validators it can be assured that suitable validation and data conversion
43   * take place. Model objects are accessed through a {@link BindingStrategy};
44   * therefore this class can collaborate with different types of model objects
45   * provided that a corresponding {@code BindingStrategy} implementation exists.
46   * </p>
47   * <p>
48   * After a {@code Form} object and its corresponding fields haven been
49   * initialized usage of this class is quite simple. To initialize the GUI
50   * widgets associated with this form call the {@link #initFields(Object)} method
51   * and pass in a model object instance with the values for the fields. (Of
52   * course, this model object must be compatible with the {@link BindingStrategy}
53   * the form was initialized with.) To perform validation and read the user's
54   * input back into a model object the {@link #validate(Object)} method can be
55   * used. This method invokes all registered validators, and if validation
56   * succeeds, the user input is converted into the correct types and transfered
57   * into the given model object.
58   * </p>
59   * <p>
60   * Implementation node: This class is not thread safe; instances should be
61   * accessed by a single thread only.
62   * </p>
63   *
64   * @author Oliver Heger
65   * @version $Id: Form.java 205 2012-01-29 18:29:57Z oheger $
66   */
67  public class Form
68  {
69      /**
70       * Constant for a default {@code FormValidator} object. This instance is
71       * used if no other {@code FormValidator} was set. It always returns a valid
72       * result object.
73       */
74      private static final FormValidator DEF_FORM_VALIDATOR = new FormValidator()
75      {
76          // always return a valid results object for this form
77          public FormValidatorResults isValid(Form form)
78          {
79              return DefaultFormValidatorResults.validResultsForForm(form);
80          }
81      };
82  
83      /** Stores the components that belong to this form. */
84      private final ComponentStore fields;
85  
86      /** The binding strategy for accessing the data of the model object. */
87      private final BindingStrategy bindingStrategy;
88  
89      /** Stores the registered form validator. */
90      private FormValidator formValidator;
91  
92      /** Stores the transformer context. */
93      private final TransformerContext transformerContext;
94  
95      /**
96       * Creates a new instance of {@code Form} and initializes it with all
97       * required helper objects.
98       *
99       * @param ctx the {@code TransformerContext} (must not be <b>null</b>)
100      * @param strat the {@code BindingStrategy} (must not be <b>null</b>)
101      * @throws IllegalArgumentException if a required parameter is <b>null</b>
102      */
103     public Form(TransformerContext ctx, BindingStrategy strat)
104     {
105         if (ctx == null)
106         {
107             throw new IllegalArgumentException(
108                     "TransformerContext must not be null!");
109         }
110         if (strat == null)
111         {
112             throw new IllegalArgumentException(
113                     "BindingStrategy must not be null!");
114         }
115 
116         transformerContext = ctx;
117         bindingStrategy = strat;
118         fields = new ComponentStoreImpl();
119     }
120 
121     /**
122      * Returns the transformer context.
123      *
124      * @return the transformer context
125      */
126     public TransformerContext getTransformerContext()
127     {
128         return transformerContext;
129     }
130 
131     /**
132      * Returns the {@code BindingStrategy} used by this form.
133      *
134      * @return the {@code BindingStrategy}
135      */
136     public final BindingStrategy getBindingStrategy()
137     {
138         return fetchBindingStrategy();
139     }
140 
141     /**
142      * Returns the form validator. This can be <b>null</b> if no specific form
143      * validator has been set.
144      *
145      * @return the object used for validating the form
146      */
147     public FormValidator getFormValidator()
148     {
149         return formValidator;
150     }
151 
152     /**
153      * Sets the form validator.
154      *
155      * @param formValidator the form validator
156      */
157     public void setFormValidator(FormValidator formValidator)
158     {
159         this.formValidator = formValidator;
160     }
161 
162     /**
163      * Adds the specified field to this form. This method must be called for
164      * each field that should be managed by this form object.
165      *
166      * @param name the field's (internal) name
167      * @param fld the field handler for this field
168      */
169     public void addField(String name, FieldHandler fld)
170     {
171         fields.addFieldHandler(name, fld);
172     }
173 
174     /**
175      * Fills the form's fields with the properties of the passed in bean. This
176      * method can be used to initialize the form.
177      *
178      * @param bean the form bean; can be <b>null</b>, then this operation has no
179      *        effect
180      * @throws FormRuntimeException if an error occurs when initializing a field
181      */
182     public void initFields(Object bean)
183     {
184         initFields(bean, getFieldNames());
185     }
186 
187     /**
188      * Fills a sub set of the form's fields with the properties of the passed in
189      * bean. This method will iterate over all fields specified in the given set
190      * and initialize them from the corresponding properties of the specified
191      * bean. The set must contain only valid names of fields that belong to this
192      * form; otherwise an exception will be thrown.
193      *
194      * @param bean the form bean; can be <b>null</b>, then this operation has no
195      *        effect
196      * @param names a set with the names of the fields to be initialized
197      * @throws FormRuntimeException if a field cannot be initialized
198      * @throws IllegalArgumentException if the set is <b>null</b>
199      */
200     public void initFields(Object bean, Set<String> names)
201     {
202         if (bean == null)
203         {
204             // nothing to do
205             return;
206         }
207 
208         if (names == null)
209         {
210             throw new IllegalArgumentException("Sub set must not be null!");
211         }
212 
213         for (String fldName : names)
214         {
215             FieldHandler fh = fetchField(fldName);
216             try
217             {
218                 Object data = readModelProperty(bean, propertyName(fldName));
219                 fh.setData(data);
220             }
221             catch (Exception ex)
222             {
223                 // handle all possible exceptions the same way
224                 throw new FormRuntimeException("Error when initializing field "
225                         + fldName, ex);
226             }
227         }
228     }
229 
230     /**
231      * Validates this form and writes its content into the specified model
232      * object if validation is successful. This method performs validation on
233      * both the field and the form level. The former validation ensures that all
234      * fields contain syntactically correct data, i.e. the data they contain can
235      * be converted to their expected data type (e.g. the string entered by the
236      * user is indeed a valid date). The latter validation takes the form as the
237      * whole into account. Here for instance relations between fields can be
238      * checked (e.g. the date of delivery is greater than the shipment date).
239      * The passed in model object is populated with the form's data when all
240      * validation steps succeed. It must be compatible with the
241      * {@link BindingStrategy} used by the form. It is modified only if
242      * validation is successful; otherwise it is not changed.
243      *
244      * @param model the model object in which to write the form fields; can be
245      *        <b>null</b>, then no data is copied
246      * @return an object with validation results
247      */
248     public FormValidatorResults validate(Object model)
249     {
250         FormValidatorResults results = validateFields();
251         if (results.isValid())
252         {
253             // read form fields into bean and perform form level validation
254             results = DefaultFormValidatorResults.merge(results,
255                     validateForm(model));
256         }
257         return results;
258     }
259 
260     /**
261      * Returns a set with the names of all defined fields.
262      *
263      * @return a set with the field names
264      */
265     public Set<String> getFieldNames()
266     {
267         return fields.getFieldHandlerNames();
268     }
269 
270     /**
271      * Returns the <code>FieldHandler</code> object for the field with the given
272      * name. If no such field exists, <b>null </b> is returned.
273      *
274      * @param name the name of the desired field
275      * @return the field handler for this field
276      */
277     public FieldHandler getField(String name)
278     {
279         return fields.findFieldHandler(name);
280     }
281 
282     /**
283      * Returns the display name for the specified field. This implementation
284      * checks whether a display name is explicitly defined for the field handler
285      * with the given name. If this is the case, it is returned. Otherwise the
286      * field's name is returned. If the field is unknown, <b>null</b> is
287      * returned.
288      *
289      * @param fldName the name of the field
290      * @return the display name for this field
291      */
292     public String getDisplayName(String fldName)
293     {
294         FieldHandler fh = getField(fldName);
295         if (fh == null)
296         {
297             return null;
298         }
299         return (fh.getDisplayName() != null) ? fh.getDisplayName() : fldName;
300     }
301 
302     /**
303      * Returns the component store of this form. In this object all components
304      * that belong to this form are stored.
305      *
306      * @return the component store of this form
307      */
308     public ComponentStore getComponentStore()
309     {
310         return fields;
311     }
312 
313     /**
314      * Validates the fields of this form. This method ensures that all form
315      * fields are syntactically and semantically correct, i.e. it performs
316      * validation on both the fields and form level. After this method has been
317      * called and returned a positive result, the form bean is available and
318      * contains the current data.
319      *
320      * @return an object with results of the validation
321      */
322     public FormValidatorResults validateFields()
323     {
324         return validateFields(getFieldNames());
325     }
326 
327     /**
328      * Validates a sub set of the fields of this form. This method works like
329      * the overloaded version, but only fields whose name is contained in the
330      * passed in set are taken into account. This is useful if a partly
331      * validation is to be performed. If the set contains an invalid field name,
332      * a runtime exception will be thrown.
333      *
334      * @param names a set with the names of the fields to be validated
335      * @return an object with results of the validation
336      * @throws FormRuntimeException if an invalid field name is specified
337      * @throws IllegalArgumentException if the set is <b>null</b>
338      * @see #validateFields()
339      */
340     public FormValidatorResults validateFields(Set<String> names)
341     {
342         if (names == null)
343         {
344             throw new IllegalArgumentException("Sub set must not be null!");
345         }
346 
347         Map<String, ValidationResult> results = new HashMap<String, ValidationResult>();
348         performFieldValidation(ValidationPhase.SYNTAX, results, names);
349         performFieldValidation(ValidationPhase.LOGIC, results, names);
350 
351         return new DefaultFormValidatorResults(results);
352     }
353 
354     /**
355      * Validates the whole form using the {@code FormValidator}. This is an
356      * additional validation that can be performed after it was ensured that all
357      * fields are syntactically and semantically correct. The aim of this method
358      * is to apply high level validation rules that are able to check
359      * dependencies between form fields. Calling this method requires that
360      * validation of the field level has already been performed (e.g. by
361      * {@link #validateFields()}). If validation is successful (or if no {@code
362      * FormValidator} is defined), the passed in model object is populated with
363      * the content of this form. Otherwise it is not modified.
364      *
365      * @param model the model object; can be <b>null</b>, then no data is copied
366      * @return an object with the results of the validation
367      */
368     public FormValidatorResults validateForm(Object model)
369     {
370         FormValidatorResults results = fetchFormValidator().isValid(this);
371 
372         if (results.isValid())
373         {
374             // update the model object after a successful validation
375             readFields(model);
376         }
377 
378         return results;
379     }
380 
381     /**
382      * Reads the form's fields and copies their content into the passed in form
383      * bean. Before this method can be called validation of the form's fields
384      * must have been successful, i.e. {@link #validateFields()} must have been
385      * invoked and returned a positive result. If {@link #validateFields()} has
386      * not been called before, the passed in bean won't contain the current data
387      * of the form's fields. The contents of the fields is converted to the
388      * correct data types and written into the bean's properties.
389      *
390      * @param bean the bean in which to store the fields' content; can be
391      *        <b>null</b>, then this operation has no effect
392      * @throws FormRuntimeException if a field cannot be read
393      */
394     public void readFields(Object bean)
395     {
396         readFields(bean, getFieldNames());
397     }
398 
399     /**
400      * Reads a sub set of this form's fields and writes their content into the
401      * specified bean. This method works like the overloaded variant, but
402      * operates on a sub set of the fields only. If the passed in set contains
403      * an invalid name, a runtime exception is thrown.
404      *
405      * @param bean the bean in which to store the fields' content; can be
406      *        <b>null</b>, then this operation has no effect
407      * @param names the set with the names of the fields to read
408      * @throws FormRuntimeException if a field cannot be read
409      * @throws IllegalArgumentException if the set is <b>null</b>
410      */
411     public void readFields(Object bean, Set<String> names)
412     {
413         if (bean == null)
414         {
415             return;
416         }
417 
418         if (names == null)
419         {
420             throw new IllegalArgumentException("Sub set must not be null!");
421         }
422 
423         for (String fldName : names)
424         {
425             FieldHandler fh = fetchField(fldName);
426             try
427             {
428                 writeModelProperty(bean, propertyName(fldName), fh.getData());
429             }
430             catch (Exception ex)
431             {
432                 // Handle all reflection exceptions the same way
433                 throw new FormRuntimeException("Error when reading field "
434                         + fldName, ex);
435             }
436         }
437     }
438 
439     /**
440      * Fetches the validator to be used for form validation. In contrast to
441      * {@link #getFormValidator()} this method never returns <b>null</b>. If no
442      * {@link FormValidator} has been set, a dummy implementation is returned.
443      *
444      * @return the {@code FormValidator} to be used
445      */
446     FormValidator fetchFormValidator()
447     {
448         return (getFormValidator() != null) ? getFormValidator()
449                 : DEF_FORM_VALIDATOR;
450     }
451 
452     /**
453      * Obtains the current {@code BindingStrategy}. (This method mainly exists
454      * for testing purposes.)
455      *
456      * @return the {@code BindingStrategy}
457      */
458     BindingStrategy fetchBindingStrategy()
459     {
460         return bindingStrategy;
461     }
462 
463     /**
464      * Reads a property from the given model object. This method is called by
465      * {@code initFields()}. It delegates to the {@code BindingStrategy} to read
466      * the data.
467      *
468      * @param model the model object
469      * @param propertyName the name of the property to read
470      * @return the value of this property
471      */
472     Object readModelProperty(Object model, String propertyName)
473     {
474         return fetchBindingStrategy().readProperty(model, propertyName);
475     }
476 
477     /**
478      * Writes a property of the given model object. This method is called by
479      * {@code readFields()}. It delegates to the {@code BindingStrategy} to
480      * write the data.
481      *
482      * @param model the model object
483      * @param propertyName the name of the property to write
484      * @param value the value to be written
485      */
486     void writeModelProperty(Object model, String propertyName, Object value)
487     {
488         fetchBindingStrategy().writeProperty(model, propertyName, value);
489     }
490 
491     /**
492      * Helper method for performing validation on the form's fields. This method
493      * checks only those fields that either have not been checked ore are valid.
494      *
495      * @param phase the validation phase
496      * @param validationResults the validation results to be filled
497      * @param names a set with the names of the affected fields
498      */
499     private void performFieldValidation(ValidationPhase phase,
500             Map<String, ValidationResult> validationResults, Set<String> names)
501     {
502         for (String fldName : names)
503         {
504             if (getField(fldName) == null)
505             {
506                 throw new FormRuntimeException("Invalid field name: " + fldName);
507             }
508 
509             ValidationResult vres = validationResults.get(fldName);
510             if (vres == null || vres.isValid())
511             {
512                 validationResults.put(fldName, DefaultValidationResult.merge(
513                         vres, getField(fldName).validate(phase)));
514             }
515         }
516     }
517 
518     /**
519      * Returns the name of the property for the specified field. If the
520      * corresponding field handler defines a property name, this name is
521      * returned. Otherwise the name of the field itself is used.
522      *
523      * @param field the name of the field
524      * @return the corresponding property name
525      * @throws FormRuntimeException if the field is unknown
526      */
527     private String propertyName(String field)
528     {
529         FieldHandler fh = fetchField(field);
530         return (fh.getPropertyName() != null) ? fh.getPropertyName() : field;
531     }
532 
533     /**
534      * Fetches the field handler for the specified field. If the field is
535      * unknown, an exception is thrown.
536      *
537      * @param field the name of the desired field
538      * @return the handler for this field
539      * @throws FormRuntimeException if the field is unknown
540      */
541     private FieldHandler fetchField(String field)
542     {
543         FieldHandler fh = getField(field);
544         if (fh == null)
545         {
546             throw new FormRuntimeException("Cannot resolve field: " + field);
547         }
548 
549         return fh;
550     }
551 }