REST hypermedia/links to collections - java

Part of REST best practice is to make use of links in the responses to allow clients to navigate from one entity to another.
For example if I had a customer object type which has child account. If I was to request a customer using /customers/1 then I might provide the following response
{
"self": "http://localhost:43002/rest/v1/customers/1",
"id": 1,
"name": "Isabella Button",
"number": "000001",
"forename": "Isabella",
"surname": "Button",
"accounts": [
{
"self": "http://localhost:43002/rest/v1/accounts/1",
"id": 1,
"name": "Main Account",
"number": "000001",
"currency": "GBP",
"fromDate": "2013-01-01",
"toDate": "9999-01-01",
"createdDttm": "2013-01-01T00:00:00.000"
}
]
}
Note the self property holds the links.
However let's say I didn't want to return the accounts in the customer query, perhaps the number of accounts might be very large so I don't want to return them by default.
{
"self": "http://localhost:43002/rest/v1/customers/1",
"id": 1,
"name": "Isabella Button",
"number": "000001",
"forename": "Isabella",
"surname": "Button"
}
A resource URL for a customer's accounts could be /customers/1/accounts
However with the customer response above the client would be unable to discover the /customers/1/accounts link.
Is there a best practice for providing hyperlinks in a response that point to "child" collections of the returned resource?

One practice is to use a links element like this:
{
"self": "http://localhost:43002/rest/v1/customers/1",
"id": 1,
"name": "Isabella Button",
"number": "000001",
"forename": "Isabella",
"surname": "Button",
"links" : [
{
"rel" : "http://www.yourapi.com/rels/accounts",
"href" : "http://localhost:43002/rest/v1/customers/1/accounts"
},
{
"rel" : "http://www.yourapi.com/rels/someOtherCollection",
"href" : "http://localhost:43002/rest/v1/customers/1/someOtherCollection",
}
]
}
Or, if you find easier to construct/read the response, you can put the same links as Link http headers.

provide a links attribute like in this example http://caines.ca/blog/programming/json-is-under-defined-for-rest/
{
"links": {
"self" : { "href": "{id}" },
"up" : { "href": "{upId}" },
"children" : { "href": "{id}/children" }
}
}

Related

Concatenating a string to a JOLT value output

