I have added a new attribute Pan (pancard number) on the registration form. Validations are also working fine. But if I hit the register tab, I get an error like
de.hybris.platform.servicelayer.exceptions.ModelSavingException: [de.hybris.platform.servicelayer.interceptor.impl.MandatoryAttributesValidator#246420ba]:missing values for [pan] in model CustomerModel () to create a new Customer] with root cause de.hybris.platform.servicelayer.interceptor.impl.MandatoryAttributesValidator$MissingMandatoryAttributesException: [de.hybris.platform.servicelayer.interceptor.impl.MandatoryAttributesValidator#246420ba]:missing values for [pan] in model CustomerModel () to create a new Customer
To resolve this error I tried modifier optional = "true" in items.xml doing that above error got resolved but I am not able to store the value for the pan.
So please help me to solve both issues customermodel error and storing value in the database.
You need to extend RegisterForm, RegisterData, RegistrationPageController and CustomerFacade to transfer new field value to model.
Before I jump into the solution, I'd like to clarify my approach:
A new change request (CR) is needed in the default (OOTB) registration functionality (or it can be any other functionality in SAP CC). The CR includes UI and data model changes. Basically, adding new field in the registration form, then passing it to CustomerModel to persist it in the database.
Now to the solution:
I'm assuming that you managed to add the new form registration field in the UI, hence, I'm proceeding with java code side.
The method for the registration is in LoginPageController.java class.
#RequestMapping(value = "/register", method = RequestMethod.POST)
public String doRegister(#RequestHeader(value = "referer", required = false)
final String referer, final RegisterForm form, final BindingResult bindingResult, final Model model,
final HttpServletRequest request, final HttpServletResponse response, final RedirectAttributes redirectModel)
throws CMSItemNotFoundException
{
getRegistrationValidator().validate(form, bindingResult);
return processRegisterUserRequest(referer, form, bindingResult, model, request, response, redirectModel);
}
First of all, you need to add the new attribute pan to RegisterForm.java by extending the original form.
public class CustomRegisterForm extends RegisterForm
{
private String pan;
public String getPan() {
return pan;
}
public void setPan(String pan) {
this.pan = pan;
}
}
And you need to replace the parameter final RegisterForm form with final CustomRegisterForm formin above doRegister method.
Next, you need to add a new property pan to RegisterData bean in custom-beans.xml file.
<bean class="de.hybris.platform.commercefacades.user.data.RegisterData">
<property name="pan" type="String"/>
</bean>
Then you need to write your a custom processRegisterUserRequest method that has the parameter final CustomRegisterForm form instead of final RegisterForm form.
The content of the method will be almost the same with adding one extra line to set the value of the new attribute pan in RegisterData.
final RegisterData data = new RegisterData();
...
...
data.setPan(form.getPan());
Lastly, you need to extend the default implementation of DefaultCustomerFacade.java (let's say DefaultCustomCustomerFacade.java) and override the bean definition in custom-spring.xml file.
<alias name="defaultCustomCustomerFacade" alias="b2bCustomerFacade"/>
<bean id="defaultCustomCustomerFacade" class="xxx.yyy.uuu.DefaultCustomCustomerFacade" parent="defaultB2BCustomerFacade">
</bean>
In your DefaultCustomCustomerFacade.java you mainly will override two methods which are register and setCommonPropertiesForRegister
In setCommonPropertiesForRegister you will set the value of the new attribute pan in CustomerModel.
protected void setCommonPropertiesForRegister(final RegisterData registerData, final CustomerModel customerModel)
{
...
...
...
customerModel.setPan(registerData.getPan());
}
As the CustomerModel.Pan value is set now, when the CustomerModel is saved, the value of pan will be persisted in the database.
Related
I have a question regarding Thymeleaf and Spring Boot. I'm creating a form wizard and I would like to have the same object passed between multiple controllers, so that the object (SimpleCharacter) stores each time the value from each page.
What I have right now is, that with each endpoint, I get a new object created that "forgets" what I wanted to store from the previous page. How can I achieve that to have the same instance of the object passed between endpoints and in the end fully completed object with fields from each previous endpoint?
private static final String CHARACTER = "character";
#GetMapping(value = "/new-char/01_race")
public String showCharWizRace(Model model) {
CharacterDto character = new SimpleCharacter();
model.addAttribute(CHARACTER, character);
return "new-char/01_race";
}
#PostMapping(value = "/new-char/02_class")
public String showCharWizClass(Model model, #ModelAttribute CharacterDto character) {
model.addAttribute(CHARACTER, character);
model.addAttribute("classes", charClassService.findAll());
return "new-char/02_class";
}
#PostMapping(value = "/new-char/03_attributes")
public String showCharWizAttributes(Model model, #ModelAttribute CharacterDto character) {
model.addAttribute(CHARACTER, character);
return "new-char/03_attributes";
}
Thank you very much for all valuable hints and help. I've searched the Web, but couldn't find anything useful to point me in the right direction.
EDIT: But if you make CharacterDto have more fields for example race, class, attributes and use each time only one page (one form) to provide one field, spring "forgets" the other property when opening the next form. For example: 1st page: race is set, 2nd page (no race field existing here) class is set but in this place the previously set race had been already forgotten.
CharacterDto fields, that should be filled step by step on each page:
private String race;
private String charClass;
private int strength;
private int endurance;
private int dexterity;
private int charisma;
private int intelligence;
private int perception;
private String name;
private String surname;
private String description;
private String title;
private String avatar;
First, your character field are inside a spring form?
If yes, you also could to store your variable in a hidden field and pass this by #RequestParam.
Follow a example:
<input th:field="*{character}" name="character"/>
And in your controller method add a request parameter variable
showCharWizClass(#RequestParam(value = "character", required = false) String character, otherVariables){}
If it doesn't work, you also try to use something like a template strategy with session.
Putting your variable in a session scope, changing the variable with each request and removing it on last access.
Here a good link about access data from templates:
https://www.thymeleaf.org/doc/articles/springmvcaccessdata.html
UPDATE
You need to combine Model and Session Attributes in your workflow pages.
In your controller add a SessionAttribute pointing to the DTO that is using, like this:
#Controller
#SessionAttributes("character")
public class WizardController { ..
And when you have finished your flow, you can end session attributes this way.
#GetMapping(value = "/new-char/04_clear")
public String clearSession(SessionStatus sessionStatus) {
sessionStatus.setComplete();
return "new-char/04_clear";
}
If you look at my example code I add a new page to clean session and restart a form with a default DTO values.
After going through some tutorials and initial document reading from the docs.spring.org reference I understood that it is created in the controller of a POJO class created by the developer.
But while reading this I came across the paragraph below:
An #ModelAttribute on a method argument indicates the argument should be retrieved from the model. If not present in the model, the argument should be instantiated first and then added to the model. Once present in the model, the argument's fields should be populated from all request parameters that have matching names. This is known as data binding in Spring MVC, a very useful mechanism that saves you from having to parse each form field individually.
#RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(#ModelAttribute Pet pet) {
}
Spring Documentation
In the paragraph what is most disturbing is the line:
"If not present in the model ... "
How can the data be there in the model? (Because we have not created a model - it will be created by us.)
Also, I have seen a few controller methods accepting the Model type as an argument. What does that mean? Is it getting the Model created somewhere? If so who is creating it for us?
If not present in the model, the argument should be instantiated first and then added to the model.
The paragraph describes the following piece of code:
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
} else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
...
}
}
...
mavContainer.addAllAttributes(attribute);
(taken from ModelAttributeMethodProcessor#resolveArgument)
For every request, Spring initialises a ModelAndViewContainer instance which records model and view-related decisions made by HandlerMethodArgumentResolvers and HandlerMethodReturnValueHandlers during the course of invocation of a controller method.
A newly-created ModelAndViewContainer object is initially populated with flash attributes (if any):
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
It means that the argument won't be initialised if it already exists in the model.
To prove it, let's move to a practical example.
The Pet class:
public class Pet {
private String petId;
private String ownerId;
private String hiddenField;
public Pet() {
System.out.println("A new Pet instance was created!");
}
// setters and toString
}
The PetController class:
#RestController
public class PetController {
#GetMapping(value = "/internal")
public void invokeInternal(#ModelAttribute Pet pet) {
System.out.println(pet);
}
#PostMapping(value = "/owners/{ownerId}/pets/{petId}/edit")
public RedirectView editPet(#ModelAttribute Pet pet, RedirectAttributes attributes) {
System.out.println(pet);
pet.setHiddenField("XXX");
attributes.addFlashAttribute("pet", pet);
return new RedirectView("/internal");
}
}
Let's make a POST request to the URI /owners/123/pets/456/edit and see the results:
A new Pet instance was created!
Pet[456,123,null]
Pet[456,123,XXX]
A new Pet instance was created!
Spring created a ModelAndViewContainer and didn't find anything to fill the instance with (it's a request from a client; there weren't any redirects). Since the model is empty, Spring had to create a new Pet object by invoking the default constructor which printed the line.
Pet[456,123,null]
Once present in the model, the argument's fields should be populated from all request parameters that have matching names.
We printed the given Pet to make sure all the fields petId and ownerId had been bound correctly.
Pet[456,123,XXX]
We set hiddenField to check our theory and redirected to the method invokeInternal which also expects a #ModelAttribute. As we see, the second method received the instance (with own hidden value) which was created for the first method.
To answer the question i found few snippets of code with the help of #andrew answer. Which justify a ModelMap instance[a model object] is created well before our controller/handler is called for specific URL
public class ModelAndViewContainer {
private boolean ignoreDefaultModelOnRedirect = false;
#Nullable
private Object view;
private final ModelMap defaultModel = new BindingAwareModelMap();
....
.....
}
If we see the above snippet code (taken from spring-webmvc-5.0.8 jar). BindingAwareModelMap model object is created well before.
For better Understanding adding the comments for the class BindingAwareModelMap
/**
* Subclass of {#link org.springframework.ui.ExtendedModelMap} that automatically removes
* a {#link org.springframework.validation.BindingResult} object if the corresponding
* target attribute gets replaced through regular {#link Map} operations.
*
* <p>This is the class exposed to handler methods by Spring MVC, typically consumed through
* a declaration of the {#link org.springframework.ui.Model} interface. There is no need to
* build it within user code; a plain {#link org.springframework.ui.ModelMap} or even a just
* a regular {#link Map} with String keys will be good enough to return a user model.
*
#SuppressWarnings("serial")
public class BindingAwareModelMap extends ExtendedModelMap {
....
....
}
I worked out a concept to conditionally validate using JSR 303 groups. "Conditionally" means that I have some fields which are only relevant if another field has a specific value.
Example: There is an option to select whether to register as a person or as a company. When selecting company, the user has to fill a field containing the name of the company.
Now I thought I use groups for that:
class RegisterForm
{
public interface BasicCheck {}
public interface UserCheck {}
public interface CompanyCheck {}
#NotNull(groups = BasicCheck.class)
private Boolean isCompany
#NotNull(groups = UserCheck.class)
private String firstName;
#NotNull(groups = UserCheck.class)
private String lastName;
#NotNull(groups = CompanyCheck.class)
private String companyName;
// getters / setters ...
}
In my controller, I validate step by step depending on the respective selection:
#Autowired
SmartValidator validator;
public void onRequest(#ModelAttribute("registerForm") RegisterForm registerForm, BindingResult result)
{
validator.validate(registerForm, result, RegisterForm.BasicCheck.class);
if (result.hasErrors()
return;
// basic check successful => we can process fields which are covered by this check
if (registerForm.getIsCompany())
{
validator.validate(registerForm, result, RegisterForm.CompanyCheck.class)
}
else
{
validator.validate(registerForm, result, RegisterForm.UserCheck.class);
}
if (!result.hasErrors())
{
// process registration
}
}
I only want to validate what must be validated. If the user selects "company" fills a field with invalid content and then switches back to "user", the invalid company related content must be ignored by the validator. A solution would be to clear those fields using Javascript, but I also want my forms to work with javascript disabled. This is why I totally like the approach shown above.
But Spring breaks this idea due to data binding. Before validation starts, Spring binds the data to registerForm. It adds error to result if, for instance, types are incompatible (expected int-value, but user filled the form with letters). This is a problem as these errors are shown in the JSP-view by <form:errors /> tags
Now I found a way to prevent Spring from adding those errors to the binding result by implementing a custom BindingErrorProcessor. If a field contains null I know that there was a validation error. In my concept null is not allowed - every field gets annotated with #NotNull plus the respective validation group.
As I am new to Spring and JSR-303 I wonder, whether I am totally on the wrong path. The fact that I have to implement a couple of things on my own makes me uncertain. Is this a clean solution? Is there a better solution for the same problem, as I think this is a common problem?
EDIT
Please see my answer here if you are interested in my solution in detail: https://stackoverflow.com/a/30500985/395879
You are correct that Spring MVC is a bit picky in this regard,and it is a common problem. But there are work-arounds:
Make all your backing fields strings, and do number/date etc conversions and null checks manually.
Use JavaScript to set fields to null when they become irrelevant.
Use JavaScript to validate fields when they are entered. This will fix almost all of your problems.
Good luck!
I know this question is old, but I came upon it looking for an answer for a different situation.
I think for your situation you could use inheritance for the forms and then use two controller methods:
The forms would look like this:
public class RegistrationForm
{
// Common fields go here.
}
public class UserRegistrationForm
extends RegistrationForm
{
#NotNull
private String firstName;
#NotNull
private String lastName;
// getters / setters ...
}
public class CompanyRegistrationForm
extends RegistrationForm
{
#NotNull
private String companyName;
// getters / setters ...
}
The controller methods would look like this:
#RequestMapping(method = RequestMethod.POST, params = "isCompany=false")
public void onRequest(
#ModelAttribute("registerForm") #Valid UserRegistrationForm form,
BindingResult result)
{
if (!result.hasErrors())
{
// process registration
}
}
#RequestMapping(method = RequestMethod.POST, params = "isCompany=true")
public void onRequest(
#ModelAttribute("registerForm") #Valid CompanyRegistrationForm form,
BindingResult result)
{
if (!result.hasErrors())
{
// process registration
}
}
Notice that the #RequestMapping annotations include a params attribute so the value of the isCompany parameter determines which method is called.
Also notice that the #Valid annotation is place on the form parameter.
Finally, no groups are needed in this case.
I have a JSF page where users can enter their car into my database. The form has three input fields:
Manufacturer
Model
Registration
I am using PrimeFaces 3.0.M2 and both the Manufacturer and Model field are autocomplete input fields:
<p:autoComplete id="manufacturer"
minQueryLength="3"
completeMethod="#{carController.completeManufacturer}"
forceSelection="true"
value="#{carController.manufacturer}" />
The field for the model looks the same, with slightly different values obviously.
The managed bean looks as follows (slightly abbreviated):
private String manufacturer;
private String model;
private String registration;
public List<String> completeManufacturer(String query) {
List<String> ms = new ArrayList<String>();
for (Manufacturer m : manufacturerFacade.findAllByName(query)) {
ms.add(m.getLongName());
}
return ms;
}
public List<String> completeModel(String query) {
List<String> ms = new ArrayList<String>();
for (Model m :
modelFacade.findAllByManufacturer(manufacturerFacade.findByName(manufacturer))) {
ms.add(m.getShortName());
}
return ms;
}
The problem lies in completing the model field. I need this field to only display autocompletion results based on the selected manufacturer, but the manufacturer String in the managed bean does not get populated until the entire form is submitted, so I cannot look the models up that are associated with the selected manufacturer.
How would I go about submitting only the manufacturer field, without submitting the entire form, so I can look the models of the selected manufacturer up?
Thanks!
Just add a selectListener, like so:
<p:autoComplete id="manufacturer"
minQueryLength="3"
completeMethod="#{carController.completeManufacturer}"
forceSelection="true"
selectListener="#{carController.manufacturerSelected}"
value="#{carController.manufacturer}" />
and then in the controller:
public void manufacturerSelected(SelectEvent vce) {
String nameOfSelected = vce.getObject().toString();
// whatever logic comes to your mind
}
You could add an extra ajax handler to the manafucturer input field and then handle the onchange event. In the server-side handler, simply remember the new value in your backing bean.
If you then put your backing bean in the view scope, the ajax requests originating from the model input field will get the same instance and you have direct access to the manufacturer field that you previously remembered.
I have a data model that is something like this:
public class Report {
// report owner
private User user;
... typical getter setter ...
}
public class User {
... omitted for clarity
}
What happens is when a report is created, the current user is set to the report user object. When the report is edited, the spring controller handling the POST request is receiving a report where the user object is null. Here is what my controller looks like:
#Controller
#RequestMapping("/report")
public class ReportController {
#RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
public String editReport(#PathVariable Long id, Model model) {
Report r = backend.getReport(id); // fully loads object
model.addAttribute("report", report);
return "report/edit";
}
#RequestMapping(value = "/edit/{id}", method = RequestMethod.POST)
public String process(#ModelAttribute("report") Report r) {
backend.save(r);
return "redirect:/report/show" + r.getId();
}
}
I ran things throw the debugger and it looks like in the editReport method the model object is storing the fully loaded report object (I can see the user inside the report). On the form jsp I can do the following:
${report.user.username}
and the correct result is rendered. However, when I look at the debugger in the process method, the passed in Report r has a null user. I don't need to do any special data binding to ensure that information is retained do I?
It seems that unless the object being edited is stored in the #SessionAttributes, then spring will instantiate a new object from the information included in the form. Tagging the controller with #SessionAttributes("report") resolved my issue. Not sure of the potential impact of doing so however.