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
015package org.apache.tapestry.valid;
016
017import java.util.ArrayList;
018import java.util.Collections;
019import java.util.HashMap;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.tapestry.IMarkupWriter;
025import org.apache.tapestry.IRender;
026import org.apache.tapestry.IRequestCycle;
027import org.apache.tapestry.Tapestry;
028import 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
038public 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     * &nbsp;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>&amp;nbsp;&lt;font color="red"&gt;**&lt;/font&gt;</code>.
310     */
311
312    public void writeSuffix(IMarkupWriter writer, IRequestCycle cycle, IFormComponent component,
313            IValidator validator)
314    {
315        if (isInError())
316        {
317            writer.printRaw("&nbsp;");
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}