I using JOLT to transform and extract a URL of an object, but it's only the URL slug. Is it possible to concatenate a static string to that slug to have a full URL?
The current output is:
{
"title" : "Where is deleted user data visible, and for how long is it there?",
"externalId" : "where_is_deleted_user_data_visible__and_for_how_lo",
"summary" : "Deleted user data is stored in LP and visible for 13 months following deletion.",
"detail" : "<p>Deleted user data is stored in LP and visible for <strong>13 months following deletion.</strong></p>\n<p>Deleted agents are displayed with a '(deleted)' indicator in the Open Conversations, All Conversations, Web History lists, and the Conversation History widget.</p>\n<object type=\"application/kenticocloud\" data-type=\"item\" data-rel=\"component\" data-codename=\"n6fdd091f_fcb5_012a_8745_c1671c268e6b\"></object>\n<p>Filter by deleted agents is available in the Open Conversations, All Conversations, and web history lists.</p>\n<p>In the visitor conversation window, the name of the deleted agent will be displayed in the window header (with no indication that they have been deleted).</p>\n<p>The LP user limit '<code>collections.maxElements</code>' will not count the number of deleted users in the total.</p>\n<object type=\"application/kenticocloud\" data-type=\"item\" data-rel=\"component\" data-codename=\"n3f1dea42_ac2e_01c6_cbd6_3f802d4e8fb3\"></object>",
"url" : "faqs-data-reporting1.html"
}
And the expected output is:
{
"title" : "Where is deleted user data visible, and for how long is it there?",
"externalId" : "where_is_deleted_user_data_visible__and_for_how_lo",
"summary" : "Deleted user data is stored in LP and visible for 13 months following deletion.",
"detail" : "<p>Deleted user data is stored in LP and visible for <strong>13 months following deletion.</strong></p>\n<p>Deleted agents are displayed with a '(deleted)' indicator in the Open Conversations, All Conversations, Web History lists, and the Conversation History widget.</p>\n<object type=\"application/kenticocloud\" data-type=\"item\" data-rel=\"component\" data-codename=\"n6fdd091f_fcb5_012a_8745_c1671c268e6b\"></object>\n<p>Filter by deleted agents is available in the Open Conversations, All Conversations, and web history lists.</p>\n<p>In the visitor conversation window, the name of the deleted agent will be displayed in the window header (with no indication that they have been deleted).</p>\n<p>The LP user limit '<code>collections.maxElements</code>' will not count the number of deleted users in the total.</p>\n<object type=\"application/kenticocloud\" data-type=\"item\" data-rel=\"component\" data-codename=\"n3f1dea42_ac2e_01c6_cbd6_3f802d4e8fb3\"></object>",
"url" : "https://www.static-url.com/faqs-data-reporting1.html"
}
The JOLT Spec is:
[
{
"operation": "shift",
"spec": {
"item": {
"system": {
"name": "title",
"codename": "externalId"
},
"elements": {
"short_answer": {
"value": ["summary"]
},
"long_answer": {
"value": "detail"
}
}
},
"modular_content": {
"*": {
"elements": {
"permalink": {
"value": "url"
}
}
}
}
}
}
]
The JSON input is:
{
"item": {
"system": {
"id": "6e55g261aee-34d9-47be-8830-145ec71b564f",
"name": "Where is deleted user data visible, and for how long is it there?",
"codename": "where_is_deleted_user_data_visible__and_for_how_lo",
"language": "en-US",
"type": "faq",
"collection": "default",
"sitemap_locations": [],
"last_modified": "2022-04-11T15:32:08.0844256Z",
"workflow_step": "published"
},
"elements": {
"question": {
"type": "text",
"name": "Question",
"value": "Where is deleted user data visible, and for how long is it there?"
},
"short_answer": {
"type": "text",
"name": "Bot Teaser Answer",
"value": "Deleted user data is stored in LP and visible for 13 months following deletion."
},
"long_answer": {
"type": "rich_text",
"name": "Full Answer",
"images": {},
"links": {},
"modular_content": [
"n6fdd091f_fcb5_012a_87545_c1671c268e6b",
"n3f1dea42_ac2e_01cdf6_cbd6_3f802d4e8fb3"
],
"value": "<p>Deleted user data is stored in LP and visible for <strong>13 months following deletion.</strong></p>\n<p>Deleted agents are displayed with a '(deleted)' indicator in the Open Conversations, All Conversations, Web History lists, and the Conversation History widget.</p>\n<object type=\"application/kenticocloud\" data-type=\"item\" data-rel=\"component\" data-codename=\"n6fdd091f_fcb5_012a_8745_c1671c268e6b\"></object>\n<p>Filter by deleted agents is available in the Open Conversations, All Conversations, and web history lists.</p>\n<p>In the visitor conversation window, the name of the deleted agent will be displayed in the window header (with no indication that they have been deleted).</p>\n<p>The LP user limit '<code>collections.maxElements</code>' will not count the number of deleted users in the total.</p>\n<object type=\"application/kenticocloud\" data-type=\"item\" data-rel=\"component\" data-codename=\"n3f1dea42_ac2e_01c6_cbd6_3f802d4e8fb3\"></object>"
},
"faq_page": {
"type": "modular_content",
"name": "This FAQ item's FAQ page(s)",
"value": [
"data___reporting_9658b36"
]
},
"related_article": {
"type": "modular_content",
"name": "Related article",
"value": []
}
}
},
"modular_content": {
"data___reporting_9658b36": {
"system": {
"id": "9658bddf362-735b-47fd3c-9232-26de14835d8f",
"name": "Data & Reporting",
"codename": "data___reporting_9658b36",
"language": "en-US",
"type": "kc_faqs",
"collection": "default",
"sitemap_locations": [],
"last_modified": "2022-04-13T16:18:12.2538592Z",
"workflow_step": "published"
},
"elements": {
"pagename": {
"type": "text",
"name": "Title",
"value": "Data & Reporting"
},
"faq_items": {
"type": "modular_content",
"name": "FAQ Items",
"value": [
"how_do_i_find_chats_that_only_contain_private_mess",
"how_do_i_understand_why_a_chat_ended_",
"how_do_i_change_the_email_address_for_scheduled_re",
"how_do_i_retrieve_data_that_has_been_masked_",
"where_is_deleted_user_data_visible__and_for_how_lo"
]
},
"related_articles": {
"type": "modular_content",
"name": "Related Articles",
"value": []
},
"permalink": {
"type": "url_slug",
"name": "URL slug",
"value": "faqs-data-reporting1.html"
},
"redirects": {
"type": "text",
"name": "redirects",
"value": "faqs-data-reporting1.html"
}
}
},
"n3f1dea42_ac2e_01c6_cbd6_3f802d4e8fb3": {
"system": {
"id": "3f1dea42-ac2e-01c6-cbd6-3f802d4e8fb3",
"name": "3f1dea42-ac2e-01c6-cbd6-3f802d4e8fb3",
"codename": "n3f1dea42_ac2e_01c6_cbd6_3f802d4e8fb3",
"language": "en-US",
"type": "contentbox",
"collection": "default",
"sitemap_locations": [],
"last_modified": "2022-04-11T15:32:08.0844256Z"
},
"elements": {
"type": {
"type": "multiple_choice",
"name": "Type",
"value": [
{
"name": "Warning (orange)",
"codename": "warning"
}
]
},
"notice_text": {
"type": "rich_text",
"name": "Text",
"images": {},
"links": {},
"modular_content": [],
"value": "<p>A user who joined a chat and then was deleted won’t be reflected as deleted in the history.</p>"
}
}
},
"n6fdd091f_fcb5_012a_8745_c1671c268e6b": {
"system": {
"id": "6fdd091f-fcb5-012a-8745-c1671c268e6b",
"name": "6fdd091f-fcb5-012a-8745-c1671c268e6b",
"codename": "n6fdd091f_fcb5_012a_8745_c1671c268e6b",
"language": "en-US",
"type": "contentbox",
"collection": "default",
"sitemap_locations": [],
"last_modified": "2022-04-11T15:32:08.0844256Z"
},
"elements": {
"type": {
"type": "multiple_choice",
"name": "Type",
"value": [
{
"name": "Notice (blue)",
"codename": "notice"
}
]
},
"notice_text": {
"type": "rich_text",
"name": "Text",
"images": {},
"links": {},
"modular_content": [],
"value": "<p>Before deleting a user, ensure that the agent doesn’t own any open conversations (if so, these conversations should be transferred to the queue) and that the user is logged out of LP.</p>"
}
}
}
}
}
Any option to add the static URL link to the output?
You can add a modify spec with concat function after the current shift transformation spec such as
{
"operation": "modify-overwrite-beta",
"spec": {
"url": "=concat('https://www.static-url.com/',#(1,&))" // first argument is static, and the second one is dynamic(directly inherited from the current value of "url") content
}
}

