How to make variable set into a handler visible into another handler? - java

I have the following GetMapping handler:
#GetMapping("/auto-upgrade/{id}")
public String followJob(Model model, #PathVariable String id) {
boolean stopMessageAlreadyThere = model.asMap().containsKey("stopMessage");
if (!stopMessageAlreadyThere) {
model.addAttribute("stopMessage", null);
}
//do some stuff
return "dashboard";
}
In the dashboard.html that I return, I have the following form embedded:
<form action="#" th:action="#{/auto-upgrade/stop}" th:object="${stopRequest}" method="post" th:if="${#strings.toString(upgradeState.upgradeStatus) == 'RUNNING'}">
<input style="visibility: hidden" th:field="*{jobId}" th:value="${upgradeState.id}"/>
<p>Your mail address: <input type="email" id="stoppingEmail" oninput="saveLastInput()" th:field="*{userEmail}"></p>
<p><input type="submit" value="Request stop upgrade" />
</form>
... and then right after I have the following element:
<div th:if="${stopMessage != null}">
<p>Message for stop command: <b th:text="${stopMessage}"></b></p>
</div>
When I render the view, the stopMessage is null because it's me inserting it as such.
If I submit the form, I go through this PostMapping handler:
#PostMapping("/auto-upgrade/stop")
public String stopJob(#ModelAttribute StopRequest stopRequest, Model model) {
try {
//something that may fail
model.addAttribute("stopMessage", "Stop request accepted");
} catch (UpgradeNotRunningException | NotInGracePeriodException | StopUpgradeException e) {
model.addAttribute("stopMessage", "FAILED TO STOP: " + e.getMessage());
}
return "redirect:/auto-upgrade/" + stopRequest.getJobId();
}
So basically I'm always going to add a `stopMessage, whether successful or failing.
However, when I'm called back to my #GetMapping("/auto-upgrade/{id}") to which I redirect, the Model model is empty meaning that my value previously saved is not added.
I would have expected Model to be shared across handlers, but it doesn't look to be the case.
How can I retrieve the value of a property set into another handler? Is it possible with Thymeleaf ?
Note: for the time being, I have made stopMessage a field of my Controller class and I set it null / not null around the handlers so that I'm always able to see its value. It is a solution, but it doesn't seem to be the right way to do it. So since I'm new to Thymeleaf, I'd be happy to hear the proper way of handling such cases.

You need to use RedirectAttributes:
#PostMapping("/auto-upgrade/stop")
public String stopJob(#ModelAttribute StopRequest stopRequest,
Model model,
RedirectAttributes redirectAttributes) {
try {
//something that may fail
redirectAttributes.addFlashAttribute("stopMessage", "Stop request accepted");
} catch (UpgradeNotRunningException | NotInGracePeriodException | StopUpgradeException e) {
redirectAttributes.addFlashAttribute("stopMessage", "FAILED TO STOP: " + e.getMessage());
}
return "redirect:/auto-upgrade/" + stopRequest.getJobId();
}
That way, stopMessage is available after the redirect. (Note that if the user manually refreshes the page after the redirect, they are lost)
See https://www.baeldung.com/spring-web-flash-attributes for more info.

Related

React + Spring #GetMapping with #RequestParam simple app

I am trying to make a simple web app, that will have 3 forms for data input, Submit button and Results form.
Requirements: When I enter a data in forms and push Submit button, these parameters should go to a controller and after that React should also parse data from json response that comes from the controller and show parsed data in the Results form.
I have the Spring Controller that takes 3 parameters and return json file as a response.
But I am new with React. I've tried to use different approaches, but stuck with what way exactly I need to do it in this case. So need a help to create a simple React part.
Controller part:
#GetMapping("/current/city")
public JSONObject getCurrentPollutionDataByCityStateCountry(
#RequestParam(value = "city") String city,
#RequestParam(value = "state") String state,
#RequestParam(value = "country") String country
) {
try {
return pollutionService.getCurrentPollutionDataByCityStateCountry(city, state, country);
} catch (Exception e) {
e.printStackTrace();
}
return new JSONObject();
}
Response example:
{"date":"Tue Dec 06 22:13:32 CET 2022","no2":"48.67","pm10":"9.51","pm2_5":"5.85"}
UPDATE
Here is my App.js part:
import React, {Component} from 'react'
import './App.css'
import axios from 'axios'
class App extends Component {
constructor(props) {
super(props);
this.state = {
city: 'New York',
province: 'New York',
country: 'US',
responseData: ''
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
[name]: value
});
}
handleSubmit(event) {
axios.get('http://localhost:8080/pollution/current/city?' +
'city=' + this.state.city +
'&state='+ this.state.province +
'&country=' + this.state.country)
//not sure about setState here and what is going after that
.then(response => this.setState({responseData: response.data.date}))
//need to take all fields from response
//just alert message with returned response for now
alert('Response: ' + this.state.responseData);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
City:
<input name="city" type="text" value={this.state.city} onChange={this.handleInputChange}/>
</label>
<br/>
<label>
State:
<input name="state" type="text" value={this.state.province} onChange={this.handleInputChange}/>
</label>
<br/>
<label>
Country:
<input name="country" type="text" value={this.state.country} onChange={this.handleInputChange} />
</label>
<br/>
<input type="submit" value="Submit"/>
</form>
);
}
}
export default App
But I am not sure about my approach in general and particularly about parts where I left comments in App.js.
To be more precise with questions:
Should this approach work for my case or I need to implement some different logic?
If yes, how I can get all fields from response? I mean not only the first one (date) but no2, pm10, pm2_5 also. For now this logic can return only a first value from json.
How I can set these fields to show them in pop-up window (alert) or if question 2 will be solved, this thing also will be good in current form?
The only problem I think you need to solve is to set the data not data.date
use this:
this.setState({responseData: response.data}))
then access your data fields using
responseData.date
you can replace date with any other field.
do the same for alerts.
your code is working fine, furthermore, you can learn to use functional component instead of class, its much easier.

