From e0b3bda7d06a12edae105d2cd046bfd78f6269a8 Mon Sep 17 00:00:00 2001 From: Hendrixx-RE Date: Fri, 8 May 2026 02:07:59 +0530 Subject: [PATCH] [MOSIP] SDK-based biometric quality evaluation with SBI fallback Introduces evaluateQuality() orchestrator in BioServiceImpl that routes quality scoring through the SDK when available, falling back to SBI when the provider is unbound, and blocking on configured SDK failures (timeout, null response, runtime exception). Fixes getCapturedBiometrics() which previously used SBI qualityScore for threshold gating even when sdkScore was populated. Changes: - BioServiceImpl: add evaluateQuality(), getSdkTimeoutMs(), sdkExecutor; refactor captureModality() to use evaluateQuality(); fix threshold gating in getCapturedBiometrics() to use effective score (SDK > SBI) - QualityEvaluationResult: new DTO carrying score, source, and fallback flag - BiometricsDto: add qualitySource field to track score origin end-to-end - RegistrationExceptionConstants: add REG-BQC-003 through REG-BQC-007 - RegistrationConstants: add SDK_QUALITY_TIMEOUT config key - AuditEvent: add SDK_QUALITY_SUCCESS/FAILED/FALLBACK/DISABLED and NO_VALID_QUALITY_SCORE Signed-off-by: Hendrixx-RE --- .../registration/constants/AuditEvent.java | 7 + .../constants/RegistrationConstants.java | 2 + .../biometric/QualityEvaluationResult.java | 35 +++++ .../dto/packetmanager/BiometricsDto.java | 1 + .../RegistrationExceptionConstants.java | 5 + .../service/bio/impl/BioServiceImpl.java | 134 ++++++++++++++++-- 6 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 registration/registration-services/src/main/java/io/mosip/registration/dto/biometric/QualityEvaluationResult.java diff --git a/registration/registration-services/src/main/java/io/mosip/registration/constants/AuditEvent.java b/registration/registration-services/src/main/java/io/mosip/registration/constants/AuditEvent.java index 937d040a908..c0448fe463c 100644 --- a/registration/registration-services/src/main/java/io/mosip/registration/constants/AuditEvent.java +++ b/registration/registration-services/src/main/java/io/mosip/registration/constants/AuditEvent.java @@ -280,6 +280,13 @@ public enum AuditEvent { MDM_NO_DEVICE_AVAILABLE("REG-EVT-087", USER_EVENT.getCode(), "DEVICE_NOT_FOUND", "No device is available"), MDM_DEVICE_FOUND("REG-EVT-088", USER_EVENT.getCode(), "MDM_DEVICE_FOUND", "Device is found"), + // SDK Quality Evaluation + SDK_QUALITY_SUCCESS("REG-EVT-104", SYSTEM_EVENT.getCode(), "SDK_QUALITY_SUCCESS", "SDK biometric quality evaluation succeeded"), + SDK_QUALITY_FAILED("REG-EVT-105", SYSTEM_EVENT.getCode(), "SDK_QUALITY_FAILED", "SDK biometric quality evaluation failed, registration blocked"), + SDK_FALLBACK_TO_SBI("REG-EVT-106", SYSTEM_EVENT.getCode(), "SDK_FALLBACK_TO_SBI", "SDK unavailable, fell back to SBI quality score"), + SDK_QUALITY_DISABLED("REG-EVT-107", SYSTEM_EVENT.getCode(), "SDK_QUALITY_DISABLED", "SDK quality evaluation disabled, using SBI score"), + NO_VALID_QUALITY_SCORE("REG-EVT-108", SYSTEM_EVENT.getCode(), "NO_VALID_QUALITY_SCORE", "No valid quality score from SDK or SBI, registration blocked"), + REG_DOC_SCAN("REG-EVT-089", USER_EVENT.getCode(), "REG_DOC_SCAN", "Doc: Click of Scan"), REG_DOC_VIEW("REG-EVT-090", USER_EVENT.getCode(), "REG_DOC_VIEW", "Doc: View"), REG_DOC_DELETE("REG-EVT-091", USER_EVENT.getCode(), "REG_DOC_DELETE", "Doc: Delete"), diff --git a/registration/registration-services/src/main/java/io/mosip/registration/constants/RegistrationConstants.java b/registration/registration-services/src/main/java/io/mosip/registration/constants/RegistrationConstants.java index 76befc7dd2b..756f05a9482 100644 --- a/registration/registration-services/src/main/java/io/mosip/registration/constants/RegistrationConstants.java +++ b/registration/registration-services/src/main/java/io/mosip/registration/constants/RegistrationConstants.java @@ -1266,6 +1266,8 @@ private RegistrationConstants() { // flag for quality check with SDK public static final String QUALITY_CHECK_WITH_SDK = "mosip.registration.quality_check_with_sdk"; public static final String UPDATE_SDK_QUALITY_SCORE = "mosip.registration.replace_sdk_quality_score"; + public static final String SDK_QUALITY_TIMEOUT = "mosip.registration.sdk_quality_timeout_ms"; + public static final long SDK_QUALITY_TIMEOUT_DEFAULT_MS = 5000L; // Packet Sync public static final String PACKET_SYNC = "packet_sync"; diff --git a/registration/registration-services/src/main/java/io/mosip/registration/dto/biometric/QualityEvaluationResult.java b/registration/registration-services/src/main/java/io/mosip/registration/dto/biometric/QualityEvaluationResult.java new file mode 100644 index 00000000000..06cc0f22335 --- /dev/null +++ b/registration/registration-services/src/main/java/io/mosip/registration/dto/biometric/QualityEvaluationResult.java @@ -0,0 +1,35 @@ +package io.mosip.registration.dto.biometric; + +/** + * Holds the outcome of a biometric quality evaluation, including the score, + * the source that produced it (SDK or SBI), and whether a fallback occurred. + */ +public class QualityEvaluationResult { + + private final double score; + private final String source; + private final boolean fallback; + + public QualityEvaluationResult(double score, String source, boolean fallback) { + this.score = score; + this.source = source; + this.fallback = fallback; + } + + public double getScore() { + return score; + } + + public String getSource() { + return source; + } + + public boolean isFallback() { + return fallback; + } + + @Override + public String toString() { + return "QualityEvaluationResult{score=" + score + ", source='" + source + "', fallback=" + fallback + "}"; + } +} diff --git a/registration/registration-services/src/main/java/io/mosip/registration/dto/packetmanager/BiometricsDto.java b/registration/registration-services/src/main/java/io/mosip/registration/dto/packetmanager/BiometricsDto.java index c05b52fa34e..1d681f09d0b 100644 --- a/registration/registration-services/src/main/java/io/mosip/registration/dto/packetmanager/BiometricsDto.java +++ b/registration/registration-services/src/main/java/io/mosip/registration/dto/packetmanager/BiometricsDto.java @@ -20,6 +20,7 @@ public class BiometricsDto { private boolean isCaptured; private String subType; private double sdkScore; + private String qualitySource; private String payLoad; private String signature; private String specVersion; diff --git a/registration/registration-services/src/main/java/io/mosip/registration/exception/RegistrationExceptionConstants.java b/registration/registration-services/src/main/java/io/mosip/registration/exception/RegistrationExceptionConstants.java index 1b029f1f174..f8ba85b89a4 100644 --- a/registration/registration-services/src/main/java/io/mosip/registration/exception/RegistrationExceptionConstants.java +++ b/registration/registration-services/src/main/java/io/mosip/registration/exception/RegistrationExceptionConstants.java @@ -91,6 +91,11 @@ public enum RegistrationExceptionConstants { REG_FINGERPRINT_SCANNING_ERROR(RegistrationConstants.USER_REG_FINGERPRINT_CAPTURE_EXP_CODE+"FCS-002", "Exception while scanning fingerprints of the individual"), REG_BIOMETRIC_QUALITY_CHECK_ERROR("REG-BQC-001", "Exception while evaluating the biometrics quality with SDK"), REG_BIOMETRIC_QUALITY_SCORE_RANGE_ERROR("REG-BQC-002", "Exception while evaluating the biometrics quality score"), + REG_SDK_TIMEOUT("REG-BQC-003", "Biometric quality evaluation timed out. Please re-capture biometrics."), + REG_SDK_NULL_RESPONSE("REG-BQC-004", "Invalid biometric quality score received from SDK."), + REG_SDK_RUNTIME_ERROR("REG-BQC-005", "Error occurred during SDK-based quality evaluation. Please retry capture."), + REG_SBI_SCORE_MISSING("REG-BQC-006", "Default biometric quality score unavailable."), + REG_NO_QUALITY_SOURCE("REG-BQC-007", "Biometric quality evaluation is not configured. Registration cannot proceed."), REG_OTP_VALIDATION("REG-OV-001","Erroe while validating the otp"), REG_PACKET_DATE_PARSER_CODE(PACKET_CREATION_EXP_CODE + "TGE-001", "Exception while parsing the date to display in acknowledgement receipt"), REG_PACKET_JSON_VALIDATOR_ERROR_CODE(PACKET_CREATION_EXP_CODE + "PCS-003", "Exception while validating ID json file"), diff --git a/registration/registration-services/src/main/java/io/mosip/registration/service/bio/impl/BioServiceImpl.java b/registration/registration-services/src/main/java/io/mosip/registration/service/bio/impl/BioServiceImpl.java index 48f96cbbc34..518e5596ff8 100644 --- a/registration/registration-services/src/main/java/io/mosip/registration/service/bio/impl/BioServiceImpl.java +++ b/registration/registration-services/src/main/java/io/mosip/registration/service/bio/impl/BioServiceImpl.java @@ -7,6 +7,7 @@ import java.io.InputStream; import java.time.temporal.ValueRange; import java.util.*; +import java.util.concurrent.*; import io.mosip.registration.dto.schema.UiFieldDTO; import io.mosip.registration.enums.Modality; @@ -29,6 +30,7 @@ import io.mosip.registration.context.ApplicationContext; import io.mosip.registration.context.SessionContext; import io.mosip.registration.dto.RegistrationDTO; +import io.mosip.registration.dto.biometric.QualityEvaluationResult; import io.mosip.registration.dto.packetmanager.BiometricsDto; import io.mosip.registration.exception.RegBaseCheckedException; import io.mosip.registration.exception.RegistrationExceptionConstants; @@ -68,6 +70,8 @@ public class BioServiceImpl extends BaseService implements BioService { @Autowired private BIRBuilder birBuilder; + private final ExecutorService sdkExecutor = Executors.newSingleThreadExecutor(); + /** * Gets the registration DTO from session. * @@ -97,16 +101,11 @@ public List captureModality(MDMRequestDto mdmRequestDto) throws R throw new RegBaseCheckedException(RegistrationExceptionConstants.REG_BIOMETRIC_QUALITY_SCORE_RANGE_ERROR.getErrorCode(), RegistrationExceptionConstants.REG_BIOMETRIC_QUALITY_SCORE_RANGE_ERROR.getErrorMessage()); - if (RegistrationConstants.ENABLE.equalsIgnoreCase((String) ApplicationContext.map() - .getOrDefault(RegistrationConstants.QUALITY_CHECK_WITH_SDK, RegistrationConstants.DISABLE))) { - try { - biometricsDto.setSdkScore(getSDKScore(biometricsDto)); - } catch (BiometricException e) { - LOGGER.error("Unable to fetch SDK Score ", e); - throw new RegBaseCheckedException(RegistrationExceptionConstants.REG_BIOMETRIC_QUALITY_CHECK_ERROR.getErrorCode(), - RegistrationExceptionConstants.REG_BIOMETRIC_QUALITY_CHECK_ERROR.getErrorMessage()); - } - } + QualityEvaluationResult result = evaluateQuality(biometricsDto); + biometricsDto.setSdkScore(result.getScore()); + biometricsDto.setQualitySource(result.getSource()); + LOGGER.info("Quality evaluation complete: score={}, source={}, fallback={}", + result.getScore(), result.getSource(), result.isFallback()); list.add(biometricsDto); } } catch (RegBaseCheckedException e) { @@ -161,6 +160,111 @@ public InputStream getStream(MdmBioDevice mdmBioDevice, String modality) throws RegistrationExceptionConstants.MDS_BIODEVICE_NOT_FOUND.getErrorMessage()); } + /** + * Orchestrates biometric quality evaluation across SDK and SBI sources. + * + * Decision rules: + * - SDK disabled in config: use SBI score (AS-03) + * - SDK enabled but provider unbound: fall back to SBI score (AS-02) + * - SDK enabled, provider bound, call fails (timeout/null/exception): block, no fallback + */ + private QualityEvaluationResult evaluateQuality(BiometricsDto biometricsDto) throws RegBaseCheckedException { + boolean sdkEnabled = RegistrationConstants.ENABLE.equalsIgnoreCase( + (String) ApplicationContext.map().getOrDefault( + RegistrationConstants.QUALITY_CHECK_WITH_SDK, RegistrationConstants.DISABLE)); + + if (!sdkEnabled) { + LOGGER.info("SDK quality evaluation disabled, using SBI score for {}", biometricsDto.getBioAttribute()); + return new QualityEvaluationResult(biometricsDto.getQualityScore(), "SBI", false); + } + + BiometricType biometricType = BiometricType + .fromValue(io.mosip.commons.packet.constants.Biometric + .getSingleTypeByAttribute(biometricsDto.getBioAttribute()).name()); + + // Check provider availability before committing to SDK path + Object provider = null; + try { + provider = bioAPIFactory.getBioProvider(biometricType, BiometricFunction.QUALITY_CHECK); + } catch (Exception e) { + LOGGER.warn("SDK provider not available for {}, falling back to SBI", biometricType); + } + + if (provider == null) { + LOGGER.info("SDK provider unbound, falling back to SBI score for {}", biometricsDto.getBioAttribute()); + double sbiScore = biometricsDto.getQualityScore(); + if (sbiScore <= 0) { + LOGGER.error("SBI score also unavailable for {}", biometricsDto.getBioAttribute()); + throw new RegBaseCheckedException( + RegistrationExceptionConstants.REG_SBI_SCORE_MISSING.getErrorCode(), + RegistrationExceptionConstants.REG_SBI_SCORE_MISSING.getErrorMessage()); + } + return new QualityEvaluationResult(sbiScore, "SBI", true); + } + + // SDK provider is bound - call with timeout. Any failure here blocks registration. + long timeoutMs = getSdkTimeoutMs(); + BIR bir = birBuilder.buildBir(biometricsDto, ProcessedLevelType.RAW); + BIR[] birList = new BIR[]{bir}; + + Future future = sdkExecutor.submit(() -> { + try { + Map scoreMap = bioAPIFactory + .getBioProvider(biometricType, BiometricFunction.QUALITY_CHECK) + .getModalityQuality(birList, null); + return (scoreMap != null && scoreMap.get(biometricType) != null) + ? scoreMap.get(biometricType).doubleValue() + : null; + } catch (Exception e) { + LOGGER.error("SDK provider threw during quality check", e); + throw new RuntimeException(e); + } + }); + + try { + Double score = future.get(timeoutMs, TimeUnit.MILLISECONDS); + if (score == null) { + LOGGER.error("SDK returned null quality score for {}", biometricsDto.getBioAttribute()); + throw new RegBaseCheckedException( + RegistrationExceptionConstants.REG_SDK_NULL_RESPONSE.getErrorCode(), + RegistrationExceptionConstants.REG_SDK_NULL_RESPONSE.getErrorMessage()); + } + LOGGER.info("SDK quality score {} received for {}", score, biometricsDto.getBioAttribute()); + return new QualityEvaluationResult(score, "SDK", false); + + } catch (TimeoutException e) { + future.cancel(true); + LOGGER.error("SDK quality evaluation timed out after {}ms for {}", timeoutMs, biometricsDto.getBioAttribute()); + throw new RegBaseCheckedException( + RegistrationExceptionConstants.REG_SDK_TIMEOUT.getErrorCode(), + RegistrationExceptionConstants.REG_SDK_TIMEOUT.getErrorMessage()); + + } catch (ExecutionException e) { + LOGGER.error("SDK runtime error during quality evaluation for {}", biometricsDto.getBioAttribute(), e); + throw new RegBaseCheckedException( + RegistrationExceptionConstants.REG_SDK_RUNTIME_ERROR.getErrorCode(), + RegistrationExceptionConstants.REG_SDK_RUNTIME_ERROR.getErrorMessage()); + + } catch (RegBaseCheckedException e) { + throw e; + + } catch (Exception e) { + LOGGER.error("Unexpected error during SDK quality evaluation", e); + throw new RegBaseCheckedException( + RegistrationExceptionConstants.REG_SDK_RUNTIME_ERROR.getErrorCode(), + RegistrationExceptionConstants.REG_SDK_RUNTIME_ERROR.getErrorMessage()); + } + } + + private long getSdkTimeoutMs() { + String configured = getGlobalConfigValueOf(RegistrationConstants.SDK_QUALITY_TIMEOUT); + try { + return configured != null ? Long.parseLong(configured) : RegistrationConstants.SDK_QUALITY_TIMEOUT_DEFAULT_MS; + } catch (NumberFormatException e) { + return RegistrationConstants.SDK_QUALITY_TIMEOUT_DEFAULT_MS; + } + } + @Override public double getSDKScore(BiometricsDto biometricsDto) throws BiometricException { BiometricType biometricType = BiometricType @@ -196,7 +300,15 @@ public Map getCapturedBiometrics(@NonNull UiFieldDTO fieldDto, capturedContext.put(attribute, true); continue; } - quality = quality + biometricsDto.getQualityScore(); + double effectiveScore = (biometricsDto.getSdkScore() > 0) + ? biometricsDto.getSdkScore() + : biometricsDto.getQualityScore(); + if (effectiveScore <= 0) { + LOGGER.warn("No valid quality score available for attribute {}", attribute); + capturedContext.put(attribute, false); + continue; + } + quality = quality + effectiveScore; capturedAttributes.add(attribute); } //if some attributes are captured, determine capture status based on threshold check