http://jira.mfd.msk:8088/browse/CLS-769 вывод средств OM*T checks
This commit is contained in:
parent
62f0f3246f
commit
ef0bb6cf7d
8 changed files with 377 additions and 10 deletions
|
|
@ -15,8 +15,17 @@ public class ClearingServiceSettings {
|
|||
private KafkaConsumerSettings kafkaConsumer;
|
||||
private KafkaProducerSettings kafkaProducer;
|
||||
private SessionStageSettings sessionStage;
|
||||
private boolean balanceCheckOmt = false;
|
||||
private TradeSettings trade = new TradeSettings();
|
||||
|
||||
public boolean isBalanceCheckOmt() {
|
||||
return balanceCheckOmt;
|
||||
}
|
||||
|
||||
public void setBalanceCheckOmt(boolean balanceCheckOmt) {
|
||||
this.balanceCheckOmt = balanceCheckOmt;
|
||||
}
|
||||
|
||||
public HazelcastClientParams getHazelcast() {
|
||||
return hazelcast;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
package ru.spcex.clearing.service.payment;
|
||||
|
||||
import static ru.spcex.clearing.session.stage.impl.GatewayRequester.mapError;
|
||||
import static ru.spcex.platform.utils.number.BigDecimalUtil.safeBD;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Locale;
|
||||
|
|
@ -38,11 +35,13 @@ import ru.spcex.clearing.service.Sdf54Creator;
|
|||
import ru.spcex.clearing.service.builder.PaymentInstructionBuilderV2;
|
||||
import ru.spcex.clearing.service.integration.GatewayRequestCreator;
|
||||
import ru.spcex.clearing.service.registry.AssetTBFProcessing;
|
||||
import ru.spcex.clearing.service.registry.BalanceChecker;
|
||||
import ru.spcex.clearing.service.registry.DmiService;
|
||||
import ru.spcex.clearing.service.registry.RegistryManager;
|
||||
import ru.spcex.clearing.service.schedule.TradingTimeService;
|
||||
import ru.spcex.clearing.service.validation.ValidationStored;
|
||||
import ru.spcex.clearing.session.stage.impl.GatewayRequester;
|
||||
import static ru.spcex.clearing.session.stage.impl.GatewayRequester.mapError;
|
||||
import ru.spcex.clearing.util.LocaleUtil;
|
||||
import ru.spcex.clearing.util.security.UserRoleVerification;
|
||||
import ru.spcex.platform.enumeration.CurrencyCode;
|
||||
|
|
@ -57,6 +56,7 @@ import ru.spcex.platform.utils.enumeration.EnumMessage;
|
|||
import ru.spcex.platform.utils.enumeration.IEnumId;
|
||||
import ru.spcex.platform.utils.enumeration.IMessageResolver;
|
||||
import ru.spcex.platform.utils.localization.SpringPropertiesLocalizer;
|
||||
import static ru.spcex.platform.utils.number.BigDecimalUtil.safeBD;
|
||||
import ru.spcex.platform.utils.text.TextUtil;
|
||||
import ru.spcex.platform.utils.validation.IValidator;
|
||||
|
||||
|
|
@ -83,6 +83,7 @@ public class PaymentInstructionOutboundService {
|
|||
private final TradingTimeService time;
|
||||
private final GatewayRequester gateway;
|
||||
private final NotificationSender notification;
|
||||
private final BalanceChecker balanceChecker;
|
||||
private final SpringPropertiesLocalizer resolver;
|
||||
|
||||
@Autowired
|
||||
|
|
@ -90,7 +91,7 @@ public class PaymentInstructionOutboundService {
|
|||
IMessageResolver msgs,
|
||||
ImdgProvider imdgProvider,
|
||||
Function<PIClearingOutbondActionNewRequest, IValidator> validation, RegistryManager rgsMng, KafkaSender kafkaSender, AssetTBFProcessing assetsMng, DmiService dmiService, TradingTimeService time, GatewayRequester gateway, NotificationSender notification,
|
||||
SpringPropertiesLocalizer resolver) {
|
||||
SpringPropertiesLocalizer resolver, BalanceChecker balanceChecker) {
|
||||
this.imdgProvider = imdgProvider;
|
||||
this.rights = rights;
|
||||
this.msgs = msgs;
|
||||
|
|
@ -102,6 +103,7 @@ public class PaymentInstructionOutboundService {
|
|||
this.rgsImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Registry, Registry.class);
|
||||
this.currImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Currency, Currency.class);
|
||||
this.sdf54Imdg = imdgProvider.getImdg(IMDGDistributedNames.Map_SDf54, SDf54.class);
|
||||
this.balanceChecker = balanceChecker;
|
||||
this.sdf54Creator = new Sdf54Creator(imdgProvider, resolver);
|
||||
this.idGenerator = imdgProvider.getImdgIdGenerator();
|
||||
this.rgsMng = rgsMng;
|
||||
|
|
@ -173,14 +175,22 @@ public class PaymentInstructionOutboundService {
|
|||
//fixme pmt.setDocumentNumber(registry.getSecurityId());
|
||||
|
||||
pmtImdg.insert(pmt);
|
||||
log.debug("new PaymentInstruction.id: {}", pmt.getId());
|
||||
log.debug("PIClearingOutbondActionNewRequest.id={} new PaymentInstruction.id: {}",
|
||||
req.getId(),
|
||||
pmt.getId());
|
||||
Optional<AssetTrio> assets = assetsMng.searchMoneyByAccAndCompany(addressee.getId(), accCred.getId());
|
||||
if (assets.isEmpty() || assetsMng.insufficientBalance(assets.get().a__f(), amount)) {
|
||||
log.debug("PIClearingOutbondActionNewRequest.id={} a__f asset id={}",
|
||||
req.getId(),
|
||||
assets.map(AssetTrio::a__f).map(Registry::getId).orElse(null));
|
||||
if (assets.isEmpty() || balanceChecker.insufficientBalance(assets.get().a__f(),
|
||||
tcr.getId(), //never null because validation
|
||||
accCred.getCurrency(),
|
||||
amount)) {
|
||||
pmt.setTransactionStatus(TransactionStatus.cher.getKey());
|
||||
pmt.setUpdated(now);
|
||||
pmtImdg.update(pmt);
|
||||
log.debug("insufficient balance, pmt.id: {}, am*f.balance={}",
|
||||
pmt.getId(), assets.map(AssetTrio::a__f).map(Registry::getBalance).orElse(null));
|
||||
pmt.getId(), assets.map(AssetTrio::a__f).map(Registry::getBalance).orElse(null));
|
||||
return error(req.getId(), ClearingError.BalanceInsufficient);
|
||||
}
|
||||
log.debug("assets found and balance is ok. am*b.id={}, am*t.id={}, am*f.id={}",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,247 @@
|
|||
package ru.spcex.clearing.service.registry;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.clearing.classes.statics.data.registry.Registry;
|
||||
import ru.clearing.classes.statics.data.security.CurrencyPairSecurity;
|
||||
import ru.clearing.classes.statics.data.security.Rates;
|
||||
import ru.clearing.classes.statics.data.security.RiskParameter;
|
||||
import ru.spcex.clearing.config.element.ClearingServiceSettings;
|
||||
import ru.spcex.clearing.imdg.IMDGDistributedNames;
|
||||
import static ru.spcex.clearing.util.NumberUtils.safeCast;
|
||||
import ru.spcex.platform.classes.base.SpcexObjectBase;
|
||||
import ru.spcex.platform.enumeration.RegistryStatus;
|
||||
import static ru.spcex.platform.enumeration.RegistryTradingParams.OM_T;
|
||||
import ru.spcex.platform.enumeration.WorkflowStatus;
|
||||
import ru.spcex.platform.imdg.api.Imdg;
|
||||
import ru.spcex.platform.imdg.api.ImdgProvider;
|
||||
import ru.spcex.platform.imdg.api.predicate.ImdgPredicate;
|
||||
import ru.spcex.platform.imdg.api.predicate.ImdgPredicateBuilder;
|
||||
import ru.spcex.platform.imdg.api.predicate.specific.RegistryCodeSqlBuilder;
|
||||
import ru.spcex.platform.imdg.iml.hazelcast.adapter.CashCloser;
|
||||
import ru.spcex.platform.imdg.iml.hazelcast.adapter.CashV2;
|
||||
import ru.spcex.platform.imdg.iml.hazelcast.adapter.key.GenericCashingKey1;
|
||||
import ru.spcex.platform.imdg.iml.hazelcast.adapter.key.GenericCashingKey2;
|
||||
import static ru.spcex.platform.utils.number.BigDecimalUtil.safeBD;
|
||||
import ru.spcex.platform.utils.text.TextUtil;
|
||||
|
||||
@Component
|
||||
public class BalanceChecker {
|
||||
private final Logger log = LoggerFactory.getLogger(getClass());
|
||||
private final Imdg<Registry> rgsImdg;
|
||||
private final Imdg<Rates> rateImdg;
|
||||
private final Imdg<RiskParameter> riskParamImdg;
|
||||
private final Imdg<CurrencyPairSecurity> currPairImdg;
|
||||
private final boolean balanceCheckOmts;
|
||||
|
||||
@Autowired
|
||||
public BalanceChecker(ImdgProvider imdgProvider, ClearingServiceSettings settings) {
|
||||
this.rgsImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Registry, Registry.class);
|
||||
this.rateImdg = imdgProvider.getCashingImdg(IMDGDistributedNames.Map_Rates, Rates.class, null);
|
||||
this.riskParamImdg = imdgProvider.getCashingImdg(IMDGDistributedNames.Map_RiskParameter, RiskParameter.class, null);
|
||||
this.currPairImdg = imdgProvider.getCashingImdg(IMDGDistributedNames.Map_CurrencyPairSecurity, CurrencyPairSecurity.class, null);
|
||||
this.balanceCheckOmts = settings.isBalanceCheckOmt();
|
||||
}
|
||||
|
||||
public boolean insufficientBalance(Registry a__f,
|
||||
Long tcrId,
|
||||
String accCurrency,
|
||||
BigDecimal amount) {
|
||||
if (!balanceCheckOmts) {
|
||||
log.info("{}#id={}#balance={} check for amount {}; OM*T checks disabled",
|
||||
a__f.getRegistryCode(), a__f.getId(), a__f.getBalance(), amount);
|
||||
return safeBD(a__f.getBalance()).compareTo(safeBD(amount)) < 0;
|
||||
}
|
||||
log.info("{}#id={}#balance={} check for amount {}; OM*T checks enabled",
|
||||
a__f.getRegistryCode(), a__f.getId(), a__f.getBalance(), amount);
|
||||
LocalDate today = LocalDate.now();
|
||||
ImdgPredicateBuilder rgsPb = rgsImdg.predicateBuilder();
|
||||
ImdgPredicateBuilder ratePb = rateImdg.predicateBuilder();
|
||||
|
||||
Function<ImdgPredicate, ImdgPredicate> rgsAnd = p -> rgsPb.and(
|
||||
rgsPb.sql(RegistryCodeSqlBuilder.getInstance(OM_T).build()),
|
||||
rgsPb.equals("tradingClearingRegistryId", tcrId),
|
||||
rgsPb.or(
|
||||
rgsPb.equals("registryStatus", RegistryStatus.PROC.getKey()),
|
||||
rgsPb.equals("registryStatus", RegistryStatus.MNG.getKey())
|
||||
),
|
||||
p
|
||||
);
|
||||
|
||||
try (BalanceCheckerCash cash = new BalanceCheckerCash()) {
|
||||
Collection<Registry> omtsToday = rgsImdg.getCollectionObjectsByPredicate(
|
||||
rgsAnd.apply(
|
||||
rgsPb.equals("settlementDate", today)
|
||||
)
|
||||
);
|
||||
Collection<Registry> omtsNotToday = rgsImdg.getCollectionObjectsByPredicate(
|
||||
rgsAnd.apply(
|
||||
rgsPb.not(rgsPb.equals("settlementDate", today))
|
||||
)
|
||||
);
|
||||
log.debug("a__f.id={} balance checking; omt today: {}, omt !today: {}",
|
||||
a__f.getId(), omtsToday.size(), omtsNotToday.size());
|
||||
|
||||
BiFunction<Collection<Registry>, String, BigDecimal> omtSum = (rgss, settlType)
|
||||
-> rgss
|
||||
.stream()
|
||||
.map(r -> {
|
||||
Optional<Rates> crossRate = loadRate(r, today, cash);
|
||||
Optional<RiskParameter> riskParam = loadRisk(r, settlType, cash);
|
||||
BigDecimal oblAmount = r.getBalance();
|
||||
BigDecimal rate = crossRate.map(Rates::getValue).orElse(BigDecimal.ONE);
|
||||
BigDecimal risk = riskParam.map(RiskParameter::getValue).orElse(BigDecimal.ONE);
|
||||
BigDecimal singleOblSum = safeBD(oblAmount)
|
||||
.multiply(rate)
|
||||
.multiply(risk);
|
||||
log.debug("for {}.id={}: Rate.id={}; RiskParameter.id={}",
|
||||
r.getRegistryCode(), r.getId(),
|
||||
crossRate.map(SpcexObjectBase::getId).orElse(null),
|
||||
riskParam.map(SpcexObjectBase::getId).orElse(null)
|
||||
);
|
||||
return singleOblSum;
|
||||
})
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
|
||||
BigDecimal todaySum = omtSum.apply(omtsToday, "T0");
|
||||
BigDecimal notTodaySum = omtSum.apply(omtsNotToday, "T1");
|
||||
|
||||
Rates rate = rateImdg.getFirstObjectByPredicate(
|
||||
ratePb.and(
|
||||
ratePb.equals("currency", accCurrency),
|
||||
ratePb.equals("valueDate", today)
|
||||
)
|
||||
);
|
||||
BigDecimal rateValue = (rate != null && rate.getValue() != null) ?
|
||||
rate.getValue() : BigDecimal.ONE;
|
||||
|
||||
BigDecimal sumForObligations = todaySum
|
||||
.add(notTodaySum)
|
||||
.divide(rateValue, RoundingMode.HALF_UP);
|
||||
|
||||
log.debug("a__f.id={}, today OM*T sum {}; !today OM*T sum {}; rate for final formula: id={}, value={}; overall OM*T sum: {}",
|
||||
a__f.getId(), todaySum, notTodaySum,
|
||||
rate != null ? rate.getId() : null,
|
||||
rateValue,
|
||||
sumForObligations
|
||||
);
|
||||
|
||||
return safeBD(a__f.getBalance())
|
||||
.subtract(sumForObligations)
|
||||
.compareTo(safeBD(amount)) < 0;
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Rates> loadRate(Registry rgs,
|
||||
LocalDate valueDate,
|
||||
BalanceCheckerCash cash) {
|
||||
if (TextUtil.isEmpty(rgs.getSecuritySymbol())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
ImdgPredicateBuilder ratePb = rateImdg.predicateBuilder();
|
||||
ImdgPredicate ratePredicate = ratePb.and(
|
||||
ratePb.equals("valueDate", valueDate),
|
||||
ratePb.equals("currency", rgs.getSecuritySymbol())
|
||||
);
|
||||
return Optional.ofNullable(rateImdg.getFirstObjectByPredicate(
|
||||
ratePb.cashed(
|
||||
ratePredicate,
|
||||
cash.ratesCash,
|
||||
new GenericCashingKey2<>(valueDate, rgs.getSecuritySymbol())
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
private CurrencyPairSecurity loadCurrPairSec(String contract, BalanceCheckerCash cash) {
|
||||
ImdgPredicateBuilder currPb = currPairImdg.predicateBuilder();
|
||||
Long contractCasted = safeCast(contract);
|
||||
if (contractCasted == null) {
|
||||
return null;
|
||||
}
|
||||
ImdgPredicate prdct = currPb.cashed(
|
||||
currPb.equals("currencyPairId", contractCasted),
|
||||
cash.currPairCash,
|
||||
new GenericCashingKey1<>(contractCasted));
|
||||
return currPairImdg.getFirstObjectByPredicate(
|
||||
prdct
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private Optional<RiskParameter> loadRisk(
|
||||
Registry rgs, String settlementType, BalanceCheckerCash cash
|
||||
) {
|
||||
CurrencyPairSecurity pair = loadCurrPairSec(
|
||||
rgs.getContract(), cash
|
||||
);
|
||||
|
||||
if (pair == null) {
|
||||
log.warn("cannot find currencyPairSecurity for {}#id={}#contract={}",
|
||||
rgs.getRegistryCode(),
|
||||
rgs.getId(),
|
||||
rgs.getContract());
|
||||
return Optional.empty();
|
||||
}
|
||||
log.debug("found currencyPairSecurity.id={} for {}.id={}",
|
||||
rgs.getRegistryCode(),
|
||||
rgs.getId(),
|
||||
pair.getId());
|
||||
|
||||
ImdgPredicateBuilder riskPb = riskParamImdg.predicateBuilder();
|
||||
|
||||
|
||||
ImdgPredicate prdct = riskPb.and(
|
||||
riskPb.equals("currencyPairId", pair.getId()),
|
||||
riskPb.equals("settlementType", settlementType),
|
||||
riskPb.equals("workflowStatus", WorkflowStatus.Active.getKey())
|
||||
);
|
||||
ImdgPredicate prdctF = riskPb.cashed(
|
||||
prdct,
|
||||
cash.riskCash,
|
||||
new GenericCashingKey2<>(pair.getId(), settlementType)
|
||||
);
|
||||
return Optional.ofNullable(riskParamImdg.getFirstObjectByPredicate(prdctF));
|
||||
}
|
||||
|
||||
protected static class BalanceCheckerCash extends CashCloser {
|
||||
protected final CashV2<GenericCashingKey2<LocalDate, String>, Rates> ratesCash;
|
||||
protected final CashV2<GenericCashingKey2<Long, String>, RiskParameter> riskCash;
|
||||
protected final CashV2<GenericCashingKey1<Long>, CurrencyPairSecurity> currPairCash;
|
||||
|
||||
public BalanceCheckerCash() {
|
||||
cashes = new ArrayList<>();
|
||||
ratesCash = add(new CashV2<GenericCashingKey2<LocalDate, String>, Rates>() {
|
||||
@Override
|
||||
protected GenericCashingKey2<LocalDate, String> extractKey(Rates obj) {
|
||||
return new GenericCashingKey2<>(obj.getValueDate(), obj.getCurrency());
|
||||
}
|
||||
});
|
||||
riskCash = add(new CashV2<GenericCashingKey2<Long, String>, RiskParameter>() {
|
||||
@Override
|
||||
protected GenericCashingKey2<Long, String> extractKey(RiskParameter obj) {
|
||||
return new GenericCashingKey2<>(obj.getCurrencyPairId(), obj.getSettlementType());
|
||||
}
|
||||
});
|
||||
currPairCash = add(new CashV2<GenericCashingKey1<Long>, CurrencyPairSecurity>() {
|
||||
@Override
|
||||
protected GenericCashingKey1<Long> extractKey(CurrencyPairSecurity obj) {
|
||||
return new GenericCashingKey1<>(obj.getCurrencyPairId());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package ru.spcex.clearing.util;
|
||||
|
||||
import ru.spcex.platform.utils.text.TextUtil;
|
||||
|
||||
public class NumberUtils {
|
||||
public static Long safeCast(String s) {
|
||||
if (TextUtil.isEmpty(s)) return null;
|
||||
try {
|
||||
return Long.parseLong(s);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package ru.spcex.platform.imdg.iml.hazelcast.adapter.key;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class GenericCashingKey1<T1> {
|
||||
private final T1 first;
|
||||
|
||||
public GenericCashingKey1(T1 first) {
|
||||
this.first = first;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof GenericCashingKey1<?> that)) return false;
|
||||
|
||||
return Objects.equals(first, that.first);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(first);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package ru.spcex.platform.imdg.iml.hazelcast.adapter.key;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class GenericCashingKey2<T1, T2> {
|
||||
private final T1 first;
|
||||
private final T2 second;
|
||||
|
||||
public GenericCashingKey2(T1 first, T2 second) {
|
||||
this.first = first;
|
||||
this.second = second;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof GenericCashingKey2<?, ?> that)) return false;
|
||||
|
||||
return Objects.equals(first, that.first) && Objects.equals(second, that.second);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hashCode(first);
|
||||
result = 31 * result + Objects.hashCode(second);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package ru.spcex.platform.imdg.iml.hazelcast.adapter.key;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class GenericCashingKey3<T1, T2, T3> {
|
||||
private final T1 first;
|
||||
private final T2 second;
|
||||
private final T3 third;
|
||||
|
||||
public GenericCashingKey3(T1 first, T2 second, T3 third) {
|
||||
this.first = first;
|
||||
this.second = second;
|
||||
this.third = third;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof GenericCashingKey3<?, ?, ?> that)) return false;
|
||||
|
||||
return Objects.equals(first, that.first)
|
||||
&& Objects.equals(second, that.second)
|
||||
&& Objects.equals(third, that.third);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hashCode(first);
|
||||
result = 31 * result + Objects.hashCode(second);
|
||||
result = 31 * result + Objects.hashCode(third);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
package ru.spcex.platform.utils.number;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
import ru.spcex.platform.utils.text.TextUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import ru.spcex.platform.utils.text.TextUtil;
|
||||
|
||||
public class BigDecimalUtil {
|
||||
|
||||
|
|
@ -77,6 +75,10 @@ public class BigDecimalUtil {
|
|||
return bd == null ? BigDecimal.ZERO : bd;
|
||||
}
|
||||
|
||||
public static BigDecimal safeBD(BigDecimal bd, BigDecimal defaultValue) {
|
||||
return bd == null ? defaultValue : bd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Null safe add:
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue