I am starting to build a Microservice API Gateway, and I am considering Spring Cloud to help me with the routing. But some calls to the Gateway API will need multiple requests to different services.
Lets say I have 2 services: Order Details Service and Delivery Service. I want to have a Gateway endpoint GET /orders/{orderId} that makes a call to Order Details service and then Delivery Service and combine the two to return full Order details with delivery. Is this possible with the routing of Spring cloud or should I make these by hand using something like RestTemplate to make the calls?
There is an enhancement proposal posted on GitHub to have routes support multiple URIs. So far, there aren't any plans to implement this yet, at least, not according to one of the contributors.
As posted in the Spring Cloud Gateway Github issue mentioned by g00glen00b, until the library develops a Filter for this, I resolved it using the ModifyResponseBodyGatewayFilterFactory in my own custom Filter.
Just in case it's useful for anyone else, I provide the base implementation here (it may need some rework, but it should be enough to make the point).
Simply put, I have a "base" service retrieving something like this:
[
{
"targetEntryId": "624a448cbc728123b47d08c4",
"sections": [
{
"title": "sadasa",
"description": "asda"
}
],
"id": "624a448c45459c4d757869f1"
},
{
"targetEntryId": "624a44e5bc728123b47d08c5",
"sections": [
{
"title": "asda",
"description": null
}
],
"id": "624a44e645459c4d757869f2"
}
]
And I want to enrich these entries with the actual targetEntry data (of course, identified by targetEntryId).
So, I created my Filter based on the ModifyResponseBody one:
/**
* <p>
* Filter to compose a response body with associated data from a second API.
* </p>
*
* #author rozagerardo
*/
#Component
public class ComposeFieldApiGatewayFilterFactory extends
AbstractGatewayFilterFactory<ComposeFieldApiGatewayFilterFactory.Config> {
public ComposeFieldApiGatewayFilterFactory() {
super(Config.class);
}
#Autowired
ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilter;
ParameterizedTypeReference<List<Map<String, Object>>> jsonType =
new ParameterizedTypeReference<List<Map<String, Object>>>() {
};
#Value("${server.port:9080}")
int aPort;
#Override
public GatewayFilter apply(final Config config) {
return modifyResponseBodyFilter.apply((c) -> {
c.setRewriteFunction(List.class, List.class, (filterExchange, input) -> {
List<Map<String, Object>> castedInput = (List<Map<String, Object>>) input;
// extract base field values (usually ids) and join them in a "," separated string
String baseFieldValues = castedInput.stream()
.map(bodyMap -> (String) bodyMap.get(config.getOriginBaseField()))
.collect(Collectors.joining(","));
// Request to a path managed by the Gateway
WebClient client = WebClient.create();
return client.get()
.uri(UriComponentsBuilder.fromUriString("http://localhost").port(aPort)
.path(config.getTargetGatewayPath())
.queryParam(config.getTargetQueryParam(), baseFieldValues).build().toUri())
.exchangeToMono(response -> response.bodyToMono(jsonType)
.map(targetEntries -> {
// create a Map using the base field values as keys fo easy access
Map<String, Map> targetEntriesMap = targetEntries.stream().collect(
Collectors.toMap(pr -> (String) pr.get("id"), pr -> pr));
// compose the origin body using the requested target entries
return castedInput.stream().map(originEntries -> {
originEntries.put(config.getComposeField(),
targetEntriesMap.get(originEntries.get(config.getOriginBaseField())));
return originEntries;
}).collect(Collectors.toList());
})
);
});
});
}
;
#Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("originBaseField", "targetGatewayPath", "targetQueryParam",
"composeField");
}
/**
* <p>
* Config class to use for AbstractGatewayFilterFactory.
* </p>
*/
public static class Config {
private String originBaseField;
private String targetGatewayPath;
private String targetQueryParam;
private String composeField;
public Config() {
}
// Getters and Setters...
}
}
For completeness, this is the corresponding route setup using my Filter:
spring:
cloud:
gateway:
routes:
# TARGET ENTRIES ROUTES
- id: targetentries_route
uri: ${configs.api.tagetentries.baseURL}
predicates:
- Path=/api/target/entries
- Method=GET
filters:
- RewritePath=/api/target/entries(?<segment>.*), /target-entries-service$\{segment}
# ORIGIN ENTRIES
- id: originentries_route
uri: ${configs.api.originentries.baseURL}
predicates:
- Path=/api/origin/entries**
filters:
- RewritePath=/api/origin/entries(?<segment>.*), /origin-entries-service$\{segment}
- ComposeFieldApi=targetEntryId,/api/target/entries,ids,targetEntry
And with this, my resulting response looks as follows:
[
{
"targetEntryId": "624a448cbc728123b47d08c4",
"sections": [
{
"title": "sadasa",
"description": "asda"
}
],
"id": "624a448c45459c4d757869f1",
"targetEntry": {
"id": "624a448cbc728123b47d08c4",
"targetEntityField": "whatever"
}
},
{
"targetEntryId": "624a44e5bc728123b47d08c5",
"sections": [
{
"title": "asda",
"description": null
}
],
"id": "624a44e645459c4d757869f2",
"targetEntry": {
"id": "624a44e5bc728123b47d08c5",
"targetEntityField": "somethingelse"
}
}
]
Related
I need a dynamic response with different values using traffic parrot and wiremock.
I have an integration test with patterns in json files to get a response when I call the API's.
I need to call a product service to retrieve a specific product, my request json:
request.json
{
"request" {
"urlPathPattern": "/urlResponse/product/1000",
"method": "GET"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "response.json",
"transformers": [
"response-template"
]
}
}
And I need something like this:
response.json
{
"id": {{request.path.[2]}},
"type": VARIABLE or DYNAMIC CONTENT,
"other attr"....
}
I Want to pass a variable content depending of the request, for example, I have an Object
public CustomObject {
private int id = 1000;
private String type = "product";
}
When I call the API of product with ID 1000, I want a response.json with type set to "product". I see documentation of Traffic parrot example, but I do not know how to apply.
Edit 1: It is possible to define in request.json a map to define the variable type for the response? Something like this:
If I have an ID with 1000
objMap = 1000; type = "product",
objMap = 2000; type = "Balloon",
etc...
So I've got a Ninja endpoint here:
public Result processRecurring(Context context, RecurOrderJSON recurOrderJSON) {
String id = recurOrderJSON.id;
String event_type = recurOrderJSON.event_type;
String request_id = recurOrderJSON.request_id;
//Map data = recurOrderJSON.data;
//recurringRouter(event_type, data);
log.info("ID value");
log.info(id);
return JsonResponse.build()
.message("OK")
.toResult();
}
The class I am trying to map to:
public class RecurOrderJSON {
public String id;
public String event_type;
public String request_id;
// Maybe switch data type?
//public Map data;
}
And the route:
router.POST().route("/recurring").with(RecurringController::processRecurring);
I am just trying to send some simple JSON to a webhook and for some reason the object mapping doesn't seem to be working. I think maybe I am misunderstanding the documentation?
http://www.ninjaframework.org/documentation/working_with_json_jsonp.html
The example they give you is this:
If you send that JSON to your application via the HTTP body you only need to add the POJO class to the controller method and Ninja will parse the incoming JSON for you:
package controllers;
public class ApplicationController {
public Result parsePerson(Person person) {
String nameOfPerson = person.name; // will be John Johnson
...
}
}
As far as I can tell, I am doing this correctly? Am I understanding the documentation wrong? Here's an example JSON object - currently I am only trying to grab the top level strings, but I'll eventually want to grab data as well:
{
"id": "hook-XXXXX",
"event_type": "tx-pending",
"data": {
"button_id": "static",
"publisher_organization": "org-XXXXXXX",
"campaign_id": "camp-097714a40aaf8965",
"currency": "USD",
"order_currency": "USD",
"id": "tx-XXXXXXX",
"category": "new-user-order",
"modified_date": "2018-10-15T05:41:12.577Z",
"order_total": 9680,
"button_order_id": "btnorder-77c9e56fd990f127",
"publisher_customer_id": "XymEz8GO2M",
"rate_card_id": "ratecard-41480b2a6b1196a7",
"advertising_id": null,
"event_date": "2018-10-15T05:41:06Z",
"status": "pending",
"pub_ref": null,
"account_id": "acc-4b17f5a014d0de1a",
"btn_ref": "srctok-0adf9e958510b3f1",
"order_id": null,
"posting_rule_id": null,
"order_line_items": [
{
"identifier": "Antique Trading Card",
"description": "Includes Lifetime Warranty",
"amount": 9680,
"publisher_commission": 968,
"attributes": {},
"total": 9680,
"quantity": 1
}
],
"order_click_channel": "webview",
"order_purchase_date": null,
"validated_date": null,
"amount": 968,
"customer_order_id": null,
"created_date": "2018-10-15T05:41:12.577Z",
"commerce_organization": "org-XXXXXX"
},
"request_id": "attempt-XXXXXXX"
}
Currently I am just trying to get the string values, yet I am constantly getting a 500 error and no other indication in my logs of any error.
As far as I can tell, Ninja should just automatically map the JSON to my object, correct?
I successfully reproduced your issue, and then fixed it.
First, for easy way to try/test, I recommend (temporary) modifications:
package controllers;
import models.RecurOrderJSON;
import ninja.Context;
import ninja.Result;
public class RecurringController {
public Result processRecurring(Context context, RecurOrderJSON recurOrderJSON) {
log.info("recurOrderJSON => " + recurOrderJSON);
return ninja.Results.ok();
}
}
And then, update your model this way:
package models;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
#JsonIgnoreProperties(ignoreUnknown = true)
public class RecurOrderJSON {
public String id;
public String event_type;
public String request_id;
public Map data;
#Override
public String toString() {
return "RecurOrderJSON [id=" + id + ", event_type=" + event_type + ", request_id=" + request_id + ", data="
+ data.toString() + "]";
}
}
You can notice:
The data type must stay raw (generic can't be used here)
the important #JsonIgnoreProperties(ignoreUnknown = true) annotation to avoid deserialize issue, if ever your source data does not perfectly match your model (be sure to use the recent version of annotation, in fasterxml sub-package, instead of the old one, in codehaus sub-package)
the toString() implementation only allowing quick check of OK/KO deserialization
Then you can easily test the system with wget, or curl:
curl -H 'Content-Type: application/json' -d "#/tmp/jsonINput.json" -X POST http://localhost:8080/recurring
Notice it is very important to specify the Content-type for good interpretation.
With the /tmp/jsonINput.json file containing exactly the json contents you specified in your question.
This way, everything is working like a charm, obtaining this output:
recurOrderJSON => RecurOrderJSON [id=hook-XXXXX, event_type=tx-pending, request_id=attempt-XXXXXXX, data={button_id=static, publisher_organization=org-XXXXXXX, campaign_id=camp-097714a40aaf8965, currency=USD, order_currency=USD, id=tx-XXXXXXX, category=new-user-order, modified_date=2018-10-15T05:41:12.577Z, order_total=9680, button_order_id=btnorder-77c9e56fd990f127, publisher_customer_id=XymEz8GO2M, rate_card_id=ratecard-41480b2a6b1196a7, advertising_id=null, event_date=2018-10-15T05:41:06Z, status=pending, pub_ref=null, account_id=acc-4b17f5a014d0de1a, btn_ref=srctok-0adf9e958510b3f1, order_id=null, posting_rule_id=null, order_line_items=[{identifier=Antique Trading Card, description=Includes Lifetime Warranty, amount=9680, publisher_commission=968, attributes={}, total=9680, quantity=1}], order_click_channel=webview, order_purchase_date=null, validated_date=null, amount=968, customer_order_id=null, created_date=2018-10-15T05:41:12.577Z, commerce_organization=org-XXXXXX}]
Given the specific input code with data field commented out
//public Map data;
and the posted input JSON that includes this field, the request should fail with 400 Bad Request.
The reason being that Ninja uses Jackson for JSON parsing and it will throw on unknown fields by default.
The quick workaround is to add #JsonIgnoreProperties annotation to RecurOrderJSON class.
e.g.
#JsonIgnoreProperties(ignoreUnknown = true)
public class RecurOrderJSON {
...
}
See: Ignoring new fields on JSON objects using Jackson
Now if the error was not 400 there isn't much information to go by as there doesn't seem to be anything else obviously wrong with the code.
Either post an SSCCE demonstrating the problem or attempt to debug by surfacing the error page with the following method:
Launch the application in debug mode with mvn package ninja:run
Access the end-point with a tool that allows to inspect the response in detail such as curl e.g.
Store request JSON in input.json
Run curl -v -o result.html -H 'Content-Type: application/json' --data '#input.json' http://localhost:8080/recurring
Open result.html to examine the response
Might it be that you are performing a bad request (hence the JSON is not found) but for some Ninja bug it returns error 500?
For example you can take a look here where is stated that parsing an empty JSON in a JSON request does leads to a misguiding error (500) while it is supposed to return 400 "Bad Request"
Context not needed in processRecurring and use Results.json() and return original
public Result processRecurring(RecurOrderJSON recurOrderJSON) {
String id = recurOrderJSON.id;
String event_type = recurOrderJSON.event_type;
String request_id = recurOrderJSON.request_id;
//Map data = recurOrderJSON.data;
//recurringRouter(event_type, data);
log.info("ID value");
log.info(id);
return Results.json().render(recurOrderJSON);
}
Make sure you get the namespace in your RecurOrderJSON
package models;
public class RecurOrderJSON {
public String id;
public String event_type;
public String request_id;
// Maybe switch data type?
//public Map data;
}
Good luck!
I am working with vertx and java, I am new to vertx and i have an api. please go to link below for a better understanding
https://www.tez-tour.com/tariffsearch/hotels?countryId=1104&cityId=345&locale=en (countryId (destination)= Turkey, cityId (city of depature)= Moscow)
and a python example script to fetch data from tour api
import asyncio
from aiohttp import ClientSession
import json
async def do_request(url):
async with ClientSession() as session:
async with session.get(url) as response:
resp = await response.read()
#print(str(resp, 'utf-8'))
parsed = json.loads(str(resp, 'utf-8'))
print(parsed)
tasks = []
tourCities = {
/*'Turkey': {
'cities': [1285,12689,12706,143330,9004247,4433,5736,139343,4434,12691,21301,12705,149827,4151426]
},*/
'Austria': {
'tourId': [308122,3024267,147579,353869,320460,3024283,253138,3026464,3024262,293808,469713,3024272,314293,467029,348518,544505,384331,594027,3025654,258494],
'params': {
'hotelClasses': [ // Типы отелей
{"classId": 269506,"name": "Special Cat.","weight": -9},
{"classId": 261942,"name": "Chalet","weight": -8},
{"classId": 253005,"name": "Apts","weight": -6},
{"classId": 253006,"name": "Pens","weight": -5},
],
"tourTypes": [ // Состав тура
{"id": 1,"name": "Полный пакет","haveResidence": true,"haveTransfer": true,"haveFly": true,"haveInsurance": true},
{"id": 2,"name": "Проживание+трансфер","haveResidence": true,"haveTransfer": true,"haveFly": false,"haveInsurance": true},
],
"pansion": [ // Пансион
{"rAndBId": 15350,"name": "Размещение без питания","weight": 0,"sortOrder": 0},
{"rAndBId": 2424,"name": "Только завтраки","weight": 1,"sortOrder": 1},
{"rAndBId": 2474,"name": "Завтрак и ужин","weight": 3,"sortOrder": 3},
],
"tours": [ // Регионы (города, куда ищем тур)
{"name": "Бад-Кляйнкирхайм","tourId": [308122]},
{"name": "Баден","tourId": [3024267]},
tour operator has a web site (or external REST API) where from we can fetch tour data
each of them provide us with authentication data (login & password) to connect to their tour database (no jdbc, only web based access)
So i have some interface to be implemented and i should use WebClient but i dont fully understand how to write this method to fetch from the api above
I have two method to implement as follows
#Override
public YuService runParserTask(String tourOperator, Handler<AsyncResult<Void>> handler) {
return this;
}
#Override
public YuService getTaskStatus(String tourOperator, Handler<AsyncResult<ParseTask>> handler) {
return this;
}
and a parser dto with ENUM status as follows
#DataObject(generateConverter = true)
public class ParseTask {
private String type;
private Status status;
public ParseTask(String type, Status status) {
this.type = type;
this.status = status;
}
public ParseTask(JsonObject json) {
ParseTaskConverter.fromJson(json, this);
}
public JsonObject toJson() {
JsonObject json = new JsonObject();
ParseTaskConverter.toJson(this, json);
return json;
}
Can i get an explanation may be a bit of code to help me get a better understanding on how to implement this method
Hi I'm trying to send a PUT request using Retrofit that uses $addToSet to my Mlab Server. I can do this using Postman but I'm having trouble doing it using Retrofit.
The collection looks like:
[
{
"_id": {
"$oid": "5abe74bac2ef1603f4045686"
},
"email": "test#gmail.com",
"completedWalks": [
"South Leinster Way"
],
"favWalks": []
}
]
The post man request has the API key, Query, and then $addToSet is passed in the body as so.
And the response is:
I'm trying to do it like this in android.
Retrofit:
#PUT("databases/walks/collections/user")
Call<Update> addCompleted (#Query("apiKey") String apiKey,#Query("q") String Email, #Body Update Query);
My model:
public class Update {
#SerializedName("n")
private String n;
public String getN() {
return n;
}
public Update(String n) {
this.n = n;
}
}
Creating the update object:
String updateComplete = String.format("'$addToSet': {'completedWalks': '%s'}} ", TrailName);
final String query =String.format("{'email': '%s'}",email) ;
final Update queryComplete = new Update(updateComplete);
And the Request:
Call<Update> completeCall = apiService.addCompleted(mlabAPi, query, queryComplete);
completeCall.enqueue(new Callback<Update>() {
#Override
public void onResponse(Call<Update> call, Response<Update> response) {
Toast.makeText(getApplicationContext(),"Walk marked as Complete", Toast.LENGTH_SHORT).show();
}
#Override
public void onFailure(Call<Update> call, Throwable t) {
Log.e(TAG, t.getMessage());
}
});
But this only overwrites whats in the collection and I have:
[
{
"_id": {
"$oid": "5abe74bac2ef1603f4045686"
},
"n": "'$addToSet': {'completedWalks': 'Kerry Head Cycleway'}} "
}
]
Does anyone know where I'm going wrong, should I not be passing $addToSet as a model because it seems to be overwriting all, how do I pass it then?
Thank You.
#Body Update Query -- Retrofit will encode the object passed to this as JSON (assuming you are using the Gson converter, which it appears you are). That is where this "n": "'$addToSet': {'completedWalks': 'Kerry Head Cycleway'}} " is coming from. You need to structure you Java Object the same as your JSON object for gson to serialize it correctly.
I am not familiar with the mlab api, but from your postman, it looks like you want a request body something like this --
public class UpdateRequest {
#SerializedName("$addToSet")
Map<String, String> addToSet = new HashMap();
}
Update your interface to send this object as the body --
#PUT("databases/walks/collections/user")
Call<Update> addCompleted (#Query("apiKey") String apiKey,#Query("q") String Email, #Body UpdateRequest Query);
And create the request body --
UpdateRequest requestBody = new UpdateRequest();
requestBody.addToSet.put("completedWalks", Trailname);
and create the call --
Call<Update> completeCall = apiService.addCompleted(mlabAPi, query, requestBody);
For further debugging, you can see what is actually being sent in your logcat by adding HttpLoggingInterceptor to your retrofit instance.
See here for setup. Then you can compare what your app is sending vs postman and see where things might be going sideways.
I'm stuck trying to send JSON data to by Struts2 REST server using the struts2-rest-plugin.
It works with XML, but I can't seem to figure out the right JSON format to send it in.
Anybody has any experience with this?
Thanks,
Shaun
Update:
Sorry I wasn't clear. The problem is that Struts2 doesn't seem to be mapping the JSON data I send in to my model in the controller.
Here's the code:
Controller:
public class ClientfeatureController extends ControllerParent implements ModelDriven<Object> {
private ClientFeatureService clientFeatureService;
private ClientFeature clientFeature = new ClientFeature();
private List<ClientFeature> clientFeatureList;
//Client ID
private String id;
public ClientfeatureController() {
super(ClientfeatureController.class);
}
#Override
public Object getModel() {
return (clientFeatureList != null ? clientFeatureList : clientFeature);
}
/**
* #return clientFeatureList through Struts2 model-driven design
*/
public HttpHeaders show() {
//logic to return all client features here. this works fine..
//todo: add ETag and lastModified information for client caching purposes
return new DefaultHttpHeaders("show").disableCaching();
}
// PUT request
public String update() {
logger.info("client id: " + clientFeature.getClientId());
logger.info("clientFeature updated: " + clientFeature.getFeature().getDescription());
return "update";
}
public HttpHeaders create() {
logger.info("client id: " + clientFeature.getClientId());
logger.info("feature description: " + clientFeature.getFeature().getDescription());
return new DefaultHttpHeaders("create");
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public void setClientFeatureService(ClientFeatureService clientFeatureService) {
this.clientFeatureService = clientFeatureService;
}
public List<ClientFeature> getClientFeatureList() {
return clientFeatureList;
}
public void setClientFeatureList(List<ClientFeature> clientFeatureList) {
this.clientFeatureList = clientFeatureList;
}
public ClientFeature getClientFeature() {
return clientFeature;
}
public void setClientFeature(ClientFeature clientFeature) {
this.clientFeature = clientFeature;
}
}
This is the URL I'm making the request to:
..http://localhost:8080/coreserviceswrapper/clientfeature.json
-Method: POST or PUT (tried both, POST maps to create() and PUT maps to update())
-Header: Content-Type: application/json
Payload:
{"clientFeature":{
"feature": {
"id": 2,
"enabled": true,
"description": "description1",
"type": "type1"
},
"countries": ["SG"],
"clientId": 10}
}
And the output in the Struts2 logs when I make the request:
1356436 [http-bio-8080-exec-5] WARN net.sf.json.JSONObject - Tried to assign property clientFeature:java.lang.Object to bean of class com.foo.bar.entity.ClientFeature
1359043 [http-bio-8080-exec-5] INFO com.foo.bar.rest.ClientfeatureController - client id: null
Let me also add that XML requests work just fine:
URL: ..http://localhost:8080/coreserviceswrapper/clientfeature.xml
Method: POST/PUT
Content-Type: text/xml
Payload:
<com.foo.bar.entity.ClientFeature>
<clientId>100</clientId>
<feature>
<description>test</description>
</feature>
</com.foo.bar.entity.ClientFeature>
Output:
1738685 [http-bio-8080-exec-7] INFO com.foo.bar.rest.ClientfeatureController - client id: 100
1738685 [http-bio-8080-exec-7] INFO com.foo.bar.rest.ClientfeatureController - feature description: test
1738717 [http-bio-8080-exec-7] INFO org.apache.struts2.rest.RestActionInvocation - Executed action [/clientfeature!create!xml!200] took 1466 ms (execution: 1436 ms, result: 30 ms)
I also encounter same issue, my environment is:
Structs 2.3.16.3, Jquery 1.11, Struts-rest-plugin
symptom: post json data, rest controller not parse json data to model.
solution:
since the controller is modeldriven, browser client just post Json string is OK. but seems you have to force jquery to change conenttype of ajax call.
_self.update= function(model, callback) {
$.ajax({
beforeSend: function(xhrObj){
xhrObj.setRequestHeader("Content-Type","application/json");
xhrObj.setRequestHeader("Accept","application/json");
},
type: 'PUT',
url: this.svrUrl+"/"+ model.id + this.extension,
data: JSON.stringify(model), // '{"name":"' + model.name + '"}',
//contentType: this.contentType,
//dataType: this.dataType,
processData: false,
success: callback,
error: function(req, status, ex) {},
timeout:60000
});
};
the model data format is :
var model = {"id":"2",
"name":"name2",
"author":"author2",
"key":"key2"
}
when you put or post data whit "Content-Type"="application/json", the plugin will handle it with Jsonhandler automatically.
I got such a problem. Strange but got solved by changing the name 'clientFeature' to 'model'