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