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