dooApp/FXForm2

Support for object graph validation

Closed this issue · 9 comments

oova commented

Hi,

is object graph validation (as described here) supported by FXForm2? I could not find anything about this in the docs or on your blog.

Thanks.

The object graph validation should work out of the box. However note that your field will be a complex type in this case, so there is not default editor for this. You will need a custom editor factory for this object type as explained here.

oova commented

Thank your for your reply. I wrote a small example application to test this feature but I failed to make it work. Here is the code:

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

import com.dooapp.fxform.AbstractFXForm;
import com.dooapp.fxform.FXForm;
import com.dooapp.fxform.adapter.Adapter;
import com.dooapp.fxform.adapter.AdapterException;
import com.dooapp.fxform.adapter.DefaultAdapterProvider;
import com.dooapp.fxform.annotation.Accessor;
import com.dooapp.fxform.model.Element;
import com.dooapp.fxform.view.FXFormNode;
import com.dooapp.fxform.view.FXFormNodeWrapper;
import com.dooapp.fxform.view.factory.DefaultFactoryProvider;

import org.hibernate.validator.constraints.NotBlank;

import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Callback;

public class ObjectGraphForm extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        Pane root = new Pane();
        FXForm<User> form = new FXForm<>();

        User user = new User();
        Partner partner = new Partner();
        user.partner.set(partner);

        DefaultFactoryProvider editorFactoryProvider = new DefaultFactoryProvider();
        editorFactoryProvider.addFactory(element -> element.getName().equals(user.partner.getName()), new PartnerTextFieldFactory<>(partner));
        form.setEditorFactoryProvider(editorFactoryProvider);

        DefaultAdapterProvider adapterProvider = new DefaultAdapterProvider();
        adapterProvider.addAdapter((fromClass, toClass, element, fxFormNode) -> fromClass.equals(ObjectProperty.class) && element.getName().equals(user.partner.getName()), new PartnerAdapter());
        form.setAdapterProvider(adapterProvider);

        form.setSource(user);
        root.getChildren().add(form);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    @Accessor(value = Accessor.AccessType.METHOD)
    public static class User {
        public StringProperty firstName = new SimpleStringProperty("Jon");
        public StringProperty lastName = new SimpleStringProperty("Smith");
        public ObjectProperty<Partner> partner = new SimpleObjectProperty<>(this, "partner");

        @NotBlank
        public String getFirstName() {
            return firstName.get();
        }

        public void setFirstName(String firstName) {
            this.firstName.set(firstName);
        }

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public String getLastName() {
            return lastName.get();
        }

        public void setLastName(String lastName) {
            this.lastName.set(lastName);
        }

        public StringProperty lastNameProperty() {
            return lastName;
        }

        @NotNull
        @Valid
        public Partner getPartner() {
            return partner.get();
        }

        public void setPartner(Partner partner) {
            this.partner.set(partner);
        }

        public ObjectProperty<Partner> partnerProperty() {
            return partner;
        }
    }

    @Accessor(value = Accessor.AccessType.METHOD)
    public static class Partner {
        public StringProperty firstName = new SimpleStringProperty("Jack");
        public StringProperty lastName = new SimpleStringProperty("Myers");

        @NotBlank
        public String getFirstName() {
            return firstName.get();
        }

        public void setFirstName(String firstName) {
            this.firstName.set(firstName);
        }

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public String getLastName() {
            return lastName.get();
        }

        public void setLastName(String lastName) {
            this.lastName.set(lastName);
        }

        public StringProperty lastNameProperty() {
            return lastName;
        }

        @Override
        public String toString() {
            return getFirstName() + " " + getLastName();
        }
    }

    public static class PartnerTextFieldFactory<T> implements Callback<Void, FXFormNode> {

        private final Partner partner;

        public PartnerTextFieldFactory(Partner partner) {
            this.partner = partner;
        }

        public FXFormNode call(Void aVoid) {

            TextField textField = new TextField();

            return new FXFormNodeWrapper(textField, textField.textProperty()) {
                @Override
                public void dispose() {
                    textField.textProperty().unbindBidirectional(partner.firstNameProperty());
                    super.dispose();
                }

                @Override
                public void init(Element element, AbstractFXForm fxForm) {
                    super.init(element, fxForm);
                    textField.textProperty().bindBidirectional(partner.firstNameProperty());
                }

                @Override
                public boolean isEditable() {
                    return true;
                }

            };
        }
    }

    public static class PartnerAdapter implements Adapter<Partner, String> {
        @Override
        public String adaptTo(Partner from) throws AdapterException {
            if (from == null) {
                return null;
            }
            return from.getFirstName();
        }

        @Override
        public Partner adaptFrom(String to) throws AdapterException {
            Partner p = new Partner();
            p.setFirstName(to);
            return p;
        }
    }
}

