
org.nuiton.validator.bean.BeanValidator Maven / Gradle / Ivy
/*
* #%L
* Validation :: API
* %%
* Copyright (C) 2021 - 2024 Ultreia.io
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* .
* #L%
*/
package org.nuiton.validator.bean;
import com.google.common.base.Preconditions;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.nuiton.validator.NuitonValidator;
import org.nuiton.validator.NuitonValidatorModel;
import org.nuiton.validator.NuitonValidatorProvider;
import org.nuiton.validator.NuitonValidatorProviders;
import org.nuiton.validator.NuitonValidatorResult;
import org.nuiton.validator.NuitonValidatorScope;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.Objects;
/**
* Validator for a javaBean object.
*
* A such validator is designed to validate to keep the validation of a bean,
* means the bean is attached to the validator (via the context field {@link #context}.
*
* A such validator is also a JavaBean and you can listen his states
* modifications via the classic java bean api.
*
* Note: The {@link org.nuiton.validator.bean.BeanValidator} should never be used for
* validation in a service approach since it needs to keep a reference to the
* bean to validate.
*
* @author Tony Chemit - [email protected]
* @see BeanValidatorListener
* @since 2.5.2
*/
public class BeanValidator extends AbstractValidator {
/**
* Name of the bounded property {@code bean}.
*
* @see #getBean()
* @see #setBean(Object)
*/
public static final String BEAN_PROPERTY = "bean";
private static final Logger log = LogManager.getLogger(BeanValidator.class);
/**
* Context of the registered bean to validate.
*
* @since 2.5.2
*/
protected final BeanValidatorContext context;
/**
* To chain to another validator (acting as parent of this one).
*
* @since 2.5.2
*/
protected BeanValidator> parentValidator;
/**
* Obtain a new {@link BeanValidator} for the given parameters.
*
* Note: It will use the default provider of {@link NuitonValidator}
*
* @param type type of bean to validate
* @param context context of validation
* @param scopes authorized scopes (if {@code null}, will use all scopes)
* @param type of bean to validate
* @return the new instantiated {@link BeanValidator}.
* @see NuitonValidatorProviders#getDefaultProviderName()
*/
public static BeanValidator newValidator(Class type, String context, NuitonValidatorScope... scopes) {
// get the provider default name
String providerName = NuitonValidatorProviders.getDefaultProviderName();
// get the bean validator with this provider
return newValidator(providerName, type, context, scopes);
}
/**
* Obtain a new {@link BeanValidator} for the given parameters.
*
* Note: It will use the provider of {@link NuitonValidator}
* defined by the {@code providerName}.
*
* @param providerName name of {@link NuitonValidator} to use
* @param type type of bean to validate
* @param context context of validation
* @param scopes authorized scopes (if {@code null}, will use all scopes)
* @param type of bean to validate
* @return the new instantiated {@link BeanValidator}.
* @throws NullPointerException if type is {@code null}
* @see NuitonValidatorProviders#getProvider(String)
*/
public static BeanValidator newValidator(String providerName, Class type, String context, NuitonValidatorScope... scopes) {
Objects.requireNonNull(type, "type parameter can not be null.");
// get delegate validator provider
NuitonValidatorProvider provider = NuitonValidatorProviders.getProvider(providerName);
// create the new instance of bean validator
return new BeanValidator<>(provider, type, context, scopes);
}
public BeanValidator(NuitonValidatorProvider validatorProvider, Class beanClass, String context) {
this(validatorProvider, beanClass, context, NuitonValidatorScope.values());
}
public BeanValidator(NuitonValidatorProvider validatorProvider, Class beanClass, String context, NuitonValidatorScope... scopes) {
super(validatorProvider, beanClass);
this.context = new BeanValidatorContext<>();
// build delegate validator
rebuildDelegateValidator(beanClass, context, scopes);
// context has changed
firePropertyChange(CONTEXT_PROPERTY, null, context);
// scopes has changed
firePropertyChange(SCOPES_PROPERTY, null, scopes);
}
/**
* Obtain the actual bean attached to the validator.
*
* @return the bean attached to the validor or {@code null} if no bean
* is attached
*/
public O getBean() {
return context.getBean();
}
/**
* Change the attached bean.
*
* As a side effect, the internal
* {@link BeanValidatorContext#getMessages()} will be reset.
*
* @param bean the bean to attach (can be {@code null} to reset the
* validator).
*/
public void setBean(O bean) {
O oldBean = getBean();
if (log.isDebugEnabled()) {
log.debug(this + " : " + bean);
}
if (oldBean != null) {
try {
BeanUtil.removePropertyChangeListener(l, oldBean);
} catch (Exception eee) {
if (log.isInfoEnabled()) {
log.info("Can't unregister as listener for bean " + oldBean.getClass() +
" for reason " + eee.getMessage(), eee);
}
}
}
context.setBean(bean);
if (bean == null) {
// remove all messages for all fields of the validator
mergeMessages(null);
} else {
try {
BeanUtil.addPropertyChangeListener(l, bean);
} catch (Exception eee) {
if (log.isInfoEnabled()) {
log.info("Can't register as listener for bean " + bean.getClass() +
" for reason " + eee.getMessage(), eee);
}
}
validate();
}
setChanged(false);
setValid(context.isValid());
firePropertyChange(BEAN_PROPERTY, oldBean, bean);
}
public BeanValidator> getParentValidator() {
return parentValidator;
}
public void setParentValidator(BeanValidator> parentValidator) {
this.parentValidator = parentValidator;
}
@Override
public boolean hasFatalErrors() {
return context.hasFatalErrors();
}
@Override
public boolean hasErrors() {
return context.hasErrors();
}
@Override
public boolean hasWarnings() {
return context.hasWarnings();
}
@Override
public boolean hasInfos() {
return context.hasInfos();
}
@Override
public boolean isValid(String fieldName) {
// field is valid if no fatal messages nor error messages
return context.isValid(fieldName);
}
@Override
public NuitonValidatorScope getHighestScope(String field) {
return context.getHighestScope(field);
}
@Override
public void doValidate() {
validate();
setValid(context.isValid());
setChanged(true);
}
public void addBeanValidatorListener(BeanValidatorListener listener) {
listenerList.add(BeanValidatorListener.class, listener);
}
public void removeBeanValidatorListener(BeanValidatorListener listener) {
listenerList.remove(BeanValidatorListener.class, listener);
}
public BeanValidatorListener[] getBeanValidatorListeners() {
return listenerList.getListeners(BeanValidatorListener.class);
}
@Override
protected void doValidate(O bean) {
Preconditions.checkState(
Objects.equals(bean, getBean()),
"Can not validate the bean [" + bean +
"] which is not the one registered [" + bean +
"] in this validator.");
doValidate();
}
@Override
protected NuitonValidator getDelegate() {
return context.getValidator();
}
@Override
protected void rebuildDelegateValidator(Class beanType,
String context,
NuitonValidatorScope... scopes) {
// changing context could change fields definition
// so dettach bean, must rebuild the fields
// Dettach the bean before any thing, because with the new delegate
// validator some old fields could not be used any longer, and then
// listeners will never have the full reset of their model...
if (getBean() != null) {
setBean(null);
}
if (scopes == null || scopes.length == 0) {
scopes = NuitonValidatorScope.values();
}
// compute the new validator model
NuitonValidatorModel validatorModel = validatorProvider.getModel(beanType,
context,
scopes
);
// remove old delegate validator
NuitonValidator delegate = validatorProvider.newValidator(validatorModel);
this.context.setValidator(delegate);
}
/**
* il faut eviter le code re-intrant (durant une validation, une autre est
* demandee). Pour cela on fait la validation dans un thread, et tant que la
* premiere validation n'est pas fini, on ne repond pas aux solicitations.
* Cette method est public pour permettre de force une validation par
* programmation, ce qui est utile par exemple si le bean ne supporte pas
* les {@link PropertyChangeListener}
*
* Note: la methode est protected et on utilise la methode
* {@link #doValidate()} car la méthode ne modifie pas les etats
* internes et cela en rend son utilisation delicate (le validateur entre
* dans un etat incoherent par rapport aux messages envoyés).
*/
protected void validate() {
// on ne valide que si il y a un bean et que le resultat de la validation
// pourra etre affiche quelque part
if (isCanValidate()) {
NuitonValidatorResult result = context.validate();
mergeMessages(result);
if (parentValidator != null) {
// chained validation
// the parent validator should not be changed from this validation
boolean wasModified = parentValidator.isChanged();
parentValidator.doValidate();
if (!wasModified) {
// push back old state
parentValidator.setChanged(false);
}
}
}
}
protected void fireFieldChanged(BeanValidatorEvent evt) {
for (BeanValidatorListener listener :
listenerList.getListeners(BeanValidatorListener.class)) {
listener.onFieldChanged(evt);
}
}
protected void mergeMessages(NuitonValidatorResult newMessages) {
List> events = context.mergeMessages(this,
newMessages);
if (events != null) {
// send all messages
for (BeanValidatorEvent event : events) {
fireFieldChanged(event);
}
}
}
}