Elasticsearch nested sort - mismatch between document and nested object used for sorting

I've been developing a new search API with AWS Elasticsearch (version 6.2) as backend.
Right now, I'm trying to support "sort" options for the API.
My mapping is as follows (unrelated fields not included):
{
"properties": {
"id": {
"type": "text",
"fields": {
"raw": {
"type": "keyword"
}
}
},
"description": {
"type": "text"
},
"materialDefinitionProperties": {
"type": "nested",
"properties": {
"id": {
"type": "text",
"fields": {
"raw": {
"type": "keyword"
}
},
"analyzer": "case_sensitive_analyzer"
},
"value" : {
"type": "nested",
"properties": {
"valueString": {
"type": "text",
"fields": {
"raw": {
"type": "keyword"
}
}
}
}
}
}
}
}
}
I'm attempting to allow the users sort by property value (path: materialDefinitionProperties.value.valueLong.raw).
Note that it's inside 2 levels of nested objects (materialDefinitionProperties and materialDefinitionProperties.value are nested objects).
To sort the results by the value of property with ID "PART NUMBER", my request for sorting is:
{
"fieldName": "materialDefinitionProperties.value.valueString.raw",
"nestedSort": {
"path": "materialDefinitionProperties",
"filter": {
"fieldName": "materialDefinitionProperties.id",
"value": "PART NUMBER",
"slop": 0,
"boost": 1
},
"nestedSort": {
"path": "materialDefinitionProperties.value"
}
},
"order": "ASC"
}
However, as I examined the response, the "sort" field does not match with document's property value:
{
"_index": "material-definition-index-v2",
"_type": "default",
"_id": "development_LITL4ZCNE",
"_source": {
"id": "LITL4ZCNE",
"description": [
"CPU, Intel, Cascade Lake, 8259CL, 24C, 210W, B1 Prod"
]
"materialDefinitionProperties": [
{
"id": "PART NUMBER",
"description": [],
"value": [
{
"valueString": "202-001193-001",
"isOriginal": true
}
]
}
]
},
"sort": [
"100-000018"
]
},
The document's PART NUMBER property is "202-001193-001", the "sort" field says "100-000018", which is the part number of another document.
It seems that there's a mismatch between the master document and nested object used for sorting.
This request worked well when there's only a small number of documents in the cluster. But once I backfill the cluster with ~1 million of records, the symptom appears. I've also tried creating a new ES cluster but the results are the same.
Sorting by other non-nested attributes worked well.
Did I misunderstand the concept of nested objects, or misuse the nested sort feature?
Any ideas appreciated!
This is a bug in Elasticsearch. Upgrading to 6.4.0 fixed the issue.
Issue tracker: https://github.com/elastic/elasticsearch/pull/32204
Release note: https://www.elastic.co/guide/en/elasticsearch/reference/current/release-notes-6.4.0.html