Spring MVC - How do I call the same URL with different mapping annotations

I have set up a Controller with two methods. The URL mappings are exactly the same, and the only difference is the mapping annotation method. One is #PostMapping and the other #DeleteMapping written in that order. However, when I try to call the #DeleteMapping method, the #PostMapping method is called.
Note that even though variables in the two URL forms are different, they have the same values. They are just from different HTML pages.
The question is how to call the desired method each time.
#PostMapping method
#Secured({"ROLE_ADMIN", "ROLE_STUDENT"})
#PostMapping("students/{username}/internships/{id}")
public String addInternship(Model model, #PathVariable("username") String username, #PathVariable("id") int id) {
/* DOES SOME STUFF
Student student = userService.getUser(username).getStudent();
Internship internship = userService.getInternship(id);
StudentInternship studentInternship = new StudentInternship(internship, student, "Sent");
internship.setNumberOfPositions(internship.getNumberOfPositions() - 1);
userService.updateInternship(internship);
student.setApplicationNumber(student.getApplicationNumber() + 1);
userService.updateStudent(student);
userService.addStudentInternship(studentInternship);
*/
return "redirect:/internships";
}
#DeleteMapping method
#Secured({"ROLE_ADMIN", "ROLE_STUDENT"})
#DeleteMapping("students/{username}/internships/{id}")
public String removeInternship(Model model, #PathVariable("username") String username, #PathVariable("id") int id) {
/* DOES SOME STUFF
Student student = userService.getUser(username).getStudent();
Internship internship = userService.getInternship(id);
int studentInternshipID = userService.getStudentInternshipByParams(student, internship).getId();
internship.setNumberOfPositions(internship.getNumberOfPositions() + 1);
userService.updateInternship(internship);
student.setApplicationNumber(student.getApplicationNumber() - 1);
userService.updateStudent(student);
userService.removeStudentInternship(studentInternshipID);
*/
return "redirect:/students/" + username + "/internships";
}
HTML
<form:form action="${pageContext.request.contextPath}/students/${username}/internships/${tempInternship.id}" method="POST">
<input type="submit" value="Request" ${disabled}/>
</form:form>
<form:form action="${pageContext.request.contextPath}/students/${tempStudentInternship.student.username}/internships/${tempStudentInternship.internship.id}" method="DELETE">
<input type="submit" value="Dismiss" />
</form:form>
Browsers only support GET and POST as http request methods. The solution is to send your form with the POST method and inject a hidden field inside the same html form called _method with your desired method as a value, in your case here, it is just DELETE. For the case of POST, just write your form as usual.
Example :
<form:form action="${pageContext.request.contextPath}/students/${tempStudentInternship.student.username}/internships/${tempStudentInternship.internship.id}" method="POST">
<input type="hidden" name="_method" value="DELETE"/>
<input type="submit" value="Dismiss" />
</form:form>
Please, have a look at this answer for creating the spring bean and then applying the mentioned form attribute inside spring:form html forms.
Only GET and POST are allowed from a FORM. You need to use AJAX to specify additional types of the request.
Hi you need to use javascript and XMLHttpRequest in order to specify the type of the request.
Here is one example I took randomly from internet:
// Delete a user
var url = "http://localhost:8080/api/v1/users";
var xhr = new XMLHttpRequest();
xhr.open("DELETE", url+'/12', true);
xhr.onload = function () {
var users = JSON.parse(xhr.responseText);
if (xhr.readyState == 4 && xhr.status == "200") {
console.table(users);
} else {
console.error(users);
}
}
xhr.send(null);

