JSR 303-Validierung: Wenn ein Feld gleich "etwas" ist, sollten diese anderen Felder nicht null sein

87

Ich möchte eine kleine benutzerdefinierte Validierung mit JSR-303 durchführen javax.validation.

Ich habe ein Feld. Und wenn ein bestimmter Wert in dieses Feld eingegeben wird, möchte ich verlangen, dass einige andere Felder dies nicht tun null.

Ich versuche das herauszufinden. Ich bin mir nicht sicher, wie ich das nennen würde, um eine Erklärung zu finden.

Jede Hilfe wäre dankbar. Ich bin ziemlich neu darin.

Im Moment denke ich an eine benutzerdefinierte Einschränkung. Ich bin mir jedoch nicht sicher, wie ich den Wert des abhängigen Felds innerhalb der Anmerkung testen soll. Grundsätzlich bin ich mir nicht sicher, wie ich über die Annotation auf das Panel-Objekt zugreifen soll.

public class StatusValidator implements ConstraintValidator<NotNull, String> {

    @Override
    public void initialize(NotNull constraintAnnotation) {}

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if ("Canceled".equals(panel.status.getValue())) {
            if (value != null) {
                return true;
            }
        } else {
            return false;
        }
    }
}

Es ist das, panel.status.getValue();was mir Ärger macht. Ich bin mir nicht sicher, wie ich das erreichen soll.

Eric
quelle

Antworten:

103

In diesem Fall schlage ich vor, einen benutzerdefinierten Validator zu schreiben, der auf Klassenebene validiert (damit wir auf die Felder des Objekts zugreifen können), dass ein Feld nur erforderlich ist, wenn ein anderes Feld einen bestimmten Wert hat. Beachten Sie, dass Sie einen generischen Validator schreiben sollten, der 2 Feldnamen erhält und nur mit diesen 2 Feldern arbeitet. Um mehr als ein Feld zu benötigen, sollten Sie diesen Validator für jedes Feld hinzufügen.

Verwenden Sie den folgenden Code als Idee (ich habe ihn nicht getestet).

  • Validator-Schnittstelle

    /**
     * Validates that field {@code dependFieldName} is not null if
     * field {@code fieldName} has value {@code fieldValue}.
     **/
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Repeatable(NotNullIfAnotherFieldHasValue.List.class) // only with hibernate-validator >= 6.x
    @Constraint(validatedBy = NotNullIfAnotherFieldHasValueValidator.class)
    @Documented
    public @interface NotNullIfAnotherFieldHasValue {
    
        String fieldName();
        String fieldValue();
        String dependFieldName();
    
        String message() default "{NotNullIfAnotherFieldHasValue.message}";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    
        @Target({TYPE, ANNOTATION_TYPE})
        @Retention(RUNTIME)
        @Documented
        @interface List {
            NotNullIfAnotherFieldHasValue[] value();
        }
    
    }
  • Validator-Implementierung

    /**
     * Implementation of {@link NotNullIfAnotherFieldHasValue} validator.
     **/
    public class NotNullIfAnotherFieldHasValueValidator
        implements ConstraintValidator<NotNullIfAnotherFieldHasValue, Object> {
    
        private String fieldName;
        private String expectedFieldValue;
        private String dependFieldName;
    
        @Override
        public void initialize(NotNullIfAnotherFieldHasValue annotation) {
            fieldName          = annotation.fieldName();
            expectedFieldValue = annotation.fieldValue();
            dependFieldName    = annotation.dependFieldName();
        }
    
        @Override
        public boolean isValid(Object value, ConstraintValidatorContext ctx) {
    
            if (value == null) {
                return true;
            }
    
            try {
                String fieldValue       = BeanUtils.getProperty(value, fieldName);
                String dependFieldValue = BeanUtils.getProperty(value, dependFieldName);
    
                if (expectedFieldValue.equals(fieldValue) && dependFieldValue == null) {
                    ctx.disableDefaultConstraintViolation();
                    ctx.buildConstraintViolationWithTemplate(ctx.getDefaultConstraintMessageTemplate())
                        .addNode(dependFieldName)
                        .addConstraintViolation();
                        return false;
                }
    
            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
                throw new RuntimeException(ex);
            }
    
            return true;
        }
    
    }
  • Verwendungsbeispiel für den Validator (Ruhezustand-Validator> = 6 mit Java 8+)

    @NotNullIfAnotherFieldHasValue(
        fieldName = "status",
        fieldValue = "Canceled",
        dependFieldName = "fieldOne")
    @NotNullIfAnotherFieldHasValue(
        fieldName = "status",
        fieldValue = "Canceled",
        dependFieldName = "fieldTwo")
    public class SampleBean {
        private String status;
        private String fieldOne;
        private String fieldTwo;
    
        // getters and setters omitted
    }
  • Verwendungsbeispiel für den Validator (Ruhezustand-Validator <6; das alte Beispiel)

    @NotNullIfAnotherFieldHasValue.List({
        @NotNullIfAnotherFieldHasValue(
            fieldName = "status",
            fieldValue = "Canceled",
            dependFieldName = "fieldOne"),
        @NotNullIfAnotherFieldHasValue(
            fieldName = "status",
            fieldValue = "Canceled",
            dependFieldName = "fieldTwo")
    })
    public class SampleBean {
        private String status;
        private String fieldOne;
        private String fieldTwo;
    
        // getters and setters omitted
    }

