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