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