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 * 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>&nbsp;<font color="red">**</font></code>. 293 */ 294 295 public void writeSuffix(IMarkupWriter writer, IRequestCycle cycle, IFormComponent component, 296 IValidator validator) 297 { 298 if (isInError()) 299 { 300 writer.printRaw(" "); 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 }