001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.markup.html.form; 018 019import java.io.Serializable; 020import java.text.Format; 021import java.text.SimpleDateFormat; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.Iterator; 028import java.util.LinkedHashSet; 029import java.util.List; 030import java.util.Locale; 031import java.util.Map; 032import java.util.Set; 033 034import org.apache.wicket.Application; 035import org.apache.wicket.Component; 036import org.apache.wicket.IConverterLocator; 037import org.apache.wicket.IGenericComponent; 038import org.apache.wicket.Localizer; 039import org.apache.wicket.WicketRuntimeException; 040import org.apache.wicket.ajax.AjaxRequestTarget; 041import org.apache.wicket.behavior.Behavior; 042import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; 043import org.apache.wicket.core.util.lang.WicketObjects; 044import org.apache.wicket.markup.ComponentTag; 045import org.apache.wicket.markup.html.form.AutoLabelResolver.AutoLabelMarker; 046import org.apache.wicket.model.IModel; 047import org.apache.wicket.model.IObjectClassAwareModel; 048import org.apache.wicket.model.IPropertyReflectionAwareModel; 049import org.apache.wicket.model.Model; 050import org.apache.wicket.request.IRequestParameters; 051import org.apache.wicket.util.convert.ConversionException; 052import org.apache.wicket.util.convert.IConverter; 053import org.apache.wicket.util.lang.Args; 054import org.apache.wicket.util.lang.Classes; 055import org.apache.wicket.util.string.StringList; 056import org.apache.wicket.util.string.StringValue; 057import org.apache.wicket.util.string.Strings; 058import org.apache.wicket.util.string.interpolator.VariableInterpolator; 059import org.apache.wicket.util.visit.IVisit; 060import org.apache.wicket.util.visit.IVisitFilter; 061import org.apache.wicket.util.visit.IVisitor; 062import org.apache.wicket.util.visit.Visits; 063import org.apache.wicket.validation.IErrorMessageSource; 064import org.apache.wicket.validation.INullAcceptingValidator; 065import org.apache.wicket.validation.IValidatable; 066import org.apache.wicket.validation.IValidationError; 067import org.apache.wicket.validation.IValidator; 068import org.apache.wicket.validation.ValidationError; 069import org.apache.wicket.validation.ValidatorAdapter; 070import org.slf4j.Logger; 071import org.slf4j.LoggerFactory; 072 073 074/** 075 * An HTML form component knows how to validate itself. Validators that implement IValidator can be 076 * added to the component. They will be evaluated in the order they were added and the first 077 * Validator that returns an error message determines the error message returned by the component. 078 * <p> 079 * FormComponents are not versioned by default. If you need versioning for your FormComponents, you 080 * will need to call Form.setVersioned(true), which will set versioning on for the form and all form 081 * component children. 082 * <p> 083 * If this component is required and that fails, the error key that is used is the "Required"; if 084 * the type conversion fails, it will use the key "IConverter" if the conversion failed in a 085 * converter, or "ConversionError" if type was explicitly specified via {@link #setType(Class)} or a 086 * {@link IPropertyReflectionAwareModel} was used. Notice that both "IConverter" and 087 * "ConversionError" have a more specific variant of "key.classname" where classname is the type 088 * that we failed to convert to. Classname is not full qualified, so only the actual name of the 089 * class is used. 090 * 091 * Property expressions that can be used in error messages are: 092 * <ul> 093 * <li>${input}: the input the user did give</li> 094 * <li>${name}: the name of the component that failed</li> 095 * <li>${label}: the label of the component</li> 096 * </ul> 097 * 098 * @author Jonathan Locke 099 * @author Eelco Hillenius 100 * @author Johan Compagner 101 * @author Igor Vaynberg (ivaynberg) 102 * 103 * @param <T> 104 * The model object type 105 * 106 */ 107public abstract class FormComponent<T> extends LabeledWebMarkupContainer implements 108 IFormVisitorParticipant, IFormModelUpdateListener, IGenericComponent<T, FormComponent<T>> 109{ 110 private static final Logger logger = LoggerFactory.getLogger(FormComponent.class); 111 112 /** 113 * {@link IErrorMessageSource} used for error messages against this form components. 114 * 115 * @author ivaynberg 116 */ 117 private class MessageSource implements IErrorMessageSource 118 { 119 private final Set<String> triedKeys = new LinkedHashSet<>(); 120 121 @Override 122 public String getMessage(String key, Map<String, Object> vars) 123 { 124 final FormComponent<T> formComponent = FormComponent.this; 125 126 // Use the following slf4j-simple config for detailed logging 127 // on the property resolution process 128 // org.slf4j.simpleLogger.log.org.apache.wicket.resource.loader=DEBUG 129 // org.slf4j.simpleLogger.log.org.apache.wicket.Localizer=DEBUG 130 131 final Localizer localizer = formComponent.getLocalizer(); 132 133 // retrieve prefix that will be used to construct message keys 134 String prefix = formComponent.getValidatorKeyPrefix(); 135 String message; 136 137 // first try the full form of key [form-component-id].[prefix].[key] 138 String resource = getId() + "." + prefix(prefix, key); 139 message = getString(localizer, resource, formComponent); 140 141 // if not found, try a more general form (without prefix) 142 // [form-component-id].[key] 143 if (Strings.isEmpty(message) && Strings.isEmpty(prefix)) 144 { 145 resource = getId() + "." + key; 146 message = getString(localizer, resource, formComponent); 147 } 148 149 // If not found try a more general form [prefix].[key] 150 if (Strings.isEmpty(message)) 151 { 152 resource = prefix(prefix, key); 153 message = getString(localizer, resource, formComponent); 154 } 155 156 // If not found try the most general form [key] 157 if (Strings.isEmpty(message)) 158 { 159 // Try a variation of the resource key 160 message = getString(localizer, key, formComponent); 161 } 162 163 // convert empty string to null in case our default value of "" was 164 // returned from localizer 165 if (Strings.isEmpty(message)) 166 { 167 message = null; 168 } 169 else 170 { 171 message = substitute(message, addDefaultVars(vars)); 172 } 173 return message; 174 } 175 176 private String prefix(String prefix, String key) 177 { 178 if (!Strings.isEmpty(prefix)) 179 { 180 return prefix + "." + key; 181 } 182 else 183 { 184 return key; 185 } 186 } 187 188 /** 189 * 190 * @param localizer 191 * @param key 192 * @param component 193 * @return string 194 */ 195 private String getString(Localizer localizer, String key, Component component) 196 { 197 triedKeys.add(key); 198 199 // Note: It is important that the default value of "" is 200 // provided to getString() not to throw a MissingResourceException or to 201 // return a default string like "[Warning: String ..." 202 return localizer.getString(key, component, ""); 203 } 204 205 private String substitute(String string, final Map<String, Object> vars) 206 throws IllegalStateException 207 { 208 return new VariableInterpolator(string, Application.get() 209 .getResourceSettings() 210 .getThrowExceptionOnMissingResource()) 211 { 212 private static final long serialVersionUID = 1L; 213 214 @SuppressWarnings({ "rawtypes", "unchecked" }) 215 @Override 216 protected String getValue(String variableName) 217 { 218 Object value = vars.get(variableName); 219 220 if (value == null ||value instanceof String) 221 { 222 return String.valueOf(value); 223 } 224 225 IConverter converter = getConverter(value.getClass()); 226 227 if (converter == null) 228 { 229 return Strings.toString(value); 230 } 231 else 232 { 233 return converter.convertToString(value, getLocale()); 234 } 235 } 236 }.toString(); 237 } 238 239 /** 240 * Creates a new params map that additionally contains the default input, name, label 241 * parameters 242 * 243 * @param params 244 * original params map 245 * @return new params map 246 */ 247 private Map<String, Object> addDefaultVars(Map<String, Object> params) 248 { 249 // create and fill the new params map 250 final HashMap<String, Object> fullParams; 251 if (params == null) 252 { 253 fullParams = new HashMap<>(6); 254 } 255 else 256 { 257 fullParams = new HashMap<>(params.size() + 6); 258 fullParams.putAll(params); 259 } 260 261 // add the input param if not already present 262 if (!fullParams.containsKey("input")) 263 { 264 fullParams.put("input", getInput()); 265 } 266 267 // add the name param if not already present 268 if (!fullParams.containsKey("name")) 269 { 270 fullParams.put("name", getId()); 271 } 272 273 // add the label param if not already present 274 if (!fullParams.containsKey("label")) 275 { 276 fullParams.put("label", getLabel()); 277 } 278 return fullParams; 279 } 280 281 /** 282 * @return value of label param for this form component 283 */ 284 private String getLabel() 285 { 286 final FormComponent<T> fc = FormComponent.this; 287 String label = null; 288 289 // first try the label model ... 290 if (fc.getLabel() != null) 291 { 292 label = fc.getLabel().getObject(); 293 } 294 // ... then try a resource of format [form-component-id] with 295 // default of '[form-component-id]' 296 if (label == null) 297 { 298 299 label = fc.getDefaultLabel(); 300 } 301 return label; 302 } 303 } 304 305 @Override 306 protected void onBeforeRender() 307 { 308 // WICKET-7101 update related auto-label 309 getRequestCycle().find(IPartialPageRequestHandler.class).ifPresent(handler -> updateAutoLabels(handler, true)); 310 super.onBeforeRender(); 311 } 312 313 /** 314 * Adapter that makes this component appear as {@link IValidatable} 315 * 316 * @author ivaynberg 317 */ 318 private class ValidatableAdapter implements IValidatable<T> 319 { 320 /** 321 * @see org.apache.wicket.validation.IValidatable#error(org.apache.wicket.validation.IValidationError) 322 */ 323 @Override 324 public void error(IValidationError error) 325 { 326 FormComponent.this.error(error); 327 } 328 329 /** 330 * @see org.apache.wicket.validation.IValidatable#getValue() 331 */ 332 @Override 333 public T getValue() 334 { 335 return getConvertedInput(); 336 } 337 338 /** 339 * @see org.apache.wicket.validation.IValidatable#isValid() 340 */ 341 @Override 342 public boolean isValid() 343 { 344 return FormComponent.this.isValid(); 345 } 346 347 @Override 348 public IModel<T> getModel() 349 { 350 return FormComponent.this.getModel(); 351 } 352 } 353 354 /** 355 * The value separator 356 */ 357 public static final String VALUE_SEPARATOR = ";"; 358 359 private static final String[] EMPTY_STRING_ARRAY = new String[] { "" }; 360 361 /** Whether or not this component's value is required (non-empty) */ 362 private static final short FLAG_REQUIRED = FLAG_RESERVED3; 363 364 private static final String NO_RAW_INPUT = "[-NO-RAW-INPUT-]"; 365 366 private static final long serialVersionUID = 1L; 367 368 /** 369 * Make empty strings null values boolean. Used by AbstractTextComponent subclass. 370 */ 371 protected static final short FLAG_CONVERT_EMPTY_INPUT_STRING_TO_NULL = FLAG_RESERVED1; 372 373 /** 374 * Visits any form components inside component if it is a container, or component itself if it 375 * is itself a form component 376 * 377 * @param <R> 378 * the type of the visitor's result 379 * 380 * @param component 381 * starting point of the traversal 382 * 383 * @param visitor 384 * The visitor to call 385 * @return the visitor's result 386 */ 387 public static <R> R visitFormComponentsPostOrder(Component component, 388 final IVisitor<? extends FormComponent<?>, R> visitor) 389 { 390 return Visits.visitPostOrder(component, visitor, new IVisitFilter() 391 { 392 393 @Override 394 public boolean visitChildren(Object object) 395 { 396 if (object instanceof IFormVisitorParticipant) 397 { 398 return ((IFormVisitorParticipant)object).processChildren(); 399 } 400 return true; 401 } 402 403 @Override 404 public boolean visitObject(Object object) 405 { 406 return (object instanceof FormComponent<?>); 407 } 408 409 }); 410 411 } 412 413 /** 414 * Visits any form components inside component if it is a container, or component itself if it 415 * is itself a form component 416 * 417 * @param <R> 418 * the type of the visitor's result 419 * @param component 420 * starting point of the traversal 421 * 422 * @param visitor 423 * The visitor to call 424 * @return the visitor's result 425 */ 426 public static <R> R visitComponentsPostOrder(Component component, 427 final org.apache.wicket.util.visit.IVisitor<Component, R> visitor) 428 { 429 Args.notNull(visitor, "visitor"); 430 431 return Visits.visitPostOrder(component, visitor, new IVisitFilter() 432 { 433 @Override 434 public boolean visitObject(Object object) 435 { 436 return true; 437 } 438 439 @Override 440 public boolean visitChildren(Object object) 441 { 442 if (object instanceof IFormVisitorParticipant) 443 { 444 return ((IFormVisitorParticipant)object).processChildren(); 445 } 446 return true; 447 } 448 449 }); 450 } 451 452 private transient T convertedInput; 453 454 /** 455 * Raw Input entered by the user or NO_RAW_INPUT if nothing is filled in. 456 */ 457 private String rawInput = NO_RAW_INPUT; 458 459 /** 460 * Type that the raw input string will be converted to 461 */ 462 private String typeName; 463 464 /** 465 * @see org.apache.wicket.Component#Component(String) 466 */ 467 public FormComponent(final String id) 468 { 469 this(id, null); 470 } 471 472 /** 473 * @param id 474 * @param model 475 * @see org.apache.wicket.Component#Component(String, IModel) 476 */ 477 public FormComponent(final String id, IModel<T> model) 478 { 479 super(id, model); 480 // the form decides whether form components are versioned or not 481 // see Form.setVersioned 482 setVersioned(false); 483 } 484 485 /** 486 * Gets the string the component would use as a label when one was requested but no label model 487 * was set via {@link #getLabel()}. The value of this string is usually set in a property file; 488 * if the value is not set the default value equivalent to component id will be returned. 489 * 490 * @return localized label 491 */ 492 public final String getDefaultLabel() 493 { 494 return getDefaultLabel(getId()); 495 } 496 497 /** 498 * Gets the string the component would use as a label when one was requested but no label model 499 * was set via {@link #getLabel()}. The value of this string is usually set in a property file; 500 * if the value is not set the {@code defaultValue} will be returned. 501 * 502 * @param defaultValue 503 * 504 * @return localized label 505 */ 506 public final String getDefaultLabel(String defaultValue) 507 { 508 return getLocalizer().getString(getId(), getParent(), defaultValue); 509 } 510 511 512 /** 513 * Adds a validator to this form component 514 * 515 * @param validator 516 * validator to be added 517 * @return <code>this</code> for chaining 518 * @throws IllegalArgumentException 519 * if validator is null 520 * @see IValidator 521 */ 522 @SuppressWarnings({ "rawtypes", "unchecked" }) 523 public final FormComponent<T> add(final IValidator<? super T> validator) 524 { 525 Args.notNull(validator, "validator"); 526 527 if (validator instanceof Behavior) 528 { 529 add((Behavior)validator); 530 } 531 else 532 { 533 add((Behavior)new ValidatorAdapter(validator)); 534 } 535 return this; 536 } 537 538 /** 539 * Removes a validator from the form component. 540 * 541 * @param validator 542 * validator 543 * @throws IllegalArgumentException 544 * if validator is null or not found 545 * @see IValidator 546 * @see #add(IValidator) 547 * @return form component for chaining 548 */ 549 public final FormComponent<T> remove(final IValidator<? super T> validator) 550 { 551 Args.notNull(validator, "validator"); 552 Behavior match = null; 553 for (Behavior behavior : getBehaviors()) 554 { 555 if (behavior.equals(validator)) 556 { 557 match = behavior; 558 break; 559 } 560 else if (behavior instanceof ValidatorAdapter) 561 { 562 if (((ValidatorAdapter<?>)behavior).getValidator().equals(validator)) 563 { 564 match = behavior; 565 break; 566 } 567 } 568 } 569 570 if (match != null) 571 { 572 remove(match); 573 } 574 else 575 { 576 throw new IllegalStateException( 577 "Tried to remove validator that was not previously added. " 578 + "Make sure your validator's equals() implementation is sufficient"); 579 } 580 return this; 581 } 582 583 /** 584 * Adds a validator to this form component. 585 * 586 * @param validators 587 * The validator(s) to be added 588 * @return This 589 * @throws IllegalArgumentException 590 * if validator is null 591 * @see IValidator 592 */ 593 public final FormComponent<T> add(final IValidator<? super T>... validators) 594 { 595 Args.notNull(validators, "validators"); 596 597 for (IValidator<? super T> validator : validators) 598 { 599 add(validator); 600 } 601 602 // return this for chaining 603 return this; 604 } 605 606 /** 607 * Checks if the form component's 'required' requirement is met by first checking 608 * {@link #isRequired()} to see if it has to check for requirement. If that is true then by 609 * default it checks if the input is null or an empty String 610 * {@link Strings#isEmpty(CharSequence)} 611 * <p> 612 * Subclasses that overwrite this method should also call {@link #isRequired()} first. 613 * </p> 614 * 615 * @return true if the 'required' requirement is met, false otherwise 616 * 617 * @see Strings#isEmpty(CharSequence) 618 * @see #isInputNullable() 619 */ 620 public boolean checkRequired() 621 { 622 if (isRequired()) 623 { 624 final String input = getInput(); 625 626 // when null, check whether this is natural for that component, or 627 // whether - as is the case with text fields - this can only happen 628 // when the component was disabled 629 if (input == null && !isInputNullable() && !isEnabledInHierarchy()) 630 { 631 // this value must have come from a disabled field 632 // do not perform validation 633 return true; 634 } 635 636 // perform validation by looking whether the value is null or empty 637 return !Strings.isEmpty(input); 638 } 639 return true; 640 } 641 642 /** 643 * Clears the user input. 644 */ 645 public void clearInput() 646 { 647 rawInput = NO_RAW_INPUT; 648 } 649 650 /** 651 * Reports a validation error against this form component. 652 * 653 * The actual error is reported by creating a {@link ValidationErrorFeedback} object that holds 654 * both the validation error and the generated error message - so a custom feedback panel can 655 * have access to both. 656 * 657 * @param error 658 * validation error 659 */ 660 public void error(IValidationError error) 661 { 662 Args.notNull(error, "error"); 663 664 MessageSource source = new MessageSource(); 665 Serializable message = error.getErrorMessage(source); 666 667 if (message == null) 668 { 669 StringBuilder buffer = new StringBuilder(); 670 buffer.append("Could not locate error message for component: "); 671 buffer.append(Classes.simpleName(getClass())); 672 buffer.append("@"); 673 buffer.append(getPageRelativePath()); 674 buffer.append(" and error: "); 675 buffer.append(error.toString()); 676 buffer.append(". Tried keys: "); 677 Iterator<String> keys = source.triedKeys.iterator(); 678 while (keys.hasNext()) 679 { 680 buffer.append(keys.next()); 681 if (keys.hasNext()) 682 { 683 buffer.append(", "); 684 } 685 } 686 buffer.append('.'); 687 message = buffer.toString(); 688 logger.warn(message.toString()); 689 } 690 error(message); 691 } 692 693 /** 694 * Gets the converted input. The converted input is set earlier though the implementation of 695 * {@link #convertInput()}. 696 * 697 * {@link FormComponentPanel} often access this method when constructing their converted input 698 * value which is often the combination of converted values of the embedded FormComponents 699 * 700 * To access the model object resulted by the full form processing, use 701 * {@link #getModelObject()} instead, that is an generified version of 702 * {@link #getDefaultModelObject()} 703 * 704 * @return value of input possibly converted into an appropriate type 705 */ 706 public final T getConvertedInput() 707 { 708 return convertedInput; 709 } 710 711 /** 712 * Sets the converted input. This method is typically not called by clients, unless they 713 * override {@link #convertInput()}, in which case they should call this method to update the 714 * input for this component instance. 715 * 716 * @param convertedInput 717 * the converted input 718 */ 719 public final void setConvertedInput(T convertedInput) 720 { 721 this.convertedInput = convertedInput; 722 } 723 724 /** 725 * @return The parent form for this form component 726 */ 727 public Form<?> getForm() 728 { 729 Form<?> form = Form.findForm(this); 730 if (form == null) 731 { 732 throw new WicketRuntimeException("Could not find Form parent for " + this); 733 } 734 return form; 735 } 736 737 738 /** 739 * Gets the request parameter for this component as a string. 740 * 741 * @return The value in the request for this component 742 */ 743 public String getInput() 744 { 745 String[] input = getInputAsArray(); 746 if (input == null || input.length == 0) 747 { 748 return null; 749 } 750 else 751 { 752 return trim(input[0]); 753 } 754 } 755 756 /** 757 * Gets the request parameters for this component as strings. 758 * 759 * @return The values in the request for this component 760 */ 761 public String[] getInputAsArray() 762 { 763 List<StringValue> list = getParameterValues(getInputName()); 764 765 String[] values = null; 766 if (list != null) 767 { 768 values = new String[list.size()]; 769 for (int i = 0; i < list.size(); ++i) 770 { 771 values[i] = list.get(i).toString(); 772 } 773 } 774 775 if (!isInputNullable()) 776 { 777 if (values != null && values.length == 1 && values[0] == null) 778 { 779 // the key got passed in (otherwise values would be null), 780 // but the value was set to null. 781 // As the servlet spec isn't clear on what to do with 'empty' 782 // request values - most return an empty string, but some null - 783 // we have to workaround here and deliberately set to an empty 784 // string if the component is not nullable (text components) 785 return EMPTY_STRING_ARRAY; 786 } 787 } 788 return values; 789 } 790 791 /** 792 * Reads the value(s) of the request parameter with name <em>inputName</em> 793 * from either the query parameters for <em>GET</em> method or the request body 794 * for <em>POST</em> method. 795 * 796 * @param inputName 797 * The name of the request parameter 798 * @return The parameter's value(s) 799 */ 800 protected List<StringValue> getParameterValues(String inputName) 801 { 802 final IRequestParameters parameters = Form.getRequestParameters(this); 803 804 return parameters.getParameterValues(inputName); 805 } 806 807 /** 808 * Gets the string to be used for the <tt>name</tt> attribute of the form element. Generated 809 * using the path from the form to the component, excluding the form itself. Override it if you 810 * want even a smaller name. E.g. if you know for sure that the id is unique within a form. 811 * 812 * @return The string to use as the form element's name attribute 813 */ 814 public String getInputName() 815 { 816 String inputName = Form.getRootFormRelativeId(this); 817 Form<?> form = findParent(Form.class); 818 819 if (form != null) 820 { 821 return form.getInputNamePrefix() + inputName; 822 } 823 else 824 { 825 return inputName; 826 } 827 } 828 829 /** 830 * Use hasRawInput() to check if this component has raw input because null can mean 2 things: It 831 * doesn't have rawinput or the rawinput is really null. 832 * 833 * @return The raw form input that is stored for this formcomponent 834 */ 835 public final String getRawInput() 836 { 837 return NO_RAW_INPUT.equals(rawInput) ? null : rawInput; 838 } 839 840 /** 841 * @return the type to use when updating the model for this form component 842 */ 843 @SuppressWarnings("unchecked") 844 public final Class<T> getType() 845 { 846 return typeName == null ? null : (Class<T>)WicketObjects.resolveClass(typeName); 847 } 848 849 /** 850 * @see Form#getValidatorKeyPrefix() 851 * @return prefix used when constructing validator key messages 852 */ 853 public String getValidatorKeyPrefix() 854 { 855 Form<?> form = findParent(Form.class); 856 if (form != null) 857 { 858 return getForm().getValidatorKeyPrefix(); 859 } 860 return null; 861 } 862 863 /** 864 * Gets an unmodifiable list of validators for this FormComponent. 865 * 866 * @return List of validators 867 */ 868 @SuppressWarnings("unchecked") 869 public final List<IValidator<? super T>> getValidators() 870 { 871 final List<IValidator<? super T>> list = new ArrayList<>(); 872 873 for (Behavior behavior : getBehaviors()) 874 { 875 if (behavior instanceof IValidator) 876 { 877 list.add((IValidator<? super T>)behavior); 878 } 879 } 880 881 return Collections.unmodifiableList(list); 882 } 883 884 /** 885 * Gets current value for a form component, which can be either input data entered by the user, 886 * or the component's model object if no input was provided. 887 * 888 * @return The value 889 */ 890 public final String getValue() 891 { 892 if (NO_RAW_INPUT.equals(rawInput)) 893 { 894 return getModelValue(); 895 } 896 else 897 { 898 if (getEscapeModelStrings() && rawInput != null) 899 { 900 return Strings.escapeMarkup(rawInput).toString(); 901 } 902 return rawInput; 903 } 904 } 905 906 /** 907 * Returns whether this component has raw input. Raw input is unconverted input straight from 908 * the client. 909 * 910 * @return boolean whether this component has raw input. 911 */ 912 public final boolean hasRawInput() 913 { 914 return !NO_RAW_INPUT.equals(rawInput); 915 } 916 917 /** 918 * Used by Form to tell the FormComponent that a new user input is available 919 */ 920 public final void inputChanged() 921 { 922 if (isVisibleInHierarchy() && isEnabledInHierarchy()) 923 { 924 // Get input as String array 925 final String[] input = getInputAsArray(); 926 927 // If there is any input 928 if (input != null && input.length > 0 && input[0] != null) 929 { 930 // join the values together with ";", for example, "id1;id2;id3" 931 rawInput = StringList.valueOf(input).join(VALUE_SEPARATOR); 932 } 933 else if (isInputNullable()) 934 { 935 // no input 936 rawInput = null; 937 } 938 else 939 { 940 rawInput = NO_RAW_INPUT; 941 } 942 } 943 } 944 945 /** 946 * Indicate that validation of this form component failed. 947 */ 948 public final void invalid() 949 { 950 onInvalid(); 951 } 952 953 /** 954 * Gets whether this component's input can be null. By default, components that do not get input 955 * will have null values passed in for input. However, component TextField is an example 956 * (possibly the only one) that never gets a null passed in, even if the field is left empty 957 * UNLESS it has attribute <code>disabled="disabled"</code> set. 958 * 959 * @return True if this component's input can be null. Returns true by default. 960 */ 961 public boolean isInputNullable() 962 { 963 return true; 964 } 965 966 /** 967 * @return True if this component encodes data in a multipart form submit 968 */ 969 public boolean isMultiPart() 970 { 971 return false; 972 } 973 974 /** 975 * @return whether or not this component's value is required 976 */ 977 public boolean isRequired() 978 { 979 return getFlag(FLAG_REQUIRED); 980 } 981 982 /** 983 * Gets whether this component is 'valid'. Valid in this context means that no validation errors 984 * were reported the last time the form component was processed. This variable not only is 985 * convenient for 'business' use, but is also necessary as we don't want the form component 986 * models updated with invalid input. 987 * 988 * @return valid whether this component is 'valid' 989 */ 990 public final boolean isValid() 991 { 992 class IsValidVisitor implements IVisitor<FormComponent<?>, Boolean> 993 { 994 @Override 995 public void component(final FormComponent<?> formComponent, final IVisit<Boolean> visit) 996 { 997 if (formComponent.hasErrorMessage()) 998 { 999 visit.stop(Boolean.FALSE); 1000 } 1001 } 1002 } 1003 IsValidVisitor tmp = new IsValidVisitor(); 1004 final Object result = visitFormComponentsPostOrder(this, tmp); 1005 return (Boolean.FALSE != result); 1006 } 1007 1008 /** 1009 * @see IFormVisitorParticipant#processChildren() 1010 */ 1011 @Override 1012 public boolean processChildren() 1013 { 1014 return true; 1015 } 1016 1017 /** 1018 * This method will retrieve the request parameter, validate it, and if valid update the model. 1019 * These are the same steps as would be performed by the form. 1020 * 1021 * This is useful when a formcomponent is used outside a form. 1022 * 1023 */ 1024 public final void processInput() 1025 { 1026 inputChanged(); 1027 validate(); 1028 if (hasErrorMessage()) 1029 { 1030 invalid(); 1031 } 1032 else 1033 { 1034 valid(); 1035 updateModel(); 1036 } 1037 } 1038 1039 /** 1040 * The value will be made available to the validator property by means of ${label}. It does not 1041 * have any specific meaning to FormComponent itself. 1042 * 1043 * @param labelModel 1044 * @return this for chaining 1045 */ 1046 @Override 1047 public FormComponent<T> setLabel(IModel<String> labelModel) 1048 { 1049 super.setLabel(labelModel); 1050 return this; 1051 } 1052 1053 /** 1054 * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT USE IT! 1055 * 1056 * Sets the value for a form component. 1057 * 1058 * @param value 1059 * The value 1060 */ 1061 public void setModelValue(final String[] value) 1062 { 1063 convertedInput = convertValue(value); 1064 updateModel(); 1065 } 1066 1067 /** 1068 * Sets the required flag 1069 * 1070 * @param required 1071 * @return this for chaining 1072 */ 1073 public final FormComponent<T> setRequired(final boolean required) 1074 { 1075 if (!required && getType() != null && getType().isPrimitive()) 1076 { 1077 throw new WicketRuntimeException( 1078 "FormComponent has to be required when the type is primitive class: " + this); 1079 } 1080 if (required != isRequired()) 1081 { 1082 addStateChange(); 1083 } 1084 setFlag(FLAG_REQUIRED, required); 1085 return this; 1086 } 1087 1088 /** 1089 * Sets the type that will be used when updating the model for this component. If no type is 1090 * specified String type is assumed. 1091 * 1092 * @param type 1093 * @return this for chaining 1094 */ 1095 public FormComponent<T> setType(Class<?> type) 1096 { 1097 typeName = type == null ? null : type.getName(); 1098 if (type != null && type.isPrimitive()) 1099 { 1100 setRequired(true); 1101 } 1102 return this; 1103 } 1104 1105 /** 1106 * Updates this components model from the request, it expects that the object is already 1107 * converted through the convertInput() call that is called by the validate() method when a form 1108 * is being processed. 1109 * 1110 * By default it just does this: 1111 * 1112 * <pre> 1113 * setModelObject(getConvertedInput()); 1114 * </pre> 1115 * 1116 * <strong>DO NOT CALL THIS METHOD DIRECTLY UNLESS YOU ARE SURE WHAT YOU ARE DOING. USUALLY UPDATING 1117 * YOUR MODEL IS HANDLED BY THE FORM, NOT DIRECTLY BY YOU.</strong> 1118 */ 1119 @Override 1120 public void updateModel() 1121 { 1122 setModelObject(getConvertedInput()); 1123 } 1124 1125 1126 /** 1127 * Called to indicate that the user input is valid. 1128 */ 1129 public final void valid() 1130 { 1131 clearInput(); 1132 1133 onValid(); 1134 } 1135 1136 /** 1137 * Performs full validation of the form component, which consists of calling validateRequired(), 1138 * convertInput(), and validateValidators(). This method should only be used if the form 1139 * component needs to be fully validated outside the form process. 1140 */ 1141 public void validate() 1142 { 1143 // clear any previous feedback messages 1144 1145 if (hasFeedbackMessage()) 1146 { 1147 getFeedbackMessages().clear(); 1148 } 1149 1150 // validate 1151 1152 validateRequired(); 1153 if (isValid()) 1154 { 1155 convertInput(); 1156 if (isValid()) 1157 { 1158 if (isRequired() && getConvertedInput() == null && isInputNullable()) 1159 { 1160 reportRequiredError(); 1161 } 1162 else 1163 { 1164 validateValidators(); 1165 } 1166 } 1167 } 1168 } 1169 1170 /** 1171 * Converts and validates the conversion of the raw input string into the object specified by 1172 * {@link FormComponent#getType()} and records any thrown {@link ConversionException}s. 1173 * Converted value is available through {@link FormComponent#getConvertedInput()}. 1174 * 1175 * <p> 1176 * Usually the user should do custom conversions by specifying an {@link IConverter} by 1177 * registering it with the application by overriding {@link Application#getConverterLocator()}, 1178 * or at the component level by overriding {@link #getConverter(Class)} . 1179 * </p> 1180 * 1181 * <strong>DO NOT CALL THIS METHOD DIRECTLY UNLESS YOU ARE SURE WHAT YOU ARE DOING. USUALLY UPDATING 1182 * YOUR MODEL IS HANDLED BY THE FORM, NOT DIRECTLY BY YOU.</strong> 1183 * 1184 * @see IConverterLocator 1185 * @see Application#newConverterLocator() 1186 * @see IConverter#convertToObject(String, Locale) 1187 * @see #newValidationError(ConversionException) 1188 */ 1189 public void convertInput() 1190 { 1191 if (typeName == null) 1192 { 1193 try 1194 { 1195 convertedInput = convertValue(getInputAsArray()); 1196 } 1197 catch (ConversionException e) 1198 { 1199 error(newValidationError(e)); 1200 } 1201 } 1202 else 1203 { 1204 final IConverter<T> converter = getConverter(getType()); 1205 1206 try 1207 { 1208 convertedInput = converter.convertToObject(getInput(), getLocale()); 1209 } 1210 catch (ConversionException e) 1211 { 1212 error(newValidationError(e)); 1213 } 1214 } 1215 } 1216 1217 /** 1218 * This method is called, when the validation triggered by {@link FormComponent#convertInput()} 1219 * failed with a {@link ConversionException}, to construct a {@link ValidationError} based on 1220 * the exception. 1221 * <p> 1222 * Override this method to modify the ValidationError object, e.g. add a custom variable for 1223 * message substitution: 1224 * <p> 1225 * 1226 * <pre> 1227 * new FormComponent<T>(id) 1228 * { 1229 * protected ValidationError newValidationError(ConversionException cause) 1230 * { 1231 * return super.newValidationError(cause).setVariable("foo", foovalue); 1232 * } 1233 * }; 1234 * </pre> 1235 * 1236 * @param cause 1237 * the original cause 1238 * @return {@link ValidationError} 1239 */ 1240 protected ValidationError newValidationError(ConversionException cause) 1241 { 1242 ValidationError error = new ValidationError(cause.getMessage()); 1243 1244 if (cause.getResourceKey() != null) 1245 { 1246 error.addKey(cause.getResourceKey()); 1247 } 1248 1249 if (typeName == null) 1250 { 1251 if (cause.getTargetType() != null) 1252 { 1253 error.addKey("ConversionError." + Classes.simpleName(cause.getTargetType())); 1254 } 1255 error.addKey("ConversionError"); 1256 } 1257 else 1258 { 1259 String simpleName = Classes.simpleName(getType()); 1260 error.addKey("IConverter." + simpleName); 1261 error.addKey("IConverter"); 1262 error.setVariable("type", simpleName); 1263 } 1264 1265 final Locale locale = cause.getLocale(); 1266 if (locale != null) 1267 { 1268 error.setVariable("locale", locale); 1269 } 1270 1271 error.setVariable("exception", cause); 1272 1273 Format format = cause.getFormat(); 1274 if (format instanceof SimpleDateFormat) 1275 { 1276 error.setVariable("format", ((SimpleDateFormat)format).toLocalizedPattern()); 1277 } 1278 1279 Map<String, Object> variables = cause.getVariables(); 1280 if (variables != null) 1281 { 1282 error.getVariables().putAll(variables); 1283 } 1284 1285 return error; 1286 } 1287 1288 /** 1289 * Subclasses should overwrite this if the conversion is not done through the type field and the 1290 * {@link IConverter}. <strong>WARNING: this method may be removed in future versions.</strong> 1291 * 1292 * If conversion fails then a ConversionException should be thrown 1293 * 1294 * @param value 1295 * The value can be the getInput() or through a cookie 1296 * 1297 * @return The converted value. default returns just the given value 1298 * @throws ConversionException 1299 * If input can't be converted 1300 */ 1301 @SuppressWarnings("unchecked") 1302 protected T convertValue(String[] value) throws ConversionException 1303 { 1304 return (T)(value != null && value.length > 0 && value[0] != null ? trim(value[0]) : null); 1305 } 1306 1307 /** 1308 * @return Value to return when model value is needed 1309 */ 1310 protected String getModelValue() 1311 { 1312 return getDefaultModelObjectAsString(); 1313 } 1314 1315 /** 1316 * Gets the request parameter for this component as an int. 1317 * 1318 * @return The value in the request for this component 1319 */ 1320 protected final int inputAsInt() 1321 { 1322 final String string = getInput(); 1323 try 1324 { 1325 return Integer.parseInt(string); 1326 } 1327 catch (NumberFormatException e) 1328 { 1329 throw new IllegalArgumentException( 1330 exceptionMessage("Internal error. Request string '" + string + 1331 "' not a valid integer")); 1332 } 1333 } 1334 1335 /** 1336 * Gets the request parameter for this component as an int, using the given default in case no 1337 * corresponding request parameter was found. 1338 * 1339 * @param defaultValue 1340 * Default value to return if request does not have an integer for this component 1341 * @return The value in the request for this component 1342 */ 1343 protected final int inputAsInt(final int defaultValue) 1344 { 1345 final String string = getInput(); 1346 if (string != null) 1347 { 1348 try 1349 { 1350 return Integer.parseInt(string); 1351 } 1352 catch (NumberFormatException e) 1353 { 1354 throw new IllegalArgumentException(exceptionMessage("Request string '" + string + 1355 "' is not a valid integer")); 1356 } 1357 } 1358 else 1359 { 1360 return defaultValue; 1361 } 1362 } 1363 1364 /** 1365 * Gets the request parameters for this component as ints. 1366 * 1367 * @return The values in the request for this component 1368 */ 1369 protected final int[] inputAsIntArray() 1370 { 1371 final String[] strings = getInputAsArray(); 1372 if (strings != null) 1373 { 1374 final int[] ints = new int[strings.length]; 1375 for (int i = 0; i < strings.length; i++) 1376 { 1377 ints[i] = Integer.parseInt(strings[i]); 1378 } 1379 return ints; 1380 } 1381 return null; 1382 } 1383 1384 /** 1385 * @see org.apache.wicket.Component#internalOnModelChanged() 1386 */ 1387 @Override 1388 protected void internalOnModelChanged() 1389 { 1390 // If the model for this form component changed, we should make it 1391 // valid again because there can't be any invalid input for it anymore. 1392 valid(); 1393 } 1394 1395 /** 1396 * Processes the component tag. 1397 * 1398 * @param tag 1399 * Tag to modify 1400 * @see org.apache.wicket.Component#onComponentTag(ComponentTag) 1401 */ 1402 @Override 1403 protected void onComponentTag(final ComponentTag tag) 1404 { 1405 tag.put("name", getInputName()); 1406 1407 if (!isEnabledInHierarchy()) 1408 { 1409 onDisabled(tag); 1410 } 1411 1412 super.onComponentTag(tag); 1413 } 1414 1415 /** 1416 * Sets the temporary converted input value to null. 1417 * 1418 * @see org.apache.wicket.Component#onDetach() 1419 */ 1420 @Override 1421 protected void onDetach() 1422 { 1423 super.onDetach(); 1424 convertedInput = null; 1425 } 1426 1427 /** 1428 * Called by {@link #onComponentTag(ComponentTag)} when the component is disabled. By default, 1429 * this method will add a disabled="disabled" attribute to the tag. Components may override this 1430 * method to tweak the tag as they think is fit. 1431 * 1432 * @param tag 1433 * the tag that is being rendered 1434 */ 1435 protected void onDisabled(final ComponentTag tag) 1436 { 1437 tag.put("disabled", "disabled"); 1438 } 1439 1440 /** 1441 * Handle invalidation 1442 */ 1443 protected void onInvalid() 1444 { 1445 } 1446 1447 /** 1448 * Handle validation 1449 */ 1450 protected void onValid() 1451 { 1452 } 1453 1454 /** 1455 * Determines whether or not this component should trim its input prior to processing it. The 1456 * default value is <code>true</code> 1457 * 1458 * @return True if the input should be trimmed. 1459 */ 1460 protected boolean shouldTrimInput() 1461 { 1462 return true; 1463 } 1464 1465 /** 1466 * Trims the input according to {@link #shouldTrimInput()} 1467 * 1468 * @param string 1469 * @return trimmed input if {@link #shouldTrimInput()} returns true, unchanged input otherwise 1470 */ 1471 protected String trim(String string) 1472 { 1473 String trimmed = string; 1474 if (trimmed != null && shouldTrimInput()) 1475 { 1476 trimmed = trimmed.trim(); 1477 } 1478 return trimmed; 1479 } 1480 1481 /** 1482 * Checks if the raw input value is not null if this component is required. 1483 */ 1484 protected final void validateRequired() 1485 { 1486 if (!checkRequired()) 1487 { 1488 reportRequiredError(); 1489 } 1490 } 1491 1492 /** 1493 * Reports required error against this component 1494 */ 1495 protected void reportRequiredError() 1496 { 1497 error(new ValidationError().addKey("Required")); 1498 } 1499 1500 /** 1501 * Validates this component using the component's validators. 1502 */ 1503 @SuppressWarnings("unchecked") 1504 protected final void validateValidators() 1505 { 1506 final IValidatable<T> validatable = newValidatable(); 1507 1508 boolean isNull = getConvertedInput() == null; 1509 1510 IValidator<T> validator; 1511 1512 for (Behavior behavior : getBehaviors()) 1513 { 1514 if (isBehaviorAccepted(behavior) == false) 1515 { 1516 continue; 1517 } 1518 1519 validator = null; 1520 if (behavior instanceof ValidatorAdapter) 1521 { 1522 validator = ((ValidatorAdapter<T>)behavior).getValidator(); 1523 } 1524 else if (behavior instanceof IValidator) 1525 { 1526 validator = (IValidator<T>)behavior; 1527 } 1528 if (validator != null) 1529 { 1530 if (isNull == false || validator instanceof INullAcceptingValidator<?>) 1531 { 1532 try 1533 { 1534 validator.validate(validatable); 1535 } 1536 catch (Exception e) 1537 { 1538 throw new WicketRuntimeException("Exception '" + e.getMessage() + 1539 "' occurred during validation " + validator.getClass().getName() + 1540 " on component " + getPath(), e); 1541 } 1542 } 1543 if (!isValid()) 1544 { 1545 break; 1546 } 1547 } 1548 } 1549 } 1550 1551 /** 1552 * Creates an IValidatable that can be used to validate this form component. This validatable 1553 * incorporates error key lookups that correspond to this form component. 1554 * 1555 * This method is useful when validation needs to happen outside the regular validation workflow 1556 * but error messages should still be properly reported against the form component. 1557 * 1558 * @return IValidatable<T> for this form component 1559 */ 1560 public final IValidatable<T> newValidatable() 1561 { 1562 return new ValidatableAdapter(); 1563 } 1564 1565 /** 1566 * Updates auto label css classes such as error/required during ajax updates when the labels may 1567 * not be directly repainted in the response. 1568 * 1569 * @param target The {@link IPartialPageRequestHandler} 1570 * @param checkAuto if true then we check is the related auto-label is marked as auto before updating it. 1571 */ 1572 public final void updateAutoLabels(IPartialPageRequestHandler target, boolean checkAuto) 1573 { 1574 AutoLabelMarker marker = getMetaData(AutoLabelResolver.MARKER_KEY); 1575 1576 if (marker == null) 1577 { 1578 // this component does not have an auto label 1579 return; 1580 } 1581 1582 if (checkAuto) 1583 { 1584 if (!marker.isAuto()) 1585 { 1586 return; 1587 } 1588 marker.updateFrom(this, target); 1589 return; 1590 } 1591 1592 marker.updateFrom(this, target); 1593 } 1594 1595 1596 /** 1597 * @deprecated method in favor of the one receiving {@link IPartialPageRequestHandler} 1598 */ 1599 @Deprecated(since = "9.17.0, 10.0.0", forRemoval = true) 1600 public final void updateAutoLabels(AjaxRequestTarget target) 1601 { 1602 updateAutoLabels(target, false); 1603 } 1604 1605 /** 1606 * Update the model of a {@link FormComponent} containing a {@link Collection}. 1607 * 1608 * If the model object does not yet exists, a new suitable collection is filled with the converted 1609 * input and used as the new model object. Otherwise the existing collection is modified 1610 * in-place, then {@link IModel#setObject(Object)} is called with the same instance: it allows 1611 * the Model to be notified of changes even when {@link Model#getObject()} returns a different 1612 * {@link Collection} at every invocation. 1613 * 1614 * @param <S> 1615 * collection type 1616 * @param formComponent 1617 * the form component to update 1618 * @see FormComponent#updateModel() 1619 * @throws WicketRuntimeException 1620 * if the existing model object collection is unmodifiable and no setter exists 1621 */ 1622 public static <S> void updateCollectionModel(FormComponent<Collection<S>> formComponent) 1623 { 1624 Collection<S> convertedInput = formComponent.getConvertedInput(); 1625 if (convertedInput == null) { 1626 convertedInput = Collections.emptyList(); 1627 } 1628 1629 Collection<S> collection = formComponent.getModelObject(); 1630 if (collection == null) 1631 { 1632 Class<?> hint = null; 1633 if (formComponent.getModel() instanceof IObjectClassAwareModel) { 1634 hint = ((IObjectClassAwareModel)formComponent.getModel()).getObjectClass(); 1635 } 1636 if (hint == null) { 1637 hint = List.class; 1638 } 1639 collection = newCollection(hint, convertedInput); 1640 formComponent.setModelObject(collection); 1641 } 1642 else 1643 { 1644 boolean modified = false; 1645 1646 formComponent.modelChanging(); 1647 1648 try 1649 { 1650 collection.clear(); 1651 collection.addAll(convertedInput); 1652 modified = true; 1653 } 1654 catch (UnsupportedOperationException unmodifiable) 1655 { 1656 if (logger.isDebugEnabled()) 1657 { 1658 logger.debug("An error occurred while trying to modify the collection attached to " 1659 + formComponent, unmodifiable); 1660 } 1661 collection = newCollection(collection.getClass(), convertedInput); 1662 } 1663 1664 try 1665 { 1666 formComponent.getModel().setObject(collection); 1667 } 1668 catch (Exception noSetter) 1669 { 1670 if (!modified) 1671 { 1672 throw new WicketRuntimeException("An error occurred while trying to set the collection attached to " 1673 + formComponent, noSetter); 1674 } 1675 else if (logger.isDebugEnabled()) 1676 { 1677 logger.debug("An error occurred while trying to set the collection attached to " 1678 + formComponent, noSetter); 1679 } 1680 } 1681 1682 formComponent.modelChanged(); 1683 } 1684 } 1685 1686 /** 1687 * Creates a new collection. 1688 * 1689 * @param hint type deciding the type of the returned collection 1690 * @param elements elements for the new collection 1691 * @return collection 1692 * @throws IllegalArgumentException if type is not supported 1693 */ 1694 private static <S> Collection<S> newCollection(Class<?> hint, Collection<S> elements) 1695 { 1696 if (Set.class.isAssignableFrom(hint)) { 1697 return new LinkedHashSet<>(elements); 1698 } else { 1699 return new ArrayList<>(elements); 1700 } 1701 } 1702}