Reactor lazy pagination with expand - kotlin

Based on How to collect paginated API responses using spring boot WebClient?
I created the following crawler class
class GitlabCrawler(private val client: WebClient, private val token: String) {
fun fetchCommits(project: URI): Flux<Commit> {
return fetchCommitsInternal(project).expand { cr: ClientResponse? ->
val nextUrl = getNextUrl(cr)
nextUrl?.let { fetchCommitsInternal(URI.create(it)) }
?: Mono.empty<ClientResponse>()
}.limitRate(1)
.flatMap { cr: ClientResponse? -> cr?.bodyToFlux(Commit::class.java) ?: Flux.empty() }
}
private fun getNextUrl(cr: ClientResponse?):String? {
// TODO replace with proper link parsing
return cr?.headers()?.header(HttpHeaders.LINK)?.firstOrNull()
?.splitToSequence(",")
?.find { it.endsWith("rel=\"next\"") }
?.let { it.substring(it.indexOf('<') + 1, it.lastIndexOf('>')) }
}
private fun fetchCommitsInternal(url: URI): Mono<ClientResponse> {
return client.get()
.uri(url)
.accept(MediaType.APPLICATION_JSON_UTF8)
.header("Private-Token", token)
.exchange()
}
}
data class Commit(
val id: String,
val message: String,
#JsonProperty("parent_ids") val parentIds: List<String>,
#JsonProperty("created_at") val createdAt: String)
I'd like to avoid unnecessary request, but it performs more request than needed to fulfill the request.
gitlabCrawler.fetchCommits(URI.create("https://...")).take(15).collectList().block()
Would only need one request as each page contains 20 entries, but it starts the second page request. It seems to always request one more page than necessary. I tried using limitRate but that doesn't seem to have an effect.
Is there a way to make it lazy, i.e., only request the next page when the current is exhausted?

Are you positive that it actually performs the request? fetchCommitInternal being invoked means that WebFlux "prepared" the request, not necessarily that it was executed (ie. subscribed).
The following simplification of your use case shows the difference:
private static Tuple2<Integer, Flux<Integer>> nextPage(int index, int pageSize) {
System.out.println("prepared a request for page " + index);
return Tuples.of(index, Flux.range((pageSize * (index - 1)) + 1, pageSize));
}
#Test
public void expandLimitedRequest() {
int pageSize = 5;
Flux.just(nextPage(1, pageSize))
.doOnSubscribe(sub -> System.out.println("requested first page"))
.expand(page -> {
int currentPage = page.getT1();
if (currentPage < 3) {
int nextPage = currentPage + 1;
return Flux.just(nextPage(nextPage, pageSize))
.doOnSubscribe(sub -> System.out.println("requested page " + nextPage));
}
return Flux.empty();
})
.doOnNext(System.out::println)
.flatMap(Tuple2::getT2)
.doOnNext(System.out::println)
.take(8)
.blockLast();
}
Prints:
prepared a request for page 1
requested first page
[1,FluxRange]
1
2
3
4
5
prepared a request for page 2
requested page 2
[2,FluxRange]
6
7
8
prepared a request for page 3
As you can see, it prepares the request for page 3 but never execute it (because the take downstream cancels the expand before that).

Related

How to have Kotlin "Listen" when a function finish executing Successfully

This is my first time using Kotlin, I have to write a simple command-line application where it takes a list of user input strings. Valid inputs are only "Apple" or "Orange" and calculate the price (which is 60 cents and 25 cents respectively). I'm having some trouble with the 3rd requirement
"Build a service that listens for when orders are complete and sends a notification to the customer regarding its status and estimated delivery time. The Mail service subscribes to events from the Orders service and publishes the appropriate event that the customer (you) is able to read from the terminal"
this is what I have done so far
MainApp.tk
import java.util.Scanner
import kotlin.system.exitProcess;
import app.Checkout;
var shopRunning = true;
var applecount = 0;
fun main(args: Array<String>) {
while (shopRunning) {
println("Welcome to Express Store");
println("1. Checkout");
println("2. exit");
var userOption = 0;
//request the user to eneter an option
//if user eneter a options that is not valid it will keep looping til option that is enterd is accepted;
var userSeletedOption = false;
val inputScanner = Scanner(System.`in`);
while (!userSeletedOption) {
print("Select an Option: ");
userOption = inputScanner.nextInt();
//if input entered by the user is not accepted and invaliud message is printed and is promted to enter an option again.
if (userOption != 1 && userOption != 2) {
println("Invalid input detected!");
} else {
userSeletedOption = true;
}
}
if (userOption == 1) {
val checkout = Checkout();
println("We currently have apples and oranges in Stock.")
var list: MutableList<String> = ArrayList();
println(list.size);
var doneAddingToCart = false;
while(!doneAddingToCart){
print("enter name of item to be enter or exit to finish adding to the cart: ")
var item = inputScanner.next();
if(item.equals("exit")){
doneAddingToCart=true;
}
else{
list.add(item);
}
}
if(checkout.verify(list)){ //checks if list has any item that is not an apple or orange
println("Thank you for your Pruchse");
val cost = checkout.Chasher(list)
println("You bought: "+ list.toString());
print("your total is: "+ cost);//returns the total cost
exitProcess(1);//exits from the application
}
} else if (userOption == 2) {
print("Have a great day.");
exitProcess(1);
}
}
}
CheckOut.tk
class Checkout {
//checks if the user entered any invaild items
public fun verify (cart: MutableList<String>) : Boolean{
for(item in cart){
if(!item.equals("Apple") && !item.equals("Orange")){
return false;
}
}
return true;
}
public fun Chasher (cart: MutableList<String>) : Double{
var total = 0.0;
var orangecount = 0;//step 2 offers
var applecount = 0;//step 2 offers
for(item in cart){//step 1 function
if(item.equals("Apple") || item.equals("apple")){
applecount+=1;
total= total + 0.6;
}
if(item.equals("Orange") || item.equals("orange")){
orangecount +=1;
total=total +0.25;
}
}
if(orangecount ==3){//buy three for the price of 2.step 2
println("You qaulidified for our buy 3 oragnes for the price of 2 offer")
total -=0.25;
}
if(applecount ==1){//buy one aple get 1 free. step 2
println("You buy 1 apple get one free")
cart.add("Apple");
}
return total;
}
}
I don't need to send an email just send a message to the command line. Currently, I'm just printing messages (just to see if what I currently have even works). Yeah, I know there many spelling errors, english and writing was never my strongest subject
I can only provide three hints that might help you:
If you exit your program using System.exit, use 0 if the run did not have any problem. (Excerpt from JavaDoc: "The argument serves as a status code; by convention, a nonzero status code indicates abnormal termination.")
For checking equality, simply use == which corresponds to equals in Java. In your special case however, you can use item.equals("apple", ignoreCase=true) or simply item.equals("apple", true).
I'm not sure what the author of your task exactly expects as a solution.
In can imagine you are expected to use lambdas.
An example: Your could refactor your Checkout class like that:
class Checkout {
/**
* Checks if the given [cart] contains only apples and oranges,
* and calls [onSuccess].
* If also other articles are contained, [onSuccess] is not called.
*/
fun verify(cart: List<String>, onSuccess: (List<String>) -> Unit): Unit {
for (item in cart) {
if (!item.equals("apple", true) && !item.equals("Orange", true)) {
return
}
}
onSuccess(cart)
}
}
And then call
val cart = listOf("Orange", "Apple", "apple", "orange")
Checkout().verify(cart, { cart: List<String> ->
println("Thanks you for your purchase: $cart")
})
or even shorter (curly brackets are outside of parenthesis)
Checkout().verify(cart) { cart: List<String> ->
println("Thanks you for your purchase: $cart")
}
What I did here was to extract what is executed if your validation succeeds:
For that, I used a lambda function that accepts a list of articles/strings (List<String>) and returns something I ignore/don't care about -> Unit.
The advantage of that approach is that callers of your verify method can decide what to do on success at their liking because they can pass a lambda function around like any other variable. Here:
val cart = listOf("Orange", "Apple", "apple", "orange")
val onSuccess = { cart: List<String> ->
println("Thanks you for your purchase: $cart")
}
Checkout().verify(cart, onSuccess)
You could also extend Checkout to allow an observer to register.
I deliberately kept the code very simple. Normally you would allow multiple observers to register, only expose what clients are supposed to see and hide the rest, etc.
class Checkout(
val onSuccess : (List<String>) -> Unit
) {
fun verify(cart: List<String>): Unit {
for (item in cart) {
if (!item.equals("apple", true) && !item.equals("Orange", true)) {
return
}
}
onSuccess(cart)
}
}
val checkout = Checkout({ cart: List<String> ->
println("Thanks you for your purchase: $cart")
})
and then
val cart = listOf("Orange", "Apple", "apple", "orange")
checkout.verify(cart)
Be sure to check out https://play.kotlinlang.org/byExample/04_functional/01_Higher-Order%20Functions to learn more about lambda / higher-order functions.

How to return bad request in spring webflux when there is an error?

I have this server endpoint using spring-webflux and I would like to return ServerResponse.badRequest() when the serverRequest receives a wrong parameter. The request curl -s "http://localhost:8080/4?a=5&b=3"; echo for instance, contains the right parameters. But the request curl -s "http://localhost:8080/one?a=5&b=3"; echo contains a string instead of an Integer. Then the conversion new BidRequest(Integer.parseInt(tuple2.getT1()), tuple2.getT2().toSingleValueMap()) will throw an error.
I was doing .onErrorReturn(new BidRequest(0, null)) but now I want to implement some operation that return ServerResponse.badRequest(). So I added in the end .onErrorResume(error -> ServerResponse.badRequest().build()) in the end, but It is not working. I also added on the place of the code .onErrorReturn() and it does not compile.
public Mono<ServerResponse> bidRequest(ServerRequest serverRequest) {
var adId = serverRequest.pathVariable("id");
var attributes = serverRequest.queryParams();
log.info("received bid request with adID: {} attributes: {}", adId, attributes);
return Mono.just(Tuples.of(adId, attributes))
.map(tuple2 -> new BidRequest(Integer.parseInt(tuple2.getT1()), tuple2.getT2().toSingleValueMap()))
// I WANT TO REPLACE IT FOR A BAD REQUEST
// .onErrorReturn(new BidRequest(0, null))
.flatMap(bidRequest -> {
return Flux.fromStream(bidderService.bidResponseStream(bidRequest))
.flatMap(this::gatherResponses)
.reduce((bidResp1, bidResp2) -> {
if (bidResp1.getBid() > bidResp2.getBid()) return bidResp1;
else return bidResp2;
});
})
.map(bid -> {
var price = bid.getContent().replace("$price$", bid.getBid().toString());
bid.setContent(price);
return bid;
})
.flatMap(winner -> {
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(winner.getContent()));
})
.switchIfEmpty(ServerResponse.notFound().build())
// THIS DOES NOT RETURN ANY BAD REQUEST
.onErrorResume(error -> ServerResponse.badRequest().build());
}
I solved based on this answer using flatmap and returning a Mono.just() or a Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST));.
return Mono
.just(Tuples.of(adId, attributes))
.flatMap(tuple2 -> {
if (validate(tuple2)) {
log.info("request parameters valid: {}", tuple2);
return Mono.just(new BidRequest(Integer.parseInt(tuple2.getT1()), tuple2.getT2().toSingleValueMap()));
} else {
log.error("request parameters invalid: {}", tuple2);
return Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
})
.flatMap(....
private boolean validate(Tuple2<String, MultiValueMap<String, String>> tuple2) {
return GenericValidator.isInteger(tuple2.getT1());
}

Kotlin Wi-Fi loss/bad signal during FTP transfer

I'm nearly done creating an app that sends txt files, containing scanned data, to an ftp server.
The issue that I'm currently struggling with is: what if my Wi-Fi has terrible signal or no signal at all.
I noticed that my 'isOnline()' check works fine and that if there is no internet, it alerts the user. However a few hours ago I tested the app in the basement and noticed that when the Wi-Fi signal has no bars, it still sends the data but it gets lost somewhere along the way.
Currently the flow of the data is as follow:
user presses 'send'
check internet and if true, continue
clear content list and send
the content to viewmodel
viewmodel checks internet again before
creating the txt files
txt files get sent via FTP code below.
private fun sendTXT(result: String) {
try {
val name = "00_VER${LocalDateTime.now().format(fileNameFormatter)}.txt"
val path = getApplication<Application>().applicationContext.filesDir.path
.toString() + name
val f = File(path)
val isNewFileCreated: Boolean = f.createNewFile()
if (isNewFileCreated) {
f.writeText(result, Charsets.UTF_8)
}
val ftpClient = FTPClient()
ftpClient.addProtocolCommandListener(PrintCommandListener(PrintWriter(System.out)))
ftpClient.connect("xxx.xx.xxx.xx", 21)
val reply: Int = ftpClient.replyCode
if (!FTPReply.isPositiveCompletion(reply)) {
ftpClient.disconnect()
throw IOException("Exception in connecting to FTP Server")
}
if (ftpClient.login("username", "pass")) {
ftpClient.enterLocalPassiveMode()
ftpClient.setFileType(FTP.BINARY_FILE_TYPE)
val inp = FileInputStream(f)
var directory = "/files/input"
ftpClient.changeWorkingDirectory(directory)
val result = ftpClient.storeFile(name, inp)
inp.close()
if (result) {
ftpClient.logout()
ftpClient.disconnect()
f.delete()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun isOnline(context: Context): Boolean {
var result = false
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val networkCapabilities = connectivityManager.activeNetwork ?: return false
val actNw =
connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
result = when {
actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
} else {
connectivityManager.run {
connectivityManager.activeNetworkInfo?.run {
result = when (type) {
ConnectivityManager.TYPE_WIFI -> true
ConnectivityManager.TYPE_MOBILE -> true
ConnectivityManager.TYPE_ETHERNET -> true
else -> false
}
}
}
}
return result
}
I'm stuck at finding a way to make sure the data gets to the server. Is there a more advanced way to check for internet connectivity?
I was considering adding all the scan objects as JSON to sharedpreferences, and if at the end of the day the user notices a scan didn't make it through, they can look up the missing scan and resend it.
However this seems very unconventional and I'm pretty sure there must be a better way to handle things.

RxJava2 Flowable that emits results of multiple network calls without using create?

I have a generic screen that subscribes to an RxJava2 flowable that returns a List. It then displays the content in the list.
I have a use case now though where I need to collect data from multiple endpoints, and emit data once some complete, and then emit data again once the remaining ones complete.
I'm doing this using Flowable.create() but I've seen a lot of posts saying that there's usually a better and safer way to do so than using create? I seem to believe that is the case since I need to subscribe to an observable within the observable which ideally I wouldn't want to do?
Because I subscribe within, I know the emitter can become cancelled within the observable while other network calls are completing so I've added checks to ensure it doesn't throw an error after its disposed which do work (at least in testing...) [I also just remembered I have the code available to dispose of the inner subscription if I kept it like this, when the outer is disposed]
The first 2 calls may be incredibly fast (or instant) which is why i want to emit the first result right away, and then the following 4 network calls which rely on that data may take time to process.
It looks roughly like this right now...
return Flowable.create<List<Object>>({ activeEmitter ->
Single.zip(
single1(),
single2(),
BiFunction { single1Result: Object, single2result: Object ->
if (single1result.something || single2Result.somethingElse) {
activeEmitter.onNext(function(single1result, single2result) //returns list
}
Single.zip(
single3(single1result),
single4(single2result),
single5(single1result),
single6(single2result),
Function4 { single3Result: Object,
single4Result: Object,
single5Result: Object,
single6Result: Object ->
ObjectHolder(single1Result, single2Result, single3Result, single4Result, single5Result, single6Result)
}
)
}
).flatMap { objectHolder ->
objects.flatMap { objectHolder ->
Single.just(parseObjects(objectHolder))
}
}.subscribeBy(
onError = { error ->
if (!activeEmitter.isCancelled) {
activeEmitter.onError(error)
}
},
onSuccess = { results ->
if (!activeEmitter.isCancelled) {
activeEmitter.onNext(results)
activeEmitter.onComplete()
}
}
)
}, BackpressureStrategy.BUFFER)
I can't figure out another way to return a Flowable that emits the results of multiple different network calls without doing it like this?
Is there a different/better way I can't find?
I worked this out given ctranxuan response. Posting so he can tweak/optimize and then I accept his answer
return Single.zip(single1(), single2(),
BiFunction { single1result: Object, single2result: Object ->
Pair(single1result, single2result)
}
).toFlowable()
.flatMap { single1AndSingle2 ->
if (isFirstLoad) {
createItemOrNull(single1AndSingle2.first, single1AndSingle2.second)?.let { result ->
Single.just(listOf(result)).mergeWith(proceedWithFinalNetworkCalls(single1AndSingle2))
}.orElse {
proceedWithFinalNetworkCalls(single1AndSingle2).toFlowable()
}
} else {
proceedWithFinalNetworkCalls(single1AndSingle2).toFlowable()
}
}.doOnComplete {
isFirstLoad = false
}
fun proceedWithFinalNetworkCalls(): Flowable<List> {
return Single.zip(
single3(single1result),
single4(single2result),
single5(single1result),
single6(single2result),
Function4 { single3Result: Object,
single4Result: Object,
single5Result: Object,
single6Result: Object ->
ObjectHolder(single1Result, single2Result, single3Result, single4Result, single5Result, single6Result)
}
)
Sorry, it's in Java but from what I've understood, something like that may be a possible solution?
public static void main(String[] args) {
final Single<String> single1 = single1().cache();
single1.map(List::of)
.mergeWith(single1.zipWith(single2(), Map::entry)
.flatMap(entry -> Single.zip(
single3(entry.getKey()),
single4(entry.getValue()),
single5(entry.getKey()),
single6(entry.getValue()),
(el3, el4, el5, el6) -> objectHolder(entry.getKey(), entry.getValue(), el3, el4, el5, el6))))
.subscribe(System.out::println,
System.err::println);
Flowable.timer(1, MINUTES) // Just to block the main thread for a while
.blockingSubscribe();
}
private static List<String> objectHolder(final String el1,
final String el2,
final String el3,
final String el4,
final String el5,
final String el6) {
return List.of(el1, el2, el3, el4, el5, el6);
}
static Single<String> single1() {
return Single.just("s1");
}
static Single<String> single2() {
return Single.just("s2");
}
static Single<String> single3(String value) {
return single("s3", value);
}
static Single<String> single4(String value) {
return single("s4", value);
}
static Single<String> single5(String value) {
return single("s5", value);
}
static Single<String> single6(String value) {
return single("s6", value);
}
static Single<String> single(String value1, String value2) {
return Single.just(value1).map(l -> l + "_" + value2);
}
This outputs:
[s1]
[s1, s2, s3_s1, s4_s2, s5_s1, s6_s2]

In Ktor how do I get the sessionId within a get() {}?

install(Sessions) {
header<MySession>("MY_SESSION", SessionStorageMemory())
}
get("/session/increment") {
val session = call.sessions.get<MySession>() ?: throw AuthorizationException()
call.sessions.set(session.copy(count = session.count + 1))
// insert code here to get sessionId ?
call.respondText("Counter is ${session.count}. Refresh to increment.")
}
I've been trying to get it out of the attributes but it seems the framework has made all those data structures private to prevent me from getting the sessionId and have no working solution yet.
val attributeKey = call.attributes.allKeys.map{
val x = it as AttributeKey<Any>
call.attributes.get(x)
}
// AttributeKey<SessionData>("SessionKey")
SessionData is private so I can't get access to data structure that holds sessionId