Get all documents from an index using spring-data-elasticsearch - spring

I am trying to connect to my external ElasticSearch server with Spring Boot.
If I do a curl from command line, I get expected results.
curl "http://ipAddr:9200/indexName/TYPE/_search?pretty=true"
But getting this error when I try to access it via Spring Boot.
<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Mon Sep 11 12:39:15 IST 2017</div><div>There was an unexpected error (type=Internal Server Error, status=500).</div><div>Could not write JSON: (was java.lang.NullPointerException); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: java.util.ArrayList[0]->org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl["facets"])</div></body></html>
Not sure why a NullPointerException and what is aggregartion.impl
Here is my Spring Application:
Controller:
#RestController
public class PojoController {
#Autowired
PojoService pojoService;
#RequestMapping(value = "/", method=RequestMethod.GET)
public #ResponseBody String index() {
return new String("Welcome:)");
}
#RequestMapping(value = "/all", method = RequestMethod.GET,
produces = { MediaType.APPLICATION_JSON_VALUE })
#ResponseBody List<POJO> findAll() {
try {
List<POJO> pojoObj = pojoService.findAll();
return pojoObj;
} catch (Exception exp) {
exp.printStackTrace();
return null;
}
}
}
Repository:
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
List<POJO> findAll();
}
Service:
#Service
public class POJOServiceImpl implements POJOService{
private POJORepository pojoRepository;
private ElasticsearchTemplate elasticsearchTemplate;
#Autowired
public void setPojoRepository(PojoRepository pojoRepository) {
this.pojoRepository = pojoRepository;
}
public POJO findOne(String id) {
return pojoRepository.findOne(id);
}
public List<POJO> findAll() {
return (List<POJO>) pojoRepository.findAll();
}
}
POJO class:
#Document(indexName = "INDEX", type = "TYPE")
public class POJO {
#Id
private Integer id;
private String name;
public POJO(){
// empty
}
public POJO(Integerid, String name) {
super();
this.id = id;
this.name = name;
}
// getters and setters
}
I should be able to query all the documents in the index. Later on, I will try and use filters etc.
Any help is appreciated. Thanks :)

It looks like Jackson has a problem with handling your POJO (probably related to this issue: DATAES-274) - the problematic part is casting in repository from Iterable collection to List.
Update
In case of repositories, spring-data-elasticsearch behaves a bit different than you would expect. Taking your example:
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
List<POJO> findAll();
}
and after calling in your rest controller:
List<POJO> pojoObj = pojoService.findAll();
in debugger you will see something like this:
You would expect that pojoObj list contains objects of POJO class.
And here comes the surprise - pojoObj ArrayList contains one object of AggregatedPageImpl type and its content field is the right list that contains your POJO objects.
This is the reason why you get:
Could not write JSON: ... java.util.ArrayList[0]->org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl[\"facets\"])
As I wrote before, Jackson cannot handle this while serializing POJO objects.
Solution 1
Let repositories return Iterable collection (by default).
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
}
Move the conversion part to the service but use some utility method (here with Guava) in order to have it like this:
import com.google.common.collect.Lists;
public List<POJO> findAll() {
return Lists.newArrayList(pojoRepository.findAll());
}
Solution 2
Use Page in repository (here simplified version without parameters):
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
Page<TestDto> findAll();
}
If you still want to operate on list - get content from page in service:
public List<POJO> findAll() {
return testDtoRepository.findAll().getContent();
}

Related

How to use #RequestParam with DTO

I have to make GET method with a DTO.
But when I code like this↓, an error occurs.
org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'param' for method parameter type SampleDTO is not present
After checking that error, I figured out I need add option #RequestParam(required=false).
Then, I restarted tomcat.
Although there was no more error, my param was null(I actually sent sample_name).
And I tried to use both of no annotation and #ModelAttribute.
Both of them occurs same error↓
Caused by: java.lang.NoSuchMethodError: org.springframework.beans.BeanUtils.getResolvableConstructor(Ljava/lang/Class;)Ljava/lang/reflect/Constructor;
What should I do? plz give me advice.
I don't know best way handling DTO.
Because I usually coded using HashMap actually.
Here is my example code.
//Controller sample
point: insertSample method works well.
#RestController
#RequestMapping("/sample")
public class SampleController {
#Autowired
private SampleService sampleService;
#GetMapping
public Result getSampleList(#RequestParam SampleDTO param) throws Exception {
// (#RequestParam(required=false) SampleDTO param)
// (#ModelAttribute SampleDTO param)
// (SampleDTO param)
return sampleService.getFolderList(param);
}
#PostMapping
public Result insertSample(#RequestBody SampleDTO param) throws Exception {
return sampleService.insertFolder(param);
}
}
// DTO sample
#Getter // I didn't attach #Setter because of #Builder.
#NoArgsConstructor
#JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
#Alias("SampleDTO")
public class SampleDTO {
#NotNull
private Long sampleNo;
#NotBlank
private String sampleName;
private String sampleDesc;
#Builder
public SampleDTO(Long sampleNo, String sampleName, String sampleDesc) {
this.sampleNo = sampleNo;
this.sampleName = sampleName;
this.sampleDesc = sampleDesc;
}
}
In order to bind request parameters to object you need to have standard getters/setters in your DTO class. Add #Setter to your method, then you can bind without even any annotation.
#GetMapping
public Result getSampleList(SampleDTO param) throws Exception {
return sampleService.getFolderList(param);
}
#GetMapping
public Result getSampleList(#RequestParam("param") SampleDTO param) throws Exception {
// (#RequestParam(required=false) SampleDTO param)
// (#ModelAttribute SampleDTO param)
// (SampleDTO param)
return sampleService.getFolderList(param);
}
}
Try like this, You should have to designate variable

