I'm relatively new to webflux and i want to find solution to avoid nested flatMap when there are conditionals:
I have 3 simple entities:
Item, Brand, Category.
Item basically contains: brandId and categoryId
public Mono<Item> patch(String itemId, PatchSpecs specs) {
return itemRepository.findById(itemId)
.switchIfEmpty(Mono.error(..)))
.flatMap(item -> {
final String brandId = specs.getBrandId();
if (brandId != null) {
return brandService.getById(brandId)
.switchIfEmpty(Mono.error(..)))
.flatMap(brand -> {
final String categoryId = specs.getCategoryId();
if (categoryId != null) {
return categoryService.getById(categoryId)... -> return updateAndSave(..)
}
return updateAndSave(specs, item, brand, null);
});
}
else {
final String categoryId = specs.getCategoryId();
if (categoryId != null) {
return categoryService.getById(categoryId)... -> return updateAndSave(..)
}
return updateAndSave(specs, item, null, null);
}
});
}
How do I prevent this branching conditional flatMaps mess? I cannot imagine if i have another entity inside Item. There will be more nested flatMaps?
If I understand your code well category and brand are optional attributes of the item. In this case I'd recommend the following:
public Mono<Item> patch(String itemId, PatchSpecs specs)
{
return itemRepository.findById(itemId)
.switchIfEmpty(Mono.error(new RuntimeException("Can not find item.")))
.flatMap(item -> updateAndSave(specs, item));
}
private Mono<? extends Item> updateAndSave(PatchSpecs specs, Item item)
{
Mono<Optional<Brand>> brand = getBrand(specs.getBrandId());
Mono<Optional<Category>> category = getCategory(specs.getCategoryId());
return Mono.zip(Mono.just(item), brand, category)
.flatMap(tuple -> updateAndSave(specs, tuple));
}
private Mono<Optional<Brand>> getBrand(String inputBrandId)
{
return Mono.justOrEmpty(inputBrandId)
.flatMap(brandId -> brandService.getById(brandId)
.switchIfEmpty(Mono.error(new RuntimeException("Can not find brand."))))
.map(Optional::of)
.defaultIfEmpty(Optional.empty());
}
private Mono<Optional<Category>> getCategory(String inputCategoryId)
{
return Mono.justOrEmpty(inputCategoryId)
.flatMap(brandId -> categoryService.getById(brandId)
.switchIfEmpty(Mono.error(new RuntimeException("Can not find brand."))))
.map(Optional::of)
.defaultIfEmpty(Optional.empty());
}
private Mono<Item> updateAndSave(PatchSpecs specs, Tuple3<Item, Optional<Brand>, Optional<Category>> tuple)
{
Item item = tuple.getT1();
Brand brand = tuple.getT2().orElse(null);
Category category = tuple.getT3().orElse(null);
// TODO do update and save here
return null;
}
It will be more extensible this way and no need for duplication and nested conditionals. Please, verify it works as you expect.
Related
I am using JPA to store data and faced two problems during implementation. I have two entities (Station and Commodity) that have many-to-many relationship with intermediate table so that I had to created the third one. When app receives message it converts its data to entites and should save but sometimes app throwing a ConstraintViolationException because there is null value at foreign key field referencing to Commodity entity.
I've tried simple approach: selecting needed commodity from database and saving it if there is no one. Then I started to use bulk searching all commodities of message and then putting it where are needed. None of them did a trick.
In my opinion the problem could be caused by multi-threading read\insert.
The second problem is that service stop running when exception is thrown. App can lost some of transactions that's not a big deal but it simply stops after rollback.
How can I resolve these conflicts?
Here is code of data handling class and diagram of entities :
#Service
#AllArgsConstructor
#Slf4j
public class ZeromqCommoditiesServiceImpl implements ZeromqCommoditesService {
private final CategoryTransactionHandler categoryHandler;
private final CommodityTransactionHandler commodityHandler;
private final EconomyTransactionHandler economyHandler;
private final StationTransactionHandler stationHandler;
private final SystemTransactionHandler systemHandler;
#Override
#Transactional(
isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRES_NEW,
rollbackFor = Throwable.class)
#Modifying
public void saveData(ZeromqCommodityPayload payload) {
CommodityContent content = payload.getContent();
var station = stationHandler.createOrFindStation(content.getStationName());
var system = systemHandler.createOrFindSystem(content.getSystemName());
var commodityReferences = getMapOfCommodities(content);
station.setSystem(system);
updateEconomies(station, content);
updateProhibited(station, content, commodityReferences);
updateStationCommodities(station, content, commodityReferences);
try {
saveStation(station);
} catch (ConstraintViolationException | PersistentObjectException | DataAccessException e) {
log.error("Error saving commodity info \n" + content, e);
}
}
public void saveStation(StationEntity station) {
stationHandler.saveStation(station);
if (station.getId() != null) {
log.debug(String.format("Updated \"%s\" station info", station.getName()));
} else {
log.debug(String.format("Updated \"%s\" station info", station.getName()));
}
}
private void updateEconomies(StationEntity station, CommodityContent content) {
station.getEconomies().clear();
if (content.getEconomies() != null) {
var economies = content.getEconomies()
.stream()
.map(economy -> {
var stationEconomyEntity = economyHandler.createOrFindEconomy(economy.getName());
Double proportion = economy.getProportion();
stationEconomyEntity.setProportion(proportion != null ? proportion : 1.0);
return stationEconomyEntity;
})
.peek(economy -> economy.setStation(station))
.toList();
station.getEconomies().addAll(economies);
}
}
private void updateProhibited(
StationEntity station,
CommodityContent content,
Map<String, CommodityEntity> commodityEntityMap) {
station.getProhibited().clear();
if (content.getProhibited() != null) {
var prohibitedCommodityEntities = content.getProhibited()
.stream()
.map(prohibited -> {
String eddnName = prohibited.toLowerCase(Locale.ROOT);
CommodityEntity commodityReference = getCommodityEntity(commodityEntityMap, eddnName);
return new ProhibitedCommodityEntity(station, commodityReference);
}
)
.toList();
station.getProhibited().addAll(prohibitedCommodityEntities);
}
}
private void updateStationCommodities(
StationEntity station,
CommodityContent content,
Map<String, CommodityEntity> commodityEntityMap) {
station.getCommodities().clear();
if (content.getCommodities() != null) {
var commodities = content.getCommodities()
.stream()
.map(commodity -> {
CommodityEntity commodityReference = getCommodityEntity(
commodityEntityMap,
commodity.getEddnName());
return StationCommodityEntity.builder()
.commodity(commodityReference)
.buyPrice(commodity.getBuyPrice())
.sellPrice(commodity.getSellPrice())
.demand(commodity.getDemand())
.stock(commodity.getStock())
.station(station)
.build();
})
.toList();
station.getCommodities().addAll(commodities);
}
}
private CommodityEntity getCommodityEntity(Map<String, CommodityEntity> commodityEntityMap, String eddnName) {
return commodityEntityMap.get(eddnName);
}
private Map<String, CommodityEntity> getMapOfCommodities(#NotNull CommodityContent content) {
Set<String> commodities = content.getCommodities()
.stream()
.map(Commodity::getEddnName)
.collect(Collectors.toSet());
if (content.getProhibited() != null && content.getProhibited().size() > 0) {
commodities.addAll(content.getProhibited().
stream()
.map(item -> item.toLowerCase(Locale.ROOT))
.collect(Collectors.toSet()));
}
var commodityReferencesMap = commodityHandler.findAllByEddnName(commodities)
.stream()
.collect(Collectors.toMap(
CommodityEntity::getEddnName,
item -> item
));
commodities.forEach(commodity -> {
if (commodityReferencesMap.get(commodity.toLowerCase()) == null) {
CommodityCategoryEntity category = categoryHandler.createOrFindCategory("Unknown");
CommodityEntity newCommodity = new CommodityEntity(commodity, commodity, category);
CommodityEntity managedCommodity = commodityHandler.saveCommodity(newCommodity);
commodityReferencesMap.put(managedCommodity.getEddnName(), managedCommodity);
}
});
return commodityReferencesMap;
}
}
Thanks in advance
I have this function inside a #Service in Spring Webflux and it is called with a list of friends to know if everyone has been joined to a group. If some friends have no group this method calls an API to get its user's information and then calls another API to tag these guys with joined tag false.
#Service
...
public Flux<Boolean> checkUserHaveGroup(final List<String> friends) {
MatchOperation match1 = Aggregation.match(Criteria.where("friends").in(friends).and("status").is("ACTIVE"));
UnwindOperation unwind1 = Aggregation.unwind("friends");
MatchOperation match2 = Aggregation.match(Criteria.where("friends").in(friends));
GroupOperation group1 = Aggregation.group("friends");
TypedAggregation<Group> a = Aggregation.newAggregation(
Group.class,
match1, unwind1, match2, group1);
return this.reactiveMongoTemplate.aggregate(a, FriendInGroup.class)
.map(friendInGroup -> friendInGroup.id)
.collectList()
.map(users -> haveNoGroupsList(users, friends))
.flatMapMany(noGroupUsers -> {
return Flux.fromIterable(noGroupUsers)
.flatMap(pn -> crmService.deleteAttribute(pn, "joinedAGroup"));
});
}
(this method get the user information)
...
public Mono<UserInfo> userInfoById(final String userId) {
return webClient.get()
.uri(uriBuilder -> uriBuilder.path(constants.getByIdPath() + "/{id}")
.build(userId))
.header("auth", tokenService.token())
.exchange()
.flatMap(response -> {
Mono<UserInfo> responseMono;
if (response.statusCode().equals(HttpStatus.UNAUTHORIZED)) {
responseMono = Mono.error(new UnauthorizedException());
} else if (response.statusCode().equals(HttpStatus.OK)) {
responseMono = response.bodyToMono(UserInfoResponse.class)
.flatMap(uir -> Mono.just(uir.getData()));
} else {
responseMono = Mono.error(new UnhandledException());
}
return responseMono;
});
}
...
private Mono<UserInfo> getUserInfo(String userId) {
return userInfoAdapter.userInfoById(userId);
}
...
public Mono<Boolean> deleteAttribute(final String userId, final String attribute) {
return getUserInfo(userId) <<<<< here we get the users info
.flatMap(ui -> crmDeleteAttribute(ui, attribute)); <<<< this call is never done.
}
...
public Mono<Boolean> crmDeleteAttribute(final UserInfo user, final String attribute) {
return webClient.delete()
.uri(uriBuilder -> uriBuilder
.path(contants.path())
.build(user.getId(), attribute))
.header("auth", tokenService.token())
.exchange().flatMap(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return Mono.just(Boolean.TRUE);
}
if (response.statusCode().equals(HttpStatus.BAD_REQUEST)) {
return Mono.error(CrmServiceBadRequestException::new);
}
if (response.statusCode().equals(HttpStatus.UNAUTHORIZED)) {
return Mono.error(CrmServiceUnauthorizedException::new);
}
return Mono.error(CrmServiceUnhandledException::new);
});
}
After getting the users' info on the API, the API for assign a tag is never been called no matter what I do. I can see in the debugger terminal that the call to UserInfo API was done but after that, the application returns to the controller. Someone could point me to what I'm doing wrong?
Any help is welcome.
Thanks!
if you were developing a search endpoint that can have one or more filters and there are specific rules for combining these filters, how to avoid using multiple "ifs"?
private void addCriteria(ProductFilter filter, Query query) {
//user does not fill anything, throws exception
if (ObjectUtils.isEmpty(filter.getIds()) && !validateComboFilters(filter)) {
throw new RequiredFilterException();
}
//user does not fill id filter but fills other filters
if (ObjectUtils.isEmpty(filter.getIds()) && validateComboFilters(filter)) {
query.addCriteria(Criteria.where(Constants.CREATED_AT)
.gte(filter.getFromDate())
.lte(filter.getToDate()));
query.addCriteria(Criteria.where(Constants.STATUS).in(filter.getStatus()));
}
//user fills id filter and one or more other filters
if (ObjectUtils.isNotEmpty(filter.getIds())) {
query.addCriteria(Criteria.where(Constants.ID)
.is(filter.getIds()));
if (filter.filterByRangeDate()) {
query.addCriteria(Criteria.where(Constants.CREATED_AT)
.gte(filter.getFromDate())
.lte(filter.getToDate()));
}
if (filter.filterByStatus()) {
query.addCriteria(Criteria.where(Constants.STATUS).in(filter.getStatus()));
}
}
}
It could look like this. I didn't check that for compilation, but I think that general approach is more or less correct.
private static final BiFunction<ProductFilter, Query, Boolean> ADD_CRITERIA_IDS = (filter, query) -> {
boolean res = ObjectUtils.isNotEmpty(filter.getIds());
if (res)
query.addCriteria(Criteria.where(Constants.ID).is(filter.getIds()));
return res;
};
private static final BiFunction<ProductFilter, Query, Boolean> ADD_CRITERIA_STATUS = (filter, query) -> {
boolean res = filter.filterByStatus();
if (res)
query.addCriteria(Criteria.where(Constants.STATUS).in(filter.getStatus()));
return res;
};
private static final BiFunction<ProductFilter, Query, Boolean> ADD_CRITERIA_DATE_RANGE = (filter, query) -> {
boolean res = filter.filterByRangeDate();
if (res)
query.addCriteria(Criteria.where(Constants.CREATED_AT)
.gte(filter.getFromDate())
.lte(filter.getToDate()));
return res;
};
private void addCriteria(ProductFilter filter, Query query) {
boolean added = ADD_CRITERIA_IDS.apply(filter, query);
added |= ADD_CRITERIA_STATUS.apply(filter, query);
added |= ADD_CRITERIA_DATE_RANGE.apply(filter, query);
if (!added)
throw new RequiredFilterException();
}
I have ProductInfo object which looks like this
ProductInfo.java
public class ProductInfo
{
private List<String> servicetagInfo;
}
I have Order object like this which has list of Products info
OrderDetail.java
public class OrderDetail
{
private String orderNum;
private List<ProductInfo> productInfo;
}
And then I have a Response object which basically has List of Order objects
Response.java
public class Response
{
private List<OrderDetail> orderInfo;
}
I am getting response as expected.But right now in this format
orderInfo:
0: {orderNum: "162293591",...}
productInfo:
0: {servicetag_info: ["7LSMW33", "49SMW33"]}
1: {servicetag_info: ["JF6XN33", "CQ5XN33"]}
2: {servicetag_info: ["5VRR523", "13LR523"]}
Here I am trying to merge productInfo List to be like this
productInfo:
0: {servicetag_info: ["7LSMW33", "49SMW33","JF6XN33", "CQ5XN33","5VRR523", "13LR523"]}
Just add all strings into one main property.
Here is my code
List<String> serviceTagList = new ArrayList<>();
for (OrderDetail orderDetail : arInvoiceOrderResponseBody.getOrders()) { //Here i am getting orders from external service
if (orderDetail != null) {
if (orderDetail.getProductInfo() != null && orderDetail.getProductInfo().size() > 0) {
for (ProductInfo productInfoDetail : orderDetail.getProductInfo()) {
if (productInfoDetail != null) {
if (productInfoDetail.getServicetagInfo() != null) {
for (String serviceTag : productInfoDetail.getServicetagInfo()) {
serviceTagList.add(serviceTag);
}
}
}
}
}
}
ProductInfo productInfo = new ProductInfo();
productInfo.setServicetagInfo(serviceTagList);
orderDetail.setProductInfo(Arrays.asList(productInfo));
}
Can anyone suggest how can i achieve same using streams in java so that it will be readable.
Try this:
Set<String> tags = order.stream()
.flatMap(order -> order.getProductInfo().stream())
.map(ProductInfo::getServicetagInfo)
.collect(Collectors.toSet());
Full implementation:
for (OrderDetail orderDetail : arInvoiceOrderResponseBody.getOrders()) {
if (orderDetail != null && orderDetail.getProductInfo() != null) {
orderDetail.getProductInfo().removeAll(null); // Remove any null elems
Set<String> tags = orderDetail.getProductInfo().stream()
.flatMap(product -> (product.getServicetagInfo() == null) ? null : product.getServicetagInfo().stream())
.collect(Collectors.toSet());
tags.remove(null); // Remove null if exists
}
ProductInfo productInfo = new ProductInfo();
productInfo.setServicetagInfo(tags);
orderDetail.setProductInfo(Arrays.asList(productInfo));
}
With streams your code could be like this:
arInvoiceOrderResponseBody.getOrders().stream()
.filter(Objects::nonNull)
.forEach(YourClassName::mergeProductInfo);
The method mergeProductInfo would be:
private static void mergeProductInfo(OrderDetail orderDetail) {
List<String> serviceTagList = new ArrayList<>();
if (orderDetail.getProductInfo() != null) {
serviceTagList = orderDetail.getProductInfo().stream()
.filter(Objects::nonNull)
.map(ProductInfo::getServicetagInfo)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
ProductInfo productInfo = new ProductInfo();
productInfo.setServicetagInfo(serviceTagList);
orderDetail.setProductInfo(Arrays.asList(productInfo));
}
It could be simplified if you could be sure that you are not going to receive null lists or elements.
I have this code and I want to know if this is possible with RxJava:
Function queries for a User from a server (async)
The server returns a User JSON object with a list of ID's of associated UserProfile(s)
Then for each of this ID, it needs to fetch the UserProfile given the ID (async also)
For each asynchronously fetched UserProfile append it to the User object, below is my pseudo-code.
I cannot use any blocking code, all request should be async.
Here's the code:
#Override
public Single<User> retrieve(String entityId) {
BaasUser baasUser = new BaasUser();
baasUser.setEntityId(entityId);
baasUser.setIncludes(Arrays.asList("userProfile"));
return baasUser.retrieve().map(user -> {
String username = user.getUsername();
String dateCreated = user.getDateCreated();
String dateUpdated = user.getDateUpdated();
List<UserProfile> userProfiles = new LinkedList<>();
BaasLink userProfileLink = user.getFirstLink();
userProfileLink.getEntities().forEach(stubEntity -> {
Single<UserProfile> observable = stubEntity.retrieve().map(entity -> {
UserProfile userProfile = new UserProfile();
userProfile.setEntityId(entity.getStringProperty("entityId"));
userProfile.setName(entity.getStringProperty("name"));
return userProfile;
});
observable.subscribe(userProfile -> {
// until all UserProfile is fetched this 'retrieve' "callback" should not return
userProfiles.add(userProfile);
}, error -> {
// err
});
});
User user1 = new User();
user1.setDateCreated(dateCreated);
user1.setDateUpdated(dateUpdated);
user1.setUsername(username);
user1.setUserProfiles(userProfiles);
return user1;
});
}
Here you have en example how to do your jobe maybe there is any typeerror becouse i dont have your objects
Single.just("user")
.observeOn(Schedulers.io())
.flatMap(user -> Observable.zip(getCurrentUserData(user),getUserProfiles(user),(t1, t2) -> {
//first func will return user eith some data next function wll return List<UserProfile> userProfiles
return newuser;
}))
.subscribeOn(Schedulers.io())
}
private Single<List<UserProfile>> getUserProfiles(User user) {
Observable.fromIterable( user.getFirstLink().getEntities())
.flatMap(stubEntity ->stubEntity.retrieve())
.map(o -> {
//set user profile data
return userprofile
})
.toList();
}
private Single<User> getCurrentUserData(User user) {
Observable.just(user)
.map(s -> {
//set data
return user;
})
}