Nested objects with Spring Boot, MongoDB - spring

How get nested objects with MongoDb using Spring Boot?
I have 3 DTO, BoardResponse, ColumnsResponse, CardResponse.
public class BoardResponse {
//id, name, createdBy,createdDate,updatedBy,updatedDate, getters
List<ColumnResponse> columns = new ArrayList<ColumnResponse>();
public BoardResponse(KanbanBoard board, List<ColumnResponse> columns) {
super();
this.id = board.getId();
this.name = board.getName();
this.createdBy = board.getCreatedBy();
this.createdDate = board.getCreatedDate();
this.updatedBy = board.getUpdatedBy();
this.updatedDate = board.getUpdatedDate();
this.columns = columns;
}
public class ColumnResponse {
//id, name, createdBy,createdDate,updatedBy,updatedDate, getters
private ObjectId idBoard;
List<CardResponse> cards = new ArrayList<>();
public ColumnResponse(KanbanColumn column, List<CardResponse> cards) {
super();
this.id = column.getId();
this.name = column.getName();
this.createdBy = column.getCreatedBy();
this.createdDate = column.getCreatedDate();
this.updatedBy = column.getUpdatedBy();
this.updatedDate = column.getUpdatedDate();
this.id = column.getIdBoard();
this.idBoard = column.getIdBoard();
this.cards = cards;
}
public class CardResponse {
//id, name, createdBy,createdDate,updatedBy,updatedDate, getters
private ObjectId idColumn;
public CardResponse(KanbanCard card) {
super();
this.id = card.getId();
this.name = card.getName();
this.createdBy = card.getCreatedBy();
this.createdDate = card.getCreatedDate();
this.updatedBy = card.getUpdatedBy();
this.updatedDate = card.getUpdatedDate();
this.idColumn = card.getIdColumn();
}
I want how to do nested with MongoTemplate, I got It using business logic if board exist find by column with board id, if column exist find by card with card id.
I don't know if doing that is a good way.
KanbanBoard, KanbanColumn, is the same as the entity Board and Column, the same properties.
public BoardResponse findBoardById(ObjectId id, UserPrincipal currentUser) {
KanbanBoard board = this.kanbanBoardRepository.findById(id).orElse(null);//find board by id
//find all columns by board ID within the entity #document KanbanColumn
List<KanbanColumn> columns = this.kanbanColumnRepository.findAllByIdBoard(board.getId());
//return list of ColumnResponse DTO
List<ColumnResponse> columnResponse = columns.stream().map(column -> {
return new ColumnResponse(column);
}).collect(Collectors.toList());
//List Column map
List<ColumnResponse> columnMap = new ArrayList<>();
for (ColumnResponse column : columnResponse) {
List<CardResponse> cards = this.kanbanCardRepository.findAllByIdColumn(column.getId()).stream()
.map(card -> new CardResponse(card)).collect(Collectors.toList());
column.setCards(cards); //column set list of cards
columnMap.add(column);//add columns with list of cards inside list of column map
}
//return board with list of column map, with list of cards
return new BoardResponse(board, columnMap);
}
The result is
{
"id": "5e717d6d6e7cbf226074c3fe",
"name": null,
"createdBy": "admin",
"createdDate": 1584495981290,
"updatedBy": "admin",
"updatedDate": 1584495981290,
"columns": [
{
"id": "5e72bfa6cc3ff9000ae93c92",
"name": null,
"createdBy": "admin",
"createdDate": 1584578470269,
"updatedBy": "admin",
"updatedDate": 1584578470269,
"idBoard": null,
"cards": [
{
"id": "5e72de720715f131878b4ed2",
"name": "esse é o card",
"createdBy": "admin",
"createdDate": 1584586354958,
"updatedBy": "admin",
"updatedDate": 1584586354958,
"idColumn": "5e72bfa6cc3ff9000ae93c92"
}
]
},
{
"id": "5e72bfefcc3ff9000ae93c95",
"name": "coluna criada com sucesso.",
"createdBy": "admin",
"createdDate": 1584578543201,
"updatedBy": "admin",
"updatedDate": 1584578543201,
"idBoard": null,
"cards": [
{
"id": "5e72de550715f131878b4ed0",
"name": "esse é o card",
"createdBy": "admin",
"createdDate": 1584586325485,
"updatedBy": "admin",
"updatedDate": 1584586325485,
"idColumn": "5e72bfefcc3ff9000ae93c95"
},
{
"id": "5e72de630715f131878b4ed1",
"name": "esse é o card",
"createdBy": "admin",
"createdDate": 1584586339140,
"updatedBy": "admin",
"updatedDate": 1584586339140,
"idColumn": "5e72bfefcc3ff9000ae93c95"
}
]
}
]
}

The use of MongoTemplate, Aggregation, can be verbose, but the result is the same as MongoRepository.
public BoardResponse findBoardById(ObjectId id, UserPrincipal currentUser) {
//board
AggregationOperation boardId = Aggregation.match(Criteria.where("id").is(id));
Aggregation boardAggregation = Aggregation.newAggregation(boardId);
KanbanBoard boards = mongoTemplate.aggregate(boardAggregation, KanbanBoard.class, KanbanBoard.class)
.getUniqueMappedResult();
//column idBoard equal board ID
AggregationOperation ColumnIdBoardIsLikeBoardId = Aggregation
.match(Criteria.where("idBoard").is(boards.getId()));
Aggregation columnAggregation = Aggregation.newAggregation(ColumnIdBoardIsLikeBoardId);
List<ColumnResponse> listaColumns = mongoTemplate
.aggregate(columnAggregation, KanbanColumn.class, ColumnResponse.class).getMappedResults();
//map every column, column set list of cards
listaColumns.stream().map(column -> {
List<CardResponse> cards =
// find all cards by column ID
this.kanbanCardRepository.findAllByIdColumn(column.getId()).stream()
.map(card -> new CardResponse(card)).collect(Collectors.toList());
column.setCards(cards);
return column;
}).collect(Collectors.toList());
//board DTO set board, list of columns with list of cards
return new BoardResponse(boards, listaColumns);
}

Related

Schema validation failed by Swagger/OpenAPI online validator

I have a config my Swagger schema, /api-docs -> openapi 3.0.1:
#Configuration
public class SwaggerConfig {
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components().addSecuritySchemes("Bearer",
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("Bearer")
.in(SecurityScheme.In.HEADER).name("Authorization")))
.info(new Info().title("SSM API").version("0.1"));
}
}
Example of controller:
#Tag(name = "MetalBalance", description = "Контроллер баланса металла")
#RestController
#RequestMapping(path = "/metal-balance", produces = MediaType.APPLICATION_JSON_VALUE)
#SecurityRequirement(name = "Bearer")
#ResponseBody
#RequiredArgsConstructor
public class MetalBalanceController {
...
#DeleteMapping("/operations/{id}")
#Operation(summary = "Удаление операции над агрегатом")
#Parameters(value = {
#Parameter(in = ParameterIn.PATH, name = "id", example = "720050516121420278143",
description = "Идентификатор операции",
content = #Content(schema = #Schema(type = "integer"))),
})
#RolesAllowed({"MASTER_SHIHTA", "NACH_SMEN_VZKKC1", "MASTER_KONV", "FERRO_R6", "NACH_SMEN_VZKKC1", "FERRO_R", "DESIGNER",
"KONV_R", "LAB_ST_PR", "NACH_SMEN_VZKKC1", "NACH_UPPVS", "ING_CEN_CEH", "RAB_UPK", "UPK_R",
"MASTER_VAK", "VAK_R", "R_VAKUUMAT", "MASTER_UDM", "UDM_R"})
public void deleteOperation(#PathVariable("id") BigInteger operationId) {
balanceService.deleteUnitOperation(operationId);
}
}
openapi response, what i have from '/api-docs' endpoint:
"/metal-balance/operations/{id}": {
"delete": {
"tags": [
"MetalBalance"
],
"summary": "Удаление операции над агрегатом",
"operationId": "deleteOperation",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Идентификатор операции",
"required": true,
"example": 720050516121420300000,
"content": {
"*/*": {
"schema": {
"type": "integer"
}}}}],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"content": {
"*/*": {
"schema": {
"type": "object"
}}}}},
"security": [
{
"Bearer": []}
]}}
When I try to validate it by Swagger/OpenAPI online validator I recieved the exceptions, attached: Validation Error
How to tune my config correct?
Thanks in advance!
There's no need to wrap the schema into content in case of path parameters and primitive parameters in general (i.e. numbers/strings/booleans). The #Content annotation is typically only used with request bodies and responses.
Replace
#Parameter(in = ParameterIn.PATH, name = "id", example = "720050516121420278143",
description = "Идентификатор операции",
content = #Content(schema = #Schema(type = "integer"))),
with
#Parameter(in = ParameterIn.PATH, name = "id", example = "720050516121420278143",
description = "Идентификатор операции",
schema = #Schema(type = "integer")), // <----------