Spring TransactionManager behavior with Spring Data and JpaRepository

I have a controller which does the following
A submit end point which save an entry in db and then call some external service asynchronously
Track the update of asynchronous call (this call updates an associated table) by watching the db and update the status of the entry created in step one
I was using the #Query Annotation to verify if step one entry exist in db and it was always returning empty. I tried changing it to the default spring method and it starts returning the inserted value.
I read about proxies, #Transactional and how non CRUD methods in a JPARepository are non transactional and tried few things like transaction propagation and self injection and even explicitly marking the repo method #Transactional. But none of them fixed the issue. Using spring data method solved it but I still don't understand what happened. Can someone help with an explanation of this behavior.
Basic code snippet is below
MyController
#RestController
public class MyController {
private final MyService myService;
private final MyRepository myRepository;
#Autowired
public MyController(MyService myService,
MyRepository myRepository) {
this.myService = myService;
this.myRepository = myRepository;
}
#PostMapping(value = "/submit")
public ResponseEntity<MyResponse> submit(#Valid #RequestBody MyRequest myRequest) {
return ResponseEntity
.accepted()
.body(MyResponse.success(myService.submit(myRequest), "SUBMITTED"));
}
/**
* This method is to update the status of the entry created by /submit endpoint
* if the asynchoronous process triggered by submit endpoint update an associated table
*/
#PostConstruct
private void trackUpdates() {
..
someObserver.subscribe(trackedAssociatedEntity -> {
myService.trackAndUpdateBasedOnAssociatedEntity(trackedAssociatedEntity);
});
}
}
MyService
#Service
#Transactional
public class MyService {
private final MyRepository myRepository;
#Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
submit(MyRequest myRequest) {
myRepository.save(myEntity);
//makes that asynchronous call
}
public void trackAndUpdateBasedOnAssociatedEntity(#NotNull MyAssociatedEntity myassociatedEntity) {
// This commented call always return empty but the uncommented code works as expected
// List<MyEntity> existingEntity =
// myRepository.findEntityByField1AndField2(myassociatedEntity.getField1(),myassociatedEntity.getField2());
List<MyEntity> existingEntities =
myRepository.findByField1AndField2(myassociatedEntity.getField1(),myassociatedEntity.getField2());
if(existingEntities.isEmpty()){
//create new
}else{
//update
}
}
}
}
}
MyRepository
#Repository
public interface MyRepository extends JpaRepository<MyEntity, Long> {
#Query("SELECT e FROM MyEntity e WHERE e.field1 = ':field1' and e.field2 = ':field2' ")
List<MyEntity> findEntityByField1AndField2(String field1, String field2);
List<MyEntity> findByField1AndField2(String field1, String field2);
}
I believe that '' are not needed. Please try the following:
#Repository
public interface MyRepository extends JpaRepository<MyEntity, Long> {
#Query("SELECT e FROM MyEntity e WHERE e.field1 = :field1 and e.field2 = :field2")
List<MyEntity> findEntityByField1AndField2(String field1, String field2);
List<MyEntity> findByField1AndField2(String field1, String field2);
}

JSON field Desrializing to lowercase in Spring Boot

I have a Spring Boot Controller -
#RestController
public class UserController {
#PostMapping
#ResponseStatus(CREATED)
public UserResponse register( #Valid #RequestBody UserRequest userRequest) {
//return ....
}
}
Below is UserRequest.java
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
public class UserRequest {
private String email;
//other property
}
I am sending below json in request body -
{
"email" : "TEST#Example.com",
//some other fields.
}
Sometime client send email in uppercase or in camel case so in userRquest I want to change value of email field to lowercase like test#example.com while de serializing to UserRequest Object.
Is there any easy way to do this. Can I introduce my own annotation like #ToLowerCase how I can create my own annotation and use that at field level in UserRequest.
There is no easy way just by introducing a new annotation #ToLowerCase,
because then you would also need to implement some annotation processor
for doing the real conversion work.
But you can achieve your goal in a slightly different way.
In your UserRequest class annotate the email property
with #JsonDeserialize and specify a converter there.
#JsonDeserialize(converter = ToLowerCaseConverter.class)
private String email;
You need to implement the converter class by yourself,
but it is easy by extending it from StdConverter.
public class ToLowerCaseConverter extends StdConverter<String, String> {
#Override
public String convert(String value) {
return value.toLowerCase();
}
}
Jackson will use the setter methods in your class.
Perform the conversion to lower case in the setter.
For example
public void setEmail(String newValue)
{
email = StringUtils.lowerCase(newValue);
}
StringUtils is an apache commons class.
You can make a general StringDeserializer and register it in ObjectMapper as shown below:-
StringDeserializer class
public final class StringDeserializer extends StdDeserializer<String> {
public StringDeserializer() {
super((Class<String>) null);
}
#Override
public String deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonToken token = parser.getCurrentToken();
if (token == JsonToken.VALUE_STRING) {
String text = parser.getText();
return text == null ? null : text.toLowerCase().trim();
}
return null;
}
}
JacksonConfiguration class
#Configuration
public class JacksonConfiguration {
#Autowired
void mapper(ObjectMapper mapper) {
mapper.registerModule(initModule());
}
private Module initModule() {
SimpleModule module = new SimpleModule();
module.addDeserializer(String.class, new StringDeserializer());
return module;
}
}
The above code makes jackson deserialize all strings to lowercase and trimmed.

