boot(1.4.0) "Pageable" for pagination.It works fine without any issue.But by default the page value starts from "0" but in the front-end the page value starts from "1". So is there any standard approach to increment value instead of manually increment the page number inside the code?
public Page<Device> find(DeviceFindCommand deviceFindCommand, Pageable pageable){
//page = 0 //Actual is 0, Expected increment by 1.
}
Any help should be appreciable.
After implementing Alan answers having the following issues,
1) Still i am able to access zero page which returns the first page(I don't know this is issue or not but i want to get a better clarity).
http://localhost:8180/api/v1/books/?page=3&size=2
Response
{
"content": [
{
"id": "57da9eadbee83fb037a66029",
.
.
.
}{
.
.
.
}
],
"last": false,
"totalElements": 5,
"totalPages": 3,
"size": 2,
"number": 2, //strange always getting 1 less than page number.
"sort": null,
"first": true,
"numberOfElements": 2
}
2) "number": 2, in the response always getting one less than the page number.It should return the current page index.
If you are using Spring Boot 2.X you could switch from WebMvcConfigurerAdapter to application properties. There is a set of properties to configure pageable:
# DATA WEB (SpringDataWebProperties)
spring.data.web.pageable.default-page-size=20 # Default page size.
spring.data.web.pageable.max-page-size=2000 # Maximum page size to be accepted.
spring.data.web.pageable.one-indexed-parameters=false # Whether to expose and assume 1-based page number indexes.
spring.data.web.pageable.page-parameter=page # Page index parameter name.
spring.data.web.pageable.prefix= # General prefix to be prepended to the page number and page size parameters.
spring.data.web.pageable.qualifier-delimiter=_ # Delimiter to be used between the qualifier and the actual page number and size properties.
spring.data.web.pageable.size-parameter=size # Page size parameter name.
spring.data.web.sort.sort-parameter=sort # Sort parameter name.
But please remember that even if you change the one-indexed-parameter the page response (PageImpl class) will return results with zero-based page number. It could be a little misleading.
Spring added this future as well. Just make oneIndexed parameter equals to true in your configuration file and pagination will start from page 1.By default its false and pagination starts from 0.
spring.data.web.pageable.one-indexed-parameters=true
Spring Boot will be using Spring Data under the covers.
The Spring Data class you need to configure is the following:
org.springframework.data.web.PageableHandlerMethodArgumentResolver
and in particular the following method:
http://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/web/PageableHandlerMethodArgumentResolver.html#setOneIndexedParameters-boolean-
This will allow you to use you current UI paging as is i.e. with first page = 1.
In a Boot application I think the config may look something like:
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
resolver.setOneIndexedParameters(true);
argumentResolvers.add(resolver);
super.addArgumentResolvers(argumentResolvers);
}
}
To get better result, you need to extend RepositoryRestMvcConfiguration, like below
#Configuration
public class RespositoryConfiguration extends RepositoryRestMvcConfiguration {
#Override
#Bean
public HateoasPageableHandlerMethodArgumentResolver pageableResolver() {
HateoasPageableHandlerMethodArgumentResolver resolver = super.pageableResolver();
resolver.setOneIndexedParameters(true);
return resolver;
}
}
Then you shall get all the links and pagination information correct except 'number', which is one less.
See the result for page=1,
"_links": {
"first": {
"href": "http://localhost:8080/user/user?page=1&size=5"
},
"self": {
"href": "http://localhost:8080/user/user"
},
"next": {
"href": "http://localhost:8080/user/user?page=2&size=5"
},
"last": {
"href": "http://localhost:8080/user/user?page=2&size=5"
}
},
"page": {
"size": 5,
"totalElements": 6,
"totalPages": 2,
"number": 0
}
Related
I'm working on a "global search" for my application.
Currently, I'm using hibernate-search to search for instances of multiple different objects and return them to the user.
The relevant code looks as follows:
Search.session(entityManager)
.search(ModelA.classs, ModelB.class)
.where(...)
.sort(...)
.fetch(skip, count);
Skip and count are calculated based on a Pageable and the result is used to create an instance of Page, which will be returned to the controller.
This works as I'd expect, however, the types generated by swagger-docs obviously doesn't know, what the type within the Page is, and therefore uses Object.
I'd like to expose the correct types, as I use them to generate the types for the frontend application.
I was able to set the type to an array, when overwriting the schema like this:
#ArraySchema(schema = #Schema(anyOf = {ModelA.class, ModelB.class}))
public Page<?> search(Pageable pageable) {
However, this just disregards the Page and also isn't correct.
The next thing I tried is extending the PageImpl, overwriting the getContent method, and specifying the same schema on this method, but this wasn't included in the output at all.
Next was implementing Page<T> myself (and later removing the implements reference to Page<T>) and specifying the same schema on getContent, iterator, and the field itself, but also to no effect.
How do I tell spring-docs, what the content of the resulting Page might be?
I stumbled upon this when trying to solve a similar problem
Inspired from this thread Springdoc with a generic return type i came up with the following solution, and it seems to apply to your case also. Code examples are in Kotlin.
I introduced a stub class that will just act as the Schema for the response:
private class PageModel(
#Schema(oneOf = [ModelA::class, ModelB::class]))
content: List<Object>
): PageImpl<Object>(content)
Then i annotated my Controller like this:
#Operation(
responses = [
ApiResponse(
responseCode = "200",
content = [Content(schema = Schema(implementation = PageModel::class))]
)
]
)
fun getPage(pageable: Pageable): Page<Object>
This generated this api response:
"PageModel": {
"properties": {
"content": {
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ModelA"
},
{
"$ref": "#/components/schemas/ModelB"
}
],
"type": "object"
},
"type": "array"
},
... -> more page stuff from spring's PageImpl<>
And in the "responses" section for the api call:
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PageModel"
}
}
},
"description": "OK"
}
All generated openapi doc is similar to the autogenerated json when returning a Page, it just rewrites the "content" array property to have a specific type.
So I'm trying to generate my own configuration metadata from #ConfigurationProperties items, following documentation available at https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html#configuration-metadata-annotation-processor. Note that I'm using Spring Boot 2.0 and Kotlin.
It works fine, except for one thing: the defaultValue fields are not getting filled with my default values, but instead just contain standard values like 0 or false.
My #ConfigurationProperties file looks like this:
#Component
#ConfigurationProperties("flowr.epg")
class EpgProperties {
/** Whether or not schedules should be computed/refreshed on (re)start */
var refreshOnRestart = true
/** Number of threads used to store new images */
var nImagesUploadThreads = 10
}
The result looks like this:
{
"hints": [],
"groups": [
{
"sourceType": "com.taktik.flowr.epg.properties.EpgProperties",
"name": "flowr.epg",
"type": "com.taktik.flowr.epg.properties.EpgProperties"
}
],
"properties": [
{
"sourceType": "com.taktik.flowr.epg.properties.EpgProperties",
"defaultValue": false,
"name": "flowr.epg.refresh-on-restart",
"description": "Whether or not schedules should be computed\/refreshed on (re)start",
"type": "java.lang.Boolean"
},
{
"sourceType": "com.taktik.flowr.epg.properties.EpgProperties",
"defaultValue": 0,
"name": "flowr.epg.n-images-upload-threads",
"description": "Number of threads used to store new images in Ozone",
"type": "java.lang.Integer"
}
}
The documentation is kind of poor about that defaultValue field. Am I doing something wrong? Is it possible to fill that defaultValue field automatically? If yes, what am I doing wrong?
How to loop the paged Rest service with the Java DSL Http.outboundGatewaymethod?
The rest URL is for example
http://localhost:8080/people?page=3
and it returns for example
"content": [
{"name": "Mike",
"city": "MyCity"
},
{"name": "Peter",
"city": "MyCity"
},
...
]
"pageable": {
"sort": {
"sorted": false,
"unsorted": true
},
"pageSize": 20,
"pageNumber": 3,
"offset": 60,
"paged": true,
"unpaged": false
},
"last": false,
"totalElements": 250,
"totalPages": 13,
"first": false,
"sort": {
"sorted": false,
"unsorted": true
},
"number": 3,
"numberOfElements": 20,
"size": 20
}
where the variable totalPages tells the total pages amount.
So if the implementation
integrationFlowBuilder
.handle(Http
.outboundGateway("http://localhost:8080/people?page=3")
.httpMethod(HttpMethod.GET)
.expectedResponseType(String.class))
access one page, how to loop all the pages?
The easiest way to do this is like wrapping the call to this Http.outboundGateway() with the #MessagingGateway and provide a page number as an argument:
#MessagingGateway
public interface HttpPagingGateway {
#Gateway(requestChannel = "httpPagingGatewayChannel")
String getPage(int page);
}
Then you get a JSON as a result, where you can convert it into some domain model or just perform a JsonPathUtils.evaluate() (based on json-path) to get the value of the last attribute to be sure that you need to call that getPage() for the page++ or not.
The page argument is going to be a payload of the message to send and that can be used as an uriVariable:
.handle(Http
.outboundGateway("http://localhost:8080/people?page={page}")
.httpMethod(HttpMethod.GET)
.uriVariable("page", Message::getPayload)
.expectedResponseType(String.class))
Of course, we can do something similar with Spring Integration, but there are going to be involved filter, router and some other components.
UPDATE
First of all I would suggest you to create a domain model (some Java Bean), let's say PersonPageResult, to represent that JSON response and this type to the expectedResponseType(PersonPageResult.class) property of the Http.outboundGateway(). The RestTemplate together with the MappingJackson2HttpMessageConverter out-of-the-box will do the trick for you to return such an object as a reply for the downstream processing.
Then, as I said before, looping would be better done from some Java code, which you could wrap to the service activator call. For this purpose you should daclare a gateway like this:
public interface HttpPagingGateway {
PersonPageResult getPage(int page);
}
Pay attention: no annotations at all. The trick is done via IntegrationFlow:
#Bean
public IntegrationFlow httpGatewayFlow() {
return IntegrationFlows.from(HttpPagingGateway.class)
.handle(Http
.outboundGateway("http://localhost:8080/people?page={page}")
.httpMethod(HttpMethod.GET)
.uriVariable("page", Message::getPayload)
.expectedResponseType(PersonPageResult.class))
}
See IntegrationFlows.from(Class<?> aClass) JavaDocs.
Such a HttpPagingGateway can be injected into some service with hard looping logic:
int page = 1;
boolean last = false;
while(!last) {
PersonPageResult result = this.httpPagingGateway.getPage(page++);
last = result.getLast();
List<Person> persons = result.getPersons();
// Process persons
}
For processing those persons I would suggest to have separate IntegrationFlow, which may start from the gateway as well or you can just send a Message<List<Person>> to its input channel.
This way you will separate concerns about paging and processing and will have a simple loop logic in some POJO method.
I am pretty new in Spring and in REST web services and I have the following dout following a tutorial that show how to implement a RESTful web service using Spring MVC.
So, into a controller class I have this method:
#Controller
#RequestMapping("/api/categories")
public class CategoryRestController {
#RequestMapping
#ResponseBody
public CategoryList getCategories(#RequestParam("start") int start, #RequestParam("size") int size ) {
List<Category> categoryEntries = categoryService.findCategoryEntries(start, size);
return new CategoryList(categoryEntries);
}
}
This method handle HTTP GET request toward the resoruce /api/categories and return the retrieved list into JSON format (I think that it depends by the content negotiazion: if the caller put the Accept header as JSON the method return the result in JSON format, is it right?)
By the way my doubt is related the HTTP request shown in the tutorial, infact it do:
http://localhost:8080/springchocolatestore/api/categories?start=0&size=2
that is handled by the previous controller method to return a paginated list (that could be hude) in JSON format, infact I retrieve the following output:
{
"categories": [
{
"links": [
{
"rel": "self",
"href": "http://localhost:8080/springchocolatestore/api/categories/1",
"variables": [],
"templated": false,
"variableNames": []
}
],
"name": "Truffles",
"description": "Truffles",
"id": {
"rel": "self",
"href": "http://localhost:8080/springchocolatestore/api/categories/1",
"variables": [],
"templated": false,
"variableNames": []
}
},
{
"links": [
{
"rel": "self",
"href": "http://localhost:8080/springchocolatestore/api/categories/2",
"variables": [],
"templated": false,
"variableNames": []
}
],
"name": "Belgian Chocolates",
"description": "Belgian Chocolates",
"id": {
"rel": "self",
"href": "http://localhost:8080/springchocolatestore/api/categories/2",
"variables": [],
"templated": false,
"variableNames": []
}
}
]
}
Ok, so in the request I specify the pagination parameter by categories?start=0&size=2
My doubt is related to the user of this parameter. From what I have understand (but maybe it could be wrong) the use of the parameter is against the RESTful principles. Is it true or am I mising something?
Or maybe in this specific case are valid because the parameter are not specifing an object (that have to be returned into my JSON output) but are only related to some options?
I mean that maybe I can't use parameter to specify a specific object, something like this:
// RETRIEVE THE PRODUCT WITH ID=1
http://localhost:8080/springchocolatestore/api/producs?product=1
So I think that the previous is not following the RESTfull standard because I am specifing a product object with a parameter and not accessing to it as a resource, so I have to do in this way:
http://localhost:8080/springchocolatestore/api/producs/1
Can you give me some clarification?
Tnx
REST doesn't have much to do with URLs, and using request parameters is not unRESTful.
But I agree that path variables are generally used to identify a specific resource, and parameters are generally used for search or pagination parameters.
I'm on a Symfony 2.5.6 project using FosElasticaBundle (#dev).
In my project, i just need to get the total hits count of a request on Elastic Search. That is, i'm querying Elastic Search with a normal request, but through the special "count" URL:
localhost:9200/_search?search_type=count
Note the "search_type=count" URL param.
Here's the example query:
{
"query": {
"filtered": {
"query": {
"match_all": []
},
"filter": {
"bool": {
"must": [
{
"terms": {
"media.category.id": [
681
]
}
}
]
}
}
}
},
"sort": {
"published_at": {
"order": "desc"
}
},
"size": 1
}
The results contains a normal JSON response but without any documents in the hits part. From this response i easily get the total count:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits": {
"total": 81,
"max_score": 0,
"hits": [ ]
}
}
Okay, hits.total == 81.
Now, i couldn't find any solution to do the same through FOSElasticaBundle, from a repository.
I tried this:
$query = (...) // building the Elastica query here
$count = $this->finder->findPaginated(
$query,
array(ES::OPTION_SEARCH_TYPE => ES::OPTION_SEARCH_TYPE_COUNT)
)->getNbResults();
But i get an Attempted to load class "Pagerfanta". I don't want Pagerfanta.
Then this:
$count = $this->finder->createPaginatorAdapter(
$query,
array(ES::OPTION_SEARCH_TYPE => ES::OPTION_SEARCH_TYPE_COUNT)
)->getTotalHits();
But it would always give me 0.
Would be easy if i had access to the Elastica Finder service from the repository (i could then get a ResultSet from the query search, and this ResultSet has a correct getTotalHits() method). But services from repository... you know.
Thank you for any help or clue!
I faced the same challenge, getting access to the searchable interface from inside the repo. Here's what I ended up with:
Create AcmeBundle\Elastica\ExtendedTransformedFinder. This just extends the TransformedFinder class and makes the searchable interface accessible.
<?php
namespace AcmeBundle\Elastica;
use FOS\ElasticaBundle\Finder\TransformedFinder;
class ExtendedTransformedFinder extends TransformedFinder
{
/**
* #return \Elastica\SearchableInterface
*/
public function getSearch()
{
return $this->searchable;
}
}
Make the bundle use our new class; in service.yml:
parameters:
fos_elastica.finder.class: AcmeBundle\Elastica\ExtendedTransformedFinder
Then in a repo use the getSearch method of our class and do what you want :)
class SomeSearchRepository extends Repository
{
public function search(/* ... */)
{
// create and set your query as you like
$query = Query::create();
// ...
// then run a count query
$count = $this->finder->getSearch()->count($query);
}
}
Heads up this works for me with version 3.1.x. Should work starting with 3.0.x.
Ok, so, here we go: it is not possible.
You cannot, as of version 3.1.x-dev (2d8903a), get the total matching document count returned by elastic search from FOSElasticaBundle, because this bundle does not expose this value.
The RawPaginatorAdapter::getTotalHits() method contains this code:
return $this->query->hasParam('size')
? min($this->totalHits, (integer) $this->query->getParam('size'))
: $this->totalHits;
which prevents to get the correct $this->totalHits without actually requiring any document. Indeed, if you set size to 0, to tell elasticsearch not to return any document, only meta information, RawPaginatorAdapter::getTotalHits() will return 0.
So FosElasticaBundle doesn't provide a way to know this total hits count, you could only do that through the Elastica library directly. Of course with the downisde that Elastica finders are natively available in \FOS\ElasticaBundle\Repository. You'd had to make a new service, do some injection, and inovke your service instead of the FOSElasticaBundle one for repositories... ouch.
I chose another path, i forked https://github.com/FriendsOfSymfony/FOSElasticaBundle and changed the method code as follow:
/**
* Returns the number of results.
*
* #param boolean $genuineTotal make the function return the `hits.total`
* value of the search result in all cases, instead of limiting it to the
* `size` request parameter.
* #return integer The number of results.
*/
public function getTotalHits($genuineTotal = false)
{
if ( ! isset($this->totalHits)) {
$this->totalHits = $this->searchable->search($this->query)->getTotalHits();
}
return $this->query->hasParam('size') && !$genuineTotal
? min($this->totalHits, (integer) $this->query->getParam('size'))
: $this->totalHits;
}
$genuineTotal boolean restores the elasticsearch behaviour, without introducing any BC break. I could also have named it $ignoreSize and use it the opposite way.
I opened a Pull Request: https://github.com/FriendsOfSymfony/FOSElasticaBundle/pull/748
We'll see! If that could help just one person i'd be happy already!
While, you can get the index instance as a service (fos_elastica.index.INDEX_NAME.TYPE_NAME) and ask for count() method.
Joan