How to implement Flutter api json data to a list view at 3rd level category?

I'm trying to build a flutter app, the idea is to have:
List of Hospitals and a Hospital has many doctors.
Doctors are categories by departments, Example: Cardiology department, Neurology department.
The idea is to have a list of hospitals list from api json data, after clicking a specific hospital, there should be list of departments (api json data) and after selecting a specific department from the list - it will load all the doctors in a list from that department.
Can anyone help please? I'm using Laravel 9 as a backend.
don't know if it can help you
- Json code
{
"hospital":[
{
"name": "hospital1",
"department":[
{
"name": "department1",
"doctors":[
{
"name": "doctor1"
},
{
"name": "doctor2"
}
]
},
{
"name": "department2",
"doctors":[
{
"name": "doctor1"
},
{
"name": "doctor2"
}
]
}
]
},
{
"name": "hospital2",
"department":[
{
"name": "department1",
"doctors":[
{
"name": "doctor1"
},
{
"name": "doctor2"
}
]
},
{
"name": "department2",
"doctors":[
{
"name": "doctor1"
},
{
"name": "doctor2"
}
]
}
]
}
]
}
- Dart code
class DataResponse{
DataResponse({
this.hospital,
});
List<Hospital> hospital;
factory DataResponse.fromJson(Map<String, dynamic> json) => Welcome(
hospital: List<Hospital>.from(json["hospital"].map((x) => Hospital.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"hospital": List<dynamic>.from(hospital.map((x) => x.toJson())),
};
}
class Hospital {
Hospital({
this.name,
this.department,
});
String name;
List<Department> department;
factory Hospital.fromJson(Map<String, dynamic> json) => Hospital(
name: json["name"],
department: List<Department>.from(json["department"].map((x) => Department.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"name": name,
"department": List<dynamic>.from(department.map((x) => x.toJson())),
};
}
class Department {
Department({
this.name,
this.doctors,
});
String name;
List<Doctor> doctors;
factory Department.fromJson(Map<String, dynamic> json) => Department(
name: json["name"],
doctors: List<Doctor>.from(json["doctors"].map((x) => Doctor.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"name": name,
"doctors": List<dynamic>.from(doctors.map((x) => x.toJson())),
};
}
class Doctor {
Doctor({
this.name,
});
String name;
factory Doctor.fromJson(Map<String, dynamic> json) => Doctor(
name: json["name"],
);
Map<String, dynamic> toJson() => {
"name": name,
};
}

How to create a ResponseBody without an "entity" tag

I have built a controller which returns a list of objects
fun getUserCommunicationSettings(
#PathVariable commId: String,
#CommunicationTypeConstraint #RequestParam(required = false) commType: CommunicationType?
): ResponseEntity<out UserCommResponse> {
return communicationSettingsService.getUserCommSettings(commType, commId)
.mapError { mapUserCommErrorToResponse(it) }
.map { ResponseEntity.ok(SuccessResponse(it)) }
.fold<ResponseEntity<SuccessResponse<List<CommunicationSettingsDto>>>, ResponseEntity<ErrorResponse>, ResponseEntity<out UserCommResponse>>(
{ it },
{ it },
)
}
problem is it returns the following json with "entity" tag which i'd like to get rid of
{
"entity": [
{
"userId": "1075",
"userType": "CUSTOMER",
"communicationId": "972547784682",
"communicationType": "CALL",
"messageType": "CALL_COLLECTION"
}
]
}
any ideas?

How to assign a single object to a list?

I'm working with an API rest and it returns me two types of json, object and array.
Object(When there is only one record in the database):
{ "complain": { "id": "1" , "description": "hello", "date": "2017-01-24 11:46:22", "Lat": "20.5204446", "Long": "-100.8249097" } }
Array(When there is more than one record in the database):
{ "complain": [ { "id": "1" , "description": "hello", "date": "2017-01-24 11:46:22", "Lat": "20.587446", "Long": "-100.8246490" }, { "id": "2" , "description": "hello 2", "date": "2017-01-24 11:50:12", "Lat": "20.529876", "Long": "-100.8249097" } ] }
The code I use to consume the json is as follows:
content = await response.Content.ReadAsStringAsync();
var token = JToken.Parse(content);
if (token["complain"] is JArray)
{
var jsonArray = JsonConvert.DeserializeObject<RootArray>(content);
}
else if (token["complain"] is JObject)
{
var jsonObject = JsonConvert.DeserializeObject<RootObject>(content);
}
When it comes to a json array if I can add it to a listview:
myList.ItemsSource = jsonArray.listArray;
But if it is an object I can not and I get the following error:
Cannot implicitly convert type Object to IEnumerable.
Finally I was able to solve my error, it was just a matter of creating a list and adding the deserialized json object.
var jsonObject = JsonConvert.DeserializeObject<RootObject>(content);
List<Complain> simpleList = new List<Complain>();
simpleList.Add(jsonObject.ComplainObject);
myList.ItemsSource = simpleList;
The class to deserialize the Json object:
public class RootObject
{
[JsonProperty("complain")]
public Complain ComplainObject { get; set; }
}

How to provide highlighting with Spring data elasticsearch

it seems that SpringData ES don't provide classes to fetch highlights returned by ES. Spring Data can return Lists of Objects but the highlights sections in the Json returned by ES is in a separated part that is not handled by the "ElasticSearchTemplate" class.
Code example :-
QueryBuilder query = QueryBuilders.matchQuery("name","tom");
SearchQuery searchQuery =new NativeSearchQueryBuilder().withQuery(query).
with HighlightFields(new Field("name")).build();
List<ESDocument> publications = elasticsearchTemplate.queryForList
(searchQuery, ESDocument.class);
I might be wrong, but I can't figure out to do only with SpringDataES. Someone can post an example of how we can get highlights with Spring Data ES ?
Thanks in advance !
From the test cases in spring data elasticsearch I've found solution to this :
This can be helpful.
#Test
public void shouldReturnHighlightedFieldsForGivenQueryAndFields() {
//given
String documentId = randomNumeric(5);
String actualMessage = "some test message";
String highlightedMessage = "some <em>test</em> message";
SampleEntity sampleEntity = SampleEntity.builder().id(documentId)
.message(actualMessage)
.version(System.currentTimeMillis()).build();
IndexQuery indexQuery = getIndexQuery(sampleEntity);
elasticsearchTemplate.index(indexQuery);
elasticsearchTemplate.refresh(SampleEntity.class);
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(termQuery("message", "test"))
.withHighlightFields(new HighlightBuilder.Field("message"))
.build();
Page<SampleEntity> sampleEntities = elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class, new SearchResultMapper() {
#Override
public <T> Page<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<SampleEntity> chunk = new ArrayList<SampleEntity>();
for (SearchHit searchHit : response.getHits()) {
if (response.getHits().getHits().length <= 0) {
return null;
}
SampleEntity user = new SampleEntity();
user.setId(searchHit.getId());
user.setMessage((String) searchHit.getSource().get("message"));
user.setHighlightedMessage(searchHit.getHighlightFields().get("message").fragments()[0].toString());
chunk.add(user);
}
if (chunk.size() > 0) {
return new PageImpl<T>((List<T>) chunk);
}
return null;
}
});
assertThat(sampleEntities.getContent().get(0).getHighlightedMessage(), is(highlightedMessage));
}
Spring Data Elasticsearch 4.0 now has the SearchPage result type, which makes things a little easier if we need to return highlighted results:
This is a working sample:
String query = "(id:123 OR id:456) AND (database:UCLF) AND (services:(sealer?), services:electronic*)"
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withPageable(pageable)
.withQuery(queryStringQuery(query))
.withSourceFilter(sourceFilter)
.withHighlightFields(new HighlightBuilder.Field("goodsAndServices"))
.build();
SearchHits<Trademark> searchHits = template.search(searchQuery, Trademark.class, IndexCoordinates.of("trademark"));
SearchPage<Trademark> page = SearchHitSupport.searchPageFor(searchHits, searchQuery.getPageable());
return (Page<Trademark>) SearchHitSupport.unwrapSearchHits(page);
And this would be the response from Page object in json:
{
"content": [
{
"id": "123",
"score": 12.10748,
"sortValues": [],
"content": {
"_id": "1P0XzXIBdRyrchmFplEA",
"trademarkIdentifier": "abc234",
"goodsAndServices": null,
"language": "EN",
"niceClass": "2",
"sequence": null,
"database": "UCLF",
"taggedResult": null
},
"highlightFields": {
"goodsAndServices": [
"VARNISHES, <em>SEALERS</em>, AND NATURAL WOOD FINISHES"
]
}
}
],
"pageable": {
"sort": {
"unsorted": true,
"sorted": false,
"empty": true
},
"offset": 0,
"pageNumber": 0,
"pageSize": 20,
"unpaged": false,
"paged": true
},
"searchHits": {
"totalHits": 1,
"totalHitsRelation": "EQUAL_TO",
"maxScore": 12.10748,
"scrollId": null,
"searchHits": [
{
"id": "123",
"score": 12.10748,
"sortValues": [],
"content": {
"_id": "1P0XzXIBdRyrchmFplEA",
"trademarkIdentifier": "abc234",
"goodsAndServices": null,
"language": "EN",
"niceClass": "2",
"sequence": null,
"database": "UCLF",
"taggedResult": null
},
"highlightFields": {
"goodsAndServices": [
"VARNISHES, <em>SEALERS</em>, AND NATURAL WOOD FINISHES"
]
}
}
],
"aggregations": null,
"empty": false
},
"totalPages": 1,
"totalElements": 1,
"size": 20,
"number": 0,
"numberOfElements": 1,
"last": true,
"first": true,
"sort": {
"unsorted": true,
"sorted": false,
"empty": true
},
"empty": false
}
Actually, you could do the following, with a custom ResultExtractor:
QueryBuilder query = QueryBuilders.matchQuery("name", "tom");
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(query)
.withHighlightFields(new Field("name")).build();
return elasticsearchTemplate.query(searchQuery.build(), new CustomResultExtractor());
And then
public class CustomResultExtractor implements ResultsExtractor<List<MyClass>> {
private final DefaultEntityMapper defaultEntityMapper;
public CustomResultExtractor() {
defaultEntityMapper = new DefaultEntityMapper();
}
#Override
public List<MyClass> extract(SearchResponse response) {
return StreamSupport.stream(response.getHits().spliterator(), false)
.map(this::searchHitToMyClass)
.collect(Collectors.toList());
}
private MyClass searchHitToMyClass(SearchHit searchHit) {
MyElasticSearchObject myObject;
try {
myObject = defaultEntityMapper.mapToObject(searchHit.getSourceAsString(), MyElasticSearchObject.class);
} catch (IOException e) {
throw new ElasticsearchException("failed to map source [ " + searchHit.getSourceAsString() + "] to class " + MyElasticSearchObject.class.getSimpleName(), e);
}
List<String> highlights = searchHit.getHighlightFields().values()
.stream()
.flatMap(highlightField -> Arrays.stream(highlightField.fragments()))
.map(Text::string)
.collect(Collectors.toList());
// Or whatever you want to do with the highlights
return new MyClass(myObject, highlights);
}}
Note that I used a list but you could use any other iterable data structure. Also, you could do something else with the highlights. Here I'm simply listing them.
https://stackoverflow.com/a/37163711/6643675
The first answer does works,but I found some pageable problems with its returned result,which display with the wrong total elements and toalpages.Arter I checkout the DefaultResultMapper implementation, the returned statement shoud be return new AggregatedPageImpl((List<T>) chunk, pageable, totalHits, response.getAggregations(), response.getScrollId(), maxScore);,and then it works with paging.wish i could help you guys~

Resources