OmniFaces 3.8 has been released!
In this version, the <o:validateBean>
has been improved to support validating nested properties annotated with @Valid
. Previously it only supported validating bean's own properties. Further a handful of new utilities have been added: Beans#isProxy(Object)
, Beans#unwrapIfNecessary(Object)
, Components#getExpectedType(ValueExpression)
, Components#getExpectedValueType(UIComponent)
, Messages#asConverterException()
, Messages#asValidatorException()
and #{of:encodeBase64(String)}
.
You can find the complete list of additions, changes and fixes at What's new in OmniFaces 3.8.1? list in showcase.
Installation
Non-Maven users: download OmniFaces 3.8.1 JAR and drop it in /WEB-INF/lib
the usual way, replacing the older version if any.
Maven users: use <version>3.8.1</version>
.
<dependency>
<groupId>org.omnifaces</groupId>
<artifactId>omnifaces</artifactId>
<version>3.8.1</version>
</dependency>
If you're already on Jakarta EE 9 (e.g. GlassFish 6), then use 4.0-M3
instead. It's the Jakartified version of 3.8.1.
Validating nested properties
The <o:validateBean>
now finally supports validating nested properties which are annotated with the @javax.validation.Valid
cascade. Previously this was not supported because nested properties were not automatically copied into the shadow bean, and the violation messages were not associatable with the input components. Both issues have been resolved with help of Andre Wachsmuth.
Here's an example with an imagined Setting
model:
@Named
@ViewScoped
public class Bean implements Serializable {
@Valid
private List<Setting> settings;
@Inject
private SettingService settingService;
@PostConstruct
public void init() {
settings = settingService.list();
}
public void save() {
settingService.save(settings);
}
public List<Setting> getSettings() {
return settings;
}
}
@ValidSetting
public class Setting {
public enum Type {
STRING, BOOLEAN, NUMBER, DECIMAL;
}
@NotNull
private String name;
@NotNull
private Type type;
@NotNull
private String value;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Boolean getAsBoolean() {
return type == Type.BOOLEAN ? Boolean.parseBoolean(getValue()) : null;
}
public Long getAsNumber() {
return type == Type.NUMBER ? Long.valueOf(getValue()) : null;
}
public BigDecimal getAsDecimal() {
return type == Type.DECIMAL ? new BigDecimal(getValue()) : null;
}
}
@Constraint(validatedBy = SettingValidator.class)
@Documented
@Target({ TYPE, METHOD, FIELD })
@Retention(RUNTIME)
public @interface ValidSetting {
String message() default "Invalid setting value";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SettingValidator implements ConstraintValidator<ValidSetting, Setting>{
@Override
public boolean isValid(Setting setting, ConstraintValidatorContext context) {
if (setting == null) {
return true; // Let @NotNull handle it.
}
else {
return isValid(setting);
}
}
public static boolean isValid(Setting setting) {
switch (setting.getType()) {
case STRING:
return true;
case BOOLEAN:
return setting.getValue().matches("(true|false)");
case NUMBER:
try {
setting.getAsNumber();
return true;
}
catch (NumberFormatException e) {
return false;
}
case DECIMAL:
try {
setting.getAsDecimal();
return true;
}
catch (NumberFormatException e) {
return false;
}
default:
return false;
}
}
}
<h:form>
<h:dataTable value="#{bean.settings}" var="setting">
<h:column>
<f:facet name="header">Name</f:facet>
#{setting.name}
</h:column>
<h:column>
<f:facet name="header">Type</f:facet>
#{setting.type}
<h:inputHidden id="type" value="#{setting.type}" />
</h:column>
<h:column>
<f:facet name="header">Value</f:facet>
<h:inputText id="value" value="#{setting.value}" />
<h:message id="value_m" for="value" />
</h:column>
</h:dataTable>
<o:validateBean value="#{bean}" showMessageFor="@violating" />
<h:commandButton value="Save" action="#{bean.save}">
<f:ajax execute="@form" render="@messages" />
</h:commandButton>
</h:form>
In this use case, the <o:validateBean>
will cascade into @Valid
and execute SettingValidator
for each setting. The showMessageFor="@violating"
will associate each message with the violating input, which shall be the <h:inputText id="value">
.
Note that the render="@messages"
is also specific to OmniFaces, it's handled by the MessagesKeywordResolver
, just in case you didn't knew ;)
Generic converter for @Param
The @Param
has also been improved to pass the expected type into the UIComponent
passed around any associated validators/converters. In case of so-called "generic entity converters" this now allows you to inspect the expected type as well. This has always worked fine for <f|o:viewParam>
, but it did previously not work with @Param
.
For example, this <f|o:viewParam>
approach:
<f:viewParam name="id" value="#{bean.product}" />
private Product product; // +getter +setter
public class Product extends BaseEntity {
// ...
}
@FacesConverter(forClass = BaseEntity.class)
public class BaseEntityConverter implements Converter<BaseEntity> {
@Inject
private BaseEntityService service;
@Override
public String getAsString(FacesContext context, UIComponent component, BaseEntity modelValue) {
return modelValue == null ? "" : modelValue.getId().toString();
}
@Override
public BaseEntity getAsObject(FacesContext context, UIComponent component, String submittedValue) {
if (submittedValue == null) {
return null;
}
try {
Class<? extends BaseEntity> type = Components.getExpectedValueType(component);
Long id = Long.valueOf(submittedValue);
return service.find(type, id);
}
catch (Exception e) {
throw Messages.asConverterException("Cannot convert because it threw " + e);
}
}
}
Will now also work fine when using @Param
instead of <f|o:viewParam>
.
@Param(name = "id")
private Product product; // no getter/setter
How about OmniFaces 2.x and 1.1x?
The 2.x got the same bugfixes as 3.8.1 and has been released as 2.7.8. This version is for JSF 2.2 users with CDI. In case you've already migrated to JSF 2.3, use 3.x instead.
The 1.1x is basically already since 2.5 in maintenance mode. I.e. only critical bugfix versions will be released. It's currently still at 1.14.1 (May 2017), featuring the same features as OmniFaces 2.4, but without any JSF 2.2 and CDI things and therefore compatible with CDI-less JSF 2.0/2.1.
No comments:
Post a Comment