Question

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!

Was it helpful?

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 :-).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top