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 }