package jnpf.limsService.provider; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.exception.DataException; import jnpf.lims.api.model.ElnSourceReference; import lombok.Data; import lombok.Value; import java.time.Instant; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.function.Supplier; import java.util.stream.Collectors; /** Configuration writes and authorized business-data reads use separate adapters. */ public class LimsElnSourceCatalog { private final Store store; private final Supplier actors; private final SourceReader reader; private final ObjectMapper json = new ObjectMapper(); public LimsElnSourceCatalog(Store store, Supplier actors) { this(store, actors, (definition, actor, sampleId) -> { throw new DataException("Source preview is unavailable"); }); } public LimsElnSourceCatalog(Store store, Supplier actors, SourceReader reader) { this.store = store; this.actors = actors; this.reader = reader; } @AuditLog(action = "ELN_SOURCE_DRAFT_CREATE", bizType = "ELN_SOURCE", bizCodeExpr = "#result?.propertyCode", bizCodeRequired = false, targetTable = "lims_eln_provider_property_version", targetIdExpr = "#result?.id") public Version createDraft(Draft draft) { Actor actor = requireManager(); if (draft == null || draft.getPropertyCode() == null || !draft.getPropertyCode().matches("[a-zA-Z][a-zA-Z0-9_.-]{0,79}")) { throw new DataException("Invalid source property code"); } validateMetadata(draft); if (store.list(actor.getTenantId()).stream() .anyMatch(v -> draft.getPropertyCode().equals(v.getPropertyCode()))) { throw new DataException("Source property already exists"); } Version version = new Version(UUID.randomUUID().toString(), actor.getTenantId(), draft.getPropertyCode(), 1, "DRAFT", draft.getDisplayName(), draft.getDataType(), encode(draft.getDefinition()), null, null, null, null, 0); store.insert(version); return version; } @AuditLog(action = "ELN_SOURCE_PUBLISH", failureAction = "ELN_SOURCE_PUBLISH_FAILED", bizType = "ELN_SOURCE", bizCodeExpr = "#result?.propertyCode", bizCodeRequired = false, targetTable = "lims_eln_provider_property_version", targetIdExpr = "#id") public Version publish(String id, int expectedEditVersion, String sampleId) { Actor actor = requireManager(); Version draft = detail(id); if (!"DRAFT".equals(draft.getStatus())) throw new DataException("Published source is immutable"); if (draft.getEditVersion() != expectedEditVersion) throw new LimsElnReadonlyResolver.SourceFailure("SOURCE_CONFLICT", "Source version has changed"); if (sampleId == null || sampleId.trim().isEmpty()) { throw new DataException("An authorized preview sample is required"); } Preview preview = reader.read(decode(draft.getDefinitionJson()), actor, sampleId); if (preview == null || preview.getValues() == null || preview.getValues().size() != 1 || preview.getValues().get(0) == null || preview.getValues().get(0) instanceof String && ((String) preview.getValues().get(0)).trim().isEmpty()) { throw new DataException("Source preview must resolve one value"); } if (!matchesType(draft.getDataType(), preview.getValues().get(0))) { throw new DataException("Source preview type does not match"); } try { JsonNode frozen = preview.getFrozenDefinitionJson() == null ? null : json.readTree(preview.getFrozenDefinitionJson()); if (frozen == null || !frozen.isObject() || frozen.isEmpty()) { throw new DataException("A complete resolved definition is required"); } } catch (JsonProcessingException e) { throw new DataException("Resolved source definition is invalid"); } Version published = new Version(draft.getId(), draft.getTenantId(), draft.getPropertyCode(), draft.getRevision(), "PUBLISHED", draft.getDisplayName(), draft.getDataType(), draft.getDefinitionJson(), preview.getFrozenDefinitionJson(), sampleId, actor.getUserId(), Instant.now().toString(), draft.getEditVersion() + 1); if (!store.replaceDraft(draft, published)) throw new LimsElnReadonlyResolver.SourceFailure("SOURCE_CONFLICT", "Source version has changed"); return published; } public Preview preview(String id, String sampleId) { Actor actor = requireManager(); Version version = detail(id); if (sampleId == null || sampleId.trim().isEmpty()) { throw new DataException("An authorized preview sample is required"); } return reader.read(decode(version.getDefinitionJson()), actor, sampleId); } public Version detail(String id) { Actor actor = requireManager(); Version version = store.find(actor.getTenantId(), id); if (version == null) throw new DataException("Source version not found"); return version; } @AuditLog(action = "ELN_SOURCE_DRAFT_UPDATE", bizType = "ELN_SOURCE", bizCodeExpr = "#result?.propertyCode", bizCodeRequired = false, targetTable = "lims_eln_provider_property_version", targetIdExpr = "#id") public Version updateDraft(String id, int expectedEditVersion, Draft changes) { Version previous = detail(id); if (!"DRAFT".equals(previous.getStatus())) throw new DataException("Published source is immutable"); if (previous.getEditVersion() != expectedEditVersion) throw new LimsElnReadonlyResolver.SourceFailure("SOURCE_CONFLICT", "Source version has changed"); if (changes == null || !previous.getPropertyCode().equals(changes.getPropertyCode())) { throw new DataException("Source property code is immutable"); } validateMetadata(changes); Version replacement = new Version(previous.getId(), previous.getTenantId(), previous.getPropertyCode(), previous.getRevision(), "DRAFT", changes.getDisplayName(), changes.getDataType(), encode(changes.getDefinition()), null, null, null, null, previous.getEditVersion() + 1); if (!store.replaceDraft(previous, replacement)) throw new LimsElnReadonlyResolver.SourceFailure("SOURCE_CONFLICT", "Source version has changed"); return replacement; } @AuditLog(action = "ELN_SOURCE_VERSION_CREATE", bizType = "ELN_SOURCE", bizCodeExpr = "#result?.propertyCode", bizCodeRequired = false, targetTable = "lims_eln_provider_property_version", targetIdExpr = "#result?.id") public Version newVersion(String publishedId, Draft changes) { Version previous = detail(publishedId); if (!"PUBLISHED".equals(previous.getStatus())) throw new DataException("Published source required"); if (changes == null || !previous.getPropertyCode().equals(changes.getPropertyCode())) { throw new DataException("Source property code is immutable"); } validateMetadata(changes); int revision = 0; for (Version version : store.list(previous.getTenantId())) { if (!previous.getPropertyCode().equals(version.getPropertyCode())) continue; if ("DRAFT".equals(version.getStatus())) throw new DataException("Source already has a draft"); revision = Math.max(revision, version.getRevision()); } Version draft = new Version(UUID.randomUUID().toString(), previous.getTenantId(), previous.getPropertyCode(), revision + 1, "DRAFT", changes.getDisplayName(), changes.getDataType(), encode(changes.getDefinition()), null, null, null, null, 0); store.insert(draft); return draft; } public ElnSourceReference reference(String code, int revision) { Version version = published(code, revision); return reference(version); } public List references() { return catalog().stream().map(version -> reference(version, true)).collect(Collectors.toList()); } private ElnSourceReference reference(Version version) { return reference(version, store.enabled(version.getTenantId(), version.getPropertyCode())); } private ElnSourceReference reference(Version version, boolean enabled) { ElnSourceReference result = new ElnSourceReference(); result.setPropertyCode(version.getPropertyCode()); result.setRevision(version.getRevision()); result.setDisplayName(version.getDisplayName()); result.setDataType(version.getDataType()); result.setEnabled(enabled); return result; } public Version published(String code, int revision) { return published(code, revision, requireActor()); } Version published(String code, int revision, Actor actor) { requireActor(actor); return store.list(actor.getTenantId()).stream() .filter(v -> code.equals(v.getPropertyCode()) && revision == v.getRevision() && "PUBLISHED".equals(v.getStatus())).findFirst() .orElseThrow(() -> new LimsElnReadonlyResolver.SourceFailure("UNSUPPORTED_PROPERTY", "Published source version not found")); } @AuditLog(action = "ELN_SOURCE_SET_ENABLED", bizType = "ELN_SOURCE", bizCodeExpr = "#code", targetTable = "lims_eln_provider_property", targetIdExpr = "#code") public void setEnabled(String code, boolean enabled) { Actor actor = requireManager(); if (store.list(actor.getTenantId()).stream().noneMatch(v -> code.equals(v.getPropertyCode()))) { throw new DataException("Source property not found"); } store.setEnabled(actor.getTenantId(), code, enabled); } public Definition definition(String id) { return decode(detail(id).getDefinitionJson()); } Definition definition(Version version) { if (version == null) throw new DataException("Source version is required"); return decode(version.getDefinitionJson()); } private Definition decode(String definitionJson) { try { return json.readValue(definitionJson, Definition.class); } catch (JsonProcessingException e) { throw new DataException("Source definition is invalid"); } } public List catalog() { Actor actor = requireManager(); Map enabledStates = store.enabledStates(actor.getTenantId()); Map current = new LinkedHashMap<>(); for (Version version : store.list(actor.getTenantId())) { if (!"PUBLISHED".equals(version.getStatus())) continue; Version previous = current.get(version.getPropertyCode()); if (previous == null || previous.getRevision() < version.getRevision()) { current.put(version.getPropertyCode(), version); } } return current.values().stream() .filter(v -> enabledStates.getOrDefault(v.getPropertyCode(), false)) .collect(Collectors.toList()); } public List versions() { return store.list(requireManager().getTenantId()); } public boolean enabled(String code) { return store.enabled(requireManager().getTenantId(), code); } Map enabledStates() { return store.enabledStates(requireManager().getTenantId()); } private Actor requireManager() { Actor actor = requireActor(); if (!actor.isFieldManager()) throw new LimsElnReadonlyResolver.SourceFailure("SOURCE_FORBIDDEN", "Field management permission required"); return actor; } private Actor requireActor() { return requireActor(actors.get()); } static Actor requireActor(Actor actor) { if (actor == null || actor.getUserId() == null || actor.getUserId().trim().isEmpty()) { throw new LimsElnReadonlyResolver.SourceFailure("SOURCE_FORBIDDEN", "Authenticated source caller required"); } if (!"0".equals(actor.getTenantId())) throw new LimsElnReadonlyResolver.SourceFailure("SOURCE_FORBIDDEN", "Invalid tenant context"); return actor; } private String encode(Definition definition) { if (definition == null) throw new DataException("Source definition is required"); try { return json.writeValueAsString(definition); } catch (JsonProcessingException e) { throw new DataException("Source definition is invalid"); } } private static boolean matchesType(String type, Object value) { if ("TEXT".equals(type)) return value instanceof String; if ("DECIMAL".equals(type)) { return value instanceof java.math.BigDecimal || value instanceof java.math.BigInteger || value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long; } if ("DATE".equals(type)) return value instanceof java.time.LocalDate || value instanceof java.sql.Date; if ("DATETIME".equals(type)) { return value instanceof java.time.LocalDateTime || value instanceof java.time.OffsetDateTime || value instanceof Instant || value instanceof java.sql.Timestamp; } return "BOOLEAN".equals(type) && value instanceof Boolean; } private static void validateMetadata(Draft draft) { if (draft.getDisplayName() == null || draft.getDisplayName().trim().isEmpty() || draft.getDisplayName().length() > 200) { throw new DataException("Source business name is required and must not exceed 200 characters"); } if (!java.util.Arrays.asList("TEXT", "DECIMAL", "DATE", "DATETIME", "BOOLEAN").contains(draft.getDataType())) { throw new DataException("Unsupported source scalar type"); } } public interface Store { void insert(Version version); Version find(String tenantId, String id); List list(String tenantId); boolean replaceDraft(Version expected, Version replacement); boolean enabled(String tenantId, String code); Map enabledStates(String tenantId); void setEnabled(String tenantId, String code, boolean enabled); } /** * Trusted business-data adapter: validate controlled metadata, authorize the sample and enforce * single-valued relationships. Return the complete executable snapshot, never a client preview token. */ public interface SourceReader { Preview read(Definition definition, Actor actor, String sampleId); } @Value public static class Preview { List values; String frozenDefinitionJson; } /** Server-derived context, not a request body. */ @Value public static class Actor { String tenantId; String userId; boolean fieldManager; } @Value public static class Draft { String propertyCode; String displayName; String dataType; Definition definition; } @Data @com.fasterxml.jackson.databind.annotation.JsonNaming(com.fasterxml.jackson.databind.PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class Definition { private String objectId; private String fieldId; private String datasetCode; private Integer datasetRevision; } @Value @com.fasterxml.jackson.databind.annotation.JsonNaming(com.fasterxml.jackson.databind.PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class Version { String id; String tenantId; String propertyCode; int revision; String status; String displayName; String dataType; String definitionJson; @com.fasterxml.jackson.annotation.JsonIgnore String frozenDefinitionJson; String validationSampleId; String publishedBy; String publishedAt; int editVersion; } }