I have a recipe app with several routes (get, getById, post, delete, etc).
So far it hasn't been difficult to implement them, since, for example, a get route would be like this:
Controller:
#RequestMapping(value = "/", method = RequestMethod.GET)
public List<Recipe> getAllRecipe() {
return recipesRepository.findAll();
}
Or by id:
Repository:
public interface RecipesRepository extends MongoRepository<Recipe, String> {
Recipe findBy_id(String _id);
}
Controller:
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public Recipe getRecipeById(#PathVariable("id") String id) {
return recipesRepository.findBy_id(id);
}
I want to make a search screen in my app, so when you write something (like "straw") I get all results that contain the word "straw" in my database (like strawberry ice cream, strawberry milkshake...) in the name field (my model has a field called name).
My knowledge in Java isnt by any means good, so I'm lost here, so far I have a route that gets all results using "name", but I need it to search anything that contains that value, not only if you put the exact value (like if I put "Stew", it gives me "Stew" result if it exists, but I need to get every result that have the word "Stew" in their name). If it helps, this is the code for that:
Repository:
public interface RecipesRepository extends MongoRepository<Recipe, String> {
Recipe findBy_id(String _id);
Recipe findByName(String name);
}
Controller:
#RequestMapping(value = "/recipe/{name}", method = RequestMethod.GET)
public Recipe getRecipeByName(#PathVariable("name") String name) {
return recipesRepository.findByName(name);
}
About the case sensitive problem, you can add keywords, like this:
public interface RecipesRepository extends MongoRepository<Recipe, String> {
{
public List<Recipe> findByNameLikeIgnoreCase(String name);
}
I think you can use smth like this one :
public interface RecipesRepository extends MongoRepository<Recipe, String> {
{
public List<Recipe> findByNameLike(String name);
}
Related
I am trying to get a list of the objects with the same field.
I have a model named Song, with the fields: name, genre, artist and id.
I read online about how to implement the method in the repository and i added it like this:
#Repository public interface ISongRepository extends JpaRepository<Song, Long>{
#Query("SELECT s.name FROM Song s WHERE s.genre = ?1")
Song findByGenre(String genre);
}
Now i want to retrieve the objects with an endpoint that i create in the controller. But i can't find online how i need to correctly write this method.
I tried to use the same format as my get by id but that doesn't work, is there a different format when it comes to custom methods?
Code of my get by id:
#GetMapping(value = "/{id}")
#ResponseBody
public ResponseEntity getSongById(#PathVariable("id") Long id) {
Optional<Song> song = songRepository.findById(id);
if(song.isEmpty()) {
return ResponseEntity.badRequest().body(song);
}
return ResponseEntity.ok(song.get());
}
Code of my method to retrieve the output of the custom method:
#GetMapping(value = "/genre/{genre}")
#ResponseBody
public ResponseEntity getSongByGenre(#PathVariable("genre") String songGenre){
List<Song> song = song.(songRepository.findByGenre(songGenre));
if(song.isEmpty()){
return ResponseEntity.badRequest().body(song);
}
return ResponseEntity.ok(song.get());
}
}
Try to add #Param at ISongRepository and change the return type findByGenre method to List<Song>
, Also make sure that you retrieve the id or genre from the rest controllers
#Repository public interface ISongRepository extends JpaRepository<Song, Long>{
#Query("SELECT s FROM Song s WHERE s.genre =:genre")
List<Song> findByGenre(#Param("genre") String genre);
}
I changed the method to this in the Controller:
#GetMapping("/genre/{genre}")
public ResponseEntity getAllSongsGenre(#PathVariable("genre") String genre){
List<Song> song;
if(genre != null){
song = songRepository.findByGenre(genre);
if(song.isEmpty()){
return ResponseEntity.badRequest().body(song);
}
return ResponseEntity.ok(song);
}
song = songRepository.findAll();
return ResponseEntity.ok(song);
}
And in the JPARepository to this:
List<Song> findByGenre(String genre);
The output is like this (In Postman):
{
"songName": "Someday",
"songArtist": "Kygo",
"songGenre": "House",
"songId": 1
}
I have a request like:
example.com/search?sort=myfield1,-myfield2,myfield3
I would like to split those params to bind a List<String> sort in my controller or List<SortParam> where SortParam is the class with fields like: name (String) and ask (boolean).
So the final controller would look like this:
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<MyResponse> search(#RequestParam List<String> sort) {
//...
}
or
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<MyResponse> search(#RequestParam List<SortParam> sort) {
//...
}
Is there a way to make it?
UPDATE:
The standard way of passing parameters does not satisfy my requirements. I.e. I cannot use sort=myfield1&sort=-myfield2&sort=myfield3. I have to use comma separated names.
Also, I do understand that I can accept #RequestParam String sort in my controller and then split the string inside the controller like sort.split(",") but it also doesn't solve the above problem.
Its just a simple Type Covertion task. Spring defines an SPI (Service Provider Interface) to implement type conversion logic. For your specific problem you can define your Type Conversion logic by implementing Converter interface.
#Component
public class StringToListConverter implements Converter<String, List<String>> {
#Override
public List<String> convert(String source) {
return Arrays.asList(source.split(","));
}
}
You can also convert your request parameter to List<SortPram> according your logic (but I am not sure about your that logic from your question). This is it! Now Spring get known that how to bind your request paramter to a list.
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<MyResponse> participants(#RequestParam("sort") List<String> sort) {
// .. do your logic
}
There are many more ways to define you custom data binder. Check this
A Custom Data Binder in Spring MVC article.
Spring documentation about Validation, Data Binding, and Type Conversion
Yes, you can certainly do that, you're almost there.
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<MyResponse> participants(#RequestParam("sort") List<String> sort) {
//...
}
You should now be able to call your service like this (if search is located at your root, otherwise adapt according to your situation):
curl "localhost:8080/search?sort=sortField1&sort=sortField2&sort=sortField3"
Hope this helps!
EDIT
Sorry, I have read your comments and what you need is clear to me now. I have created a workaround for you that is almost what you want I think.
So first a SortParams class:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class SortParams {
private List<SortParam> sortParamList;
public SortParams(String commaSeparatedString) {
sortParamList = Arrays.stream(commaSeparatedString.split(","))
.map(p -> SortParam.valueOf(p))
.collect(Collectors.toList());
}
public List<SortParam> getSortParamList() {
return this.sortParamList;
}
public enum SortParam {
FOO, BAR, FOOBAR;
}
}
Then your controller could look like this:
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<List<SortParams.SortParam>> search(#RequestParam("sort") SortParams sort) {
return ResponseEntity.ok(sort.getSortParamList());
}
Now your SortParams object has a list of SortParam:
curl "localhost:8080/search?sort=FOO,BAR"
["FOO","BAR"]
Would something like this fit what you're looking for?
It could be helpfull if Kotlin
private const val DELIMITER: Char = ':'
private val DEFAULT_DIRECTION: Sort.Direction = Sort.Direction.ASC
private fun parseFrom(source: String): Sort.Order = if (source.contains(DELIMITER)) {
Sort.Order(Sort.Direction.fromString(source.substringAfter(DELIMITER)), source.substringBefore(DELIMITER))
} else Sort.Order(DEFAULT_DIRECTION, source)
// if few sort paremeters
#Component
class RequestSortConverterArray : Converter<Array<String>, Sort> {
override fun convert(source: Array<String>): Sort? = if (source.isEmpty()) Sort.unsorted()
else source.map { parseFrom(it) }.let { Sort.by(it) }
}
// if single sort paremeter
#Component
class RequestSortConverterString : Converter<String, Sort> {
override fun convert(source: String): Sort? = if (source.isEmpty()) Sort.unsorted()
else Sort.by(parseFrom(source))
}
...
#GetMapping
fun search(
#RequestParam(required = false, defaultValue = "0") page: Int,
#RequestParam(required = false, defaultValue = "20") size: Int,
#RequestParam(required = false, defaultValue = "myfield1:asc") sort: Sort
) {
val pageable = PageRequest.of(page, size, sort)
// make a search by repository
}
Two questions:
1:
All the Controllers extend from the MasterController and implement its abstract method in every #GetMapping in order to have consistency. In every #Controller I have to populate the variables inherited from the MasterController. There is no assurance that I do all of them all the time. A shortcut would be to call a method from MasterController to populate all. Is it possible to catch the same request on multiple places so I do not need to execute the same methods every time but it "gets done" automatically?
#SessionAttributes(value = {"user", "..."})
public abstract class MasterController<T>
{
public Model MODEL;
public HashMap<String, String> INCOMING_PARAMS;
public RequestType REQUEST_TYPE;
public User USER;
public abstract T request(
#ModelAttribute("user") User _user,
#RequestParam HashMap<String, String> _params,
Model _model,
SessionStatus _sessionStatus,
HttpServletRequest _request,
HttpServletResponse _response);
public String otherCommonFunction(String _a)
{
...
}
}
#Controller
public class LoginController extends MasterController<String>
{
#GetMapping(value = "/account-login-page")
#Override
public String request(
#ModelAttribute("user") User _user,
#RequestParam HashMap<String, String> _params,
Model _model,
SessionStatus _sessionStatus,
HttpServletRequest _request,
HttpServletResponse _response)
{
USER = _user; //Do this every time
REQUEST_TYPE = RequestType.GET; //Do this every time
INCOMING_PARAMS = _params; //Do this every time
MODEL = _model; //Do this every time
...
return "..."
}
}
2:
If there are multiple #GetMapping in one #Controller, how can I have assurance that all of the methods have exactly the same parameters? An abstract method can be inherited once in the child class. It is possible to just copy and paste it but that looks unprofessional. Is there any solution to this?
I'm creating a Java application using Elastic Search.
Here is the link for my project.
https://github.com/chanakaDe/ensembl-elastic-rest
In this project, I have implemented a rest controller to take data as JSON.
This is the controller class. Now it only has 2 methods. But I need to add some method like this.
#RequestMapping(value = "/find-by/{id}/{param1}/{param2}/{param3}", method = RequestMethod.GET)
public Iterable<Track> findAllWithParams(#PathVariable int id, #PathVariable String param1, #PathVariable String param2, #PathVariable String param3) {
return trackService.someMethodWithParams(id, param1, param2, param3);
}
What I need to do is take some values from user and send them into Elastic server and make a search. I just refered some of these links and got some idea.
https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/java-search.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html
TrackService.java and TrackServiceImpl.java are implemented by TrackRepository.java and it's extended by ElasticsearchRepository default class. https://github.com/chanakaDe/ensembl-elastic-rest/blob/master/src/main/java/com/chanaka/book/repository/TrackRepository.java
I need to take values via REST URL and create an object like following and pass that to Elastic Server. How can I implement that with my current project configuration ?
{
"query": {
"constant_score" : {
"filter" : {
"terms" : { "user" : ["kimchy", "elasticsearch"]}
}
}
}
}
This is my TrackService.java interface.
public interface TrackService {
Track save(Track track);
Track findOne(int id);
Iterable<Track> findAll();
}
And also this is my TrackServiceImpl.java class implemented by TrackService.java.
public class TrackServiceImpl implements TrackService {
private TrackRepository trackRepository;
#Autowired
public void setTrackRepository(TrackRepository trackRepository) {this.trackRepository = trackRepository;}
#Override
public Track save(Track track) {
return trackRepository.save(track);
}
#Override
public Track findOne(int id) {
return trackRepository.findOne(id + "");
}
#Override
public Iterable<Track> findAll() {
return trackRepository.findAll();
}
}
Do I need to implement a custom method for that ? Or is there any default methods like findAll() and findOne() ?
Simply pass an object and get the value ?
I think, there's no such existing method and you need to create your own by using QueryBuilders.wrapperQuery(query.toString()) and ElasticsearchTemplate. Just to note, wrapperQuery supports only query not filter. But you can achieve filter context query with constant_score.
I have been looking for a way to somehow reduce the amount of code that is duplicated with subtle variance in my Spring MVC controllers, but searching through the SO questions so far has only yielded some questions without any satisfactory answers.
One example of duplication that I want to remove is this, where the user creation page and the role creation page share similarities:
#RequestMapping(value = "user/create", method = RequestMethod.GET)
public String create(#ModelAttribute("user") User user, BindingResult errors) {
LOG.debug("Displaying user creation page.");
return "user/create";
}
#RequestMapping(value = "role/create", method = RequestMethod.GET)
public String create(#ModelAttribute("role") Role role, BindingResult errors) {
LOG.debug("Displaying role creation page.");
return "role/create";
}
A slightly more involved variant of duplication that I would like to remove is the one for posting the create form:
#RequestMapping(value = "user/create", method = RequestMethod.POST)
public String save(#ModelAttribute("user") User user, BindingResult errors) {
LOG.debug("Entering save ({})", user);
validator.validate(user, errors);
validator.validatePassword(user, errors);
validator.validateUsernameAvailable(user, errors);
String encodedPassword = encoder.encode(user.getPassword());
user.setPassword(encodedPassword);
if (errors.hasErrors()) {
return create(user, errors);
} else {
service.save(user);
}
return "redirect:/user/index/1";
}
#RequestMapping(value = "role/create", method = RequestMethod.POST)
public String save(#ModelAttribute("role") Role role, BindingResult errors) {
LOG.debug("Entering save({})", role);
validator.validate(role, errors);
if (errors.hasErrors()) {
return create(role, errors);
} else {
service.save(role);
}
return "redirect:/index";
}
This example includes a validate then save if correct and a redirect to the error page if things don't go as planned.
How to remove this duplication?
Spring uses your handler method parameter types to create class instances from the request parameters or body. As such, there is no way to create a handler (#RequestMapping) method that could take an Object and check if it is either a Role or a User. (Technically you could have both parameters and just check which one isn't null, but that is terrible design).
Consequently, you need a handler method for each. This makes sense since, even through the logic is similar, it is still specific to the exact type of model object you are trying to create. You perform different validation, call a different service method, and return a different view name.
I say your code is fine.
Thought I would provide the solution that I settled on in the hope that it might help someone. My gf suggested that I use the name of the entity as a path variable for the controller, and this has proved to provide a very nice solution for the problem at hand.
The two methods now look like this:
#RequestMapping(value = "{entityName}/create", method = RequestMethod.GET)
public String create(#PathVariable("entityName") String entityName, #ModelAttribute("entity") BaseEntity entity, BindingResult errors) {
LOG.debug("Displaying create page for entity named: [{}]", entityName);
return handlerFactory.getHandler(entityName).getCreateView();
}
#RequestMapping(value = "{entityName}/create", method = RequestMethod.POST)
public String save(#PathVariable("entityName") String entityName, #ModelAttribute("entity") BaseEntity entity, BindingResult errors) {
LOG.debug("Saving entity of type {}", entityName);
CrudHandler handler = handlerFactory.getHandler(entityName);
handler.getCreateValidator().validate(entity, errors);
if (errors.hasErrors()) {
return create(entityName, entity, errors);
}
handler.preSave(entity);
handler.getService().save(entity);
return "redirect:" + DASHBOARD_URL;
}
The CrudHandler interface has implementations for each entity, and provides the controller with the entity specific classes that it needs, such as service and validator. A sample CrudHandler implementation looks like this for me:
#Component
public class RoleCrudHandler implements CrudHandler {
private static final String ENTITY_NAME = "role";
public static final String CREATE_VIEW = "role/create";
public static final String EDIT_VIEW = "role/edit";
#Resource
private RoleService roleService;
#Resource
private RoleValidator validator;
#Resource
private CrudHandlerFactory handlerFactory;
#PostConstruct
public void init() {
handlerFactory.register(ENTITY_NAME, this);
}
#Override
public GenericService getService() {
return roleService;
}
#Override
public Validator getCreateValidator() {
return validator;
}
#Override
public Validator getUpdateValidator() {
return validator;
}
#Override
public BaseEntity createEntity() {
return new Role();
}
#Override
public void preSave(BaseEntity entity) {
}
#Override
public String getCreateView() {
return CREATE_VIEW;
}
#Override
public String getUpdateView() {
return EDIT_VIEW;
}
}
If someone sees some ways to improve this, feel free to share. Hope this will be of use for someone.