Extract variable from spring webflux reactive pipeline - java

I am working on reactive streams using Spring webflux. I want to extract a variable(name) from the middle of the reactive pipeline and use it in a different place as follows.
public class Example {
public Mono<String> test() {
String name;
return Mono.just("some random string")
.map(s -> {
name = s.toUpperCase();
return name;
}).map(...)
.map(...)
.flatMap(...)
.map(...)
.map(result -> result+name)
.doOnSuccess(res -> asyncPublish(name));
public void asyncPublish(String name) {
// logic to write to a Messaging queue asynchronously
}
}
}
The above code is not working. This is a contrived example but shows what I want to achieve.
Note: I don't want to use multiple zip operators just to bring the name all the way to the last map where I want to use it. Is there a way I can store it in a variable as shown above and then use it somewhere else whereever I need it.

You might use for example a Tuple2 to pass the value of name along with the modified data through the chain.
return Mono.just("some random string")
.map(s -> s.toUpperCase())
.map(s -> Tuples.of(s, x(s))) // given that x(s) is the transformation of this map statement
.map(...) // keeping Tuple with the value of `name` in the first slot...
.flatMap(...) // keeping Tuple with the value of `name` in the first slot...
.map(resultTuple -> Tuples.of(resultTuple.getT1(), resultTuple.getT2() + resultTuple.getT1()) // keeping Tuple with the value of `name` in the first slot...
.doOnSuccess(resultTuple -> asyncPublish(resultTuple.getT1()))
.map(resultTuple -> resultTuple.getT2()); // in case that the returned Mono should contain the modified value...
Tuples is from the package reactor.util.function and part of reactor-core.
Another way (without passing the value through the chain using Tuples) could be to use AtomicReference (but I still think that the Tuple way is cleaner). The AtomicReference way might look like this:
public Mono<String> test() {
final AtomicReference<String> nameRef = new AtomicReference<>();
return Mono.just("some random string")
.map(s -> {
final String name = s.toUpperCase();
nameRef.set(name);
return name;
}).map(...)
.map(...)
.flatMap(...)
.map(...)
.map(result -> result+nameRef.get())
.doOnSuccess(res -> asyncPublish(nameRef.get()));
public void asyncPublish(String name) {
// logic to write to a Messaging queue asynchronously
}
}

Related

Can this code be reduced using Java 8 Streams?

I want to use Java 8 lambdas and streams to reduce the amount of code in the following method that produces an Optional. Is it possible to achieve?
My code:
protected Optional<String> getMediaName(Participant participant) {
for (ParticipantDevice device : participant.getDevices()) {
if (device.getMedia() != null && StringUtils.isNotEmpty(device.getMedia().getMediaType())) {
String mediaType = device.getMedia().getMediaType().toUpperCase();
Map<String, String> mediaToNameMap = config.getMediaMap();
if (mediaMap.containsKey(mediaType)) {
return Optional.of(mediaMap.get(mediaType));
}
}
}
return Optional.empty();
}
Yes. Assuming the following class hierarchy (I used records here).
record Media(String getMediaType) {
}
record ParticipantDevice(Media getMedia) {
}
record Participant(List<ParticipantDevice> getDevices) {
}
It is pretty self explanatory. Unless you have an empty string as a key you don't need, imo, to check for it in your search. The main difference here is that once the map entry is found, Optional.map is used to return the value instead of the key.
I also checked this out against your loop version and it works the same.
public static Optional<String> getMediaName(Participant participant) {
Map<String, String> mediaToNameMap = config.getMediaMap();
return participant.getDevices().stream()
.map(ParticipantDevice::getMedia).filter(Objects::nonNull)
.map(media -> media.getMediaType().toUpperCase())
.filter(mediaType -> mediaToNameMap.containsKey(mediaType))
.findFirst()
.map(mediaToNameMap::get);
}
Firstly, since your Map of media types returned by config.getMediaMap() doesn't depend on a particular device, it makes sense to generate it before processing the collection of devices. I.e. regurless of the approach (imperative or declarative) do it outside a Loop, or before creating a Stream, to avoid generating the same Map multiple times.
And to implement this method with Streams, you need to use filter() operation, which expects a Predicate, to apply the conditional logic and map() perform a transformation of stream elements.
To get the first element that matches the conditions apply findFirst(), which produces an optional result, as a terminal operation.
protected Optional<String> getMediaName(Participant participant) {
Map<String, String> mediaToNameMap = config.getMediaMap();
return participant.getDevices().stream()
.filter(device -> device.getMedia() != null
&& StringUtils.isNotEmpty(device.getMedia().getMediaType())
)
.map(device -> device.getMedia().getMediaType().toUpperCase())
.filter(mediaToNameMap::containsKey)
.map(mediaToNameMap::get)
.findFirst();
}

How to iterate on a Flux to produce a new Flux and still work in a Reactive way?

I have 2 methods, respectively producing a Flux<String> and a Flux<Integer>.
public Flux<String> buildName() {
return Flux.just(personProcessor.buildName());
}
public Flux<Integer> computeAge() {
return Flux.just(personProcessor.computeAge());
}
These methods generate randomized values to populate a Person object.
Now I would like to iterate and create X Person with randomized values in each of them.
I have created a new Flux (processor is the name of the service holding the methods) :
Flux<Person> persons = Flux.zip(processor.buildName(), processor.computeAge(),
(name, age) -> Person.builder().name(name).age(age).build() );
My problem is that when I iterate with a regular loop on the "persons" Flux, I always get Person objects with the same values for name and age. I think it's normal because my loop only subscribes to the Flux and then get the same "first" object of the Flux.
I just don't know how to iterate on the Flux "persons" so that I will obtain each time a Person object with its own name and age values.
How can I iterate X times over this Flux in order to get X Person objects with randomized values ?
Of course, this iteration should also produce a Flux that will let me process the Person object.
You can simplify it as follows:
Flux.range(1, n)
.map(i -> Person.builder()
.name(personProcessor.buildName())
.age(personProcessor.computeAge())
.build())
Besides Ikatiforis's most simple solution, you can also do the following which resembles your original solution. This can be useful if the random values are provided in some asynchronous way.
public Flux<String> buildName() {
return Mono.fromCallable(() -> personProcessor.buildName()).repeat();
}
public Flux<Integer> computeAge() {
return Mono.fromCallable(() -> personProcessor.computeAge()).repeat();
}
And then the zip part is pretty much the same:
Flux<Person> persons = Flux.zip(processor.buildName(), processor.computeAge(),
(name, age) -> Person.builder().name(name).age(age).build())
.take(X);

Java 8 - Update two properties in same Stream code

I'm wondering if there is a way I can update two times an object in a Stream lambda code, I need to update two properties of a class, I need to update the value and the recordsCount properties
Object:
public class HistoricDataModelParsed {
private Date startDate;
private Date endDate;
private Double value;
private int recordsCount;
}
I tried doing something like this:
val existingRecord = response.stream()
.filter(dateTime ->fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(null);
response.stream()
.filter(dateTime ->fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(existingRecord)
.setValue(valueAdded)
.setRecordsCount(amount);
But I got this error: "Cannot invoke setRecordsCount(int) on the primitive type void"
So I ended up doing the stream two times to update each of the two fields I needed
response.stream()
.filter(dateTime ->fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(existingRecord)
.setValue(valueAdded);
response.stream()
.filter(dateTime ->fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(existingRecord)
.setRecordsCount(amount);
Is there a way I can achieve what I need without the need to stream two times the list?
The return type of setValue is void and not HistoricDataModelParsed. So you cannot invoke the method setRecordsCount which is in HistoricDataModelParsed class.
You could have added a method in HistoricDataModelParsed which takes two parameters for value and recordsCount:
public void setValueAndCount(Double value, int count) {
this.value = value;
this.recordsCount = count;
}
Then call this method after orElse:
response.stream()
.filter(dateTime ->fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(existingRecord)
.setValueAndCount(valueAdded, amount);
The state of an object should not change within a stream. It can lead to inconsistent results. But you can create new instances of the objects and pass new values via the constructor. Here is a simple record that demonstrates the method. Records are basically immutable classes that have no setters. The getters are the names of the variables. A class would also work in this example.
record Temp(int getA, int getB) {
#Override
public String toString() {
return "[" + getA + ", " + getB +"]";
}
}
Some data
List<Temp> list = List.of(new Temp(10, 20), new Temp(50, 200),
new Temp(100, 200));
And the transformation. A new instance of Temp with new values is created along with the old ones to completely populate the constructor. Otherwise, the existing object is passed along.
List<Temp> result = list.stream().map(
t -> t.getA() == 50 ? new Temp(2000, t.getB()) : t)
.toList();
System.out.println(result);
Prints
[[10, 20], [2000, 200], [100, 200]]
To answer the void error you got it's because a stream expects values to continue thru out the stream so if a method is void, it isn't returning anything so you would have to return it. Here is an example:
stream.map(t->{voidReturnMethod(t); return t;}).toList();
The return ensures the pipeline continues.
Simply store the result of orElse and then call your methods on it.
HistoricDataModelParsed record =
response.stream()
.filter(dateTime -> fromDate.equals(dateTime.getStartDate()))
.findAny()
.orElse(existingRecord);
record.setValue(valueAdded)
record.setRecordsCount(amount);

How to get Map with condition using RxJava2 and RxAndroid?

So, I have sorted by condition list of objects
private Observable<CallServiceCode> getUnansweredQuestionList() {
return Observable.fromIterable(getServiceCodeArrayList())
.subscribeOn(Schedulers.computation())
.filter(iServiceCode -> iServiceCode.getServiceCodeFormStatus().isUnanswered());
}
and now what I need to do:
Every object has list servicePartList , I need to filter this list by condition and eventually if final size of this filtered list >0, so I need to add object that contains this list CallServiceCode object as a key and this filtered list as a value.
So it should be like this:
private Map<CallServiceCode, ArrayList<CallServicePart>> getSortedMap() {
Map<CallServiceCode, ArrayList<CallServicePart>> result = new HashMap<>();
getUnansweredQuestionList()
.filter(callServiceCode -> Observable.fromIterable(callServiceCode.getCallServicePartList()) //
.filter(servicePart -> servicePart.getServicePartFormStatus().isUnanswered())//
.isNotEmpty())
.subscribe(callServiceCode -> result.put(callServiceCode, Observable.fromIterable(callServiceCode.getCallServicePartList()) //
.filter(servicePart -> servicePart.getServicePartFormStatus().isUnanswered()));
return result;
}
But there is no such method isNotEmpty() in RxJava2 and also it is not right to add key like this:
Observable.fromIterable(callServiceCode.getCallServicePartList())
.filter(servicePart -> servicePart.getServicePartFormStatus().isUnanswered())
So question is how to make it properly?
One solution could be to use collect to create the Map directly from the observable:
return getUnansweredQuestionList()
.collect(HashMap<CallServiceCode, List<CallServicePart>>::new,(hashMap, callServiceCode) -> {
List<CallServicePart> callServiceParts = Observable.fromIterable(callServiceCode.getServicePartList())
.filter(s -> !s.getServicePartFormStatus().isUnanswered())
.toList().blockingGet();
if (!callServiceParts.isEmpty())
hashMap.put(callServiceCode, callServiceParts);
}).blockingGet();
If you extract filtering into a method (could be also member of CallServiceCode) then the code is much cleaner:
return getUnansweredQuestionList()
.collect(HashMap<CallServiceCode, List<CallServicePart>>::new, (hashMap, callServiceCode) -> {
List<CallServicePart> filteredParts = getFilteredServiceParts(callServiceCode.getServicePartList());
if (!filteredParts .isEmpty())
hashMap.put(callServiceCode, filteredParts);
}).blockingGet();

Fill Map<String,Map<String,Integer>> with Stream

I have a Linkedlist with Data ( author, date , LinkedList<Changes(lines, path)> )
now i want to create with a stream out of this a Map< Filepath, Map< Author, changes >>
public Map<String, Map<String, Integer>> authorFragmentation(List<Commit> commits) {
return commits.stream()
.map(Commit::getChangesList)
.flatMap(changes -> changes.stream())
.collect(Collectors.toMap(
Changes::getPath,
Collectors.toMap(
Commit::getAuthorName,
(changes) -> 1,
(oldValue, newValue) -> oldValue + 1)));
}
I try it so but this doesnt work.
How can i create this Map in a Map with the Stream and count at the same time the changes ?
Jeremy Grand is completely correct in his comment: in your collector it has long been forgotten that you started out from a stream of Commit objects, so you cannot use Commit::getAuthorName there. The challenge is how to keep the author name around to a place where you also got the path. One solution is to put both into a newly created string array (since both are strings).
public Map<String, Map<String, Long>> authorFragmentation(List<Commit> commits) {
return commits.stream()
.flatMap(c -> c.getChangesList()
.stream()
.map((Changes ch) -> new String[] { c.getAuthorName(), ch.getPath() }))
.collect(Collectors.groupingBy(sa -> sa[1],
Collectors.groupingBy(sa -> sa[0], Collectors.counting())));
}
Collectors.counting() insists on counting into a Long, not Integer, so I have modified your return type. I’m sure a conversion to Integer would be possible if necessary, but I would first consider whether I could live with Long.
It’s not the most beautiful stream code, and I will wait to see if other suggestions come up.
The code is compiled, but since I neither have your classes nor your data, I have not tried running it. If there are any issues, please revert.
Your mistake is that map/flatMap call "throws away" the Commit. You do not know which Commit a Change belongs to when trying to collect. In order to keep that information I'd recommend creating a small helper class (you could use a simple Pair, though):
public class OneChange
{
private Commit commit;
private Change change;
public OneChange(Commit commit, Change change)
{
this.commit = commit;
this.change = change;
}
public String getAuthorName() { return commit.getAuthorName(); };
public String getPath() { return change.getPath(); };
public Integer getLines() { return change.getLines(); };
}
You can then flatMap to that, group it by path and author, and then sum up the lines changed:
commits.stream()
.flatMap(commit -> commit.getChanges().stream().map(change -> new OneChange(commit, change)))
.collect(Collectors.groupingBy(OneChange::getPath,
Collectors.groupingBy(OneChange::getAuthorName,
Collectors.summingInt(OneChange::getLines))));
In case you do not want to sum up the lines, but just count the Changes, replace Collectors.summingInt(OneChange::getLines) by Collectors.counting().

Categories

Resources