Beachten Sie, dass die Validator-Implementierung eine BeanUtilsKlasse aus der commons-beanutilsBibliothek verwendet, Sie sie jedoch auch BeanWrapperImplaus Spring Framework verwenden können .

Siehe auch diese großartige Antwort: Feldübergreifende Validierung mit Hibernate Validator (JSR 303)

Slava Semushin
quelle
1
@Benedictus Dieses Beispiel funktioniert nur mit Zeichenfolgen, Sie können es jedoch so ändern, dass es mit beliebigen Objekten funktioniert. Es gibt zwei Möglichkeiten: 1) Parametrisieren Sie den Validator mit der Klasse, die Sie validieren möchten (anstelle von Object). In diesem Fall müssen Sie nicht einmal Reflection verwenden, um die Werte abzurufen. In diesem Fall wird der Validator jedoch weniger allgemein. 2) Verwenden Sie BeanWrapperImpSpring Framework (oder andere Bibliotheken) und seine getPropertyValue()Methode. In diesem Fall können Sie einen Wert als erhalten Objectund in einen beliebigen Typ umwandeln.
Slava Semushin
Ja, aber Sie können Object nicht als Annotationsparameter haben. Daher benötigen Sie für jeden Typ, den Sie validieren möchten, eine Reihe verschiedener Annotationen.
Ben
1
Ja, das meine ich, als ich sagte "In diesem Fall wird der Validator weniger allgemein".
Slava Semushin
Ich möchte diesen Trick für protoBuffer-Klassen verwenden. Dies ist sehr hilfreich (:
Saeed
Schöne Lösung. Sehr hilfreich, um benutzerdefinierte Anmerkungen zu erstellen!
Vishwa
119

Definieren Sie eine Methode, die auf true validiert werden muss, und setzen Sie die @AssertTrueAnmerkung oben drauf:

  @AssertTrue
  private boolean isOk() {
    return someField != something || otherField != null;
  }

Die Methode muss mit 'is' beginnen.

Audrius Meskauskas
quelle
Ich habe Ihre Methode verwendet und sie funktioniert, aber ich kann nicht herausfinden, wie ich die Nachricht erhalte. Würdest du zufällig wissen?
AnaBad
12
Dies war bei weitem die effizienteste Option. Vielen Dank! @anaBad: Die AssertTrue-Annotation kann eine benutzerdefinierte Nachricht aufnehmen, genau wie andere Constraint-Annotationen.
ernest_k
@ErnestKiwele Danke für die Antwort, aber mein Problem ist nicht das Einstellen der Nachricht, sondern das Abrufen in meinem JSP. Ich habe folgende Funktion das Modell: @AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() { return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); } Und das in meinem JSP: <th><form:label path="reference"><s:message code="reference"/></form:label></th><td><form:input path="reference" cssErrorClass="errorField"/><br/><form:errors path="isReferenceOk" cssClass="error"/></td> Aber es wirft einen Fehler.
AnaBad
@ErnestKiwele Egal, ich habe es herausgefunden, ich habe ein boolesches Attribut erstellt, das beim Aufruf von setReference () festgelegt wird.
AnaBad
2
Ich musste die Methode öffentlich machen
Tibi
19

Sie sollten benutzerdefinierte verwenden DefaultGroupSequenceProvider<T>:

