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