package jnpf.elnService.impl; import jnpf.audit.model.AuditEventDTO; import jnpf.eln.RecordFillSaveCommand; import jnpf.eln.RecordFillSaveOperation; import jnpf.elnEntity.runtime.ElnRecordEntity; import jnpf.elnEntity.runtime.ElnRecordForms; import jnpf.elnEntity.runtime.ElnRecordRepeatRowEntity; import jnpf.elnEntity.runtime.ElnRecordRevisionEntity; import jnpf.elnEntity.runtime.ElnRecordVos; import jnpf.elnEntity.template.ElnTemplateVos; import jnpf.elnMapper.ElnRecordMapper; import jnpf.elnMapper.ElnRecordRepeatRowMapper; import jnpf.elnMapper.ElnRecordRevisionMapper; import jnpf.elnService.ElnRecordFillAccessService; import jnpf.elnService.ElnRecordFillTicketService; import jnpf.elnService.template.ElnTemplateRepeatGroupService; import jnpf.exception.DataException; import jnpf.util.JsonUtil; import org.junit.Test; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.SimpleTransactionStatus; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.fail; public class ElnRecordRepeatServiceImplTest { @Test public void appendsOnceAndReplaysThePreparedRowForTheSameOperationCode() { ElnRecordEntity record = new ElnRecordEntity(); record.setId("record-1"); record.setMubanId("template-1"); record.setDangqianXiudingId("revision-1"); ElnRecordRevisionEntity revision = new ElnRecordRevisionEntity(); revision.setId("revision-1"); revision.setDianziJiluId("record-1"); revision.setJiluWendang("file-1"); revision.setBizStatus("DRAFT"); AtomicReference inserted = new AtomicReference<>(); AtomicInteger rowSelections = new AtomicInteger(); AtomicInteger inserts = new AtomicInteger(); ElnRecordRepeatRowMapper rows = mapper(ElnRecordRepeatRowMapper.class, (method, arguments) -> { if ("selectOne".equals(method)) { return rowSelections.incrementAndGet() >= 3 ? inserted.get() : null; } if ("insert".equals(method)) { inserted.set((ElnRecordRepeatRowEntity) arguments[0]); inserts.incrementAndGet(); return 1; } if ("selectCount".equals(method)) return 0L; return null; }); RecordingTransactionManager transactions = new RecordingTransactionManager(); ElnRecordRepeatServiceImpl service = new ElnRecordRepeatServiceImpl( mapper(ElnRecordMapper.class, (method, arguments) -> "selectOne".equals(method) ? record : null), mapper(ElnRecordRevisionMapper.class, (method, arguments) -> "selectOne".equals(method) ? revision : null), rows, repeatGroups(group("measurements", 0, 10), group("results", 0, 2)), allowFill(), transactions, ignored -> { }, new ElnRecordRepeatServiceImpl.TenantRoute() { @Override public void switchTo(String tenantId) { } @Override public void clear() { } }); ElnRecordForms.RepeatAppend form = new ElnRecordForms.RepeatAppend(); form.setGroupCode("results"); form.setOperationCode("operation-1"); ElnRecordVos.RepeatRow first = service.append(ticket(), form); ElnRecordVos.RepeatRow replay = service.append(ticket(), form); assertNotNull(first.getRowId()); assertNotEquals(inserted.get().getId(), inserted.get().getRowId()); assertEquals(first.getRowId(), replay.getRowId()); assertEquals(Integer.valueOf(1), first.getRowOrder()); assertEquals("PREPARED", first.getStatus()); assertEquals(1, inserts.get()); assertEquals(2, transactions.commits.get()); } @Test public void allocatesAnotherPreparedRowForADifferentOperationCode() { ElnRecordEntity record = new ElnRecordEntity(); record.setId("record-1"); record.setMubanId("template-1"); record.setDangqianXiudingId("revision-1"); ElnRecordRevisionEntity revision = new ElnRecordRevisionEntity(); revision.setId("revision-1"); revision.setDianziJiluId("record-1"); revision.setJiluWendang("file-1"); revision.setBizStatus("DRAFT"); ElnRecordRepeatRowEntity pending = new ElnRecordRepeatRowEntity(); pending.setRowId("row-pending"); pending.setDianziJiluId("record-1"); pending.setMubanId("template-1"); pending.setRepeatGroupCode("results"); pending.setRowOrder(1); pending.setOperationStatus("PREPARED"); AtomicInteger selections = new AtomicInteger(); AtomicInteger inserts = new AtomicInteger(); ElnRecordRepeatServiceImpl service = new ElnRecordRepeatServiceImpl( mapper(ElnRecordMapper.class, (method, arguments) -> "selectOne".equals(method) ? record : null), mapper(ElnRecordRevisionMapper.class, (method, arguments) -> "selectOne".equals(method) ? revision : null), mapper(ElnRecordRepeatRowMapper.class, (method, arguments) -> { if ("selectOne".equals(method)) { return selections.incrementAndGet() == 2 ? pending : null; } if ("insert".equals(method)) { inserts.incrementAndGet(); return 1; } if ("selectCount".equals(method)) return 1L; return null; }), repeatGroups(group("results", 0, 2)), allowFill(), new RecordingTransactionManager(), ignored -> { }, new ElnRecordRepeatServiceImpl.TenantRoute() { @Override public void switchTo(String tenantId) { } @Override public void clear() { } }); ElnRecordForms.RepeatAppend form = new ElnRecordForms.RepeatAppend(); form.setGroupCode("results"); form.setOperationCode("operation-after-reopen"); ElnRecordVos.RepeatRow appended = service.append(ticket(), form); assertNotEquals("row-pending", appended.getRowId()); assertEquals(Integer.valueOf(2), appended.getRowOrder()); assertEquals("PREPARED", appended.getStatus()); assertEquals(1, inserts.get()); } @Test public void rejectsAppendWhenAllocatedRowsReachTheTargetGroupMaximum() { ElnRecordEntity record = record(); ElnRecordRevisionEntity revision = revision(); AtomicInteger inserts = new AtomicInteger(); ElnRecordRepeatRowMapper rows = mapper(ElnRecordRepeatRowMapper.class, (method, arguments) -> { if ("selectOne".equals(method)) return null; if ("selectCount".equals(method)) return 2L; if ("insert".equals(method)) inserts.incrementAndGet(); return null; }); ElnRecordRepeatServiceImpl service = new ElnRecordRepeatServiceImpl( mapper(ElnRecordMapper.class, (method, arguments) -> "selectOne".equals(method) ? record : null), mapper(ElnRecordRevisionMapper.class, (method, arguments) -> "selectOne".equals(method) ? revision : null), rows, repeatGroups(group("results", 0, 2)), allowFill(), new RecordingTransactionManager(), ignored -> { }, new ElnRecordRepeatServiceImpl.TenantRoute() { @Override public void switchTo(String tenantId) { } @Override public void clear() { } }); ElnRecordForms.RepeatAppend form = new ElnRecordForms.RepeatAppend(); form.setGroupCode("results"); form.setOperationCode("operation-over-limit"); try { service.append(ticket(), form); fail("allocated rows at maxRows must reject append"); } catch (DataException expected) { assertEquals("重复组已达到最大行数 2", expected.getMessage()); } assertEquals(0, inserts.get()); } @Test public void preparesAndRestoresDeleteIntentIdempotentlyWithAudit() { ElnRecordEntity record = record(); ElnRecordRevisionEntity revision = revision(); ElnRecordRepeatRowEntity row = activeRow(); AtomicInteger selections = new AtomicInteger(); AtomicInteger updates = new AtomicInteger(); ElnRecordRepeatRowMapper rows = mapper(ElnRecordRepeatRowMapper.class, (method, arguments) -> { if ("selectOne".equals(method)) { int selection = selections.incrementAndGet(); if (selection == 1) return null; return row; } if ("selectCount".equals(method)) return 2L; if ("updateById".equals(method)) { updates.incrementAndGet(); return 1; } return null; }); List audits = new ArrayList<>(); ElnRecordRepeatServiceImpl service = new ElnRecordRepeatServiceImpl( mapper(ElnRecordMapper.class, (method, arguments) -> "selectOne".equals(method) ? record : null), mapper(ElnRecordRevisionMapper.class, (method, arguments) -> "selectOne".equals(method) ? revision : null), rows, repeatGroups(group("results", 1, 10)), allowFill(), new RecordingTransactionManager(), audits::add, new ElnRecordRepeatServiceImpl.TenantRoute() { @Override public void switchTo(String tenantId) { } @Override public void clear() { } }); ElnRecordForms.RepeatDelete delete = new ElnRecordForms.RepeatDelete(); delete.setGroupCode("results"); delete.setRowId("row-1"); delete.setOperationCode("delete-1"); delete.setReason("录入错误"); ElnRecordVos.RepeatRow prepared = service.requestDelete(ticket(), delete); ElnRecordVos.RepeatRow replay = service.requestDelete(ticket(), delete); assertEquals("DELETE_PREPARED", prepared.getStatus()); assertEquals("delete-1", prepared.getDeleteOperationCode()); assertEquals("录入错误", replay.getDeleteReason()); assertEquals(1, updates.get()); ElnRecordForms.RepeatRestore restore = new ElnRecordForms.RepeatRestore(); restore.setGroupCode("results"); restore.setRowId("row-1"); restore.setOperationCode("delete-1"); ElnRecordVos.RepeatRow restored = service.restoreDelete(ticket(), restore); assertEquals("ACTIVE", restored.getStatus()); assertEquals(null, restored.getDeleteOperationCode()); assertEquals(2, updates.get()); assertEquals(Arrays.asList("ELN_RECORD_REPEAT_DELETE_PREPARE", "ELN_RECORD_REPEAT_DELETE_RESTORE"), Arrays.asList(audits.get(0).getActionCode(), audits.get(1).getActionCode())); assertNotEquals(audits.get(0).getClientEventId(), audits.get(1).getClientEventId()); } @Test public void rejectsDeleteIntentWhenItWouldFallBelowTheGroupMinimum() { ElnRecordRepeatRowEntity row = activeRow(); AtomicInteger selections = new AtomicInteger(); ElnRecordRepeatServiceImpl service = new ElnRecordRepeatServiceImpl( mapper(ElnRecordMapper.class, (method, arguments) -> "selectOne".equals(method) ? record() : null), mapper(ElnRecordRevisionMapper.class, (method, arguments) -> "selectOne".equals(method) ? revision() : null), mapper(ElnRecordRepeatRowMapper.class, (method, arguments) -> { if ("selectOne".equals(method)) { return selections.incrementAndGet() == 1 ? null : row; } if ("selectCount".equals(method)) return 1L; return null; }), repeatGroups(group("results", 1, 10)), allowFill(), new RecordingTransactionManager(), ignored -> { }, new ElnRecordRepeatServiceImpl.TenantRoute() { @Override public void switchTo(String tenantId) { } @Override public void clear() { } }); ElnRecordForms.RepeatDelete delete = new ElnRecordForms.RepeatDelete(); delete.setGroupCode("results"); delete.setRowId("row-1"); delete.setOperationCode("delete-min"); delete.setReason("录入错误"); try { service.requestDelete(ticket(), delete); fail("delete intent below minRows must be rejected"); } catch (DataException expected) { assertEquals("重复组至少保留 1 行", expected.getMessage()); } } private static ElnRecordRepeatRowEntity activeRow() { ElnRecordRepeatRowEntity row = new ElnRecordRepeatRowEntity(); row.setId("snapshot-1"); row.setRowId("row-1"); row.setDianziJiluId("record-1"); row.setXiudingId("revision-1"); row.setMubanId("template-1"); row.setRepeatGroupCode("results"); row.setRowOrder(1); row.setOperationCode("append-1"); row.setOperationStatus("ACTIVE"); row.setFTenantId("tenant-1"); row.setFDeleteMark(0); return row; } private static ElnRecordEntity record() { ElnRecordEntity record = new ElnRecordEntity(); record.setId("record-1"); record.setMubanId("template-1"); record.setDangqianXiudingId("revision-1"); return record; } private static ElnRecordRevisionEntity revision() { ElnRecordRevisionEntity revision = new ElnRecordRevisionEntity(); revision.setId("revision-1"); revision.setDianziJiluId("record-1"); revision.setJiluWendang("file-1"); revision.setBizStatus("DRAFT"); return revision; } private static ElnTemplateVos.RepeatGroup group(String code, int minRows, int maxRows) { ElnTemplateVos.RepeatGroup group = new ElnTemplateVos.RepeatGroup(); group.setGroupCode(code); group.setMinRows(minRows); group.setMaxRows(maxRows); return group; } private static ElnTemplateRepeatGroupService repeatGroups( ElnTemplateVos.RepeatGroup... groups) { List configured = Arrays.asList(groups); return new ElnTemplateRepeatGroupService() { @Override public List list( String tenantId, String templateId) { return configured; } @Override public ElnTemplateVos.RepeatGroup ensureConfigured( String tenantId, String templateId, String groupCode, Integer minRows, Integer maxRows, String stepId, String operatorId) { return null; } @Override public void copyForVersion(String tenantId, String sourceTemplateId, String targetTemplateId) { } @Override public void afterBindingsActivated(String tenantId, String sourceTemplateId, String targetTemplateId, Map bindingIdMap) { } }; } private static ElnRecordFillAccessService allowFill() { return new ElnRecordFillAccessService() { @Override public boolean canFill(String tenantId, String userId, String recordId, String fileId) { return true; } @Override public RecordFillSaveOperation preflight(RecordFillSaveCommand command) { return null; } @Override public RecordFillSaveOperation commit(RecordFillSaveCommand command) { return null; } @Override public RecordFillSaveOperation abort(RecordFillSaveCommand command) { return null; } @Override public RecordFillSaveOperation saveStatus(String tenantId, String operationCode) { return null; } }; } private static ElnRecordFillTicketService.FillTicketContext ticket() { Map payload = new LinkedHashMap<>(); payload.put("scope", ElnRecordFillTicketService.SCOPE); payload.put("tenantId", "tenant-1"); payload.put("userId", "user-1"); payload.put("fileId", "file-1"); payload.put("bizDataId", "record-1"); payload.put("bizModule", ElnRecordFillTicketService.BIZ_MODULE); payload.put("bizScene", ElnRecordFillTicketService.BIZ_SCENE); payload.put("pluginGuid", "asc.{7F4E8F35-5D66-47F8-A5B4-6AB3FAACF565}"); return new ElnRecordFillTicketService( ignored -> JsonUtil.getObjectToString(payload)).parse("ticket"); } @SuppressWarnings("unchecked") private static T mapper(Class type, MapperHandler handler) { return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, (proxy, method, arguments) -> { if (method.getDeclaringClass() == Object.class) { if ("toString".equals(method.getName())) return type.getSimpleName() + "Fake"; if ("hashCode".equals(method.getName())) return System.identityHashCode(proxy); if ("equals".equals(method.getName())) return proxy == arguments[0]; } Object result = handler.invoke(method.getName(), arguments); if (result != null) return result; if (method.getReturnType().isPrimitive()) return 0; return null; }); } @FunctionalInterface private interface MapperHandler { Object invoke(String method, Object[] arguments); } private static final class RecordingTransactionManager implements PlatformTransactionManager { private final AtomicInteger commits = new AtomicInteger(); @Override public TransactionStatus getTransaction(TransactionDefinition definition) { return new SimpleTransactionStatus(); } @Override public void commit(TransactionStatus status) { commits.incrementAndGet(); } @Override public void rollback(TransactionStatus status) { } } }