001 // Copyright 2004, 2005 The Apache Software Foundation
002 //
003 // Licensed under the Apache License, Version 2.0 (the "License");
004 // you may not use this file except in compliance with the License.
005 // You may obtain a copy of the License at
006 //
007 // http://www.apache.org/licenses/LICENSE-2.0
008 //
009 // Unless required by applicable law or agreed to in writing, software
010 // distributed under the License is distributed on an "AS IS" BASIS,
011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 // See the License for the specific language governing permissions and
013 // limitations under the License.
014
015 package org.apache.tapestry.valid;
016
017 import org.apache.tapestry.IMarkupWriter;
018 import org.apache.tapestry.IRender;
019 import org.apache.tapestry.IRequestCycle;
020 import org.apache.tapestry.Tapestry;
021 import org.apache.tapestry.form.IFormComponent;
022
023 import java.util.*;
024
025 /**
026 * A base implementation of {@link IValidationDelegate} that can be used as a
027 * managed bean. This class is often subclassed, typically to override
028 * presentation details.
029 *
030 * @author Howard Lewis Ship
031 * @since 1.0.5
032 */
033
034 public class ValidationDelegate implements IValidationDelegate
035 {
036
037 private static final long serialVersionUID = 6215074338439140780L;
038
039 private transient IFormComponent _currentComponent;
040
041 private transient String _focusField;
042
043 private transient int _focusPriority = -1;
044
045 /**
046 * A list of {@link IFieldTracking}.
047 */
048
049 private final List _trackings = new ArrayList();
050
051 /**
052 * A map of {@link IFieldTracking}, keyed on form element name.
053 */
054
055 private final Map _trackingMap = new HashMap();
056
057 public void clear()
058 {
059 _currentComponent = null;
060 _trackings.clear();
061 _trackingMap.clear();
062 }
063
064 public void clearErrors()
065 {
066 if (_trackings == null)
067 return;
068
069 Iterator i = _trackings.iterator();
070 while(i.hasNext())
071 {
072 FieldTracking ft = (FieldTracking) i.next();
073 ft.setErrorRenderer(null);
074 ft.setRenderError(false);
075 }
076 }
077
078 /**
079 * If the form component is in error, places a <font color="red"<
080 * around it. Note: this will only work on the render phase after a rewind,
081 * and will be confused if components are inside any kind of loop.
082 */
083
084 public void writeLabelPrefix(IFormComponent component,
085 IMarkupWriter writer, IRequestCycle cycle)
086 {
087 if (isInError(component))
088 {
089 writer.begin("font");
090 writer.attribute("color", "red");
091 }
092 }
093
094 /**
095 * Does nothing by default. {@inheritDoc}
096 */
097
098 public void writeLabelAttributes(IMarkupWriter writer, IRequestCycle cycle,
099 IFormComponent component)
100 {
101 }
102
103 /**
104 * {@inheritDoc}
105 */
106 public void beforeLabelText(IMarkupWriter writer, IRequestCycle cycle, IFormComponent component)
107 {
108 }
109
110 /**
111 * {@inheritDoc}
112 */
113 public void afterLabelText(IMarkupWriter writer, IRequestCycle cycle, IFormComponent component)
114 {
115 }
116
117 /**
118 * Closes the <font> element,started by
119 * {@link #writeLabelPrefix(IFormComponent,IMarkupWriter,IRequestCycle)},
120 * if the form component is in error.
121 */
122
123 public void writeLabelSuffix(IFormComponent component,
124 IMarkupWriter writer, IRequestCycle cycle)
125 {
126 if (isInError(component))
127 {
128 writer.end();
129 }
130 }
131
132 /**
133 * Returns the {@link IFieldTracking}for the current component, if any. The
134 * {@link IFieldTracking}is usually created in
135 * {@link #record(String, ValidationConstraint)}or in
136 * {@link #record(IRender, ValidationConstraint)}.
137 * <p>
138 * Components may be rendered multiple times, with multiple names (provided
139 * by the {@link org.apache.tapestry.form.Form}, care must be taken that
140 * this method is invoked <em>after</em> the Form has provided a unique
141 * {@link IFormComponent#getName()}for the component.
142 *
143 * @see #setFormComponent(IFormComponent)
144 * @return the {@link FieldTracking}, or null if the field has no tracking.
145 */
146
147 protected FieldTracking getComponentTracking()
148 {
149 return (FieldTracking) _trackingMap.get(_currentComponent.getName());
150 }
151
152 public void setFormComponent(IFormComponent component)
153 {
154 _currentComponent = component;
155 }
156
157 public boolean isInError()
158 {
159 IFieldTracking tracking = getComponentTracking();
160
161 return tracking != null && tracking.isInError();
162 }
163
164 public String getFieldInputValue()
165 {
166 IFieldTracking tracking = getComponentTracking();
167
168 return tracking == null ? null : tracking.getInput();
169 }
170
171 /**
172 * Returns all the field trackings as an unmodifiable List.
173 */
174
175 public List getFieldTracking()
176 {
177 if (Tapestry.size(_trackings) == 0) return null;
178
179 return Collections.unmodifiableList(_trackings);
180 }
181
182 /** @since 3.0.2 */
183 public IFieldTracking getCurrentFieldTracking()
184 {
185 return findCurrentTracking();
186 }
187
188 public void reset()
189 {
190 IFieldTracking tracking = getComponentTracking();
191
192 if (tracking != null)
193 {
194 _trackings.remove(tracking);
195 _trackingMap.remove(tracking.getFieldName());
196 }
197 }
198
199 /**
200 * Invokes {@link #record(String, ValidationConstraint)}, or
201 * {@link #record(IRender, ValidationConstraint)}if the
202 * {@link ValidatorException#getErrorRenderer() error renderer property}is
203 * not null.
204 */
205
206 public void record(ValidatorException ex)
207 {
208 IRender errorRenderer = ex.getErrorRenderer();
209
210 if (errorRenderer == null)
211 record(ex.getMessage(), ex.getConstraint());
212 else
213 record(errorRenderer, ex.getConstraint());
214 }
215
216 /**
217 * Invokes {@link #record(IRender, ValidationConstraint)}, after wrapping
218 * the message parameter in a {@link RenderString}.
219 */
220
221 public void record(String message, ValidationConstraint constraint)
222 {
223 record(new RenderString(message), constraint);
224 }
225
226 /**
227 * Records error information about the currently selected component, or
228 * records unassociated (with any field) errors.
229 * <p>
230 * Currently, you may have at most one error per <em>field</em> (note the
231 * difference between field and component), but any number of unassociated
232 * errors.
233 * <p>
234 * Subclasses may override the default error message (based on other
235 * factors, such as the field and constraint) before invoking this
236 * implementation.
237 *
238 * @since 1.0.9
239 */
240
241 public void record(IRender errorRenderer, ValidationConstraint constraint)
242 {
243 FieldTracking tracking = findCurrentTracking();
244
245 // Note that recording two errors for the same field is not advised; the
246 // second will override the first.
247
248 tracking.setErrorRenderer(errorRenderer);
249 tracking.setConstraint(constraint);
250 }
251
252 /** @since 4.0 */
253
254 public void record(IFormComponent field, String message)
255 {
256 setFormComponent(field);
257
258 record(message, null);
259 }
260
261 public void recordFieldInputValue(String input)
262 {
263 FieldTracking tracking = findCurrentTracking();
264
265 tracking.setInput(input);
266 }
267
268 /**
269 * Finds or creates the field tracking for the
270 * {@link #setFormComponent(IFormComponent)} current component. If no
271 * current component, an unassociated error is created and returned.
272 *
273 * @since 3.0
274 */
275
276 protected FieldTracking findCurrentTracking()
277 {
278 FieldTracking result = null;
279
280 if (_currentComponent == null)
281 {
282 result = new FieldTracking();
283
284 // Add it to the field trackings, but not to the
285 // map.
286
287 _trackings.add(result);
288 }
289 else
290 {
291 result = getComponentTracking();
292
293 if (result == null)
294 {
295 String fieldName = _currentComponent.getName();
296
297 result = new FieldTracking(fieldName, _currentComponent);
298
299 _trackings.add(result);
300 _trackingMap.put(fieldName, result);
301 }
302 }
303
304 return result;
305 }
306
307 /**
308 * Does nothing. Override in a subclass to decorate fields.
309 */
310
311 public void writePrefix(IMarkupWriter writer, IRequestCycle cycle,
312 IFormComponent component, IValidator validator)
313 {
314 }
315
316 /**
317 * Currently appends a single css class attribute of <code>fieldInvalid</code> if the field
318 * is in error. If the field has a matching constraint of {@link ValidationConstraint#REQUIRED}
319 * the <code>fieldMissing</code> is written instead.
320 */
321
322 public void writeAttributes(IMarkupWriter writer, IRequestCycle cycle,
323 IFormComponent component, IValidator validator)
324 {
325 IFieldTracking tracking = getFieldTracking(component);
326 if (tracking == null || !tracking.getRenderError())
327 return;
328
329 if (tracking.getConstraint() != null
330 && tracking.getConstraint() == ValidationConstraint.REQUIRED)
331 {
332 writer.appendAttribute("class", "fieldMissing");
333 } else if (tracking.isInError())
334 {
335
336 writer.appendAttribute("class", "fieldInvalid");
337 }
338 }
339
340 /**
341 * Default implementation; if the current field is in error, then a suffix
342 * is written. The suffix is:
343 * <code>&nbsp;<font color="red">**</font></code>.
344 */
345
346 public void writeSuffix(IMarkupWriter writer, IRequestCycle cycle,
347 IFormComponent component, IValidator validator)
348 {
349 IFieldTracking tracking = getComponentTracking();
350
351 if (tracking != null && tracking.isInError() && tracking.getRenderError())
352 {
353 writer.printRaw(" ");
354 writer.begin("font");
355 writer.attribute("color", "red");
356 writer.print("**");
357 writer.end();
358 }
359 }
360
361 public boolean getHasErrors()
362 {
363 return getFirstError() != null;
364 }
365
366 /**
367 * A convienience, as most pages just show the first error on the page.
368 * <p>
369 * As of release 1.0.9, this returns an instance of {@link IRender}, not a
370 * {@link String}.
371 */
372
373 public IRender getFirstError()
374 {
375 if (Tapestry.size(_trackings) == 0)
376 return null;
377
378 Iterator i = _trackings.iterator();
379
380 while(i.hasNext())
381 {
382 IFieldTracking tracking = (IFieldTracking) i.next();
383
384 if (tracking.isInError()) return tracking.getErrorRenderer();
385 }
386
387 return null;
388 }
389
390 /**
391 * Checks to see if the field is in error. This will <em>not</em> work
392 * properly in a loop, but is only used by {@link FieldLabel}. Therefore,
393 * using {@link FieldLabel}in a loop (where the {@link IFormComponent}is
394 * renderred more than once) will not provide correct results.
395 */
396
397 protected boolean isInError(IFormComponent component)
398 {
399 // Get the name as most recently rendered.
400
401 String fieldName = component.getName();
402
403 IFieldTracking tracking = (IFieldTracking) _trackingMap.get(fieldName);
404
405 return tracking != null && tracking.isInError();
406 }
407
408 protected IFieldTracking getFieldTracking(IFormComponent component)
409 {
410 String fieldName = component.getName();
411
412 return (IFieldTracking) _trackingMap.get(fieldName);
413 }
414
415 /**
416 * Returns a {@link List}of {@link IFieldTracking}s. This is the master
417 * list of trackings, except that it omits and trackings that are not
418 * associated with a particular field. May return an empty list, or null.
419 * <p>
420 * Order is not determined, though it is likely the order in which
421 * components are laid out on in the template (this is subject to change).
422 */
423
424 public List getAssociatedTrackings()
425 {
426 int count = Tapestry.size(_trackings);
427
428 if (count == 0) return null;
429
430 List result = new ArrayList(count);
431
432 for(int i = 0; i < count; i++)
433 {
434 IFieldTracking tracking = (IFieldTracking) _trackings.get(i);
435
436 if (tracking.getFieldName() == null) continue;
437
438 result.add(tracking);
439 }
440
441 return result;
442 }
443
444 /**
445 * Like {@link #getAssociatedTrackings()}, but returns only the
446 * unassociated trackings. Unassociated trackings are new (in release
447 * 1.0.9), and are why interface {@link IFieldTracking}is not very well
448 * named.
449 * <p>
450 * The trackings are returned in an unspecified order, which (for the
451 * moment, anyway) is the order in which they were added (this could change
452 * in the future, or become more concrete).
453 */
454
455 public List getUnassociatedTrackings()
456 {
457 int count = Tapestry.size(_trackings);
458
459 if (count == 0) return null;
460
461 List result = new ArrayList(count);
462
463 for(int i = 0; i < count; i++)
464 {
465 IFieldTracking tracking = (IFieldTracking) _trackings.get(i);
466
467 if (tracking.getFieldName() != null) continue;
468
469 result.add(tracking);
470 }
471
472 return result;
473 }
474
475 public List getErrorRenderers()
476 {
477 List result = new ArrayList();
478
479 Iterator i = _trackings.iterator();
480 while(i.hasNext())
481 {
482 IFieldTracking tracking = (IFieldTracking) i.next();
483
484 IRender errorRenderer = tracking.getErrorRenderer();
485
486 if (errorRenderer != null)
487 result.add(errorRenderer);
488 }
489
490 return result;
491 }
492
493 /** @since 4.0 */
494
495 public void registerForFocus(IFormComponent field, int priority)
496 {
497 if (priority > _focusPriority)
498 {
499 _focusField = field.getClientId();
500 _focusPriority = priority;
501 }
502 }
503
504 /**
505 * Returns the focus field, or null if no form components registered for
506 * focus (i.e., they were all disabled).
507 */
508
509 public String getFocusField()
510 {
511 return _focusField;
512 }
513
514 }