Why Optional and #PathVariable combination is not supported by Spring/java? - java

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}"}).

Related

Getting a 400 bad request response when submitting a form in Thymeleaf to my spring boot application

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.

Using a form to display model objects in Thymeleaf. Is there a better way to do this?

been trying to get comfortable with Spring Boot/Thymeleaf/frontend development in general. I'm currently using a table on the frontend to display a list of objects from the backend, as well as a form to allow users to view properties of each item in the list.
I feel like the way I'm doing this is really weird and feel that there could be a better way. Would appreciate any tips/feedback, thank you.
HTML/Thymeleaf
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Gambler Card List</title>
<link th:href="#{/css/styles.css}" rel="stylesheet" />
</head>
<body>
<h1>Gambler Card List</h1>
<div class="cardlist-wrapper">
<div class="cardlist-names">
<form action="/gambler/cardlist" method="get">
<table class="cardlist-table">
<tr th:each="card : ${cardList.getCardList()}">
<td>
<input th:class="card-btn"
th:attr="id=${{card.getId()} == {currentCard.getId()} ? 'selected' : ''}"
type="submit" th:value="${card.getName()}" name="cardName">
</td>
</tr>
</table>
</form>
</div>
<div class="cardlist-description">
<h4 th:text="${currentCard.getDescriptionTitle()}"></h4>
<p th:text="${currentCard.getDescription()}"></p>
</div>
</div>
</body>
</html>
Controller
#RequestMapping(value = "gambler/cardlist")
public String baseCardList(Model model,
#RequestParam(value="cardName", required=false) String cardName) {
model.addAttribute("currentCard", cardList.getCardByName(cardName));
model.addAttribute("cardList", cardList);
return "gambler/cardlist";
}
Output (need 10 rep to post image, so here's a link)
One potential improvement/simplification is to remove the form and replace the inputs with anchors. The anchors would look something like this (not tested!):
<a th:href="#{/gambler/cardlist(cardName=${card.name})}">
<button th:text="${card.name}"
th:classappend="${card.id == currentCard.id ? 'selectedButton' : 'deselectedButton'}">Card Name</button>
</a>
You can style selectedButton and deselectedButton as you wish.

Controller sends empty Object from Thymeleaf - Spring Boot

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 ;)

Login form transforming thymeleaf to REST solution

as far as I am a big fan of division frontend and backend I would like to transform my login form from Thymeleaf to REST solution. Could anyone help me with it? I got a code as following:
#Controller
public class LoginController {
#GetMapping("/login")
public String login () {
return "login";
}
}
and my form:
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org">
<head>
<title>Login</title>
</head>
<body>
<form action="" method="post">
<fieldset>
<legend>Please Login</legend>
<!-- use param.error assuming FormLoginConfigurer#failureUrl
contains the query parameter error -->
<div th:if="${param.error != null}">
Failed to login.
<div th:if="${SPRING_SECURITY_LAST_EXCEPTION != null}">
Reason: <span
th:text="${SPRING_SECURITY_LAST_EXCEPTION.message}">
</span>
</div>
</div>
<!-- the configured LogoutConfigurer#logoutSuccessUrl is /login?logout and contains the query param logout -->
<div th:if="${param.logout != null}">You have been logged out.</div>
<p>
<label for="username">Username</label> <input type="text"
id="username" name="username" />
</p>
<p>
<label for="password">Password</label> <input type="password"
id="password" name="password" />
</p>
<div>
<button type="submit" class="btn">Log in</button>
</div>
</fieldset>
My idea is to transform a controller into the REST one and than use AJAX to post the JSON with username and password. Is it a good solution? Also to which address should I send it?
Well, it's not a good approach to use thymeleaf with REST controller. Because your architecture be like, you will have one client application which calls your Server application consist of REST controller. So your design be like following
(Client Application)
[thymeleaf pages -> client controller -> client service ] => [REST controller -> Service -> repository] (Server Application)
You can refer code here if you want to go with this approach
Else you can go with a different approach in which you will have Server application with REST controller and client application with your client code with your HTML page, AJAX calls and navigation logic. This approach will be like, your client code will be in Angular/React like framework and your server-side application will have your controllers only

Best practice struts/jsp/i18n/resourceBundle

I am using Struts 2 and Apache tiles, and I'm new to both. I am trying to "clean" some existing sources that don't feel correct to me (tell me if I'm wrong).
I have the following structure :
in a layout.jsp :
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><tiles:getAsString name="title" /></title>
<tiles:insertAttribute name="header" />
</head>
<body>
<div id="content-size-and-border">
<s:if test="display!='nomenu'">
<tiles:insertAttribute name="menu" />
</s:if>
<div id="maincontent">
<tiles:insertAttribute name="maincontent" />
</div>
</div>
</body>
The main content part displays various jsp/action depending on the item-menu clicked.
The menu part uses some java code directly in the jsp to generate a number of sub-folders by iterating on a list.
<li class="highlight sousmenu"><a href="#"><s:text
name="menu.demarrage" /></a>
<ul class="niveau2">
<%
Locale language = (Locale) session.getAttribute("WW_TRANS_I18N_LOCALE");
// the attribute (used by i18n struts interceptor)
// set in session when the user changes the language
if (language == null)
language = request.getLocale() ;
// if the language was not changed, get the default Locale from browser
User user = ((CustomUserDetails) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal()).getBpmUser();
if (user != null) {
for (Iterator iterator = user.getProcesses().iterator(); iterator
.hasNext();) {
String processToStart = (String) iterator.next();
String processPath = BpmUiConstantes.BPMUI_PROCESS_CONFIG_ROOT + "/" + processToStart ;
String processLib = "process." + processToStart + ".label";
%>
<li>
<a href="<%=request.getContextPath()%>/restricted/DemarrerProcessAvecTache?processName=<%=processToStart%>">
<fmt:setLocale value="<%=language%>"/>
<fmt:bundle basename="<%=processPath%>">
<fmt:message key="<%=processLib%>"/>
</fmt:bundle>
</a>
</li>
<%
}
}
%>
</ul>
</li>
I was wondering if there was a better way to achieve the same result, without the java code in the jsp. Is it important to remove the java code from the jsp from a conception point of view ?
The application uses struts i18n interceptor for the language changes. Is there a way to have the menu use a struts i18n interceptor in some way?
I would definitely look more into JSTL core (C tag to use foreach) and fmt(for the locale- language), with these two libraries you should be able able to safely remove the embedded java code into your page and use a more congruent/safe approach
Here is the oracle webpage to JSTL so you can have some reference
JSTL Reference
Feel free to ask if you have any other questions.
I will add some piece of answer to the previous post :
There is no way to have this menu use a struts i18n interceptor and, above all, it is useless because ResourceBundle's Locale or fmt:bundle's Locale would not be set by this interceptor. It is necessary to specify explicitely the Locale (like I did in the code) => this part cannot be changed.
I'm still not sure about the importance of removing this java code from the jsp though.

Categories