Hi Guys!
I have been implementing service in Spring Boot which
allows users to send anonymouse questionaries to server.
I have already implemented most of the backend like adding users etc. and right now I have been struggling with one action which take answers from user and sends into server (save in database).
Object containing answers (filledSurvey) is being sent as empty. In this same logic in logging users fields from form are corectly send forward.
This endpoint displays questionary:
#RequestMapping(path = {"/try", "/try/{id}"})
public String tryCompletingSurvey(Model model, #PathVariable("id") Long id) {
Connection connection = connectionService.getConnection(id);
FilledSurvey filledSurvey = connection.getSurvey().getTemplate();
for (FilledQuestion filledQuestion : filledSurvey.getFilledQuestions()) {
filledQuestion.getFilledAnswers().get(0).setCheck(true);
}
model.addAttribute("filledSurvey", filledSurvey);
return "completing/completing";
}
This is thymeleaf html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Completing survey</title>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.1/css/all.css">
</head>
<body>
<center>
<form action="#" th:action="#{/user/surveys/finish}" th:object="${filledSurvey}" method="post">
<!-- <div th:each="question, questionStat : ${survey.getFilledQuestions()}" >-->
<!-- <p th:text="${question.getQuestion()}"></p>-->
<!-- <div th:each="answer, answerStat: ${question.getFilledAnswers()}" >-->
<!-- <input type="radio"-->
<!-- th:name="question+${questionStat.index}"-->
<!-- th:field="*{}"-->
<!-- th:value="${true}">-->
<!-- <label th:text="${answer.answer}">-->
<!-- </label>-->
<!-- </div>-->
<!-- </div>-->
<h2>Survey name: </h2>
<h3 th:text="${filledSurvey.getSurveyName()}"></h3>
<h2>Number of questions: </h2>
<h3 th:text="${filledSurvey.filledQuestions.size()}"></h3>
<div class="col-md-6">
<input type="submit" style="align-content: center" class="btn btn-primary" value=" Send ">
</div>
</form>
</center>
</body>
</html>
And this is endpoint which stores empty object from thymeleaf:
#RequestMapping(path = "/finish", method = RequestMethod.POST)
public String getHash(FilledSurvey filledSurvey) {
StringBuilder sb = new StringBuilder();
for (FilledQuestion question : filledSurvey.getFilledQuestions()) {
for (FilledAnswer answer : question.getFilledAnswers()) {
if (answer.isCheck()) sb.append(answer.getAnswer());
}
}
LocalDateTime date = LocalDateTime.now();
sb.append(date);
String hash = sb.toString();
hash = Base64.getEncoder().encodeToString(sb.toString().getBytes());
filledSurvey.setHash(hash);
surveyMagazinService.addSurveyToMagazin(filledSurvey);
return "completing/finish";
}
I changed code to automaticly mark answers for now.
This the picture of the object in the next endpoint:
filledSurvey object
I am aware that this is common question but i have been looking for the answer for a while now and couldn't figure it out. I have no errors in the console as well. I would appreciate any help or feedback.
If I understood correctly, I see following issue:
You are using a form to submit the survey data and use the th:object="${filledSurvey}" to bind the data. But there is actually not data send back to the controller, when the form is submitted, because there are no input fields defined that have the th:field attribute applied.
The request that will be send to the server on a submit, will contain form encoded data of all fields that you assign a th:field attribute to. The controller will map the form encoded data to a FilledSurvey object using java bean convention in the getHash method.
EDIT: can you try adding the #ModelAttribute annotation:
#RequestMapping(path = "/finish", method = RequestMethod.POST)
public String getHash(#ModelAttribute FilledSurvey filledSurvey) {
...
Try adding an input field like this inside your form:
<input type="hidden" th:field="*{surveyId}" >
This should give you at least an FilledSurvey object with the id set on your "/finish" endpoint. You can then use the id to fetch the survey like its done in the first code snippet.
The way you are using the th:field within your list of questions will not work, because spring cannot map this kind of structure. See https://spring.io/guides/gs/handling-form-submission/ to understand how form submission works with spring mvc.
I hope this helps a bit, best regards ;)
Related
I would like to make an api that could accept two Path variables and one of them could be optional.
In this article they said that we can achieve this by using Optional, but this is not working.
here is my Controller
#GetMapping("/users/{dateDu}/{dateAu}")
public ResponseEntity<List<User>> getAllUsersByDate(
#PathVariable LocalDate dateDu,
#PathVariable(required = false) Optional<LocalDate> dateAu,
#org.springdoc.api.annotations.ParameterObject Pageable pageable
) {
if(dateAu.isEmpty()){
dateAu = Optional.of(LocalDate.now());
}
Page<User> page = userRepository.getUsersByDate(dateDu, dateAu.get(), pageable);
HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page);
return ResponseEntity.ok().headers(headers).body(page.getContent());
}
in this image of swagger the two parameters are required even if the second one has Optional
Since I generated my spring boot app with JHipster, on postman when I don't give a the value of the Optional parameter. I got this error
<!DOCTYPE html>
<html class="no-js" lang="fr" dir="ltr">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title></title>
<meta name="description" content="Description for " />
<meta name="google" content="notranslate" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<link rel="icon" href="favicon.ico" />
<link rel="manifest" href="manifest.webapp" />
<link rel="stylesheet" href="content/css/loading.css" />
<!-- jhipster-needle-add-resources-to-root - JHipster will add new resources here -->
<base href="/">
</head>
<body>
<!--[if lt IE 9]>
<p class="browserupgrade">
You are using an <strong>outdated</strong> browser. Please upgrade your browser to improve
your experience.
</p>
<![endif]-->
<div id="root">
<div class="app-loading">
<div class="lds-pacman">
<div>
<div></div>
<div></div>
<div></div>
</div>
<div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
<div class="app-loading">
<div id="jhipster-error" style="display: none">
<!-- This content is for troubleshooting purpose and will be removed when app renders -->
<h1>An error has occurred :-(</h1>
<h2>Usual error causes</h2>
<ol>
<li>
You started the application from an IDE and you didn't run
<code style="color: red">npm start</code> or
<code style="color: red">npm run webapp:build</code>.
</li>
<li>
You had a network error while running <code style="color: red">npm install</code>. If you are
behind a corporate proxy, it is
likely that this error was caused by your proxy. Have a look at the JHipster error logs, you
will probably have the cause of
the error.
</li>
<li>
You installed a Node.js version that doesn't work with JHipster: please use an LTS (long-term
support) version, as it's the
only version we support.
</li>
</ol>
<h2>Building the client side code again</h2>
<p>If you want to go fast, run <code style="color: red">./mvnw</code> to build and run everything.</p>
<p>If you want to have more control, so you can debug your issue more easily, you should follow the
following steps:</p>
<ol>
<li>Install npm dependencies with the command <code style="color: red">npm install</code></li>
<li>
Build the client with the command <code style="color: red">npm run webapp:build</code> or
<code style="color: red">npm start</code>
</li>
<li>Start the server with <code style="color: red">./mvnw</code> or using your IDE</li>
</ol>
<h2>Getting more help</h2>
<h3>If you have a question on how to use JHipster</h3>
<p>
Go to Stack Overflow with the
<a href="http://stackoverflow.com/tags/jhipster" target="_blank"
rel="noopener noreferrer">"jhipster"</a> tag.
</p>
<h3>If you have a bug or a feature request</h3>
<p>
First read our
<a href="https://github.com/jhipster/generator-jhipster/blob/main/CONTRIBUTING.md" target="_blank"
rel="noopener noreferrer">contributing guidelines</a>.
</p>
<p>
Then, fill a ticket on our
<a href="https://github.com/jhipster/generator-jhipster/issues/new/choose" target="_blank"
rel="noopener noreferrer">bug tracker</a>, we'll be happy to resolve your issue!
</p>
<h3>If you want to chat with contributors and other users</h3>
<p>
Join our chat room on
<a href="https://gitter.im/jhipster/generator-jhipster" target="_blank"
rel="noopener noreferrer">Gitter.im</a>. Please note
that this is a public chat room, and that we expect you to respect other people and write in a
correct English language!
</p>
<!-- end of troubleshooting content -->
</div>
</div>
</div>
<noscript>
<h1>You must enable JavaScript to view this page.</h1>
</noscript>
<script type="text/javascript">
// show an error message if the app loading takes more than 4 sec
window.onload = function () {
setTimeout(showError, 4000);
};
function showError() {
var errorElm = document.getElementById('jhipster-error');
if (errorElm && errorElm.style) {
errorElm.style.display = 'block';
}
}
</script>
<!-- uncomment this for adding service worker
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/service-worker.js')
.then(function () {
console.log('Service Worker Registered');
});
});
}
</script>
-->
<!-- Google Analytics: uncomment and change UA-XXXXX-X to be your site's ID.
<script>
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
e=o.createElement(i);r=o.getElementsByTagName(i)[0];
e.src='//www.google-analytics.com/analytics.js';
r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
ga('create','UA-XXXXX-X');ga('send','pageview');
</script>-->
<script defer src="app/vendors.bundle.js"></script>
<script defer src="app/main.bundle.js"></script>
</body>
</html>
I would appreciate if any one could help me to understand why this is happening and/or how I could solve it. Thank you in advance.
This is a composite question. I found some issues in your question.
First, you should change your path to
#GetMapping(value = {"/users/{dateDu}", "/users/{dateDu}/{dateAu}"})
Otherwise you will get 404 error if you don't provide dateAu in your request.
Second, you should add #DateTimeFormat(iso = DateTimeFormat.ISO.DATE) to your parameters to tell Spring how to convert String to LocalDate. Otherwise you will get a Bad Request with error message.
Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDate'
You didn't notice this issue because Swagger have already handled it for you, I guess.
I try to fix your code:
#GetMapping(value = {"/users/{dateDu}", "/users/{dateDu}/{dateAu}"})
public ResponseEntity<List<User>> getAllUsersByDate(
#PathVariable #DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate dateDu,
#PathVariable(required = false) #DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Optional<LocalDate> dateAu,
#org.springdoc.api.annotations.ParameterObject Pageable pageable) {
if (dateAu.isEmpty()) {
dateAu = Optional.of(LocalDate.now());
}
Page<User> page = userRepository.getUsersByDate(dateDu, dateAu.get(), pageable);
HttpHeaders headers = PaginationUtil
.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page);
return ResponseEntity.ok().headers(headers).body(page.getContent());
}
I think it should be work if you use postman to test.
Third, according to this post, all of #PathVariable MUST be required in Swagger. It means your question is nothing to do with combination of #PathVariable and Optional in Java. It's a spec/limitation of Swagger. Thus, if you really want to use Swagger to test your API, you have 2 choices:
Split it to 2 endpoints, as #tucuxi said.
Use QueryString style instead.
Last, it seems like you probably don't need an Optional. Optional is designed to be used as a return type in most case. But this is a controversial design issue. So, if you really want to use Optional, you can follow the best practice described by this excellent article
Maybe try adding the 2 paths to the #GetMapping?
#GetMapping("/users/{dateDu}", "/users/{dateDu}/{dateAu}")
Not sure if that solves your problem, but you should probably remove the false flag in the annotation of your param:
#PathVariable Optional<LocalDate> dateAu and add both paths: #GetMapping(value = {"/users/{dateDu}", "/users/{dateDu}/{dateAu}"}).
I am creating a Spring boot application with a thymeleaf front-end. I am trying to create an object of type "expense" (seen in the code snippet below) but, whenever I call the POST operation on the correct path, I am given a 400 bad request errors message on my app and a "An error happened during template parsing (template: "ServletContext resource [/WEB-INF/views/error.html]")" in the console.
The entity below represents the data type I am trying to create using the form. To create this object, only the variables expenses_id, expenses_name and expenses_date are required. I was able to create one of these objects with null values in the remaining fields.
#Entity(name = "expenses")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Expenses {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long expenses_id;
private String expenses_name;
private Integer expenses_amount;
private Date expenses_date;
#ManyToOne
#JoinTable(
name = "budget_expenses",
joinColumns = #JoinColumn(name = "expenses_id"),
inverseJoinColumns = #JoinColumn(name = "budgets_id"))
private Budgets budget;
//Gettes setters and constructor
}
The snippet below shows the template with my form
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>Expense Creation</title>
</head>
<body>
<div class="container">
<div class="container">
<div th:insert="fragments/navbar.html"> </div>
<div class="jumbotron">
<div class="row">
<h3>Expense Creation</h3>
</div>
<form action="#" th:action="#{/budgets/{id}/add-expense(id = ${budgetId})}" th:object="${expense}" method="post">
<p>Name: <input type="text" th:field="*{expenses_name}" /></p>
<p>Amount: <input type="number" th:field="*{expenses_amount}" /></p>
<p>Date: <input type="text" th:field="*{expenses_date}" /></p>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
</form>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</div>
</body>
</html>
The snippet below contains the GET and POST operations. The GET manages to provide the correct view template with the form in question. The problem comes from the POST mapping. As soon as I click "submit" the errors happen. I have done quite a lot of debugging and I believe that the problem is with the #ModelAttribute mainly because it is the component which breaks the application anytime it is present, I just don't understand how to correct it. I have verified the paths and made sure that the GET and POST are being done to the correct mapping. I have verified the variables to make sure they have the correct names and correspondence. I am at a loss as to why this is happening, even more so due to the fact that I have a similar creation via form dynamic with another object just a few lines above this code.
#GetMapping("{id}/add-expense")
public String expensesForm(Model model, #PathVariable Long id){
model.addAttribute("expense", new Expenses());
model.addAttribute("budgetId", id);
return "expenseForm";
}
#PostMapping("{id}/add-expense")
public String expenseSubmitted(#ModelAttribute Expenses expense, #PathVariable Long id, Model model){
Budgets budget = budgetsRepository.getById(id);
if(budget != null){
budget.addExpense(expense);
expense.setBudget(budget);
expensesRepository.saveAndFlush(expense);
}
else{
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "not all variables there");
}
model.addAttribute("expense", expense);
return "expenseResult";
}
If someone would be able to find what I am missing, I'd much appreciate it.
EDIT:
I saw this in the stack trace:
Caused by: java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/views/error.html]
Which is strange since I do not have a error.html. I tried to rename the homing page to "error.html" and it stopped giving me the 400 response even though the overall behaviour is still wrong.
there is an error in the name of the object in your HTML template:
th:object="${expense}"
where do you use the class object:
public class Expenses
change in you HTML expense to expenses.
My thymeleaf form is not picking up the input longUrl. When I run the application on debug, it hits the post request below with "". How can I make it pick up the form input and send it over in the body?
Thymeleaf Form:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="POST" th:action="#{create_short_url_thymeleaf}">
<h1>Hit Enter to Shorten Your Long Url</h1>
<input type="text" th:name="longUrl"/>
</form>
</body>
</html>
Even when I try to manually populate the value, it still doesn't make it into the controller
<input type="text" name="longUrl" value="somelongurl"/>
Controller Code:
#Controller
#RequestMapping("/api/v1")
public class UrlShorteningController {
#GetMapping("/create_short_url")
public String newShortUrl(Model model) {
model.addAttribute("longUrl",
"");
return "some-form";
}
#PostMapping("/create_short_url_thymeleaf")
ResponseEntity<String> newShortUrlFromThymeleaf(#ModelAttribute String longUrl) {
// Running the application on debug, I make it here, but the longUrl is empty.
....
}
try to replace
th:action="#{create_short_url_thymeleaf} with
th:action="#{/create_short_url_thymeleaf}
Ah, I solved it. I needed to swap that #ModelMapper annotation for a #RequestBody and now it works.
I am trying to get a list of objects that are sent to the server from the html form as parameters for my list, then I will loop through those entries and then return them through the springboot th:each. But it doesn't seem to be working at all. On load the form appears but when I enter a value in it, then it returns an error page and the URL however turns:
http://localhost:8080/#%7B/%7D?%24%7Bcontent%7D=hello
this output in eclipse says:
Expression "content" is not valid: only variable expressions ${...} or selection expressions *{...} are allowed in Spring field bindings
Note: content here is the value property in my form.
My controller looks like this:
#Controller
public HelloList() {
this.addUs = new ArrayList <>();
}
#RequestMapping("/")
public String getlist(#RequestParam (required = false) String content, Model model) {
if (content != null && !content.trim().isEmpty()) {
this.addUs.add(content);
}
model.addAttribute("list",addUs);
return "index";
}
the index.html looks like this
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Insert title here</title>
</head>
<body>
<div>
<ul>
<li th:each="amHere: ${addUs}">
<span th:text="${amHere}">hello! world</span>
</li>
</ul>
<form action="#{/}" method="GET">
<input type="text" name="content"/>
<input type="submit" value="Submit"/>
</form>
</div>
</body>
</html>
This might be a duplicate but it seems like most of the solutions I came across are not helping. So any help is mostly appreciated. Thank you in advance.
Turns out I was missing the initialization of my list in the constructor. I initialized the list by adding a value to it first in the constructor like this.
this.addUs.add("Hello World");
Because the #RequestMapping is mapped to the home path in my case index.html, any request gets sent there automatically.
working example
action = "#{/}" should be th:action="#{/}". That's the reason you're seeing the weird url (because it's url encoding #{/}). Thymeleaf only evaluates expressions that start with th:.
I'm not sure about the other error. It looks like the html you've pasted doesn't match up with the error you are getting.
If you urldecode http://localhost:8080/#%7B/%7D?%24%7Bcontent%7D=hello, you get http://localhost:8080/#{/}?${content}=hello, which doesn't line up with your form.
How do I hook up an angularjs web form with a spring mvc REST controller, so that only a small fragment of a page is refreshed (instead of the entire page) when the form is submitted?
In this case, only a string is refreshed in the page instead of refreshing the entire page. Specifically, how do I alter the code below so that the only element on the page that refreshes from the server after form submit is <H1>${person.answer}</H1>.
NOTE: the code below has been edited to adhere to #Cotta's suggestions. I am replacing the original code with #Cotta's suggestions instead of appending because I want this to be easy to read.
index.html is:
<!DOCTYPE html>
<html>
<head>
<!-- CSS ===================== -->
<!-- load bootstrap -->
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
<style>
body { padding-top:30px; }
</style>
<!-- JS ===================== -->
<!-- load angular -->
<script src="http://code.angularjs.org/1.2.6/angular.js"></script>
<script src="app.js"></script>
</head>
<!-- apply angular app and controller to our body -->
<body ng-app="validationApp" ng-controller="mainController">
<div class="container">
<div class="col-sm-8 col-sm-offset-2">
<!-- PAGE HEADER -->
<div class="page-header"><h1>What is your name?</h1></div>
<!-- FORM -->
<!-- pass in the variable if our form is valid or invalid -->
<form name="userForm" ng-submit="submitForm(userForm.$valid)" novalidate> <!-- novalidate prevents HTML5 validation since we will be validating ourselves -->
<!-- NAME -->
<div class="form-group">
<label>Name</label>
<input type="text" name="name" class="form-control" ng-model="name" required>
</div>
<!-- SUBMIT BUTTON -->
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div><!-- col-sm-8 -->
</div><!-- /container -->
</body>
</html>
<!-- example from: https://scotch.io/tutorials/angularjs-form-validation -->
app.js is:
var validationApp = angular.module('validationApp', ['ngResource']);
validationApp.controller('mainController', ['$scope'], '$resource', function($scope, $resource) {
// function to submit the form after all validation has occurred
$scope.submitForm = function(isValid) {
// check if form is valid before sending data to the server
if (isValid) {
var NameCheck = $resource('/api/namecheck', {});
var result = NameCheck.get();
console.log(result.answer);
}
};
});
RingRestController.java is:
#RestController
#RequestMapping("/api")
public class RingRestController {
#RequestMapping(value = "/namecheck", method = RequestMethod.POST)
public #ResponseBody Person create(#RequestBody Person person) {
String answer = "";
if (person.getName().equals("Frodo") || person.getName().equals("frodo")){
answer = "Frodo, would you please hold onto this ring for me?";
}
else {
answer = "Thank you for telling us your name.";
}
person.setTransientAnswer(answer);
return person;
}
}
NOTE: this uses the same core project as the angularjs branch of the spring petclinic sample app (I deleted everything in /src/main/webapp and built up new angularjs within the spring project shell. Thus, it uses the same spring.xml config files, which might be related to a solution. You can read the spring config files by clicking on this link.
You may solve this by:
Include ngResource in your application
var validationApp = angular.module('validationApp', ['ngResource']);
Inject $resource on your controller
validationApp.controller('mainController', ['$scope', '$resource', function($scope, $resource) { ... }]);
Then you may use $resource to create an object responsible for sending your requests inside submitForm:
var NameCheck = $resource('http://url-to-service', {});
var result = NameCheck.get();
console.log(result.answer);
Remember to include angular-resource.js file.
You may also do something similiar with $http and a few lines of code more.