Filtering with Spring Data Projection

I've created a class based Projection for my Spring Data Repository. That works great. Then I tried to annotate the constructor with #QueryProjection from QueryDSL, hoping to get a REST Endpoint with paging, sorting and filtering.
The code looks like this, but there are way more fields and details omitted for brevity:
Entity:
#Data
#Entity
public class Entity extends BaseEntity {
private String fieldA, fieldB;
private AnotherEntity ae;
}
DTO:
#Getter
#FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class EntityDto {
private final String fieldA;
private final String anotherEntityFieldC;
#QueryProjection
public EntityDto(final String fieldA, final String anotherEntityFieldC) {
this.fieldA = fieldA;
this.anotherEntityFieldC = anotherEntityFieldC;
}
}
Repository:
public EntityRepo extends JpaRepository<Entity, Long>, QuerydslBinderCustomizer<EntityPath<Entity>>, QuerydslPredicateExecutor<Entity> {
Page<EntityDto> findPageProjectedBy(Predicate predicate, Pageable pageable);
}
Endpoint:
#RestController
#RequestMapping(EntityEndpoint.ROOT)
#RequiredArgsConstructor(onConstructor = #__({#Autowired}))
public EntityEndpoint {
private final EntityRepo er;
#GetMapping
public ResponseEntity<PagedResources<Resource<EntityDto>>> getAllEntities(
Pageable pageable,
#QuerydslPredicate(root = EntityDto#.class) Predicate predicate,
PagedResourcesAssembler<EntityDto> assembler) {
Page<EntityDto> page = er.findPageProjectedBy(predicate, pageable);
return new ResponseEntity<>(assembler.toResource(page), HttpStatus.OK);
}
}
I get the exception:
java.lang.IllegalStateException: Did not find a static field of the same type in class at.dataphone.logis4.model.dto.QBuchungDto!
Stacktrace as gist
And that's the URL:
curl -X GET --header 'Accept: application/json' 'http://localhost:8080/Entity'
Well, it seems like you can only query with the actual Entity.
I think of it as the part in the 'WHERE'-clause, which can only have columns actually in the selected Entities, while the DTO projection happens in the 'SELECT'-clause.
The #QueryProjection-Annotation seems to serve only for the QueryDsl code generator to mark class which then can be used to create projections in a select-clause

How are request parameters converted in Spring Data Rest

I have a repository for a mapped class in Spring Data Rest. The mapped class has an enum field:
#Entity
class Run {
#Id
#GeneratedValue
Integer id
Status status
public static enum Status {
IN_PROCESS('inProcess'),
FAILED('failed'),
SUCCESS('success')
String value
Status(String value) {
this.value = value
}
#JsonValue
#Override
public String toString() {
return value
}
}
}
I have declared the following repository for the Run entity:
interface RunRepository extends CrudRepository<Run, Integer> {
public Iterable<Run> findByStatus(#Param('status') Run.Status status)
}
I've implemented a custom Converter for the Run.Status enum, which I've for the sake of simplicity, cut down to the following:
class RunStatusConverter implements Converter<String, Run.Status> {
#Override
Run.Status convert(String s) {
return Run.Status.IN_PROCESS
}
}
This converter has been registered using a #Configuration annotated class, and it works fine when used to convert custom #Controller method parameters, such as in the following:
#Controller
class TestController {
#RequestMapping(path = '/test-status')
public #ResponseBody String testStatus(#RequestParam Run.Status status) {
println 'status: ' + status
return status.value
}
}
When, however, I call the findByStatus method (over HTTP) on the RunRepository class, the converter does not get called and instead I get a org.springframework.core.convert.ConversionFailedException whose underlying cause is the following: java.lang.IllegalArgumentException: No enum constant Run.Status.inProcess.
It seems the custom converter is not getting called when passing parameters to the repository.
How do I get the repository to receive the correctly converted request parameter, when calling exposed repository methods over HTTP?

Resources