I'm working on a new app using Spring 4 and Thymeleaf 3.
My LoginController.java has the following mapping:
#RequestMapping(value = { "/", "/home", "/welcome" }, method = RequestMethod.GET)
public String index(Principal principal) {
//logs debug message
if(logger.isDebugEnabled()){
logger.debug("getWelcome is executed!");
}
return principal != null ? "home/homeSignedIn" : "home/homeNotSignedIn";
}
So, when I access the app, without being logged in the called page is home/homeNotSignedIn.html, which is posted bellow.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="fragments/app">
<head>
<title th:text="#{view.index.title}">Welcome!</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<div layout:fragment="content">
<div class="container2">
<th:block th:if="${message != null}">
<div th:replace="fragments/alert :: alertFragment (type=${#strings.toLowerCase(message.type)}, message=${message.message})"> </div>
</th:block>
<p />
<p />
Sign up
<p />
</div>
</div>
</body>
</html>
This page has a direct reference to a file called app.html which contains the layout for the application, seen bellow.
I'm having basically two problems.
1) Thymeleaf cries about this:
SEVERE: Servlet.service() for servlet [dispatcher] in context with path [/cms-stock] threw exception [Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "ServletContext resource [/WEB-INF/templates/home/homeNotSignedIn.html]")] with root cause
java.lang.IllegalArgumentException: layout:decorate/data-layout-decorate must appear in the root element of your template
2) Maybe related to 1), I get no CSS or JS loaded.
I have started this app as Spring-boot and, probably very stupid of me, I could't get it running. Then, adapted a working pom.xml from another app, and got the problem number 2, further described here.
I'm at a loss here. Please advise!
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}"}).
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 ;)
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.
I am trying to design a common error page for Spring MVC using Mustache. I am able to print out the exception type (example: Internal Server Error) using variable "error" and the exception message using the variable called "message".
My Sample Mustache Template:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Common Error</title>
<h2>Unexpected Error: </h2>
<h3>Error: {{error}}</h3>
<h3>Message: {{message}}</h3>
</head>
<body>
</body>
</html>
This shows up as:
Unexpected Error:
Error: Internal Server Error
Message: Unexpected Runtime Error Message
Question 1: How do I print out the Exception Stacktrace?
Question 2: Is there a way to print all the model variable available to the mustache template?
Rather than resolving this at the Mustache Template engine layer. I stepped back to the have Spring MVC setup the Model properly for the Mustache Error View.
I did this by using #ControllerAdvice and #ExceptionHandler.
My Controller Advice with catch-all exception handler looks like below:
#ControllerAdvice
public class ExceptionControllerAdvice {
#ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
#ExceptionHandler(Throwable.class)
public ModelAndView genericExceptionHandler(Exception ex){
String errorMessage = getMessage(ex);
String errorRootCauseMessage = getRootCauseMessage(ex);
String errorStacktrace = getStackTrace(ex);
ModelAndView mv = new ModelAndView("errors/error");
Map<String, Object> model = mv.getModelMap();
model.put("errorMessage", errorMessage);
model.put("rootCauseErrorMessage", errorRootCauseMessage);
model.put("stackTrace", errorStacktrace);
return mv;
}
}
And then my Mustache Template is as follows:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Generic Error</title>
<h3>Wahooo!!! You broke us! Well done. We will take a look at it and try to fix the issue.</h3>
<h4>What Happend?</h4><h5> {{errorMessage}}<h5>
<h4>What was the root cause? </h4><h5>{{rootCauseErrorMessage}}</h5>
<h4>Stacktrace:</h4>
<h6>{{stackTrace}}</h6>
</head>
<body>
</body>
</html>
This solved my issue.