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)} 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>&nbsp;<font color="red">**</font></code>. 343 */ 344 345 public void writeSuffix(IMarkupWriter writer, IRequestCycle cycle, 346 IFormComponent component, IValidator validator) 347 { 348 if (isInError()) 349 { 350 writer.printRaw(" "); 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 }