When I run this application, I can see a constraint violation if I clear the user's first name, but I do not see a violation if I clear the partner's first name.

Is there a mistake in the way I define the editor factory or the adapter for the Partner field?

Thank you for your support.

oova commented

After a little digging around, I found out why the constraint violation for the partner's first name does not appear in the form's list of constraint violations:

When we look at the method DefaultFXFormValidator#getConstraintViolation, we can see the following lines of code

...
for (ConstraintViolation constraintViolation : constraintViolations) {
  if (classLevelConstraints.contains(constraintViolation.getConstraintDescriptor())) {
    list.add(constraintViolation);
  }
}
...

The missing constraint violation is contained in the constraintViolations list here (which is what we want), but since it is not a class-level constraint, it will not be added to the list list (which is probably not what we want).

There is another chance the missing violation could be found, which is in the DefaultFXFormValidator#validate method. This method, however, uses the Validator.validateValue method which returns an empty collection of violations for the partner's first name property when using the Hibernate validator impl. So I guess the violation should not be found here (as I assume Hibernate's validator works as expected), but rather in the aforementioned DefaultFXFormValidator#getConstraintViolation method where it does not pass the filter, unfortunately.

@amischler Can you confirm this analysis?

Thank you for your support.

Thanks you for this analysis. You are right, the issue seems to be related to DefaultFXFormValidator#validate that should report this violation at the field level. This violation should not be reported a the class level.

I have checked the Hibernate validator documentation which states that @Valid is not honored by validateProperty() or validateValue().

I will need to dig a bit more on this, I did not find in the Hibernate document how to honor the @Valid annotation using field validation methods.

I fixed this by checking if the PropertyDescriptor is cascaded in the DefaultFXFormValidator#validate method (which means that it is annotated with a @Valid annotation). In this case we perform a full validation of the property value and report any constraints found on the object at the field level.

Deployed in 8.3.0-SNAPSHOT

BTW note that you can simplify your example :

  • you can use the existing WrappedTypeHandler as handler to register your custom factory
  • you can use the existing ObjectPropertyAdapterMatcher as matcher to register your custom apdater
  • you don't need to do any model binding in your custom node factory. The node factory is only responsible for building the node used to edit the field and to expose the property of the node that the model should be bound to. FXForm2 will do the binding. In your case you could use the existing TextFieldFactory which does exactly what you want.

See Issue171Test for more details.

oova commented

Thanks! I can confirm your changes fixed the graph validation issue.

Your feedback on simplifying my example is very much appreciated. As someone who was rather new to working with FXForm, it took me some effort to collect documentation/sample code for more advanced features . There is the wiki, there is a blog post from 2013, and there is a bit of support on SO. I think it would be particularly helpful for beginners to have more samples showing advanced features like object graph validation. Maybe some tests could be turned into sample applications?

Thank you for your feedback on your experience with the FXForm documentation. I totally agree with your analysis, the documentation of form customization and advanced features could be improved. I created a ticket for this : #176

Any help is welcome ;-)