Reputation: 24567
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!
Upvotes: 2
Views: 870
Reputation: 24567
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)
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!
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 :-)
.
Upvotes: 2