From 0b436ed76e5dca781a50f58b52027101bb0477ad Mon Sep 17 00:00:00 2001 From: Piotr Gawron <piotr.gawron@uni.lu> Date: Mon, 27 Feb 2017 16:24:01 +0100 Subject: [PATCH] lih data parser added --- .../parse/LihControlMappingParser.java | 136 +++++++++++++ .../appointment/parse/LihControlParser.java | 179 ++++++++++++++++++ .../java/smash/appointment/parse/Main.java | 24 +++ .../appointment/parse/PrcControlParser.java | 10 +- .../appointment/parse/PrcFlyingParser.java | 9 +- .../appointment/parse/PrcSubjectsParser.java | 15 +- .../appointment/parse/SubjectParser.java | 17 ++ .../smash/appointment/parse/AllTests.java | 3 + .../parse/LihControlMappingParserTest.java | 58 ++++++ .../parse/LihControlParserTest.java | 64 +++++++ .../testFiles/lihControlExample.xlsx | Bin 0 -> 9814 bytes 11 files changed, 512 insertions(+), 3 deletions(-) create mode 100644 appointment-import/src/main/java/smash/appointment/parse/LihControlMappingParser.java create mode 100644 appointment-import/src/main/java/smash/appointment/parse/LihControlParser.java create mode 100644 appointment-import/src/test/java/smash/appointment/parse/LihControlMappingParserTest.java create mode 100644 appointment-import/src/test/java/smash/appointment/parse/LihControlParserTest.java create mode 100644 appointment-import/testFiles/lihControlExample.xlsx diff --git a/appointment-import/src/main/java/smash/appointment/parse/LihControlMappingParser.java b/appointment-import/src/main/java/smash/appointment/parse/LihControlMappingParser.java new file mode 100644 index 00000000..8f5b0e2d --- /dev/null +++ b/appointment-import/src/main/java/smash/appointment/parse/LihControlMappingParser.java @@ -0,0 +1,136 @@ +package smash.appointment.parse; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.ss.usermodel.Row; + +public class LihControlMappingParser extends SubjectParser { + + @Override + protected String parseScreeningNumber(Row row) { + String number = getString(row.getCell(0)); + if (number.trim().isEmpty()) { + return ""; + } else { + if (number.length() == 1) { + number = "00" + number; + } else if (number.length() == 2) { + number = "0" + number; + } + return "L-" + number; + } + } + + @Override + protected String parseName(Row row) { + return getString(row.getCell(8)); + } + + @Override + protected String parseSurname(Row row) { + return getString(row.getCell(9)); + } + + @Override + protected String parseNdNumber(Row row) { + return getString(row.getCell(7)); + } + + @Override + protected String getSheetName() { + return "Sheet1"; + } + + @Override + protected int getInitRow() { + return 1; + } + + @Override + protected String parseBirthDate(Row row) { + return ""; + } + + @Override + protected String parsemPowerId(Row row) { + return ""; + } + + @Override + protected String parseAddDate(Row row) { + return getDate(row.getCell(1)); + } + + @Override + protected String parseReferal(Row row) { + return ""; + } + + @Override + protected String parseDiagnosisYear(Row row) { + return ""; + } + + @Override + protected String parseMail(Row row) { + return ""; + } + + @Override + protected String parsePhone3(Row row) { + return ""; + } + + @Override + protected String parsePhone2(Row row) { + return ""; + } + + @Override + protected String parsePhone1(Row row) { + return getString(row.getCell(10)); + } + + @Override + protected String parseCity(Row row) { + return ""; + + } + + @Override + protected String parseCountry(Row row) { + return ""; + } + + @Override + protected String parseZipCode(Row row) { + return ""; + } + + @Override + protected String parseAddress(Row row) { + return ""; + } + + @Override + protected String parseRemarks(Row row) { + return ""; + } + + @Override + protected String parseDiagnosis(Row row) { + return ""; + } + + @Override + protected SubjectType parseType(Row row) { + return SubjectType.CONTROL; + } + + @Override + protected List<String> parseLanguages(Row row) { + return new ArrayList<>(); + } + +} diff --git a/appointment-import/src/main/java/smash/appointment/parse/LihControlParser.java b/appointment-import/src/main/java/smash/appointment/parse/LihControlParser.java new file mode 100644 index 00000000..82955aa2 --- /dev/null +++ b/appointment-import/src/main/java/smash/appointment/parse/LihControlParser.java @@ -0,0 +1,179 @@ +package smash.appointment.parse; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.ss.usermodel.Row; + +public class LihControlParser extends SubjectParser { + + @Override + protected String parseScreeningNumber(Row row) { + String number = getString(row.getCell(0)); + if (number.trim().isEmpty()) { + return ""; + } else { + return "L-" + number; + } + } + + @Override + protected String parseName(Row row) { + return getString(row.getCell(2)); + } + + @Override + protected String parseSurname(Row row) { + return getString(row.getCell(1)); + } + + @Override + protected String parseNdNumber(Row row) { + return ""; + } + + @Override + protected String getSheetName() { + return "Screening log"; + } + + @Override + protected int getInitRow() { + return 1; + } + + @Override + protected String parseBirthDate(Row row) { + return getDate(row.getCell(5)); + } + + @Override + protected String parsemPowerId(Row row) { + return ""; + } + + @Override + protected String parseAddDate(Row row) { + return ""; + } + + @Override + protected String parseReferal(Row row) { + return ""; + } + + @Override + protected String parseDiagnosisYear(Row row) { + return ""; + } + + @Override + protected String parseMail(Row row) { + return getString(row.getCell(8)); + } + + @Override + protected String parsePhone3(Row row) { + return ""; + } + + @Override + protected String parsePhone2(Row row) { + return getString(row.getCell(7)); + } + + @Override + protected String parsePhone1(Row row) { + return getString(row.getCell(6)); + } + + @Override + protected String parseCity(Row row) { + return getString(row.getCell(12)); + + } + + @Override + protected String parseCountry(Row row) { + return ""; + } + + @Override + protected String parseZipCode(Row row) { + return getString(row.getCell(11)); + } + + @Override + protected String parseAddress(Row row) { + return getString(row.getCell(9)) + ", " + getString(row.getCell(10)); + } + + @Override + protected String parseRemarks(Row row) { + List<String> remarks = new ArrayList<>(); + String info = getString(row.getCell(4)); + if (!info.trim().isEmpty()) { + remarks.add("PD family relation=" + info); + } + remarks.add(getString(row.getCell(13))); + remarks.add(getComments(row.getCell(14))); + remarks.add(getComments(row.getCell(15))); + remarks.add(getComments(row.getCell(16))); + remarks.add(getString(row.getCell(18))); + remarks.add(getString(row.getCell(19))); + String result = ""; + for (String string : remarks) { + if (!string.trim().isEmpty()) { + result += string.trim() + "\n"; + } + } + return result; + } + + @Override + protected String parseDiagnosis(Row row) { + return ""; + } + + @Override + protected SubjectType parseType(Row row) { + return SubjectType.CONTROL; + } + + @Override + protected List<String> parseLanguages(Row row) { + List<String> result = new ArrayList<>(); + + String languages = getString(row.getCell(3)); + String langAbbreviations[] = new String[] {}; + if (languages.indexOf(",") >= 0) { + langAbbreviations = languages.split(","); + } else { + langAbbreviations = languages.split("/"); + } + for (String string : langAbbreviations) { + if (!string.trim().isEmpty()) { + result.add(getMappedLanguage(string.trim())); + } + } + return result; + } + + private String getMappedLanguage(String abbreviation) { + switch (abbreviation.toUpperCase()) { + case ("F"): + return "French"; + case ("D"): + return "German"; + case ("GB"): + return "English"; + case ("P"): + return "Portuguese"; + case ("ENG"): + return "English"; + } + logger.warn("Unknown language abbreviation: " + abbreviation); + return ""; + } + +} diff --git a/appointment-import/src/main/java/smash/appointment/parse/Main.java b/appointment-import/src/main/java/smash/appointment/parse/Main.java index 96965759..5c6f94cb 100644 --- a/appointment-import/src/main/java/smash/appointment/parse/Main.java +++ b/appointment-import/src/main/java/smash/appointment/parse/Main.java @@ -27,10 +27,14 @@ public class Main { Option subjects = Option.builder().required().argName("file").hasArg().desc("PRC subjects").longOpt("subjects").build(); Option controls = Option.builder().required().argName("file").hasArg().desc("PRC controls").longOpt("controls").build(); Option flyingTeam = Option.builder().required().argName("file").hasArg().desc("PRC flying-team").longOpt("flying-team").build(); + Option lihControls = Option.builder().required().argName("file").hasArg().desc("LIH controls").longOpt("lih-controls").build(); + Option lihMappingControls = Option.builder().required().argName("file").hasArg().desc("LIH controls mapping").longOpt("lih-mapping").build(); options.addOption(agenda); options.addOption(subjects); options.addOption(controls); options.addOption(flyingTeam); + options.addOption(lihControls); + options.addOption(lihMappingControls); CommandLineParser parser = new DefaultParser(); try { @@ -51,6 +55,16 @@ public class Main { subjectDao.addSubject(subject, "[" + flyingTeamFile + ";" + subject.getScreeningNumber() + ";" + subject.getName() + " " + subject.getSurname() + "]"); } + String lihMappingControlsFile = line.getOptionValue("lih-mapping"); + for (Subject subject : processLihMappingControls(lihMappingControlsFile)) { + subjectDao.addSubject(subject, "[" + lihMappingControlsFile + ";" + subject.getScreeningNumber() + ";" + subject.getName() + " " + subject.getSurname() + "]"); + } + + String lihControlsFile = line.getOptionValue("lih-controls"); + for (Subject subject : processLihControls(lihControlsFile)) { + subjectDao.addSubject(subject, "[" + lihControlsFile + ";" + subject.getScreeningNumber() + ";" + subject.getName() + " " + subject.getSurname() + "]"); + } + logger.debug("SUBJECTS: "); for (Subject subject : subjectDao.getSubjects()) { logger.debug(subject); @@ -66,6 +80,16 @@ public class Main { } } + private List<Subject> processLihMappingControls(String lihMappingControlsFile) throws Exception{ + LihControlMappingParser parser = new LihControlMappingParser(); + return parser.processExcel(lihMappingControlsFile); + } + + private List<Subject> processLihControls(String lihControlsFile) throws Exception{ + LihControlParser parser = new LihControlParser(); + return parser.processExcel(lihControlsFile); + } + private List<Subject> processFlyingTeamControls(String flyingTeamFile) throws Exception { PrcFlyingParser parser = new PrcFlyingParser(); return parser.processExcel(flyingTeamFile); diff --git a/appointment-import/src/main/java/smash/appointment/parse/PrcControlParser.java b/appointment-import/src/main/java/smash/appointment/parse/PrcControlParser.java index 44496a88..dbf0dc8c 100644 --- a/appointment-import/src/main/java/smash/appointment/parse/PrcControlParser.java +++ b/appointment-import/src/main/java/smash/appointment/parse/PrcControlParser.java @@ -1,5 +1,8 @@ package smash.appointment.parse; +import java.util.ArrayList; +import java.util.List; + import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; @@ -8,7 +11,7 @@ public class PrcControlParser extends SubjectParser { @Override protected String parseScreeningNumber(Row row) { String number = getString(row.getCell(16)); - if (number.isEmpty()) { + if (number.trim().isEmpty()) { return ""; } else { return "P-" + number; @@ -153,4 +156,9 @@ public class PrcControlParser extends SubjectParser { return SubjectType.CONTROL; } + @Override + protected List<String> parseLanguages(Row row) { + return new ArrayList<>(); + } + } diff --git a/appointment-import/src/main/java/smash/appointment/parse/PrcFlyingParser.java b/appointment-import/src/main/java/smash/appointment/parse/PrcFlyingParser.java index 5e27552d..ff69003a 100644 --- a/appointment-import/src/main/java/smash/appointment/parse/PrcFlyingParser.java +++ b/appointment-import/src/main/java/smash/appointment/parse/PrcFlyingParser.java @@ -1,5 +1,8 @@ package smash.appointment.parse; +import java.util.ArrayList; +import java.util.List; + import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; @@ -8,7 +11,7 @@ public class PrcFlyingParser extends SubjectParser { @Override protected String parseScreeningNumber(Row row) { String number = getString(row.getCell(25)); - if (number.isEmpty()) { + if (number.trim().isEmpty()) { return ""; } else { return "P-" + number; @@ -142,5 +145,9 @@ public class PrcFlyingParser extends SubjectParser { protected SubjectType parseType(Row row) { return null; } + @Override + protected List<String> parseLanguages(Row row) { + return new ArrayList<>(); + } } diff --git a/appointment-import/src/main/java/smash/appointment/parse/PrcSubjectsParser.java b/appointment-import/src/main/java/smash/appointment/parse/PrcSubjectsParser.java index 7b76573f..c2aea102 100644 --- a/appointment-import/src/main/java/smash/appointment/parse/PrcSubjectsParser.java +++ b/appointment-import/src/main/java/smash/appointment/parse/PrcSubjectsParser.java @@ -1,5 +1,8 @@ package smash.appointment.parse; +import java.util.ArrayList; +import java.util.List; + import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; @@ -7,7 +10,12 @@ public class PrcSubjectsParser extends SubjectParser { @Override protected String parseScreeningNumber(Row row) { - return "P-" + getString(row.getCell(25)); + String number = getString(row.getCell(25)); + if (number.trim().isEmpty()) { + return ""; + } else { + return "P-" + number; + } } @Override @@ -125,4 +133,9 @@ public class PrcSubjectsParser extends SubjectParser { return SubjectType.SUBJECT; } + @Override + protected List<String> parseLanguages(Row row) { + return new ArrayList<>(); + } + } diff --git a/appointment-import/src/main/java/smash/appointment/parse/SubjectParser.java b/appointment-import/src/main/java/smash/appointment/parse/SubjectParser.java index 6b218352..3d5652f6 100644 --- a/appointment-import/src/main/java/smash/appointment/parse/SubjectParser.java +++ b/appointment-import/src/main/java/smash/appointment/parse/SubjectParser.java @@ -16,7 +16,9 @@ import org.apache.poi.hssf.usermodel.HSSFDateUtil; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Comment; import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.RichTextString; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; @@ -84,10 +86,13 @@ public abstract class SubjectParser { result.setmPowerId(parsemPowerId(row)); result.setBirthDate(parseBirthDate(row)); result.setType(parseType(row)); + result.setLanguages(parseLanguages(row)); return result; } + protected abstract List<String> parseLanguages(Row row); + protected abstract SubjectType parseType(Row row); protected abstract String parseBirthDate(Row row); @@ -142,6 +147,18 @@ public abstract class SubjectParser { return df.formatCellValue(cell).trim(); } + protected String getComments(Cell cell) { + Comment comment = cell.getCellComment(); + if (comment != null) { + RichTextString richTextString = comment.getString(); + if (richTextString != null) { + return richTextString.getString(); + + } + } + return ""; + } + protected String getDate(Cell cell) { if (cell == null) { return ""; diff --git a/appointment-import/src/test/java/smash/appointment/parse/AllTests.java b/appointment-import/src/test/java/smash/appointment/parse/AllTests.java index 5aaea5c9..71c7e659 100644 --- a/appointment-import/src/test/java/smash/appointment/parse/AllTests.java +++ b/appointment-import/src/test/java/smash/appointment/parse/AllTests.java @@ -6,6 +6,8 @@ import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) @SuiteClasses({ CellParserTest.class, // + LihControlMappingParserTest.class, // + LihControlParserTest.class, // PrcFlyingParserTest.class, // PrcSubjectsParserTest.class, // RedcapParserTest.class, // @@ -14,6 +16,7 @@ import org.junit.runners.Suite.SuiteClasses; SubjectParserTest.class, // XlsxCalendarProcessorTest.class, // }) + public class AllTests { } diff --git a/appointment-import/src/test/java/smash/appointment/parse/LihControlMappingParserTest.java b/appointment-import/src/test/java/smash/appointment/parse/LihControlMappingParserTest.java new file mode 100644 index 00000000..33d36da9 --- /dev/null +++ b/appointment-import/src/test/java/smash/appointment/parse/LihControlMappingParserTest.java @@ -0,0 +1,58 @@ +package smash.appointment.parse; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.apache.log4j.Logger; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +public class LihControlMappingParserTest extends TestBase { + Logger logger = Logger.getLogger(LihControlMappingParserTest.class); + + LihControlMappingParser processor = new LihControlMappingParser(); + + + @AfterClass + public static void tearDownAfterClass() throws Exception { + } + + @Before + public void setUp() { + super.setUp(); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void test() throws Exception { + List<Subject> entries = processor.processExcel("testFiles/lihControlMappingExample.xlsx"); + assertTrue(entries.size() > 0); + + Subject subject = entries.get(0); + assertEquals("L-001", subject.getScreeningNumber()); + assertEquals("Piotrek", subject.getName()); + assertEquals("Gawron", subject.getSurname()); + assertEquals("", subject.getAddress()); + assertEquals("", subject.getZipCode()); + assertEquals("", subject.getCity()); + assertEquals("", subject.getCountry()); + assertEquals("343", subject.getPhone1()); + assertEquals("", subject.getPhone2()); + assertEquals("", subject.getPhone3()); + assertEquals("", subject.getMail()); + assertEquals("", subject.getDiagnosisYear()); + assertEquals("", subject.getDiagnosis()); + assertEquals("", subject.getReferal()); + assertEquals("2015-08-03", subject.getAddDate()); + assertEquals("ND3333", subject.getNdNumber()); + assertEquals("", subject.getBirthDate()); + } + + +} diff --git a/appointment-import/src/test/java/smash/appointment/parse/LihControlParserTest.java b/appointment-import/src/test/java/smash/appointment/parse/LihControlParserTest.java new file mode 100644 index 00000000..b610128e --- /dev/null +++ b/appointment-import/src/test/java/smash/appointment/parse/LihControlParserTest.java @@ -0,0 +1,64 @@ +package smash.appointment.parse; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.apache.log4j.Logger; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +public class LihControlParserTest extends TestBase { + Logger logger = Logger.getLogger(LihControlParserTest.class); + + LihControlParser processor = new LihControlParser(); + + + @AfterClass + public static void tearDownAfterClass() throws Exception { + } + + @Before + public void setUp() { + super.setUp(); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void test() throws Exception { + List<Subject> entries = processor.processExcel("testFiles/lihControlExample.xlsx"); + assertTrue(entries.size() > 0); + + Subject subject = entries.get(0); + assertEquals("L-001", subject.getScreeningNumber()); + assertEquals("Name", subject.getName()); + assertEquals("Surname", subject.getSurname()); + assertTrue(subject.getRemarks().contains("001 rdv 01/09/2015 9h jyf")); + assertTrue(subject.getRemarks().contains("PD family relation=pd info")); + assertEquals("11, Rue blabla", subject.getAddress()); + assertEquals("L-3322", subject.getZipCode()); + assertEquals("Luxembourg", subject.getCity()); + assertEquals("", subject.getCountry()); + assertEquals("123456789", subject.getPhone1()); + assertEquals("321654", subject.getPhone2()); + assertEquals("", subject.getPhone3()); + assertEquals("email@pt.lu", subject.getMail()); + assertEquals("", subject.getDiagnosisYear()); + assertEquals("", subject.getDiagnosis()); + assertEquals("", subject.getReferal()); + assertEquals("", subject.getAddDate()); + assertEquals("", subject.getNdNumber()); + assertEquals("1937-01-03", subject.getBirthDate()); + assertTrue(subject.getRemarks().contains("some other remark")); + assertTrue(subject.getRemarks().contains("at home: NMS + RFQ 1 + RFQ 2 + REM + PDSS: manque une page ds RFQ => Linda pr level b 09/09/15")); + assertTrue(subject.getLanguages().contains("French")); + assertTrue(subject.getLanguages().contains("German")); + } + + +} diff --git a/appointment-import/testFiles/lihControlExample.xlsx b/appointment-import/testFiles/lihControlExample.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..526e749bdf8cb818d13e98b763afb22b3ec3b071 GIT binary patch literal 9814 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 literal 0 HcmV?d00001 -- GitLab