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)} &nbsp;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>&amp;nbsp;&lt;font color="red"&gt;**&lt;/font&gt;</code>.
317         */
318    
319        public void writeSuffix(IMarkupWriter writer, IRequestCycle cycle,
320                IFormComponent component, IValidator validator)
321        {
322            if (isInError())
323            {
324                writer.printRaw("&nbsp;");
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    }