3

I have a dynamic model consisting of X number each of radio, text, select, select-multi. This is basically like an EAV database in the backend.

I need to present this dynamic form with N total of fields then validate the submitted dynamic model object, which will then get validated using field-defined regexes. I would like this validation to be performed via JSR 303 annotations.

So, I would like to bind the form object using the typical way of doing Spring MVC development, via the ModelAttribute @Valid features etc. Only difference is that the model object is unknown/undefined until Runtime.

My inclination is to use CGLIB or something similar to generate the class at run time and present it with a special taglib then validate it using special validation somehow using reflection.

Is something like this completely out of the realm of possibility? Again, I would like to do regular Spring MVC controllers and models, but with a dynamic form object.

2 Answers2

1

This is possible, but not with cglib. Cglib is a proxying library and only allows you to override existing methods but not to define new ones which you would require in order to communicate a dynamic model to Spring or any other bean validation library.

It is of course possible to generate such classes using Javassist or Byte Buddy (disclosure: I am the author) which allow you to define such meta data. For example, with Byte Buddy, you can define a dynamic class:

new ByteBuddy()
  .subclass(Object.class)
  .defineField("foo", String.class, Visibility.PUBLIC)
  .annotateField(AnnotationDescription.Builder.ofType(NotNull.class).build())
  .make();

With this code, you could load the class, assign the "foo" field a value and present this bean object to a bean validation library.

At the same time, this approach feels over-engineered to me. I would only approach this solution if you require to provide compatibility to specific JSR 303 validators that you cannot reimplement. Otherwise, you might rather validate the data directly.

If your framework also requires getter and setters, you can add them via:

builder = builder
  .defineMethod("getFoo", String.class, Visibility.PUBLIC)
    .intercept(FieldAccessor.ofField("foo"));
  .defineMethod("setFoo", void.class, Visibility.PUBLIC)
    .withParameters(String.class)
    .intercept(FieldAccessor.ofField("foo"));
Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • Tried this. I'm getting `java.lang.IllegalStateException: Cannot access annotation property public abstract java.lang.Class[] javax.validation.constraints.NotNull.groups() net.bytebuddy.description.annotation.AnnotationDescription$ForLoadedAnnotation.getValue(AnnotationDescription.java:673)` –  Nov 13 '16 at 18:12
  • My bad, I did not think of that the `NotNull` property has optional attributes where the trick with the lambda does not work. Its really an anti-pattern anyways, I was on my phone and was lazy. The official way of defining annotations is now included in my updated answer above where you can also set the optional attributes. – Rafael Winterhalter Nov 13 '16 at 22:11
  • Looks like the error's gone away. Only issue now is that the value's not being set. Appears Spring MVC requires a getter/setter. Kind of at a loss on how to add the getter/setter with ByteBuddy. Any ideas on that? –  Nov 14 '16 at 01:13
  • I added example code for defining getters and setters. You can find extensive documentation on http://bytebuddy.net – Rafael Winterhalter Nov 14 '16 at 08:24
-1

Write code for the controller class (RegisterController.java) as follows:

File: src/main/java/net/codejava/spring/controller/RegisterController.java

package net.codejava.spring.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import net.codejava.spring.model.User;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value = "/register")
public class RegisterController {

    @RequestMapping(method = RequestMethod.GET)
    public String viewRegistration(Map<String, Object> model) {
        User userForm = new User();    
        model.put("userForm", userForm);

        List<String> professionList = new ArrayList<>();
        professionList.add("Developer");
        professionList.add("Designer");
        professionList.add("IT Manager");
        model.put("professionList", professionList);

        return "Registration";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String processRegistration(@ModelAttribute("userForm") User user,
            Map<String, Object> model) {

        // implement your own registration logic here...

        // for testing purpose:
        System.out.println("username: " + user.getUsername());
        System.out.println("password: " + user.getPassword());
        System.out.println("email: " + user.getEmail());
        System.out.println("birth date: " + user.getBirthDate());
        System.out.println("profession: " + user.getProfession());

        return "RegistrationSuccess";
    }
}

We can see that this controller is designed to handle the request URL /register:

@RequestMapping(value = "/register")

We implement two methods viewRegistration() and processRegistration() to handle the GET and POST requests, respectively. Writing handler methods in Spring is very flexible, as we can freely choose our own method names and necessary parameters. Let’s look at each method of the above controller class in details: viewRegistration(): in this method we create a model object and put it into the model map with the key “userForm”:

User userForm = new User();
model.put("userForm", userForm);

This creates a binding between the specified object with the form in the view returned by this method (which is the registration form). Note that the key “userForm” must match value of the commandName attribute of the tag.

Another interesting point is that we create a list of Strings and put it into the model map with the key “professionList”:

List<String> professionList = new ArrayList<>();
professionList.add("Developer");
professionList.add("Designer");
professionList.add("IT Manager");
model.put("professionList", professionList);

This collection will be used by the tag in the Registration.jsp page in order to render the profession dropdown list dynamically. Finally this method returns a view name (“Registration”) which will be mapped to the registration form page above. processRegistration(): this method handles the form submission (via POST request). The important parameter here is:

@ModelAttribute("userForm") User user

This will make the model object which is stored under the key “userForm” in the model map available to the method body. Again, the key “userForm” must match value of the commandName attribute of the tag. When the form is submitted, Spring automatically binds the form’s field values to the backing object in the model, thus we can access the form values inputted by the user through this backing object like this:

System.out.println("username: " + user.getUsername());

For demo purpose, this method only prints out details of the User object, and finally returns a view name of the success page (“RegistrationSuccess”).

  • This does not answer my question at all. I have a dynamic model. Please re-read my question--Downvote. –  Nov 12 '16 at 15:27