ConditionalValidation.java

// Marker interface
public interface ConditionalValidation {}

MyCustomFormSequenceProvider.java

public class MyCustomFormSequenceProvider
    implements DefaultGroupSequenceProvider<MyCustomForm> {

    @Override
    public List<Class<?>> getValidationGroups(MyCustomForm myCustomForm) {

        List<Class<?>> sequence = new ArrayList<>();

        // Apply all validation rules from ConditionalValidation group
        // only if someField has given value
        if ("some value".equals(myCustomForm.getSomeField())) {
            sequence.add(ConditionalValidation.class);
        }

        // Apply all validation rules from default group
        sequence.add(MyCustomForm.class);

        return sequence;
    }
}

MyCustomForm.java

@GroupSequenceProvider(MyCustomFormSequenceProvider.class)
public class MyCustomForm {

    private String someField;

    @NotEmpty(groups = ConditionalValidation.class)
    private String fieldTwo;

    @NotEmpty(groups = ConditionalValidation.class)
    private String fieldThree;

    @NotEmpty
    private String fieldAlwaysValidated;


    // getters, setters omitted
}

Siehe auch verwandte Fragen zu diesem Thema .

user11153
quelle
Interessante Art, es zu tun. Die Antwort könnte jedoch eine genauere Erklärung der Funktionsweise gebrauchen, da ich sie zweimal lesen musste, bevor ich sah, was los war ...
Jules
Hallo, ich habe Ihre Lösung implementiert, stehe aber vor einem Problem. Es wird kein Objekt an die getValidationGroups(MyCustomForm myCustomForm)Methode übergeben. Könnten Sie hier möglicherweise helfen? : stackoverflow.com/questions/44520306/…
user238607
2
@ user238607 getValidationGroups (MyCustomForm myCustomForm) ruft viele Male pro Bean-Instanz auf und vergeht einige Zeit null. Sie ignorieren es einfach, wenn es null übergibt.
Pramoth
6

Hier ist meine Meinung dazu, ich habe versucht, es so einfach wie möglich zu halten.

Die Schnittstelle:

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = OneOfValidator.class)
@Documented
public @interface OneOf {

    String message() default "{one.of.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String[] value();
}

Validierungsimplementierung:

public class OneOfValidator implements ConstraintValidator<OneOf, Object> {

    private String[] fields;

    @Override
    public void initialize(OneOf annotation) {
        this.fields = annotation.value();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {

        BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);

        int matches = countNumberOfMatches(wrapper);

        if (matches > 1) {
            setValidationErrorMessage(context, "one.of.too.many.matches.message");
            return false;
        } else if (matches == 0) {
            setValidationErrorMessage(context, "one.of.no.matches.message");
            return false;
        }

        return true;
    }

    private int countNumberOfMatches(BeanWrapper wrapper) {
        int matches = 0;
        for (String field : fields) {
            Object value = wrapper.getPropertyValue(field);
            boolean isPresent = detectOptionalValue(value);

            if (value != null && isPresent) {
                matches++;
            }
        }
        return matches;
    }

    private boolean detectOptionalValue(Object value) {
        if (value instanceof Optional) {
            return ((Optional) value).isPresent();
        }
        return true;
    }

    private void setValidationErrorMessage(ConstraintValidatorContext context, String template) {
        context.disableDefaultConstraintViolation();
        context
            .buildConstraintViolationWithTemplate("{" + template + "}")
            .addConstraintViolation();
    }

}

Verwendung:

@OneOf({"stateType", "modeType"})
public class OneOfValidatorTestClass {

    private StateType stateType;

    private ModeType modeType;

}

Mitteilungen:

one.of.too.many.matches.message=Only one of the following fields can be specified: {value}
one.of.no.matches.message=Exactly one of the following fields must be specified: {value}
jokarl
quelle
3

Ein anderer Ansatz wäre, einen (geschützten) Getter zu erstellen, der ein Objekt zurückgibt, das alle abhängigen Felder enthält. Beispiel:

public class MyBean {
  protected String status;
  protected String name;

  @StatusAndSomethingValidator
  protected StatusAndSomething getStatusAndName() {
    return new StatusAndSomething(status,name);
  }
}

StatusAndSomethingValidator kann jetzt auf StatusAndSomething.status und StatusAndSomething.something zugreifen und eine abhängige Prüfung durchführen.

Michael Wyraz
quelle