I have a question about the formatter in Spring.
I have a Formatter for my select boxes, for example:
public class SportTypeFormatter implements Formatter<SportType> {
#Autowired
private SportTypeRepository sportTypeRepository;
#Override
public String print(SportType sportType, Locale locale) {
return String.valueOf(sportType.getTypeId());
}
#Override
public SportType parse(String sportTypeId, Locale locale) throws ParseException {
return sportTypeRepository.findSportTypeByTypeId(Long.valueOf(sportTypeId));
}
}
in thymeleaf something like this:
<select class="form-control" name="sportTypeId" th:field="*{person.sport.sportType}">
<option th:each="spoType : ${allSportTypes}" th:value="${spoType.typeId}" th:selected="${spoType.typeId == person.sport.sportType}" th:text="#{${'login.sport.sportType.' + spo.typeId}}" >Sporttype</option>
</select>
Thats easy, because i only need one value (the id) and i'm going with select-box.
But what is if i need two values?
Suggest i have Email, there i need the id and the value (mail-address). I can build an Formatter for email but i have no chance to transfer the email and id at the same time.
In the print method i can something like that:
#Override
public String print(Email email, Locale locale) {
return email.getId() + email.getEmail();
}
in thymeleaf:
<input id="email" type="text" class="form-control" th:field="*{{person.email}}" th:placeholder="#{login.email}" />
But with this the user can see the id.
If i do the binding the "standard" Spring way i get the following exception (thats the reason why i use the formatter):
{timestamp=Wed Dec 10 11:14:47 CET 2014, status=400, error=Bad Request, exception=org.springframework.validation.BindException,
errors=[Field error in object 'person' on field 'email': rejected value [com.sample.persistence.user.model.Email#0];
codes [typeMismatch.person.email,typeMismatch.person.institutionEmployees.email,
typeMismatch.email,typeMismatch.email,
typeMismatch.email,typeMismatch.com.sample.persistence.user.model.Email,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [person.email,email];
arguments [];
default message [email]];
default message [Failed to convert property value of type 'com.sample.persistence.user.model.Email'
to required type 'com.sample.persistence.user.model.Email'
for property 'email';
nested exception is org.springframework.core.convert.ConversionFailedException:
Failed to convert from type com.sample.persistence.user.model.Email
to type #javax.persistence.OneToOne #javax.persistence.JoinColumn #com.google.gson.annotations.Expose
com.sample.persistence.user.model.Email for value
'com.sample.persistence.user.model.Email#0';
nested exception is org.springframework.dao.InvalidDataAccessApiUsageException:
Provided id of the wrong type for class com.sample.persistence.user.model.Email.
Expected: class java.lang.Long, got class java.lang.String;
nested exception is java.lang.IllegalArgumentException:
Provided id of the wrong type for class com.sample.persistence.user.model.Email.
Expected: class java.lang.Long, got class java.lang.String]],
message=Validation failed for object='person'. Error count: 1, path=/manageUsers/Ab-Soul/edit}
Any suggestion are welcome.
Thanks in advance.
1. EDIT:
The Controller-method
#RequestMapping(value = "/{login}/edit", method = RequestMethod.GET)
public ModelAndView editUserByLogin(#PathVariable("login") final String login) {
final User currentUser = UserRepository.findPersonByLogin(login);
ModelAndView mav = new ModelAndView(URL_EDIT_USER);
mav.addObject(MODEL_USER, currentUser);
return mav;
}
the scenario, the 'admin' get a list of all current user, if he clicked on the table the requestmapping-method would be called with the name of the user he has clicked.
the email class:
#Entity(name="Email")
public class Email implements Serializable
{
private static final long serialVersionUID = 6891079722082340011L;
#Id()
#GeneratedValue(strategy=GenerationType.AUTO)
#Expose
protected Long emailId;
#Expose
protected String value;
//getter/setter
#Override
public boolean equals(Object obj)
{
if(obj instanceof Email){
return value.equals(((Email) obj).getValue());
}
return false;
}
#Override
public int hashCode()
{
return value.hashCode();
}
}
2. EDIT:
Now i have change the email field of child to emailChild
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'person' on field 'child[0].emailChild':
rejected value [com.sample.persistence.user.model.Email#0];
codes [typeMismatch.person.child[0].emailChild,
typeMismatch.person.child.emailChild,
typeMismatch.child[0].emailChild,
typeMismatch.child.emailChild,
typeMismatch.emailChild,
typeMismatch.com.sample.persistence.user.model.Email,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [person.child[0].emailChild,child[0].emailChild];
arguments [];
default message [child[0].emailChild]];
default message [Failed to convert property value of type 'com.sample.persistence.user.model.Email' to required type 'com.sample.persistence.user.model.Email'
for property 'child[0].emailChild';
nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type com.sample.persistence.user.model.Email
to type #javax.persistence.OneToOne #javax.persistence.JoinColumn #com.google.gson.annotations.Expose com.sample.persistence.user.model.Email
for value 'com.sample.persistence.user.model.Email#0';
nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: Provided id of the wrong type for class com.sample.persistence.user.model.Email. Expected: class java.lang.Long, got class java.lang.String;
nested exception is java.lang.IllegalArgumentException: Provided id of the wrong type for class com.sample.persistence.user.model.Email. Expected: class java.lang.Long, got class java.lang.String]
3. EDIT:
Adding the controller method for the post:
#RequestMapping(value = "/{login}/edit", method = RequestMethod.POST)
public ModelAndView updateUser(#PathVariable("login") final String login, #ModelAttribute(MODEL_USER) final Person person, BindingResult bindingResult, final Model model) {
Person repositoryPerson = personRepository.findPersonByLogin(login);
repositoryPerson = repositoryPerson.updateWith(person);
manageUserService.updatePerson(repositoryPerson);
model.asMap().clear();
return new ModelAndView("redirect:" + URL_USERS_OVERVIEW, MODEL, model);
}
I have answered my own question.
We should remember that
the registration process works fine, it would bind fine
Only the Problem occurs by the edit process, so i have a valid Email-Object in the db, bind with a valid Child-Object and this is bind with a valid User-Object.
I have decided to build a EmailFormatter with the following implemenatation:
public class EmailFormatter implements Formatter<Email> {
#Override
public String print(Email email, Locale locale) {
return email.getValue();
}
#Override
public Email parse(String mailAddress, Locale locale) throws ParseException {
return new Email(mailAddress);
}
}
with this, i get in my Child-Instance a new Email-Object (without the id). Now i have a valid Email-Object in a valid Child-Object. Take a look at the post-Method:
#RequestMapping(value = "/{login}/edit", method = RequestMethod.POST)
public ModelAndView updateUser(#PathVariable("login") final String login, #ModelAttribute(MODEL_USER) final Person person, BindingResult bindingResult, final Model model) {
Person repositoryPerson = personRepository.findPersonByLogin(login);
repositoryPerson = repositoryPerson.updateWith(person);
manageUserService.updatePerson(repositoryPerson);
model.asMap().clear();
return new ModelAndView("redirect:" + URL_USERS_OVERVIEW, MODEL, model);
}
At this time i have the login name from the user, i get the "old" (valid) user-Object from the db. I have wrote a "merge" method, i give the "old" user object the modified user object.
Here i can get the old Email Object with id and set the email-adress just like this:
this.emailChild.setValue(modifiedChild.getemailChild().getValue());
After this i look at this solution, i notice thats really simple.
But if the binding problem occurs by a "complexer" (more then one relevant field) object then, i think, you go better white the solution that PatrickLC suggest:
#InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("emailId");
}
Related
I'm learning Spring with "Spring in action 5" and faced some problem: when i switch to Spring Data JPA from JDBC (which is 100% working correctly) in chapter 3, the code stops working as i try to open main page with taco's ingredients. I made a few logs to see what's going on and found out that method findById(String id) can't convert my value from DB (or something like that). I'm using MySQL.
I tried to call convertor's method convert(String id) by myself using #Autowired, but the only thing i discovered was that when key is wrong, another error will appear. So data is visible. I will try to provide some code here, but i'm not sure what is useful and what is not. I'm getting error on the first attemp to log something. Errors in IDE and in browser are different.
Here's full project https://github.com/thedistantblue/taco-cloud-jpa.
Here's my converter:
public class IngredientByIdConverter implements Converter<String,
Ingredient> {
private IngredientRepository ingredientRepo;
#Autowired
public IngredientByIdConverter(IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
#Override
public Ingredient convert(String id) {
log.info("In converter.convert(): "
+ingredientRepo.findById(id).toString());
Optional<Ingredient> optionalIngredient =
ingredientRepo.findById(id);
return optionalIngredient.orElse(null);
}
}
And controller class:
#Slf4j
#Controller
#RequestMapping("/design")
#SessionAttributes("order")
public class DesignTacoController {
#ModelAttribute(name = "order")
public Order order() {
return new Order();
}
#ModelAttribute(name = "taco")
public Taco taco() {
return new Taco();
}
private final IngredientRepository ingredientRepository;
private TacoRepository designRepository;
private IngredientByIdConverter converter;
#Autowired
public DesignTacoController(IngredientRepository ingredientRepository,
TacoRepository designRepository,
IngredientByIdConverter converter) {
this.ingredientRepository = ingredientRepository;
this.designRepository = designRepository;
this.converter = converter;
}
#GetMapping
public String showDesignForm(Model model) {
List<Ingredient> ingredients = new ArrayList<>();
log.info(converter.convert("CARN").getName());
log.info("in DTC: " + ingredientRepository.findAll());
ingredientRepository.findAll().forEach(i -> ingredients.add(i));
In IDE:
java.lang.NumberFormatException: For input string: "PROTEIN"
at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054) ~[na:na]
at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110) ~[na:na]
at java.base/java.lang.Double.parseDouble(Double.java:543) ~[na:na]
In browser:
There was an unexpected error (type=Internal Server Error, status=500).
For input string: "PROTEIN"; nested exception is java.lang.NumberFormatException: For input string: "PROTEIN"
org.springframework.dao.InvalidDataAccessApiUsageException: For input string: "PROTEIN"; nested exception is java.lang.NumberFormatException: For input string: "PROTEIN"
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:373)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
For Ingredient.java need to add annotation #Enumerated(EnumType.STRING) for field Type type
#Id
#NaturalId(mutable = false)
private final String id;
private final String name;
#Enumerated(EnumType.STRING)
private final Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
#Pavel 's solution does work.
I just want to point out that there is another thing, may be worth checking.
This book talks about JDBC before JPA, and it uses data.sql to insert data into table 'Ingredient'. In file data.sql, the type of 'Ingredient.Type', whose value contains 'WRAP', 'PROTEIN', etc. , is String.
However, in the example of JPA, the content of data.sql is moved to TacoCloudApplication, which is wroted in method 'dataLoader', and, in this method, you can see that it just creates Ingredient instance with Ingredient.Type(not a String).
To find the difference between two ways, you can run TacoCloudApplication, then look up Table 'Ingredient'.
The value of Type field is String, if you use the JDBC version example code. The value of Type field is Interger, if you use the JPA version example code.
in the spring MVC application, I have Question entity class
#Entity
public class Question {
#Lob
#Column(name="QUESTION_TITLE")
private String question;
...
}
I use Thymeleaf. for this field my view is bellow
<input type="text" class="form-control" id="question"
th:field="*{question}" th:value="${question}" placeholder="">
my controller save method is
#PostMapping("/save")
public String saveQuestion(Question question, BindingResult bindingResult){
questionService.save(question);
return "redirect:/admin/questions/all/";
}
but when I submit, I have got error
Failed to bind request element:
org.springframework.beans.TypeMismatchException:Failed to convert value of type 'java.lang.String' to required type com.sendit.security.model.Question';
nested exception is
org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Long] for value 'what';
nested exception is java.lang.NumberFormatException:
For input string: "what"
when I add #Convert(converter = QuestionConverter.class) attribute to the question field and implemented QuestionConverter method like bellow.
#Converter
public static class QuestionConverter implements AttributeConverter<String, Integer> {
#Override
public Integer convertToDatabaseColumn(String attribute) {
return attribute.length();
}
#Override
public String convertToEntityAttribute(Integer dbData) {
return "";
}
}
I got the error again.
I have found the solution. simply have changed "question" field name to another field name (like "title") and it works. it was caused because when in the View I want to send "question" attribute, it known like object and sent object hash value.
I'm trying to implement a partial update of the Manager entity based in the following:
Entity
public class Manager {
private int id;
private String firstname;
private String lastname;
private String username;
private String password;
// getters and setters omitted
}
SaveManager method in Controller
#RequestMapping(value = "/save", method = RequestMethod.PATCH)
public #ResponseBody void saveManager(#RequestBody Manager manager){
managerService.saveManager(manager);
}
Save object manager in Dao impl.
#Override
public void saveManager(Manager manager) {
sessionFactory.getCurrentSession().saveOrUpdate(manager);
}
When I save the object the username and password has changed correctly but the others values are empty.
So what I need to do is update the username and password and keep all the remaining data.
If you are truly using a PATCH, then you should use RequestMethod.PATCH, not RequestMethod.POST.
Your patch mapping should contain the id with which you can retrieve the Manager object to be patched. Also, it should only include the fields with which you want to change. In your example you are sending the entire entity, so you can't discern the fields that are actually changing (does empty mean leave this field alone or actually change its value to empty).
Perhaps an implementation as such is what you're after?
#RequestMapping(value = "/manager/{id}", method = RequestMethod.PATCH)
public #ResponseBody void saveManager(#PathVariable Long id, #RequestBody Map<Object, Object> fields) {
Manager manager = someServiceToLoadManager(id);
// Map key is field name, v is value
fields.forEach((k, v) -> {
// use reflection to get field k on manager and set it to value v
Field field = ReflectionUtils.findField(Manager.class, k);
field.setAccessible(true);
ReflectionUtils.setField(field, manager, v);
});
managerService.saveManager(manager);
}
Update
I want to provide an update to this post as there is now a project that simplifies the patching process.
The artifact is
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.13</version>
</dependency>
The implementation to patch the Manager object in the OP would look like this:
Controller
#Operation(summary = "Patch a Manager")
#PatchMapping("/{managerId}")
public Task patchManager(#PathVariable Long managerId, #RequestBody JsonPatch jsonPatch)
throws JsonPatchException, JsonProcessingException {
return managerService.patch(managerId, jsonPatch);
}
Service
public Manager patch(Long managerId, JsonPatch jsonPatch) throws JsonPatchException, JsonProcessingException {
Manager manager = managerRepository.findById(managerId).orElseThrow(EntityNotFoundException::new);
JsonNode patched = jsonPatch.apply(objectMapper.convertValue(manager, JsonNode.class));
return managerRepository.save(objectMapper.treeToValue(patched, Manager.class));
}
The patch request follows the specifications in RFC 6092, so this is a true PATCH implementation. Details can be found here
With this, you can patch your changes
1. Autowire `ObjectMapper` in controller;
2. #PatchMapping("/manager/{id}")
ResponseEntity<?> saveManager(#RequestBody Map<String, String> manager) {
Manager toBePatchedManager = objectMapper.convertValue(manager, Manager.class);
managerService.patch(toBePatchedManager);
}
3. Create new method `patch` in `ManagerService`
4. Autowire `NullAwareBeanUtilsBean` in `ManagerService`
5. public void patch(Manager toBePatched) {
Optional<Manager> optionalManager = managerRepository.findOne(toBePatched.getId());
if (optionalManager.isPresent()) {
Manager fromDb = optionalManager.get();
// bean utils will copy non null values from toBePatched to fromDb manager.
beanUtils.copyProperties(fromDb, toBePatched);
updateManager(fromDb);
}
}
You will have to extend BeanUtilsBean to implement copying of non null values behaviour.
public class NullAwareBeanUtilsBean extends BeanUtilsBean {
#Override
public void copyProperty(Object dest, String name, Object value)
throws IllegalAccessException, InvocationTargetException {
if (value == null)
return;
super.copyProperty(dest, name, value);
}
}
and finally, mark NullAwareBeanUtilsBean as #Component
or
register NullAwareBeanUtilsBean as bean
#Bean
public NullAwareBeanUtilsBean nullAwareBeanUtilsBean() {
return new NullAwareBeanUtilsBean();
}
First, you need to know if you are doing an insert or an update. Insert is straightforward. On update, use get() to retrieve the entity. Then update whatever fields. At the end of the transaction, Hibernate will flush the changes and commit.
You can write custom update query which updates only particular fields:
#Override
public void saveManager(Manager manager) {
Query query = sessionFactory.getCurrentSession().createQuery("update Manager set username = :username, password = :password where id = :id");
query.setParameter("username", manager.getUsername());
query.setParameter("password", manager.getPassword());
query.setParameter("id", manager.getId());
query.executeUpdate();
}
ObjectMapper.updateValue provides all you need to partially map your entity with values from dto.
As an addition, you can use either of two here: Map<String, Object> fields or String json, so your service method may look like this:
#Autowired
private ObjectMapper objectMapper;
#Override
#Transactional
public Foo save(long id, Map<String, Object> fields) throws JsonMappingException {
Foo foo = fooRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Foo not found for this id: " + id));
return objectMapper.updateValue(foo , fields);
}
As a second solution and addition to Lane Maxwell's answer you could use Reflection to map only properties that exist in a Map of values that was sent, so your service method may look like this:
#Override
#Transactional
public Foo save(long id, Map<String, Object> fields) {
Foo foo = fooRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Foo not found for this id: " + id));
fields.keySet()
.forEach(k -> {
Method method = ReflectionUtils.findMethod(LocationProduct.class, "set" + StringUtils.capitalize(k));
if (method != null) {
ReflectionUtils.invokeMethod(method, foo, fields.get(k));
}
});
return foo;
}
Second solution allows you to insert some additional business logic into mapping process, might be conversions or calculations ect.
Also unlike finding reflection field Field field = ReflectionUtils.findField(Foo.class, k); by name and than making it accessible, finding property's setter actually calls setter method that might contain additional logic to be executed and prevents from setting value to private properties.
My problem is with having Spring bind the data I get from a form to a JPA entity. The wierd part is, it works just fine if I do not look at the BindingResults. The BindingResults says there were binding errors when an empty string is passed in for the field graduation, but I know it does bind them correctly because when I don't check Hibernate updates the database perfectly. Is there a way to not have to write logic to circumnavigate the wrongly fired binding errors?
#Entity
#Table(name="child")
public class Child {
#Id
#Column(name="id")
private Integer childId;
#ManyToOne(fetch=FetchType.EAGER )
#JoinColumn(name="house", referencedColumnName="house")
private House house;
#NotNull()
#Past()
#Column(name="birthday")
private Date birthday;
#Column(name="graduation_date")
private Date graduationDay;
}
I have tried the following lines in a property editor to no avail
SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy");
registry.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
Here is the method signature for the controller method Handling the request
#Controller
#SessionAttributes(value="child")
#RequestMapping(value="child")
public class ChildModController {
#RequestMapping(value="save-child.do", params="update", method = RequestMethod.POST)
public #ResponseBody Map<String,?> updateChild(
HttpServletRequest request,
#Valid #ModelAttribute(value="child")Child child,
BindingResult results)
}
This is what I get from the BindingResult class as a message
09:01:36.006 [http-thread-pool-28081(5)] INFO simple - Found fieldError: graduationDay,
Failed to convert property value of type java.lang.String to required type java.util.Date for property graduationDay;
nested exception is org.springframework.core.convert.ConversionFailedException:
Failed to convert from type java.lang.String to type #javax.persistence.Column java.util.Date for value ';
nested exception is java.lang.IllegalArgumentException
Spring automatically binds simple object types like String and Number, but for complex objects like java.util.Date or your own defined types, you will need to use what is called a PropertyEditors or Converters, both could solve your problem.
Spring already has a predefiend PropertyEditors and Converters like #NumberFormat and #DateTimeFormat
You can use them directly on your fields like this
public class Child {
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date birthday;
#DateTimeFormat(iso=ISO.DATE)
private Date graduationDay;
#NumberFormat(style = Style.CURRENCY)
private Integer myNumber1;
#NumberFormat(pattern = "###,###")
private Double myNumber2;
}
Spring also allows you to define your own type converters which you must use it combined with Spring ConversionService
For example if you have a Color class like this
public class Color {
private String colorString;
public Color(String color){
this.colorString = color;
}
}
You would define the color converter for example like this
public class StringToColor implements Converter<String, Color> {
public Color convert(String source) {
if(source.equal("red") {
return new Color("red");
}
if(source.equal("green") {
return new Color("green");
}
if(source.equal("blue") {
return new Color("blue");
}
// etc
return null;
}
}
To check more about converters check this, also check this to know the difference between Converters and PropertyEditors
I am getting the following validation error when I try to submit my form. The dropdown box populated with values from Document.java gives this error:
Failed to convert property value of type java.lang.String to required type
testapp.domain.Document for property document_number; nested exception is
java.lang.IllegalStateException: Cannot convert value of type [java.lang.String]
to required type [testapp.domain.Document] for property document_number: no matching
editors or conversion strategy found
Is there something wrong with my mapping?
Document.java mapping
#OneToMany(mappedBy="document_number", cascade = CascadeType.ALL)
private Set<DocumentRevision> documentRevisions;
public void setDocumentRevisions(Set<DocumentRevision> documentRevisions){
this.documentRevisions = documentRevisions;
}
DocumentRevision.java mapping
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name="DOCUMENT_NUMBER")
private Document document_number;
public void setDocument_number(Document document_number){
this.document_number = document_number;
}
public Document getDocument_number(){
return document_number;
}
The relationship between the two tables is that several DocumentRevisions can have the same DocumentNumber but a single DocumentRevision can only have one DocumentNumber
Thank you for your help
/D
Edit
I am using spring 3.0 and hibernate 3. Here is a recent thread by me which includes the controller and jsp page.
Edit 2
Here is the DAO Implementation method that is supposed to save the document_number into DocumentRevision
public void saveDocumentRevision(DocumentRevision documentRevision) {
this.sessionFactory.getCurrentSession().save(documentRevision);
}
Edit 3
I believe this is the part of the code where document_number should be recorded. Do I have to use "documentNumberList" somewhere in the .POST method? How would I do that in that case?
part of controller:
#RequestMapping(value="/add", method=RequestMethod.GET)
public String getDocumentRevision(Model model) {
DocumentRevision documentRevision = new DocumentRevision();
model.addAttribute("documentRevisionAttribute", documentRevision);
model.addAttribute("documentNumberList", documentService.retrieveAllDocumentNumbers());
return "new-documnent-revision";
}
#RequestMapping(value="/add", method=RequestMethod.POST)
public String postDocumentRevision(#ModelAttribute("documentRevisionAttribute") #Valid DocumentRevision documentRevision, BindingResult result) {
if(result.hasErrors()){
return "new-document-revision";
}
documentRevisionService.createDocumentRevision(documentRevision);
return "redirect:/testapp/document-revision/list";
}
Update
I have added the following to my controller:
/**
* Property editor to map Documents from documents IDs.
*/
class DocumentPropertyEditor extends PropertyEditorSupport {
#Override
public void setAsText(String text) throws IllegalArgumentException {
Document value = documentService.retrieveDocumentNumber(text);
setValue(value);
}
#Override
public String getAsText() {
Document d = (Document) getValue();
return d != null ? String.valueOf(d.getDocumentNumber()) : "";
}
}
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Document.class, new DocumentPropertyEditor());
}
I am now getting a NullPointerException:
java.lang.NullPointerException
testapp.controller.DocumentRevisionController$DocumentPropertyEditor.getAsText(DocumentRevisionController.java:59)
Why isn't the getter in th PropertyEditor getting a value?
Edit 4
the retrieveDocumentNumber(text) method:
public Document retrieveDocumentNumber(String text){
return (Document) this.sessionFactory.getCurrentSession().get(Document.class, text);
}
Spring WebDataBinder fail to convert the String document_number to a Document instance when
populating the documentRevision model attribute. You have two posibilities:
Initialize the WebDataBinder with a PropertyEditor that can handle the conversion. Most direct, but work only for this controller.
Register a Converter<String, Document>
if choose the first, annotate a controller method with #InitBinder annotation and register
the property editor. Seems like you only need to fetch the document from database by document_number. See Customizing WebDataBinder initatization on reference documentation.
Edit
Property editor example to map Documents to/from Ids Strings.
/**
* Property editor to map Documents from documents IDs.
*/
class DocumentPropertyEditor extends PropertyEditorSupport {
#Override
public void setAsText(String text) throws IllegalArgumentException {
Long id = Long.parseLong(text);
Document value = id == 0 ? null : documentService.get(id);
setValue(value);
}
#Override
public String getAsText() {
Document d = (Document) getValue();
return d != null ? String.valueOf(d.getId()) : "";
}
}
For the second approach, look at Validation, Data Binding, and Type Conversion to see
how to create and register a Converter in the Spring ConversionService.