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        public void recordFieldInputValue(String input)
227        {
228            FieldTracking tracking = findCurrentTracking();
229    
230            tracking.setInput(input);
231        }
232    
233        /**
234         * Finds or creates the field tracking for the {@link #setFormComponent(IFormComponent)}
235         * &nbsp;current component. If no current component, an unassociated error is created and
236         * returned.
237         * 
238         * @since 3.0
239         */
240    
241        protected FieldTracking findCurrentTracking()
242        {
243            FieldTracking result = null;
244    
245            if (_currentComponent == null)
246            {
247                result = new FieldTracking();
248    
249                // Add it to the field trackings, but not to the
250                // map.
251    
252                _trackings.add(result);
253            }
254            else
255            {
256                result = getComponentTracking();
257    
258                if (result == null)
259                {
260                    String fieldName = _currentComponent.getName();
261    
262                    result = new FieldTracking(fieldName, _currentComponent);
263    
264                    _trackings.add(result);
265                    _trackingMap.put(fieldName, result);
266                }
267            }
268    
269            return result;
270        }
271    
272        /**
273         * Does nothing. Override in a subclass to decoreate fields.
274         */
275    
276        public void writePrefix(IMarkupWriter writer, IRequestCycle cycle, IFormComponent component,
277                IValidator validator)
278        {
279        }
280    
281        /**
282         * Does nothing. Override in a subclass to decorate fields.
283         */
284    
285        public void writeAttributes(IMarkupWriter writer, IRequestCycle cycle,
286                IFormComponent component, IValidator validator)
287        {
288        }
289    
290        /**
291         * Default implementation; if the current field is in error, then a suffix is written. The
292         * suffix is: <code>&amp;nbsp;&lt;font color="red"&gt;**&lt;/font&gt;</code>.
293         */
294    
295        public void writeSuffix(IMarkupWriter writer, IRequestCycle cycle, IFormComponent component,
296                IValidator validator)
297        {
298            if (isInError())
299            {
300                writer.printRaw("&nbsp;");
301                writer.begin("font");
302                writer.attribute("color", "red");
303                writer.print("**");
304                writer.end();
305            }
306        }
307    
308        public boolean getHasErrors()
309        {
310            return getFirstError() != null;
311        }
312    
313        /**
314         * A convienience, as most pages just show the first error on the page.
315         * <p>
316         * As of release 1.0.9, this returns an instance of {@link IRender}, not a {@link String}.
317         */
318    
319        public IRender getFirstError()
320        {
321            if (Tapestry.size(_trackings) == 0)
322                return null;
323    
324            Iterator i = _trackings.iterator();
325    
326            while (i.hasNext())
327            {
328                IFieldTracking tracking = (IFieldTracking) i.next();
329    
330                if (tracking.isInError())
331                    return tracking.getErrorRenderer();
332            }
333    
334            return null;
335        }
336    
337        /**
338         * Checks to see if the field is in error. This will <em>not</em> work properly in a loop, but
339         * is only used by {@link FieldLabel}. Therefore, using {@link FieldLabel}in a loop (where the
340         * {@link IFormComponent}is renderred more than once) will not provide correct results.
341         */
342    
343        protected boolean isInError(IFormComponent component)
344        {
345            // Get the name as most recently rendered.
346    
347            String fieldName = component.getName();
348    
349            IFieldTracking tracking = (IFieldTracking) _trackingMap.get(fieldName);
350    
351            return tracking != null && tracking.isInError();
352        }
353    
354        /**
355         * Returns a {@link List}of {@link IFieldTracking}s. This is the master list of trackings,
356         * except that it omits and trackings that are not associated with a particular field. May
357         * return an empty list, or null.
358         * <p>
359         * Order is not determined, though it is likely the order in which components are laid out on in
360         * the template (this is subject to change).
361         */
362    
363        public List getAssociatedTrackings()
364        {
365            int count = Tapestry.size(_trackings);
366    
367            if (count == 0)
368                return null;
369    
370            List result = new ArrayList(count);
371    
372            for (int i = 0; i < count; i++)
373            {
374                IFieldTracking tracking = (IFieldTracking) _trackings.get(i);
375    
376                if (tracking.getFieldName() == null)
377                    continue;
378    
379                result.add(tracking);
380            }
381    
382            return result;
383        }
384    
385        /**
386         * Like {@link #getAssociatedTrackings()}, but returns only the unassociated trackings.
387         * Unassociated trackings are new (in release 1.0.9), and are why interface
388         * {@link IFieldTracking}is not very well named.
389         * <p>
390         * The trackings are returned in an unspecified order, which (for the moment, anyway) is the
391         * order in which they were added (this could change in the future, or become more concrete).
392         */
393    
394        public List getUnassociatedTrackings()
395        {
396            int count = Tapestry.size(_trackings);
397    
398            if (count == 0)
399                return null;
400    
401            List result = new ArrayList(count);
402    
403            for (int i = 0; i < count; i++)
404            {
405                IFieldTracking tracking = (IFieldTracking) _trackings.get(i);
406    
407                if (tracking.getFieldName() != null)
408                    continue;
409    
410                result.add(tracking);
411            }
412    
413            return result;
414        }
415    
416        public List getErrorRenderers()
417        {
418            List result = new ArrayList();
419    
420            Iterator i = _trackings.iterator();
421            while (i.hasNext())
422            {
423                IFieldTracking tracking = (IFieldTracking) i.next();
424    
425                IRender errorRenderer = tracking.getErrorRenderer();
426    
427                if (errorRenderer != null)
428                    result.add(errorRenderer);
429            }
430    
431            return result;
432        }
433    
434        /** @since 4.0 */
435    
436        public void registerForFocus(IFormComponent field, int priority)
437        {
438            if (priority > _focusPriority)
439            {
440                _focusField = field.getClientId();
441                _focusPriority = priority;
442            }
443        }
444    
445        /**
446         * Returns the focus field, or null if no form components registered for focus (i.e., they were
447         * all disabled).
448         */
449    
450        public String getFocusField()
451        {
452            return _focusField;
453        }
454    
455    }