如何在 jsf 复合组件中对集合 属性 进行 bean 验证,约束不会触发
How to bean-validate a collection property in a jsf composite component, constraints do not fire
如何正确定义 jsf 复合组件,以便在其包含集合的情况下正确地对其值进行 bean 验证?
我们有一个引用详细信息集合的实体。两者都带有 bean-validation-constraints 注释。请注意 details
-属性.
处的注释
public class Entity implements Serializable {
@NotEmpty
private String name;
@NotEmpty
@UniqueCategory(message="category must be unique")
private List<@Valid Detail> details;
/* getters/setters */
}
public class Detail implements Serializable {
@Pattern(regexp="^[A-Z]+$")
private String text;
@NotNull
private Category category;
/* getters/setters */
}
public class Category implements Serializable {
private final int id;
private final String description;
Category(int id, String description) {
this.id = id;
this.description = description;
}
/* getters/setters */
}
public class MyConstraints {
@Target({ ElementType.TYPE, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueCategoryValidator.class)
@Documented
public static @interface UniqueCategory {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public static class UniqueCategoryValidator implements ConstraintValidator<UniqueCategory, Collection<Detail>> {
@Override
public boolean isValid(Collection<Detail> collection, ConstraintValidatorContext context) {
if ( collection==null || collection.isEmpty() ) {
return true;
}
Set<Category> set = new HashSet<>();
collection.forEach( d-> set.add( d.getCategory() ));
return set.size() == collection.size();
}
public void initialize(UniqueCategory constraintAnnotation) {
// intentionally empty
}
}
private MyConstraints() {
// only static stuff
}
}
实体可以用jsf形式编辑,其中所有涉及细节的任务都封装在一个复合组件中,例如
<h:form id="entityForm">
<h:panelGrid columns="3">
<p:outputLabel for="@next" value="name"/>
<p:inputText id="name" value="#{entityUiController.entity.name}"/>
<p:message for="name"/>
<p:outputLabel for="@next" value="details"/>
<my:detailsComponent id="details" details="#{entityUiController.entity.details}"
addAction="#{entityUiController.addAction}"/>
<p:message for="details"/>
<f:facet name="footer">
<p:commandButton id="saveBtn" value="save"
action="#{entityUiController.saveAction}"
update="@form"/>
</f:facet>
</h:panelGrid>
</h:form>
其中 my:detailsComponent
定义为
<ui:component xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:p="http://primefaces.org/ui"
>
<cc:interface>
<cc:attribute name="details" required="true" type="java.lang.Iterable"/>
<cc:attribute name="addAction" required="true" method-signature="void action()"/>
</cc:interface>
<cc:implementation>
<p:outputPanel id="detailsPanel">
<ui:repeat id="detailsContainer" var="detail" value="#{cc.attrs.details}">
<p:inputText id="text" value="#{detail.text}" />
<p:message for="text"/>
<p:selectOneMenu id="category" value="#{detail.category}"
converter="#{entityUiController.categoriesConverter}"
placeholder="please select" >
<f:selectItem noSelectionOption="true" />
<f:selectItems value="#{entityUiController.categoryItems}"/>
</p:selectOneMenu>
<p:message for="category"/>
</ui:repeat>
</p:outputPanel>
<p:commandButton id="addDetailBtn" value="add" action="#{cc.attrs.addAction}"
update="detailsPanel" partialSubmit="true" process="@this detailsPanel"/>
</cc:implementation>
</ui:component>
并且 EntityUiController 是
@Named
@ViewScoped
public class EntityUiController implements Serializable {
private static final Logger LOG = Logger.getLogger( EntityUiController.class.getName() );
@Inject
private CategoriesBoundary categoriesBoundary;
@Valid
private Entity entity;
@PostConstruct
public void init() {
this.entity = new Entity();
}
public Entity getEntity() {
return entity;
}
public void saveAction() {
LOG.log(Level.INFO, "saved entity: {0}", this.entity );
}
public void addAction() {
this.entity.getDetails().add( new Detail() );
}
public List<SelectItem> getCategoryItems() {
return categoriesBoundary.getCategories().stream()
.map( cat -> new SelectItem( cat, cat.getDescription() ) )
.collect( Collectors.toList() );
}
public Converter<Category> getCategoriesConverter() {
return new Converter<Category>() {
@Override
public String getAsString(FacesContext context, UIComponent component, Category value) {
return value==null ? null : Integer.toString( value.getId() );
}
@Override
public Category getAsObject(FacesContext context, UIComponent component, String value) {
if ( value==null || value.isEmpty() ) {
return null;
}
try {
return categoriesBoundary.findById( Integer.valueOf(value).intValue() );
} catch (NumberFormatException e) {
throw new ConverterException(e);
}
}
};
}
}
当我们现在按下上面 h:form
中的保存按钮时,名称输入文本已正确验证,但 @NotEmpty
- 和 @UniqueCategory
- 对详细信息的约束-属性 被忽略。
我错过了什么?
我们在 java-ee-7,payara 4。
深入研究之后,我们最终找到了一个使用支持组件的解决方案 ValidateListComponent
。它的灵感来自 UIValidateWholeBean
和 WholeBeanValidator
。
该组件从 UIInput
扩展并覆盖验证方法以对上述 details
集合的克隆进行操作,该集合填充了子 UIInput
的已验证值。似乎暂时有效。
<ui:component ...>
<cc:interface componentType="validatedListComponent">
<cc:attribute name="addAction" required="true" method-signature="void action()"/>
</cc:interface>
<cc:implementation>
... see above ...
</cc:implementation>
</ui:component>
支持组件定义为
@FacesComponent(value = "validatedListComponent")
@SuppressWarnings("unchecked")
public class ValidatedListComponent extends UIInput implements NamingContainer {
@Override
public String getFamily() {
return "javax.faces.NamingContainer";
}
/**
* Override {@link UIInput#processValidators(FacesContext)} to switch the order of
* validation. First validate this components children, then validate this itself.
*/
@Override
public void processValidators(FacesContext context) {
// Skip processing if our rendered flag is false
if (!isRendered()) {
return;
}
pushComponentToEL(context, this);
for (Iterator<UIComponent> i = getFacetsAndChildren(); i.hasNext(); ) {
i.next().processValidators(context);
}
if (!isImmediate()) {
Application application = context.getApplication();
application.publishEvent(context, PreValidateEvent.class, this);
executeValidate(context);
application.publishEvent(context, PostValidateEvent.class, this);
}
popComponentFromEL(context);
}
/**
* Override {@link UIInput#validate(FacesContext)} to validate a cloned collection
* instead of the submitted value.
*
* Inspired by {@link UIValidateWholeBean} and {@link WholeBeanValidator}.
*/
@Override
public void validate(FacesContext context) {
AreDetailsValidCallback callback = new AreDetailsValidCallback();
visitTree( VisitContext.createVisitContext(context)
, callback
);
if ( callback.isDetailsValid() ) {
Collection<?> clonedValue = cloneCollectionAndSetDetailValues( context );
validateValue(context, clonedValue);
}
}
/**
* private method copied from {@link UIInput#executeValidate(FacesContext)}.
* @param context
*/
private void executeValidate(FacesContext context) {
try {
validate(context);
} catch (RuntimeException e) {
context.renderResponse();
throw e;
}
if (!isValid()) {
context.validationFailed();
context.renderResponse();
}
}
private Collection<Object> cloneCollectionAndSetDetailValues(FacesContext context) {
ValueExpression collectionVE = getValueExpression("value");
Collection<?> baseCollection = (Collection<?>) collectionVE.getValue(context.getELContext());
if ( baseCollection==null ) {
return null;
}
// Visit all the components children to find their already validated values.
FindDetailValuesCallback callback = new FindDetailValuesCallback(context);
this.visitTree( VisitContext.createVisitContext(context)
, callback
);
// Clone this components value and put in all cloned details with validated values set.
try {
Collection<Object> clonedCollection = baseCollection.getClass().newInstance();
for( Entry<Object,Map<String,Object>> entry : callback.getDetailSubmittedData().entrySet() ) {
Object clonedDetail = cloneDetailAndSetValues( entry.getKey(), entry.getValue() );
clonedCollection.add( clonedDetail );
}
return clonedCollection;
} catch ( Exception e ) {
throw new ConverterException(e);
}
}
private <T> T cloneDetailAndSetValues(T detail, Map<String, Object> propertyMap) throws Exception {
T clonedDetail = clone(detail);
// check the properties we have in the detail
Map<String, PropertyDescriptor> availableProperties = new HashMap<>();
for (PropertyDescriptor propertyDescriptor : getBeanInfo(detail.getClass()).getPropertyDescriptors()) {
availableProperties.put(propertyDescriptor.getName(), propertyDescriptor);
}
// put their value (or local value) into our clone
for (Map.Entry<String, Object> propertyToSet : propertyMap.entrySet()) {
availableProperties.get(propertyToSet.getKey()).getWriteMethod().invoke(clonedDetail,
propertyToSet.getValue());
}
return clonedDetail;
}
private static <T> T clone(T object) throws Exception {
// clone an object using serialization.
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
out.writeObject(object);
byte[] bytes = byteArrayOutputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(byteArrayInputStream);
return (T) in.readObject();
}
private class FindDetailValuesCallback implements VisitCallback {
private final FacesContext context;
private final Map<Object, Map<String, Object>> detailSubmittedData = new HashMap<>();
public FindDetailValuesCallback(final FacesContext context) {
this.context = context;
}
final Map<Object, Map<String, Object>> getDetailSubmittedData() {
return detailSubmittedData;
}
@Override
public VisitResult visit(VisitContext visitContext, UIComponent component) {
if ( isVisitorTarget(component) ) {
ValueExpression ve = component.getValueExpression("value");
Object value = ((EditableValueHolder)component).getValue();
if (ve != null) {
ValueReference vr = ve.getValueReference(context.getELContext());
String prop = (String)vr.getProperty();
Object base = vr.getBase();
Map<String, Object> propertyMap
= Optional.ofNullable( detailSubmittedData.get(base) )
.orElseGet( HashMap::new );
propertyMap.put(prop, value );
detailSubmittedData.putIfAbsent( base, propertyMap);
}
}
return ACCEPT;
}
}
private class AreDetailsValidCallback implements VisitCallback {
private boolean detailsValid;
public AreDetailsValidCallback() {
this.detailsValid = true;
}
public boolean isDetailsValid() {
return detailsValid;
}
@Override
public VisitResult visit(VisitContext visitContext, UIComponent component) {
if ( isVisitorTarget(component) ) {
if ( !((EditableValueHolder)component).isValid() ) {
this.detailsValid = false;
}
}
return ACCEPT;
}
}
private boolean isVisitorTarget( UIComponent component ) {
return component instanceof EditableValueHolder && component.isRendered()
&& component!=ValidatedListComponent.this;
}
}
更新:有时在 FindDetailValuesCallback#visit
中获取 ValueReference
是个问题。 Michele here 给出的答案解决了这个问题。
如何正确定义 jsf 复合组件,以便在其包含集合的情况下正确地对其值进行 bean 验证?
我们有一个引用详细信息集合的实体。两者都带有 bean-validation-constraints 注释。请注意 details
-属性.
public class Entity implements Serializable {
@NotEmpty
private String name;
@NotEmpty
@UniqueCategory(message="category must be unique")
private List<@Valid Detail> details;
/* getters/setters */
}
public class Detail implements Serializable {
@Pattern(regexp="^[A-Z]+$")
private String text;
@NotNull
private Category category;
/* getters/setters */
}
public class Category implements Serializable {
private final int id;
private final String description;
Category(int id, String description) {
this.id = id;
this.description = description;
}
/* getters/setters */
}
public class MyConstraints {
@Target({ ElementType.TYPE, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueCategoryValidator.class)
@Documented
public static @interface UniqueCategory {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public static class UniqueCategoryValidator implements ConstraintValidator<UniqueCategory, Collection<Detail>> {
@Override
public boolean isValid(Collection<Detail> collection, ConstraintValidatorContext context) {
if ( collection==null || collection.isEmpty() ) {
return true;
}
Set<Category> set = new HashSet<>();
collection.forEach( d-> set.add( d.getCategory() ));
return set.size() == collection.size();
}
public void initialize(UniqueCategory constraintAnnotation) {
// intentionally empty
}
}
private MyConstraints() {
// only static stuff
}
}
实体可以用jsf形式编辑,其中所有涉及细节的任务都封装在一个复合组件中,例如
<h:form id="entityForm">
<h:panelGrid columns="3">
<p:outputLabel for="@next" value="name"/>
<p:inputText id="name" value="#{entityUiController.entity.name}"/>
<p:message for="name"/>
<p:outputLabel for="@next" value="details"/>
<my:detailsComponent id="details" details="#{entityUiController.entity.details}"
addAction="#{entityUiController.addAction}"/>
<p:message for="details"/>
<f:facet name="footer">
<p:commandButton id="saveBtn" value="save"
action="#{entityUiController.saveAction}"
update="@form"/>
</f:facet>
</h:panelGrid>
</h:form>
其中 my:detailsComponent
定义为
<ui:component xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:p="http://primefaces.org/ui"
>
<cc:interface>
<cc:attribute name="details" required="true" type="java.lang.Iterable"/>
<cc:attribute name="addAction" required="true" method-signature="void action()"/>
</cc:interface>
<cc:implementation>
<p:outputPanel id="detailsPanel">
<ui:repeat id="detailsContainer" var="detail" value="#{cc.attrs.details}">
<p:inputText id="text" value="#{detail.text}" />
<p:message for="text"/>
<p:selectOneMenu id="category" value="#{detail.category}"
converter="#{entityUiController.categoriesConverter}"
placeholder="please select" >
<f:selectItem noSelectionOption="true" />
<f:selectItems value="#{entityUiController.categoryItems}"/>
</p:selectOneMenu>
<p:message for="category"/>
</ui:repeat>
</p:outputPanel>
<p:commandButton id="addDetailBtn" value="add" action="#{cc.attrs.addAction}"
update="detailsPanel" partialSubmit="true" process="@this detailsPanel"/>
</cc:implementation>
</ui:component>
并且 EntityUiController 是
@Named
@ViewScoped
public class EntityUiController implements Serializable {
private static final Logger LOG = Logger.getLogger( EntityUiController.class.getName() );
@Inject
private CategoriesBoundary categoriesBoundary;
@Valid
private Entity entity;
@PostConstruct
public void init() {
this.entity = new Entity();
}
public Entity getEntity() {
return entity;
}
public void saveAction() {
LOG.log(Level.INFO, "saved entity: {0}", this.entity );
}
public void addAction() {
this.entity.getDetails().add( new Detail() );
}
public List<SelectItem> getCategoryItems() {
return categoriesBoundary.getCategories().stream()
.map( cat -> new SelectItem( cat, cat.getDescription() ) )
.collect( Collectors.toList() );
}
public Converter<Category> getCategoriesConverter() {
return new Converter<Category>() {
@Override
public String getAsString(FacesContext context, UIComponent component, Category value) {
return value==null ? null : Integer.toString( value.getId() );
}
@Override
public Category getAsObject(FacesContext context, UIComponent component, String value) {
if ( value==null || value.isEmpty() ) {
return null;
}
try {
return categoriesBoundary.findById( Integer.valueOf(value).intValue() );
} catch (NumberFormatException e) {
throw new ConverterException(e);
}
}
};
}
}
当我们现在按下上面 h:form
中的保存按钮时,名称输入文本已正确验证,但 @NotEmpty
- 和 @UniqueCategory
- 对详细信息的约束-属性 被忽略。
我错过了什么?
我们在 java-ee-7,payara 4。
深入研究之后,我们最终找到了一个使用支持组件的解决方案 ValidateListComponent
。它的灵感来自 UIValidateWholeBean
和 WholeBeanValidator
。
该组件从 UIInput
扩展并覆盖验证方法以对上述 details
集合的克隆进行操作,该集合填充了子 UIInput
的已验证值。似乎暂时有效。
<ui:component ...>
<cc:interface componentType="validatedListComponent">
<cc:attribute name="addAction" required="true" method-signature="void action()"/>
</cc:interface>
<cc:implementation>
... see above ...
</cc:implementation>
</ui:component>
支持组件定义为
@FacesComponent(value = "validatedListComponent")
@SuppressWarnings("unchecked")
public class ValidatedListComponent extends UIInput implements NamingContainer {
@Override
public String getFamily() {
return "javax.faces.NamingContainer";
}
/**
* Override {@link UIInput#processValidators(FacesContext)} to switch the order of
* validation. First validate this components children, then validate this itself.
*/
@Override
public void processValidators(FacesContext context) {
// Skip processing if our rendered flag is false
if (!isRendered()) {
return;
}
pushComponentToEL(context, this);
for (Iterator<UIComponent> i = getFacetsAndChildren(); i.hasNext(); ) {
i.next().processValidators(context);
}
if (!isImmediate()) {
Application application = context.getApplication();
application.publishEvent(context, PreValidateEvent.class, this);
executeValidate(context);
application.publishEvent(context, PostValidateEvent.class, this);
}
popComponentFromEL(context);
}
/**
* Override {@link UIInput#validate(FacesContext)} to validate a cloned collection
* instead of the submitted value.
*
* Inspired by {@link UIValidateWholeBean} and {@link WholeBeanValidator}.
*/
@Override
public void validate(FacesContext context) {
AreDetailsValidCallback callback = new AreDetailsValidCallback();
visitTree( VisitContext.createVisitContext(context)
, callback
);
if ( callback.isDetailsValid() ) {
Collection<?> clonedValue = cloneCollectionAndSetDetailValues( context );
validateValue(context, clonedValue);
}
}
/**
* private method copied from {@link UIInput#executeValidate(FacesContext)}.
* @param context
*/
private void executeValidate(FacesContext context) {
try {
validate(context);
} catch (RuntimeException e) {
context.renderResponse();
throw e;
}
if (!isValid()) {
context.validationFailed();
context.renderResponse();
}
}
private Collection<Object> cloneCollectionAndSetDetailValues(FacesContext context) {
ValueExpression collectionVE = getValueExpression("value");
Collection<?> baseCollection = (Collection<?>) collectionVE.getValue(context.getELContext());
if ( baseCollection==null ) {
return null;
}
// Visit all the components children to find their already validated values.
FindDetailValuesCallback callback = new FindDetailValuesCallback(context);
this.visitTree( VisitContext.createVisitContext(context)
, callback
);
// Clone this components value and put in all cloned details with validated values set.
try {
Collection<Object> clonedCollection = baseCollection.getClass().newInstance();
for( Entry<Object,Map<String,Object>> entry : callback.getDetailSubmittedData().entrySet() ) {
Object clonedDetail = cloneDetailAndSetValues( entry.getKey(), entry.getValue() );
clonedCollection.add( clonedDetail );
}
return clonedCollection;
} catch ( Exception e ) {
throw new ConverterException(e);
}
}
private <T> T cloneDetailAndSetValues(T detail, Map<String, Object> propertyMap) throws Exception {
T clonedDetail = clone(detail);
// check the properties we have in the detail
Map<String, PropertyDescriptor> availableProperties = new HashMap<>();
for (PropertyDescriptor propertyDescriptor : getBeanInfo(detail.getClass()).getPropertyDescriptors()) {
availableProperties.put(propertyDescriptor.getName(), propertyDescriptor);
}
// put their value (or local value) into our clone
for (Map.Entry<String, Object> propertyToSet : propertyMap.entrySet()) {
availableProperties.get(propertyToSet.getKey()).getWriteMethod().invoke(clonedDetail,
propertyToSet.getValue());
}
return clonedDetail;
}
private static <T> T clone(T object) throws Exception {
// clone an object using serialization.
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
out.writeObject(object);
byte[] bytes = byteArrayOutputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(byteArrayInputStream);
return (T) in.readObject();
}
private class FindDetailValuesCallback implements VisitCallback {
private final FacesContext context;
private final Map<Object, Map<String, Object>> detailSubmittedData = new HashMap<>();
public FindDetailValuesCallback(final FacesContext context) {
this.context = context;
}
final Map<Object, Map<String, Object>> getDetailSubmittedData() {
return detailSubmittedData;
}
@Override
public VisitResult visit(VisitContext visitContext, UIComponent component) {
if ( isVisitorTarget(component) ) {
ValueExpression ve = component.getValueExpression("value");
Object value = ((EditableValueHolder)component).getValue();
if (ve != null) {
ValueReference vr = ve.getValueReference(context.getELContext());
String prop = (String)vr.getProperty();
Object base = vr.getBase();
Map<String, Object> propertyMap
= Optional.ofNullable( detailSubmittedData.get(base) )
.orElseGet( HashMap::new );
propertyMap.put(prop, value );
detailSubmittedData.putIfAbsent( base, propertyMap);
}
}
return ACCEPT;
}
}
private class AreDetailsValidCallback implements VisitCallback {
private boolean detailsValid;
public AreDetailsValidCallback() {
this.detailsValid = true;
}
public boolean isDetailsValid() {
return detailsValid;
}
@Override
public VisitResult visit(VisitContext visitContext, UIComponent component) {
if ( isVisitorTarget(component) ) {
if ( !((EditableValueHolder)component).isValid() ) {
this.detailsValid = false;
}
}
return ACCEPT;
}
}
private boolean isVisitorTarget( UIComponent component ) {
return component instanceof EditableValueHolder && component.isRendered()
&& component!=ValidatedListComponent.this;
}
}
更新:有时在 FindDetailValuesCallback#visit
中获取 ValueReference
是个问题。 Michele here 给出的答案解决了这个问题。