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)} 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>&nbsp;<font color="red">**</font></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(" "); 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 }