javaspringgenericsconverterspropertyeditor

Global Generic PropertyEditorSupport for @PathVariable -> Domain Object


I'm using Spring 4 with Spring Data MongoDB and want to get rid of some boilerplate code in my controllers.

I just want to replace this:

@RequestMapping("{id}")
void a(@PathVariable ObjectId id) {
   DomainObject do = service.getDomainObjectById(id);
   // ...
}

with this:

@RequestMapping("{id}")
void a(@PathVariable("id") DomainObject do) {
  // ...
}

At the moment I've got to write a pair of PropertyEditorSupport and @ControllerAdvice classes for each domain object I have:

@Component
public class SomeDomainObjectEditor extends PropertyEditorSupport {
    @Autowired
    SomeDomainObjectService someDomainObjectService;

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        setValue(someDomainObjectService.getById(new ObjectId(text)));
    }

    @Override
    public String getAsText() {
        SomeDomainObject value = (SomeDomainObject) getValue();
        return (value != null ? value.getId().toString() : null);
    }
}

@ControllerAdvice
public class SomeDomainObjectControllerAdvice {
    @Autowired
    SomeDomainObjectEditor someDomainObjectEditor;

    @InitBinder
    public void register(WebDataBinder binder, WebRequest request) {
        binder.registerCustomEditor(SomeDomainObject.class, someDomainObjectEditor);
    }
}

And I can't figure out an easy way to get this done in a generic way, because I have a lot of domain objects and all behave the same.

All my domain objects implement BaseDocument<ID> and thus have the getId() method. So basically I want something like this:

public class BaseDocumentPropertyEditor extends PropertyEditorSupport { ... }

It would also be okay (= nice) to have this working, using a Converter<String, BaseDocument<?>> which can be used also in other places within the Spring Framework.

My main problem is, that I can't imagine an easy way to find the corresponding @Service in order to fetch the domain object from DB. (I can't use the Repository because of access restriction for certain data).

Hopefully you have some advice. Thank you!


Solution

  • NEWER:

    If you also want proper Exception handling, you should use DomainClassPropertyEditorRegistrar, because DomainClassConverter swallows underlying exceptions...

    Here we go! Just update your WebMvcConfigurationSupport with:

    @Override
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
        RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
        ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer();
        initializer.setPropertyEditorRegistrar(domainClassPropertyEditorRegistrar());
        return adapter;
    }
    
    @Bean
    public DomainClassPropertyEditorRegistrar domainClassPropertyEditorRegistrar() {
        return new DomainClassPropertyEditorRegistrar();
    }
    

    (Maybe @Bean is unnecessary, but at least it works this way)

    NEW:

    Spring Data already provides everything I need: DomainClassConverter

    Just put

    @Bean
    public DomainClassConverter<?> domainClassConverter() {
        return new DomainClassConverter<FormattingConversionService>(mvcConversionService());
    }
    

    in the WebMvcConfigurationSupport class and it all works out of the box!

    OLD:

    My final solution was to just stay with the one pair of classes per domain object approach. I just built 2 abstract classes plus an interface, to minimize the effort:

    1. PropertyEditor

    public abstract class AbstractEntityEditor<ID extends Serializable, SERVICE extends CanGetEntityById<?, ID>> extends PropertyEditorSupport {
    
        @Autowired
        SERVICE service;
    
        @Autowired
        ConversionService cs;
    
        final Class<ID> id;
    
        public AbstractEntityEditor(Class<ID> id) {
            this.id = id;
        }
    
        @Override
        public void setAsText(String text) throws IllegalArgumentException {
            setValue(service.getById(cs.convert(text, id)));
        }
    
    }
    

    2. ControllerAdvice

    public abstract class AbstractEntityEditorControllerAdvice<EDITOR extends PropertyEditor> {
    
        @Autowired
        EDITOR editor;
    
        final Class<?> entity;
    
        public AbstractEntityEditorControllerAdvice(Class<?> entity) {
            this.entity = entity;
        }
    
        @InitBinder
        public void register(WebDataBinder binder, WebRequest request) {
            binder.registerCustomEditor(entity, editor);
        }
    
    }
    

    3. Service interface to retrieve an domain object

    public interface CanGetEntityById<ENTITY, ID extends Serializable> {
        ENTITY getById(ID id) throws NotFoundException;
    }
    

    And here's a sample use case:

    1.

    @Component
    public class UserEditor extends AbstractEntityEditor<ObjectId, UserService> {
        public UserEditor() {
            super(ObjectId.class);
        }
    }
    

    2.

    @ControllerAdvice
    public class UserControllerAdvice extends AbstractEntityEditorControllerAdvice<UserEditor>{
        public UserControllerAdvice() {
            super(User.class);
        }
    }
    

    3.

    public interface UserService extends GetEntityById<User, ObjectId> { }
    

    4.

    @Service
    public class UserServiceImpl implements UserService {
        public User getById(ObjectId id) throws NotFoundException {
            // fetch User from repository and return
        }
    }
    

    Maybe there's a way to make it a bit better, but at least it works! And now it's just 5 lines of code to write :-).