package jnpf.limsService.provider; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import jnpf.exception.DataException; import lombok.Data; import lombok.Value; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; /** Business-facing data-item management; dataset and revision choreography stays behind this interface. */ public class LimsElnDataItemManager { private final LimsElnSourceCatalog properties; private final LimsElnDatasetCatalog paths; private final LimsElnSourceMetadata metadata; public LimsElnDataItemManager(LimsElnSourceCatalog properties, LimsElnDatasetCatalog paths, LimsElnSourceMetadata metadata) { this.properties = properties; this.paths = paths; this.metadata = metadata; } public List items() { Map pathVersions = new LinkedHashMap<>(); for (LimsElnDatasetCatalog.Version version : paths.versions()) { pathVersions.put(pathKey(version.getCode(), version.getRevision()), version); } Map pathDefinitions = new LinkedHashMap<>(); Map objectNames = objectNameMap(); Map> fieldsByObject = new LinkedHashMap<>(); Map enabledStates = properties.enabledStates(); List result = new ArrayList<>(); for (LimsElnSourceCatalog.Version version : properties.versions()) { LimsElnSourceCatalog.Definition definition = properties.definition(version); LimsElnDatasetCatalog.Version path = pathVersions.get( pathKey(definition.getDatasetCode(), definition.getDatasetRevision())); if (path == null) throw new DataException("Source path version not found"); LimsElnDatasetCatalog.Draft pathDefinition = pathDefinitions.computeIfAbsent( path.getId(), ignored -> paths.definition(path)); result.add(item(version, definition, path, pathDefinition, objectNames, fieldsByObject, enabledStates.getOrDefault(version.getPropertyCode(), false))); } return result; } public List sourcePaths() { Map objectNames = objectNameMap(); Map> fieldsByObject = new LinkedHashMap<>(); List result = new ArrayList<>(); for (LimsElnDatasetCatalog.Version version : paths.versions()) { result.add(sourcePath(version, paths.definition(version), objectNames, fieldsByObject)); } return result; } public SourcePath createSourcePath(SourcePathDraft command) { if (command == null || command.getRootObjectId() == null || command.getTargetObjectId() == null || command.getOutputFieldId() == null || command.getSampleId() == null) { throw new DataException("Source path and preview sample are required"); } boolean related = !Objects.equals(command.getRootObjectId(), command.getTargetObjectId()); if (related && (command.getFromFieldId() == null || command.getTargetFieldId() == null)) { throw new DataException("Source path relation is required"); } LimsElnDatasetCatalog.Selection output = new LimsElnDatasetCatalog.Selection(); output.setFieldId(command.getOutputFieldId()); output.setStep(related ? 1 : 0); LimsElnDatasetCatalog.Draft draft = new LimsElnDatasetCatalog.Draft(); draft.setCode(command.getCode()); draft.setDisplayName(command.getDisplayName()); draft.setRootObjectId(command.getRootObjectId()); draft.setFields(Collections.singletonMap(command.getOutputKey(), output)); if (related) { LimsElnDatasetCatalog.Relation relation = new LimsElnDatasetCatalog.Relation(); relation.setFromStep(0); relation.setFromFieldId(command.getFromFieldId()); relation.setObjectId(command.getTargetObjectId()); relation.setTargetFieldId(command.getTargetFieldId()); draft.setRelations(Collections.singletonList(relation)); } return sourcePath(paths.createPublished(draft, command.getSampleId())); } public DataItem create(DataItemDraft command) { SourceSelection source = source(command); return item(properties.createDraft(draft(command, source))); } public DataItem save(String id, DataItemDraft command) { if (command == null || command.getExpectedEditVersion() == null) { throw new DataException("Expected edit version is required"); } LimsElnSourceCatalog.Version current = properties.detail(id); if (!current.getPropertyCode().equals(command.getPropertyCode())) { throw new DataException("Source property code is immutable"); } if (current.getEditVersion() != command.getExpectedEditVersion()) { throw new LimsElnReadonlyResolver.SourceFailure("SOURCE_CONFLICT", "Source version has changed"); } SourceSelection source = source(command); LimsElnSourceCatalog.Draft draft = draft(command, source); return item("PUBLISHED".equals(current.getStatus()) ? properties.newVersion(id, draft) : properties.updateDraft(id, command.getExpectedEditVersion(), draft)); } public Preview preview(String id, String sampleId) { LimsElnSourceCatalog.Preview result = properties.preview(id, sampleId); Object value = result.getValues() == null || result.getValues().isEmpty() ? null : result.getValues().get(0); return new Preview(value); } public DataItem publish(String id, int expectedEditVersion, String sampleId) { return item(properties.publish(id, expectedEditVersion, sampleId)); } private LimsElnSourceCatalog.Draft draft(DataItemDraft command, SourceSelection source) { if (command == null) throw new DataException("Data item is required"); LimsElnSourceCatalog.Definition definition = new LimsElnSourceCatalog.Definition(); definition.setDatasetCode(source.path.getCode()); definition.setDatasetRevision(source.path.getRevision()); definition.setFieldId(command.getOutputKey()); return new LimsElnSourceCatalog.Draft(command.getPropertyCode(), command.getDisplayName(), source.dataType, definition); } private SourceSelection source(DataItemDraft command) { if (command == null || command.getSourcePathId() == null || command.getOutputKey() == null) { throw new DataException("Source path and return content are required"); } LimsElnDatasetCatalog.Version path = paths.detail(command.getSourcePathId()); if (!"PUBLISHED".equals(path.getStatus())) throw new DataException("Published source path required"); LimsElnDatasetCatalog.Draft definition = paths.definition(path); LimsElnDatasetCatalog.Selection selection = definition.getFields().get(command.getOutputKey()); if (selection == null) throw new DataException("Source path return content not found"); LimsElnSourceMetadata.Candidate field = candidate(definition, selection); String dataType = "DATE_ONLY".equals(selection.getFormat()) ? "DATE" : field.getDataType(); return new SourceSelection(path, dataType); } private DataItem item(LimsElnSourceCatalog.Version version) { LimsElnSourceCatalog.Definition definition = properties.definition(version); LimsElnDatasetCatalog.Version path = paths.version(definition.getDatasetCode(), definition.getDatasetRevision()); LimsElnDatasetCatalog.Draft pathDefinition = paths.definition(path); return item(version, definition, path, pathDefinition, objectNameMap(), new LinkedHashMap<>(), properties.enabled(version.getPropertyCode())); } private DataItem item(LimsElnSourceCatalog.Version version, LimsElnSourceCatalog.Definition definition, LimsElnDatasetCatalog.Version path, LimsElnDatasetCatalog.Draft pathDefinition, Map objectNames, Map> fieldsByObject, boolean enabled) { LimsElnDatasetCatalog.Selection selection = pathDefinition.getFields().get(definition.getFieldId()); if (selection == null) throw new DataException("Source path return content not found"); PathDescription description = describe(pathDefinition, definition.getFieldId(), objectNames, fieldsByObject); return new DataItem(version.getId(), version.getPropertyCode(), version.getDisplayName(), version.getDataType(), version.getStatus(), enabled, version.getRevision(), version.getEditVersion(), path.getId(), definition.getFieldId(), description.path, description.outputName, returnContent(selection), scope(pathDefinition), version.getPublishedAt(), version.getPublishedBy()); } private SourcePath sourcePath(LimsElnDatasetCatalog.Version version) { LimsElnDatasetCatalog.Draft definition = paths.definition(version); return sourcePath(version, definition, objectNameMap(), new LinkedHashMap<>()); } private SourcePath sourcePath(LimsElnDatasetCatalog.Version version, LimsElnDatasetCatalog.Draft definition, Map objectNames, Map> fieldsByObject) { List outputs = definition.getFields().keySet().stream().map(key -> { LimsElnDatasetCatalog.Selection selection = definition.getFields().get(key); PathDescription description = describe(definition, key, objectNames, fieldsByObject); LimsElnSourceMetadata.Candidate candidate = candidate(definition, selection, fieldsByObject); String type = "DATE_ONLY".equals(selection.getFormat()) ? "DATE" : candidate.getDataType(); return new PathOutput(key, description.outputName, type, returnContent(selection), description.path); }).collect(Collectors.toList()); String pathLabel = outputs.isEmpty() ? objectNames(definition, objectNames).stream().collect(Collectors.joining(" > ")) : outputs.get(0).getPath(); return new SourcePath(version.getId(), version.getCode(), version.getDisplayName(), version.getStatus(), version.getRevision(), version.getEditVersion(), pathLabel, scope(definition), outputs, version.getPublishedAt(), version.getPublishedBy()); } private PathDescription describe(LimsElnDatasetCatalog.Draft definition, String outputKey, Map objectNames, Map> fieldsByObject) { LimsElnDatasetCatalog.Selection selection = definition.getFields().get(outputKey); if (selection == null) throw new DataException("Source path return content not found"); List objects = objectNames(definition, objectNames); if (selection.getStep() < 0 || selection.getStep() >= objects.size()) { throw new DataException("Invalid source path output"); } LimsElnSourceMetadata.Candidate field = candidate(definition, selection, fieldsByObject); List segments = new ArrayList<>(objects.subList(0, selection.getStep() + 1)); segments.add(field.getDisplayName()); return new PathDescription(String.join(" > ", segments), field.getDisplayName()); } private Map objectNameMap() { return metadata.objects().stream().collect(Collectors.toMap( LimsElnSourceMetadata.BusinessObject::getId, LimsElnSourceMetadata.BusinessObject::getDisplayName, (left, right) -> left, LinkedHashMap::new)); } private List objectNames(LimsElnDatasetCatalog.Draft definition, Map names) { List result = new ArrayList<>(); result.add(names.getOrDefault(definition.getRootObjectId(), definition.getRootObjectId())); for (LimsElnDatasetCatalog.Relation relation : definition.getRelations()) { result.add(names.getOrDefault(relation.getObjectId(), relation.getObjectId())); } return result; } private LimsElnSourceMetadata.Candidate candidate(LimsElnDatasetCatalog.Draft definition, LimsElnDatasetCatalog.Selection selection) { return candidate(definition, selection, new LinkedHashMap<>()); } private LimsElnSourceMetadata.Candidate candidate(LimsElnDatasetCatalog.Draft definition, LimsElnDatasetCatalog.Selection selection, Map> fieldsByObject) { List objectIds = new ArrayList<>(); objectIds.add(definition.getRootObjectId()); objectIds.addAll(definition.getRelations().stream().map(LimsElnDatasetCatalog.Relation::getObjectId) .collect(Collectors.toList())); if (selection.getStep() < 0 || selection.getStep() >= objectIds.size()) { throw new DataException("Invalid source path output"); } String objectId = objectIds.get(selection.getStep()); return fieldsByObject.computeIfAbsent(objectId, metadata::fields).stream() .filter(item -> item.getId().equals(selection.getFieldId())).findFirst() .orElseThrow(() -> new DataException("Source path field is no longer available")); } private static String returnContent(LimsElnDatasetCatalog.Selection selection) { return "DISPLAY_NAME".equals(selection.getValueMode()) ? "显示名称" : "原值"; } private static String scope(LimsElnDatasetCatalog.Draft definition) { return definition.getConditions() == null || definition.getConditions().isEmpty() ? "全部检验项" : "指定范围(" + definition.getConditions().size() + " 条规则)"; } private static String pathKey(String code, Integer revision) { return code + "\u0000" + revision; } @Data @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class DataItemDraft { private String propertyCode; private String displayName; private String sourcePathId; private String outputKey; private Integer expectedEditVersion; } @Data @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class SourcePathDraft { private String code; private String displayName; private String rootObjectId; private String targetObjectId; private String fromFieldId; private String targetFieldId; private String outputKey; private String outputFieldId; private String sampleId; } @Value @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class Preview { Object value; } @Value @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class DataItem { String id; String propertyCode; String displayName; String dataType; String status; boolean enabled; int revision; int editVersion; String sourcePathId; String outputKey; String sourcePath; String outputName; String returnContent; String scope; String publishedAt; String publishedBy; } @Value @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class SourcePath { String id; String code; String displayName; String status; int revision; int editVersion; String path; String scope; List outputs; String publishedAt; String publishedBy; } @Value @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class PathOutput { String key; String displayName; String dataType; String returnContent; String path; } @Value private static class SourceSelection { LimsElnDatasetCatalog.Version path; String dataType; } @Value private static class PathDescription { String path; String outputName; } }