Returning HAL json from HttpServletResponse - spring

I am trying to return a response body in my "successfullAuthentication" method in "UsernamePasswordAuthenticationFilter" using HATEOAS, but is returning the links in this format:
"links": [
{
"rel": "self",
"href": "http://localhost:8080/api/users/5c55ee26911e9f04acb77c91",
"hreflang": null,
"media": null,
"title": null,
"type": null,
"deprecation": null
},
I would like it to return HAL json format so it would look like this:
"_links": {
"self": {
"href": "http://localhost:8080/api/users/5c55ee26911e9f04acb77c91"
},
I have this in my method (response is HttpServletResponse):
User user = userService.findById(authResult.getName());
String json = Jackson.toJsonString(userResourceAssembler.toResource(user));
response.setContentType("application/hal+json");
response.setCharacterEncoding("UTF-8");
response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
response.getWriter().write(json);
I also have this in my WebConfig: #EnableHypermediaSupport(type = { EnableHypermediaSupport.HypermediaType.HAL })
Does anyone know why this is happening?

I found the answer in this github issue: https://github.com/spring-projects/spring-hateoas/issues/270#issuecomment-145606558
Basically:
private String convertToHalString(ResourceSupport resource) {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(
new EvoInflectorRelProvider(), null, null));
String resourceString = null;
try {
resourceString = mapper.writeValueAsString(resource);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return resourceString;
}

Try to extend all model classes- to add HATEOAS links –
with org.springframework.hateoas.ResourceSupport class
Presuming you have respective URI's in controller class
Link link = ControllerLinkBuilder
.linkTo(UserController.class)
.slash(user.getXXX())
.withSelfRel();
//for single resource
user.add(link);
Link userLink = ControllerLinkBuilder
.linkTo(ControllerLinkBuilder
.methodOn(UserController.class).getAllUsers())
.withSelfRel();
//For collections
userList.add(userLink);
Reference: https://spring.io/understanding/HATEOAS

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")), // <----------

Dynamics 365 Search Resource Availability

I am attempting to implement the Search Resource Capability as described here: https://cloudblogs.microsoft.com/dynamics365/it/2019/05/21/retrieve-resource-availability-with-universal-resource-scheduling-api/
There is an example here of how to implement it via JavaScript (although the JavaScript libraries are probably deprecated or unsupported), which I have referenced here: https://cloudblogs.microsoft.com/dynamics365/it/2019/07/15/how-to-use-resource-schedulings-search-resource-availability-api/
I have written a .NET Core Class Library that uses the Dynamics 365 OData Service to POST to the msdyn_SearchResourceAvailability Action.
I have seen some examples on the internet, but they all use the Dynamics 365 SDK, not the Dynamics 365 Web API.
I am getting an error and have therefore extracted the JSON that is being posted and tried the same call in Postman, where I am getting the same error:
{
"error": {
"code": "0x0",
"message": "An error occurred while validating input parameters: Microsoft.OData.ODataException: Does not support untyped value in non-open type.\r\n at System.Web.OData.Formatter.Deserialization.DeserializationHelpers.ApplyProperty(ODataProperty property, IEdmStructuredTypeReference resourceType, Object resource, ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext)\r\n at System.Web.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyStructuralProperties(Object resource, ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)\r\n at Microsoft.Crm.Extensibility.CrmODataEntityDeserializer.ApplyStructuralProperties(Object resource, ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)\r\n at System.Web.OData.Formatter.Deserialization.ODataResourceDeserializer.ReadResource(ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)\r\n at Microsoft.Crm.Extensibility.ODataV4.CrmODataActionPayloadDeserializer.ReadEntry(ODataDeserializerContext readContext, ODataParameterReader reader, IEdmOperationParameter parameter)\r\n at Microsoft.Crm.Extensibility.ODataV4.CrmODataActionPayloadDeserializer.Read(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext)\r\n at System.Web.OData.Formatter.ODataMediaTypeFormatter.ReadFromStream(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)",
"innererror": {
"message": "An error occurred while validating input parameters: Microsoft.OData.ODataException: Does not support untyped value in non-open type.\r\n at System.Web.OData.Formatter.Deserialization.DeserializationHelpers.ApplyProperty(ODataProperty property, IEdmStructuredTypeReference resourceType, Object resource, ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext)\r\n at System.Web.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyStructuralProperties(Object resource, ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)\r\n at Microsoft.Crm.Extensibility.CrmODataEntityDeserializer.ApplyStructuralProperties(Object resource, ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)\r\n at System.Web.OData.Formatter.Deserialization.ODataResourceDeserializer.ReadResource(ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)\r\n at Microsoft.Crm.Extensibility.ODataV4.CrmODataActionPayloadDeserializer.ReadEntry(ODataDeserializerContext readContext, ODataParameterReader reader, IEdmOperationParameter parameter)\r\n at Microsoft.Crm.Extensibility.ODataV4.CrmODataActionPayloadDeserializer.Read(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext)\r\n at System.Web.OData.Formatter.ODataMediaTypeFormatter.ReadFromStream(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)",
"type": "Microsoft.Crm.CrmHttpException",
"stacktrace": " at Microsoft.Crm.Extensibility.OData.CrmODataUtilities.ValidateInputParameters(ModelStateDictionary controllerModelState)\r\n at Microsoft.Crm.Extensibility.OData.ActionController.<>c__DisplayClass9_0.<PostUnboundAction>b__0()\r\n at Microsoft.PowerApps.CoreFramework.ActivityLoggerExtensions.Execute[TResult](ILogger logger, EventId eventId, ActivityType activityType, Func`1 func, IEnumerable`1 additionalCustomProperties)\r\n at Microsoft.Xrm.Telemetry.XrmTelemetryExtensions.Execute[TResult](ILogger logger, XrmTelemetryActivityType activityType, Func`1 func)\r\n at lambda_method(Closure , Object , Object[] )\r\n at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass10.<GetExecutor>b__9(Object instance, Object[] methodParameters)\r\n at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(HttpControllerContext controllerContext, IDictionary`2 arguments, CancellationToken cancellationToken)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()"
}
}
}
The JSON that is being posted is as follows:
{
"Version": "1",
"Requirement": {
"msdyn_duration": 180,
"msdyn_effort": 1,
"msdyn_fromdate": "2020-03-10T00:00:00+00:00",
"msdyn_latitude": 55.784129,
"msdyn_longitude": -3.982742,
"msdyn_name": "Super Heroes Resource Requirement",
"msdyn_remainingduration": 180,
"msdyn_todate": "2020-03-12T00:00:00+00:00",
"msdyn_worklocation": 690970002
},
"Settings": {
"ConsiderSlotsWithLessThanRequiredCapacity": false,
"ConsiderSlotsWithLessThanRequiredDuration": false,
"ConsiderTravelTime": false,
"ConsiderSlotsWithOverlappingBooking": false,
"ConsiderSlotsWithProposedBookings": false,
"MovePastStartDateToCurrentDate": false,
"UseRealTimeResourceLocation": false,
"MaxResourceTravelRadius": {
"Value": 10,
"Unit": 192350000
},
"SortOrder": {
"value": [
{
"Name": "bookableresource",
"SortOrder": 0
}
]
}
},
"ResourceSpecification": {
"ResourceTypes": {
"value": [
2,
3,
5
]
},
"PreferredResources": {
"value": [
{
"bookableresourceid": "d7315245-b162-ea11-a811-000d3a0bad7c"
},
{
"bookableresourceid": "b54bc744-b162-ea11-a811-000d3a0ba110"
}
]
},
"RestrictedResources": {
"value": [
{
"bookableresourceid": "ba6d4a4b-b162-ea11-a811-000d3a0bad7c"
},
{
"bookableresourceid": "ca6d4a4b-b162-ea11-a811-000d3a0bad7c"
}
]
},
"Constraints": {
"Characteristics": {
"value": [
{
"characteristicid": "a02db73e-b162-ea11-a811-000d3a0ba110"
}
]
},
"Roles": {
"value": [
{
"bookableresourcecategoryid": "d56d4a4b-b162-ea11-a811-000d3a0bad7c"
}
]
},
"Territories": {
"value": [
{
"territoryid": "7c2db73e-b162-ea11-a811-000d3a0ba110"
}
]
},
"UnspecifiedTerritory": false,
"OrganizationalUnits": {
"value": [
{
"msdyn_organizationalunitid": "822db73e-b162-ea11-a811-000d3a0ba110"
}
]
},
"BusinessUnits": {
"value": [
{
"businessunitid": "fba6cf4b-f24a-ea11-a813-00224801cd21"
}
]
}
}
}
}
Could anyone please advise where I am going wrong?
I was able to upgrade to Field service v8.8.x from v8.7.x and test the CRM action msdyn_SearchResourceAvailability using web api with the following payload. I don't have all the config & data setup it seems but the web api is resulting good response (different than 400 = Bad request.. lol)
var parameters = {};
parameters.Version = "2";
var requirement = {};
requirement.msdyn_resourcerequirementid = "B9E6F413-0063-EA11-A811-000D3A5A1CAC"; //Delete if creating new record
requirement["#odata.type"] = "Microsoft.Dynamics.CRM.msdyn_resourcerequirement";
parameters.Requirement = requirement;
var settings = {};
settings.systemuserid = "26ADDD07-D9F4-E711-8138-E0071B715B11"; //Delete if creating new record
settings["#odata.type"] = "Microsoft.Dynamics.CRM.systemuser";
parameters.Settings = settings;
var req = new XMLHttpRequest();
req.open("POST", Xrm.Page.context.getClientUrl() + "/api/data/v9.1/msdyn_SearchResourceAvailability", true);
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.onreadystatechange = function() {
if (this.readyState === 4) {
req.onreadystatechange = null;
if (this.status === 200) {
var results = JSON.parse(this.response);
alert(this.response)
} else {
alert(this.status);
}
}
};
req.send(JSON.stringify(parameters));
Response:
{
"#odata.context": "https://crmdev.crm.dynamics.com/api/data/v9.1/$metadata#Microsoft.Dynamics.CRM.msdyn_SearchResourceAvailabilityResponse",
"TimeSlots": [
{
"#odata.type": "#Microsoft.Dynamics.CRM.organization"
},
{
"#odata.type": "#Microsoft.Dynamics.CRM.organization"
},
{
"#odata.type": "#Microsoft.Dynamics.CRM.organization"
},
{
"#odata.type": "#Microsoft.Dynamics.CRM.organization"
},
{
"#odata.type": "#Microsoft.Dynamics.CRM.organization"
}
],
"Resources": [
{
"#odata.type": "#Microsoft.Dynamics.CRM.organization"
}
],
"Related": {
"#odata.type": "#Microsoft.Dynamics.CRM.organization"
},
"Exceptions": {
"#odata.type": "#Microsoft.Dynamics.CRM.organization"
}
}
Update:
Only Version, Requirement, and Settings are required for this call, so start with minimal code input & enhance it
While troubleshooting the error message Microsoft.OData.ODataException: Does not support untyped value in non-open type, proceed in this direction - typo in some schema name could be the reason
Not sure if this msdyn_SearchResourceAvailability action message is yet available in web api, but only OrganizationRequest SDK is tried out Reference
This is the sample request along with required payload:
var parameters = {};
var workorder = {};
workorder.msdyn_workorderid = "ADE6F413-0063-EA11-A811-000D3A5A1CAC"; //Delete if creating new record
workorder["#odata.type"] = "Microsoft.Dynamics.CRM.msdyn_workorder";
parameters.WorkOrder = workorder;
parameters.RealTimeMode = true;
parameters.Duration = 30;
parameters.IgnoreDuration = true;
parameters.IgnoreTravelTime = true;
parameters.AllowOverlapping = true;
parameters.Radius = 0;
parameters.StartTime = new Date("3/10/2020").toISOString();
parameters.EndTime = new Date("3/10/2020").toISOString();
var resources1 = {};
resources1.systemuserid = "3BD2ADED-20B2-E911-A98E-000D3A374B53"; //Delete if creating new record
resources1["#odata.type"] = "Microsoft.Dynamics.CRM.systemuser";
parameters.Resources = [resources1];
var req = new XMLHttpRequest();
req.open("POST", Xrm.Page.context.getClientUrl() + "/api/data/v9.1/msdyn_RetrieveResourceAvailability", true);
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.onreadystatechange = function() {
if (this.readyState === 4) {
req.onreadystatechange = null;
if (this.status === 200) {
var results = JSON.parse(this.response);
alert("response: "+this.response)
} else {
alert(this.status);
}
}
};
req.send(JSON.stringify(parameters));
I just generated this with my sandbox without any data, but you can try the CRM REST builder for building up the request.
Part of the problem is that the C# samples out there use the SDK, which in turn uses the deprecated 2011 WCF service, not the current OData Service. It looks as if the OData Service is more strict in terms of the #odata.type you specify, in that it looks to check that the attributes you are providing are real attributes/fields of that entity. The problem with Settings and ResourceSpecification is that in the Action, the type of entity is not specified.
You need to specify a couple of things:
You should now be able to call msdyn_SearchResourceAvailability via Web API.
Key Tasks:
Vesrion 3 is being used
“IsWebApi”: true is specified
Proper “#odata.type” annotations for nested objects are provided
Sample:
{
"Version": "3",
"IsWebApi": true,
"Requirement": {
"msdyn_fromdate": "2021-08-17T00:00:00Z",
"msdyn_todate": "2021-08-18T23:59:00Z",
"msdyn_remainingduration": 60,
"msdyn_duration": 60,
"#odata.type": "Microsoft.Dynamics.CRM.msdyn_resourcerequirement"
},
"Settings": {
"ConsiderSlotsWithProposedBookings": false,
"MovePastStartDateToCurrentDate": true,
"#odata.type": "Microsoft.Dynamics.CRM.expando"
},
"ResourceSpecification": {
"#odata.type": "Microsoft.Dynamics.CRM.expando",
"ResourceTypes#odata.type": "Collection(Microsoft.Dynamics.CRM.expando)",
"ResourceTypes": [
{
"#odata.type": "Microsoft.Dynamics.CRM.expando",
"value": "1"
},
{
"#odata.type": "Microsoft.Dynamics.CRM.expando",
"value": "2"
}
]
}
}

kendo grid Filter not pass into method

I want to pass kendo grid DataSourceRequest to web api
my web api iS:
[HttpPost]
public HttpResponseMessage GetAll([FromBody] DataSourceRequest request)
{
try
{
var itemList = new JsonListFormat<ItemVm>
{
Data = new ItemCrud().GetItemList(request),
Total = new ItemCrud().GetItemTotalCount()
};
return Request.CreateResponse(HttpStatusCode.OK, itemList);
}
catch (Exception ex)
{
return HttpResponseController.HttpResponseException(Request, ex);
}
}
but request.Filters is always null.
for test I call my web api method with postman and this json data:
{
"page": 10,
"pageSize": 20,
"sorts": [
{
"member": "Title",
"sortDirection": 0
}
],
"filters": [
{
"convertedValue": "test",
"member": "Title",
"memberType": null,
"operator": 2,
"value": "test"
}
],
"groups": null,
"aggregates": []
}
everything pass to request parameter but rquest.Filters is null !!!
anyone can explain what's my problem.
thanks
Did you try this option?
dataSource.serverFiltering = true;
please check Server filtering

Error on generating self link on pageable resource

Make a simple RestController
#RestController
public class Controloler
#Value
class MyData {
int value;
}
#GetMapping(value = "/datas", produces = MediaTypes.HAL_JSON_VALUE)
public PagedResources<Resource<MyData>> getMyData(PagedResourcesAssembler<MyData> assembler,
#RequestParam(required = false) String param,
#PageableDefault Pageable pageRequest)
{
MyData data = new MyData(1);
Page<MyData> page = new PageImpl<>(Collections.singletonList(data), pageRequest, 100);
Link selfLink = linkTo(methodOn(Controloler.class).getMyData(assembler, param, pageRequest)).withSelfRel();
return assembler.toResource(page, selfLink);
}
}
When I try to get page curl "http://localhost:8080/datas?param=12&page=2" have a problem with self link generation
{
"_embedded": {
"myDataList": [
{
"value": 1
}
]
},
"_links": {
"first": {
"href": "http://localhost:8080/datas?param=12&page=0&size=10"
},
"prev": {
"href": "http://localhost:8080/datas?param=12&page=1&size=10"
},
"self": {
"href": "http://localhost:8080/datas?param=12"
},
"next": {
"href": "http://localhost:8080/datas?param=12&page=3&size=10"
},
"last": {
"href": "http://localhost:8080/datas?param=12&page=9&size=10"
}
},
"page": {
"size": 10,
"totalElements": 100,
"totalPages": 10,
"number": 2
}
}
In my opinion, self link should be http://localhost:8080/datas?param=12&page=2&size=10.
Just now I can solve this problem without using pageable in arguments, just exact params page and size. But, I hope there is some solution with pageable
I've seen that in case of spring-data-rest self have a type of template. But I'd like to get the url I've requested
In my opinion, self link should be http://localhost:8080/datas?param=12&page=2&size=10.
I agree. In fact, it seems to be a bug. The most recent version of PagedResourcesAssembler does it differently:
Link selfLink = link.map(it -> it.withSelfRel())//
.orElseGet(() -> createLink(base, page.getPageable(), Link.REL_SELF));
(source)
Buggy versions of that class are doing this:
resources.add(createLink(base, null, Link.REL_SELF));
The createLink method is never passed the needed Pageable, but null as the second argument.
So, if you can't upgrade to the most recent version you can still work-around it:
Link selfLink = linkTo(methodOn(Controloler.class).getMyData(assembler, param, pageRequest)).withSelfRel();
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(selfLink.expand().getHref());
new HateoasPageableHandlerMethodArgumentResolver().enhance(builder, null, pageRequest);
Link newSelfLink = new Link(builder.build().toString());
According to Oliver's comment in an issue opened to address this, the self link should not contain template information, and this is not a bug.

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