In my Spring Boot app, I want to read data from the following APIs using Spring WebClient as shown below (I have no prior experience and after making several search on many pages and articles, I concluded to use Spring WebClient):
The endpoint URLs are:
service:
private static final String BASE_URL = "https://demo-api/v1";
private WebClient webClient = WebClient.create(BASE_URL);
public Mono fetchMergedData(String city) {
Mono<EduData> edu = getEduData(city);
Mono<GeoData> geo = getGeoData(city);
return Mono.zip(edu, geo, MergedData::new);
}
public Mono<EduData> getEduData(String city) {
return webClient.get()
.uri("/edu/{city}", city)
.retrieve()
.bodyToMono(EduData.class);
}
public Mono<GeoData> getGeoData(String city) {
return webClient.get()
.uri("/geo/{city}", city)
.retrieve()
.bodyToMono(GeoData.class);
}
Here are the models:
models:
#Getter
public class EduData {
private int institution;
}
#Getter
public class GeoData {
private int population;
}
#Getter
public class MergedData {
private int institution;
private int population;
public MergedData(EduData edu, GeoData geo) {
this.institution = edu.getInstitution();
this.population = get.getPopulation();
}
}
Although there is no error and all the endpoints return data when I test using Postman, I cannot see any data in neither edu, geo variables, nor the return of fetchMergedData() method. So, where is the problem?
To get data out of a Mono, you can either block() (turning this into a blocking operation), or subscribe() and pass it a Consumer or Subscriber.
Simply blocking the call would give you the results, but if you want to do this in a reactive manner then you'll need to subscribe to the Mono.
// Using a block
this.institution = edu.getInstitution().block();
// Using a subscription, when available, EduData can be accessed via response.get
AtomicReference<EduData> response = new AtomicReference<>();
edu.getInstitution().subscribe(response::set);
Here's a simple subscription that provides a Consumer.
public class SomeClass {
EduData eduData;
public void setEduData(EduData eduData) {
this.eduData = eduData;
}
public void fetchData(String city) {
webClient.get()
.uri("/edu/{city}", city)
.retrieve()
.bodyToMono(EduData.class).subscribe(this::setEduData);
}
}
When the response is available, the setEduData method will be called with the result.
Related
I made my declarative http client on my app built in micronaut. This need to consume a services which responds with text/html type.
I manage to get a list but with LinkedHashMap inside. And I want them to be objects of Pharmacy
My question is: how I can transform that response into a List of object?
#Client("${services.url}")
public interface PharmacyClient {
#Get("${services.path}?${services.param}=${services.value}")
Flowable<List<Pharmacy>> retrieve();
}
public class StoreService {
private final PharmacyClient pharmacyClient;
public StoreService(PharmacyClient pharmacyClient) {
this.pharmacyClient = pharmacyClient;
}
public Flowable<List<Store>> all() {
Flowable<List<Pharmacy>> listFlowable = this.pharmacyClient.retrieve();
return listFlowable
.doOnError(throwable -> log.error(throwable.getLocalizedMessage()))
.flatMap(pharmacies ->
Flowable.just(pharmacies.stream() // here is a list of LinkedHashMap and i'd like to user Pharmacy objects
.map(pharmacy -> Store.builder().borough(pharmacy.getBoroughFk()).build())
.collect(Collectors.toList())
)
);
}
}
Code: https://github.com/j1cs/drugstore-demo/tree/master/backend
There is no fully-fledged framework AFAIK that provides support for HTML content to POJO mapping (which is usually referred to as scraping) as is the case for Micronaut, .
Meanwhile you can easily plug a converter bean based on jspoon intercepting and transforming your API results in equivalent POJOs:
class Root {
#Selector(value = ".pharmacy") List<Pharmacy> pharmacies;
}
class Pharmacy {
#Selector(value = "span:nth-child(1)") String name;
}
#Client("${services.minsal.url}")
public interface PharmacyClient {
#Get("${services.minsal.path}?${services.minsal.param}=${services.minsal.value}")
Flowable<String> retrieve();
}
#Singleton
public class ConverterService {
public List<Pharmacy> toPharmacies(String htmlContent) {
Jspoon jspoon = Jspoon.create();
HtmlAdapter<Root> htmlAdapter = jspoon.adapter(Root.class);
return htmlAdapter.fromHtml(htmlContent).pharmacies;
}
}
public class StoreService {
private final PharmacyClient pharmacyClient;
private final ConverterService converterService;
public StoreService(PharmacyClient pharmacyClient, ConverterService converterService) {
this.pharmacyClient = pharmacyClient;
this.converterService = converterService;
}
public Flowable<List<Store>> all() {
Flowable<List<Pharmacy>> listFlowable = this.pharmacyClient.retrieve().map(this.converterService::toPharmacies)
return listFlowable
.doOnError(throwable -> log.error(throwable.getLocalizedMessage()))
.flatMap(pharmacies ->
Flowable.just(pharmacies.stream() // here is a list of LinkedHashMap and i'd like to user Pharmacy objects
.map(pharmacy -> Store.builder().borough(pharmacy.getBoroughFk()).build())
.collect(Collectors.toList())
)
);
}
}
I ended up with this.
#Client("${services.url}")
public interface PharmacyClient {
#Get(value = "${services.path}?${services.param}=${services.value}")
Flowable<Pharmacy[]> retrieve();
}
public class StoreService {
private final PharmacyClient pharmacyClient;
public StoreService(PharmacyClient pharmacyClient) {
this.pharmacyClient = pharmacyClient;
}
public Flowable<List<Store>> all() {
Flowable<Pharmacy[]> flowable = this.pharmacyClient.retrieve();
return flowable
.switchMap(pharmacies ->
Flowable.just(Arrays.stream(pharmacies)
.map(pharmacyStoreMapping)
.collect(Collectors.toList())
)
).doOnError(throwable -> log.error(throwable.getLocalizedMessage()));
}
}
Still I want to know if i can change arrays to list in the declarative client.
Meanwhile i think this it's a good option.
UPDATE
I have been wrong all this time. First of all I don't need to add a list to the flowable because when the framework exposes the service it responds with a list of elements already.
So finally I did this:
#Client("${services.url}")
public interface PharmacyClient {
#Get(value = "${services.path}?${services.param}=${services.value}")
Flowable<Pharmacy> retrieve();
}
public class StoreService {
private final PharmacyClient pharmacyClient;
public StoreService(PharmacyClient pharmacyClient) {
this.pharmacyClient = pharmacyClient;
}
public Flowable<Store> all() {
Flowable<Pharmacy> flowable = this.pharmacyClient.retrieve();
return flowable
.switchMap(pharmacyPublisherFunction)
.doOnError(throwable -> log.error(throwable.getLocalizedMessage()));
}
As we can see the http client automatically transform the text/html data into json and it parses it well. I don't know why really. Maybe #JeffScottBrown can gives us some hints.
I am still new to Spring boot Webflux. I am using Casssandra as my database, below i have a friends tables, i want to get all my friends based on my login ID. How can i get the current login ID and pass the ID to a flux
// Here i retrieved currently login user ID which is a mono string
public Mono<String> getUserID(Mono<String> principal) {
return principal
.map(Principal::getName)
}
OR
public static Mono<String> getUserIDFromRequest(ServerRequest request) {
return request.principal()
.cast(UsernamePasswordAuthenticationToken.class)
.map(UsernamePasswordAuthenticationToken::getName);
}
// My friends table
public class Friends {
#PrimaryKey("userid")
private long userId;
private long friendId;
private FriendObject friendObject;
private String since;
private boolean enableNotifications;
private boolean allowMessages;
// Getters and setters
}
#Repository
public interface FriendsRepository extends ReactiveCassandraRepository<Friends, Long> {
}
#Service
public class FriendshipServiceImpl implements FriendshipService {
#Override
public Mono<Friends> addFriend(Friends friends) {
return friendsRepository.save(friends);
}
}
public class FriendshipHandler {
// How to pass the Login Mono<String> to this flux Or how can i combine Mono and Flux?
#GetMapping(path="/friends/list", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<Friends> getFriends(Mono<Principal> principal) {
return friendshipService.getFriends(Long.valueOf("The login ID"));
}
OR
public Mono<ServerResponse> getFriends(ServerRequest request) {
return ok().body(
friendshipService
.getFriends(Long.valueOf("The login ID")), Friends.class);
}
}
this is basic webflux so if you can't do this you need to read up
#GetMapping(path="/friends", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<Friends> getFriends(Mono<Principal> principal) {
return principal.map(principal -> {
return // Here you do what you need to with the principal
});
}
Please, read about ReactiveSecurityContextHolder
I am trying to build a micro service using web-flux which will send/publish some data based on an event for a particular subscriber.
With the below implementation (Another Stackflow Issue) I am able to create one publisher and all who are subscribed will receive the data automatically when we trigger the event by calling "/send" API
#SpringBootApplication
#RestController
public class DemoApplication {
final FluxProcessor processor;
final FluxSink sink;
final AtomicLong counter;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
public DemoApplication() {
this.processor = DirectProcessor.create().serialize();
this.sink = processor.sink();
this.counter = new AtomicLong();
}
#GetMapping("/send/{userId}")
public void test(#PathVariable("userId") String userId) {
sink.next("Hello World #" + counter.getAndIncrement());
}
#RequestMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent> sse() {
return processor.map(e -> ServerSentEvent.builder(e).build());
}
}
Problem statement - My app is having user based access and for each user there will be some notifications which I want to push only based on an event. Here the events will be stored in the DB with user id's and when we hit the "send" end point from another API along with "userId" as a path variable, it should only send the data related to that user only if it is registered as a subscriber and still listening on the channel.
This is not an accurate or actual solution, but this thing works:
First the SSE end-point :
#RestController
public class SSEController {
private String value = "";
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
#GetMapping(path = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> getWords() {
System.out.println("sse ?");
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> getValue());
}
}
And from any Service or wherever you can autowire, just autowire it:
#Service
public class SomeService {
#Autowired
private SSEController sseController;
...
void something(){
....
sseController.setValue("some value");
....
}
This is the way I'm using.
I have the following Java class that is uploaded on Amazon's Lambda service:
public class DevicePutHandler implements RequestHandler<DeviceRequest, Device> {
private static final Logger log = Logger.getLogger(DevicePutHandler.class);
public Device handleRequest(DeviceRequest request, Context context) {
AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
DynamoDBMapper mapper = new DynamoDBMapper(client);
if (request == null) {
log.info("The request had a value of null.");
return null;
}
log.info("Retrieving device");
Device deviceRetrieved = mapper.load(Device.class, request.getDeviceId());
log.info("Updating device properties");
deviceRetrieved.setBuilding(request.getBuilding());
deviceRetrieved.setMotionPresent(request.getMotionPresent());
mapper.save(deviceRetrieved);
log.info("Updated device has been saved");
return deviceRetrieved;
}
}
I also have an Execution Role set that gives me complete control over DynamoDB. My permissions should be perfectly fine since I've used the exact same permissions with other projects that used Lambda and DynamoDB in this exact manner (the only difference being a different request type).
The intended point of this class is to have it be called by API Gateway (API Gateway -> Lambda -> DynamoDB), but for now I simply am trying to test it on Lambda (Lambda -> DynamoDB).
For reference, in case it matters, here is the DeviceRequest class:
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({ "deviceId", "building", "motionPresent" })
public class DeviceRequest {
#JsonProperty("deviceId")
private String deviceId;
#JsonProperty("building")
private String building;
#JsonProperty("motionPresent")
private Boolean motionPresent;
#JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
#JsonProperty("deviceId")
public String getDeviceId() {
return deviceId;
}
#JsonProperty("deviceId")
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
#JsonProperty("building")
public String getBuilding() {
return building;
}
#JsonProperty("building")
public void setBuilding(String building) {
this.building = building;
}
#JsonProperty("motionPresent")
public Boolean getMotionPresent() {
return motionPresent;
}
#JsonProperty("motionPresent")
public void setMotionPresent(Boolean motionPresent) {
this.motionPresent = motionPresent;
}
#JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
#JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}
And here is the Device class:
#DynamoDBTable(tableName="DeviceTable")
public class Device {
private String deviceID;
private String building;
private String queue;
private boolean motionPresent;
#DynamoDBHashKey(attributeName="Device ID")
public String getDeviceID() {
return deviceID;
}
public void setDeviceID(String deviceID) {
this.deviceID = deviceID;
}
#DynamoDBAttribute(attributeName="Motion Present")
public boolean getMotionPresent() {
return motionPresent;
}
public void setMotionPresent(boolean motionPresent) {
this.motionPresent = motionPresent;
}
#DynamoDBAttribute(attributeName="Building")
public String getBuilding() {
return this.building;
}
public void setBuilding(String building) {
this.building = building;
}
#DynamoDBAttribute(attributeName="Queue")
public String getQueue() {
return this.queue;
}
public void setQueue(String queue) {
this.queue = queue;
}
}
Here is the JSON input that I'm trying to test the Lambda with:
{
"deviceId": "test_device_name",
"building": "building1",
"motionPresent": false
}
No exceptions whatsoever are thrown (I've tried wrapping it around a try/catch block) and the lambda timing out is the only thing that happens. I've tried using log/print statements at the very beginning prior to the initialization of the DynamoDB client to see if the request can be read properly and it does appear to properly parse the JSON fields. I've also separated the client builder out and found that the builder object is able to be initialized, but the timing out comes from when the builder calls build() to make the client.
If anyone has any insight into why this timing out is occurring, please let me know!
Turns out that by bumping up the timout period AND the allotted memory, the problem get solved. Not sure why it works since the lambda always indicated that its memory usage was under the previously set limit, but oh well. Wish that in the future Amazon will provide better error feedback that indicates if a lambda needs more resources to run.
please help with Retrofit2. I'm very new in Retrofit.
I create simple server application.
The application manage list of Journals: add Journal in the in-memory list and return Journal by id.
There is an Journal.java:
public class Journal {
private AtomicInteger getNewId = new AtomicInteger(0);
#SerializedName("id")
#Expose
private Integer id;
#SerializedName("name")
#Expose
private String name;
public Journal(String name) {
this.id = getNewId.incrementAndGet();
this.name = name;}
public Integer getId() {return id;}
public String getName() {return name;}
}
There is an controller interface:
public interface JournalController {
#GET("journal")
Call<List<Journal>> getJournalList();
#GET("journal/{id}")
Call<Journal> getJournal(#Path("id") Integer id);
#POST("journal")
Call<Journal> addJournal(#Body Journal journal);
}
This is interface implementation:
#Controller
public class JournalControllerImpl implements JournalController {
// An in-memory list that the controller uses to store the Journals
private List<Journal> journals = new ArrayList<>();
#RequestMapping(value= "journal", method=RequestMethod.GET)
public #ResponseBody Call<List<Journal>> getJournalList() {
return (Call<List<Journal>>) journals;}
#RequestMapping(value= "journal/{id}", method= RequestMethod.GET)
public #ResponseBody Call<Journal> getJournal(#Path("id") Integer id) {
Journal journal = journals.get(id);
return (Call<Journal>) journal;}
#RequestMapping(value= "journal", method= RequestMethod.POST)
public #ResponseBody Call<Journal> addJournal(#Body Journal journal) {
journals.add(journal);
return (Call<Journal> ) journal; }
}
The application started succesesully. Console output during application's startup:
Mapped "{[/journal],methods=[GET]}" onto public retrofit2.Call> main.JournalControllerImpl.getJournalList()
Mapped "{[/journal],methods=[POST]}" onto public retrofit2.Call main.JournalControllerImpl.addJournal(main.Journal)
Mapped "{[/journal{/id}],methods=[GET]}" onto public retrofit2.Call main.JournalControllerImpl.getJournal(java.lang.Integer)
Than I try to run URL http://localhost:8080/journal on browser (or GET HttpRequest http://localhost:8080/journal).
There is an error in application output:
"... java.lang.ClassCastException: java.util.ArrayList cannot be cast to retrofit2.Call..."
Could you guess what is wrong?
Is it really problems with conversion java.util.ArrayList to Call ? (I've tryed to CopyOnWriteArrayList but this doesn't help.
Thank you in advance.
Call<T> is a Retrofit class that only needs to exist on the clientside, it is meant to be an abstraction over a potential call to an external API.
I'm not sure what kind of library you are using for the server but you cannot cast completely unrelated classes. (Such as ArrayList<T> to Call<List<T>>)
Usually the library will handle serialization for you, so on the server side you would just directly return the object that you want to send:
#RequestMapping(value= "journal", method=RequestMethod.GET)
public #ResponseBody List<Journal> getJournalList() {
return journals;}
The problem has been resolved when the Call been removed from the JournalController (and implementation class JournalControllerImpl). Now JournalController looks like:
public interface JournalController {
String URL_PATH = "journal";
#GET(URL_PATH)
List<Journal> getJournalList();
#GET(URL_PATH +"/{id}")
Journal getJournal(#Path("id") Integer id);
#POST(URL_PATH)
Journal addJournal(#Body Journal journal);
}
And works perfectly.
The way to use the service interface (service) is to the service with a callback (onResponse, onFailure)implement the detail the logic need as highlighted below.
//Pass in a constructor upon creating
JournalController journalService;
//Return journal list
Call<List<Journal>> call = journalService.getJournalList();
call.enqueue(new Callback<>() {
#Override
public void onResponse(Call<List<Journal>> call, Response<List<Journal>>
response) {
int statusCode = response.code();
List<Journal> journal = response.body();
}
#Override
public void onFailure(Call<Journal> call, Throwable t) {
// Log error here since request failed
}
});
//Return a journal
Call<Journal> call = journalService.getJournal(userId);
call.enqueue(new Callback<Journal>() {
#Override
public void onResponse(Call<Journal> call, Response<Journal> response) {
int statusCode = response.code();
Journal journal = response.body();
}
#Override
public void onFailure(Call<Journal> call, Throwable t) {
// Log error here since request failed
}
});
//Add journal
Call<Journal> call = journalService.addJournal(new Journal("username");
call.enqueue(new Callback<Journal>() {
#Override
public void onResponse(Call<Journal> call, Response<Journal> response) {
int statusCode = response.code();
Journal journal = response.body();
}
#Override
public void onFailure(Call<Journal> call, Throwable t) {
// Log error here since request failed
}
});