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