Thymeleaf dynamically create forms using th:each

I would like to know how to create forms that uses th:object for each object looped in a th:each. For example, I have the following code.
HTML
<th:block th:each="store: ${stores}">
<form th:object="${store}" th:action="#{/modify-store}">
<input th:field="*{idStorePk}"/>
<input th:field="*{name}"/>
<input th:field="*{phoneNumber}"/>
<button type="submit">Modify</button>
</form>
</th:block>
Controller
#RequestMapping(value = "/stores")
public String getIndex(Model model) {
model.addAttribute("stores", storeService.getAllStores());
return "store";
}
So, I would like to add a form for each object, but it seems that it is not possible and I get the following error.
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'store' available as request attribute
So, I decided to add a #ModelAttribute in my controller, but can't get to return the actual store.
#ModelAttribute("store")
public Store getStore(Store store) {
return store;
}
With this approach all my forms have null values. I also tried to add a #PathVariable, but can't see to bind it using th:object. Is there a solution for this?
So for anyone stuck at a similar problem. I find out a work around that might help you out. First, you can't use th:object, it simply won't cut it. Instead, do the following.
<th:block th:each="store: ${stores}">
<form class="store-form" th:action="#{/modify-store}">
<input th:name="idStorePk" th:value="${store.idStorePk}"/>
<input th:name="name" th:value="${store.name}"/>
<input th:name="phoneNumber" th:value="${store.phoneNumber}"/>
<button class="submit-button" type="submit">Modify</button>
</form>
</th:block>
Then just add something similar to the controller.
#PostMapping(value = "/modify-store")
#ResponseBody
public boolean deleteEntry(#ModelAttribute Store store) throws Exception {
// Your code here...
return true;
}
If you want to send it asynchronously then you will need to add some JS code in order for it to work. It should look something like the code below.
const forms = document.querySelectorAll('.store-form');
forms.forEach(form => {
form.addEventListener('submit', event => {
// Stop the normal form submit triggered by the submit button
event.preventDefault();
const formInputs = form.getElementsByTagName("input");
let formData = new FormData();
for (let input of formInputs) {
formData.append(input.name, input.value);
}
fetch(form.action,
{
method: form.method,
body: formData
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log(error.message))
.finally(() => console.log("Done"));
});
You're sending stores in your controller in model-attribute and on your second controller where you're submitting your form you're using store that's the reason you're getting this error. So correct the spelling error on any one of your controller. Like this :-
#RequestMapping(value = "/stores")
public String getIndex(Model model) {
model.addAttribute("stores", storeService.getAllStores());
return "store";
}
And Your second controller where you're submitting your form will be like this -
#ModelAttribute("stores")
public Store getStore(Store store) {
return store;
}

How to handle form submission error without redirecting?

I have a web form that allows user to enter information and submit. If the user submits a duplicated id field in the form, the form should stay there and prompt error information. However right now if some error information is entered, the page will be redirected to an error page showing "HTTP Status 409 - Conflict". My form is:
<form action="/myapp/rest/customer/created" onsubmit="return checkForm();" method="POST">
<table border="1">
<tr>
<td>Customer name:</td>
<td><input type="text" id="name" name="name"></td>
</tr>
<tr>
<td>Customer ID:</td>
<td><input type="text" id="id" name="id"></td>
</tr>
<tr>
<td>Customer DOB:</td>
<td><input type="text" id="dob" name="dob"></td>
</tr>
</table>
<br/>
<input type="submit" value="Submit">
</form>
<div><span id="errorDiv" class="errorDiv" ></span></div>
And the JavaScript function checkForm() is:
function checkForm() {
$.post("/myapp/rest/customer/created", function(data, status) {
if (status === "200") {
// redirect to destination
return true;
} else {
//display error information in the current form page
$("#errorDiv").html("<font color=red>ID already exists!</font>");
return false;
}
});
}
The backend service is Java REST API which catches the exception if some error information is entered and submitted:
#Path("/customer")
public class CustomerService {
#Context UriInfo uriInfo;
#Context HttpServletRequest request;
#Context HttpServletResponse response;
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
CustomerJDBCTemplate dbController = (CustomerJDBCTemplate) context.getBean("customerJDBCTemplate");
#POST
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
#Path("created")
public Response createCustomer(#FormParam("id") int id,
#FormParam("name") String name, #FormParam("dob") Date dob)
throws ServletException, IOException, WebApplicationException {
URI uri = URI.create(uriInfo.getPath());
Response r;
r = Response.created(uri).build();
try {
dbController.create(id, name, dob); //This may throw exception.
request.setAttribute("name", name);
request.setAttribute("dob", dob);
request.setAttribute("id", Integer.valueOf(id));
RequestDispatcher dispatcher = request.getRequestDispatcher("/confirm.jsp");
dispatcher.forward(request, response);
} catch (DataAccessException ex) {
throw new WebApplicationException(409);
}
return r;
}
}
So why does the page always redirect to the error page showing "HTTP Status 409 - Conflict" if user submits error information? Why the ajax form validation checkForm() doesn't work here?
Your checkForm method is asynchronious. Or rather, it makes use of an asynchronious function, $.post. $.post makes use of AJAX, and AJAX stands for Asynchronious Javascript And XML. What this means, is that the function returns immediately ($.post) before completing its task and calling the callback.
You will always want to block form submission, since you are submitting using AJAX. You can do this by returning false within checkForm, instead of within the callback provided to $.post.
For example:
function checkForm() {
$.post("/myapp/rest/customer/created", function(data, status) {
if (status === "200") {
// redirect to destination
} else {
//display error information in the current form page
$("#errorDiv").html("<font color=red>ID already exists!</font>");
}
});
return false;
}
Because $.post( is asynchronous and your returns have no effect. The form is always submitted to server and line throw new WebApplicationException(409); causes error code response.
Update:
#WouterH's suggestion should work. Make sure that you see alerts at the right moments:
function checkForm() {
$.post("/myapp/rest/customer/created", function(data, status) {
if (status === "200") {
alert("post success");
// redirect to destination
} else {
alert("post error");
//display error information in the current form page
$("#errorDiv").html("<font color=red>ID already exists!</font>");
}
});
alert("form posted");
return false;
}
Update 2:
function checkForm() {
$.post("/myapp/rest/customer/created", function(data, status) {
alert("post handler, status=" + status);
});
alert("form posted");
return false;
}

Play 2 - templates without helpers

#inputText(
signupForm("email"), '_label -> "Email",
'_help -> "Enter a valid email address."
)
How would I write this in pure html?
I have no idea how i add the value to the signupForm, so that I can use it in my controller with bindfromRequest() (in html)
Edit:
I normally used this approach
final static Form<User> signupForm = form(User.class);
and the the binding process
Form<User> filledForm = signupForm.bindFromRequest();
and my rendered form looks like this:
<div class="control-group ">
<label class="control-label" for="email">Email</label>
<div class="controls">
<input type="text" id="email" name="email" value="" >
<span class="help-inline"></span>
</div>
</div>
And this worked for me I was just curious how to use pure html, so I could create my own little helpers.
Edit2:
public static Result blank() {
return ok(form.render(signupForm));
}
and in the template itself
#(signupForm: Form[User])
Edit 3:
I don't know if this helps but the helper looks like this. (for the inputtext)
I just have no idea what this means, scala looks really cryptic to me.
#(field: play.api.data.Field, args: (Symbol,Any)*)(implicit handler: FieldConstructor, lang: play.api.i18n.Lang)
#input(field, args:_*) { (id, name, value, htmlArgs) =>
<input type="text" id="#id" name="#name" value="#value" #toHtmlArgs(htmlArgs)>
}
Use your browser to check the source coderendered by Play and copy/paste the HTML into your template.
In general the most interesting for you is proper inserting value to the manually created field:
<input type="text" name="email" value='#signupForm.field("email").value' />
It's also important to set the proper name attribut otherwise filling Form in controller will fail.
Edit:
Of course in blank action your signupForm is empty so that's normal that there's no value, in next action let's name it saveBlank, you need to fill the form with request's data, validate and save. If there are errors in form you need to render form.scala.view again with data binded to the form from previous request:
public static Result saveBlank(){
signupForm = signupForm.bindFromRequest();
if (signupForm.hasErrors()){
return badRequest(form.render(signupForm));
}
User user = new User();
user = signupForm.get();
user.save();
return ok("user saved);
}
of course if you'll want to edit existing user, you have to prefill it with data from DB ie:
public static Result edit(Long id){
signupform = signupForm.fill(User.find.byId(id));
return ok(form.render(signupForm));
}
(note: I wrote it just from top of my head, so check pls if there are no typos or errors)
Finally you don't need to use Form object in every case, you can also use DynamicForm:
public static Result saySomething(){
DynamicForm df = form().bindFromRequest();
return("you entered :" + df.get("email"));
}
or even in one line:
public static Result sayShorter(){
return("you entered :" + form().bindFromRequest().get("email"));
}

Categories