Dynamic response of Retrofit

My answer Json depends on whether it succeeds or not.
Unsuccessful example:
{
"success": false,
"errors": {
"email": "Could not find email address"
}
}
Successful example:
{
"success": true,
"user": {
"id": 6,
"fname": "XXXXXX",
"lname": "XXXXXX",
"email": "Username#mail.ca",
"roles": [
"Player"
"Coach"
"manager",
"Admin"
]
"date_registered": "2018-03-16T17: 49: 05.000Z"
}
"Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiJiNjU1MDVkOGJiYzZhMTg1Y2E5MjU5NDlmNTU0OTc0MTgzM2Y2N2NiNjFjYThkMzNkMTUxY2U2MDhjMTBmNTllIiwiaWF0IjoxNTI3MjY3MzEwLCJleHAiOjE1Mjc4NzIxMTB9.p5pTlNjTsr-8N_8B3M5fW3T6PTTrcFo8D77N0WWgA3c"
}
Now, I want to have a POJO for both at the same time with retrofit.
Thank you
Edit : I just solved the problem by changing the form of JSON to :
{
"success": true,
"data": {
"user": {
"id": 6,
"fname": "XXXXXXX",
"lname": "XXXXXXXX",
"email": "Username#mail.ca",
"roles": [
"player",
"coach",
"manager",
"admin"
],
"activation_state": 0,
"date_registered": "2018-03-16T17:49:05.000Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiI0MTFlZmI5Y2ExYzY1ZWFlYzQ4Yzg1ZjJkYTQwOThmODBjOTk1NWNjNjcyOTNlODI5NmI4N2RjZWY5OTMzYzljIiwiaWF0IjoxNTI4NDI0MDA1LCJleHAiOjE1Mjg0MjQ2MDl9.lTsQ867Lk78RV2ruaQFyxUNfm58bHpfzEsZnvKJQMXQ"
},
"errors": {}
}
Thank you for help
You could simply return a String Retrofit, and then parse it manually.
If not, then you have to create a POJO object that contains all variables from both objects.
And then at runtime you check if "success" is true/false, and then try to access the underlying varaibles.
Copy your json response and past http://pojo.sodhanalibrary.com/
Then submit
You will get multiple pojo class with respect to your response ,

Parsing _embedded items from JSON HAL response

We are trying to parse the below JSON to get a list of people.
JSON Response:
{
"_embedded": {
"people": [
{
"id": 35356,
"name": "Jon",
"description": "Test",
"type": "person",
"_links": {
"self": {
"href": "http://localhost/api/v1/50452/people/35356"
},
"items": {
"href": "http://localhost/api/v1/50452/items?person_id=35356"
},
"enabled_services": [
{
"title": "Water Company",
"href": "http://localhost/api/v1/50452/services/103890"
}
]
}
},
{
"id": 46363,
"name": "Kevin",
"description": "",
"type": "person",
"_links": {
"self": {
"href": "http://localhost/api/v1/50452/people/46363"
},
"items": {
"href": "http://localhost/api/v1/50452/items?person_id=46363"
},
"enabled_services": [
{
"title": "Water Company",
"href": "http://localhost/api/v1/50452/services/103890"
}
]
}
}
]
},
"_links": {
"self": {
"href": "http://localhost/api/v1/50452/people"
}
}
}
Our code:
ParameterizedTypeReference<Resources<Person>> resource = new ParameterizedTypeReference<Resources<Person>>() {};
Traverson traverson = new Traverson(new URI("http://localhost/api/v1/people"), MediaType.APPLICATION_JSON_UTF8);
// Create our own LinkDiscoverer as our service returns application/json instead of application/json+hal
List<LinkDiscoverer> linkDiscoverers = new ArrayList<>();
linkDiscoverers.add(new JsonPathLinkDiscoverer("$._links..['%s']..href", MediaType.APPLICATION_JSON_UTF8));
traverson.setLinkDiscoverers(linkDiscoverers);
HttpHeaders headers = new HttpHeaders();
headers.add("App-Key", Globals.Appkey);
headers.add("App-Id", Globals.AppId);
Resources<Person> personResources = traverson.follow("people").withHeaders(headers).toObject(resource);
However we are getting the following error:
java.lang.IllegalStateException: Expected to find link with rel 'people' in response {"_embedded":{"people":[{"id":31350,"name":"Jon Blue","description":"Developer","type":"person","deleted":false,"disabled":false,"company_id":50452,"order":31350,"phone_prefix":"44","_links":{"self":{"href":"http://localhost/api/v1/50452/people/31350"},"items":{"href":"http://localhost/api/v1/50452/items?person_id=31350"},"enabled_services":[{"title":"Water ...
Based on the (very limited) client docs this seems to be the correct way to do things. Does anyone know what we might be missing here?
Thanks
The Traverson is meant to be used to follow links:
Component to ease traversing hypermedia APIs by following links with
relation types. Highly inspired by the equally named JavaScript
library.
There is no link with rel people in your response, so it cannot be followed.
I could imagine you wanted to call the top-level resource that has a link to people:
Traverson traverson = new Traverson(new URI("http://localhost/api/v1/"), MediaType.APPLICATION_JSON_UTF8);
Otherwise I would suggest to use a RestTemplate to get the Resource:
restTemplate.exchange(
URI.create("http://localhost/api/v1/people"),
HttpMethod.GET,
new HttpEntity<Void>(headers),
resource).getBody();

How can I tell if a build is in progress when I call the Jenkins json api?

I'm calling a url like this: /job/My-Job/710/api/json and it's returning some json like this:
{
"actions": [
{
"parameters": [
{
"name": "DEPLOY_HOST",
"value": ""
}
]
},
{
"causes": [
{
"shortDescription": "Started by user Hudson Admin",
"userId": "username",
"userName": "Hudson Admin"
}
]
},
{},
{},
{}
],
"artifacts": [],
"building": true,
"description": null,
"duration": 0,
"estimatedDuration": 390011,
"executor": {},
"fullDisplayName": "My-App #711",
"id": "2013-08-30_12-50-14",
"keepLog": false,
"number": 711,
"result": "SUCCESS",
"timestamp": 1377892214231,
"url": "http://hudsonurl:8081/job/My-App/711/",
"builtOn": "",
"changeSet": {
"items": [
{}
],
"kind": "svn",
"revisions": [
{
"module": "https://oursvn",
"revision": 27498
}
]
},
"culprits": [
{
"absoluteUrl": "http://hudsonsurl:8081/user/handsomeg",
"fullName": "handsome guy"
}
],
"mavenArtifacts": null,
"mavenVersionUsed": "3.0.4"
}
This build is actually in progress right now, but I can't see a way to know that. You'd think that the value of result should be in progress, but it's not. Is this a bug, or is there some other way to check? I'm using Jersey version 1.523
I just noticed there's a "building": true.
Makes me wonder what result is for. Maybe builds are considered SUCCESS until proven otherwise.

Categories