From 25817cf5aa38a3417a8ba26d27c866c532eb93ed Mon Sep 17 00:00:00 2001 From: Piotr Gawron <piotr.gawron@uni.lu> Date: Fri, 24 Feb 2017 17:53:52 +0100 Subject: [PATCH] java parser for prc subjects --- .../appointment/parse/PrcSubjectsParser.java | 127 ++++++++++ .../java/smash/appointment/parse/Subject.java | 238 ++++++++++++++++++ .../appointment/parse/SubjectParser.java | 147 +++++++++++ .../smash/appointment/parse/AllTests.java | 1 + .../parse/PrcSubjectsParserTest.java | 60 +++++ .../testFiles/prcSubjectsExample.xlsx | Bin 0 -> 9293 bytes 6 files changed, 573 insertions(+) create mode 100644 appointment-import/src/main/java/smash/appointment/parse/PrcSubjectsParser.java create mode 100644 appointment-import/src/main/java/smash/appointment/parse/SubjectParser.java create mode 100644 appointment-import/src/test/java/smash/appointment/parse/PrcSubjectsParserTest.java create mode 100644 appointment-import/testFiles/prcSubjectsExample.xlsx diff --git a/appointment-import/src/main/java/smash/appointment/parse/PrcSubjectsParser.java b/appointment-import/src/main/java/smash/appointment/parse/PrcSubjectsParser.java new file mode 100644 index 00000000..155399ce --- /dev/null +++ b/appointment-import/src/main/java/smash/appointment/parse/PrcSubjectsParser.java @@ -0,0 +1,127 @@ +package smash.appointment.parse; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; + +public class PrcSubjectsParser extends SubjectParser { + + @Override + protected String parseScreeningNumber(Row row) { + return "P-" + getString(row.getCell(25)); + } + + @Override + protected String parseName(Row row) { + return getString(row.getCell(1)); + } + + @Override + protected String parseSurname(Row row) { + return getString(row.getCell(0)); + } + + @Override + protected String parseNdNumber(Row row) { + return getString(row.getCell(22)); + } + + @Override + protected String getSheetName() { + return "Feuil1"; + } + + @Override + protected int getInitRow() { + return 1; + } + + @Override + protected String parseBirthDate(Row row) { + String date = getString(row.getCell(27)).replaceAll(" ", ""); + String year = date.substring(0, 4); + String month = date.substring(4, 6); + String day = date.substring(6, 8); + return year + "-" + month + "-" + day; + } + + @Override + protected String parsemPowerId(Row row) { + return getString(row.getCell(23)); + } + + @Override + protected String parseAddDate(Row row) { + return getDate(row.getCell(14)); + } + + @Override + protected String parseReferal(Row row) { + return getString(row.getCell(13)); + } + + @Override + protected String parseDiagnosisYear(Row row) { + return getString(row.getCell(11)); + } + + @Override + protected String parseMail(Row row) { + return getString(row.getCell(10)); + } + + @Override + protected String parsePhone3(Row row) { + return getString(row.getCell(9)); + } + + @Override + protected String parsePhone2(Row row) { + return getString(row.getCell(8)); + } + + @Override + protected String parsePhone1(Row row) { + return getString(row.getCell(7)); + } + + @Override + protected String parseCity(Row row) { + return getString(row.getCell(5)); + } + + @Override + protected String parseCountry(Row row) { + return getString(row.getCell(6)); + } + + @Override + protected String parseZipCode(Row row) { + return getString(row.getCell(4)); + } + + @Override + protected String parseAddress(Row row) { + return getString(row.getCell(3)); + } + + @Override + protected String parseRemarks(Row row) { + String remark1 = getString(row.getCell(2)); + String remark2 = getString(row.getCell(20)); + + String result = ""; + if (!remark1.trim().isEmpty()) { + result = result + remark1 + "\n"; + } + if (!remark2.trim().isEmpty()) { + result = result + remark2 + "\n"; + } + return result; + } + + @Override + protected String parseDiagnosis(Row row) { + return getString(row.getCell(12)); + } + +} diff --git a/appointment-import/src/main/java/smash/appointment/parse/Subject.java b/appointment-import/src/main/java/smash/appointment/parse/Subject.java index 92c2ab9f..70d87334 100644 --- a/appointment-import/src/main/java/smash/appointment/parse/Subject.java +++ b/appointment-import/src/main/java/smash/appointment/parse/Subject.java @@ -13,7 +13,21 @@ public class Subject { private String ndNumber; private String screeningNumber; private String sex; + private String remarks; private String birthDate; + private String address; + private String zipCode; + private String country; + private String city; + private String phone1; + private String phone2; + private String phone3; + private String mail; + private String diagnosisYear; + private String diagnosis; + private String referal; + private String addDate; + private String mPowerId; private List<String> languages = new ArrayList<>(); @@ -153,4 +167,228 @@ public class Subject { this.languages = languages; } + /** + * @return the remarks + * @see #remarks + */ + public String getRemarks() { + return remarks; + } + + /** + * @param remarks the remarks to set + * @see #remarks + */ + public void setRemarks(String remarks) { + this.remarks = remarks; + } + + /** + * @return the address + * @see #address + */ + public String getAddress() { + return address; + } + + /** + * @param address the address to set + * @see #address + */ + public void setAddress(String address) { + this.address = address; + } + + /** + * @return the zipCode + * @see #zipCode + */ + public String getZipCode() { + return zipCode; + } + + /** + * @param zipCode the zipCode to set + * @see #zipCode + */ + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + /** + * @return the country + * @see #country + */ + public String getCountry() { + return country; + } + + /** + * @param country the country to set + * @see #country + */ + public void setCountry(String country) { + this.country = country; + } + + /** + * @return the city + * @see #city + */ + public String getCity() { + return city; + } + + /** + * @param city the city to set + * @see #city + */ + public void setCity(String city) { + this.city = city; + } + + /** + * @return the phone1 + * @see #phone1 + */ + public String getPhone1() { + return phone1; + } + + /** + * @param phone1 the phone1 to set + * @see #phone1 + */ + public void setPhone1(String phone1) { + this.phone1 = phone1; + } + + /** + * @return the phone2 + * @see #phone2 + */ + public String getPhone2() { + return phone2; + } + + /** + * @param phone2 the phone2 to set + * @see #phone2 + */ + public void setPhone2(String phone2) { + this.phone2 = phone2; + } + + /** + * @return the phone3 + * @see #phone3 + */ + public String getPhone3() { + return phone3; + } + + /** + * @param phone3 the phone3 to set + * @see #phone3 + */ + public void setPhone3(String phone3) { + this.phone3 = phone3; + } + + /** + * @return the mail + * @see #mail + */ + public String getMail() { + return mail; + } + + /** + * @param mail the mail to set + * @see #mail + */ + public void setMail(String mail) { + this.mail = mail; + } + + /** + * @return the diagnosisYear + * @see #diagnosisYear + */ + public String getDiagnosisYear() { + return diagnosisYear; + } + + /** + * @param diagnosisYear the diagnosisYear to set + * @see #diagnosisYear + */ + public void setDiagnosisYear(String diagnosisYear) { + this.diagnosisYear = diagnosisYear; + } + + /** + * @return the diagnosis + * @see #diagnosis + */ + public String getDiagnosis() { + return diagnosis; + } + + /** + * @param diagnosis the diagnosis to set + * @see #diagnosis + */ + public void setDiagnosis(String diagnosis) { + this.diagnosis = diagnosis; + } + + /** + * @return the referal + * @see #referal + */ + public String getReferal() { + return referal; + } + + /** + * @param referal the referal to set + * @see #referal + */ + public void setReferal(String referal) { + this.referal = referal; + } + + /** + * @return the mPowerId + * @see #mPowerId + */ + public String getmPowerId() { + return mPowerId; + } + + /** + * @param mPowerId the mPowerId to set + * @see #mPowerId + */ + public void setmPowerId(String mPowerId) { + this.mPowerId = mPowerId; + } + + /** + * @return the addDate + * @see #addDate + */ + public String getAddDate() { + return addDate; + } + + /** + * @param addDate the addDate to set + * @see #addDate + */ + public void setAddDate(String addDate) { + this.addDate = addDate; + } + } diff --git a/appointment-import/src/main/java/smash/appointment/parse/SubjectParser.java b/appointment-import/src/main/java/smash/appointment/parse/SubjectParser.java new file mode 100644 index 00000000..498dc960 --- /dev/null +++ b/appointment-import/src/main/java/smash/appointment/parse/SubjectParser.java @@ -0,0 +1,147 @@ +package smash.appointment.parse; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.log4j.Logger; +import org.apache.poi.EncryptedDocumentException; +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.DataFormatter; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; + +public abstract class SubjectParser { + private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd"); + Logger logger = Logger.getLogger(SubjectParser.class); + DataFormatter df = new DataFormatter(); + + public List<Subject> processExcel(String filename) throws EncryptedDocumentException, InvalidFormatException, IOException, ParseException { + List<Subject> result = new ArrayList<>(); + InputStream inp = new FileInputStream(filename); + Workbook workbook = WorkbookFactory.create(inp); + Iterator<Sheet> sheetIter = workbook.sheetIterator(); + while (sheetIter.hasNext()) { + Sheet sheet = sheetIter.next(); + String name = sheet.getSheetName().trim(); + if (name.trim().toLowerCase().equals(getSheetName().toLowerCase())) { + result.addAll(processSheet(sheet)); + } else { + logger.debug(filename + "Skipping sheet: " + name); + } + } + return result; + } + + private List<Subject> processSheet(Sheet sheet) { + List<Subject> result = new ArrayList<>(); + int rowCount = sheet.getPhysicalNumberOfRows(); + for (int rowId = getInitRow(); rowId < rowCount; rowId++) { + Row subjectRow = sheet.getRow(rowId); + Subject subject = parseSubject(subjectRow); + if (subject != null) { + result.add(subject); + } + } + return result; + } + + private Subject parseSubject(Row row) { + String screeningNumber = parseScreeningNumber(row); + if (screeningNumber == null || screeningNumber.isEmpty()) { + return null; + } + String name = parseName(row); + String surname = parseSurname(row); + String ndNumber = parseNdNumber(row); + Subject result = new Subject(name, surname, ndNumber, screeningNumber); + + result.setRemarks(parseRemarks(row)); + result.setAddress(parseAddress(row)); + result.setZipCode(parseZipCode(row)); + result.setCity(parseCity(row)); + result.setCountry(parseCountry(row)); + result.setPhone1(parsePhone1(row)); + result.setPhone2(parsePhone2(row)); + result.setPhone3(parsePhone3(row)); + result.setMail(parseMail(row)); + result.setDiagnosisYear(parseDiagnosisYear(row)); + result.setDiagnosis(parseDiagnosis(row)); + result.setReferal(parseReferal(row)); + result.setAddDate(parseAddDate(row)); + result.setmPowerId(parsemPowerId(row)); + result.setBirthDate(parseBirthDate(row)); + + return result; + } + + protected abstract String parseBirthDate(Row row); + + protected abstract String parsemPowerId(Row row); + + protected abstract String parseAddDate(Row row); + + protected abstract String parseReferal(Row row); + + protected abstract String parseDiagnosisYear(Row row); + + protected abstract String parseDiagnosis(Row row); + + protected abstract String parseMail(Row row); + + protected abstract String parsePhone3(Row row); + + protected abstract String parsePhone2(Row row); + + protected abstract String parsePhone1(Row row); + + protected abstract String parseCity(Row row); + + protected abstract String parseCountry(Row row); + + protected abstract String parseZipCode(Row row); + + protected abstract String parseAddress(Row row); + + protected abstract String parseRemarks(Row row); + + protected abstract String parseScreeningNumber(Row row); + + protected abstract String parseName(Row row); + + protected abstract String parseSurname(Row row); + + protected abstract String parseNdNumber(Row row); + + protected abstract String getSheetName(); + + protected abstract int getInitRow(); + + protected String getString(Cell cell) { + if (cell.getCellTypeEnum().equals(CellType.NUMERIC)) { + return df.formatCellValue(cell).trim(); + } + return cell.getStringCellValue().trim(); + } + + protected String getDate(Cell cell) { + String result = null; + if (HSSFDateUtil.isCellDateFormatted(cell)) { + result = DATE_FORMATTER.format(cell.getDateCellValue()); + } else { + result = getString(cell); + } + return result; + } + +} 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 be53066b..a22dce9b 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,7 @@ import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) @SuiteClasses({ CellParserTest.class, // + PrcSubjectsParserTest.class, // RedcapParserTest.class, // SubjectDaoTest.class, // diff --git a/appointment-import/src/test/java/smash/appointment/parse/PrcSubjectsParserTest.java b/appointment-import/src/test/java/smash/appointment/parse/PrcSubjectsParserTest.java new file mode 100644 index 00000000..255d939b --- /dev/null +++ b/appointment-import/src/test/java/smash/appointment/parse/PrcSubjectsParserTest.java @@ -0,0 +1,60 @@ +package smash.appointment.parse; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +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 PrcSubjectsParserTest extends TestBase { + Logger logger = Logger.getLogger(PrcSubjectsParserTest.class); + + PrcSubjectsParser processor = new PrcSubjectsParser(); + + @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/prcSubjectsExample.xlsx"); + assertTrue(entries.size() > 0); + + Subject subject = entries.get(0); + assertEquals("P-111", subject.getScreeningNumber()); + assertEquals("abcdef", subject.getSurname()); + assertEquals("Piotr", subject.getName()); + assertTrue(subject.getRemarks().contains("this is remark")); + assertEquals("Jaskolki, 6", subject.getAddress()); + assertEquals("D-66636", subject.getZipCode()); + assertEquals("Trier", subject.getCity()); + assertEquals("Deutschland", subject.getCountry()); + assertEquals("0049 12 34 556 76", subject.getPhone1()); + assertEquals("tel2", subject.getPhone2()); + assertEquals("tel3", subject.getPhone3()); + assertEquals("piotr.cos@uni.lu", subject.getMail()); + assertEquals("1999", subject.getDiagnosisYear()); + assertEquals("unknown", subject.getDiagnosis()); + assertEquals("PG", subject.getReferal()); + assertEquals("2015-10-14", subject.getAddDate()); + assertTrue(subject.getRemarks().contains("Questionnaires OK")); + assertEquals("ND1111", subject.getNdNumber()); + assertEquals("m_id", subject.getmPowerId()); + assertEquals("1972-01-02", subject.getBirthDate()); + } + +} diff --git a/appointment-import/testFiles/prcSubjectsExample.xlsx b/appointment-import/testFiles/prcSubjectsExample.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..78e43d92529273733c3e26a0b37fa2d6983b5485 GIT binary patch literal 9293 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 ZbdJjM$Z)#+*o}Yz7>ASGjQYpZ{{U)%D82vy literal 0 HcmV?d00001 -- GitLab